├── mcerp ├── tests │ ├── __init__.py │ ├── test_lhd.py │ └── test_core.py ├── stats.py ├── umath.py ├── correlate.py ├── lhd.py └── __init__.py ├── doc ├── _static │ ├── x1.png │ ├── Z_hist.png │ ├── Z_kde.png │ ├── logo.png │ ├── favicon.ico │ ├── favicon.png │ ├── Z_kde_hist.png │ ├── after_correlation_matrixplot.png │ ├── before_correlation_matrixplot.png │ ├── favicon.svg │ ├── _default.css │ └── logo.svg ├── index_TOC.rst ├── basic_examples.rst ├── changes.rst ├── view_distribution.rst ├── contribute.rst ├── _templates │ └── layout.html ├── Makefile ├── correlations.rst ├── index.rst ├── probabilities.rst ├── advanced_example.rst ├── conf.py └── distribution_constructors.rst ├── .gitignore ├── environment.yml ├── readme_code.py ├── azure-pipelines.yml ├── LICENSE ├── setup.py └── README.rst /mcerp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/_static/x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/x1.png -------------------------------------------------------------------------------- /doc/_static/Z_hist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/Z_hist.png -------------------------------------------------------------------------------- /doc/_static/Z_kde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/Z_kde.png -------------------------------------------------------------------------------- /doc/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/logo.png -------------------------------------------------------------------------------- /doc/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/favicon.ico -------------------------------------------------------------------------------- /doc/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/favicon.png -------------------------------------------------------------------------------- /doc/_static/Z_kde_hist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/Z_kde_hist.png -------------------------------------------------------------------------------- /doc/_static/after_correlation_matrixplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/after_correlation_matrixplot.png -------------------------------------------------------------------------------- /doc/_static/before_correlation_matrixplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tisimst/mcerp/HEAD/doc/_static/before_correlation_matrixplot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.*~ 3 | *.egg-info 4 | dist 5 | build 6 | doc/_build 7 | MANIFEST 8 | .idea 9 | .eggs 10 | .coverage 11 | htmlcov 12 | -------------------------------------------------------------------------------- /doc/index_TOC.rst: -------------------------------------------------------------------------------- 1 | Table of Contents 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | Overview 8 | basic_examples 9 | distribution_constructors 10 | view_distribution 11 | probabilities 12 | correlations 13 | advanced_example 14 | changes 15 | contribute 16 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # Conda environment for mcerp development 2 | # 3 | # Install: conda env create -f environment.yml 4 | # Update: conda env update -f environment.yml 5 | # Activate: conda activate mcerp 6 | # Deactivate: conda deactivate 7 | 8 | name: mcerp 9 | 10 | dependencies: 11 | - python==3.7 12 | - ipython 13 | - jupyter 14 | - jupyterlab 15 | - numpy 16 | - scipy 17 | - matplotlib 18 | - uncertainties 19 | - pytest 20 | - pytest-cov 21 | - sphinx 22 | - pylint 23 | - flake8 24 | - pip: 25 | - setuptools_scm 26 | - numpydoc 27 | - sphinx_rtd_theme 28 | - pydocstyle 29 | - black 30 | - twine 31 | -------------------------------------------------------------------------------- /readme_code.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from mcerp import * 3 | 4 | x1 = N(24, 1) 5 | x2 = N(37, 4) 6 | x3 = Exp(2) 7 | print(x1.stats) 8 | 9 | Z = (x1 * x2 ** 2) / (15 * (1.5 + x3)) 10 | print(Z) 11 | Z.describe() 12 | 13 | x1.plot() 14 | x1.show() 15 | Z.plot() 16 | Z.show() 17 | Z.plot(hist=True) 18 | Z.show() 19 | Z.plot() 20 | Z.plot(hist=True) 21 | Z.show() 22 | 23 | print(correlation_matrix([x1, x2, x3])) 24 | plotcorr([x1, x2, x3], labels=["x1", "x2", "x3"]) 25 | plt.show() 26 | 27 | c = np.array([[1.0, -0.75, 0.0], [-0.75, 1.0, 0.0], [0.0, 0.0, 1.0]]) 28 | 29 | correlate([x1, x2, x3], c) 30 | print(correlation_matrix([x1, x2, x3])) 31 | plotcorr([x1, x2, x3], labels=["x1", "x2", "x3"]) 32 | plt.show() 33 | 34 | Z = (x1 * x2 ** 2) / (15 * (1.5 + x3)) 35 | print(Z) 36 | Z.describe() 37 | 38 | print(x1 < 15) 39 | print(Z >= 1000) 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Python package 2 | # Create and test a Python package on multiple Python versions. 3 | # Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/python 5 | 6 | jobs: 7 | 8 | - job: 'Test' 9 | pool: 10 | vmImage: 'Ubuntu 16.04' 11 | strategy: 12 | matrix: 13 | Python27: 14 | python.version: '2.7' 15 | Python35: 16 | python.version: '3.5' 17 | maxParallel: 4 18 | 19 | steps: 20 | - task: UsePythonVersion@0 21 | inputs: 22 | versionSpec: '$(python.version)' 23 | architecture: 'x64' 24 | 25 | - script: | 26 | python -m pip install --upgrade pip 27 | pip install numpy scipy matplotlib 28 | displayName: 'Install dependencies' 29 | 30 | - script: | 31 | pip install pytest pytest-cov 32 | pytest -v --junitxml=junit/test-results.xml 33 | displayName: 'pytest' 34 | 35 | - task: PublishTestResults@2 36 | inputs: 37 | testResultsFiles: '**/test-results.xml' 38 | testRunTitle: 'Python $(python.version)' 39 | condition: succeededOrFailed() 40 | 41 | - job: 'Publish' 42 | dependsOn: 'Test' 43 | pool: 44 | vmImage: 'Ubuntu 16.04' 45 | 46 | steps: 47 | - task: UsePythonVersion@0 48 | inputs: 49 | versionSpec: '3.x' 50 | architecture: 'x64' 51 | 52 | - script: python setup.py sdist 53 | displayName: 'Build sdist' -------------------------------------------------------------------------------- /mcerp/tests/test_lhd.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ..lhd import ss, lhd 3 | 4 | 5 | def test(): 6 | # test single distribution 7 | d0 = ss.uniform(loc=-1, scale=2) # uniform distribution,low=-1, width=2 8 | print(lhd(dist=d0, size=5)) 9 | 10 | # test single distribution for multiple variables 11 | d1 = ss.norm(loc=0, scale=1) # normal distribution, mean=0, stdev=1 12 | print(lhd(dist=d1, size=7, dims=5)) 13 | 14 | # test multiple distributions 15 | d2 = ss.beta(2, 5) # beta distribution, alpha=2, beta=5 16 | d3 = ss.expon(scale=1 / 1.5) # exponential distribution, lambda=1.5 17 | print(lhd(dist=(d1, d2, d3), size=6)) 18 | 19 | rand_lhs = lhd(dist=(d0, d1, d2, d3), size=100) 20 | spac_lhs = lhd( 21 | dist=(d0, d1, d2, d3), 22 | size=100, 23 | form="spacefilling", 24 | iterations=100, 25 | showcorrelations=True, 26 | ) 27 | 28 | pytest.importorskip("matplotlib") 29 | pytest.importorskip("scatterplot_matrix") 30 | try: 31 | from scatterplot_matrix import scatterplot_matrix as spm 32 | import matplotlib.pyplot as plt 33 | except ImportError: 34 | print(rand_lhs) 35 | print(spac_lhs) 36 | else: 37 | names = ["U(-1,1)", "N(0,1)", "Beta(2,5)", "Exp(1.5)"] 38 | spm(rand_lhs.T, names=names) 39 | plt.suptitle("Completely Random LHS Design") 40 | plt.show() 41 | spm(spac_lhs.T, names=names) 42 | plt.suptitle("Space-Filling LHS Design") 43 | plt.show() 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Abraham Lee 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /doc/basic_examples.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: A Simple Example 3 | 4 | .. _simple example: 5 | 6 | A Simple Example 7 | ================ 8 | 9 | Let's start with the main import:: 10 | 11 | >>> from mcerp import * # N, U, Gamma, Beta, correlate, etc. 12 | 13 | Now, we can construct many kinds of statistical distributions (both 14 | continuous and discrete). Here's a basic example that involves a 15 | three-part assembly:: 16 | 17 | >>> x1 = N(24, 1) # normally distributed 18 | >>> x2 = N(37, 4) 19 | >>> x3 = Exp(2) # exponentially distributed 20 | 21 | The first four moments can be accessed at any time:: 22 | 23 | >>> x1.mean 24 | 24.000015983319528 25 | >>> x1.var 26 | 1.0001064993938098 27 | >>> x1.skew 28 | 0.00053245280109193898 29 | >>> x1.kurt 30 | 3.0017697746273813 31 | >>> x1.stats # this returns all four as a list 32 | 33 | Now we'll compute the actual stack-up using normal mathematics and see what 34 | happens:: 35 | 36 | >>> Z = (x1*x2**2)/(15*(1.5 + x3)) 37 | 38 | We can see how the statistics of each of these distributions propagated 39 | through the calculations in two basic ways: 40 | 41 | #. Telling python to print the object:: 42 | 43 | >>> Z # Explicit "print" command not necessary at the command-line 44 | uv(1161.35518507, 116688.945979, 0.353867228823, 3.00238273799) 45 | 46 | #. Using the ``describe`` class method (provides a more detailed explanation):: 47 | 48 | >>> Z.describe() 49 | MCERP Uncertain Value: 50 | > Mean................... 1161.35518507 51 | > Variance............... 116688.945979 52 | > Skewness Coefficient... 0.353867228823 53 | > Kurtosis Coefficient... 3.00238273799 54 | -------------------------------------------------------------------------------- /doc/changes.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | This is the changelog for ``mcerp``. 5 | You can find the releases at https://pypi.org/project/mcerp/ 6 | 7 | v0.12 (Jan 9, 2019) 8 | ------------------- 9 | 10 | - Python 3 support added (now ``mcerp`` works on Python 2.7 as well as 11 | Python 3.5 or later with a single codebase) 12 | - Several small issues fixed 13 | 14 | v0.11 (Jun 12, 2014) 15 | -------------------- 16 | 17 | (changelog wasn't filled, not clear what changed) 18 | 19 | v0.10 (Dec 10, 2013) 20 | -------------------- 21 | 22 | (changelog wasn't filled, not clear what changed) 23 | 24 | v0.9.2 (Aug 17, 2013) 25 | --------------------- 26 | 27 | - Fixed bug in 'umath.py' where the 'mcpts' member references should have been 28 | '_mcpts'. 29 | 30 | v0.9 (Jul 11, 2013) 31 | ------------------- 32 | 33 | - Removed dependencies on the 'ad' package since the core functionality didn't 34 | really depend on it. 35 | 36 | - Updated the umath sub-module to only utilize numpy for its mathematical 37 | functions (i.e., sin, exp, abs, etc.). 38 | 39 | - Added many constructor functions to make creating UncertainVariables easier, 40 | like 'Normal', 'Uniform', 'Gamma', etc. All examples have been updated 41 | accordingly to show the use of these constructors. The original constructor, 42 | like 'uv(ss.norm(loc=2, scale=0.1))', is still functional. 43 | 44 | - Updated the 'describe' method to allow the user to specify a printed 45 | identifier as an input other than the 'tag' member. 46 | 47 | - Added 'covariance_matrix' and 'correlation_matrix' utility functions. 48 | 49 | - Renamed display text from 'UF(...)' and 'UV(...)' to just 'uv(...)'. 50 | 51 | - Made the sample statistics permanent properties via the @property decorator. 52 | 53 | v0.8 (Jun 27, 2013) 54 | ------------------- 55 | 56 | - First public release. 57 | 58 | -------------------------------------------------------------------------------- /doc/view_distribution.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: Viewing Distributions 3 | 4 | .. _viewing the distribution: 5 | 6 | Viewing the distribution 7 | ------------------------ 8 | 9 | Any variable created using ``mcerp`` can be visualized using the ``plot`` 10 | class method, which has the following syntax:: 11 | 12 | >>> x1.plot([hist=False], **kwargs) 13 | 14 | At the main import (i.e., ``from mcerp import *``), we make available 15 | the matplotlib.pyplot sub-module available through the object ``plt``, 16 | which is required for later use since the ``plot`` function doesn't 17 | automatically display the graph. Thus, we might have the following:: 18 | 19 | >>> x1.plot() # No inputs shows the distribution's actual PDF/PMF 20 | >>> plt.show() # explicit 'show()' required to display to screen 21 | 22 | .. image:: _static/x1.png 23 | :scale: 60% 24 | 25 | As we can see, the default title is the information about the 26 | distribution object and the plot axes is scaled to fit well within the 27 | plot window. 28 | 29 | When plotting an object that is a result of prior calculations, since 30 | a mathematical PDF/PMF may not be realizable, we resort to approximating 31 | the distribution using a Kernel Density Estimate (KDE) of the data:: 32 | 33 | >>> Z.plot() 34 | >>> plt.show() 35 | 36 | .. image:: _static/Z_kde.png 37 | :scale: 60% 38 | 39 | By using the ``hist=True`` keyword in the plot function, we an see a 40 | histogram of the data instead of the KDE:: 41 | 42 | >>> Z.plot(hist=True) # shows a histogram instead of a KDE 43 | >>> plt.show() 44 | 45 | .. image:: _static/Z_hist.png 46 | :scale: 60% 47 | 48 | Or since showing the plot is explicit, we can do both:: 49 | 50 | >>> Z.plot() 51 | >>> Z.plot(hist=True) 52 | >>> plt.show() 53 | 54 | .. image:: _static/Z_kde_hist.png 55 | :scale: 60% 56 | 57 | Using Matplotlib's API, the plot can be customized to the full extent since 58 | everything is done before the plot is displayed. -------------------------------------------------------------------------------- /mcerp/tests/test_core.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .. import N, Exp, Gamma, Chi2 3 | import mcerp.umath as umath 4 | 5 | 6 | def test_three_part_assembly(): 7 | """Example of a three part assembly""" 8 | x1 = N(24, 1) 9 | x2 = N(37, 4) 10 | x3 = Exp(2) # Exp(mu=0.5) is the same 11 | Z = (x1 * x2 ** 2) / (15 * (1.5 + x3)) 12 | Z.describe() 13 | 14 | 15 | def test_volumetric_gas_flow(): 16 | """Example of volumetric gas flow through orifice meter""" 17 | H = N(64, 0.5) 18 | M = N(16, 0.1) 19 | P = N(361, 2) 20 | t = N(165, 0.5) 21 | C = 38.4 22 | Q = C * umath.sqrt((520 * H * P) / (M * (t + 460))) 23 | Q.describe() 24 | 25 | 26 | def test_manufacturing_tolerance_stackup(): 27 | """Example of manufacturing tolerance stackup""" 28 | # for a gamma distribution we need the following conversions: 29 | # shape = mean**2/var 30 | # scale = var/mean 31 | mn = 1.5 32 | vr = 0.25 33 | k = mn ** 2 / vr 34 | theta = vr / mn 35 | x = Gamma(k, theta) 36 | y = Gamma(k, theta) 37 | z = Gamma(k, theta) 38 | w = x + y + z 39 | w.describe() 40 | 41 | 42 | def test_scheduling(): 43 | """Example of scheduling facilities (six stations)""" 44 | s1 = N(10, 1) 45 | s2 = N(20, 2 ** 0.5) 46 | mn1 = 1.5 47 | vr1 = 0.25 48 | k1 = mn1 ** 2 / vr1 49 | theta1 = vr1 / mn1 50 | s3 = Gamma(k1, theta1) 51 | mn2 = 10 52 | vr2 = 10 53 | k2 = mn2 ** 2 / vr2 54 | theta2 = vr2 / mn2 55 | s4 = Gamma(k2, theta2) 56 | s5 = Exp(5) # Exp(mu=0.2) is the same 57 | s6 = Chi2(10) 58 | T = s1 + s2 + s3 + s4 + s5 + s6 59 | T.describe() 60 | 61 | 62 | def test_two_bar_truss_analysis(): 63 | """Example of two-bar truss stress/deflection analysis""" 64 | H = N(30, 5 / 3.0, tag="H") 65 | B = N(60, 0.5 / 3.0, tag="B") 66 | d = N(3, 0.1 / 3, tag="d") 67 | t = N(0.15, 0.01 / 3, tag="t") 68 | E = N(30000, 1500 / 3.0, tag="E") 69 | rho = N(0.3, 0.01 / 3.0, tag="rho") 70 | P = N(66, 3 / 3.0, tag="P") 71 | pi = np.pi 72 | wght = 2 * pi * rho * d * t * umath.sqrt((B / 2) ** 2 + H ** 2) 73 | strs = (P * umath.sqrt((B / 2) ** 2 + H ** 2)) / (2 * pi * d * t * H) 74 | buck = (pi ** 2 * E * (d ** 2 + t ** 2)) / (8 * ((B / 2) ** 2 + H ** 2)) 75 | defl = (P * ((B / 2) ** 2 + H ** 2) ** (1.5)) / (2 * pi * d * t * H ** 2 * E) 76 | print(wght.describe("wght")) 77 | print(strs.describe("strs")) 78 | print(buck.describe("buck")) 79 | print(defl.describe("defl")) 80 | -------------------------------------------------------------------------------- /doc/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | This is an open source project on Github: https://github.com/tisimst/mcerp 5 | 6 | Contributions are very welcome, feel free to file an issue with a suggestion, 7 | or send a pull request with an improvement any time. 8 | 9 | We don't have a mailing list of other means of communication, so if you have 10 | any question, please just open a Github issue and ask there. 11 | 12 | Develop 13 | ------- 14 | 15 | To work on ``mcerp``, first get the latest version:: 16 | 17 | git clone https://github.com/tisimst/mcerp.git 18 | cd mcerp 19 | 20 | The files to edit are here: 21 | 22 | - Code in ``mcerp`` 23 | - Tests in ``mcerp/tests`` 24 | - Documentation in ``doc`` 25 | 26 | Install 27 | ------- 28 | 29 | To hack on ``mcerp``, you need to have a development environment 30 | with all packages and tools installed. 31 | 32 | If you're using ``conda``, it's easy to create a development environment:: 33 | 34 | conda env create -f environment.yml 35 | conda activate mcerp 36 | 37 | With the conda environment active, run this command:: 38 | 39 | pip install -e . 40 | 41 | This installs ``mcerp`` in editable mode, meaning a pointer 42 | is put in your site-packages to the current source folder, so 43 | that after editing the code you only have to re-start python 44 | and re-run to get this new version, and not run an install command again. 45 | 46 | Tests 47 | ----- 48 | 49 | Run all tests:: 50 | 51 | pytest -v 52 | 53 | Run tests and check coverage:: 54 | 55 | pytest -v --cov=mcerp --cov-report=html 56 | open htmlcov/index.html 57 | 58 | Code style 59 | ---------- 60 | 61 | We use the `black`_ code style. To apply it in-place to all files:: 62 | 63 | black mcerp readme_code.py setup.py 64 | 65 | Docs 66 | ---- 67 | 68 | To build the docs:: 69 | 70 | cd docs 71 | make clean && make html 72 | open _build/html/index.html 73 | 74 | Then for any other tasks go back to the top level of the package:: 75 | 76 | cd .. 77 | 78 | Release 79 | ------- 80 | 81 | To make a release for this package, follow the following steps 82 | 83 | #. check that the tests and docs build are OK 84 | #. check via ``git tag`` or on Github or PyPI what the next version should be 85 | #. ``git clean -fdx`` 86 | #. ``git tag vX.Y`` (substitute actual version number here and in the following steps) 87 | #. ``python setup.py build sdist`` 88 | #. check the package in ``dist`` (should automate somehow) 89 | #. ``twine upload dist/*`` 90 | #. ``git push --tags`` 91 | 92 | .. _black: https://black.readthedocs.io 93 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | 9 | readme = "README.rst" 10 | 11 | setup( 12 | name="mcerp", 13 | author="Abraham Lee", 14 | description="Real-time latin-hypercube-sampling-based Monte Carlo Error Propagation", 15 | author_email="tisimst@gmail.com", 16 | url="https://github.com/tisimst/mcerp", 17 | license="BSD License", 18 | long_description=read(readme), 19 | package_data={"": [readme]}, 20 | packages=["mcerp"], 21 | use_scm_version=True, 22 | setup_requires=["setuptools_scm"], 23 | install_requires=["numpy", "scipy", "matplotlib"], 24 | keywords=[ 25 | "monte carlo", 26 | "latin hypercube", 27 | "sampling calculator", 28 | "error propagation", 29 | "uncertainty", 30 | "risk analysis", 31 | "error", 32 | "real-time", 33 | ], 34 | classifiers=[ 35 | "Development Status :: 5 - Production/Stable", 36 | "Environment :: Console", 37 | "Environment :: MacOS X", 38 | "Environment :: Win32 (MS Windows)", 39 | "Environment :: X11 Applications", 40 | "Intended Audience :: Customer Service", 41 | "Intended Audience :: Developers", 42 | "Intended Audience :: Education", 43 | "Intended Audience :: Financial and Insurance Industry", 44 | "Intended Audience :: Healthcare Industry", 45 | "Intended Audience :: Manufacturing", 46 | "Intended Audience :: Other Audience", 47 | "Intended Audience :: Science/Research", 48 | "License :: OSI Approved :: BSD License", 49 | "Natural Language :: English", 50 | "Operating System :: MacOS", 51 | "Operating System :: Microsoft", 52 | "Operating System :: Microsoft :: Windows", 53 | "Operating System :: OS Independent", 54 | "Operating System :: POSIX", 55 | "Operating System :: Unix", 56 | "Programming Language :: Python", 57 | "Programming Language :: Python :: 2.7", 58 | "Programming Language :: Python :: 3", 59 | "Programming Language :: Python :: 3.5", 60 | "Programming Language :: Python :: 3.6", 61 | "Programming Language :: Python :: 3.7", 62 | "Topic :: Education", 63 | "Topic :: Scientific/Engineering", 64 | "Topic :: Scientific/Engineering :: Chemistry", 65 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 66 | "Topic :: Scientific/Engineering :: Information Analysis", 67 | "Topic :: Scientific/Engineering :: Mathematics", 68 | "Topic :: Scientific/Engineering :: Physics", 69 | "Topic :: Software Development", 70 | "Topic :: Software Development :: Libraries", 71 | "Topic :: Software Development :: Libraries :: Python Modules", 72 | "Topic :: Utilities", 73 | ], 74 | ) 75 | -------------------------------------------------------------------------------- /mcerp/stats.py: -------------------------------------------------------------------------------- 1 | """ 2 | =============================================================================== 3 | mcerp: Real-time latin-hypercube-sampling-based Monte Carlo Error Propagation 4 | =============================================================================== 5 | 6 | Generalizes many statistical functions that work on numeric objects (from the 7 | scipy.stats module) to be compatible with objects defined by statistical 8 | distributions. 9 | 10 | NOTE: Although all of these functions can be used without this import, this 11 | package was created for convenience and transparent operation. For usage, 12 | see the respective documentation at 13 | 14 | http://docs.scipy.org/doc/scipy/reference/stats.html#statistical-functions 15 | 16 | Author: Abraham Lee 17 | Copyright: 2013 18 | """ 19 | from mcerp import UncertainFunction 20 | import scipy.stats as ss 21 | 22 | __author__ = "Abraham Lee" 23 | 24 | 25 | def wrap(func): 26 | def wrappedfunc(*args, **kwargs): 27 | """ 28 | Wraps a Scipy.Stats (or any) function, checking for MCERP objects 29 | as non-keyword arguments 30 | """ 31 | tmpargs = [] 32 | for arg in args: 33 | if isinstance(arg, UncertainFunction): 34 | tmpargs += [arg._mcpts] 35 | else: 36 | tmpargs += [arg] 37 | args = tuple(tmpargs) 38 | 39 | return func(*args, **kwargs) 40 | 41 | wrappedfunc.__name__ = func.__name__ 42 | wrappedfunc.__doc__ = func.__doc__ 43 | 44 | return wrappedfunc 45 | 46 | 47 | describe = wrap(ss.describe) 48 | gmean = wrap(ss.gmean) 49 | hmean = wrap(ss.hmean) 50 | kurtosis = wrap(ss.kurtosis) 51 | kurtosistest = wrap(ss.kurtosistest) 52 | mode = wrap(ss.mode) 53 | moment = wrap(ss.moment) 54 | normaltest = wrap(ss.normaltest) 55 | skew = wrap(ss.skew) 56 | skewtest = wrap(ss.skewtest) 57 | tmean = wrap(ss.tmean) 58 | tvar = wrap(ss.tvar) 59 | tmin = wrap(ss.tmin) 60 | tmax = wrap(ss.tmax) 61 | tstd = wrap(ss.tstd) 62 | tsem = wrap(ss.tsem) 63 | variation = wrap(ss.variation) 64 | percentileofscore = wrap(ss.percentileofscore) 65 | scoreatpercentile = wrap(ss.scoreatpercentile) 66 | bayes_mvs = wrap(ss.bayes_mvs) 67 | sem = wrap(ss.sem) 68 | zmap = wrap(ss.zmap) 69 | zscore = wrap(ss.zscore) 70 | f_oneway = wrap(ss.f_oneway) 71 | pearsonr = wrap(ss.pearsonr) 72 | spearmanr = wrap(ss.spearmanr) 73 | pointbiserialr = wrap(ss.pointbiserialr) 74 | kendalltau = wrap(ss.kendalltau) 75 | linregress = wrap(ss.linregress) 76 | ttest_1samp = wrap(ss.ttest_1samp) 77 | ttest_ind = wrap(ss.ttest_ind) 78 | ttest_rel = wrap(ss.ttest_rel) 79 | kstest = wrap(ss.kstest) 80 | chisquare = wrap(ss.chisquare) 81 | ks_2samp = wrap(ss.ks_2samp) 82 | mannwhitneyu = wrap(ss.mannwhitneyu) 83 | rankdata = wrap(ss.rankdata) 84 | ranksums = wrap(ss.ranksums) 85 | wilcoxon = wrap(ss.wilcoxon) 86 | kruskal = wrap(ss.kruskal) 87 | friedmanchisquare = wrap(ss.friedmanchisquare) 88 | ansari = wrap(ss.ansari) 89 | bartlett = wrap(ss.bartlett) 90 | levene = wrap(ss.levene) 91 | shapiro = wrap(ss.shapiro) 92 | anderson = wrap(ss.anderson) 93 | binom_test = wrap(ss.binom_test) 94 | fligner = wrap(ss.fligner) 95 | mood = wrap(ss.mood) 96 | -------------------------------------------------------------------------------- /doc/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block rootrellink %} 4 | {{ toctree() }} 5 | {% endblock %} 6 | 7 | 8 | {% block relbar1 %} 9 | 10 |
11 | ad 13 |
14 | {{ super() }} 15 | {% endblock %} 16 | 17 | 18 | {#############################################################################} 19 | {# Sidebar customization #) 20 | 21 | {# put the sidebar before the body #} 22 | {% block sidebar1 %} 23 | 24 | {%- macro sidebar() %} 25 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 26 |
27 |
28 | {%- block sidebarlogo %} 29 | {%- if logo %} 30 | 33 | {%- endif %} 34 | {%- endblock %} 35 | {%- block sidebartoc %} 36 | {%- block sidebarglobaltoc %} 37 |

{{ _('Table of contents') }}

38 | {{ toctree() }} 39 | {%- endblock %} 40 | {%- endblock %} 41 | {%- block sidebarrel %} 42 | {%- endblock %} 43 | {%- block sidebarsourcelink %} 44 | {%- if show_source and has_source and sourcename %} 45 |

{{ _('This Page') }}

46 | 50 | {%- endif %} 51 | {%- endblock %} 52 | {%- if customsidebar %} 53 | {% include customsidebar %} 54 | {%- endif %} 55 | {%- if display_toc %} 56 |

{{ _('Section contents') }}

57 | {{ toc }} 58 | {%- endif %} 59 | {%- block sidebarsearch %} 60 | {%- if pagename != "search" %} 61 | 73 | 74 | {%- endif %} 75 | {%- endblock %} 76 | {%- block copyright %} 77 |

Documentation license

78 | Creative Commons License 79 | {%- endblock %} 80 |
81 |
82 | {%- endif %}{% endif %} 83 | {%- endmacro %} 84 | 85 | {{ sidebar() }}{% endblock %} 86 | 87 | 88 | {% block sidebar2 %}{% endblock %} 89 | 90 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp changes linkcheck doctest # latex 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | # @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | # The HTML needs latex because the PDF version is linked to: 33 | html: # latex 34 | # (cd _build/latex; make all-pdf) 35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 36 | @echo 37 | @echo "Build finished. The HTML pages are in _build/html." 38 | 39 | dirhtml: 40 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 41 | @echo 42 | @echo "Build finished. The HTML pages are in _build/dirhtml." 43 | 44 | pickle: 45 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 46 | @echo 47 | @echo "Build finished; now you can process the pickle files." 48 | 49 | json: 50 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 51 | @echo 52 | @echo "Build finished; now you can process the JSON files." 53 | 54 | htmlhelp: 55 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 56 | @echo 57 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 58 | ".hhp project file in _build/htmlhelp." 59 | 60 | qthelp: 61 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 62 | @echo 63 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 64 | ".qhcp project file in _build/qthelp, like this:" 65 | @echo "# qcollectiongenerator _build/qthelp/uncertaintiesPythonpackage.qhcp" 66 | @echo "To view the help file:" 67 | @echo "# assistant -collectionFile _build/qthelp/uncertaintiesPythonpackage.qhc" 68 | 69 | # latex: 70 | # $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 71 | # @echo 72 | # @echo "Build finished; the LaTeX files are in _build/latex." 73 | # @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 74 | # "run these through (pdf)latex." 75 | 76 | changes: 77 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 78 | @echo 79 | @echo "The overview file is in _build/changes." 80 | 81 | linkcheck: 82 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 83 | @echo 84 | @echo "Link check complete; look for any errors in the above output " \ 85 | "or in _build/linkcheck/output.txt." 86 | 87 | doctest: 88 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 89 | @echo "Testing of doctests in the sources finished, look at the " \ 90 | "results in _build/doctest/output.txt." 91 | -------------------------------------------------------------------------------- /doc/correlations.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: Handling Correlations 3 | 4 | .. _correlations: 5 | 6 | Correlations 7 | ------------ 8 | 9 | .. index:: correlate 10 | 11 | By default, the samples try to be as uncorrelated and independent as 12 | possible from any other inputs. However, sometimes inputs to have some 13 | degree of correlation between them. If it is desired to force a set of 14 | variables to have certain correlations, which is not uncommon in 15 | real-life situations, this can be done with the ``correlate`` function 16 | (NOTE: this should be done BEFORE any calculations have taken place in 17 | order to work properly). 18 | 19 | For example, let's look at our :ref:`simple example` with inputs 20 | ``x1``, ``x2``, and ``x3``:: 21 | 22 | # The correlation coefficients before adjusting 23 | >>> print correlation_matrix([x1, x2, x3]) 24 | [[ 1. 0.00558381 0.01268168] 25 | [ 0.00558381 1. 0.00250815] 26 | [ 0.01268168 0.00250815 1. ]] 27 | 28 | You'll notice a few things about the correlation matrix. First, the 29 | diagonals are all 1.0 (they always are). Second, the matrix is symmetric. 30 | Third, the correlation coefficients in the upper and lower triangular 31 | parts are relatively small. This is how ``mcerp`` is designed. 32 | 33 | .. index:: plotcorr 34 | 35 | Here is what the actual samples looks like in a matrix plot form (created 36 | using ``plotcorr([x1, x2, x3], labels=['x1', 'x2', 'x3'])``): 37 | 38 | .. image:: _static/before_correlation_matrixplot.png 39 | :scale: 60% 40 | 41 | Now, let's say we desire to impose a -0.75 correlation between ``x1`` 42 | and ``x2``. Let's create the desired correlation matrix (note that all 43 | diagonal elements should be 1.0):: 44 | 45 | # The desired correlation coefficient matrix 46 | >>> c = np.array([[ 1.0, -0.75, 0.0], 47 | ... [-0.75, 1.0, 0.0], 48 | ... [ 0.0, 0.0, 1.0]]) 49 | 50 | Using the ``mcerp.correlate`` function, we can now apply the desired 51 | correlation coefficients to our samples to try and force the inputs 52 | to be correlated:: 53 | 54 | # Apply the correlations into the samples (works in-place) 55 | >>> correlate([x1, x2, x3], c) 56 | 57 | # Show the new correlation coefficients 58 | >>> print correlation_matrix([x1, x2, x3]) 59 | [[ 1.00000000e+00 -7.50010477e-01 1.87057576e-03] 60 | [ -7.50010477e-01 1.00000000e+00 8.53061774e-04] 61 | [ 1.87057576e-03 8.53061774e-04 1.00000000e+00]] 62 | 63 | The correlation matrix is roughly what we expected within a few percent. 64 | Even the other correlation coefficients are closer to zero than before. If 65 | any exceptions appear during this operation, it is likely because the 66 | correlation matrix is either not **symmetric**, not **positive-definite**, 67 | or both. 68 | 69 | The newly correlated samples will now look something like: 70 | 71 | .. image:: _static/after_correlation_matrixplot.png 72 | :scale: 60% 73 | 74 | .. note: This correlation operation doesn't change any of the original sampled 75 | values, it simply re-organizes them in such a way that they closely 76 | match the desired correlations. 77 | 78 | Now that the inputs' relations have been modified, let's check how 79 | the output of our stack-up has changed (sometimes the correlations 80 | won't change the output much, but others can change a lot!):: 81 | 82 | # Z should now be a little different 83 | >>> Z = (x1*x2**2)/(15*(1.5 + x3)) 84 | >>> Z.describe 85 | MCERP Uncertain Value: 86 | > Mean................... 1153.710442 87 | > Variance............... 97123.3417748 88 | > Skewness Coefficient... 0.211835225063 89 | > Kurtosis Coefficient... 2.87618465139 90 | 91 | We can also see what adding that correlation did: reduced the mean, 92 | reduced the variance, increased the skewness, increased the kurtosis. 93 | 94 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | ``mcerp`` Python Package 3 | ======================== 4 | 5 | .. image:: https://dev.azure.com/tisimst/tisimst/_apis/build/status/tisimst.mcerp 6 | :target: https://dev.azure.com/tisimst/tisimst/_build/latest?definitionId=1 7 | :alt: Build Status 8 | 9 | - Code: https://github.com/tisimst/mcerp 10 | - Documentation: (not online yet, for now, see the doc folder on Github) 11 | - License: BSD-3-Clause 12 | 13 | Overview 14 | ======== 15 | 16 | ``mcerp`` is a stochastic calculator for `Monte Carlo methods`_ that uses 17 | `latin-hypercube sampling`_ to perform non-order specific 18 | `error propagation`_ (or uncertainty analysis). 19 | 20 | With this package you can **easily** and **transparently** track the effects 21 | of uncertainty through mathematical calculations. Advanced mathematical 22 | functions, similar to those in the standard `math`_ module, and statistical 23 | functions like those in the `scipy.stats`_ module, can also be evaluated 24 | directly. 25 | 26 | If you are familiar with Excel-based risk analysis programs like *@Risk*, 27 | *Crystal Ball*, *ModelRisk*, etc., this package **will work wonders** for you 28 | (and probably even be faster!) and give you more modelling flexibility with 29 | the powerful Python language. This package also *doesn't cost a penny*, 30 | compared to those commercial packages which cost *thousands of dollars* for a 31 | single-seat license. Feel free to copy and redistribute this package as much 32 | as you desire! 33 | 34 | Main Features 35 | ============= 36 | 37 | 1. **Transparent calculations**. **No or little modification** to existing 38 | code required. 39 | 40 | 2. Basic `NumPy`_ support without modification. (I haven't done extensive 41 | testing, so please let me know if you encounter bugs.) 42 | 43 | 3. Advanced mathematical functions supported through the ``mcerp.umath`` 44 | sub-module. If you think a function is in there, it probably is. If it 45 | isn't, please request it! 46 | 47 | 4. **Easy statistical distribution constructors**. The location, scale, 48 | and shape parameters follow the notation in the respective Wikipedia 49 | articles and other relevant web pages. 50 | 51 | 5. **Correlation enforcement** and variable sample visualization capabilities. 52 | 53 | 6. **Probability calculations** using conventional comparison operators. 54 | 55 | 7. Advanced Scipy **statistical function compatibility** with package 56 | functions. Depending on your version of Scipy, some functions might not 57 | work. 58 | 59 | Installation 60 | ============ 61 | 62 | ``mcerp`` works on Linux, MacOS and Windows, with Python 2.7 or with Python 3.5 or later. 63 | 64 | To install it, use ``pip``:: 65 | 66 | pip install mcerp 67 | 68 | The ``mcerp`` dependencies should be installed automatically if using ``pip``, 69 | otherwise they will need to be installed manually: 70 | 71 | - `NumPy`_ : Numeric Python 72 | - `SciPy`_ : Scientific Python 73 | - `Matplotlib`_ : Python plotting library 74 | 75 | See also 76 | ======== 77 | 78 | - `uncertainties`_ : First-order error propagation 79 | - `soerp`_ : Second-order error propagation 80 | 81 | Contact 82 | ======= 83 | 84 | Please send **feature requests, bug reports, or feedback** to 85 | `Abraham Lee`_. 86 | 87 | .. _Monte Carlo methods: http://en.wikipedia.org/wiki/Monte_Carlo_method 88 | .. _latin-hypercube sampling: http://en.wikipedia.org/wiki/Latin_hypercube_sampling 89 | .. _soerp: http://pypi.python.org/pypi/soerp 90 | .. _error propagation: http://en.wikipedia.org/wiki/Propagation_of_uncertainty 91 | .. _math: http://docs.python.org/library/math.html 92 | .. _NumPy: http://www.numpy.org/ 93 | .. _SciPy: http://scipy.org 94 | .. _Matplotlib: http://matplotlib.org/ 95 | .. _scipy.stats: http://docs.scipy.org/doc/scipy/reference/stats.html 96 | .. _uncertainties: http://pypi.python.org/pypi/uncertainties 97 | .. _source code: https://github.com/tisimst/mcerp 98 | .. _Abraham Lee: mailto:tisimst@gmail.com 99 | .. _package documentation: http://pythonhosted.org/mcerp 100 | .. _GitHub: http://github.com/tisimst/mcerp 101 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index: Overview 3 | 4 | =============================== 5 | ``mcerp`` Package Documentation 6 | =============================== 7 | 8 | Overview 9 | ======== 10 | 11 | ``mcerp`` is a stochastic calculator for `Monte Carlo methods`_ that uses 12 | `latin-hypercube sampling`_ to perform non-order specific 13 | `error propagation`_ (or uncertainty analysis). With this package you can 14 | **easily** and **transparently** track the effects of uncertainty 15 | through mathematical calculations. Advanced mathematical functions, similar 16 | to those in the standard `math`_ module can also be evaluated directly. 17 | 18 | Due to the nature of random sampling techniques, calculation results will vary 19 | from session to session (but be consistent within the session) since new 20 | samples are only generated when variables are newly defined or re-defined. 21 | By default, each variable uses 10,000 samples that are sufficiently random. 22 | The number of samples can be changed by assigning an integer value to the 23 | ``mcerp.npts`` object (typically, values between 1,000 and 1,000,000 are 24 | sufficiently large to ensure small errors in the resulting statistics). 25 | This should only be changed prior to performing calculations since 26 | all subsequent calculations assume that each input has the same number of 27 | sampled points (this could be changed through resampling, I suppose...). 28 | 29 | In order to correctly use ``mcerp``, knowledge of the distributions from 30 | the ``scipy.stats`` sub-module is helpful, but not required since many 31 | of the most common distributions can be created with convenient functions 32 | (see table below). 33 | The result of all calculations generates a *mean*, *variance*, and 34 | *standardized skewness and kurtosis* coefficients (this means that a 35 | *Normal* distribution has a kurtosis of 3, **NOT** 0). 36 | 37 | Main Features 38 | ============= 39 | 40 | 1. **Transparent calculations**. **No or little modification** to existing 41 | code required. 42 | 43 | 2. Basic `NumPy`_ support without modification. (I haven't done extensive 44 | testing, so please let me know if you encounter bugs.) 45 | 46 | 3. Advanced mathematical functions supported through the ``mcerp.umath`` 47 | sub-module. If you think a function is in there, it probably is. If it 48 | isn't, please request it! 49 | 50 | 4. **Easy statistical distribution constructors**. The location, scale, 51 | and shape parameters follow the notation in the respective Wikipedia 52 | articles. 53 | 54 | 5. **Correlation enforcement** and variable sample visualization capabilities. 55 | 56 | 6. **Probability calculations** using conventional comparison operators. 57 | 58 | 7. Advanced Scipy **statistical function compatibility** with package 59 | functions. Depending on your version of Scipy, some functions might not 60 | work. 61 | 62 | Installation 63 | ============ 64 | 65 | ``mcerp`` works on Linux, MacOS and Windows, with Python 2.7 or with Python 3.5 or later. 66 | 67 | To install it, use ``pip``:: 68 | 69 | pip install mcerp 70 | 71 | The ``mcerp`` dependencies should be installed automatically if using ``pip``, 72 | otherwise they will need to be installed manually: 73 | 74 | - `NumPy`_ : Numeric Python 75 | - `SciPy`_ : Scientific Python 76 | - `Matplotlib`_ : Python plotting library 77 | 78 | See also 79 | ======== 80 | 81 | - `uncertainties`_ : First-order error propagation 82 | - `soerp`_ : Second-order error propagation 83 | 84 | Contact 85 | ======= 86 | 87 | Please send **feature requests, bug reports, or feedback** to 88 | `Abraham Lee`_. 89 | 90 | .. _Monte Carlo methods: http://en.wikipedia.org/wiki/Monte_Carlo_method 91 | .. _latin-hypercube sampling: http://en.wikipedia.org/wiki/Latin_hypercube_sampling 92 | .. _soerp: http://pypi.python.org/pypi/soerp 93 | .. _error propagation: http://en.wikipedia.org/wiki/Propagation_of_uncertainty 94 | .. _math: http://docs.python.org/library/math.html 95 | .. _NumPy: http://www.numpy.org/ 96 | .. _SciPy: http://scipy.org 97 | .. _Matplotlib: http://matplotlib.org/ 98 | .. _scipy.stats: http://docs.scipy.org/doc/scipy/reference/stats.html 99 | .. _uncertainties: http://pypi.python.org/pypi/uncertainties 100 | .. _source code: https://github.com/tisimst/mcerp 101 | .. _Abraham Lee: mailto:tisimst@gmail.com 102 | -------------------------------------------------------------------------------- /doc/probabilities.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: Probabilities 3 | 4 | .. _probabilities: 5 | 6 | Probabilities 7 | ------------- 8 | 9 | To estimate the fraction of the distribution that lies above or 10 | below some point in a distribution, we can use the standard comparison 11 | operators (<, <=, >, >=, ==, !=):: 12 | 13 | # What is the percentage of samples below 21? 14 | >>> x1<21 15 | 0.0014 (i.e., about 0.1%) 16 | 17 | # What percentage of samples are 1000 and above? 18 | >>> Z>=1000 19 | 0.6622 (i.e., about 66%) 20 | 21 | On the otherhand, if we are comparing distributions to see if one is 22 | "less" or "more" than the other, we actually perform a T-test of the two 23 | objects to compare the two sample means. If the p-value is greater than 24 | 0.05 AND the t-statistic has the correct sign, then the comparison will 25 | return ``True``. Let's first create some new samples (the actual values 26 | are contained in the ``_mcpts`` member of the ``UncertainFunction`` class:: 27 | 28 | >>> rvs1 = N(5, 10) 29 | >>> rvs2 = N(5, 10) + N(0, 0.2) 30 | >>> rvs3 = N(8, 10) + N(0, 0.2) 31 | 32 | Now, let's compare ``rvs1`` and ``rvs2``. They are similar, but with slightly 33 | different variances, so we would expect the p-value to be large:: 34 | 35 | >>> from scipy.stats import ttest_rel 36 | >>> tstat, pval = ttest_rel(rvs1._mcpts, rvs2._mcpts) 37 | >>> pval 38 | 0.99888340212679583 39 | 40 | As expected, because the p-value is essentially, 1.0, the test couldn't tell 41 | them apart, so our comparison returns:: 42 | 43 | >>> rvs1>> tstat, pval = ttest_rel(rvs1._mcpts, rvs3._mcpts) 51 | >>> pval 52 | 3.0480674044727307e-97 53 | 54 | That's a very small p-value, indicating that the distributions are 55 | separated from each other distinctly enough that the test could tell them 56 | apart. Now we need to check the sign of the t-statistic to see if 57 | ``rvs1`` is on the "left" of ``rvs3`` for the comparison:: 58 | 59 | >>> float(tstat) 60 | -21.158661004433682 61 | 62 | Because we are using the *less than* comparison and the sign of the 63 | t-statistic is negative, then we say that this is "oriented" correctly 64 | and, no surprise, we get:: 65 | 66 | >>> rvs1>> x = N(0, 1) 76 | >>> y = N(0, 10) 77 | >>> x>> x>y 80 | False 81 | >>> x==y 82 | False 83 | 84 | The equality comparison operators (== and !=) actually test to see if 85 | the distributions are identical, thus:: 86 | 87 | >>> x1==x1 88 | True 89 | 90 | >>> n1 = N(0, 1) 91 | >>> n2 = N(0, 1) 92 | >>> n1==n2 # n1 and n2 are independently sampled, so they are not equal 93 | False 94 | 95 | >>> Z*Z==Z**2 # Both sides have the same root samples, so they are equal 96 | True 97 | 98 | If an MCERP object is compared to a scalar value, then a sampled probability 99 | is calculated. For example, let's say we have a 45 black marbles, 5 white 100 | marbles, and we are going to put them all into a hat and pick out 10. What's 101 | the probability that 4 of the ten will be white? Let's see:: 102 | 103 | >>> h = H(50, 5, 10) 104 | >>> h==4 105 | 0.004 # the precise answer is 0.0039..., so not bad. 106 | 107 | What's the probability we will get three or less (that includes 0):: 108 | 109 | >>> h<=3 110 | 0.9959 111 | 112 | For MCERP objects that represent continuous distributions, we see that any 113 | equality operators (usually) return a probability of zero:: 114 | 115 | >>> n = N(0, 1) 116 | >>> n==0 117 | 0.0 118 | >>> n==0.5 119 | 0.0 120 | >>> n==1.2345 121 | 0.0 122 | 123 | But inequality operators are more useful:: 124 | 125 | >>> n<0 126 | 0.5 127 | >>> n<1.5 128 | 0.9332 # actual is 0.9331927, so not to far off with 10000 samples! 129 | -------------------------------------------------------------------------------- /doc/_static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 61 | 64 | m 76 | c 88 | 89 | 92 | er 104 | p 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /doc/advanced_example.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: Advanced Examples 3 | 4 | .. _advanced example: 5 | 6 | Advanced Examples 7 | ================= 8 | 9 | In this section, some more advanced/complex usages will be presented. 10 | 11 | Volumetric Gas Flow Through an Orifice Meter 12 | -------------------------------------------- 13 | 14 | Here's an interesting engineering example, showing how the 15 | random effects of input parameters propagates through the calculation of 16 | the volumetric gas flow through an orifice meter:: 17 | 18 | >>> import mcerp.umath as umath # sin, exp, sqrt, etc. 19 | >>> H = N(64, 0.5) 20 | >>> M = N(16, 0.1) 21 | >>> P = N(361, 2) 22 | >>> t = N(165, 0.5) 23 | >>> C = 38.4 24 | >>> Q = C*umath.sqrt((520*H*P)/(M*(t + 460))) 25 | >>> Q.describe() 26 | MCERP Uncertain Value: 27 | > Mean................... 1330.9997362 28 | > Variance............... 57.5497899824 29 | > Skewness Coefficient... 0.0229295468388 30 | > Kurtosis Coefficient... 2.99662898689 31 | 32 | Interestingly enough, even though the calculations involve multiplication, 33 | division, and a square-root, the result appears to be very close to a Normal 34 | distribution (Q ~ N(1331, 7.6)). 35 | 36 | Manufacturing Tolerance Stackup 37 | ------------------------------- 38 | 39 | In a certain manufacturing plant, a record of process data is taken and 40 | just happens to fit a Gamma distribution with a mean of 1.5 and a 41 | variance of 0.25. With an assembly of three of these parts, our analysis 42 | for tolerance stackup would proceed as follows. We first need to convert 43 | the distribution statistics to the shape parameters k and theta, which 44 | are found by the relations: 45 | 46 | * ``k = mean**2/variance = (1.5)**2/(0.25) = 9`` 47 | * ``theta = variance/mean = (0.25)/(1.5) = 1/6`` 48 | 49 | We can now proceed with the analysis:: 50 | 51 | 52 | >>> k = 9.0 53 | >>> theta = 1/6.0 54 | >>> x = Gamma(k, theta) 55 | >>> y = Gamma(k, theta) 56 | >>> z = Gamma(k, theta) 57 | >>> w = x + y + z 58 | >>> w.describe() 59 | MCERP Uncertain Value: 60 | > Mean................... 4.50000470462 61 | > Variance............... 0.76376726781 62 | > Skewness Coefficient... 0.368543723948 63 | > Kurtosis Coefficient... 3.18692837067 64 | 65 | Due to the skewed nature of the inputs, the output is also slightly skewed. 66 | 67 | Scheduling Facilities 68 | --------------------- 69 | 70 | At a manufacturing plant, that has six test-and-repair stations, data 71 | is collected about the amount of time that product is present at each 72 | station: 73 | 74 | 1. Station 1: Normally distributed, mean of 10 hours, variance of 75 | 1 hour. 76 | 2. Station 2: Normally distributed, mean of 20 hours, variance of 77 | 2 hours. 78 | 3. Station 3: Gamma distributed, mean of 1.5 hours, variance of 79 | 0.25 hours. 80 | 4. Station 4: Gamma distributed, mean of 10 hours, variance of 10 81 | hours. 82 | 5. Station 5: Exponentially distributed, mean of 0.2 hours, variance 83 | of 0.04 hours (decay constant lamda=5). 84 | 6. Station 6: Chi-squared distributed, mean of 10 hours, variance of 85 | 20 hours (degree of freedom v=10). 86 | 87 | Management wants to understand the uncertainty associated with the whole 88 | process:: 89 | 90 | # Station 1 91 | >>> s1 = N(10, 1) 92 | 93 | # Station 2 94 | >>> s2 = N(20, 2**0.5) 95 | 96 | # Station 3 97 | >>> mn1 = 1.5 98 | >>> vr1 = 0.25 99 | >>> k1 = mn1**2/vr1 100 | >>> theta1 = vr1/mn1 101 | >>> s3 = Gamma(k1, theta1) 102 | 103 | # Station 4 104 | >>> mn2 = 10 105 | >>> vr2 = 10 106 | >>> k2 = mn2**2/vr2 107 | >>> theta2 = vr2/mn2 108 | >>> s4 = Gamma(k2, theta2) 109 | 110 | # Station 5 111 | >>> s5 = Exp(5) 112 | 113 | # Station 6 114 | >>> s6 = Chi2(10) 115 | 116 | >>> T = s1 + s2 + s3 + s4 + s5 + s6 117 | >>> T.describe() 118 | MCERP Uncertain Value: 119 | > Mean................... 51.6999259156 120 | > Variance............... 33.6983675299 121 | > Skewness Coefficient... 0.520212339449 122 | > Kurtosis Coefficient... 3.52754453865 123 | 124 | From this analysis, we see that the average process time is about 51.7 hours, 125 | but the variance is quite large (standard deviation = 5.8 hours). This 126 | gives management the desire to understand which is the greatest contributors, 127 | so we can analyze the standard deviations of each process step:: 128 | 129 | >>> for i, si in enumerate([s1, s2, s3, s4, s5, s6]): 130 | ... print 'Station', i + 1, ':', si.std 131 | ... 132 | Station 1 : 0.9998880644 133 | Station 2 : 1.41409415266 134 | Station 3 : 0.499878358909 135 | Station 4 : 3.16243741632 136 | Station 5 : 0.199970343107 137 | Station 6 : 4.47143708522 138 | 139 | This would seem to indicate that management could focus their efforts on 140 | making the cycle times of stations 4 and 6 more consistent. 141 | 142 | It may also be useful to understand the probability that a complete cycle 143 | will exceed a certain amount, say at 59, 62 and 68 hours:: 144 | 145 | >>> prob = [T>hr for hr in [59, 62, 68]] 146 | >>> prob 147 | [0.1091, 0.0497, 0.0083] 148 | 149 | That is to say that it is expected that the entire process will take 59 150 | hours approximately 11% of the time, 62 hours 5% of the time, and 68 hours 151 | about 1% of the time. 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ad Python package documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jun 8 18:32:22 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | from datetime import date 15 | 16 | import sys 17 | import os 18 | 19 | sys.path.insert(0, os.path.abspath("..")) 20 | 21 | import mcerp 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # sys.path.append(os.path.abspath('.')) 27 | 28 | # -- General configuration ----------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = ".rst" 39 | 40 | # The encoding of source files. 41 | # source_encoding = 'utf-8' 42 | 43 | # The master toctree document. 44 | master_doc = "index_TOC" 45 | 46 | # General information about the project. 47 | project = u"mcerp Python package" 48 | if date.today().year != 2013: 49 | copyright = u"2013–%d, Abraham Lee" % date.today().year 50 | else: 51 | copyright = u"2013, Abraham Lee" 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = "1" 59 | # The full version, including alpha/beta/rc tags. 60 | release = mcerp.__version__ 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | # today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | # today_fmt = '%B %d, %Y' 71 | 72 | # List of documents that shouldn't be included in the build. 73 | # unused_docs = [] 74 | 75 | # List of directories, relative to source directory, that shouldn't be searched 76 | # for source files. 77 | exclude_trees = ["_build"] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | # default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | # add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | # add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | # show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = "sphinx" 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | # modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. Major themes that come with 103 | # Sphinx are currently 'default' and 'sphinxdoc'. 104 | html_theme = "sphinxdoc" 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | # html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | # html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | # html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | # html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | html_favicon = "_static/favicon.ico" 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ["_static"] 134 | 135 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 136 | # using the given strftime format. 137 | # html_last_updated_fmt = '%b %d, %Y' 138 | 139 | # If true, SmartyPants will be used to convert quotes and dashes to 140 | # typographically correct entities. 141 | # html_use_smartypants = True 142 | 143 | # Custom sidebar templates, maps document names to template names. 144 | # html_sidebars = {} 145 | 146 | # Additional templates that should be rendered to pages, maps page names to 147 | # template names. 148 | # html_additional_pages = {} 149 | 150 | # If false, no module index is generated. 151 | # html_use_modindex = True 152 | 153 | # If false, no index is generated. 154 | # html_use_index = True 155 | 156 | # If true, the index is split into individual pages for each letter. 157 | # html_split_index = False 158 | 159 | # If true, links to the reST sources are added to the pages. 160 | html_show_sourcelink = False 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | # html_use_opensearch = '' 166 | 167 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 168 | # html_file_suffix = '' 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = "mcerpPythonPackagedoc" 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | # The paper size ('letter' or 'a4'). 177 | # latex_paper_size = 'letter' 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | # latex_font_size = '10pt' 181 | 182 | # Grouping the document tree into LaTeX files. List of tuples 183 | # (source start file, target name, title, author, documentclass [howto/manual]). 184 | # latex_documents = [ 185 | # ('index_TOC', 'adPythonPackage.tex', u'ad Python package Documentation', 186 | # u'Abraham Lee', 'manual'), 187 | # ] 188 | 189 | # The name of an image file (relative to this directory) to place at the top of 190 | # the title page. 191 | # latex_logo = None 192 | 193 | # For "manual" documents, if this is true, then toplevel headings are parts, 194 | # not chapters. 195 | # latex_use_parts = False 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | # latex_preamble = '' 199 | 200 | # Documents to append as an appendix to all manuals. 201 | # latex_appendices = [] 202 | 203 | # If false, no module index is generated. 204 | # latex_use_modindex = True 205 | -------------------------------------------------------------------------------- /mcerp/umath.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================================================ 3 | mcerp: Real-time latin-hypercube-sampling-based Monte Carlo Error Propagation 4 | ================================================================================ 5 | 6 | Generalizes mathematical operators that work on numeric objects (from the math 7 | module or numpy) compatible with objects with uncertainty distributions 8 | 9 | Author: Abraham Lee 10 | Copyright: 2013 11 | """ 12 | from mcerp import UncertainFunction, to_uncertain_func 13 | import numpy as np 14 | 15 | __author__ = "Abraham Lee" 16 | 17 | 18 | def abs(x): 19 | """ 20 | Absolute value 21 | """ 22 | if isinstance(x, UncertainFunction): 23 | mcpts = np.abs(x._mcpts) 24 | return UncertainFunction(mcpts) 25 | else: 26 | return np.abs(x) 27 | 28 | 29 | def acos(x): 30 | """ 31 | Inverse cosine 32 | """ 33 | if isinstance(x, UncertainFunction): 34 | mcpts = np.arccos(x._mcpts) 35 | return UncertainFunction(mcpts) 36 | else: 37 | return np.arccos(x) 38 | 39 | 40 | def acosh(x): 41 | """ 42 | Inverse hyperbolic cosine 43 | """ 44 | if isinstance(x, UncertainFunction): 45 | mcpts = np.arccosh(x._mcpts) 46 | return UncertainFunction(mcpts) 47 | else: 48 | return np.arccosh(x) 49 | 50 | 51 | def asin(x): 52 | """ 53 | Inverse sine 54 | """ 55 | if isinstance(x, UncertainFunction): 56 | mcpts = np.arcsin(x._mcpts) 57 | return UncertainFunction(mcpts) 58 | else: 59 | return np.arcsin(x) 60 | 61 | 62 | def asinh(x): 63 | """ 64 | Inverse hyperbolic sine 65 | """ 66 | if isinstance(x, UncertainFunction): 67 | mcpts = np.arcsinh(x._mcpts) 68 | return UncertainFunction(mcpts) 69 | else: 70 | return np.arcsinh(x) 71 | 72 | 73 | def atan(x): 74 | """ 75 | Inverse tangent 76 | """ 77 | if isinstance(x, UncertainFunction): 78 | mcpts = np.arctan(x._mcpts) 79 | return UncertainFunction(mcpts) 80 | else: 81 | return np.arctan(x) 82 | 83 | 84 | def atanh(x): 85 | """ 86 | Inverse hyperbolic tangent 87 | """ 88 | if isinstance(x, UncertainFunction): 89 | mcpts = np.arctanh(x._mcpts) 90 | return UncertainFunction(mcpts) 91 | else: 92 | return np.arctanh(x) 93 | 94 | 95 | def ceil(x): 96 | """ 97 | Ceiling function (round towards positive infinity) 98 | """ 99 | if isinstance(x, UncertainFunction): 100 | mcpts = np.ceil(x._mcpts) 101 | return UncertainFunction(mcpts) 102 | else: 103 | return np.ceil(x) 104 | 105 | 106 | def cos(x): 107 | """ 108 | Cosine 109 | """ 110 | if isinstance(x, UncertainFunction): 111 | mcpts = np.cos(x._mcpts) 112 | return UncertainFunction(mcpts) 113 | else: 114 | return np.cos(x) 115 | 116 | 117 | def cosh(x): 118 | """ 119 | Hyperbolic cosine 120 | """ 121 | if isinstance(x, UncertainFunction): 122 | mcpts = np.cosh(x._mcpts) 123 | return UncertainFunction(mcpts) 124 | else: 125 | return np.cosh(x) 126 | 127 | 128 | def degrees(x): 129 | """ 130 | Convert radians to degrees 131 | """ 132 | if isinstance(x, UncertainFunction): 133 | mcpts = np.degrees(x._mcpts) 134 | return UncertainFunction(mcpts) 135 | else: 136 | return np.degrees(x) 137 | 138 | 139 | def exp(x): 140 | """ 141 | Exponential function 142 | """ 143 | if isinstance(x, UncertainFunction): 144 | mcpts = np.exp(x._mcpts) 145 | return UncertainFunction(mcpts) 146 | else: 147 | return np.exp(x) 148 | 149 | 150 | def expm1(x): 151 | """ 152 | Calculate exp(x) - 1 153 | """ 154 | if isinstance(x, UncertainFunction): 155 | mcpts = np.expm1(x._mcpts) 156 | return UncertainFunction(mcpts) 157 | else: 158 | return np.expm1(x) 159 | 160 | 161 | def fabs(x): 162 | """ 163 | Absolute value function 164 | """ 165 | if isinstance(x, UncertainFunction): 166 | mcpts = np.fabs(x._mcpts) 167 | return UncertainFunction(mcpts) 168 | else: 169 | return np.fabs(x) 170 | 171 | 172 | def floor(x): 173 | """ 174 | Floor function (round towards negative infinity) 175 | """ 176 | if isinstance(x, UncertainFunction): 177 | mcpts = np.floor(x._mcpts) 178 | return UncertainFunction(mcpts) 179 | else: 180 | return np.floor(x) 181 | 182 | 183 | def hypot(x, y): 184 | """ 185 | Calculate the hypotenuse given two "legs" of a right triangle 186 | """ 187 | if isinstance(x, UncertainFunction) or isinstance(x, UncertainFunction): 188 | ufx = to_uncertain_func(x) 189 | ufy = to_uncertain_func(y) 190 | mcpts = np.hypot(ufx._mcpts, ufy._mcpts) 191 | return UncertainFunction(mcpts) 192 | else: 193 | return np.hypot(x, y) 194 | 195 | 196 | def ln(x): 197 | """ 198 | Natural logarithm (same as "log(x)") 199 | """ 200 | return log(x) 201 | 202 | 203 | def log(x): 204 | """ 205 | Natural logarithm 206 | """ 207 | if isinstance(x, UncertainFunction): 208 | mcpts = np.log(x._mcpts) 209 | return UncertainFunction(mcpts) 210 | else: 211 | return np.log(x) 212 | 213 | 214 | def log10(x): 215 | """ 216 | Base-10 logarithm 217 | """ 218 | if isinstance(x, UncertainFunction): 219 | mcpts = np.log10(x._mcpts) 220 | return UncertainFunction(mcpts) 221 | else: 222 | return np.log10(x) 223 | 224 | 225 | def log1p(x): 226 | """ 227 | Natural logarithm of (1 + x) 228 | """ 229 | if isinstance(x, UncertainFunction): 230 | mcpts = np.log1p(x._mcpts) 231 | return UncertainFunction(mcpts) 232 | else: 233 | return np.log1p(x) 234 | 235 | 236 | def radians(x): 237 | """ 238 | Convert degrees to radians 239 | """ 240 | if isinstance(x, UncertainFunction): 241 | mcpts = np.radians(x._mcpts) 242 | return UncertainFunction(mcpts) 243 | else: 244 | return np.radians(x) 245 | 246 | 247 | def sin(x): 248 | """ 249 | Sine 250 | """ 251 | if isinstance(x, UncertainFunction): 252 | mcpts = np.sin(x._mcpts) 253 | return UncertainFunction(mcpts) 254 | else: 255 | return np.sin(x) 256 | 257 | 258 | def sinh(x): 259 | """ 260 | Hyperbolic sine 261 | """ 262 | if isinstance(x, UncertainFunction): 263 | mcpts = np.sinh(x._mcpts) 264 | return UncertainFunction(mcpts) 265 | else: 266 | return np.sinh(x) 267 | 268 | 269 | def sqrt(x): 270 | """ 271 | Square-root function 272 | """ 273 | if isinstance(x, UncertainFunction): 274 | mcpts = np.sqrt(x._mcpts) 275 | return UncertainFunction(mcpts) 276 | else: 277 | return np.sqrt(x) 278 | 279 | 280 | def tan(x): 281 | """ 282 | Tangent 283 | """ 284 | if isinstance(x, UncertainFunction): 285 | mcpts = np.tan(x._mcpts) 286 | return UncertainFunction(mcpts) 287 | else: 288 | return np.tan(x) 289 | 290 | 291 | def tanh(x): 292 | """ 293 | Hyperbolic tangent 294 | """ 295 | if isinstance(x, UncertainFunction): 296 | mcpts = np.tanh(x._mcpts) 297 | return UncertainFunction(mcpts) 298 | else: 299 | return np.tanh(x) 300 | 301 | 302 | def trunc(x): 303 | """ 304 | Truncate the values to the integer value without rounding 305 | """ 306 | if isinstance(x, UncertainFunction): 307 | mcpts = np.trunc(x._mcpts) 308 | return UncertainFunction(mcpts) 309 | else: 310 | return np.trunc(x) 311 | -------------------------------------------------------------------------------- /mcerp/correlate.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import numpy as np 3 | from scipy.stats import rankdata 4 | from scipy.stats.distributions import norm 5 | from . import UncertainFunction 6 | 7 | 8 | def correlate(params, corrmat): 9 | """ 10 | Force a correlation matrix on a set of statistically distributed objects. 11 | This function works on objects in-place. 12 | 13 | Parameters 14 | ---------- 15 | params : array 16 | An array of of uv objects. 17 | corrmat : 2d-array 18 | The correlation matrix to be imposed 19 | 20 | """ 21 | # Make sure all inputs are compatible 22 | assert all( 23 | [isinstance(param, UncertainFunction) for param in params] 24 | ), 'All inputs to "correlate" must be of type "UncertainFunction"' 25 | 26 | # Put each ufunc's samples in a column-wise matrix 27 | data = np.vstack([param._mcpts for param in params]).T 28 | 29 | # Apply the correlation matrix to the sampled data 30 | new_data = induce_correlations(data, corrmat) 31 | 32 | # Re-set the samples to the respective variables 33 | for i in range(len(params)): 34 | params[i]._mcpts = new_data[:, i] 35 | 36 | 37 | def induce_correlations(data, corrmat): 38 | """ 39 | Induce a set of correlations on a column-wise dataset 40 | 41 | Parameters 42 | ---------- 43 | data : 2d-array 44 | An m-by-n array where m is the number of samples and n is the 45 | number of independent variables, each column of the array corresponding 46 | to each variable 47 | corrmat : 2d-array 48 | An n-by-n array that defines the desired correlation coefficients 49 | (between -1 and 1). Note: the matrix must be symmetric and 50 | positive-definite in order to induce. 51 | 52 | Returns 53 | ------- 54 | new_data : 2d-array 55 | An m-by-n array that has the desired correlations. 56 | 57 | """ 58 | # Create an rank-matrix 59 | data_rank = np.vstack([rankdata(datai) for datai in data.T]).T 60 | 61 | # Generate van der Waerden scores 62 | data_rank_score = data_rank / (data_rank.shape[0] + 1.0) 63 | data_rank_score = norm(0, 1).ppf(data_rank_score) 64 | 65 | # Calculate the lower triangular matrix of the Cholesky decomposition 66 | # of the desired correlation matrix 67 | p = chol(corrmat) 68 | 69 | # Calculate the current correlations 70 | t = np.corrcoef(data_rank_score, rowvar=0) 71 | 72 | # Calculate the lower triangular matrix of the Cholesky decomposition 73 | # of the current correlation matrix 74 | q = chol(t) 75 | 76 | # Calculate the re-correlation matrix 77 | s = np.dot(p, np.linalg.inv(q)) 78 | 79 | # Calculate the re-sampled matrix 80 | new_data = np.dot(data_rank_score, s.T) 81 | 82 | # Create the new rank matrix 83 | new_data_rank = np.vstack([rankdata(datai) for datai in new_data.T]).T 84 | 85 | # Sort the original data according to new_data_rank 86 | for i in range(data.shape[1]): 87 | vals, order = np.unique( 88 | np.hstack((data_rank[:, i], new_data_rank[:, i])), return_inverse=True 89 | ) 90 | old_order = order[: new_data_rank.shape[0]] 91 | new_order = order[-new_data_rank.shape[0] :] 92 | tmp = data[np.argsort(old_order), i][new_order] 93 | data[:, i] = tmp[:] 94 | 95 | return data 96 | 97 | 98 | def plotcorr(X, plotargs=None, full=True, labels=None): 99 | """ 100 | Plots a scatterplot matrix of subplots. 101 | 102 | Usage: 103 | 104 | plotcorr(X) 105 | 106 | plotcorr(..., plotargs=...) # e.g., 'r*', 'bo', etc. 107 | 108 | plotcorr(..., full=...) # e.g., True or False 109 | 110 | plotcorr(..., labels=...) # e.g., ['label1', 'label2', ...] 111 | 112 | Each column of "X" is plotted against other columns, resulting in 113 | a ncols by ncols grid of subplots with the diagonal subplots labeled 114 | with "labels". "X" is an array of arrays (i.e., a 2d matrix), a 1d array 115 | of MCERP.UncertainFunction/Variable objects, or a mixture of the two. 116 | Additional keyword arguments are passed on to matplotlib's "plot" command. 117 | Returns the matplotlib figure object containing the subplot grid. 118 | """ 119 | import matplotlib.pyplot as plt 120 | 121 | X = [Xi._mcpts if isinstance(Xi, UncertainFunction) else Xi for Xi in X] 122 | X = np.atleast_2d(X) 123 | numvars, numdata = X.shape 124 | fig, axes = plt.subplots(nrows=numvars, ncols=numvars, figsize=(8, 8)) 125 | fig.subplots_adjust(hspace=0.0, wspace=0.0) 126 | 127 | for ax in axes.flat: 128 | # Hide all ticks and labels 129 | ax.xaxis.set_visible(False) 130 | ax.yaxis.set_visible(False) 131 | 132 | # Set up ticks only on one side for the "edge" subplots... 133 | if full: 134 | if ax.is_first_col(): 135 | ax.yaxis.set_ticks_position("left") 136 | if ax.is_last_col(): 137 | ax.yaxis.set_ticks_position("right") 138 | if ax.is_first_row(): 139 | ax.xaxis.set_ticks_position("top") 140 | if ax.is_last_row(): 141 | ax.xaxis.set_ticks_position("bottom") 142 | else: 143 | if ax.is_first_row(): 144 | ax.xaxis.set_ticks_position("top") 145 | if ax.is_last_col(): 146 | ax.yaxis.set_ticks_position("right") 147 | 148 | # Label the diagonal subplots... 149 | if not labels: 150 | labels = ["x" + str(i) for i in range(numvars)] 151 | 152 | for i, label in enumerate(labels): 153 | axes[i, i].annotate( 154 | label, (0.5, 0.5), xycoords="axes fraction", ha="center", va="center" 155 | ) 156 | 157 | # Plot the data 158 | for i, j in zip(*np.triu_indices_from(axes, k=1)): 159 | if full: 160 | idx = [(i, j), (j, i)] 161 | else: 162 | idx = [(i, j)] 163 | for x, y in idx: 164 | # FIX #1: this needed to be changed from ...(data[x], data[y],...) 165 | if plotargs is None: 166 | if len(X[x]) > 100: 167 | plotargs = ",b" # pixel marker 168 | else: 169 | plotargs = ".b" # point marker 170 | axes[x, y].plot(X[y], X[x], plotargs) 171 | ylim = min(X[y]), max(X[y]) 172 | xlim = min(X[x]), max(X[x]) 173 | axes[x, y].set_ylim( 174 | xlim[0] - (xlim[1] - xlim[0]) * 0.1, xlim[1] + (xlim[1] - xlim[0]) * 0.1 175 | ) 176 | axes[x, y].set_xlim( 177 | ylim[0] - (ylim[1] - ylim[0]) * 0.1, ylim[1] + (ylim[1] - ylim[0]) * 0.1 178 | ) 179 | 180 | # Turn on the proper x or y axes ticks. 181 | if full: 182 | for i, j in zip(list(range(numvars)), itertools.cycle((-1, 0))): 183 | axes[j, i].xaxis.set_visible(True) 184 | axes[i, j].yaxis.set_visible(True) 185 | else: 186 | for i in range(numvars - 1): 187 | axes[0, i + 1].xaxis.set_visible(True) 188 | axes[i, -1].yaxis.set_visible(True) 189 | for i in range(1, numvars): 190 | for j in range(0, i): 191 | fig.delaxes(axes[i, j]) 192 | 193 | # FIX #2: if numvars is odd, the bottom right corner plot doesn't have the 194 | # correct axes limits, so we pull them from other axes 195 | if numvars % 2: 196 | xlimits = axes[0, -1].get_xlim() 197 | ylimits = axes[-1, 0].get_ylim() 198 | axes[-1, -1].set_xlim(xlimits) 199 | axes[-1, -1].set_ylim(ylimits) 200 | 201 | return fig 202 | 203 | 204 | def chol(A): 205 | """ 206 | Calculate the lower triangular matrix of the Cholesky decomposition of 207 | a symmetric, positive-definite matrix. 208 | """ 209 | A = np.array(A) 210 | assert A.shape[0] == A.shape[1], "Input matrix must be square" 211 | 212 | L = [[0.0] * len(A) for _ in range(len(A))] 213 | for i in range(len(A)): 214 | for j in range(i + 1): 215 | s = sum(L[i][k] * L[j][k] for k in range(j)) 216 | L[i][j] = ( 217 | (A[i][i] - s) ** 0.5 if (i == j) else (1.0 / L[j][j] * (A[i][j] - s)) 218 | ) 219 | 220 | return np.array(L) 221 | -------------------------------------------------------------------------------- /doc/distribution_constructors.rst: -------------------------------------------------------------------------------- 1 | 2 | .. index:: Constructing Distributions 3 | 4 | .. _using distributions: 5 | 6 | Using Distributions 7 | =================== 8 | 9 | Since all of the variables in ``mcerp`` are statistical distributions, they 10 | are created internally using the `scipy.stats`_ distributions. There are also 11 | some convenience constructors that should make defining a distribution easier, 12 | though it's not necessary to use them. See the `source code`_ of the 13 | ``UncertainVariable`` class for info that describes how to construct many 14 | of the most common statistical continuous and discrete distributions using 15 | the `scipy.stats`_ distributions (and some others not currently part of 16 | `scipy.stats`). 17 | 18 | Tracking 19 | -------- 20 | 21 | To track variables that may share the same distribution, each of the 22 | constructors below accepts an optional ``tag=...`` kwarg that can be 23 | accessed at anytime using ``x.tag``. 24 | 25 | Constructors 26 | ------------ 27 | 28 | Now, if you are like me, the easier the syntax--the better, thus, here are a 29 | set of **equivalent constructors** that I've found to be **easier to use** 30 | for the most common kinds of distributions (the location, scale, and shape 31 | parameters are described in their respective web pages and the `source code`_): 32 | 33 | +--------------------------------------------------------------------------------------------------------+ 34 | | **Continuous Distributions** | 35 | +---------------------------------------------------------------+----------------------------------------+ 36 | | ``Beta(alpha, beta, [low, high])`` | `Beta distribution`_ | 37 | +---------------------------------------------------------------+----------------------------------------+ 38 | | ``Bradford(q, [low, high])`` | `Bradford distribution`_ | 39 | +---------------------------------------------------------------+----------------------------------------+ 40 | | ``Burr(c, k)`` | `Burr distribution`_ | 41 | +---------------------------------------------------------------+----------------------------------------+ 42 | | ``ChiSquared(k)`` or ``Chi2(k)`` | `Chi-squared distribution`_ | 43 | +---------------------------------------------------------------+----------------------------------------+ 44 | | ``Erf(h)`` | `Error Function distribution`_ | 45 | +---------------------------------------------------------------+----------------------------------------+ 46 | | ``Erlang(m, b)`` | `Erlang distribution`_ | 47 | +---------------------------------------------------------------+----------------------------------------+ 48 | | ``Exponential(lamda)`` or ``Exp(lamda)`` | `Exponential distribution`_ | 49 | +---------------------------------------------------------------+----------------------------------------+ 50 | | ``ExtValueMax(mu, sigma)`` or ``EVMax(mu, sigma)`` | `Extreme Value Maximum distribution`_ | 51 | +---------------------------------------------------------------+----------------------------------------+ 52 | | ``ExtValueMin(mu, sigma)`` or ``EVMin(mu, sigma)`` | `Extreme Value Minimum distribution`_ | 53 | +---------------------------------------------------------------+----------------------------------------+ 54 | | ``Fisher(d1, d2)`` or ``F(d1, d2)`` | `F-distribution`_ | 55 | +---------------------------------------------------------------+----------------------------------------+ 56 | | ``Gamma(k, theta)`` | `Gamma distribution`_ | 57 | +---------------------------------------------------------------+----------------------------------------+ 58 | | ``LogNormal(mu, sigma)`` or ``LogN(mu, sigma)`` | `Log-normal distribution`_ | 59 | +---------------------------------------------------------------+----------------------------------------+ 60 | | ``Normal(mu, sigma)`` or ``N(mu, sigma)`` | `Normal distribution`_ | 61 | +---------------------------------------------------------------+----------------------------------------+ 62 | | ``Pareto(q, a)`` (first kind) | `Pareto distribution`_ | 63 | +---------------------------------------------------------------+----------------------------------------+ 64 | | ``Pareto2(q, b)`` (second kind) | `Pareto2 distribution`_ | 65 | +---------------------------------------------------------------+----------------------------------------+ 66 | | ``PERT(low, peak, high)`` | `PERT distribution`_ | 67 | +---------------------------------------------------------------+----------------------------------------+ 68 | | ``StudentT(v)`` or ``T(v)`` | `T-distribution`_ | 69 | +---------------------------------------------------------------+----------------------------------------+ 70 | | ``Triangular(low, peak, high)`` or ``Tri(low, peak, high)`` | `Triangular distribution`_ | 71 | +---------------------------------------------------------------+----------------------------------------+ 72 | | ``Uniform(low, high)`` or ``U(low, high)`` | `Uniform distribution`_ | 73 | +---------------------------------------------------------------+----------------------------------------+ 74 | | ``Weibull(lamda, k)`` or ``Weib(lamda, k)`` | `Weibull distribution`_ | 75 | +---------------------------------------------------------------+----------------------------------------+ 76 | | **Discrete Distributions** | 77 | +---------------------------------------------------------------+----------------------------------------+ 78 | | ``Bernoulli(p)`` or ``Bern(p)`` | `Bernoulli distribution`_ | 79 | +---------------------------------------------------------------+----------------------------------------+ 80 | | ``Binomial(n, p)`` or ``B(n, p)`` | `Binomial distribution`_ | 81 | +---------------------------------------------------------------+----------------------------------------+ 82 | | ``Geometric(p)`` or ``G(p)`` | `Geometric distribution`_ | 83 | +---------------------------------------------------------------+----------------------------------------+ 84 | | ``Hypergeometric(N, n, K)`` or ``H(N, n, K)`` | `Hypergeometric distribution`_ | 85 | +---------------------------------------------------------------+----------------------------------------+ 86 | | ``Poisson(lamda)`` or ``Pois(lamda)`` | `Poisson distribution`_ | 87 | +---------------------------------------------------------------+----------------------------------------+ 88 | 89 | For example, the following constructions are equivalent:: 90 | 91 | # explicitly calling out the scipy.stats distribution 92 | >>> import scipy.stats as ss 93 | >>> x = uv(ss.norm(loc=10, scale=1)) 94 | 95 | # using a built-in constructor 96 | >>> x = Normal(10, 1) 97 | 98 | # and if there's a short-name alias available 99 | >>> x = N(10, 1) 100 | 101 | From my experience, the first option can be tedious and difficult to work 102 | with, but it does allow you to input any distribution defined in the 103 | `scipy.stats`_ sub-module, both continuous and discrete, if you know how. 104 | For the most common distributions, the MCERP constructors are hard to beat. 105 | If you feel like another distribution should be included in the "common" 106 | list, let me know! 107 | 108 | 109 | .. _scipy.stats: http://docs.scipy.org/doc/scipy/reference/stats.html 110 | .. _source code: https://github.com/tisimst/mcerp/blob/master/mcerp/__init__.py 111 | .. _Beta distribution: http://en.wikipedia.org/wiki/Beta_distribution 112 | .. _Bradford distribution: http://www.vosesoftware.com/ModelRiskHelp/index.htm#Distributions/Continuous_distributions/Bradford_distribution.htm 113 | .. _Burr distribution: http://en.wikipedia.org/wiki/Burr_distribution 114 | .. _Chi-squared distribution: http://en.wikipedia.org/wiki/Chi-squared_distribution 115 | .. _Error Function distribution: http://www.mathwave.com/articles/error_function_distribution.html 116 | .. _Erlang distribution: http://en.wikipedia.org/wiki/Erlang_distribution 117 | .. _Exponential distribution: http://en.wikipedia.org/wiki/Exponential_distribution 118 | .. _Extreme Value Maximum distribution: http://www.math.uah.edu/stat/special/ExtremeValue.html 119 | .. _Extreme Value Minimum distribution: http://www.math.uah.edu/stat/special/ExtremeValue.html 120 | .. _F-distribution: http://en.wikipedia.org/wiki/F-distribution 121 | .. _Gamma distribution: http://en.wikipedia.org/wiki/Gamma_distribution 122 | .. _Log-normal distribution: http://en.wikipedia.org/wiki/Log-normal_distribution 123 | .. _Normal distribution: http://en.wikipedia.org/wiki/Normal_distribution 124 | .. _Pareto distribution: http://www.vosesoftware.com/ModelRiskHelp/Distributions/Continuous_distributions/Pareto_(first_kind)_distribution.htm 125 | .. _Pareto2 distribution: http://www.vosesoftware.com/ModelRiskHelp/Distributions/Continuous_distributions/Pareto_(second_kind)_distribution.htm 126 | .. _PERT distribution: http://www.vosesoftware.com/ModelRiskHelp/index.htm#Distributions/Continuous_distributions/PERT_distribution.htm 127 | .. _T-distribution: http://en.wikipedia.org/wiki/Student's_t-distribution 128 | .. _Triangular distribution: http://en.wikipedia.org/wiki/Triangular_distribution 129 | .. _Uniform distribution: http://en.wikipedia.org/wiki/Uniform_distribution_(continuous) 130 | .. _Weibull distribution: http://en.wikipedia.org/wiki/Weibull_distribution 131 | .. _Bernoulli distribution: http://en.wikipedia.org/wiki/Bernoulli_distribution 132 | .. _Binomial distribution: http://en.wikipedia.org/wiki/Binomial_distribution 133 | .. _Geometric distribution: http://en.wikipedia.org/wiki/Geometric_distribution 134 | .. _Hypergeometric distribution: http://en.wikipedia.org/wiki/Hypergeometric_distribution 135 | .. _Poisson distribution: http://en.wikipedia.org/wiki/Poisson_distribution 136 | -------------------------------------------------------------------------------- /doc/_static/_default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Alternate Sphinx design 3 | * Originally created by Armin Ronacher for Werkzeug, adapted by Georg Brandl. 4 | */ 5 | 6 | body { 7 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; 8 | font-size: 14px; 9 | letter-spacing: -0.01em; 10 | line-height: 150%; 11 | text-align: center; 12 | /*background-color: #AFC1C4; */ 13 | background-color: #BFD1D4; 14 | color: black; 15 | padding: 0; 16 | border: 1px solid #aaa; 17 | 18 | margin: 0px 80px 0px 80px; 19 | min-width: 740px; 20 | } 21 | 22 | a { 23 | color: #CA7900; 24 | text-decoration: none; 25 | } 26 | 27 | a:hover { 28 | color: #2491CF; 29 | } 30 | 31 | pre { 32 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 33 | font-size: 0.95em; 34 | letter-spacing: 0.015em; 35 | padding: 0.5em; 36 | border: 1px solid #ccc; 37 | background-color: #f8f8f8; 38 | } 39 | 40 | td.linenos pre { 41 | padding: 0.5em 0; 42 | border: 0; 43 | background-color: transparent; 44 | color: #aaa; 45 | } 46 | 47 | table.highlighttable { 48 | margin-left: 0.5em; 49 | } 50 | 51 | table.highlighttable td { 52 | padding: 0 0.5em 0 0.5em; 53 | } 54 | 55 | cite, code, tt { 56 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 57 | font-size: 0.95em; 58 | letter-spacing: 0.01em; 59 | } 60 | 61 | hr { 62 | border: 1px solid #abc; 63 | margin: 2em; 64 | } 65 | 66 | tt { 67 | background-color: #f2f2f2; 68 | border-bottom: 1px solid #ddd; 69 | color: #333; 70 | } 71 | 72 | tt.descname { 73 | background-color: transparent; 74 | font-weight: bold; 75 | font-size: 1.2em; 76 | border: 0; 77 | } 78 | 79 | tt.descclassname { 80 | background-color: transparent; 81 | border: 0; 82 | } 83 | 84 | tt.xref { 85 | background-color: transparent; 86 | font-weight: bold; 87 | border: 0; 88 | } 89 | 90 | a tt { 91 | background-color: transparent; 92 | font-weight: bold; 93 | border: 0; 94 | color: #CA7900; 95 | } 96 | 97 | a tt:hover { 98 | color: #2491CF; 99 | } 100 | 101 | dl { 102 | margin-bottom: 15px; 103 | } 104 | 105 | dd p { 106 | margin-top: 0px; 107 | } 108 | 109 | dd ul, dd table { 110 | margin-bottom: 10px; 111 | } 112 | 113 | dd { 114 | margin-top: 3px; 115 | margin-bottom: 10px; 116 | margin-left: 30px; 117 | } 118 | 119 | .refcount { 120 | color: #060; 121 | } 122 | 123 | dt:target, 124 | .highlight { 125 | background-color: #fbe54e; 126 | } 127 | 128 | dl.class, dl.function { 129 | border-top: 2px solid #888; 130 | } 131 | 132 | dl.method, dl.attribute { 133 | border-top: 1px solid #aaa; 134 | } 135 | 136 | dl.glossary dt { 137 | font-weight: bold; 138 | font-size: 1.1em; 139 | } 140 | 141 | pre { 142 | line-height: 120%; 143 | } 144 | 145 | pre a { 146 | color: inherit; 147 | text-decoration: underline; 148 | } 149 | 150 | .first { 151 | margin-top: 0 !important; 152 | } 153 | 154 | div.document { 155 | background-color: white; 156 | text-align: left; 157 | background-image: url(contents.png); 158 | background-repeat: repeat-x; 159 | } 160 | 161 | /* 162 | div.documentwrapper { 163 | width: 100%; 164 | } 165 | */ 166 | 167 | div.clearer { 168 | clear: both; 169 | } 170 | 171 | div.related h3 { 172 | display: none; 173 | } 174 | 175 | div.related ul { 176 | background-image: url(navigation.png); 177 | height: 2em; 178 | list-style: none; 179 | border-top: 1px solid #ddd; 180 | border-bottom: 1px solid #ddd; 181 | margin: 0; 182 | padding-left: 10px; 183 | } 184 | 185 | div.related ul li { 186 | margin: 0; 187 | padding: 0; 188 | height: 2em; 189 | float: left; 190 | } 191 | 192 | div.related ul li.right { 193 | float: right; 194 | margin-right: 5px; 195 | } 196 | 197 | div.related ul li a { 198 | margin: 0; 199 | padding: 0 5px 0 5px; 200 | line-height: 1.75em; 201 | color: #EE9816; 202 | } 203 | 204 | div.related ul li a:hover { 205 | color: #3CA8E7; 206 | } 207 | 208 | div.body { 209 | margin: 0; 210 | padding: 0.5em 20px 20px 20px; 211 | } 212 | 213 | div.bodywrapper { 214 | margin: 0 240px 0 0; 215 | border-right: 1px solid #ccc; 216 | } 217 | 218 | div.body a { 219 | text-decoration: underline; 220 | } 221 | 222 | div.sphinxsidebar { 223 | margin: 0; 224 | padding: 0.5em 15px 15px 0; 225 | width: 210px; 226 | float: right; 227 | text-align: left; 228 | /* margin-left: -100%; */ 229 | } 230 | 231 | div.sphinxsidebar h4, div.sphinxsidebar h3 { 232 | margin: 1em 0 0.5em 0; 233 | font-size: 0.9em; 234 | padding: 0.1em 0 0.1em 0.5em; 235 | color: white; 236 | border: 1px solid #86989B; 237 | background-color: #AFC1C4; 238 | } 239 | 240 | div.sphinxsidebar ul { 241 | padding-left: 1.5em; 242 | margin-top: 7px; 243 | list-style: none; 244 | padding: 0; 245 | line-height: 130%; 246 | } 247 | 248 | div.sphinxsidebar ul ul { 249 | list-style: square; 250 | margin-left: 20px; 251 | } 252 | 253 | p { 254 | margin: 0.8em 0 0.5em 0; 255 | } 256 | 257 | p.rubric { 258 | font-weight: bold; 259 | } 260 | 261 | h1 { 262 | margin: 0; 263 | padding: 0.7em 0 0.3em 0; 264 | font-size: 1.5em; 265 | color: #11557C; 266 | } 267 | 268 | h2 { 269 | margin: 1.3em 0 0.2em 0; 270 | font-size: 1.35em; 271 | padding: 0; 272 | } 273 | 274 | h3 { 275 | margin: 1em 0 -0.3em 0; 276 | font-size: 1.2em; 277 | } 278 | 279 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { 280 | color: black!important; 281 | } 282 | 283 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 284 | display: none; 285 | margin: 0 0 0 0.3em; 286 | padding: 0 0.2em 0 0.2em; 287 | color: #aaa!important; 288 | } 289 | 290 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 291 | h5:hover a.anchor, h6:hover a.anchor { 292 | display: inline; 293 | } 294 | 295 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 296 | h5 a.anchor:hover, h6 a.anchor:hover { 297 | color: #777; 298 | background-color: #eee; 299 | } 300 | 301 | table { 302 | border-collapse: collapse; 303 | margin: 0 -0.5em 0 -0.5em; 304 | } 305 | 306 | table td, table th { 307 | padding: 0.2em 0.5em 0.2em 0.5em; 308 | } 309 | 310 | div.footer { 311 | background-color: #E3EFF1; 312 | color: #86989B; 313 | padding: 3px 8px 3px 0; 314 | clear: both; 315 | font-size: 0.8em; 316 | text-align: right; 317 | } 318 | 319 | div.footer a { 320 | color: #86989B; 321 | text-decoration: underline; 322 | } 323 | 324 | div.pagination { 325 | margin-top: 2em; 326 | padding-top: 0.5em; 327 | border-top: 1px solid black; 328 | text-align: center; 329 | } 330 | 331 | div.sphinxsidebar ul.toc { 332 | margin: 1em 0 1em 0; 333 | padding: 0 0 0 0.5em; 334 | list-style: none; 335 | } 336 | 337 | div.sphinxsidebar ul.toc li { 338 | margin: 0.5em 0 0.5em 0; 339 | font-size: 0.9em; 340 | line-height: 130%; 341 | } 342 | 343 | div.sphinxsidebar ul.toc li p { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | div.sphinxsidebar ul.toc ul { 349 | margin: 0.2em 0 0.2em 0; 350 | padding: 0 0 0 1.8em; 351 | } 352 | 353 | div.sphinxsidebar ul.toc ul li { 354 | padding: 0; 355 | } 356 | 357 | div.admonition, div.warning { 358 | font-size: 0.9em; 359 | margin: 1em 0 0 0; 360 | border: 1px solid #86989B; 361 | background-color: #f7f7f7; 362 | } 363 | 364 | div.admonition p, div.warning p { 365 | margin: 0.5em 1em 0.5em 1em; 366 | padding: 0; 367 | } 368 | 369 | div.admonition pre, div.warning pre { 370 | margin: 0.4em 1em 0.4em 1em; 371 | } 372 | 373 | div.admonition p.admonition-title, 374 | div.warning p.admonition-title { 375 | margin: 0; 376 | padding: 0.1em 0 0.1em 0.5em; 377 | color: white; 378 | border-bottom: 1px solid #86989B; 379 | font-weight: bold; 380 | background-color: #AFC1C4; 381 | } 382 | 383 | div.warning { 384 | border: 1px solid #940000; 385 | } 386 | 387 | div.warning p.admonition-title { 388 | background-color: #CF0000; 389 | border-bottom-color: #940000; 390 | } 391 | 392 | div.admonition ul, div.admonition ol, 393 | div.warning ul, div.warning ol { 394 | margin: 0.1em 0.5em 0.5em 3em; 395 | padding: 0; 396 | } 397 | 398 | div.versioninfo { 399 | margin: 1em 0 0 0; 400 | border: 1px solid #ccc; 401 | background-color: #DDEAF0; 402 | padding: 8px; 403 | line-height: 1.3em; 404 | font-size: 0.9em; 405 | } 406 | 407 | 408 | a.headerlink { 409 | color: #c60f0f!important; 410 | font-size: 1em; 411 | margin-left: 6px; 412 | padding: 0 4px 0 4px; 413 | text-decoration: none!important; 414 | visibility: hidden; 415 | } 416 | 417 | h1:hover > a.headerlink, 418 | h2:hover > a.headerlink, 419 | h3:hover > a.headerlink, 420 | h4:hover > a.headerlink, 421 | h5:hover > a.headerlink, 422 | h6:hover > a.headerlink, 423 | dt:hover > a.headerlink { 424 | visibility: visible; 425 | } 426 | 427 | a.headerlink:hover { 428 | background-color: #ccc; 429 | color: white!important; 430 | } 431 | 432 | table.indextable td { 433 | text-align: left; 434 | vertical-align: top; 435 | } 436 | 437 | table.indextable dl, table.indextable dd { 438 | margin-top: 0; 439 | margin-bottom: 0; 440 | } 441 | 442 | table.indextable tr.pcap { 443 | height: 10px; 444 | } 445 | 446 | table.indextable tr.cap { 447 | margin-top: 10px; 448 | background-color: #f2f2f2; 449 | } 450 | 451 | img.toggler { 452 | margin-right: 3px; 453 | margin-top: 3px; 454 | cursor: pointer; 455 | } 456 | 457 | img.inheritance { 458 | border: 0px 459 | } 460 | 461 | form.pfform { 462 | margin: 10px 0 20px 0; 463 | } 464 | 465 | table.contentstable { 466 | width: 90%; 467 | } 468 | 469 | table.contentstable p.biglink { 470 | line-height: 150%; 471 | } 472 | 473 | a.biglink { 474 | font-size: 1.3em; 475 | } 476 | 477 | span.linkdescr { 478 | font-style: italic; 479 | padding-top: 5px; 480 | font-size: 90%; 481 | } 482 | 483 | ul.search { 484 | margin: 10px 0 0 20px; 485 | padding: 0; 486 | } 487 | 488 | ul.search li { 489 | padding: 5px 0 5px 20px; 490 | background-image: url(file.png); 491 | background-repeat: no-repeat; 492 | background-position: 0 7px; 493 | } 494 | 495 | ul.search li a { 496 | font-weight: bold; 497 | } 498 | 499 | ul.search li div.context { 500 | color: #888; 501 | margin: 2px 0 0 30px; 502 | text-align: left; 503 | } 504 | 505 | ul.keywordmatches li.goodmatch a { 506 | font-weight: bold; 507 | } 508 | -------------------------------------------------------------------------------- /mcerp/lhd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.stats as ss 3 | 4 | 5 | def lhd( 6 | dist=None, 7 | size=None, 8 | dims=1, 9 | form="randomized", 10 | iterations=100, 11 | showcorrelations=False, 12 | ): 13 | """ 14 | Create a Latin-Hypercube sample design based on distributions defined in the 15 | `scipy.stats` module 16 | 17 | Parameters 18 | ---------- 19 | dist: array_like 20 | frozen scipy.stats.rv_continuous or rv_discrete distribution objects 21 | that are defined previous to calling LHD 22 | 23 | size: int 24 | integer value for the number of samples to generate for each 25 | distribution object 26 | 27 | dims: int, optional 28 | if dist is a single distribution object, and dims > 1, the one 29 | distribution will be used to generate a size-by-dims sampled design 30 | 31 | form: str, optional (non-functional at the moment) 32 | determines how the sampling is to occur, with the following optional 33 | values: 34 | - 'randomized' - completely randomized sampling 35 | - 'spacefilling' - space-filling sampling (generally gives a more 36 | accurate sampling of the design when the number of sample points 37 | is small) 38 | - 'orthogonal' - balanced space-filling sampling (experimental) 39 | 40 | The 'spacefilling' and 'orthogonal' forms require some iterations to 41 | determine the optimal sampling pattern. 42 | 43 | iterations: int, optional (non-functional at the moment) 44 | used to control the number of allowable search iterations for generating 45 | 'spacefilling' and 'orthogonal' designs 46 | 47 | Returns 48 | ------- 49 | out: 2d-array, 50 | A 2d-array where each column corresponds to each input distribution and 51 | each row is a sample in the design 52 | 53 | Examples 54 | -------- 55 | 56 | Single distribution: 57 | - uniform distribution, low = -1, width = 2 58 | 59 | >>> import scipy.stats as ss 60 | >>> d0 = ss.uniform(loc=-1,scale=2) 61 | >>> print lhd(dist=d0,size=5) 62 | [[ 0.51031081] 63 | [-0.28961427] 64 | [-0.68342107] 65 | [ 0.69784371] 66 | [ 0.12248842]] 67 | 68 | Single distribution for multiple variables: 69 | - normal distribution, mean = 0, stdev = 1 70 | 71 | >>> d1 = ss.norm(loc=0,scale=1) 72 | >>> print lhd(dist=d1,size=7,dims=5) 73 | [[-0.8612785 0.23034412 0.21808001] 74 | [ 0.0455778 0.07001606 0.31586419] 75 | [-0.978553 0.30394663 0.78483995] 76 | [-0.26415983 0.15235896 0.51462024] 77 | [ 0.80805686 0.38891031 0.02076505] 78 | [ 1.63028931 0.52104917 1.48016008]] 79 | 80 | Multiple distributions: 81 | - beta distribution, alpha = 2, beta = 5 82 | - exponential distribution, lambda = 1.5 83 | 84 | >>> d2 = ss.beta(2,5) 85 | >>> d3 = ss.expon(scale=1/1.5) 86 | >>> print lhd(dist=(d1,d2,d3),size=6) 87 | [[-0.8612785 0.23034412 0.21808001] 88 | [ 0.0455778 0.07001606 0.31586419] 89 | [-0.978553 0.30394663 0.78483995] 90 | [-0.26415983 0.15235896 0.51462024] 91 | [ 0.80805686 0.38891031 0.02076505] 92 | [ 1.63028931 0.52104917 1.48016008]] 93 | """ 94 | assert dims > 0, 'kwarg "dims" must be at least 1' 95 | if not size or not dist: 96 | return None 97 | 98 | def _lhs(x, samples=20): 99 | """ 100 | _lhs(x) returns a latin-hypercube matrix (each row is a different 101 | set of sample inputs) using a default sample size of 20 for each column 102 | of X. X must be a 2xN matrix that contains the lower and upper bounds of 103 | each column. The lower bound(s) should be in the first row and the upper 104 | bound(s) should be in the second row. 105 | 106 | _lhs(x,samples=N) uses the sample size of N instead of the default (20). 107 | 108 | Example: 109 | >>> x = np.array([[0,-1,3],[1,2,6]]) 110 | >>> print 'x:'; print x 111 | x: 112 | [[ 0 -1 3] 113 | [ 1 2 6]] 114 | 115 | >>> print 'lhs(x):'; print _lhs(x) 116 | lhs(x): 117 | [[ 0.02989122 -0.93918734 3.14432618] 118 | [ 0.08869833 -0.82140706 3.19875152] 119 | [ 0.10627442 -0.66999234 3.33814979] 120 | [ 0.15202861 -0.44157763 3.57036894] 121 | [ 0.2067089 -0.34845384 3.66930908] 122 | [ 0.26542056 -0.23706445 3.76361414] 123 | [ 0.34201421 -0.00779306 3.90818257] 124 | [ 0.37891646 0.15458423 4.15031708] 125 | [ 0.43501575 0.23561118 4.20320064] 126 | [ 0.4865449 0.36350601 4.45792314] 127 | [ 0.54804367 0.56069855 4.60911539] 128 | [ 0.59400712 0.7468415 4.69923486] 129 | [ 0.63708876 0.9159176 4.83611204] 130 | [ 0.68819855 0.98596354 4.97659182] 131 | [ 0.7368695 1.18923511 5.11135111] 132 | [ 0.78885724 1.28369441 5.2900157 ] 133 | [ 0.80966513 1.47415703 5.4081971 ] 134 | [ 0.86196731 1.57844205 5.61067689] 135 | [ 0.94784517 1.71823504 5.78021164] 136 | [ 0.96739728 1.94169017 5.88604772]] 137 | 138 | >>> print 'lhs(x,samples=5):'; print _lhs(x,samples=5) 139 | lhs(x,samples=5): 140 | [[ 0.1949127 -0.54124725 3.49238369] 141 | [ 0.21128576 -0.13439798 3.65652016] 142 | [ 0.47516308 0.39957406 4.5797308 ] 143 | [ 0.64400392 0.90890999 4.92379431] 144 | [ 0.96279472 1.79415307 5.52028238]] 145 | """ 146 | 147 | # determine the segment size 148 | segmentSize = 1.0 / samples 149 | 150 | # get the number of dimensions to sample (number of columns) 151 | numVars = x.shape[1] 152 | 153 | # populate each dimension 154 | out = np.zeros((samples, numVars)) 155 | pointValue = np.zeros(samples) 156 | 157 | for n in range(numVars): 158 | for i in range(samples): 159 | segmentMin = i * segmentSize 160 | point = segmentMin + (np.random.random() * segmentSize) 161 | pointValue[i] = (point * (x[1, n] - x[0, n])) + x[0, n] 162 | out[:, n] = pointValue 163 | 164 | # now randomly arrange the different segments 165 | return _mix(out) 166 | 167 | def _mix(data, dim="cols"): 168 | """ 169 | Takes a data matrix and mixes up the values along dim (either "rows" or 170 | "cols"). In other words, if dim='rows', then each row's data is mixed 171 | ONLY WITHIN ITSELF. Likewise, if dim='cols', then each column's data is 172 | mixed ONLY WITHIN ITSELF. 173 | """ 174 | data = np.atleast_2d(data) 175 | n = data.shape[0] 176 | 177 | if dim == "rows": 178 | data = data.T 179 | 180 | data_rank = list(range(n)) 181 | for i in range(data.shape[1]): 182 | new_data_rank = np.random.permutation(data_rank) 183 | vals, order = np.unique( 184 | np.hstack((data_rank, new_data_rank)), return_inverse=True 185 | ) 186 | old_order = order[:n] 187 | new_order = order[-n:] 188 | tmp = data[np.argsort(old_order), i][new_order] 189 | data[:, i] = tmp[:] 190 | 191 | if dim == "rows": 192 | data = data.T 193 | 194 | return data 195 | 196 | if form is "randomized": 197 | if hasattr(dist, "__getitem__"): # if multiple distributions were input 198 | nvars = len(dist) 199 | x = np.vstack((np.zeros(nvars), np.ones(nvars))) 200 | unif_data = _lhs(x, samples=size) 201 | dist_data = np.empty_like(unif_data) 202 | for i, d in enumerate(dist): 203 | dist_data[:, i] = d.ppf(unif_data[:, i]) 204 | 205 | else: # if a single distribution was input 206 | nvars = dims 207 | x = np.vstack((np.zeros(nvars), np.ones(nvars))) 208 | unif_data = _lhs(x, samples=size) 209 | dist_data = np.empty_like(unif_data) 210 | for i in range(nvars): 211 | dist_data[:, i] = dist.ppf(unif_data[:, i]) 212 | 213 | elif form is "spacefilling": 214 | 215 | def euclid_distance(arr): 216 | n = arr.shape[0] 217 | ans = 0.0 218 | for i in range(n - 1): 219 | for j in range(i + 1, n): 220 | d = np.sqrt( 221 | np.sum( 222 | [(arr[i, k] - arr[j, k]) ** 2 for k in range(arr.shape[1])] 223 | ) 224 | ) 225 | ans += 1.0 / d ** 2 226 | return ans 227 | 228 | def fill_space(data): 229 | best = 1e8 230 | for it in range(iterations): 231 | d = euclid_distance(data) 232 | if d < best: 233 | d_opt = d 234 | data_opt = data.copy() 235 | 236 | data = _mix(data) 237 | 238 | print("Optimized Distance:", d_opt) 239 | return data_opt 240 | 241 | if hasattr(dist, "__getitem__"): # if multiple distributions were input 242 | nvars = len(dist) 243 | x = np.vstack((np.zeros(nvars), np.ones(nvars))) 244 | unif_data = fill_space(_lhs(x, samples=size)) 245 | dist_data = np.empty_like(unif_data) 246 | for i, d in enumerate(dist): 247 | dist_data[:, i] = d.ppf(unif_data[:, i]) 248 | 249 | else: # if a single distribution was input 250 | nvars = dims 251 | x = np.vstack((np.zeros(nvars), np.ones(nvars))) 252 | unif_data = fill_space(_lhs(x, samples=size)) 253 | dist_data = np.empty_like(unif_data) 254 | for i in range(nvars): 255 | dist_data[:, i] = dist.ppf(unif_data[:, i]) 256 | 257 | elif form is "orthogonal": 258 | raise NotImplementedError( 259 | "Sorry. The orthogonal space-filling algorithm hasn't been implemented yet." 260 | ) 261 | else: 262 | raise ValueError('Invalid "form" value: %s' % (form)) 263 | 264 | if dist_data.shape[1] > 1: 265 | cor_matrix = np.zeros((nvars, nvars)) 266 | for i in range(nvars): 267 | for j in range(nvars): 268 | x_data = dist_data[:, i].copy() 269 | y_data = dist_data[:, j].copy() 270 | x_mean = x_data.mean() 271 | y_mean = y_data.mean() 272 | num = np.sum((x_data - x_mean) * (y_data - y_mean)) 273 | den = np.sqrt( 274 | np.sum((x_data - x_mean) ** 2) * np.sum((y_data - y_mean) ** 2) 275 | ) 276 | cor_matrix[i, j] = num / den 277 | cor_matrix[j, i] = num / den 278 | inv_cor_matrix = np.linalg.pinv(cor_matrix) 279 | VIF = np.max(np.diag(inv_cor_matrix)) 280 | 281 | if showcorrelations: 282 | print("Correlation Matrix:\n", cor_matrix) 283 | print("Inverted Correlation Matrix:\n", inv_cor_matrix) 284 | print("Variance Inflation Factor (VIF):", VIF) 285 | 286 | return dist_data 287 | -------------------------------------------------------------------------------- /doc/_static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 30 | 35 | 36 | 37 | 55 | 57 | 58 | 60 | image/svg+xml 61 | 63 | 64 | 65 | 66 | 67 | 72 | 75 | mcerp 92 | monte carlo error propagation 109 | 115 | 127 | % Yield 144 | DPPM 161 | ROI 173 | 178 | 184 | 190 | 196 | 202 | 208 | 211 | 218 | x 235 | 236 | 239 | 245 | y 257 | 258 | 261 | 266 | z 278 | 279 | f(x, y, z) 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /mcerp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================================================ 3 | mcerp: Real-time latin-hypercube-sampling-based Monte Carlo Error Propagation 4 | ================================================================================ 5 | 6 | Author: Abraham Lee 7 | Copyright: 2013 - 2014 8 | """ 9 | from pkg_resources import get_distribution, DistributionNotFound 10 | import numpy as np 11 | import scipy.stats as ss 12 | from .lhd import lhd 13 | 14 | try: 15 | __version__ = get_distribution(__name__).version 16 | except DistributionNotFound: 17 | # package is not installed 18 | pass 19 | 20 | __author__ = "Abraham Lee" 21 | 22 | npts = 10000 23 | 24 | CONSTANT_TYPES = (float, int, int) 25 | 26 | 27 | class NotUpcast(Exception): 28 | """Raised when an object cannot be converted to a number with uncertainty""" 29 | 30 | 31 | def to_uncertain_func(x): 32 | """ 33 | Transforms x into an UncertainFunction-compatible object, 34 | unless it is already an UncertainFunction (in which case x is returned 35 | unchanged). 36 | 37 | Raises an exception unless 'x' belongs to some specific classes of 38 | objects that are known not to depend on UncertainFunction objects 39 | (which then cannot be considered as constants). 40 | """ 41 | if isinstance(x, UncertainFunction): 42 | return x 43 | 44 | # ! In Python 2.6+, numbers.Number could be used instead, here: 45 | elif isinstance(x, CONSTANT_TYPES): 46 | # No variable => no derivative to define: 47 | return UncertainFunction([x] * npts) 48 | 49 | raise NotUpcast("%s cannot be converted to a number with" " uncertainty" % type(x)) 50 | 51 | 52 | class UncertainFunction(object): 53 | """ 54 | UncertainFunction objects represent the uncertainty of a result of 55 | calculations with uncertain variables. Nearly all basic mathematical 56 | operations are supported. 57 | 58 | This class is mostly intended for internal use. 59 | """ 60 | 61 | def __init__(self, mcpts): 62 | self._mcpts = np.atleast_1d(mcpts).flatten() 63 | self.tag = None 64 | 65 | @property 66 | def mean(self): 67 | """ 68 | Mean value as a result of an uncertainty calculation 69 | """ 70 | mn = np.mean(self._mcpts) 71 | return mn 72 | 73 | @property 74 | def var(self): 75 | """ 76 | Variance value as a result of an uncertainty calculation 77 | """ 78 | mn = self.mean 79 | vr = np.mean((self._mcpts - mn) ** 2) 80 | return vr 81 | 82 | @property 83 | def std(self): 84 | r""" 85 | Standard deviation value as a result of an uncertainty calculation, 86 | defined as:: 87 | 88 | ________ 89 | std = \/variance 90 | 91 | """ 92 | return self.var ** 0.5 93 | 94 | @property 95 | def skew(self): 96 | r""" 97 | Skewness coefficient value as a result of an uncertainty calculation, 98 | defined as:: 99 | 100 | _____ m3 101 | \/beta1 = ------ 102 | std**3 103 | 104 | where m3 is the third central moment and std is the standard deviation 105 | """ 106 | mn = self.mean 107 | sd = self.std 108 | sk = 0.0 if abs(sd) <= 1e-8 else np.mean((self._mcpts - mn) ** 3) / sd ** 3 109 | return sk 110 | 111 | @property 112 | def kurt(self): 113 | """ 114 | Kurtosis coefficient value as a result of an uncertainty calculation, 115 | defined as:: 116 | 117 | m4 118 | beta2 = ------ 119 | std**4 120 | 121 | where m4 is the fourth central moment and std is the standard deviation 122 | """ 123 | mn = self.mean 124 | sd = self.std 125 | kt = 0.0 if abs(sd) <= 1e-8 else np.mean((self._mcpts - mn) ** 4) / sd ** 4 126 | return kt 127 | 128 | @property 129 | def stats(self): 130 | """ 131 | The first four standard moments of a distribution: mean, variance, and 132 | standardized skewness and kurtosis coefficients. 133 | """ 134 | mn = self.mean 135 | vr = self.var 136 | sk = self.skew 137 | kt = self.kurt 138 | return [mn, vr, sk, kt] 139 | 140 | def percentile(self, val): 141 | """ 142 | Get the distribution value at a given percentile or set of percentiles. 143 | This follows the NIST method for calculating percentiles. 144 | 145 | Parameters 146 | ---------- 147 | val : scalar or array 148 | Either a single value or an array of values between 0 and 1. 149 | 150 | Returns 151 | ------- 152 | out : scalar or array 153 | The actual distribution value that appears at the requested 154 | percentile value or values 155 | 156 | """ 157 | try: 158 | # test to see if an input is given as an array 159 | out = [self.percentile(vi) for vi in val] 160 | except (ValueError, TypeError): 161 | if val <= 0: 162 | out = float(min(self._mcpts)) 163 | elif val >= 1: 164 | out = float(max(self._mcpts)) 165 | else: 166 | tmp = np.sort(self._mcpts) 167 | n = val * (len(tmp) + 1) 168 | k, d = int(n), n - int(n) 169 | out = float(tmp[k] + d * (tmp[k + 1] - tmp[k])) 170 | if isinstance(val, np.ndarray): 171 | out = np.array(out) 172 | return out 173 | 174 | def _to_general_representation(self, str_func): 175 | mn, vr, sk, kt = self.stats 176 | return ( 177 | "uv({:}, {:}, {:}, {:})".format( 178 | str_func(mn), str_func(vr), str_func(sk), str_func(kt) 179 | ) 180 | if any([vr, sk, kt]) 181 | else str_func(mn) 182 | ) 183 | 184 | def __str__(self): 185 | return self._to_general_representation(str) 186 | 187 | def __repr__(self): 188 | # return self._to_general_representation(repr) 189 | return str(self) 190 | 191 | def describe(self, name=None): 192 | """ 193 | Cleanly show what the four displayed distribution moments are: 194 | - Mean 195 | - Variance 196 | - Standardized Skewness Coefficient 197 | - Standardized Kurtosis Coefficient 198 | 199 | For a standard Normal distribution, these are [0, 1, 0, 3]. 200 | 201 | If the object has an associated tag, this is presented. If the optional 202 | ``name`` kwarg is utilized, this is presented as with the moments. 203 | Otherwise, no unique name is presented. 204 | 205 | Example 206 | ======= 207 | :: 208 | 209 | >>> x = N(0, 1, 'x') 210 | >>> x.describe() # print tag since assigned 211 | MCERP Uncertain Value (x): 212 | ... 213 | 214 | >>> x.describe('foobar') # 'name' kwarg takes precedence 215 | MCERP Uncertain Value (foobar): 216 | ... 217 | 218 | >>> y = x**2 219 | >>> y.describe('y') # print name since assigned 220 | MCERP Uncertain Value (y): 221 | ... 222 | 223 | >>> y.describe() # print nothing since no tag 224 | MCERP Uncertain Value: 225 | ... 226 | 227 | """ 228 | mn, vr, sk, kt = self.stats 229 | if name is not None: 230 | s = "MCERP Uncertain Value (" + name + "):\n" 231 | elif self.tag is not None: 232 | s = "MCERP Uncertain Value (" + self.tag + "):\n" 233 | else: 234 | s = "MCERP Uncertain Value:\n" 235 | s += " > Mean................... {: }\n".format(mn) 236 | s += " > Variance............... {: }\n".format(vr) 237 | s += " > Skewness Coefficient... {: }\n".format(sk) 238 | s += " > Kurtosis Coefficient... {: }\n".format(kt) 239 | print(s) 240 | 241 | def plot(self, hist=False, show=False, **kwargs): 242 | """ 243 | Plot the distribution of the UncertainFunction. By default, the 244 | distribution is shown with a kernel density estimate (kde). 245 | 246 | Optional 247 | -------- 248 | hist : bool 249 | If true, a density histogram is displayed (histtype='stepfilled') 250 | show : bool 251 | If ``True``, the figure will be displayed after plotting the 252 | distribution. If ``False``, an explicit call to ``plt.show()`` is 253 | required to display the figure. 254 | kwargs : any valid matplotlib.pyplot.plot or .hist kwarg 255 | 256 | """ 257 | import matplotlib.pyplot as plt 258 | 259 | vals = self._mcpts 260 | low = min(vals) 261 | high = max(vals) 262 | 263 | p = ss.kde.gaussian_kde(vals) 264 | xp = np.linspace(low, high, 100) 265 | 266 | if hist: 267 | h = plt.hist( 268 | vals, 269 | bins=int(np.sqrt(len(vals)) + 0.5), 270 | histtype="stepfilled", 271 | density=True, 272 | **kwargs 273 | ) 274 | plt.ylim(0, 1.1 * h[0].max()) 275 | else: 276 | plt.plot(xp, p.evaluate(xp), **kwargs) 277 | 278 | plt.xlim(low - (high - low) * 0.1, high + (high - low) * 0.1) 279 | 280 | if show: 281 | self.show() 282 | 283 | def show(self): 284 | import matplotlib.pyplot as plt 285 | 286 | plt.show() 287 | 288 | def __add__(self, val): 289 | uf = list(map(to_uncertain_func, [self, val])) 290 | mcpts = uf[0]._mcpts + uf[1]._mcpts 291 | return UncertainFunction(mcpts) 292 | 293 | def __radd__(self, val): 294 | uf = list(map(to_uncertain_func, [self, val])) 295 | mcpts = uf[0]._mcpts + uf[1]._mcpts 296 | return UncertainFunction(mcpts) 297 | 298 | def __mul__(self, val): 299 | uf = list(map(to_uncertain_func, [self, val])) 300 | mcpts = uf[0]._mcpts * uf[1]._mcpts 301 | return UncertainFunction(mcpts) 302 | 303 | def __rmul__(self, val): 304 | uf = list(map(to_uncertain_func, [self, val])) 305 | mcpts = uf[0]._mcpts * uf[1]._mcpts 306 | return UncertainFunction(mcpts) 307 | 308 | def __sub__(self, val): 309 | uf = list(map(to_uncertain_func, [self, val])) 310 | mcpts = uf[0]._mcpts - uf[1]._mcpts 311 | return UncertainFunction(mcpts) 312 | 313 | def __rsub__(self, val): 314 | uf = list(map(to_uncertain_func, [self, val])) 315 | mcpts = uf[1]._mcpts - uf[0]._mcpts 316 | return UncertainFunction(mcpts) 317 | 318 | def __div__(self, val): 319 | uf = list(map(to_uncertain_func, [self, val])) 320 | mcpts = uf[0]._mcpts / uf[1]._mcpts 321 | return UncertainFunction(mcpts) 322 | 323 | def __rdiv__(self, val): 324 | uf = list(map(to_uncertain_func, [self, val])) 325 | mcpts = uf[1]._mcpts / uf[0]._mcpts 326 | return UncertainFunction(mcpts) 327 | 328 | def __truediv__(self, val): 329 | uf = list(map(to_uncertain_func, [self, val])) 330 | mcpts = uf[0]._mcpts / uf[1]._mcpts 331 | return UncertainFunction(mcpts) 332 | 333 | def __rtruediv__(self, val): 334 | uf = list(map(to_uncertain_func, [self, val])) 335 | mcpts = uf[1]._mcpts / uf[0]._mcpts 336 | return UncertainFunction(mcpts) 337 | 338 | def __pow__(self, val): 339 | uf = list(map(to_uncertain_func, [self, val])) 340 | mcpts = uf[0]._mcpts ** uf[1]._mcpts 341 | return UncertainFunction(mcpts) 342 | 343 | def __rpow__(self, val): 344 | uf = list(map(to_uncertain_func, [self, val])) 345 | mcpts = uf[1]._mcpts ** uf[0]._mcpts 346 | return UncertainFunction(mcpts) 347 | 348 | def __neg__(self): 349 | mcpts = -self._mcpts 350 | return UncertainFunction(mcpts) 351 | 352 | def __pos__(self): 353 | mcpts = self._mcpts 354 | return UncertainFunction(mcpts) 355 | 356 | def __abs__(self): 357 | mcpts = np.abs(self._mcpts) 358 | return UncertainFunction(mcpts) 359 | 360 | def __eq__(self, val): 361 | """ 362 | If we are comparing two distributions, check the resulting moments. If 363 | they are the same distribution, then the moments will all be zero and 364 | we can know that it is actually the same distribution we are comparing 365 | ``self`` to, otherwise, at least one statistical moment will be non- 366 | zero. 367 | 368 | If we are comparing ``self`` to a scalar, just do a normal comparison 369 | so that if the underlying distribution looks like a PMF, a meaningful 370 | probability of self==val is returned. This can still work quite safely 371 | for PDF distributions since the likelihood of comparing self to an 372 | actual sampled value is negligible when mcerp.npts is large. 373 | 374 | Examples: 375 | 376 | >>> h = H(50, 5, 10) # Hypergeometric distribution (PMF) 377 | >>> h==4 # what's the probability of getting 4 of the 5? 378 | 0.004 379 | >>> sum([h==i for i in (0, 1, 2, 3, 4, 5)]) # sum of all discrete probabilities 380 | 1.0 381 | 382 | >>> n = N(0, 1) # Normal distribution (PDF) 383 | >>> n==0 # what's the probability of being exactly 0.0? 384 | 0.0 385 | >>> n>0 # greater than 0.0? 386 | 0.5 387 | >>> n<0 # less than 0.0? 388 | 0.5 389 | >>> n==1 # exactly 1.0? 390 | 0.0 391 | """ 392 | if isinstance(val, UncertainFunction): 393 | diff = self - val 394 | return not (diff.mean or diff.var or diff.skew or diff.kurt) 395 | else: 396 | return len(self._mcpts[self._mcpts == val]) / float(npts) 397 | 398 | def __ne__(self, val): 399 | if isinstance(val, UncertainFunction): 400 | return not self == val 401 | else: 402 | return 1 - (self == val) 403 | 404 | def __lt__(self, val): 405 | """ 406 | If we are comparing two distributions, perform statistical tests, 407 | otherwise, calculate the probability that the distribution is 408 | less than val 409 | """ 410 | if isinstance(val, UncertainFunction): 411 | tstat, pval = ss.ttest_rel(self._mcpts, val._mcpts) 412 | sgn = np.sign(tstat) 413 | if pval > 0.05: # Since, statistically, we can't really tell 414 | return False 415 | else: 416 | return True if sgn == -1 else False 417 | else: 418 | return len(self._mcpts[self._mcpts < val]) / float(npts) 419 | 420 | def __le__(self, val): 421 | if isinstance(val, UncertainFunction): 422 | return self < val # since it doesn't matter to the test 423 | else: 424 | return len(self._mcpts[self._mcpts <= val]) / float(npts) 425 | 426 | def __gt__(self, val): 427 | """ 428 | If we are comparing two distributions, perform statistical tests, 429 | otherwise, calculate the probability that the distribution is 430 | greater than val 431 | """ 432 | if isinstance(val, UncertainFunction): 433 | tstat, pval = ss.ttest_rel(self._mcpts, val._mcpts) 434 | sgn = np.sign(tstat) 435 | if pval > 0.05: # Since, statistically, we can't really tell 436 | return False 437 | else: 438 | return True if sgn == 1 else False 439 | else: 440 | return 1 - (self <= val) 441 | 442 | def __ge__(self, val): 443 | if isinstance(val, UncertainFunction): 444 | return self > val 445 | else: 446 | return 1 - (self < val) 447 | 448 | def __bool__(self): 449 | return not (1 - ((self > 0) + (self < 0))) 450 | 451 | 452 | class UncertainVariable(UncertainFunction): 453 | """ 454 | UncertainVariable objects track the effects of uncertainty, characterized 455 | in terms of the first four standard moments of statistical distributions 456 | (mean, variance, skewness and kurtosis coefficients). Monte Carlo simulation, 457 | in conjunction with Latin-hypercube based sampling performs the calculations. 458 | 459 | Parameters 460 | ---------- 461 | rv : scipy.stats.distribution 462 | A distribution to characterize the uncertainty 463 | 464 | tag : str, optional 465 | A string identifier when information about this variable is printed to 466 | the screen 467 | 468 | Notes 469 | ----- 470 | 471 | The ``scipy.stats`` module contains many distributions which we can use to 472 | perform any necessary uncertainty calculation. It is important to follow 473 | the initialization syntax for creating any kind of distribution object: 474 | 475 | - *Location* and *Scale* values must use the kwargs ``loc`` and 476 | ``scale`` 477 | - *Shape* values are passed in as non-keyword arguments before the 478 | location and scale, (see below for syntax examples).. 479 | 480 | The mathematical operations that can be performed on uncertain objects will 481 | work for any distribution supplied, but may be misleading if the supplied 482 | moments or distribution is not accurately defined. Here are some guidelines 483 | for creating UncertainVariable objects using some of the most common 484 | statistical distributions: 485 | 486 | +---------------------------+-------------+-------------------+-----+---------+ 487 | | Distribution | scipy.stats | args | loc | scale | 488 | | | class name | (shape params) | | | 489 | +===========================+=============+===================+=====+=========+ 490 | | Normal(mu, sigma) | norm | | mu | sigma | 491 | +---------------------------+-------------+-------------------+-----+---------+ 492 | | Uniform(a, b) | uniform | | a | b-a | 493 | +---------------------------+-------------+-------------------+-----+---------+ 494 | | Exponential(lamda) | expon | | | 1/lamda | 495 | +---------------------------+-------------+-------------------+-----+---------+ 496 | | Gamma(k, theta) | gamma | k | | theta | 497 | +---------------------------+-------------+-------------------+-----+---------+ 498 | | Beta(alpha, beta, [a, b]) | beta | alpha, beta | a | b-a | 499 | +---------------------------+-------------+-------------------+-----+---------+ 500 | | Log-Normal(mu, sigma) | lognorm | sigma | mu | | 501 | +---------------------------+-------------+-------------------+-----+---------+ 502 | | Chi-Square(k) | chi2 | k | | | 503 | +---------------------------+-------------+-------------------+-----+---------+ 504 | | F(d1, d2) | f | d1, d2 | | | 505 | +---------------------------+-------------+-------------------+-----+---------+ 506 | | Triangular(a, b, c) | triang | c | a | b-a | 507 | +---------------------------+-------------+-------------------+-----+---------+ 508 | | Student-T(v) | t | v | | | 509 | +---------------------------+-------------+-------------------+-----+---------+ 510 | | Weibull(lamda, k) | exponweib | lamda, k | | | 511 | +---------------------------+-------------+-------------------+-----+---------+ 512 | | Bernoulli(p) | bernoulli | p | | | 513 | +---------------------------+-------------+-------------------+-----+---------+ 514 | | Binomial(n, p) | binomial | n, p | | | 515 | +---------------------------+-------------+-------------------+-----+---------+ 516 | | Geometric(p) | geom | p | | | 517 | +---------------------------+-------------+-------------------+-----+---------+ 518 | | Hypergeometric(N, n, K) | hypergeom | N, n, K | | | 519 | +---------------------------+-------------+-------------------+-----+---------+ 520 | | Poisson(lamda) | poisson | lamda | | | 521 | +---------------------------+-------------+-------------------+-----+---------+ 522 | 523 | Thus, each distribution above would have the same call signature:: 524 | 525 | >>> import scipy.stats as ss 526 | >>> ss.your_dist_here(args, loc=loc, scale=scale) 527 | 528 | ANY SCIPY.STATS.DISTRIBUTION SHOULD WORK! IF ONE DOESN'T, PLEASE LET ME 529 | KNOW! 530 | 531 | Convenient constructors have been created to make assigning these 532 | distributions easier. They follow the parameter notation found in the 533 | respective Wikipedia articles: 534 | 535 | +---------------------------+---------------------------------------------------------------+ 536 | | MCERP Distibution | Wikipedia page | 537 | +===========================+===============================================================+ 538 | | N(mu, sigma) | http://en.wikipedia.org/wiki/Normal_distribution | 539 | +---------------------------+---------------------------------------------------------------+ 540 | | U(a, b) | http://en.wikipedia.org/wiki/Uniform_distribution_(continuous)| 541 | +---------------------------+---------------------------------------------------------------+ 542 | | Exp(lamda, [mu]) | http://en.wikipedia.org/wiki/Exponential_distribution | 543 | +---------------------------+---------------------------------------------------------------+ 544 | | Gamma(k, theta) | http://en.wikipedia.org/wiki/Gamma_distribution | 545 | +---------------------------+---------------------------------------------------------------+ 546 | | Beta(alpha, beta, [a, b]) | http://en.wikipedia.org/wiki/Beta_distribution | 547 | +---------------------------+---------------------------------------------------------------+ 548 | | LogN(mu, sigma) | http://en.wikipedia.org/wiki/Log-normal_distribution | 549 | +---------------------------+---------------------------------------------------------------+ 550 | | X2(df) | http://en.wikipedia.org/wiki/Chi-squared_distribution | 551 | +---------------------------+---------------------------------------------------------------+ 552 | | F(dfn, dfd) | http://en.wikipedia.org/wiki/F-distribution | 553 | +---------------------------+---------------------------------------------------------------+ 554 | | Tri(a, b, c) | http://en.wikipedia.org/wiki/Triangular_distribution | 555 | +---------------------------+---------------------------------------------------------------+ 556 | | T(df) | http://en.wikipedia.org/wiki/Student's_t-distribution | 557 | +---------------------------+---------------------------------------------------------------+ 558 | | Weib(lamda, k) | http://en.wikipedia.org/wiki/Weibull_distribution | 559 | +---------------------------+---------------------------------------------------------------+ 560 | | Bern(p) | http://en.wikipedia.org/wiki/Bernoulli_distribution | 561 | +---------------------------+---------------------------------------------------------------+ 562 | | B(n, p) | http://en.wikipedia.org/wiki/Binomial_distribution | 563 | +---------------------------+---------------------------------------------------------------+ 564 | | G(p) | http://en.wikipedia.org/wiki/Geometric_distribution | 565 | +---------------------------+---------------------------------------------------------------+ 566 | | H(M, n, N) | http://en.wikipedia.org/wiki/Hypergeometric_distribution | 567 | +---------------------------+---------------------------------------------------------------+ 568 | | Pois(lamda) | http://en.wikipedia.org/wiki/Poisson_distribution | 569 | +---------------------------+---------------------------------------------------------------+ 570 | 571 | Thus, the following are equivalent:: 572 | 573 | >>> x = N(10, 1) 574 | >>> x = uv(ss.norm(loc=10, scale=1)) 575 | 576 | Examples 577 | -------- 578 | A three-part assembly 579 | 580 | >>> x1 = N(24, 1) 581 | >>> x2 = N(37, 4) 582 | >>> x3 = Exp(2) # Exp(mu=0.5) works too 583 | 584 | >>> Z = (x1*x2**2)/(15*(1.5 + x3)) 585 | >>> Z 586 | uv(1161.46231679, 116646.762981, 0.345533974771, 3.00791101068) 587 | 588 | The result shows the mean, variance, and standardized skewness and kurtosis 589 | of the output variable Z, which will vary from use to use due to the random 590 | nature of Monte Carlo simulation and latin-hypercube sampling techniques. 591 | 592 | Basic math operations may be applied to distributions, where all 593 | statistical calculations are performed using latin-hypercube enhanced Monte 594 | Carlo simulation. Nearly all of the built-in trigonometric-, logarithm-, 595 | etc. functions of the ``math`` module have uncertainty-compatible 596 | counterparts that should be used when possible since they support both 597 | scalar values and uncertain objects. These can be used after importing the 598 | ``umath`` module:: 599 | 600 | >>> from mcerp.umath import * # sin(), sqrt(), etc. 601 | >>> sqrt(x1) 602 | uv(4.89791765647, 0.0104291897681, -0.0614940614672, 3.00264937735) 603 | 604 | At any time, the standardized statistics can be retrieved using:: 605 | 606 | >>> x1.mean 607 | >>> x1.var # x1.std (standard deviation) is also available 608 | >>> x1.skew 609 | >>> x1.kurt 610 | 611 | or all four together with:: 612 | 613 | >>> x1.stats 614 | 615 | By default, the Monte Carlo simulation uses 10000 samples, but this can be 616 | changed at any time with:: 617 | 618 | >>> mcerp.npts = number_of_samples 619 | 620 | Any value from 1,000 to 1,000,000 is recommended (more samples means more 621 | accurate, but also means more time required to perform the calculations). 622 | Although it can be changed, since variables retain their samples from one 623 | calculation to the next, this parameter should be changed before any 624 | calculations are performed to ensure parameter compatibility (this may 625 | change to be more dynamic in the future, but for now this is how it is). 626 | 627 | Also, to see the underlying distribution of the variable, and if matplotlib 628 | is installed, simply call its plot method:: 629 | 630 | >>> x1.plot() 631 | 632 | Optional kwargs can be any valid kwarg used by matplotlib.pyplot.plot 633 | 634 | See Also 635 | -------- 636 | N, U, Exp, Gamma, Beta, LogN, X2, F, Tri, PERT, T, Weib, Bern, B, G, H, 637 | Pois 638 | 639 | """ 640 | 641 | def __init__(self, rv, tag=None): 642 | 643 | assert hasattr( 644 | rv, "dist" 645 | ), "Input must be a distribution from the scipy.stats module." 646 | self.rv = rv 647 | 648 | # generate the latin-hypercube points 649 | self._mcpts = lhd(dist=self.rv, size=npts).flatten() 650 | self.tag = tag 651 | 652 | def plot(self, hist=False, show=False, **kwargs): 653 | """ 654 | Plot the distribution of the UncertainVariable. Continuous 655 | distributions are plotted with a line plot and discrete distributions 656 | are plotted with discrete circles. 657 | 658 | Optional 659 | -------- 660 | hist : bool 661 | If true, a histogram is displayed 662 | show : bool 663 | If ``True``, the figure will be displayed after plotting the 664 | distribution. If ``False``, an explicit call to ``plt.show()`` is 665 | required to display the figure. 666 | kwargs : any valid matplotlib.pyplot.plot kwarg 667 | 668 | """ 669 | import matplotlib.pyplot as plt 670 | 671 | if hist: 672 | vals = self._mcpts 673 | low = vals.min() 674 | high = vals.max() 675 | h = plt.hist( 676 | vals, 677 | bins=int(np.sqrt(len(vals)) + 0.5), 678 | histtype="stepfilled", 679 | density=True, 680 | **kwargs 681 | ) 682 | plt.ylim(0, 1.1 * h[0].max()) 683 | else: 684 | bound = 0.0001 685 | low = self.rv.ppf(bound) 686 | high = self.rv.ppf(1 - bound) 687 | if hasattr(self.rv.dist, "pmf"): 688 | low = int(low) 689 | high = int(high) 690 | vals = list(range(low, high + 1)) 691 | plt.plot(vals, self.rv.pmf(vals), "o", **kwargs) 692 | else: 693 | vals = np.linspace(low, high, 500) 694 | plt.plot(vals, self.rv.pdf(vals), **kwargs) 695 | plt.xlim(low - (high - low) * 0.1, high + (high - low) * 0.1) 696 | 697 | if show: 698 | self.show() 699 | 700 | 701 | uv = UncertainVariable # a nicer form for the user 702 | 703 | # DON'T MOVE THIS IMPORT!!! The prior definitions must be in place before 704 | # importing the correlation-related functions 705 | from .correlate import * 706 | from . import umath 707 | from . import stats 708 | 709 | 710 | ############################################################################### 711 | # Define some convenience constructors for common statistical distributions. 712 | # Hopefully these are a little easier/more intuitive to use than the 713 | # scipy.stats.distributions. 714 | ############################################################################### 715 | 716 | ############################################################################### 717 | # CONTINUOUS DISTRIBUTIONS 718 | ############################################################################### 719 | 720 | 721 | def Beta(alpha, beta, low=0, high=1, tag=None): 722 | """ 723 | A Beta random variate 724 | 725 | Parameters 726 | ---------- 727 | alpha : scalar 728 | The first shape parameter 729 | beta : scalar 730 | The second shape parameter 731 | 732 | Optional 733 | -------- 734 | low : scalar 735 | Lower bound of the distribution support (default=0) 736 | high : scalar 737 | Upper bound of the distribution support (default=1) 738 | """ 739 | assert ( 740 | alpha > 0 and beta > 0 741 | ), 'Beta "alpha" and "beta" parameters must be greater than zero' 742 | assert low < high, 'Beta "low" must be less than "high"' 743 | return uv(ss.beta(alpha, beta, loc=low, scale=high - low), tag=tag) 744 | 745 | 746 | def BetaPrime(alpha, beta, tag=None): 747 | """ 748 | A BetaPrime random variate 749 | 750 | Parameters 751 | ---------- 752 | alpha : scalar 753 | The first shape parameter 754 | beta : scalar 755 | The second shape parameter 756 | 757 | """ 758 | assert ( 759 | alpha > 0 and beta > 0 760 | ), 'BetaPrime "alpha" and "beta" parameters must be greater than zero' 761 | x = Beta(alpha, beta, tag) 762 | return x / (1 - x) 763 | 764 | 765 | def Bradford(q, low=0, high=1, tag=None): 766 | """ 767 | A Bradford random variate 768 | 769 | Parameters 770 | ---------- 771 | q : scalar 772 | The shape parameter 773 | low : scalar 774 | The lower bound of the distribution (default=0) 775 | high : scalar 776 | The upper bound of the distribution (default=1) 777 | """ 778 | assert q > 0, 'Bradford "q" parameter must be greater than zero' 779 | assert low < high, 'Bradford "low" parameter must be less than "high"' 780 | return uv(ss.bradford(q, loc=low, scale=high - low), tag=tag) 781 | 782 | 783 | def Burr(c, k, tag=None): 784 | """ 785 | A Burr random variate 786 | 787 | Parameters 788 | ---------- 789 | c : scalar 790 | The first shape parameter 791 | k : scalar 792 | The second shape parameter 793 | 794 | """ 795 | assert c > 0 and k > 0, 'Burr "c" and "k" parameters must be greater than zero' 796 | return uv(ss.burr(c, k), tag=tag) 797 | 798 | 799 | def ChiSquared(k, tag=None): 800 | """ 801 | A Chi-Squared random variate 802 | 803 | Parameters 804 | ---------- 805 | k : int 806 | The degrees of freedom of the distribution (must be greater than one) 807 | """ 808 | assert int(k) == k and k >= 1, 'Chi-Squared "k" must be an integer greater than 0' 809 | return uv(ss.chi2(k), tag=tag) 810 | 811 | 812 | Chi2 = ChiSquared # for more concise use 813 | 814 | 815 | def Erf(h, tag=None): 816 | """ 817 | An Error Function random variate. 818 | 819 | This distribution is derived from a normal distribution by setting 820 | m = 0 and s = 1/(h*sqrt(2)), and thus is used in similar situations 821 | as the normal distribution. 822 | 823 | Parameters 824 | ---------- 825 | h : scalar 826 | The scale parameter. 827 | """ 828 | assert h > 0, 'Erf "h" must be greater than zero' 829 | return Normal(0, 1 / (h * 2 ** 0.5), tag) 830 | 831 | 832 | def Erlang(k, lamda, tag=None): 833 | """ 834 | An Erlang random variate. 835 | 836 | This distribution is the same as a Gamma(k, theta) distribution, but 837 | with the restriction that k must be a positive integer. This 838 | is provided for greater compatibility with other simulation tools, but 839 | provides no advantage over the Gamma distribution in its applications. 840 | 841 | Parameters 842 | ---------- 843 | k : int 844 | The shape parameter (must be a positive integer) 845 | lamda : scalar 846 | The scale parameter (must be greater than zero) 847 | """ 848 | assert int(k) == k and k > 0, 'Erlang "k" must be a positive integer' 849 | assert lamda > 0, 'Erlang "lamda" must be greater than zero' 850 | return Gamma(k, lamda, tag) 851 | 852 | 853 | def Exponential(lamda, tag=None): 854 | """ 855 | An Exponential random variate 856 | 857 | Parameters 858 | ---------- 859 | lamda : scalar 860 | The inverse scale (as shown on Wikipedia). (FYI: mu = 1/lamda.) 861 | """ 862 | assert lamda > 0, 'Exponential "lamda" must be greater than zero' 863 | return uv(ss.expon(scale=1.0 / lamda), tag=tag) 864 | 865 | 866 | Exp = Exponential # for more concise use 867 | 868 | 869 | def ExtValueMax(mu, sigma, tag=None): 870 | """ 871 | An Extreme Value Maximum random variate. 872 | 873 | Parameters 874 | ---------- 875 | mu : scalar 876 | The location parameter 877 | sigma : scalar 878 | The scale parameter (must be greater than zero) 879 | """ 880 | assert sigma > 0, 'ExtremeValueMax "sigma" must be greater than zero' 881 | p = U(0, 1)._mcpts[:] 882 | return UncertainFunction(mu - sigma * np.log(-np.log(p)), tag=tag) 883 | 884 | 885 | EVMax = ExtValueMax # for more concise use 886 | 887 | 888 | def ExtValueMin(mu, sigma, tag=None): 889 | """ 890 | An Extreme Value Minimum random variate. 891 | 892 | Parameters 893 | ---------- 894 | mu : scalar 895 | The location parameter 896 | sigma : scalar 897 | The scale parameter (must be greater than zero) 898 | """ 899 | assert sigma > 0, 'ExtremeValueMin "sigma" must be greater than zero' 900 | p = U(0, 1)._mcpts[:] 901 | return UncertainFunction(mu + sigma * np.log(-np.log(1 - p)), tag=tag) 902 | 903 | 904 | EVMin = ExtValueMin # for more concise use 905 | 906 | 907 | def Fisher(d1, d2, tag=None): 908 | """ 909 | An F (fisher) random variate 910 | 911 | Parameters 912 | ---------- 913 | d1 : int 914 | Numerator degrees of freedom 915 | d2 : int 916 | Denominator degrees of freedom 917 | """ 918 | assert ( 919 | int(d1) == d1 and d1 >= 1 920 | ), 'Fisher (F) "d1" must be an integer greater than 0' 921 | assert ( 922 | int(d2) == d2 and d2 >= 1 923 | ), 'Fisher (F) "d2" must be an integer greater than 0' 924 | return uv(ss.f(d1, d2), tag=tag) 925 | 926 | 927 | F = Fisher # for more concise use 928 | 929 | 930 | def Gamma(k, theta, tag=None): 931 | """ 932 | A Gamma random variate 933 | 934 | Parameters 935 | ---------- 936 | k : scalar 937 | The shape parameter (must be positive and non-zero) 938 | theta : scalar 939 | The scale parameter (must be positive and non-zero) 940 | """ 941 | assert ( 942 | k > 0 and theta > 0 943 | ), 'Gamma "k" and "theta" parameters must be greater than zero' 944 | return uv(ss.gamma(k, scale=theta), tag=tag) 945 | 946 | 947 | def LogNormal(mu, sigma, tag=None): 948 | """ 949 | A Log-Normal random variate 950 | 951 | Parameters 952 | ---------- 953 | mu : scalar 954 | The location parameter 955 | sigma : scalar 956 | The scale parameter (must be positive and non-zero) 957 | """ 958 | assert sigma > 0, 'Log-Normal "sigma" must be positive' 959 | return uv(ss.lognorm(sigma, loc=mu), tag=tag) 960 | 961 | 962 | LogN = LogNormal # for more concise use 963 | 964 | 965 | def Normal(mu, sigma, tag=None): 966 | """ 967 | A Normal (or Gaussian) random variate 968 | 969 | Parameters 970 | ---------- 971 | mu : scalar 972 | The mean value of the distribution 973 | sigma : scalar 974 | The standard deviation (must be positive and non-zero) 975 | """ 976 | assert sigma > 0, 'Normal "sigma" must be greater than zero' 977 | return uv(ss.norm(loc=mu, scale=sigma), tag=tag) 978 | 979 | 980 | N = Normal # for more concise use 981 | 982 | 983 | def Pareto(q, a, tag=None): 984 | """ 985 | A Pareto random variate (first kind) 986 | 987 | Parameters 988 | ---------- 989 | q : scalar 990 | The scale parameter 991 | a : scalar 992 | The shape parameter (the minimum possible value) 993 | """ 994 | assert q > 0 and a > 0, 'Pareto "q" and "a" must be positive scalars' 995 | p = Uniform(0, 1, tag) 996 | return a * (1 - p) ** (-1.0 / q) 997 | 998 | 999 | def Pareto2(q, b, tag=None): 1000 | """ 1001 | A Pareto random variate (second kind). This form always starts at the 1002 | origin. 1003 | 1004 | Parameters 1005 | ---------- 1006 | q : scalar 1007 | The scale parameter 1008 | b : scalar 1009 | The shape parameter 1010 | """ 1011 | assert q > 0 and b > 0, 'Pareto2 "q" and "b" must be positive scalars' 1012 | return Pareto(q, b, tag) - b 1013 | 1014 | 1015 | def PERT(low, peak, high, g=4.0, tag=None): 1016 | """ 1017 | A PERT random variate 1018 | 1019 | Parameters 1020 | ---------- 1021 | low : scalar 1022 | Lower bound of the distribution support 1023 | peak : scalar 1024 | The location of the distribution's peak (low <= peak <= high) 1025 | high : scalar 1026 | Upper bound of the distribution support 1027 | 1028 | Optional 1029 | -------- 1030 | g : scalar 1031 | Controls the uncertainty of the distribution around the peak. Smaller 1032 | values make the distribution flatter and more uncertain around the 1033 | peak while larger values make it focused and less uncertain around 1034 | the peak. (Default: 4) 1035 | """ 1036 | a, b, c = [float(x) for x in [low, peak, high]] 1037 | assert a <= b <= c, 'PERT "peak" must be greater than "low" and less than "high"' 1038 | assert g >= 0, 'PERT "g" must be non-negative' 1039 | mu = (a + g * b + c) / (g + 2) 1040 | if mu == b: 1041 | a1 = a2 = 3.0 1042 | else: 1043 | a1 = ((mu - a) * (2 * b - a - c)) / ((b - mu) * (c - a)) 1044 | a2 = a1 * (c - mu) / (mu - a) 1045 | 1046 | return Beta(a1, a2, a, c, tag) 1047 | 1048 | 1049 | def StudentT(v, tag=None): 1050 | """ 1051 | A Student-T random variate 1052 | 1053 | Parameters 1054 | ---------- 1055 | v : int 1056 | The degrees of freedom of the distribution (must be greater than one) 1057 | """ 1058 | assert int(v) == v and v >= 1, 'Student-T "v" must be an integer greater than 0' 1059 | return uv(ss.t(v), tag=tag) 1060 | 1061 | 1062 | T = StudentT # for more concise use 1063 | 1064 | 1065 | def Triangular(low, peak, high, tag=None): 1066 | """ 1067 | A triangular random variate 1068 | 1069 | Parameters 1070 | ---------- 1071 | low : scalar 1072 | Lower bound of the distribution support 1073 | peak : scalar 1074 | The location of the triangle's peak (low <= peak <= high) 1075 | high : scalar 1076 | Upper bound of the distribution support 1077 | """ 1078 | assert low <= peak <= high, 'Triangular "peak" must lie between "low" and "high"' 1079 | low, peak, high = [float(x) for x in [low, peak, high]] 1080 | return uv( 1081 | ss.triang((1.0 * peak - low) / (high - low), loc=low, scale=(high - low)), 1082 | tag=tag, 1083 | ) 1084 | 1085 | 1086 | Tri = Triangular # for more concise use 1087 | 1088 | 1089 | def Uniform(low, high, tag=None): 1090 | """ 1091 | A Uniform random variate 1092 | 1093 | Parameters 1094 | ---------- 1095 | low : scalar 1096 | Lower bound of the distribution support. 1097 | high : scalar 1098 | Upper bound of the distribution support. 1099 | """ 1100 | assert low < high, 'Uniform "low" must be less than "high"' 1101 | return uv(ss.uniform(loc=low, scale=high - low), tag=tag) 1102 | 1103 | 1104 | U = Uniform # for more concise use 1105 | 1106 | 1107 | def Weibull(lamda, k, tag=None): 1108 | """ 1109 | A Weibull random variate 1110 | 1111 | Parameters 1112 | ---------- 1113 | lamda : scalar 1114 | The scale parameter 1115 | k : scalar 1116 | The shape parameter 1117 | """ 1118 | assert ( 1119 | lamda > 0 and k > 0 1120 | ), 'Weibull "lamda" and "k" parameters must be greater than zero' 1121 | return uv(ss.exponweib(lamda, k), tag=tag) 1122 | 1123 | 1124 | Weib = Weibull # for more concise use 1125 | 1126 | 1127 | ############################################################################### 1128 | # DISCRETE DISTRIBUTIONS 1129 | ############################################################################### 1130 | 1131 | 1132 | def Bernoulli(p, tag=None): 1133 | """ 1134 | A Bernoulli random variate 1135 | 1136 | Parameters 1137 | ---------- 1138 | p : scalar 1139 | The probability of success 1140 | """ 1141 | assert ( 1142 | 0 < p < 1 1143 | ), 'Bernoulli probability "p" must be between zero and one, non-inclusive' 1144 | return uv(ss.bernoulli(p), tag=tag) 1145 | 1146 | 1147 | Bern = Bernoulli # for more concise use 1148 | 1149 | 1150 | def Binomial(n, p, tag=None): 1151 | """ 1152 | A Binomial random variate 1153 | 1154 | Parameters 1155 | ---------- 1156 | n : int 1157 | The number of trials 1158 | p : scalar 1159 | The probability of success 1160 | """ 1161 | assert ( 1162 | int(n) == n and n > 0 1163 | ), 'Binomial number of trials "n" must be an integer greater than zero' 1164 | assert ( 1165 | 0 < p < 1 1166 | ), 'Binomial probability "p" must be between zero and one, non-inclusive' 1167 | return uv(ss.binom(n, p), tag=tag) 1168 | 1169 | 1170 | B = Binomial # for more concise use 1171 | 1172 | 1173 | def Geometric(p, tag=None): 1174 | """ 1175 | A Geometric random variate 1176 | 1177 | Parameters 1178 | ---------- 1179 | p : scalar 1180 | The probability of success 1181 | """ 1182 | assert ( 1183 | 0 < p < 1 1184 | ), 'Geometric probability "p" must be between zero and one, non-inclusive' 1185 | return uv(ss.geom(p), tag=tag) 1186 | 1187 | 1188 | G = Geometric # for more concise use 1189 | 1190 | 1191 | def Hypergeometric(N, n, K, tag=None): 1192 | """ 1193 | A Hypergeometric random variate 1194 | 1195 | Parameters 1196 | ---------- 1197 | N : int 1198 | The total population size 1199 | n : int 1200 | The number of individuals of interest in the population 1201 | K : int 1202 | The number of individuals that will be chosen from the population 1203 | 1204 | Example 1205 | ------- 1206 | (Taken from the wikipedia page) Assume we have an urn with two types of 1207 | marbles, 45 black ones and 5 white ones. Standing next to the urn, you 1208 | close your eyes and draw 10 marbles without replacement. What is the 1209 | probability that exactly 4 of the 10 are white? 1210 | :: 1211 | 1212 | >>> black = 45 1213 | >>> white = 5 1214 | >>> draw = 10 1215 | 1216 | # Now we create the distribution 1217 | >>> h = H(black + white, white, draw) 1218 | 1219 | # To check the probability, in this case, we can use the underlying 1220 | # scipy.stats object 1221 | >>> h.rv.pmf(4) # What is the probability that white count = 4? 1222 | 0.0039645830580151975 1223 | 1224 | """ 1225 | assert ( 1226 | int(N) == N and N > 0 1227 | ), 'Hypergeometric total population size "N" must be an integer greater than zero.' 1228 | assert ( 1229 | int(n) == n and 0 < n <= N 1230 | ), 'Hypergeometric interest population size "n" must be an integer greater than zero and no more than the total population size.' 1231 | assert ( 1232 | int(K) == K and 0 < K <= N 1233 | ), 'Hypergeometric chosen population size "K" must be an integer greater than zero and no more than the total population size.' 1234 | return uv(ss.hypergeom(N, n, K), tag=tag) 1235 | 1236 | 1237 | H = Hypergeometric # for more concise use 1238 | 1239 | 1240 | def Poisson(lamda, tag=None): 1241 | """ 1242 | A Poisson random variate 1243 | 1244 | Parameters 1245 | ---------- 1246 | lamda : scalar 1247 | The rate of an occurance within a specified interval of time or space. 1248 | """ 1249 | assert lamda > 0, 'Poisson "lamda" must be greater than zero.' 1250 | return uv(ss.poisson(lamda), tag=tag) 1251 | 1252 | 1253 | Pois = Poisson # for more concise use 1254 | 1255 | 1256 | ############################################################################### 1257 | # STATISTICAL FUNCTIONS 1258 | ############################################################################### 1259 | 1260 | 1261 | def covariance_matrix(nums_with_uncert): 1262 | """ 1263 | Calculate the covariance matrix of uncertain variables, oriented by the 1264 | order of the inputs 1265 | 1266 | Parameters 1267 | ---------- 1268 | nums_with_uncert : array-like 1269 | A list of variables that have an associated uncertainty 1270 | 1271 | Returns 1272 | ------- 1273 | cov_matrix : 2d-array-like 1274 | A nested list containing covariance values 1275 | 1276 | Example 1277 | ------- 1278 | 1279 | >>> x = N(1, 0.1) 1280 | >>> y = N(10, 0.1) 1281 | >>> z = x + 2*y 1282 | >>> covariance_matrix([x,y,z]) 1283 | [[ 9.99694861e-03 2.54000840e-05 1.00477488e-02] 1284 | [ 2.54000840e-05 9.99823207e-03 2.00218642e-02] 1285 | [ 1.00477488e-02 2.00218642e-02 5.00914772e-02]] 1286 | 1287 | """ 1288 | ufuncs = list(map(to_uncertain_func, nums_with_uncert)) 1289 | cov_matrix = [] 1290 | for (i1, expr1) in enumerate(ufuncs): 1291 | coefs_expr1 = [] 1292 | mean1 = expr1.mean 1293 | for (i2, expr2) in enumerate(ufuncs[: i1 + 1]): 1294 | mean2 = expr2.mean 1295 | coef = np.mean((expr1._mcpts - mean1) * (expr2._mcpts - mean2)) 1296 | coefs_expr1.append(coef) 1297 | cov_matrix.append(coefs_expr1) 1298 | 1299 | # We symmetrize the matrix: 1300 | for (i, covariance_coefs) in enumerate(cov_matrix): 1301 | covariance_coefs.extend(cov_matrix[j][i] for j in range(i + 1, len(cov_matrix))) 1302 | 1303 | return cov_matrix 1304 | 1305 | 1306 | def correlation_matrix(nums_with_uncert): 1307 | """ 1308 | Calculate the correlation matrix of uncertain variables, oriented by the 1309 | order of the inputs 1310 | 1311 | Parameters 1312 | ---------- 1313 | nums_with_uncert : array-like 1314 | A list of variables that have an associated uncertainty 1315 | 1316 | Returns 1317 | ------- 1318 | corr_matrix : 2d-array-like 1319 | A nested list containing covariance values 1320 | 1321 | Example 1322 | ------- 1323 | 1324 | >>> x = N(1, 0.1) 1325 | >>> y = N(10, 0.1) 1326 | >>> z = x + 2*y 1327 | >>> correlation_matrix([x,y,z]) 1328 | [[ 0.99969486 0.00254001 0.4489385 ] 1329 | [ 0.00254001 0.99982321 0.89458702] 1330 | [ 0.4489385 0.89458702 1. ]] 1331 | 1332 | """ 1333 | ufuncs = list(map(to_uncertain_func, nums_with_uncert)) 1334 | data = np.vstack([ufunc._mcpts for ufunc in ufuncs]) 1335 | return np.corrcoef(data.T, rowvar=0) 1336 | --------------------------------------------------------------------------------