├── pyam ├── tests │ ├── __init__.py │ ├── test_problem.py │ ├── test_model.py │ └── test_inputs.py ├── __init__.py ├── visualizer.py ├── initial_guesses.py ├── model.py ├── problem.py └── inputs.py ├── MANIFEST.in ├── requirements.txt ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── setup.py └── README.rst /pyam/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE CITATION 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | pycollocation >= 0.4.0-alpha 4 | scipy 5 | sympy -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: RUWf9s8FXVDIjEYSerjfFLTruqfsxxsTI 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all .pyc files 2 | *.pyc 3 | 4 | # ignore ipython checkpoints 5 | .ipynb_checkpoints/ 6 | 7 | # ignore coverage statistics 8 | .coverage 9 | 10 | # ignore build info 11 | dist/ 12 | 13 | # ignore *.egg-info 14 | *.egg-info/ 15 | 16 | # ignore version file 17 | pycollocation/version.py 18 | 19 | #ignore MANIFEST 20 | MANIFEST 21 | 22 | # ignore egg-info 23 | pycollocation.egg-info 24 | 25 | # ignore docs/build 26 | docs/build 27 | 28 | # ignore MANIFEST 29 | MANIFEST 30 | -------------------------------------------------------------------------------- /pyam/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Objects imported here will live in the `pyam` namespace 3 | 4 | """ 5 | __all__ = ["OrthogonalPolynomialInitialGuess", "Input", 6 | "AssortativeMatchingModelLike", "AssortativeMatchingProblem", 7 | "Visualizer"] 8 | 9 | from . initial_guesses import OrthogonalPolynomialInitialGuess 10 | from . inputs import Input 11 | from . model import AssortativeMatchingModelLike 12 | from . problem import AssortativeMatchingProblem 13 | from . visualizer import Visualizer 14 | 15 | # Add Version Attribute 16 | from pkg_resources import get_distribution 17 | 18 | __version__ = get_distribution('pyAM').version 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | - 3.3 6 | 7 | notifications: 8 | email: false 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | before_install: 15 | - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh 16 | - chmod +x miniconda.sh 17 | - ./miniconda.sh -b 18 | - export PATH=/home/travis/miniconda/bin:$PATH 19 | - conda update --yes conda 20 | - sudo rm -rf /dev/shm 21 | - sudo ln -s /run/shm /dev/shm 22 | 23 | install: 24 | - conda install --yes python=$TRAVIS_PYTHON_VERSION numpy scipy nose pandas pip sympy 25 | - pip install coveralls coverage pycollocation 26 | - python setup.py install 27 | 28 | script: 29 | - nosetests --with-coverage --cover-package=pyam 30 | 31 | after_success: 32 | - coveralls 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David R. Pugh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup 4 | 5 | 6 | def read(*paths): 7 | """Build a file path from *paths* and return the contents.""" 8 | with open(os.path.join(*paths), 'r') as f: 9 | return f.read() 10 | 11 | # Meta information 12 | DESCRIPTION = ("Python package for solving assortative matching models with " + 13 | "two-sided heterogeneity.") 14 | 15 | CLASSIFIERS = ['Development Status :: 3 - Alpha', 16 | 'Intended Audience :: Education', 17 | 'Intended Audience :: Science/Research', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 3', 23 | 'Programming Language :: Python :: 2.7', 24 | 'Programming Language :: Python :: 3.3', 25 | 'Programming Language :: Python :: 3.4', 26 | 'Topic :: Scientific/Engineering', 27 | ] 28 | 29 | setup( 30 | name="pyam", 31 | packages=['pyam'], 32 | version='0.2.2-alpha', 33 | description=DESCRIPTION, 34 | long_description=read('README.rst'), 35 | license="MIT License", 36 | author="davidrpugh", 37 | author_email="david.pugh@maths.ox.ac.uk", 38 | url='https://github.com/davidrpugh/pyAM', 39 | classifiers=CLASSIFIERS, 40 | ) 41 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyAM 2 | ==== 3 | 4 | |Build Status| |Coverage Status| |Codacy Badge| |GitHub License| |Latest Version| |Downloads| |DOI| 5 | 6 | .. |Build Status| image:: https://travis-ci.org/davidrpugh/pyAM.svg?branch=master 7 | :target: https://travis-ci.org/davidrpugh/pyAM 8 | .. |Coverage Status| image:: https://coveralls.io/repos/davidrpugh/pyAM/badge.svg?branch=master 9 | :target: https://coveralls.io/github/davidrpugh/pyAM?branch=master 10 | .. |Codacy Badge| image:: https://www.codacy.com/project/badge/f051d7b5ccce47cfa3d6907c9a1bd6bf 11 | :target: https://www.codacy.com/app/drobert-pugh/pyAM 12 | .. |GitHub license| image:: https://img.shields.io/github/license/davidrpugh/pyAM.svg 13 | :target: https://img.shields.io/github/license/davidrpugh/pyAM.svg 14 | .. |Latest Version| image:: https://img.shields.io/pypi/v/pyAM.svg 15 | :target: https://pypi.python.org/pypi/pyAM/ 16 | .. |Downloads| image:: https://img.shields.io/pypi/dm/pyAM.svg 17 | :target: https://pypi.python.org/pypi/pyAM/ 18 | .. |DOI| image:: https://zenodo.org/badge/doi/10.5281/zenodo.22396.svg 19 | :target: http://dx.doi.org/10.5281/zenodo.22396 20 | 21 | Python package for solving assortative matching models with two-sided heterogeneity. The theoretical framework behind the class of models solved by pyAM is described in `Eeckhout and Kircher (2012)`_. 22 | 23 | .. _`Eeckhout and Kircher (2012)`: http://homepages.econ.ed.ac.uk/~pkircher/Papers/Sorting-and-Factor-Intensity.pdf 24 | 25 | Installation 26 | ------------ 27 | 28 | Assuming you have `pip`_ on your computer (as will be the case if you've `installed Anaconda`_) you can install the latest stable release of ``pyam`` by typing 29 | 30 | .. code:: bash 31 | 32 | $ pip install pyam 33 | 34 | at a terminal prompt. 35 | 36 | .. _pip: https://pypi.python.org/pypi/pip 37 | .. _`installed Anaconda`: http://quant-econ.net/getting_started.html#installing-anaconda 38 | 39 | Contributing 40 | ------------ 41 | If you wish to contribute to the project you will likely want to install from source. First your will need to fork and then clone the source repository. 42 | 43 | .. code:: bash 44 | 45 | $ git clone https://github.com/YOUR-USERNAME/pyAM.git 46 | 47 | Next create a new `conda` development environment 48 | 49 | .. code:: bash 50 | 51 | $ conda create -n pyam-dev python anaconda 52 | 53 | activate the newly created development environment 54 | 55 | .. code:: bash 56 | 57 | $ source activate pyam-dev 58 | 59 | and install additional dependencies not available within Anaconda. 60 | 61 | .. code:: bash 62 | 63 | $ pip install pycollocation 64 | $ pip install seaborn 65 | 66 | Finally, change into your local clone of the `pyam` source directory and install the package in development mode. 67 | 68 | .. code:: bash 69 | 70 | $ pip install -e . 71 | 72 | 73 | Example notebooks 74 | ----------------- 75 | At the moment there are two example notebooks, one for `positive assortative matching`_ and one for `negative assortative matching`_ in the `examples` directory. The positive assortative matching works fine; the negative assortative matching, however, does not yet work (I suspect because of a poor algorithm for the initial guess). 76 | 77 | .. _`positive assortative matching`: https://github.com/davidrpugh/pyAM/blob/master/examples/positive-assortative-matching.ipynb 78 | .. _`negative assortative matching`: https://github.com/davidrpugh/pyAM/blob/master/examples/negative-assortative-matching.ipynb 79 | -------------------------------------------------------------------------------- /pyam/tests/test_problem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing suite for the problem.py module. 3 | 4 | @author : David R. Pugh 5 | @date : 2015-08-02 6 | 7 | """ 8 | import unittest 9 | 10 | import numpy as np 11 | import sympy as sym 12 | 13 | import pycollocation 14 | 15 | from .. import inputs 16 | from .. import initial_guesses 17 | from .. import problem 18 | from .. import visualizer 19 | 20 | 21 | class MultiplicativeSeparabilityCase(unittest.TestCase): 22 | 23 | def setUp(self): 24 | """Set up code for test fixtures.""" 25 | # define some workers skill 26 | x, a, b = sym.var('x, a, b') 27 | skill_cdf = (x - a) / (b - a) 28 | skill_params = {'a': 1.0, 'b': 2.0} 29 | skill_bounds = [skill_params['a'], skill_params['b']] 30 | 31 | workers = inputs.Input(var=x, 32 | cdf=skill_cdf, 33 | params=skill_params, 34 | bounds=skill_bounds, 35 | ) 36 | 37 | # define some firms 38 | y = sym.var('y') 39 | productivity_cdf = (y - a) / (b - a) 40 | productivity_params = skill_params 41 | productivity_bounds = skill_bounds 42 | 43 | firms = inputs.Input(var=y, 44 | cdf=productivity_cdf, 45 | params=productivity_params, 46 | bounds=productivity_bounds, 47 | ) 48 | 49 | # define symbolic expression for CES between x and y 50 | x, y, omega_A, sigma_A = sym.var('x, y, omega_A, sigma_A') 51 | A = ((omega_A * x**((sigma_A - 1) / sigma_A) + 52 | (1 - omega_A) * y**((sigma_A - 1) / sigma_A))**(sigma_A / (sigma_A - 1))) 53 | 54 | # define symbolic expression for Cobb-Douglas between l and r 55 | l, r, omega_B, sigma_B = sym.var('l, r, omega_A, sigma_A') 56 | B = l**omega_B * r**(1 - omega_B) 57 | 58 | # generate random parameters 59 | omega, sigma = np.random.random(2) 60 | F_params = {'omega_A': omega, 'omega_B': omega, 'sigma_A': sigma, 'sigma_B': 1.0} 61 | F = A * B 62 | 63 | self.problem = problem.AssortativeMatchingProblem(assortativity='positive', 64 | input1=workers, 65 | input2=firms, 66 | F=F, 67 | F_params=F_params, 68 | ) 69 | 70 | self.solver = pycollocation.OrthogonalPolynomialSolver(self.problem) 71 | 72 | def test_solve(self): 73 | """Test trivial example for solver.""" 74 | # set up initial guess (which is actually the solution!) 75 | initial_guess = initial_guesses.OrthogonalPolynomialInitialGuess(self.solver) 76 | initial_polys = initial_guess.compute_initial_guess("Chebyshev", 77 | degrees={'mu': 1, 'theta': 1}, 78 | f=lambda x, alpha: x**alpha, 79 | alpha=1.0) 80 | initial_coefs = {'mu': initial_polys['mu'].coef, 81 | 'theta': initial_polys['theta'].coef} 82 | 83 | # solve the model over the desired domain 84 | domain = [self.problem.input1.lower, self.problem.input1.upper] 85 | self.solver.solve(kind="Chebyshev", 86 | coefs_dict=initial_coefs, 87 | domain=domain, 88 | method='hybr') 89 | 90 | # visualize the solution at some interpolation points 91 | viz = visualizer.Visualizer(self.solver) 92 | interp_knots = np.linspace(domain[0], domain[1], 1000) 93 | viz.interpolation_knots = interp_knots 94 | 95 | # check that average error is sufficiently small 96 | np.testing.assert_almost_equal(np.mean(viz.normalized_residuals.mu), 0.0) 97 | np.testing.assert_almost_equal(np.mean(viz.normalized_residuals.theta), 0.0) 98 | -------------------------------------------------------------------------------- /pyam/visualizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | import pycollocation 5 | 6 | 7 | class Visualizer(pycollocation.Visualizer): 8 | """Class for visualizing various functions of the solution to the model.""" 9 | 10 | __complementarities = ['Fxy', 'Fxl', 'Fxr', 'Fyl', 'Fyr', 'Flr'] 11 | 12 | __partials = ['F', 'Fx', 'Fy', 'Fl', 'Fr'] 13 | 14 | __solution = None 15 | 16 | @property 17 | def _combined_solution_functionals(self): 18 | """Dictionary of functions evaluated along the model solution.""" 19 | tmp = {} 20 | tmp.update(self._complementarities) 21 | tmp.update(self._partials) 22 | tmp.update(self._factor_payments) 23 | return tmp 24 | 25 | @property 26 | def _complementarities(self): 27 | """Dictionary mapping a complementarity to a callable function.""" 28 | tmp = {} 29 | for complementarity in self.__complementarities: 30 | expr = eval("self.solver.problem." + complementarity) 31 | tmp[complementarity] = self.solver.problem._lambdify_factory(expr.subs(self.solver.problem._subs)) 32 | 33 | return tmp 34 | 35 | @property 36 | def _factor_payments(self): 37 | """Dictionary mapping a factor payment to a callable function.""" 38 | tmp = {} 39 | for payment in ["factor_payment_1", "factor_payment_2"]: 40 | expr = eval("self.solver.problem." + payment) 41 | tmp[payment] = self.solver.problem._lambdify_factory(expr) 42 | 43 | return tmp 44 | 45 | @property 46 | def _partials(self): 47 | """Dictionary mapping a partial derivative to a callable function.""" 48 | tmp = {} 49 | for partial in self.__partials: 50 | expr = eval("self.solver.problem." + partial) 51 | tmp[partial] = self.solver.problem._lambdify_factory(expr.subs(self.solver.problem._subs)) 52 | 53 | return tmp 54 | 55 | @property 56 | def _solution(self): 57 | """Return the solution stored as a dict of NumPy arrays.""" 58 | if self.__solution is None: 59 | tmp = super(Visualizer, self)._solution 60 | 61 | for key, function in self._combined_solution_functionals.items(): 62 | values = function(self.interpolation_knots, 63 | tmp['mu'].values, 64 | tmp['theta'].values, 65 | *self.solver.problem.params.values()) 66 | tmp[key] = pd.Series(values, index=self.interpolation_knots) 67 | 68 | self.__solution = tmp 69 | 70 | return self.__solution 71 | 72 | def _theta_frequency(self): 73 | """Compute the frequency (i.e, measure) of firm size.""" 74 | tmp_df = self.solution.sort('theta', ascending=True, inplace=False) 75 | input1_freq = self.solver.problem.input1.evaluate_pdf(tmp_df.index.values) 76 | theta_frequency = input1_freq / tmp_df.theta 77 | return theta_frequency 78 | 79 | @staticmethod 80 | def compute_cdf(pdf): 81 | """Compute the cumulative distribution function (cdf) given a pdf.""" 82 | values = np.array([np.trapz(pdf.iloc[:x], pdf.index[:x]) for x in range(pdf.size)]) 83 | cdf = pd.Series(values, index=pdf.index.values) 84 | return cdf 85 | 86 | @staticmethod 87 | def compute_sf(cdf): 88 | """Compute the survival function (sf) given a cdf.""" 89 | sf = 1 - cdf 90 | return sf 91 | 92 | def compute_pdf(self, variable, normalize=True): 93 | """Compute the probability density function (pdf) for some variable.""" 94 | tmp_df = self.solution.sort(variable, ascending=True, inplace=False) 95 | tmp_df['theta_frequency'] = self._theta_frequency() 96 | 97 | if normalize: 98 | area = np.trapz(tmp_df.theta_frequency, tmp_df[variable]) # normalize by area! 99 | density = tmp_df.theta_frequency / area 100 | pdf = pd.Series(density.values, index=tmp_df[variable].values) 101 | else: 102 | pdf = pd.Series(tmp_df.theta_frequency.values, index=tmp_df[variable].values) 103 | 104 | return pdf 105 | -------------------------------------------------------------------------------- /pyam/tests/test_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for the model.py module. 3 | 4 | @author : David R. Pugh 5 | @date : 2014-08-02 6 | 7 | """ 8 | import nose 9 | 10 | import sympy as sym 11 | from scipy import stats 12 | 13 | from .. import inputs 14 | from .. import model 15 | 16 | 17 | # define endogenous variables 18 | mu, theta = sym.var('mu, theta') 19 | 20 | # define some workers skill 21 | x, mu1, sigma1 = sym.var('x, mu1, sigma1') 22 | skill_cdf = 0.5 + 0.5 * sym.erf((sym.log(x) - mu1) / sym.sqrt(2 * sigma1**2)) 23 | skill_params = {'mu1': 0.0, 'sigma1': 1.0} 24 | skill_bounds = [1e-3, 5e1] 25 | 26 | workers = inputs.Input(var=x, 27 | cdf=skill_cdf, 28 | params=skill_params, 29 | bounds=skill_bounds, 30 | ) 31 | 32 | # define some firms 33 | y, mu2, sigma2 = sym.var('y, mu2, sigma2') 34 | productivity_cdf = 0.5 + 0.5 * sym.erf((sym.log(y) - mu2) / sym.sqrt(2 * sigma2**2)) 35 | productivity_params = {'mu2': 0.0, 'sigma2': 1.0} 36 | productivity_bounds = [1e-3, 5e1] 37 | 38 | firms = inputs.Input(var=y, 39 | cdf=productivity_cdf, 40 | params=productivity_params, 41 | bounds=productivity_bounds, 42 | ) 43 | 44 | # define some valid model params 45 | valid_F_params = {'nu': 0.89, 'kappa': 1.0, 'gamma': 0.54, 'rho': 0.24, 'A': 1.0} 46 | 47 | # define a valid production function 48 | A, kappa, nu, rho, l, gamma, r = sym.var('A, kappa, nu, rho, l, gamma, r') 49 | valid_F = r * A * kappa * (nu * x**rho + (1 - nu) * (y * (l / r))**rho)**(gamma / rho) 50 | 51 | 52 | def test__validate_assortativity(): 53 | """Testing validation of assortativity attribute.""" 54 | 55 | # assortativity must be either 'positive' or 'negative' 56 | invalid_assortativity = 'invalid_assortativity' 57 | 58 | with nose.tools.assert_raises(AttributeError): 59 | mod = model.AssortativeMatchingModelLike() 60 | mod.assortativity = invalid_assortativity 61 | 62 | # assortativity must be a string 63 | invalid_assortativity = 0.0 64 | 65 | with nose.tools.assert_raises(AttributeError): 66 | mod = model.AssortativeMatchingModelLike() 67 | mod.assortativity = invalid_assortativity 68 | 69 | 70 | def test__validate_production_function(): 71 | """Testing validation of production function attribute.""" 72 | 73 | # production function must have type sym.Basic 74 | def invalid_F(x, y, l, r, A, kappa, nu, rho, gamma): 75 | """Valid F must return a SymPy expression.""" 76 | return r * A * kappa * (nu * x**rho + (1 - nu) * (y * (l / r))**rho)**(gamma / rho) 77 | 78 | with nose.tools.assert_raises(AttributeError): 79 | mod = model.AssortativeMatchingModelLike() 80 | mod.F = invalid_F 81 | 82 | # production function must share vars with workers and firms 83 | m, n = sym.var('m, n') 84 | invalid_F = r * A * kappa * (nu * m**rho + (1 - nu) * (n * (l / r))**rho)**(gamma / rho) 85 | 86 | with nose.tools.assert_raises(AttributeError): 87 | mod = model.AssortativeMatchingModelLike() 88 | mod.F = invalid_F 89 | 90 | # production function must depend on r and l 91 | m, n = sym.var('m, n') 92 | invalid_F = r * A * kappa * (nu * x**rho + (1 - nu) * (y * (m / n))**rho)**(gamma / rho) 93 | 94 | with nose.tools.assert_raises(AttributeError): 95 | mod = model.AssortativeMatchingModelLike() 96 | mod.F = invalid_F 97 | 98 | 99 | def test__validate_F_params(): 100 | """Testing validation of F_params attribute.""" 101 | 102 | # valid parameters must be a dict 103 | invalid_F_params = [1.0, 2.0, 3.0, 4.0, 5.0] 104 | 105 | with nose.tools.assert_raises(AttributeError): 106 | mod = model.AssortativeMatchingModelLike() 107 | mod.F_params = invalid_F_params 108 | 109 | 110 | def test__validate_input(): 111 | """Testing validation of inputs.""" 112 | 113 | # valid inputs must be an instance of the Input class 114 | invalid_input = stats.lognorm(s=1.0) 115 | 116 | with nose.tools.assert_raises(AttributeError): 117 | mod = model.AssortativeMatchingModelLike() 118 | mod.input2 = invalid_input 119 | -------------------------------------------------------------------------------- /pyam/initial_guesses.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | """ 4 | Class for computing initial guesses for the various solvers. 5 | 6 | @author : David R. Pugh 7 | @date : 2015-08-06 8 | 9 | """ 10 | import pycollocation 11 | 12 | 13 | class OrthogonalPolynomialInitialGuess(object): 14 | """ 15 | Class for generating initial guesses for solving an assortative matching 16 | boundary value problem using an orthognal polynomial collocation solver. 17 | 18 | """ 19 | 20 | def __init__(self, solver): 21 | """ 22 | Create an instance of the OrthogonalPolynomialInitialGuess class. 23 | 24 | Parameters 25 | ---------- 26 | solver : pycollocation.OrthogonalPolynomialSolver 27 | 28 | """ 29 | self.solver = self._validate_solver(solver) 30 | 31 | def _initial_mus(self, xs, f, **params): 32 | """ 33 | Return values for mu given some x values and an exponent. 34 | 35 | Parameters 36 | ---------- 37 | xs : numpy.ndarray 38 | exp: float 39 | 40 | Notes 41 | ----- 42 | Theory tells us that the function :math: `\mu(x)` must be monotonically 43 | increasing (decreasing) if the model exhibits positive (negative) 44 | assortative matching in equilibriu. We therefore guess that mu(x) is a 45 | linear transform of some power function where the intercept and slope 46 | of the linear transform are chosen so that resulting function satisfies 47 | the boundary conditions. 48 | 49 | """ 50 | input1 = self.solver.problem.input1 51 | input2 = self.solver.problem.input2 52 | 53 | slope = (input2.upper - input2.lower) / (f(input1.upper, **params) - f(input1.lower, **params)) 54 | 55 | if self.solver.problem.assortativity == "positive": 56 | intercept = input2.lower - slope * f(input1.lower, **params) 57 | 58 | else: 59 | slope = -slope 60 | intercept = input2.upper - slope * f(input1.lower, **params) 61 | 62 | return intercept + slope * f(xs, **params) 63 | 64 | def _initial_guess_mu(self, xs, mus, kind, degree, domain): 65 | """Fit basis polynomial for mu of a certain kind, degree and domain.""" 66 | coefs = self.solver._basis_polynomial_coefs({'mu': degree}) 67 | basis_func = self.solver._basis_function_factory(coefs['mu'], kind, domain) 68 | return basis_func.fit(xs, mus, degree, domain) 69 | 70 | def _initial_guess_theta(self, xs, thetas, kind, degree, domain): 71 | """Fit basis polynomial for theta of a certain kind, degree and domain.""" 72 | coefs = self.solver._basis_polynomial_coefs({'theta': degree}) 73 | basis_func = self.solver._basis_function_factory(coefs['theta'], kind, domain) 74 | return basis_func.fit(xs, thetas, degree, domain) 75 | 76 | def _initial_thetas(self, xs, initial_guess_mu): 77 | """Initial guess for theta(x) should be consistent with mu(x) guess.""" 78 | input1 = self.solver.problem.input1 79 | input2 = self.solver.problem.input2 80 | 81 | H = input1.evaluate_pdf(xs) / input2.evaluate_pdf(initial_guess_mu(xs)) 82 | if self.solver.problem.assortativity == "positive": 83 | thetas = (H / initial_guess_mu.deriv()(xs)) 84 | else: 85 | thetas = -(H / initial_guess_mu.deriv()(xs)) 86 | return thetas 87 | 88 | @staticmethod 89 | def _validate_solver(solver): 90 | """Validates the solver attribute.""" 91 | if not isinstance(solver, pycollocation.OrthogonalPolynomialSolver): 92 | raise ValueError 93 | else: 94 | return solver 95 | 96 | def compute_initial_guess(self, kind, degrees, f, N=1000, **params): 97 | """ 98 | Compute initial orthogonal polynomials. 99 | 100 | Parameters 101 | ---------- 102 | kind : string 103 | degrees : dict 104 | N : int 105 | exp : float (default=1.0) 106 | 107 | """ 108 | # get domain values 109 | domain = [self.solver.problem.input1.lower, 110 | self.solver.problem.input1.upper] 111 | xs = np.linspace(domain[0], domain[1], N) 112 | 113 | # initial guess for mu is some power function 114 | mus = self._initial_mus(xs, f, **params) 115 | initial_guess_mu = self._initial_guess_mu(xs, mus, kind, degrees['mu'], domain) 116 | 117 | # initial guess for theta depends on guess for mu 118 | thetas = self._initial_thetas(xs, initial_guess_mu) 119 | initial_guess_theta = self._initial_guess_theta(xs, thetas, kind, degrees['theta'], domain) 120 | 121 | return {'mu': initial_guess_mu, 'theta': initial_guess_theta} 122 | -------------------------------------------------------------------------------- /pyam/tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test functions for the inputs.py module. 3 | 4 | @author : David R. Pugh 5 | @date : 2015-08-02 6 | 7 | """ 8 | import nose 9 | 10 | import numpy as np 11 | from scipy import stats 12 | import sympy as sym 13 | 14 | from .. import inputs 15 | 16 | # define a valid cdf expression 17 | valid_var, mu, sigma = sym.var('x, mu, sigma') 18 | valid_cdf = 0.5 + 0.5 * sym.erf((sym.log(valid_var) - mu) / sym.sqrt(2 * sigma**2)) 19 | valid_params = {'mu': 0.0, 'sigma': 1.0} 20 | valid_bounds = (0.1, 1e1) 21 | valid_alpha = 0.005 22 | 23 | 24 | def test_validate_cdf(): 25 | """Testing validation of cdf attribute.""" 26 | 27 | def invalid_cdf(x, mu, sigma): 28 | """Valid cdf must return a SymPy expression.""" 29 | return stats.lognorm.cdf(x, sigma, scale=np.exp(mu)) 30 | 31 | with nose.tools.assert_raises(AttributeError): 32 | inputs.Input(var=valid_var, cdf=invalid_cdf, bounds=valid_bounds, 33 | alpha=valid_alpha, params=valid_params) 34 | 35 | 36 | def test_validate_lower(): 37 | """Testing validation of lower attribute.""" 38 | 39 | # lower should be a float 40 | invalid_lower = 1 41 | 42 | with nose.tools.assert_raises(AttributeError): 43 | workers = inputs.Input(var=valid_var, cdf=valid_cdf, 44 | bounds=valid_bounds, alpha=valid_alpha, 45 | params=valid_params) 46 | workers.lower = invalid_lower 47 | 48 | 49 | def test_validate_upper(): 50 | """Testing validation of upper attribute.""" 51 | 52 | # upper should be a float 53 | invalid_upper = 14 54 | 55 | with nose.tools.assert_raises(AttributeError): 56 | workers = inputs.Input(var=valid_var, cdf=valid_cdf, 57 | bounds=valid_bounds, alpha=valid_alpha, 58 | params=valid_params) 59 | workers.upper = invalid_upper 60 | 61 | 62 | def test_validate_params(): 63 | """Testing validation of bounds parameter.""" 64 | 65 | # valid parameters must be a dict 66 | invalid_params = (1.0, 2.0) 67 | 68 | with nose.tools.assert_raises(AttributeError): 69 | inputs.Input(var=valid_var, cdf=valid_cdf, bounds=valid_bounds, 70 | alpha=valid_alpha, params=invalid_params) 71 | 72 | 73 | def test_validate_var(): 74 | """Testing validation of var attribute.""" 75 | 76 | # valid var must be a sym.Symbol 77 | invalid_var = 'x' 78 | 79 | with nose.tools.assert_raises(AttributeError): 80 | inputs.Input(var=invalid_var, cdf=valid_cdf, bounds=valid_bounds, 81 | alpha=valid_alpha, params=valid_params) 82 | 83 | 84 | def test_evaluate_cdf(): 85 | """Testing the evaluation of the cdf.""" 86 | 87 | # suppose that workers are uniform on [a, b] = [0, 1] 88 | a, b = sym.var('a, b') 89 | uniform_cdf = (valid_var - a) / (b - a) 90 | params = {'a': 0.0, 'b': 1.0} 91 | workers = inputs.Input(var=valid_var, cdf=uniform_cdf, bounds=[0.0, 1.0], 92 | alpha=valid_alpha, params=params) 93 | 94 | # evaluate with scalar input 95 | actual_cdf = workers.evaluate_cdf(0.5) 96 | expected_cdf = 0.5 97 | nose.tools.assert_almost_equals(actual_cdf, expected_cdf) 98 | 99 | # evaluate with array input 100 | actual_cdf = workers.evaluate_cdf(np.array([0.0, 0.5, 1.0])) 101 | expected_cdf = actual_cdf 102 | np.testing.assert_almost_equal(actual_cdf, expected_cdf) 103 | 104 | 105 | def test_evaluate_pdf(): 106 | """Testing the evaluation of the pdf.""" 107 | 108 | # suppose that workers are uniform on [a, b] = [0, 1] 109 | a, b = sym.var('a, b') 110 | uniform_cdf = (valid_var - a) / (b - a) 111 | params = {'a': 0.0, 'b': 1.0} 112 | workers = inputs.Input(var=valid_var, cdf=uniform_cdf, bounds=[0.25, 0.75], 113 | params=params) 114 | 115 | # evaluate with scalar input and norm=False 116 | actual_pdf = workers.evaluate_pdf(0.5, norm=False) 117 | expected_pdf = 1.0 118 | nose.tools.assert_almost_equals(actual_pdf, expected_pdf) 119 | 120 | # evaluate with array input and norm=False 121 | actual_pdf = workers.evaluate_pdf(np.array([0.25, 0.5, 0.75]), norm=False) 122 | expected_pdf = np.ones(3) 123 | np.testing.assert_almost_equal(actual_pdf, expected_pdf) 124 | 125 | # evaluate with scalar input and norm=True 126 | actual_pdf = workers.evaluate_pdf(0.5, norm=True) 127 | expected_pdf = 2.0 128 | nose.tools.assert_almost_equals(actual_pdf, expected_pdf) 129 | 130 | # evaluate with array input and norm=False 131 | actual_pdf = workers.evaluate_pdf(np.array([0.25, 0.5, 0.75]), norm=True) 132 | expected_pdf = np.repeat(2.0, 2) 133 | np.testing.assert_almost_equal(actual_pdf, expected_pdf) 134 | -------------------------------------------------------------------------------- /pyam/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for building assortative matchign models with heterogenous factors of 3 | production. 4 | 5 | @author : David R. Pugh 6 | @date : 2015-01-24 7 | 8 | """ 9 | import sympy as sym 10 | 11 | from . import inputs 12 | 13 | 14 | class AssortativeMatchingModelLike(object): 15 | """Class representing a matching model with two-sided heterogeneity.""" 16 | 17 | _required_symbols = sym.symbols(['l', 'r']) 18 | 19 | @property 20 | def assortativity(self): 21 | """ 22 | String defining the matching assortativty. 23 | 24 | :getter: Return the current matching assortativity 25 | :setter: Set a new matching assortativity. 26 | :type: str 27 | 28 | """ 29 | return self._assortativity 30 | 31 | @assortativity.setter 32 | def assortativity(self, value): 33 | """Set new matching assortativity.""" 34 | self._assortativity = self._validate_assortativity(value) 35 | 36 | @property 37 | def F(self): 38 | """ 39 | Symbolic expression describing the available production technology. 40 | 41 | :getter: Return the current production function. 42 | :setter: Set a new production function. 43 | :type: sympy.Basic 44 | 45 | """ 46 | return self._F 47 | 48 | @F.setter 49 | def F(self, value): 50 | """Set a new production function.""" 51 | self._F = self._validate_production_function(value) 52 | 53 | @property 54 | def F_params(self): 55 | """ 56 | Dictionary of parameters for the production function, F. 57 | 58 | :getter: Return the current parameter dictionary. 59 | :type: dict 60 | 61 | """ 62 | return self._F_params 63 | 64 | @F_params.setter 65 | def F_params(self, value): 66 | """Set a new dictionary of parameters for F.""" 67 | self._F_params = self._validate_F_params(value) 68 | 69 | @property 70 | def input1(self): 71 | """ 72 | Class describing a heterogenous production input. 73 | 74 | :getter: Return production input1. 75 | :setter: Set new production input1. 76 | :type: inputs.Input 77 | 78 | """ 79 | return self._input1 80 | 81 | @input1.setter 82 | def input1(self, value): 83 | """Set new production input1.""" 84 | self._input1 = self._validate_input(value) 85 | 86 | @property 87 | def input2(self): 88 | """ 89 | Class describing a heterogenous production input. 90 | 91 | :getter: Return production input2. 92 | :setter: Set new production input2. 93 | :type: inputs.Input 94 | 95 | """ 96 | return self._input2 97 | 98 | @input2.setter 99 | def input2(self, value): 100 | """Set new production input2.""" 101 | self._input2 = self._validate_input(value) 102 | 103 | @staticmethod 104 | def _validate_assortativity(value): 105 | """Validates the matching assortativity.""" 106 | valid_assortativities = ['positive', 'negative'] 107 | if not isinstance(value, str): 108 | mesg = "Attribute 'assortativity' must have type str, not {}." 109 | raise AttributeError(mesg.format(value.__class__)) 110 | elif value not in valid_assortativities: 111 | mesg = "Attribute 'assortativity' must be in {}." 112 | raise AttributeError(mesg.format(valid_assortativities)) 113 | else: 114 | return value 115 | 116 | @staticmethod 117 | def _validate_input(value): 118 | """Validates the input1 and input2 attributes.""" 119 | if not isinstance(value, inputs.Input): 120 | mesg = ("Attributes 'input1' and 'input2' must have " + 121 | "type inputs.Input, not {}.") 122 | raise AttributeError(mesg.format(value.__class__)) 123 | else: 124 | return value 125 | 126 | @staticmethod 127 | def _validate_F_params(params): 128 | """Validates the dictionary of model parameters.""" 129 | if not isinstance(params, dict): 130 | mesg = "Attribute 'params' must have type dict, not {}." 131 | raise AttributeError(mesg.format(params.__class__)) 132 | else: 133 | return params 134 | 135 | def _validate_production_function(self, F): 136 | """Validates the production function attribute.""" 137 | if not isinstance(F, sym.Basic): 138 | mesg = "Attribute 'F' must have type sympy.Basic, not {}." 139 | raise AttributeError(mesg.format(F.__class__)) 140 | elif not set(self._required_symbols) < F.atoms(): 141 | mesg = "Attribute 'F' must be an expression of r and l." 142 | raise AttributeError(mesg) 143 | elif not {self.input1.var, self.input2.var} < F.atoms(): 144 | mesg = ("Attribute 'F' must be an expression of input1.var and " + 145 | "input2.var variables.") 146 | raise AttributeError(mesg) 147 | else: 148 | return F 149 | -------------------------------------------------------------------------------- /pyam/problem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for modeling assortative matching boundary value problems. 3 | 4 | @author : David R. Pugh 5 | @date : 2015-07-14 6 | 7 | """ 8 | from __future__ import division 9 | import sympy as sym 10 | 11 | from pycollocation import SymbolicTwoPointBVPLike 12 | 13 | from . model import AssortativeMatchingModelLike 14 | 15 | 16 | class AssortativeMatchingProblem(AssortativeMatchingModelLike, SymbolicTwoPointBVPLike): 17 | 18 | def __init__(self, assortativity, input1, input2, F, F_params): 19 | """ 20 | Create an instance of the MatchingProblem class. 21 | 22 | Parameters 23 | ---------- 24 | assortativity : str 25 | String defining the type of matching assortativity. Must be one of 26 | 'positive' or 'negative'. 27 | input1 : inputs.Input 28 | A heterogenous production input. 29 | input2 : inputs.Input 30 | A heterogenous production input. 31 | F : sympy.Basic 32 | Symbolic expression describing the production function. 33 | F_params : dict 34 | Dictionary of model parameters for the production function. 35 | 36 | """ 37 | self.assortativity = assortativity 38 | self.input1 = input1 39 | self.input2 = input2 40 | self.F = F 41 | self.F_params = F_params 42 | 43 | @property 44 | def _subs(self): 45 | """Dictionary of variable substitutions""" 46 | subs = {self.input2.var: self._symbolic_vars[1], 47 | self._required_symbols[0]: self._symbolic_vars[2], 48 | self._required_symbols[1]: 1.0} 49 | return subs 50 | 51 | @property 52 | def boundary_conditions(self): 53 | """Boundary conditions for the matching problem.""" 54 | if self.assortativity == "positive": 55 | bcs = {'lower': [self._symbolic_vars[1] - self.input1.lower], 56 | 'upper': [self._symbolic_vars[1] - self.input1.upper]} 57 | else: 58 | bcs = {'lower': [self._symbolic_vars[1] - self.input1.upper], 59 | 'upper': [self._symbolic_vars[1] - self.input1.lower]} 60 | return bcs 61 | 62 | @property 63 | def dependent_vars(self): 64 | """Return a list of model dependent variables.""" 65 | return ['mu', 'theta'] 66 | 67 | @property 68 | def f(self): 69 | """ 70 | Symbolic expression for intensive output. 71 | 72 | :getter: Return the current expression for intensive output. 73 | :type: sympy.Basic. 74 | 75 | """ 76 | expr = (1 / self._required_symbols[1]) * self.F 77 | return expr.subs(self._subs) 78 | 79 | @property 80 | def factor_payment_1(self): 81 | """ 82 | Symbolic expression for payments made to input 1. 83 | 84 | :getter: Return the current expression for the factor payments. 85 | :type: sympy.Basic. 86 | 87 | """ 88 | return sym.diff(self.f, self._symbolic_vars[2]) 89 | 90 | @property 91 | def factor_payment_2(self): 92 | """ 93 | Symbolic expression for payments made to input 2. 94 | 95 | :getter: Return the current expression for the factor payments. 96 | :type: sympy.Basic. 97 | 98 | """ 99 | revenue = self.f 100 | costs = self._symbolic_vars[2] * self.factor_payment_1 101 | return revenue - costs 102 | 103 | @property 104 | def Fx(self): 105 | """ 106 | Symbolic expression for the marginal product of input1. 107 | 108 | :getter: Return the the marginal product of input1. 109 | :type: sympy.Basic 110 | 111 | """ 112 | return sym.diff(self.F, self.input1.var) 113 | 114 | @property 115 | def Fy(self): 116 | """ 117 | Symbolic expression for the marginal product of input2. 118 | 119 | :getter: Return the the marginal product of input2. 120 | :type: sympy.Basic 121 | 122 | """ 123 | return sym.diff(self.F, self.input2.var) 124 | 125 | @property 126 | def Fl(self): 127 | """ 128 | Symbolic expression for the marginal product of l. 129 | 130 | :getter: Return the the marginal product of l. 131 | :type: sympy.Basic 132 | 133 | """ 134 | return sym.diff(self.F, self._required_symbols[0]) 135 | 136 | @property 137 | def Fr(self): 138 | """ 139 | Symbolic expression for the marginal product of r. 140 | 141 | :getter: Return the the marginal product of r. 142 | :type: sympy.Basic 143 | 144 | """ 145 | return sym.diff(self.F, self._required_symbols[1]) 146 | 147 | @property 148 | def Fxy(self): 149 | """ 150 | Symbolic expression for the cross-partial derivative. 151 | 152 | :getter: Return the expression for the cross-partial derivative. 153 | :type: sympy.Basic 154 | 155 | """ 156 | return sym.diff(self.F, self.input1.var, self.input2.var) 157 | 158 | @property 159 | def Fxl(self): 160 | """ 161 | Symbolic expression for the cross-partial derivative. 162 | 163 | :getter: Return the expression for the cross-partial derivative. 164 | :type: sympy.Basic 165 | 166 | """ 167 | return sym.diff(self.F, self.input1.var, self._required_symbols[0]) 168 | 169 | @property 170 | def Fxr(self): 171 | """ 172 | Symbolic expression for the cross-partial derivative. 173 | 174 | :getter: Return the expression for the cross-partial derivative. 175 | :type: sympy.Basic 176 | 177 | """ 178 | return sym.diff(self.F, self.input1.var, self._required_symbols[1]) 179 | 180 | @property 181 | def Fyl(self): 182 | """ 183 | Symbolic expression for the cross-partial derivative. 184 | 185 | :getter: Return the expression for the cross-partial derivative. 186 | :type: sympy.Basic 187 | 188 | """ 189 | return sym.diff(self.F, self.input2.var, self._required_symbols[0]) 190 | 191 | @property 192 | def Fyr(self): 193 | """ 194 | Symbolic expression for the cross-partial derivative. 195 | 196 | :getter: Return the expression for the cross-partial derivative. 197 | :type: sympy.Basic 198 | 199 | """ 200 | return sym.diff(self.F, self.input2.var, self._required_symbols[1]) 201 | 202 | @property 203 | def Flr(self): 204 | """ 205 | Symbolic expression for the cross-partial derivative. 206 | 207 | :getter: Return the expression for the cross-partial derivative. 208 | :type: sympy.Basic 209 | 210 | """ 211 | return sym.diff(self.F, *self._required_symbols) 212 | 213 | @property 214 | def H(self): 215 | """ 216 | Ratio of input1 probability density to input2 probability density. 217 | 218 | :getter: Return current density ratio. 219 | :type: sympy.Basic 220 | 221 | """ 222 | return self.input1.pdf / self.input2.pdf 223 | 224 | @property 225 | def independent_var(self): 226 | """Return the model independent variable as a string.""" 227 | return 'x' 228 | 229 | @property 230 | def mu_prime(self): 231 | """ 232 | ODE describing the equilibrium matching between production inputs. 233 | 234 | :getter: Return the current expression for mu prime. 235 | :type: sympy.Basic 236 | 237 | """ 238 | if self.assortativity == "positive": 239 | expr = self.H / self._symbolic_vars[2] 240 | else: 241 | expr = expr = -self.H / self._symbolic_vars[2] 242 | return expr.subs(self._subs) 243 | 244 | @property 245 | def params(self): 246 | """ 247 | Dictionary of model parameters. 248 | 249 | :getter: Return the current parameter dictionary. 250 | :type: dict 251 | 252 | """ 253 | model_params = {} 254 | model_params.update(self.input1.params) 255 | model_params.update(self.input2.params) 256 | model_params.update(self.F_params) 257 | return self._order_params(model_params) 258 | 259 | @property 260 | def rhs(self): 261 | """Symbolic expressions for the RHS of the system of ODEs.""" 262 | return {'mu': self.mu_prime, 'theta': self.theta_prime} 263 | 264 | @property 265 | def theta_prime(self): 266 | """ 267 | Differential equation describing the equilibrium firm size. 268 | :getter: Return the current expression for theta prime. 269 | :type: sympy.Basic 270 | """ 271 | if self.assortativity == "positive": 272 | expr = (self.H * self.Fyl - self.Fxr) / self.Flr 273 | else: 274 | expr = -(self.H * self.Fyl + self.Fxr) / self.Flr 275 | return expr.subs(self._subs) 276 | -------------------------------------------------------------------------------- /pyam/inputs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for modeling heterogenous factors of production. 3 | 4 | @author : David R. Pugh 5 | @date : 2015-08-06 6 | 7 | """ 8 | from __future__ import division 9 | import collections 10 | 11 | import numpy as np 12 | from scipy import optimize, special 13 | import sympy as sym 14 | 15 | 16 | class Input(object): 17 | """Class representing a heterogenous production input.""" 18 | 19 | _modules = [{'ImmutableMatrix': np.array, 'erf': special.erf}, 'numpy'] 20 | 21 | __numeric_cdf = None 22 | 23 | __numeric_pdf = None 24 | 25 | def __init__(self, var, cdf, bounds, params, alpha=None, measure=1.0): 26 | """ 27 | Create an instance of the Input class. 28 | 29 | Parameters 30 | ---------- 31 | var : sym.Symbol 32 | Symbolic variable representing the production input. 33 | cdf : sym.Basic 34 | Symbolic expression defining a valid probability distribution 35 | function (CDF). Must be a function of var. 36 | bounds : (float, float) 37 | Tuple of floats that should bracket the desired quantile, alpha. 38 | params : dict 39 | Dictionary of distribution parameters. 40 | alpha : float, optional (default=None) 41 | Quantile defining the lower bound on the support of the cumulative 42 | distribution function. 43 | measure : float 44 | The measure of available units of the input. 45 | 46 | """ 47 | self.var = var 48 | self.measure = measure # needs to be assigned before cdf is set! 49 | self.cdf = cdf 50 | self.alpha = alpha # needs to be assigned before params are set! 51 | self.lower = bounds[0] 52 | self.upper = bounds[1] 53 | self.params = params 54 | 55 | @property 56 | def _numeric_cdf(self): 57 | """ 58 | Vectorized function used to numerically evaluate the CDF. 59 | 60 | :getter: Return the lambdified CDF. 61 | :type: function 62 | 63 | """ 64 | if self.__numeric_cdf is None: 65 | args = [self.var] + sym.var(list(self.params.keys())) 66 | self.__numeric_cdf = sym.lambdify(args, self.cdf, self._modules) 67 | return self.__numeric_cdf 68 | 69 | @property 70 | def _numeric_pdf(self): 71 | """ 72 | Vectorized function used to numerically evaluate the pdf. 73 | 74 | :getter: Return the lambdified pdf. 75 | :type: function 76 | 77 | """ 78 | if self.__numeric_pdf is None: 79 | args = [self.var] + sym.var(list(self.params.keys())) 80 | self.__numeric_pdf = sym.lambdify(args, self.pdf, self._modules) 81 | return self.__numeric_pdf 82 | 83 | @property 84 | def cdf(self): 85 | """ 86 | Cumulative distribution function (CDF). 87 | 88 | :getter: Return the current distribution function. 89 | :setter: Set a new distribution function. 90 | :type: sym.Basic 91 | 92 | """ 93 | return self._cdf 94 | 95 | @cdf.setter 96 | def cdf(self, value): 97 | """Set a new cumulative distribution function (CDF).""" 98 | self._cdf = self.measure * self._validate_cdf(value) # rescale cdf! 99 | 100 | @property 101 | def lower(self): 102 | """ 103 | Lower bound on support of the cumulative distribution function (CDF). 104 | 105 | :getter: Return the lower bound. 106 | :setter: Set a new lower bound. 107 | :type: float 108 | 109 | """ 110 | return self._lower 111 | 112 | @lower.setter 113 | def lower(self, value): 114 | """Set a new lower bound.""" 115 | self._lower = self._validate_lower_bound(value) 116 | 117 | @property 118 | def norm_constant(self): 119 | """ 120 | Constant used to normalize the probability density function (pdf). 121 | 122 | :getter: Return the current normalization constant. 123 | :type: float 124 | 125 | """ 126 | return self.evaluate_cdf(self.upper) - self.evaluate_cdf(self.lower) 127 | 128 | @property 129 | def measure(self): 130 | """ 131 | The measure of availale units of the input. 132 | 133 | :getter: Return the measure. 134 | :setter: Set a new measure. 135 | :type: float 136 | 137 | """ 138 | return self._measure 139 | 140 | @measure.setter 141 | def measure(self, value): 142 | """Set a new lower bound.""" 143 | self._measure = self._validate_measure(value) 144 | 145 | @property 146 | def params(self): 147 | """ 148 | Dictionary of distribution parameters. 149 | 150 | :getter: Return the current parameter dictionary. 151 | :setter: Set a new parameter dictionary. 152 | :type: dict 153 | 154 | """ 155 | return self._params 156 | 157 | @params.setter 158 | def params(self, value): 159 | """Set a new parameter dictionary.""" 160 | valid_params = self._validate_params(value) 161 | self._params = self._order_params(valid_params) 162 | self._update_bounds(self.lower, self.upper) 163 | 164 | @property 165 | def pdf(self): 166 | """ 167 | Probability density function (pdf). 168 | 169 | :getter: Return the current probability density function. 170 | :type: sym.Basic 171 | 172 | """ 173 | return sym.diff(self.cdf, self.var) 174 | 175 | @property 176 | def upper(self): 177 | """ 178 | Upper bound on support of the cumulative distribution function (CDF). 179 | 180 | :getter: Return the lower bound. 181 | :setter: Set a new lower bound. 182 | :type: float 183 | 184 | """ 185 | return self._upper 186 | 187 | @upper.setter 188 | def upper(self, value): 189 | """Set a new upper bound.""" 190 | self._upper = self._validate_upper_bound(value) 191 | 192 | @property 193 | def var(self): 194 | """ 195 | Symbolic variable respresenting the production input. 196 | 197 | :getter: Return the current variable. 198 | :setter: Set a new variable. 199 | :type: sym.Symbol 200 | 201 | """ 202 | return self._var 203 | 204 | @var.setter 205 | def var(self, value): 206 | """Set a new symbolic variable.""" 207 | self._var = self._validate_var(value) 208 | 209 | @staticmethod 210 | def _order_params(params): 211 | """Cast a dictionary to an order dictionary.""" 212 | return collections.OrderedDict(sorted(params.items())) 213 | 214 | @staticmethod 215 | def _validate_cdf(cdf): 216 | """Validates the probability distribution function (CDF).""" 217 | if not isinstance(cdf, sym.Basic): 218 | mesg = "Attribute 'cdf' must have type sympy.Basic, not {}" 219 | raise AttributeError(mesg.format(cdf.__class__)) 220 | else: 221 | return cdf 222 | 223 | @staticmethod 224 | def _validate_measure(value): 225 | """Validate the measure of available input.""" 226 | if not isinstance(value, float): 227 | mesg = "Attribute 'measure' must be a float, not {}" 228 | raise AttributeError(mesg.format(value.__class__)) 229 | elif value < 0: 230 | mesg = "Attribute 'measure' attribute must be strictly positive." 231 | raise AttributeError(mesg) 232 | else: 233 | return value 234 | 235 | @staticmethod 236 | def _validate_params(value): 237 | """Validate the dictionary of parameters.""" 238 | if not isinstance(value, dict): 239 | mesg = "Attribute 'params' must have type dict, not {}" 240 | raise AttributeError(mesg.format(value.__class__)) 241 | else: 242 | return value 243 | 244 | @staticmethod 245 | def _validate_var(var): 246 | """Validates the symbolic variable.""" 247 | if not isinstance(var, sym.Symbol): 248 | mesg = "Attribute 'var' must have type sympy.Symbol, not {}" 249 | raise AttributeError(mesg.format(var.__class__)) 250 | else: 251 | return var 252 | 253 | def _validate_upper_bound(self, value): 254 | """Validate the upper bound on the suppport of the CDF.""" 255 | if not isinstance(value, float): 256 | mesg = "Attribute 'upper' must have type float, not {}" 257 | raise AttributeError(mesg.format(value.__class__)) 258 | else: 259 | return value 260 | 261 | def _find_bound(self, alpha, lower): 262 | """Find the alpha quantile of the CDF.""" 263 | return optimize.newton(self._inverse_cdf, lower, args=(alpha,)) 264 | 265 | def _inverse_cdf(self, x, alpha): 266 | """Inverse CDF used to identify the lower and upper bounds.""" 267 | return self.evaluate_cdf(x) - alpha 268 | 269 | def _update_bounds(self, lower, upper): 270 | if self.alpha is not None: 271 | self.lower = self._find_bound(self.alpha * self.measure, lower) 272 | self.upper = self._find_bound((1 - self.alpha) * self.measure, upper) 273 | 274 | def _validate_lower_bound(self, value): 275 | """Validate the lower bound on the suppport of the CDF.""" 276 | if not isinstance(value, float): 277 | mesg = "Attribute 'lower' must have type float, not {}" 278 | raise AttributeError(mesg.format(value.__class__)) 279 | else: 280 | return value 281 | 282 | def evaluate_cdf(self, value): 283 | """ 284 | Numerically evaluate the cumulative distribution function (CDF). 285 | 286 | Parameters 287 | ---------- 288 | value : numpy.ndarray 289 | Values at which to evaluate the CDF. 290 | 291 | Returns 292 | ------- 293 | out : numpy.ndarray 294 | Evaluated CDF. 295 | 296 | """ 297 | out = self._numeric_cdf(value, *self.params.values()) 298 | return out 299 | 300 | def evaluate_pdf(self, value, norm=False): 301 | """ 302 | Numerically evaluate the probability density function (pdf). 303 | 304 | Parameters 305 | ---------- 306 | value : numpy.ndarray 307 | Values at which to evaluate the pdf. 308 | norm : boolean (default=False) 309 | True if you wish to normalize the pdf so that it integrates to one; 310 | False otherwise. 311 | 312 | Returns 313 | ------- 314 | out : numpy.ndarray 315 | Evaluated pdf. 316 | 317 | """ 318 | if norm: 319 | out = (self._numeric_pdf(value, *self.params.values()) / 320 | self.norm_constant) 321 | else: 322 | out = self._numeric_pdf(value, *self.params.values()) 323 | return out 324 | --------------------------------------------------------------------------------