diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..2db76ff --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,30 @@ +name: Python Package + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + - name: Lint with pylint + run: | + pip install pylint + pylint qexpy + - name: Test with pytest + run: | + pip install pytest + pytest -v --durations=0 diff --git a/.gitignore b/.gitignore index f1f2712..4296835 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,60 @@ -*.spyderworkspace -*.pyc -Run Test* -WorkList.txt -ToDo* -*.png -*.ipynb_checkpoints -*Plot* -*.pypirc -*.DS_Store* +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +.pytest_cache/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# Environments +.DS_Store +.idea +.venv +venv/ +env.bak/ +venv.bak/ +.vscode/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..3a6a697 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,587 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,venv + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + wrong-import-order, + ungrouped-imports, + import-outside-toplevel + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=95 + +# Maximum number of lines in a module. +max-module-lines=1500 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _, + x0, + dx, + pm, + pi, + e, + x, + n, + gs, + ax, + o, + a, + mc + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make, + _id, + _formula, + _unit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..dffa51e --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,24 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: html + configuration: docs/source/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - doc + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a823624 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: prep docs test + +prep: + pip install -r requirements.txt + pip install -e . + +docs: + cd docs && make html + open docs/build/html/index.html + +test: + @echo 'Checking Code Styles' + pylint qexpy + @echo 'Running Unit Tests' + cd tests && pytest -v --durations=0 diff --git a/README.md b/README.md index 64a9da1..32b4871 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,52 @@ QExPy (Queen’s Experimental Physics) is a python 3 package designed to facilitate data analysis in undergraduate physics laboratories. The package contains a module to easily propagate errors in uncertainty calculations, and a module that provides an intuitive interface to plot and fit data. The package is designed to be efficient, correct, and to allow for a pedagogic introduction to error analysis. The package is extensively tested in the Jupyter Notebook environment to allow high quality reports to be generated directly from a browser. -**Highlights**: - * Easily propagate uncertainties in measured quantities - * Compare different uncertainty calculations (e.g. Min-Max, quadrature errors, Monte Carlo errors) - * Correctly include correlations between quantities when propagating uncertainties (e.g. the uncertainty on x-x should always be 0!) - * Calculate exact numerical values of derivatives - * Choose display format (standard, Latex, scientific notation) - * Control the number of significant figures - * Handle ensembles of measurements (e.g. combine multiple measurements, with uncertainties, of a single quantity) - * Produce interactive plots of data in the browser - * Fit data to common functions (polynomials, gaussians) or provide custom functions - * Examine residual plots after fits - * Track units in calculations (still in development) - * Plot confidence bands from the errors in fitted parameters (still in development) - * Integrates with Jupyter notebooks, numpy, bokeh - -## Examples -Up to date examples are maintained in the examples directory of the repository. These are likely the best way to get acquainted with the package. - -## More information -Refer to the example notebooks in the examples/jupyter directory to learn how to use the package, and browse through the official documentstion. - -Read the documentation at http://qexpy.readthedocs.io/en/latest/intro.html +## Getting Started + +To install the package, type the following command in your terminal or [Anaconda](https://www.anaconda.com/distribution/#download-section) shell. + +```sh +$ pip install qexpy +``` + +## Usage + +It's recommanded to use this package in the Jupyter Notebook environment. + +```python +import qexpy as q +``` + +## Contributing + +With a local clone of this repository, if you wish to do development work, run the `make prep` in the project root directory, or run the following command explicitly: + +```shell script +pip install -r requirements.txt +pip install -e . +``` + +This will install pytest which we use for testing, and pylint which we use to control code quality, as well as necessary packages for generating documentation. + +Before submitting any change, you should run `make test` in the project root directory to make sure that your code matches all code style requirements and passes all unit tests. + + The following command checks your code against all code style requirements: + +```shell script +pylint qexpy +``` + +Navigate to the tests directory, and execute the following command to run all unit tests: + +```shell script +pytest -v --durations=0 +``` + +Documentation for this package is located in the docs directory. Run `make docs` in the project root directory to build the full documentation. The html page will open after the build is complete. + +Navigate to the docs directory, and run the following commands to build and see the full documentation page: + +```shell script +make html +open docs/build/html/index.html +``` diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..63063a1 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +build/**/* \ No newline at end of file diff --git a/docs/LICENSE.rst b/docs/LICENSE.rst deleted file mode 100644 index 34c13e3..0000000 --- a/docs/LICENSE.rst +++ /dev/null @@ -1,18 +0,0 @@ -License -======= - -QExPy - A scientific computing tool for physicists -Copyright (C) 2016 Connor Kapahi - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index b409d45..69fe55e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,192 +1,19 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext +SOURCEDIR = source +BUILDDIR = build +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/QExPy.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/QExPy.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/QExPy" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/QExPy" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index af1c263..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# QExPy documentation build configuration file, created by -# sphinx-quickstart on Fri Jun 10 15:57:35 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -import shlex -import sphinx_rtd_theme - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../..')) -sys.path.append(os.path.abspath('sphinxext')) -os.environ['BOKEH_DOCS_MISSING_API_KEY_OK'] = str(1) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'nbsphinx', - 'IPython.sphinxext.ipython_console_highlighting', - 'IPython.sphinxext.ipython_directive', - 'bokeh.sphinxext.bokeh_plot', - 'sphinx.ext.autosummary' -] - -# Default Pygments lexer for syntax highlighting in code cells -nbsphinx_codecell_lexer = 'python' - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'QExPy' -copyright = '2016, Connor Kapahi' -author = 'Connor Kapahi' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.0.5' -# The full version, including alpha/beta/rc tags. -release = '0.1.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build', '**.ipynb_checkpoints', 'conf.py'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = False - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "sphinx_rtd_theme" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -# The name for this set of Sphinx documents. If None, it defaults to -# "<#project> v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'QExPydoc' - -# -- Options for LaTeX output --------------------------------------------- - -#latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -#} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -#latex_documents = [ -# (master_doc, 'QExPy.tex', 'QExPy Documentation', -# 'Connor Kapahi', 'manual'), -#] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'qexpy', 'QExPy Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'QExPy', 'QExPy Documentation', - author, 'QExPy', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# The basename for the epub file. It defaults to the project name. -#epub_basename = project - -# The HTML theme for the epub output. Since the default themes are not optimized -# for small screen space, using the same theme for HTML and epub output is -# usually not wise. This defaults to 'epub', a theme designed to save visual -# space. -#epub_theme = 'epub' - -# The language of the text. It defaults to the language option -# or 'en' if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - -# Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' - -# Fix unsupported image types using the Pillow. -#epub_fix_images = False - -# Scale large images. -#epub_max_image_width = 0 - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' - -# If false, no index is generated. -#epub_use_index = True diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 49054a4..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. QExPy documentation master file, created by - sphinx-quickstart on Fri Jun 10 15:57:35 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to QExPy’s Documentation! -================================= - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - intro - uncertainties - operations - plotting - measurement - measurement_arr - plot - xyfitter - xydataset - LICENSE - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index 820cec2..0000000 --- a/docs/intro.rst +++ /dev/null @@ -1,84 +0,0 @@ -Introduction -============ - -QExPy (Queen's Experimental Physics) is a Python 3 package designed to facilitate data analysis in undergraduate physics laboratories. The package contains a module to easily propagate errors in uncertainty calculations, and a module that provides an intuitive interface to plot and fit data. The package is designed to be efficient, correct, and to allow for a pedagogic introduction to error analysis. The package is extensively tested in the Jupyter Notebook environment to allow high quality reports to be generated directly from a browser. - -Highlights: - * Easily propagate uncertainties in measured quantities - * Compare different uncertainty calculations (e.g. Min-Max, quadrature errors, Monte Carlo errors) - * Correctly include correlations between quantities when propagating uncertainties (e.g. the uncertainty on x-x should always be 0) - * Calculate exact numerical values of derivatives - * Choose display format (standard, Latex, scientific notation) - * Control the number of significant figures - * Handle ensembles of measurements (e.g. combine multiple measurements, with uncertainties, of a single quantity) - * Produce interactive plots of data in the browser - * Fit data to common functions (polynomials, gaussians) or provide custom functions - * Examine residual plots after fits - * Track units in calculations - * Plot confidence bands from the errors in fitted parameters - * Integrates with Jupyter notebooks, numpy, Bokeh - -Examples --------- - -Up to date Jupyter notebooks highlighting the features of QExPy can be found on `Github -`_. - -We can create :py:class:`.Measurement` objects to represent quantities with uncertainties, and propagate the error in those quantities. - -.. nbinput:: ipython3 - :execution-count: 1 - - #import the module - import qexpy as q - #declare 2 Measurements, x and y - x = q.Measurement(10,1) - y = q.Measurement(5,3) - #define a quantity that depends on x and y: - z = (x+y)/(x-y) - #print z, with the correct error - print(z) - -.. nboutput:: ipython3 - - 3.0 +/- 0.6 - - -The example below shows a case of plotting data and fitting them to a straight line: - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - # There are several ways to produce a Plot object from a set of data. - # Here, we pass the data directly to the plot object: - - fig1 = q.MakePlot(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - ydata = [0.9, 1.4, 2.5, 4.2, 5.7, 6., 7.3, 7.1, 8.9, 10.8], - yerr = 0.5, - xname = 'length', xunits='m', - yname = 'force', yunits='N', - data_name = 'mydata') - - # We can now fit the data, and display a plot (optionally) showing the residuals - fig1.fit("linear") - fig1.add_residuals() - fig1.show() - -.. nboutput:: ipython3 - - -----------------Fit results------------------- - Fit of mydata to linear - Fit parameters: - mydata_linear_fit0_fitpars_intercept = -0.3 +/- 0.4, - mydata_linear_fit0_fitpars_slope = 1.06 +/- 0.06 - - Correlation matrix: - [[ 1. -0.886] - [-0.886 1. ]] - - chi2/ndof = 0.71/7 - ---------------End fit results---------------- - -When fitting data, QExPy will output the fit results including values of the parameters, correlation and the chi-squared of the fit. diff --git a/docs/make.bat b/docs/make.bat index ba8772c..543c6b1 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\QExPy.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\QExPy.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/measurement.rst b/docs/measurement.rst deleted file mode 100644 index a872c84..0000000 --- a/docs/measurement.rst +++ /dev/null @@ -1,40 +0,0 @@ -The Measurement Object -====================== - -The propagation of measurements, including the propagation of errors is at -the heart of this package. This section will describe how Measurement -objects are created as well as all the methods available to operate on Measurements. - -.. autoclass:: qexpy.error.Measurement - -Properties ----------- - -Measurements have a few properties that you can get and set. Each property has an example of how to get and set it. - -.. autoattribute:: qexpy.error.Measurement.error_on_mean -.. autoattribute:: qexpy.error.Measurement.name -.. autoattribute:: qexpy.error.Measurement.relative_error -.. autoattribute:: qexpy.error.Measurement.mean -.. autoattribute:: qexpy.error.Measurement.std -.. autoattribute:: qexpy.error.Measurement.units - -Functions ---------- - -Measurements also have a number of functions that can be used to get, set or display information about the Measurement object. - -.. automethod:: qexpy.error.Measurement.get_covariance -.. automethod:: qexpy.error.Measurement.get_correlation -.. automethod:: qexpy.error.Measurement.get_data_array -.. automethod:: qexpy.error.Measurement.get_derivative -.. automethod:: qexpy.error.Measurement.get_units_str -.. automethod:: qexpy.error.Measurement.print_deriv_error -.. automethod:: qexpy.error.Measurement.print_mc_error -.. automethod:: qexpy.error.Measurement.print_min_max_error -.. automethod:: qexpy.error.Measurement.rename -.. automethod:: qexpy.error.Measurement.set_correlation -.. automethod:: qexpy.error.Measurement.set_covariance -.. automethod:: qexpy.error.Measurement.show_error_contribution -.. automethod:: qexpy.error.Measurement.show_histogram -.. automethod:: qexpy.error.Measurement.show_MC_histogram diff --git a/docs/measurement_arr.rst b/docs/measurement_arr.rst deleted file mode 100644 index d391ba3..0000000 --- a/docs/measurement_arr.rst +++ /dev/null @@ -1,28 +0,0 @@ -The MeasurementArray Object -=========================== - -When making a series of Measurements that need to be grouped together (i.e. for plotting or group calculations) a MeasurementArray should be used. This section describes the uses of MeasurementArray objects. - -.. autofunction:: qexpy.error.MeasurementArray -.. autoclass:: qexpy.error.Measurement_Array - -Properties ----------- -MeasurementArrays have a few properties that you can get and set. Each property has an example of how to get and set it. - -.. autoattribute:: qexpy.error.Measurement_Array.error_weighted_mean -.. autoattribute:: qexpy.error.Measurement_Array.mean -.. autoattribute:: qexpy.error.Measurement_Array.means -.. automethod:: qexpy.error.Measurement_Array.std -.. autoattribute:: qexpy.error.Measurement_Array.stds -.. autoattribute:: qexpy.error.Measurement_Array.units - -Functions ---------- -MeasurementArrays also have a number of functions that can be used to change the MeasurementArray or get information about it. - -.. automethod:: qexpy.error.Measurement_Array.append(meas) -.. automethod:: qexpy.error.Measurement_Array.delete -.. automethod:: qexpy.error.Measurement_Array.insert -.. automethod:: qexpy.error.Measurement_Array.get_units_str -.. automethod:: qexpy.error.Measurement_Array.show_table \ No newline at end of file diff --git a/docs/operations.rst b/docs/operations.rst deleted file mode 100644 index 90733a9..0000000 --- a/docs/operations.rst +++ /dev/null @@ -1,137 +0,0 @@ -Formatting -========== - -Naming ------- - -In addition to containing a mean and standard deviation, :py:class:`.Measurement` -objects can also have a string name and unit associated with it. -These can then be used both in printing values and in labelling any plots -created with these values. By default, :py:class:`.Measurement` objects are named -unnamed_var0, with a unique number assigned to each object. -The name and units of a :py:class:`.Measurement` object can be declared either when the -object is created or altered after. - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(10, 1, name='Length', units='cm') - # This value can be changed using the following method - - x.rename(name='Cable Length', units='m') - # Note that units are only a marker and changing units does not change - # any values with a Measurement - - print(x) - -.. nboutput:: ipython3 - - Cable Length = 10 +/- 1 - -Units ------ - -Values which have more complicated units can also be entered using the -following syntax. Consider a measurement of acceleration, with units of -m/s^2 or meters per second squared. This can be entered as a string of the -unit letters raised to the their respective powers: - -.. nbinput:: ipython3 - - import qexpy as q - - t = q.Measurement(3, 0.25, name='Time', units='s') - a = q.Measurement(10, 1, name='Acceleration', units='m^1 s^-2') - -This also allows for the units of values produced by operations such as -multiplication to be generated automatically. Consider the calculation of -the velocity of some object that accelerates at a for t seconds: - -.. nbinput:: ipython3 - - v = a*t - print(v.units) - -.. nboutput:: ipython3 - - {'m': 1, 's': -1} - -This unit list, when used in a plot will appear as: - -.. code-block:: python - - 'm^1 s^-1' - -Print Styles ------------- - -The default format of printing a value with an uncertainty is: - -.. nbinput:: ipython3 - - import qexpy as q - x = q.Measurement(10, 1) - print(x) - -.. nboutput:: ipython3 - - 10 +/- 1 - -However, there are two other ways of outputting a :py:class:`.Measurement` object. -Furthermore, each method also allows for a specific number of significant -digits to be shown. - -One method is called scientific and will output the number in scientific -notation with the error being shown as a value with only a single whole -digit. In order to change between any printing method, the following -function will change how the package prints a :py:class:`.Measurement` object: - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(122, 10) - q.set_print_style("Scientific") - print(x) - -.. nboutput:: ipython3 - - (12 +/- 1)*10**1 - -The same process is used for a print style called Latex which, as the name -suggests, is formatted for use in Latex documents. This may be useful in -the creation of labs by allowing variables to be copied and pasted -directly into a Latex document. - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(122, 10) - q.set_print_style("Latex") - print(x) - -.. nboutput:: ipython3 - - (12 \pm 1)\e1 - -Significant Figures -------------------- - -By default, QExPy will use the least significant figure in the uncertainty as the least significant figure. This won’t affect any of the calculations, but becomes apparent when printing the value of a :py:class:`Measurement`. The number of significant figures displayed when a :py:class:`Measurement` is printed can be changed using a the following function. - -.. automethod:: qexpy.error.set_sigfigs - :noindex: - -.. nbinput:: ipython3 - - import qexpy as q - x = q.Measurement(10, 1) - y = x/3 - q.set_sigfigs(3) - print(y) - -.. nboutput:: ipython3 - - 3.333 +/- 0.333 diff --git a/docs/plot.rst b/docs/plot.rst deleted file mode 100644 index 7591708..0000000 --- a/docs/plot.rst +++ /dev/null @@ -1,40 +0,0 @@ -The Plot Object -=============== - -When plotting and fitting data, a Plot object is used. A Plot object uses either a Bokeh or matplotlib backend in order to plot data. - -.. autofunction:: qexpy.plotting.MakePlot -.. autoclass:: qexpy.plotting.Plot - -Properties ----------- -Plots have a few properties that you can get and set. Each property has an example of how to get and set it. - -.. autoinstanceattribute:: qexpy.plotting.Plot.show_fit_results - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.bk_legend_location - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.mpl_legend_location - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.errorband_sigma - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.x_range - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.y_range - :annotation: -.. autoinstanceattribute:: qexpy.plotting.Plot.yres_range - :annotation: - - -Functions ---------- - -.. automethod:: qexpy.plotting.Plot.add_dataset -.. automethod:: qexpy.plotting.Plot.add_function -.. automethod:: qexpy.plotting.Plot.add_line -.. automethod:: qexpy.plotting.Plot.add_residuals -.. automethod:: qexpy.plotting.Plot.fit -.. automethod:: qexpy.plotting.Plot.show_table -.. automethod:: qexpy.plotting.Plot.set_labels -.. automethod:: qexpy.plotting.Plot.set_plot_range -.. automethod:: qexpy.plotting.Plot.show \ No newline at end of file diff --git a/docs/plotting.rst b/docs/plotting.rst deleted file mode 100644 index 7ca4e51..0000000 --- a/docs/plotting.rst +++ /dev/null @@ -1,243 +0,0 @@ -Plotting -======== - -This module is what will allow for both fitting and the creation of -figures. Similar to the process used in creating Measurement objects, -the creation and use of figures is based on the Plot object. -Furthermore, the actual figure itself can be rendered using both the -Bokeh and MatPlotLib Python packages. While each engine has different -benifits, this document will focus on the use of the Plot object, rather -than the plotting software itself. - -The Plot Object ---------------- - -:py:class:`Plots` created with QExPy are stored in a variable like any other value. -This variable can then be operated on to add or change different aspects -of the plot, such as lines of fit, user-defined functions or simply the -colour of data point. To choose which plot engine is used by the -package, the *.plot_engine* setting can be set to either *'mpl'*, or -*'bokeh'*. The plots generated by Bokeh are more interactive than those created performed by matplotlib, but take longer to generate. It is suggested that you use matplotlib when plotting large volumes of data. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - # This produces two sets of data which should be fit to a line with a - # slope of 3 and an intercept 2 - - figure = q.MakePlot([1, 2, 3, 4, 5], [5, 7, 11, 14, 17], xerr=0.5, yerr=1) - figure.show() - -For more detailed examples of plotting, please see the -`GitHub example notebooks`_. - -.. _GitHub example notebooks: https://github.com/Queens-Physics/qexpy/tree/master/examples/jupyter - -Using methods such as *.fit* will create a best fit of the data. -The *.fit* method also has arguments of what type of fit is -required and, if the model is not built-in to the module, an -initial guess of the fitting parameters. - -.. automethod:: qexpy.plotting.Plot.fit - :noindex: - -Note, for the *parguess* argument, the expected input is a list of -numbers which should be close to the true parameters. If said values -are not known, a list of ones, of the correct length will suffice, -although the fitting algorithm may take longer to complete. -For example: - -.. code-block:: python - - def model(x, pars): - return pars[0] + pars[1]*x - - # As this model requires two parameters a guess should be: - guess = [1, 1] - -Using these methods, a plot with a best fit line and residuals can -easily be constructed. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y = q.MeasurementArray([5, 7, 11, 14, 17], [1], name='Mass', units='g') - - figure = q.MakePlot(x, y) - figure.fit('linear') - figure.show_residuals = True - figure.show() - -The included models for fitting include: - -Linear: :math:`y=m x+b` - -Polynomial: :math:`y=\sum_{i=0}^{N} a_i x^i` with parameters :math:`a_i` - -Gaussian: :math:`y=\frac{1}{\sqrt{2 \pi \sigma}}\exp{-\frac{(x-\mu)^2}{\sigma}}` - -Decaying exponential: :math:`y=Ae^{-x\lambda}` - -Once fitted, the parameters of a fit can be stored by listing variables -as equal to the *.fit* method. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y = q.MeasurementArray([5, 7, 11, 14, 17], [1], name='Mass', units='g') - - figure = q.MakePlot(x, y) - m, b = figure.fit('linear') - figure.show_residuals = True - figure.show() - -Speeding it Up --------------- - -With large amounts of data, plotting can get slow. This is usually because the Monte Carlo error propagation is very calculation intensive. You can speed this up by setting q.quick_MC to true. This will no longer account for the correlation between variables, but will make plotting (and other calculations) faster. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - # Speeds up plotting - q.quick_MC = True - - x = q.MeasurementArray([1, 2, 3, 4, 5, 6, 7, 8, 9], [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]) - y = q.MeasurementArray([0.5, 0.9, 1.6, 2, 2.5, 2.9, 3.6, 4, 4.5], 0.1) - - figure = q.MakePlot(x, y) - - figure.show() - -Parameters of a Fit -------------------- - -A common non-linear fit used in physics is the normal, or Gaussian fit. -This function is built into the QExPy package and can be used as simply -as the linear fit function. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y = q.MeasurementArray([ 0.325, 0.882 , 0.882 , 0.325, 0.0439], [1], name='Mass', units='g') - - figure = q.MakePlot(x, y) - mu, sigma, norm = figure.fit('Gauss') - figure.show_residuals = True - figure.show() - -User-Defined Fits ------------------ - -A user defined function can be plotted using the *.fit* method as -we have previously done for curve fits and residual outputs. -To add a theoretical curve, or any other curve: - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y = q.MeasurementArray([5, 7, 11, 14, 17], [1], name='Mass', units='g') - - figure = q.MakePlot(x, y) - - def theoretical(x, *pars): - return pars[0] + pars[1]*x - - figure.fit(model=theoretical, parguess=[2, 2]) - figure.show() - -The final method relevant to :py:class:`Plot` objects is the show method. -This, by default will output the Bokeh plot in a terminal, or output of a -Jupyter notebook, if that is where the code is executed. -This method does have an optional argument that determines where the plot -is shown, with options of 'inline' and 'file'. The 'inline' option is -selected by default and refers to output in the console line itself, -while 'file' creates an HTML file that should open in your default -browser and save to whatever location your Python code file is currently -in. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y = q.MeasurementArray([5, 7, 11, 14, 17], [1], name='Applied Mass', units='g') - - figure = q.MakePlot(x, y) - figure.show('file') - -For this code, there is no output, as the plot will be saved in the -working directory and opened in a browser. For example, if the above -code is located in *Diligent_Physics_Student/Documents/Python* then the -HTML file will also be in said */Python* folder. - -Plotting Multiple Datasets --------------------------- - -In many cases, multiple sets of data must be shown on a single plot, -possibly with multiple residuals. In this case, another :py:class:`XYDataSet` object -must be created and added with the following method: - -.. automethod:: qexpy.plotting.Plot.add_dataset - :noindex: - -This method is used by creating a separate :py:class:`XYDataSet` object and adding it -to the other plot. - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - x1 = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y1 = q.MeasurementArray([5, 7, 11, 14, 17], [1], name='Applied Mass', units='g') - - figure = q.MakePlot(x1, y1) - figure.fit('linear') - figure.show_residuals = True - - x2 = q.MeasurementArray([1, 2, 3, 4, 5], [0.5], name='Length', units='cm') - y2 = q.MeasurementArray([4, 8, 13, 12, 19], [1], name='Applied Mass', units='g') - - data2 = q.XYDataSet(x2, y2) - figure.add_dataset(data2) - - figure.show() - -Plotting Functions ------------------- - -Sometimes you want to draw functions that aren’t a fit of a dataset, and QExPy can do that too. The process is similar to that for drawing fits, but using the *.add_function* function instead of the *.fit* function, as shown below: - -.. bokeh-plot:: - :source-position: above - - import qexpy as q - - def func(x, *pars): - return pars[0] + pars[1]*x - - figure = q.MakePlot() - - # This function is not related to any data. - figure.add_function(func, name="Function", pars = [1, 5], - color = 'saddlebrown', x_range =[-10,10]) - - figure.show() \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 245a97a..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -nbsphinx -ipykernel -bokeh>=0.12.6 -qexpy -sphinx>=1.4 -pandas -matplotlib -numpy -ipywidgets -scipy>=0.17 \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..532cc5e --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('../..')) + +# -- Project information ----------------------------------------------------- + +project = 'QExPy' +copyright = '2019, Astral Cai, Connor Kapahi, Prof. Ryan Martin' +author = 'Astral Cai, Connor Kapahi, Prof. Ryan Martin' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '3.0.0' + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'nbsphinx', + 'nbsphinx_link', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx_autodoc_typehints', + 'sphinx.ext.todo', + 'IPython.sphinxext.ipython_console_highlighting' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'QExPydoc' + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'QExPy.tex', 'QExPy Documentation', + 'Astral Cai, Connor Kapahi, Prof. Ryan Martin', 'manual'), +] + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'qexpy', 'QExPy Documentation', + [author], 1) +] + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'QExPy', 'QExPy Documentation', + author, 'QExPy', 'One line description of project.', + 'Miscellaneous'), +] + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/source/data.rst b/docs/source/data.rst new file mode 100644 index 0000000..b5ee5b9 --- /dev/null +++ b/docs/source/data.rst @@ -0,0 +1,24 @@ +============================ +The ExperimentalValue Object +============================ + +.. autoclass:: qexpy.data.data.ExperimentalValue + +Properties +---------- + +.. autoattribute:: qexpy.data.data.ExperimentalValue.value +.. autoattribute:: qexpy.data.data.ExperimentalValue.error +.. autoattribute:: qexpy.data.data.ExperimentalValue.relative_error +.. autoattribute:: qexpy.data.data.ExperimentalValue.name +.. autoattribute:: qexpy.data.data.ExperimentalValue.unit + +Methods +------- + +.. automethod:: qexpy.data.data.ExperimentalValue.derivative +.. automethod:: qexpy.data.data.ExperimentalValue.get_covariance +.. automethod:: qexpy.data.data.ExperimentalValue.set_covariance +.. automethod:: qexpy.data.data.ExperimentalValue.get_correlation +.. automethod:: qexpy.data.data.ExperimentalValue.set_correlation + diff --git a/docs/source/error_propagation.rst b/docs/source/error_propagation.rst new file mode 100644 index 0000000..8e171b8 --- /dev/null +++ b/docs/source/error_propagation.rst @@ -0,0 +1,50 @@ +================= +Error Propagation +================= + +Error propagation is implemented as a child class of :py:class:`ExperimentalValue` called :py:class:`DerivedValue`. When working with QExPy, the result of all computations are stored as instances of this class. + +The DerivedValue Object +======================= + +.. autoclass:: qexpy.data.data.DerivedValue + +Properties +---------- + +.. autoattribute:: qexpy.data.data.DerivedValue.value +.. autoattribute:: qexpy.data.data.DerivedValue.error +.. autoattribute:: qexpy.data.data.DerivedValue.relative_error +.. autoattribute:: qexpy.data.data.DerivedValue.error_method +.. autoattribute:: qexpy.data.data.DerivedValue.mc + +Methods +------- + +.. automethod:: qexpy.data.data.DerivedValue.reset_error_method +.. automethod:: qexpy.data.data.DerivedValue.recalculate +.. automethod:: qexpy.data.data.DerivedValue.show_error_contributions + +The MonteCarloSettings Object +============================= + +QExPy provides users with many options to customize Monte Carlo error propagation. Each :py:class:`DerivedValue` object stores a :py:class:`MonteCarloSettings` object that contains some settings for the Monte Carlo error propagation of this value. + +.. autoclass:: qexpy.data.utils.MonteCarloSettings + +Properties +---------- + +.. autoattribute:: qexpy.data.utils.MonteCarloSettings.sample_size +.. autoattribute:: qexpy.data.utils.MonteCarloSettings.confidence +.. autoattribute:: qexpy.data.utils.MonteCarloSettings.xrange + +Methods +------- + +.. automethod:: qexpy.data.utils.MonteCarloSettings.set_xrange +.. automethod:: qexpy.data.utils.MonteCarloSettings.use_mode_with_confidence +.. automethod:: qexpy.data.utils.MonteCarloSettings.use_mean_and_std +.. automethod:: qexpy.data.utils.MonteCarloSettings.show_histogram +.. automethod:: qexpy.data.utils.MonteCarloSettings.samples +.. automethod:: qexpy.data.utils.MonteCarloSettings.use_custom_value_and_error diff --git a/docs/source/fitting.rst b/docs/source/fitting.rst new file mode 100644 index 0000000..b6e717e --- /dev/null +++ b/docs/source/fitting.rst @@ -0,0 +1,19 @@ +================== +The Fitting Module +================== + +.. autofunction:: qexpy.fitting.fit + +The XYFitResult Class +--------------------- + +.. autoclass:: qexpy.fitting.fitting.XYFitResult + +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.dataset +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.fit_function +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.params +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.residuals +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.chi_squared +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.ndof +.. autoattribute:: qexpy.fitting.fitting.XYFitResult.xrange + diff --git a/docs/source/getting_started.nblink b/docs/source/getting_started.nblink new file mode 100644 index 0000000..56bc2e4 --- /dev/null +++ b/docs/source/getting_started.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../examples/getting_started.ipynb" +} \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..67afd6c --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,26 @@ +.. QExPy documentation master file + +Welcome to QExPy's documentation! +================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + intro + getting_started.nblink + plotting_and_fitting.nblink + data + measurements + measurement_array + error_propagation + xydata + fitting + plotting + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/intro.rst b/docs/source/intro.rst new file mode 100644 index 0000000..1a394b1 --- /dev/null +++ b/docs/source/intro.rst @@ -0,0 +1,15 @@ +============ +Introduction +============ + +QExPy (Queen’s Experimental Physics) is a Python 3 package designed to facilitate data analysis in undergraduate physics laboratories. The package contains a module to easily propagate errors in uncertainty calculations, and a module that provides an intuitive interface to plot and fit data. The package is designed to be efficient, correct, and to allow for a pedagogic introduction to error analysis. The package is extensively tested in the Jupyter Notebook environment to allow high quality reports to be generated directly from a browser. + +Highlights: + * Easily propagate uncertainties in calculations involving measured quantities + * Compare different methods of error propagation (e.g. Quadrature errors, Monte Carlo errors) + * Correctly include correlations between quantities when propagating uncertainties + * Calculate derivatives of calculated values with respect to the measured quantities from which the value is derived + * Flexible display formats for values and their uncertainties (e.g. number of significant figures, different ways of displaying units, scientific notation) + * Smart unit tracking in calculations (in development) + * Fit data to common functions (polynomials, gaussian distribution) or any custom functions specified by the user + * Intuitive interface for data plotting built on matplotlib diff --git a/docs/source/measurement_array.rst b/docs/source/measurement_array.rst new file mode 100644 index 0000000..55d8ce9 --- /dev/null +++ b/docs/source/measurement_array.rst @@ -0,0 +1,28 @@ +=========================== +The MeasurementArray Object +=========================== + +Using QExPy, the user is able to record a series of measurements, and store them in an array. This feature is implemented in QExPy as a wrapper around :py:class:`numpy.ndarray`. The :py:class:`.ExperimentalValueArray` class, also given the alias :py:class:`.MeasurementArray` stores an array of values with uncertainties, and it also comes with methods for some basic data processing. + +.. autoclass:: qexpy.data.datasets.ExperimentalValueArray + +Properties +========== + +.. autoattribute:: qexpy.data.datasets.ExperimentalValueArray.values +.. autoattribute:: qexpy.data.datasets.ExperimentalValueArray.errors +.. autoattribute:: qexpy.data.datasets.ExperimentalValueArray.name +.. autoattribute:: qexpy.data.datasets.ExperimentalValueArray.unit + +Methods +======= + +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.mean +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.std +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.sum +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.error_on_mean +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.error_weighted_mean +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.propagated_error +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.append +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.delete +.. automethod:: qexpy.data.datasets.ExperimentalValueArray.insert diff --git a/docs/source/measurements.rst b/docs/source/measurements.rst new file mode 100644 index 0000000..65d6b66 --- /dev/null +++ b/docs/source/measurements.rst @@ -0,0 +1,50 @@ +====================== +The Measurement Object +====================== + +To record values with an uncertainty, we use the :py:class:`.MeasuredValue` object. It is a child class of :py:class:`.ExperimentalValue`, so it inherits all attributes and methods from the :py:class:`.ExperimentalValue` class. + +.. autoclass:: qexpy.data.data.MeasuredValue + +Repeated Measurements +===================== + +To record a value as the mean of a series of repeated measurements, use :py:class:`.RepeatedlyMeasuredValue` + +.. autoclass:: qexpy.data.data.RepeatedlyMeasuredValue + +Properties +---------- + +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.raw_data +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.mean +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.error_weighted_mean +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.std +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.error_on_mean +.. autoattribute:: qexpy.data.data.RepeatedlyMeasuredValue.propagated_error + +Methods +------- + +.. automethod:: qexpy.data.data.RepeatedlyMeasuredValue.use_std_for_uncertainty +.. automethod:: qexpy.data.data.RepeatedlyMeasuredValue.use_error_on_mean_for_uncertainty +.. automethod:: qexpy.data.data.RepeatedlyMeasuredValue.use_error_weighted_mean_as_value +.. automethod:: qexpy.data.data.RepeatedlyMeasuredValue.use_propagated_error_for_uncertainty +.. automethod:: qexpy.data.data.RepeatedlyMeasuredValue.show_histogram + +Correlated Measurements +======================= + +Sometimes in experiments, two measured quantities can be correlated, and this correlation needs to be accounted for during error propagation. QExPy provides methods that allows users to specify the correlation between two measurements, and it will be taken into account automatically during computations. + +.. autofunction:: qexpy.data.data.set_correlation +.. autofunction:: qexpy.data.data.get_correlation +.. autofunction:: qexpy.data.data.set_covariance +.. autofunction:: qexpy.data.data.get_covariance + +There are also shortcuts to the above methods implemented in :py:class:`.ExperimentalValue`. + +.. automethod:: qexpy.data.data.MeasuredValue.set_correlation +.. automethod:: qexpy.data.data.MeasuredValue.get_correlation +.. automethod:: qexpy.data.data.MeasuredValue.set_covariance +.. automethod:: qexpy.data.data.MeasuredValue.get_covariance diff --git a/docs/source/plotting.rst b/docs/source/plotting.rst new file mode 100644 index 0000000..899076a --- /dev/null +++ b/docs/source/plotting.rst @@ -0,0 +1,35 @@ +=================== +The Plotting Module +=================== + +.. autofunction:: qexpy.plotting.plotting.plot +.. autofunction:: qexpy.plotting.plotting.hist +.. autofunction:: qexpy.plotting.plotting.show + +The Plot Object +=============== + +.. autoclass:: qexpy.plotting.plotting.Plot + +Properties +---------- + +.. autoattribute:: qexpy.plotting.plotting.Plot.title +.. autoattribute:: qexpy.plotting.plotting.Plot.xname +.. autoattribute:: qexpy.plotting.plotting.Plot.yname +.. autoattribute:: qexpy.plotting.plotting.Plot.xunit +.. autoattribute:: qexpy.plotting.plotting.Plot.yunit +.. autoattribute:: qexpy.plotting.plotting.Plot.xlabel +.. autoattribute:: qexpy.plotting.plotting.Plot.ylabel +.. autoattribute:: qexpy.plotting.plotting.Plot.xrange + +Methods +------- + +.. automethod:: qexpy.plotting.plotting.Plot.plot +.. automethod:: qexpy.plotting.plotting.Plot.hist +.. automethod:: qexpy.plotting.plotting.Plot.fit +.. automethod:: qexpy.plotting.plotting.Plot.show +.. automethod:: qexpy.plotting.plotting.Plot.legend +.. automethod:: qexpy.plotting.plotting.Plot.error_bars +.. automethod:: qexpy.plotting.plotting.Plot.residuals diff --git a/docs/source/plotting_and_fitting.nblink b/docs/source/plotting_and_fitting.nblink new file mode 100644 index 0000000..dca6f1e --- /dev/null +++ b/docs/source/plotting_and_fitting.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../examples/plotting_and_fitting.ipynb" +} \ No newline at end of file diff --git a/docs/source/xydata.rst b/docs/source/xydata.rst new file mode 100644 index 0000000..290f2b2 --- /dev/null +++ b/docs/source/xydata.rst @@ -0,0 +1,22 @@ +==================== +The XYDataSet Object +==================== + +.. autoclass:: qexpy.data.XYDataSet + +Properties +========== + +.. autoattribute:: qexpy.data.XYDataSet.xvalues +.. autoattribute:: qexpy.data.XYDataSet.xerr +.. autoattribute:: qexpy.data.XYDataSet.yvalues +.. autoattribute:: qexpy.data.XYDataSet.yerr +.. autoattribute:: qexpy.data.XYDataSet.xname +.. autoattribute:: qexpy.data.XYDataSet.yname +.. autoattribute:: qexpy.data.XYDataSet.xunit +.. autoattribute:: qexpy.data.XYDataSet.yunit + +Methods +======= + +.. automethod:: qexpy.data.XYDataSet.fit diff --git a/docs/uncertainties.rst b/docs/uncertainties.rst deleted file mode 100644 index ddc834b..0000000 --- a/docs/uncertainties.rst +++ /dev/null @@ -1,279 +0,0 @@ -Error Propagation -================= - -QExpy is designed to facilitate the propagation of errors from a measurement -(or set of measurement) to quantities that depend on that (those) measurement(s). -This section will describe how :py:class:`.Measurement` -objects are created and used in calculations. Furthermore, features such -as the calculation of the exact numerical derivative of expressions will be -outlined. While some aspects of this documentation will not necessarily be -required to work with the package itself, many of the methods used in the -underlying code can be useful to understand. - -Creating Measurement Objects ----------------------------- - -The object that will be used most commonly is the :py:class:`.Measurement` class. This -object can store the mean, standard deviation, original data, name, units, -and other attributes which can be used by other elements of this package. - -.. autoclass:: qexpy.error.Measurement - :noindex: - -The arguments, or \*args, of this class can be entered in several forms: - -A mean and standard deviation can be entered directly. - -.. nbinput:: ipython3 - - import qexpy as q - x = q.Measurement(10, 1) - # This would create an object with a mean of 10 and a standard - # deviation of 1. - -A list or numpy array of values can be provided, from which the mean and -standard deviation of the values is calculated. These values can be -outputted by calling for the mean and std attributes of the object. - -.. nbinput:: ipython3 - - x = q.Measurement([9, 10, 11]) - # This would also produce an object with a mean of 10 and a standard - # deviation of 1. This can be shown by calling for x.mean and x.std: - - print(x.mean, x.std) - -.. nboutput:: ipython3 - - 10, 1 - -If several measurements, each with an associated error needs to be entered, -a :py:func:`MeasurementArray` should be used. There are a few ways to create a :py:func:`MeasurementArray`: - -For example, given measurements 10 +/- 1, 9 +/- 0.5 and 11 +/- 0.25, the -data can be entered as either: - -.. nbinput:: ipython3 - - x = q.MeasurementArray(data=[(10, 1), (9, 0.5), (11, 0.25)]) - y = q.MeasurementArray(data=[10, 9, 11], error=[1, 0.5, 0.25]) - # The mean and standard deviation of x and y are the same - -If the error associated with each measured value is the same, a single -value can be entered into the second list in the *y* example shown above. -This is done simply for efficiency and is treated as a list of repeated -values. - -.. nbinput:: ipython3 - - x = q.MeasurementArray([9, 10, 11], 1) - # This is equivalent to: - y = q.MeasurementArray([9, 10, 11], [1, 1, 1]) - -A :py:func:`MeasurementArray` can also be created from an array of -:py:class:`.Measurement` objects. If you have a group of :py:class:`Measurement` -objects and want to group them together, this can be done by creating a -:py:func:`MeasurementArray` containing them. - -.. nbinput:: ipython3 - - x = q.Measurement(9,1) - y = q.Measurement(10,1) - z = q.Measurement(11,1) - - arr = q.MeasurementArray([x, y, z]) - -In all cases, the optional arguments *name* and *units* can be used to -include strings for both of these parameters as shown below: - -.. nbinput:: ipython3 - - x = q.Measurement(10, 1, name='Length', units='cm') - print(x) - -.. nboutput:: ipython3 - - Length = 10 +/- 1 [cm] - -Working with Measurement Objects --------------------------------- - -Once created, :py:class:`Measurement` objects can be operated on just as any other value: - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(10, 1) - y = q.Measurement(3, 0.1) - z = x-y - - print(z) - -.. nboutput:: ipython3 - - 7 +/- 1 - -:py:class:`Measurement` objects can also be compared based on the value of their means: - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(10, 1) - y = q.Measurement(3, 0.1) - print(x>y) - -.. nboutput:: ipython3 - - True - -Elementary functions such as the trig functions, inverse trig functions, -natural logarithm and exponential function can also be used: - -.. nbinput:: ipython3 - - f = q.sin(z) - print(f) - -.. nboutput:: ipython3 - - 0.7 +/- 0.8 - -Furthermore, the use of :py:class:`.Measurement` objects in equations also allows for the -calculation of the derivative of these expressions with respect to any of -the :py:class:`.Measurement` objects used. - -.. nbinput:: ipython3 - - d1 = f.get_derivative(x) - - # This can be compared to the analytic expression of the derivative - d2 = m.cos(10-3) - print(d1 == d2) - -.. nboutput:: ipython3 - - True - -This derivative method is what is used to propagate error by the error -propagation formula. - -.. math:: - - For\ some\ F(x,y): - \sigma_F^2 = (\frac{\partial F}{\partial x} \sigma_x)^2 \ - + (\frac{\partial F}{\partial y} \sigma_y)^2 - -This formula is the default method of error propagation and will be -accurate in most cases when the error is small. - -Methods of Propagating Error ----------------------------- - -While the default method of propagating error is the derivative formula, -there are a number of other methods by which error can be calculated. -In addition to the derivative method, this package is also capable of -calculating error by the Monte Carlo and min-max methods. While this -documentation will not go into detail about how these methods work, the -output of each method is available by default, and a specific method can be -chosen as shown below. - -.. nbinput:: ipython3 - - import qexpy as q - - x = q.Measurement(13,2) - y = q.Measurement(2,0.23) - z = x**2 - x/y - - print([z.mean, z.std]) - print(z.MC) - print(z.MinMax) - -.. nboutput:: ipython3 - - [162.5, 51.00547770828149] - [162.88454043577516, 51.509516186100562] - [166.29634415140231, 53.770920422588731] - -While the Monte Carlo and min-max output of the default method are not as -elegant as the derivative method, it does provide an easy avenue to check -the error against another method to ensure accuracy. - -Furthermore, the output can be limited to a single method if desired. -In this case, the output seen in the *print(z)* line would be from whatever -method is chosen. - -.. nbinput:: ipython3 - - x = q.Measurement(10,2) - y = q.Measurement(5,1) - - q.set_error_method("Derivative") - # This option will limit the error calculation to using the derivative - # formula - - z = x-y - z.rename(name='Derivative Method') - - q.set_error_method("Monte Carlo") - # This option will limit the error calculation to using the derivative - # formula - - z = x-y - z.rename(name='Monte Carlo') - - q.set_error_method("Min Max") - # This option will limit the error calculation to using the derivative - # formula - - z = x-y - z.rename(name="Min Max") - -Correlation ------------ - -For many experiments, parameters may be correlated or may be expected to be -correlated. Thus, there exists methods to define and, in the case that the -arrays of data used to create two :py:class:`Measurement` objects are equal -in length, return the covariance or correlation of some parameters. There are -methods which can be used to set or get the correlation or covariance of two -variables. - -.. automethod:: qexpy.error.Measurement.set_covariance - :noindex: - -.. automethod:: qexpy.error.Measurement.get_covariance - :noindex: - -.. automethod:: qexpy.error.Measurement.set_correlation - :noindex: - -.. automethod:: qexpy.error.Measurement.get_correlation - :noindex: - -Furthermore, the covariance and correlation of the fitted parameters is found -automatically by the :py:meth:`.Plot.fit()` method. - - -Derivatives ------------ - -The method by which numerical solutions to the derivative of expressions -are evaluated is called automatic differentiation. This method relies on -the chain rule and the fact that the derivative of any expression can be -reduced to some combination of elementary functions and operations. -Consider the following function. - -.. math:: - - f(x,y) &= \sin{xy} \\ - \implies \partial_x f(x,y) &= y\cos{xy} \quad \textrm{Let} \quad z=xy \\ - \partial_x f(x,y) &= \frac{\partial z}{\partial x} \cos{z} = y\cos{xy} - -What this example illustrates is how, by considering an expression as a -series of elementary operations and functions, the exact numerical -derivative can be calculated. All that is required is to be able to store -the derivative of each of these elementary operations with respect to -whatever variables are involved. diff --git a/docs/xydataset.rst b/docs/xydataset.rst deleted file mode 100644 index 9ccd326..0000000 --- a/docs/xydataset.rst +++ /dev/null @@ -1,22 +0,0 @@ -The XYDataSet Object -==================== - -An XYDataset is used to store x and y data and is used in plotting. - -.. autoclass:: qexpy.fitting.XYDataSet - -Properties ----------- -.. autoinstanceattribute:: qexpy.fitting.XYDataSet.name - :annotation: - -Functions ---------- -.. automethod:: qexpy.fitting.XYDataSet.clear_fits -.. automethod:: qexpy.fitting.XYDataSet.fit -.. automethod:: qexpy.fitting.XYDataSet.show_table -.. automethod:: qexpy.fitting.XYDataSet.get_x_range -.. automethod:: qexpy.fitting.XYDataSet.get_y_range -.. automethod:: qexpy.fitting.XYDataSet.get_yres_range -.. automethod:: qexpy.fitting.XYDataSet.print_fit_results -.. automethod:: qexpy.fitting.XYDataSet.save_textfile \ No newline at end of file diff --git a/docs/xyfitter.rst b/docs/xyfitter.rst deleted file mode 100644 index 26adf65..0000000 --- a/docs/xyfitter.rst +++ /dev/null @@ -1,10 +0,0 @@ -The XYFitter Object -=================== - -When fitting data, an XYFitter object is used behind the scenes. It contains the functionality to fit data to a number of built-in functions, or have the user define their own. - -.. autoclass:: qexpy.fitting.XYFitter - -Functions ---------- -.. automethod:: qexpy.fitting.XYFitter.fit \ No newline at end of file diff --git a/examples/getting_started.ipynb b/examples/getting_started.ipynb new file mode 100644 index 0000000..ee661b0 --- /dev/null +++ b/examples/getting_started.ipynb @@ -0,0 +1,891 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import qexpy as q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The core of QExPy is a data structure called ```ExperimentalValue```, which represents a value with an uncertainty. Any measurements recorded with QExPy and the result of any data analysis done with these measurements will all be wrapped in an instance of this class." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mass = 15.0 +/- 0.5 [kg]\n" + ] + } + ], + "source": [ + "# a measurement can be taken with a value, an uncertainty, a unit and a name \n", + "# (the last two are optional)\n", + "m = q.Measurement(15, 0.5, unit=\"kg\", name=\"mass\")\n", + "print(m)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean: 5.0\n", + "Error on the mean: 0.10327955589886441\n", + "Standard deviation: 0.2529822128134702\n" + ] + } + ], + "source": [ + "# multiple measurements can be taken towards the same quantity\n", + "t = q.Measurement([5, 4.9, 5.3, 4.7, 4.8, 5.3], unit=\"s\", name=\"time\")\n", + "\n", + "# this measurement will contain some basic statistic properties\n", + "print(\"Mean: {}\".format(t.mean))\n", + "print(\"Error on the mean: {}\".format(t.error_on_mean))\n", + "print(\"Standard deviation: {}\".format(t.std))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time = 5.0 +/- 0.1 [s]\n" + ] + } + ], + "source": [ + "# by default, the mean and error on the mean are used for this measurement\n", + "print(t)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time = 5.0 +/- 0.3 [s]\n" + ] + } + ], + "source": [ + "# however, you can change that if you want\n", + "t.use_std_for_uncertainty()\n", + "print(t)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "acceleration = 2.0 +/- 0.1 [m⋅s^-2]\n" + ] + } + ], + "source": [ + "# let's do some calculations with measurements\n", + "vi = q.Measurement(0, unit=\"m/s\", name=\"initial speed\")\n", + "vf = q.Measurement(10, 0.5, unit=\"m/s\", name=\"final speed\")\n", + "a = (vf - vi) / t # acceleration\n", + "a.name = \"acceleration\"\n", + "print(a)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "force = 30 +/- 2 [kg⋅m⋅s^-2]\n" + ] + } + ], + "source": [ + "f = m * a\n", + "f.name = \"force\"\n", + "# as you can see below, the errors as well as units are propagated properly.\n", + "print(f)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Methods of Error Propagation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two error methods supported by QExPy.\n", + "\n", + "#### Derivative Method (default)\n", + "\n", + "By default, QExPy propagates the uncertainties using the \"derivative\" method. That is, for a function, $f(x,y)$, that depends on measured quantities $x\\pm\\sigma_x$ and $y\\pm\\sigma_y$, with covariance $\\sigma_{xy}$ between the two measured quantities, the uncertainty in $f$ is given by:\n", + "\n", + "$$ \\sigma_f = \\sqrt{ \\left(\\frac{\\partial f}{\\partial x} \\sigma_x \\right)^2 + \\left(\\frac{\\partial f}{\\partial y} \\sigma_y \\right)^2 + 2 \\frac{\\partial f}{\\partial x} \\frac{\\partial f}{\\partial y}\\sigma_{xy} }$$\n", + "\n", + "Although the derivative method is commonly taught in undergraduate laboratories, it is only valid when the relative uncertainties in the quantities being propagated are small (e.g. less than ~10% relative uncertainty). This method is thus not strongly encouraged, although it has been made the default because it is so prevalent in undergraduate teaching.\n", + "\n", + "#### Monte Carlo Method (recommanded)\n", + "\n", + "The MC method is based on a statiscal understanding of the measurements. In the QExPy implementation, currently, the main assumptions is that the uncertainty in a quantity is given by a \"standard error\"; that is, if $x = 10\\pm 1$, then we *assume* that this error and uncertainty should be interpreted as: \"if we measure $x$ multiple times, we will obtain a set of measurements that are normally distributed with a mean of 10 and a standard deviation of 1\". In other words, we assume that $x$ has a 68% chance of being in the range between 9 and 11.\n", + "\n", + "The MC method then uses the assumption that measured quantities are normally distributed and use this to propagate the errors by using Monte Carlo simulation. Suppose that we have measured $x$ and $y$ and wish to determine the central value and uncertainty in $x=x+y$. The Monte Carlo method will generate normally distributed random values for $x$ and $y$ (the random numbers will be correctly correlated if the user has indicated that $x$ and $y$ are correlated), then it will add those random values together, to obtain a set of values for $z$. The mean and standard deviation of the random values for $z$ are taken as the central value and uncertainty in $z$. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "force = 30.08 +/- 2.38 [kg⋅m⋅s^-2]\n" + ] + }, + { + "data": { + "image/png": "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YWbEDIJJo/B/W/oD1yEYS7slMr2UfYo45yY557Kdc9lNmpT6QWWMMaaSKgz6zrnbnHMZzrlMYDDBxEaXAPMJ7n6D4I7C5/3jWfxwh+Egv73z6YP96J7WBDdSLK62MzHGGFOhaPv0S3MrwSRQdxEMvXvcpz8OTJOUT3Bn32AA59wKP+JnJcEdmNfayB1jjImvhL4jNzs721mfvjGwZ88eCgoK2LVrV9hFMQkkLS2NjIwM6tY9cOJVSUudc9ml7VOVlr4xJk4KCgpo0KABmZmZHDgRpklVzjm2bNlCQUEBrVu3jno/m4bBmCSwa9cujj76aAv4pogkjj766Ji//VnQNyZJWMA3xVXmPWFB3xhjUogFfZOcbrghWIwpZt26dXTv3p2srCw6derEo48+WrRu6dKldOnShbZt23LddddR2kAW5xzXXXcdbdu2pWvXrrz//g9T699yyy106tSJY489tsz9E51dyDWxe/vt4O/JJ1d71pmjXyw1fe295xyYkJdX7ceOSQ3Wgama9PR03nnnHQ499FC++eYbOnfuzHnnnUfz5s25+uqreeyxxzjxxBMZMGAAL7/8Mv379z9g/5deeonVq1ezevVqFi1axNVXX82iRYt4++23WbhwIcuXLwfglFNO4Y033qB3794hnGXlWUvfxO7kky3YpVgdrF27lg4dOjB8+HDat2/PJZdcwmuvvUbPnj1p164dixcH91l+++23XHbZZfTo0YNu3brx/PPPF+3fq1cvunfvTvfu3Xnbf2i+/vrr9O7dm0GDBtGhQwcuueSSKreeDznkEA499FAAdu/ezf79wa9Lbty4ka+++oqcnBwkMXToUJ577rkS+z///PMMHToUSeTk5LB9+3Y2btyIJHbt2sX333/P7t272bNnD82aNSuxf6Kzlr6JnbVyw62DG26o/m86WVnwwAPlbpKfn8/TTz/N5MmTOeGEE3jqqadYsGABs2bN4u677+a5555j/Pjx9O3bl8mTJ7N9+3Z69OjBGWecQdOmTXn11VdJS0tj9erVDBkypGherQ8++IAVK1bQvHlzevbsycKFCznllFMOOPbvf/97/va3v5Uo06mnnsqf/vSnEunr16/nnHPOIT8/n9///vc0b96cJUuWkJGRUbRNRkYGGzaUmAmGDRs20LJlyxLbnXTSSfTp04f09HScc4wcOZJjjz22/HpNQBb0Texuvz34+/rroRw+c/SL5K7ZAkCsP9BbbUKugzC0bt2aLl26ANCpUydOP/10JNGlSxfWrl0LwL/+9S9mzZrF/fffDwRDTf/73//SvHlzRo4cSV5eHrVr1+aTTz4pyrdHjx5FwTgrK4u1a9eWCPqjRo1i1KhRUZe1ZcuWLF++nM8++4zzzz+fQYMGVbxTBfLz81m1ahUFBQUAnHnmmbz11lv06tWrynnHkwV9Y5JNBS3ymlLYZQJQq1atoue1atVi795glnXnHM8++yzHHHPMAfuOHTuWZs2asWzZMvbv309aWlqp+dauXbsor0ixtvQLNW/enM6dO/PWW2/Rs2fPooANwQ1vLVqUnN29RYsWrF+/vsR206dPJycnh/r16wPQv39/3nnnnaQL+tanb4ypNmeffTYPPfRQUb/8Bx98AMCOHTtIT0+nVq1aTJs2jX37Ypt2a9SoUeTl5ZVYSgv4BQUFfPfddwBs27aNBQsWcMwxx5Cens4RRxzBu+++i3OOJ598koEDB5bY/7zzzuPJJ5/EOce7777LkUceSXp6Oq1ateKNN95g79697NmzhzfeeMO6d4ypjLJG7Jjk83//93/ccMMNdO3alf3799O6dWtmz57NNddcw//8z//w5JNP0q9fPw4//PAaK8OqVau46aabkIRzjptvvrmoW+qRRx5h+PDhfPfdd/Tv379o5E7hsM6rrrqKAQMGMGfOHNq2bUu9evV44oknABg0aBDz5s2jS5cuSKJfv3789Kc/rbHzqCk24ZqJXeEQtWrqz44m6EcO2cwc/SK5T40GIOe/H1ZLGWJWzXVQkVWrViVlq9LUvNLeGzbhmqleIfQpJ9y3gZD61Y2pKgv6JnZZWWGXoEjkh0GJG7hqUgLVgTGxsAu5JnavvRYsqczqwCQpa+mb2N11V/D3jDPCLUcxxbuAarTln6B1YExFrKVvjDEpxIK+McakkAqDvqQ0SYslLZO0QtI4nz5F0qeS8vyS5dMl6U+S8iUtl9Q9Iq9hklb7ZVjNnZYxproV3omaqPlVZMqUKYwcObJKeezcuZNzzjmHDh060KlTJ0aPHl207sYbbyQrK4usrCzat29Pw4YNS+z/9ddfF22TlZVF48aNucFPEf7mm2/SvXt36tSpwzPPPFOlcpYnmj793UBf59w3kuoCCyS95NeNcs4VL11/oJ1fTgQmAidKOgq4A8gGHLBU0izn3LbqOBFjjImHm2++mT59+vD9999z+umn89JLL9G/f38mTJhQtM1DDz1UdDdypAYNGpAXMVne8ccfz4UXXghAq1atmDJlStG8RTWlwpa+C3zjn9b1S3l3dA0EnvT7vQs0lJQOnA286pzb6gP9q0C/qhXfhOIvfwmWKsgc/WLRkpSqoQ6SlXOOUaNG0blzZ7p06cKMGTMAuPbaa5k1axYAF1xwAZdddhkAkydP5te//nWped14441Fk7dt2rQJgLy8PHJycujatSsXXHAB27YF7cLevXsXzcy5efNmMjMzgaAFf+GFF9KvXz/atWvHLbfcUpT/E088Qfv27enRowcLFy6s8rnXq1ePPn36AMEUzt27dz9gPp9Cf//73xkyZEi5eX3yySd8+eWXRXP3ZGZm0rVrV2rVqtle96hG70iqDSwF2gIPO+cWSboaGC9pDDAXGO2c2w20ANZH7F7g08pKL36sEcAICD75TAIqNplWtOId4Gt0DH8l66DalPbDHRddBNdcAzt3woABJdcPHx4smzdD8VknY7iz+B//+Ad5eXksW7aMzZs3c8IJJ3DqqafSq1cv3nrrLc477zw2bNjAxo0bAXjrrbcYPHhwiXy+/fZbsrOzmTBhAnfeeSfjxo3jz3/+M0OHDuWhhx7itNNOY8yYMYwbN44HKrgZLi8vjw8++IBDDz2UY445hl/96lfUqVOHO+64g6VLl3LkkUfSp08funXrVmLf+fPnc+ONN5ZIr1evXtG8/6XZvn07L7zwAtdff/0B6evWrePTTz+lb9++5ZY5NzeXiy++OO6/fRxV0HfO7QOyJDUE/impM3Ab8DlwCDAJuBW4s6oFcs5N8vmRnZ2duHNEpLIXXgj+JuG8I9UmhetgwYIFDBkyhNq1a9OsWTNOO+003nvvPXr16sUDDzzAypUr6dixI9u2bWPjxo288847pU6MVqtWLS6++GIALr30Ui688EJ27NjB9u3bOe200wAYNmwYP/vZzyos0+mnn86RRx4JQMeOHVm3bh2bN2+md+/eNGnSBICLL774gCmdC/Xp0+eALpdo7N27lyFDhnDdddfRpk2bA9bl5uYyaNAgateuXW4eubm5TJs2LabjVoeYxuk757ZLmg/0c84VdjztlvQEcLN/vgFoGbFbhk/bAPQulv56JcpswvaHPwR/UzDgFQm7DsprmderV/76xo1rZM6gFi1asH37dl5++WVOPfVUtm7dysyZM6lfvz4NGjSocP+KWrx16tQp+hWsXbt2HbAumumZy1KZlv6IESNo165d0UXYSLm5uTz88MPlHnPZsmXs3buX448/PupyVpdoRu808S18JB0GnAl87PvpUfBKnQ985HeZBQz1o3hygB3OuY3AK8BZkhpJagSc5dOMMUmkV69ezJgxg3379rFp0ybefPNNevToAUBOTg4PPPBAUXfP/fffX+Z88/v37y8apfLUU09xyimncOSRR9KoUSPeeustAKZNm1bU6s/MzGTp0qUAUY1uOfHEE3njjTfYsmULe/bs4emnny51u8KWfvGlrID/m9/8hh07dpTa5fTxxx+zbds2TjrppHLLFk2ff02J5opBOjBf0nLgPYKLsbOBv0n6EPgQaAz4WxSZA6wB8oHHgGsAnHNbgd/6PN4D7vRpxpgkcsEFF9C1a1eOO+44+vbty3333cePfvQjIPhA2Lt3L23btqV79+5s3bq1zKB/+OGHs3jxYjp37sy8efMYM2YMAFOnTmXUqFF07dqVvLy8ovSbb76ZiRMn0q1bNzZv3lxhOdPT0xk7diwnnXQSPXv2rJZZSgsKChg/fjwrV66ke/fuZGVl8de//rVofW5uLoMHDy7xrSWr2FxNM2fOLBH033vvPTIyMnj66ae58sor6dSpU5XLWxqbWtnErpLTClfnhdzCqZUH//zeqLav9gu5NrWySRCxTq1sd+QaY0wKsQnXTOxCGHGQcKwOTJKyoG9i17Jlxdsc7KwOTJKy7h0TuxkzgiWVWR2YJGUtfRO7iRODv/7GmpRkdWCSlAV9kxJC+1lFYxKMde8YYyq0du1aOnfuXOq6MWPG8FoFPx05duzYqGaPTMbplgH69evHcccdR6dOnbjqqqvYt28fAKNGjaJDhw5Fk8dt3769zDz27dtHt27dOPfcc4vSnHP8+te/pn379hx77LGlTmcRKwv6xpgqufPOOzkjxX82cubMmSxbtoyPPvqITZs2Fd39e+aZZ/LRRx+xfPly2rdvzz333FNmHg8++GCJ8fZTpkxh/fr1fPzxx6xatarUietiZUHfGBOVffv2ccUVV9CpUyfOOussvvvuOwCGDx9eNC3CnDlz6NChA8cffzzXXXfdAa3WlStX0rt3b9q0aVNu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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# first change the number of significant figures so that we can see the difference\n", + "q.set_sig_figs_for_error(3)\n", + "\n", + "# you can change the error method\n", + "q.set_error_method(q.ErrorMethod.MONTE_CARLO)\n", + "print(f)\n", + "\n", + "# you can see the histogram of samples from the Monte Carlo simulation\n", + "f.mc.show_histogram()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the above example, the result of a Monte Carlo simulation has a perfect Gaussian distribution. However, this might not always be the case." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.848 +/- 0.436 [kg^2⋅m^-2]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAEICAYAAACwDehOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deZgU1dn38e8Ng4yIskuYGeJARBEE2UQUwVFUNncRNS4QNajgApeCaB7Z3DAhjyi+kmBEBjdAMEoQF1YVXFgENYARHhxlCCrDqgIJy3n/qJqyGWbvma7umd/nuvqa7qpT5z69TN91TlWfMuccIiIiAFXCboCIiMQPJQUREQkoKYiISEBJQUREAkoKIiISUFIQEZGAkkKcMLP+ZrakkPVvmVm/Yta12MxuKWBdupk5M0sqy3oLKH+dmb1b3PJyuKI+E2Exsylm9nAZ1lfsz5WZZZhZdhnEjOqzWZL/m0SjpFBCZpZlZv81s/p5lq/yv2zTI5Z1NLO5ZrbTzLab2TIz+11p4jrnejrnMqNrffnVmzfZ+HW/5Jy7MNq6pWRKmrwro5J8Ns1slJm9mGf7cvl/jAdKCqXzNXBt7gMzawXUiCxgZmcCC4H3gBOBesDtQM/YNVMkfphH3zlxTm9Q6bwA3BjxuB8wNU+ZPwGZzrnHnXM5zrPSOde3sIrNbJyZ7TCzr82sZ8TyYO/PzKqa2Z/NLMcvd0fevXTgBDNbamY/mtm7eXs25VDv+/7fnWb2k5mdmXf4w6/rNjNb7/ee/p+ZWQliR7Y7y8yGmtnnZvazmT1nZg39bv2PZjbfzOpElO9kZh/6cT8zs4yIdb8zs3X+dhvN7NaIdRlmlm1m95jZD2a2pbDeXjR1mVk9M5ttZrvNbBnwm0LiJJvZi2a2zX9Oy/3n/wjQBXjafx+e9ss/aWab/LpXmlmXiLpGmdkMM5vqt3uNmXWIWN/WzD71100HkiPW1TGzOWa21f/czjGztIj1i83sETNbCuwBmprZBWb2pZnt8ttnhTzPo80brtphZmuB0/OsTzGzWX78r83srojle82sbp7nkWNm1fL5bOb7+phZD+AB4Gr/9fws4nnl/t9UMbP/MbNv/Pd1qpnV8tfl9qD7mdm3fvw/FPR844JzTrcS3IAs4HzgX8ApQFUgGzgBcEA6Xq/hIHBuCertD+wHfu/XeTvwb8D89YuBW/z7twFrgTSgDjDfj50UUfb/gJOAo/3HY/116fmULfN6I57TkojHDpgD1AZ+DWwFehQndgHvw8dAQyAV+AH4FGiL96W1EBjpl00FtgG98HaELvAfN/DX98b7AjbgHLwvr3b+ugzgADAGqObXsQeoU0C7Sl0XMA2YARwDnApsjnz98sS5FfgH3metKtAeOC7vexpR/nq83moScA/wHZDsrxsF7PPbUxV4DPjYX3cU8A0wxG9zH7zP6cP++nrAlX47jgVeBV6PiLsY+BZo6cduAPzo11PNr/dA3vZGbD8W+ACoCzQG/glk++uqACuBEX47mwIbge7++oXA7yPq+hPwlwI+m0W9Pi/maVfwGgM3ARv8+DWB14AX8vxfPIv3P3Ma8B/glLC/ywr8Lgq7AYl245ek8D/+P08PYJ7/YcpNCqn+/eYlqLc/sCHicQ2/jl/5jyM/hAuBWyPKns+RX97/E7F+IPC2fz89n7JlXm/Ec8qbFM6OeDwDGF6c2AW8D9dFPJ4FTIx4fCf+lxNwX+4/acT6d4B+BdT9OnC3fz8D2Jvnef0AdCrm+1qsuvC+jPdHfmaARyk4KdwEfAi0zmdd8J4W0q4dwGn+/VHA/Ih1LYC9/v2uROyc+Ms+xE8K+dTbBtiRpy1jIh7fiJ9w/MeGt1NVUFLYiL/j4D8ewC9J4Qzg2zzl7wee9+/fAiyMiLMJ6JrfZ7MYr09hSWEBMDBi3cn+e5nEL/8XaRHrlwHXFOfzE8ZNw0el9wLwW7wPV96hox3AIaBRCev8LveOc26Pf7dmPuVS8D7guTblU+a7iPt7CqgnVvUWZ/vixM7r+4j7e/N5nFv3CcBV/jDLTjPbCZyN//6YWU8z+9i8kwF24u0xRw63bXPOHSig3YeJoq4GeF8ikc/7m0Ke+wt4iW2amf3bzP5oZtUKKmxm9/rDWrv8dtXK066870uyeUN3KcBm53+b5W2XmdUws7/6Qye78YYRa5tZ1Yjykc/psPfZr7ew9zrv5yLyNTkBSMnzvj6A13sEb0fhTDNrhJfcDuH1Oo5QjNenMCl52vUN3nvZMGJZtP83MaOkUErOuW/wDjj3wusuRq7bA3yE160uD1vwhllyNY6DeqOdbre8nhN4XyovOOdqR9yOcc6NNbPqeF8e44CGzrnawFwKGecuSJR1bcUbRol83r8uqLBzbr9zbrRzrgVwFnARvxznOuy98MfHhwF98YaqagO7itmuLUCqmUWWjWzXPXh7xmc4547D+/IlT92R7dlCxHP06y3svd5Cwa/JJuDrPO/rsc65XgDOuR3Au8DVeDtw0/Ikt9w2FPX6FPXZ/jdegops4wEO30lJGEoK0bkZOM8593M+64YB/c07GFoPwMxOM7NpZRB3BnC3maWaWW284ZGyEE29W/H2xJqGELsoLwIXm1l38w5oJ5t30DcNbyy6Ov6XsnkH90t7Gm2p63LOHcTbuRjl7323wDuBIV9mdq6ZtfL3yHfjDVcc8ld/z+Hvw7F4X1JbgSQzGwEcV8zn9JG/7V3+AdorgI556t6Ld4JBXWBkEfW9CbQ0syv8nshdwK8KKT8DuN8/oJ2GNyyYaxnwo5nd5x+Qrmpmp5pZ5MHol/GSZR//fn6Ken2+B9Kt4DOnXgGGmFkTM6uJN+w3PU+PMGEoKUTBOfd/zrkVBaz7EDjPv200s+3AJLw9x2g9i7cH9Dmwyq/zAN7B7VDq9XtHjwBL/a58p1jFLkbbNgGX4g0tbMXbwxwKVHHO/Yj3xTQDb9jvt8DsUsaJtq478IYVvgOmAM8XUvZXwEy8hLAO79TnF/x1TwJ9/DN2nsIbZnob+ApvaGMfxRuewzn3X+AKvGHS7Xh73ZE94/F4B1Bz8A78v11EfTnAVXgHkLcBzYClhWwy2m/z13ifj9znmJtIL8I7jvG134a/4Q395Jrtx/jOOfdZATGKen1e9f9uM7NP89l+st+u9/127OPw5JVQLJ/elCQYf4/0L865E4osHAf1xntskcpMPYUE5HeVe5lZkpml4nXZ/x6v9cZ7bBH5hXoKCcjMauANFzTHG899E++0x93xWG+8xxaRXygpiIhIQMNHIiISyHdemURQv359l56eHnYzREQSxsqVK3Occw0KK5OwSSE9PZ0VK/I9G1RERPJhZoX9Sh7Q8JGIiERQUhARkYCSgoiIBBL2mIKIFGz//v1kZ2ezb9++sJsiIUhOTiYtLY1q1QqcOLdASgoiFVB2djbHHnss6enpHD7BqVR0zjm2bdtGdnY2TZo0KfH2Gj4SqYD27dtHvXr1lBAqITOjXr16pe4lFpkUzGyyf93Rf0Ysq2tm88y71u4886+Fa56nzGyDedfObRexTT+//Hoz6xexvL2ZfeFv85TpUyxSJvSvVHlF894Xp6cwBe+Sk5GGAwucc83wLkU33F/eE2+a2mZ4l82b6Dcwd571M/DmYh9pv1xUfSLedYlzt8sbS0REYqTIpOCcex9vHvVIlwKZ/v1M4LKI5VOd52O8y/I1AroD85xz2/2rIc0DevjrjnPOfexfEWlqRF1SVgYP9m4ikq/MzEyaNWtGs2bNyMzMzLfM6tWr6dSpE23atKFDhw4sW7YMgMWLF1OrVi3atGlDmzZtGDNmTCybXuZKe6C5oXNui3//O365Fmkqh1+cIttfVtjy7HyW58vMBuD1QPj1rwu8UmHF8+GH3t+zzsp3dfrwN4P7WWN7H1lg9epyjS+SyLZv387o0aNZsWIFZkb79u255JJLqFOnzmHlhg0bxsiRI+nZsydz585l2LBhLF68GIAuXbowZ86cEFpf9qI+0Ozv4cdkqlXn3CTnXAfnXIcGDQqdvqNiOeuscL+Qw44vCScrK4vmzZvTv39/TjrpJK677jrmz59P586dadasWbCX/fPPP3PTTTfRsWNH2rZtyxtvvBFs36VLF9q1a0e7du340N8xWbx4MRkZGfTp04fmzZtz3XXXEe1Mz++88w4XXHABdevWpU6dOlxwwQW8/faRF5AzM3bv9mZy37VrFykpKVHFjVel7Sl8b2aNnHNb/CGgH/zlmzn8Ittp/rLNQEae5Yv95Wn5lJdIYe+phx1fojN4cPS9xbzatIHx4wstsmHDBl599VUmT57M6aefzssvv8ySJUuYPXs2jz76KK+//jqPPPII5513HpMnT2bnzp107NiR888/n+OPP5558+aRnJzM+vXrufbaa4O5zlatWsWaNWtISUmhc+fOLF26lLPPPvuw2H/605946aWXjmhT165deeqppw5btnnzZho3/uVrKy0tjc2bj/waGj9+PN27d+fee+/l0KFDQaIC+OijjzjttNNISUlh3LhxtGzZsujXME6VNinMxruo+Fj/7xsRy+/wL05/BrDLTxzvAI9GHFy+ELjfObfdzHb71/P9BO8C2xNK2aaK64EHvL9+V7XSxZeE1KRJE1q1agVAy5Yt6datG2ZGq1atyMrKAuDdd99l9uzZjBs3DvBOpf32229JSUnhjjvuYPXq1VStWpWvvvoqqLdjx46kpXn7km3atCErK+uIpDB06FCGDh1aps9n4sSJPPHEE1x55ZXMmDGDm2++mfnz59OuXTu++eYbatasydy5c7nssstYv359mcaOpSKTgpm9greXX9/MsvHOIhoLzDCzm/EudN3XLz4X6AVsAPYAvwPwv/wfApb75cY453IPXg/EO8PpaOAt/yalVOTxBal8itijLy/Vq1cP7lepUiV4XKVKFQ4cOAB4P7SaNWsWJ5988mHbjho1ioYNG/LZZ59x6NAhkpOT8623atWqQV2RStJTSE1NDY4NgPfDv4yMjCO2zczM5MknnwTgqquu4pZbbgHguOOOC8r06tWLgQMHkpOTQ/369Y+oIxEUmRScc9cWsKpbPmUdMKiAeiYDk/NZvgI4tah2iEjF0717dyZMmMCECRMwM1atWkXbtm3ZtWsXaWlpVKlShczMTA4ePFiiekvSU+jevTsPPPAAO3bsALzey2OPPXZEuZSUFN577z0yMjJYuHAhzZo1A+C7776jYcOGmBnLli3j0KFD1KtXr0TtjSea5kJEQvPggw8yePBgWrduzaFDh2jSpAlz5sxh4MCBXHnllUydOpUePXpwzDHHlFsb6taty4MPPsjpp58OwIgRI6hbty4At9xyC7fddhsdOnTg2Wef5e677+bAgQMkJyczadIkAGbOnMnEiRNJSkri6KOPZtq0aQn9w8GEvUZzhw4dXKW5yE5uV7aAMf3IIaNIwfBREdtHG1/iz7p16zjllFPCboaEKL/PgJmtdM51KGw79RQSQUhjwnETX0RiRkkhEbRpU7nji0jMaJbURDB/vnerrPFFJGbUU0gEDz/s/T3//MoZX0RiRkkhARV0YFlEJFoaPhIRkYCSgoiUi5o1a8Z1fUWZMmUKd9xxR9T1rFy5klatWnHiiSdy1113FTqB3/Lly0lKSmLmzJnBsh49elC7dm0uuuiiqNtSHEoKIiLl6Pbbb+fZZ59l/fr1rF+/Pt8ZWAEOHjzIfffdx4UXXnjY8qFDh/LCCy/EoqmAkkJi+OtfvVtljS8JzTnH0KFDOfXUU2nVqhXTp08HYNCgQcyePRuAyy+/nJtuugmAyZMn84c//CHfuoYMGRJMrrd161bgl4vftG7dmssvvzyYriIjIyOYWTUnJ4f09HTA6wFcccUV9OjRg2bNmjFs2LCg/ueff56TTjqJjh07snTp0qif+5YtW9i9ezedOnXCzLjxxht5/fXX8y07YcIErrzySo4//vjDlnfr1o1jjz026rYUlw40J4I8k4VVuvgSvXwmeKNvXxg4EPbsgV69jlzfv793y8mBPn0OX1eCX7e/9tprrF69ms8++4ycnBxOP/10unbtSpcuXfjggw+45JJL2Lx5M1u2eNft+uCDD7jmmmuOqOfnn3+mQ4cOPPHEE4wZM4bRo0fz9NNPc+ONNzJhwgTOOeccRowYwejRoxlfxA8uV69ezapVq6hevTonn3wyd955J0lJSYwcOZKVK1dSq1Ytzj33XNq2bXvEtosWLWLIkCFHLK9Ro8Zh02mDNy137oyuUPC03Js3b+bvf/87ixYtYvny5UesjyUlhUTwj394fy++uHLGl4S2ZMkSrr32WqpWrUrDhg0555xzWL58OV26dGH8+PGsXbuWFi1asGPHDrZs2cJHH310xEym4M2uevXVVwNw/fXXc8UVV7Br1y527tzJOeecA0C/fv246qqrimxTt27dqFWrFgAtWrTgm2++IScnh4yMDHIv4HX11VcfNmV3rnPPPZfVZXx9isGDB/P4449TpUr4gzdKCongz3/2/ob1pRx2fIleYXv2NWoUvr5+/XKZ9yo1NZWdO3fy9ttv07VrV7Zv386MGTOoWbNmsYZLipp0LikpiUOHDgHedRoiFWf67YKUpKeQmppKdvYvVxzOzs4mNfXIKw6vWLEi6B3l5OQwd+5ckpKSuOyy2F+yPvy0JOUmffibpA9/k483bgu7KVKJdenShenTp3Pw4EG2bt3K+++/T8eOHQHo1KkT48ePD4aTxo0bR5cuXfKt59ChQ8FZOS+//DJnn302tWrVok6dOnzwwQcAvPDCC0GvIT09nZUrVwIcdjZPQc444wzee+89tm3bxv79+3n11VfzLZfbU8h7y5sQABo1asRxxx3Hxx9/jHOOqVOncumllx5R7uuvvyYrK4usrCz69OnDM888E0pCAPUURKScXX755cHlKs2MP/7xj/zqV78CvITx7rvvcuKJJ3LCCSewffv2ApPCMcccw7Jly3j44Yc5/vjjgwPWmZmZ3HbbbezZs4emTZvy/PPPA3DvvffSt29fJk2aRO/eRV9wqlGjRowaNYozzzyT2rVr06aM5vx65pln6N+/P3v37qVnz5707NkTgL/85S8A3HbbbYVu36VLF7788kt++ukn0tLSeO655+jevXuZtC0/mjo7EeSZurqkv2ie9vJwOjWtp6mzKxFNnS2lnTpbw0ciIhLQ8FEiiOEPV+IyvojEjJJCImjcuHLHF5GY0fBRIpg+3btV1vgiEjPqKSSCiRO9v/4PdypdfBGJGfUUREQkoKQgImUuKyuLU089Nd91I0aMYH4Rl3cdNWoU48aNKzJOok6n/Yc//IHGjRsX2v558+bRvn17WrVqRfv27Vm4cGGw7pVXXqFVq1a0bt2aHj16kJOTE3WbcikpiEhMjRkzhvMr+aVdL774YpYtW1Zomfr16/OPf/yDL774gszMTG644QYADhw4wN13382iRYv4/PPPad26NU8//XSZtU1JQUTKxcGDB/n9739Py5YtufDCC9m7dy8A/fv3D6admDt3Ls2bN6d9+/bcddddh11IZu3atWRkZNC0adN8J8jLlWjTaYM3vUejRo0KLdO2bVtSUlIAaNmyJXv37uU///kPzjmcc/z8888459i9e3dQriwoKSSCmTO9W2WNL9HLyDjy9swz3ro9e/JfP2WKtz4n58h1xbB+/XoGDRrEmjVrqF27NrNmzTps/b59+7j11lt56623WLlyZfCFnuvLL7/knXfeYdmyZYwePZr9+/cfESN3Ou01a9ZwzjnnMHr0aABuvPFGHn/8cT7//HNatWoVLC/M6tWrmT59Ol988QXTp09n06ZNbNmyhZEjR7J06VKWLFnC2rVr89120aJFtGnT5ojbWWedVazXqiizZs2iXbt2VK9enWrVqjFx4kRatWpFSkoKa9eu5eabby6TOKCkkBjq1/dulTW+JKQmTZoE8we1b9+erKysw9Z/+eWXNG3alCZNmgBw7bXXHra+d+/eVK9enfr163P88cfz/fffHxEj73TaS5YsyXc67ffff7/I9uZOp52cnBxMp/3JJ58E02kfddRRQay8SjJJXkmtWbOG++67j7/6F7rav38/EydOZNWqVfz73/+mdevWPPbYY1HHyaVTUhNB7h5b//6VM75EL4Sps/NOT507fFTa7YszvXUiTKddEtnZ2Vx++eVMnTqV3/zmNwDBtRxyH/ft25exY8eWOkZe6ikkgilTfvlirozxpUI6+eST2bhxY9CDmF6KH0gm4nTaxbVz50569+7N2LFj6dy5c7A8NTWVtWvXBsNt8+bNK9PJD5UURCQURx99NM888ww9evSgffv2HHvsscHV0IordzrtU089lYULFzJixAjAm0576NChtG7dmtWrVwfL7733XiZOnEjbtm2LdRpn5HTanTt3LrMv32HDhpGWlsaePXtIS0tj1KhRAMyePTto69NPP82GDRsYM2ZMcIzihx9+ICUlhZEjR9K1a9fg+T3wwANl0i7Q1NmJoQymzga45rdeFzNrbNFzyxcWX+Jfokyd/dNPP1GzZk2ccwwaNIhmzZrlOwwjJRfK1NlmNsTM1pjZP83sFTNLNrMmZvaJmW0ws+lmdpRftrr/eIO/Pj2invv95f8ys/K7eoSIxJVnn32WNm3a0LJlS3bt2sWtt94adpMqvVInBTNLBe4COjjnTgWqAtcAjwNPOOdOBHYAuedK3Qzs8Jc/4ZfDzFr427UEegDPmFnV0rZLRBLHkCFDWL16NWvXruWll16iRo0aYTep0ov27KMk4Ggz2w/UALYA5wG/9ddnAqOAicCl/n2AmcDT5p0qcCkwzTn3H+BrM9sAdAQ+irJtFcfcuZU7vpSKc67Is3GkYormsECpewrOuc3AOOBbvGSwC1gJ7HTO5Z7LlQ2k+vdTgU3+tgf88vUil+ezzWHMbICZrTCzFXl/6FKh1ajh3SprfCmx5ORktm3bFtWXgyQm5xzbtm0jOTm5VNuXuqdgZnXw9vKbADuBV/GGf8qNc24SMAm8A83lGSuu5P7ydODAyhlfSiwtLY3s7OwjfiUslUNycjJpaWml2jaa4aPzga+dc1sBzOw1oDNQ28yS/N5AGrDZL78ZaAxkm1kSUAvYFrE8V+Q2AjBjhvc3rC/lsONLiVWrVi34pbBISURz9tG3QCczq+EfG+gGrAUWAX38Mv2AN/z7s/3H+OsXOq9vOxu4xj87qQnQDCh8+kARESkXpe4pOOc+MbOZwKfAAWAV3tDOm8A0M3vYX/acv8lzwAv+geTteGcc4ZxbY2Yz8BLKAWCQc+5gadslIiKlF9XZR865kcDIPIs34p09lLfsPuCqAup5BHgkmraIiEj0NCFegvh44zauKeEvmUVESkpJIREsXhxuQtD0FiKVhibEExGRgJJCIhg3jt9/8lqo8SnGRdRFJPEpKSSCOXPo9n8hnqU7Z453E5EKT0lBREQCSgoiIhJQUhARkYBOSU0ERx/NvqTqRZcrx/giUjkoKSSCt96if5i/U3jrrfBii0hMafhIREQCSgqJ4KGHuHPpK6HG56GHwosvIjGjpJAIFiyg8zefhRqfBQvCiy8iMaOkICIiASUFEREJKCmIiEhAp6Qmgnr12LFlf6jxRaRyUFJIBLNmcXuYv1OYNSu82CISUxo+EhGRgJJCIrj/foa9NyXU+Nx/f3jxRSRmNHyUCD76iHabt5VZdekRQ1FZY3sXK76IVA7qKYiISEBJQUREAkoKIiIS0DGFRJCWxpatFmp8EakclBQSwYsvMiTM3ym8+GJ4sUUkpjR8JCIiASWFRDB4MCPmTwo1PoMHhxdfRGJGw0eJYPVqWvxQdr9TKE18Eakc1FMQEZGAkoKIiASUFEREJBBVUjCz2mY208y+NLN1ZnammdU1s3lmtt7/W8cva2b2lJltMLPPzaxdRD39/PLrzaxftE+qwjnpJDbWTQ01PiedFF58EYmZaHsKTwJvO+eaA6cB64DhwALnXDNggf8YoCfQzL8NACYCmFldYCRwBtARGJmbSMQ3aRIP9Lgz1PhMCvHsJxGJmVInBTOrBXQFngNwzv3XObcTuBTI9ItlApf59y8FpjrPx0BtM2sEdAfmOee2O+d2APOAHqVtl4iIlF40PYUmwFbgeTNbZWZ/M7NjgIbOuS1+me+Ahv79VGBTxPbZ/rKClkuuAQN49O0JocZnwIDw4otIzESTFJKAdsBE51xb4Gd+GSoCwDnnABdFjMOY2QAzW2FmK7Zu3VpW1ca/r76i6fbNocbnq6/Ciy8iMRNNUsgGsp1zn/iPZ+Ilie/9YSH8vz/46zcDjSO2T/OXFbT8CM65Sc65Ds65Dg0aNIii6SIikp9SJwXn3HfAJjM72V/UDVgLzAZyzyDqB7zh358N3OifhdQJ2OUPM70DXGhmdfwDzBf6y0REJMainebiTuAlMzsK2Aj8Di/RzDCzm4FvgL5+2blAL2ADsMcvi3Nuu5k9BCz3y41xzm2Psl0iIlIKUSUF59xqoEM+q7rlU9YBgwqoZzIwOZq2VGht2rD2p69DjS8ilYMmxEsE48czJszrKYwfH15sEYkpTXMhIiIB9RTiWLrfO3jiH+N4Ahhy8b3lFgMga2zv/Atdf733V1dgE6nwlBQSQKMfc8JtQHZ2uPFFJGY0fCQiIgElBRERCSgpiIhIQMcUEsCnqc3DbcCZZ4YbX0RiRkkhAfzxnP7hNuCxx8KNLyIxo+EjEREJKCkkgIl/f5SJf380vAZceaV3E5EKT8NHCaDO3t3hNmDbtnDji0jMqKcgIiIBJQUREQkoKYiISEDHFBLA0hNOC7cB3Y64PIaIVFBKCglgQudrw23Agw+GG19EYkbDRyIiElBSSABTZoxkyoyR4TWgZ0/vJiIVnoaPEkDygf+E24C9e8ONLyIxo56CiIgElBRERCSgpCAiIgEdU0gAC37TMSZx0oe/GdzPGtv7lxUXXRST+CISPiWFBPDsGVeE24B77w03vojEjIaPREQkoKSQAKa9PJxpLw8PrwEZGd5NRCo8JQUREQkoKYiISEBJQUREAkoKIiIS0CmpCWBO8y7hNqBv3+6slq8AAAqBSURBVHDji0jMKCkkgBfb9S66UHkaODDc+CISM1EPH5lZVTNbZWZz/MdNzOwTM9tgZtPN7Ch/eXX/8QZ/fXpEHff7y/9lZt2jbVNFk7x/H8n794XXgD17vJuIVHhlcUzhbmBdxOPHgSeccycCO4Cb/eU3Azv85U/45TCzFsA1QEugB/CMmVUtg3ZVGFNeHcWUV0eF14BevbybiFR4USUFM0sDegN/8x8bcB4w0y+SCVzm37/Uf4y/vptf/lJgmnPuP865r4ENQGwm+xERkcNE21MYDwwDDvmP6wE7nXMH/MfZQKp/PxXYBOCv3+WXD5bns81hzGyAma0wsxVbt26NsukiIpJXqZOCmV0E/OCcW1mG7SmUc26Sc66Dc65DgwYNYhVWRKTSiObso87AJWbWC0gGjgOeBGqbWZLfG0gDNvvlNwONgWwzSwJqAdsilueK3EZERGKo1EnBOXc/cD+AmWUA9zrnrjOzV4E+wDSgH/CGv8ls//FH/vqFzjlnZrOBl83sf4EUoBmwrLTtqohmtjo/3Ab07x9ufBGJmfL4ncJ9wDQzexhYBTznL38OeMHMNgDb8c44wjm3xsxmAGuBA8Ag59zBcmhXwlJSEJFYKZOk4JxbDCz2728kn7OHnHP7gKsK2P4R4JGyaEtFVGfPLgB21KgVTgNycry/9euHE19EYka/aE4AE19/DIBrfjs2nAb06eP9Xbw4nPgiEjOaEE9ERALqKUi+0oe/GdzPCq8ZIhJj6imIiEhASUFERAIaPkoAL7YNeTK6228PN76IxIySQgKYc0rXcBtw9dXhxheRmNHwUQJotHsrjXaHOAHgpk3eTUQqPPUUEsATc/4MhPg7hRtu8P7qdwoiFZ6SQpyJPBVURCTWNHwkIiIBJQUREQkoKYiISEDHFBLAsx0vD7cB99wTbnwRiRklhQSw4MQzwm3AxReHG19EYkbDRwmg6bZsmm7LDq8B//qXdxORCk89hQTw6DtPAyH+TuHWW72/+p2CSIWnnoKIiASUFEREJKCkICIiASUFKdLHG7fx8cZtmoJDpBLQgeYEMOGsayp1fBGJHSWFBLA0vU2lji8isaPhowTQ4vuNtPh+Y6WNLyKxo6SQAEYsmMSIBZMqbXwRiR0lBRERCSgpiIhIQElBREQCSgoiIhLQKakJ4I9d+1Xq+CISO0oKCeDTtFMqdXwRiR0NHyWAdtnraJe9rtLGF5HYKXVSMLPGZrbIzNaa2Rozu9tfXtfM5pnZev9vHX+5mdlTZrbBzD43s3YRdfXzy683M41V5DHs/UyGvZ8ZF/HTh78Z3ESk4ommp3AAuMc51wLoBAwysxbAcGCBc64ZsMB/DNATaObfBgATwUsiwEjgDKAjMDI3kYiISGyVOik457Y45z717/8IrANSgUuB3N3aTOAy//6lwFTn+RiobWaNgO7APOfcdufcDmAe0KO07RIRkdIrk2MKZpYOtAU+ARo657b4q74DGvr3U4FNEZtl+8sKWp5fnAFmtsLMVmzdurUsmi4iIhGiTgpmVhOYBQx2zu2OXOecc4CLNkZEfZOccx2ccx0aNGhQVtWKiIgvqlNSzawaXkJ4yTn3mr/4ezNr5Jzb4g8P/eAv3ww0jtg8zV+2GcjIs3xxNO2qaMZ0G1Cp44tI7ERz9pEBzwHrnHP/G7FqNpB7BlE/4I2I5Tf6ZyF1Anb5w0zvABeaWR3/APOF/jLxrW3YlLUNm1ba+CISO9H0FDoDNwBfmNlqf9kDwFhghpndDHwD9PXXzQV6ARuAPcDvAJxz283sIWC5X26Mc257FO2qcDpneS9vWBe7CTu+iMROqZOCc24JYAWs7pZPeQcMKqCuycDk0ralorvzw2lAeF/KBcWP/K1C1tjeMW2TiJQPTXMRB/RDMBGJF5rmQkREAkoKIiISUFIQEZGAjikkgAe631Gp44tI7CgpJICN9dIqdXwRiR0NHyWAbhs+oduGTyptfBGJHfUUEsDvl/0dgAUnnhG38fWbBZGKQT0FEREJKCmIiEhASUFERAJKCiIiEtCB5gQw5KJ7KnV8EYkdJYUEsOW4cK8yF3Z8EYkdJYUEcNG69wGYc0rXhIiv01NFEpeSQgK4ftVcILykEHZ8EYkdHWgWEZGAkoKIiASUFEREJKBjClKudNBZJLEoKSSA2y+7v1LHF5HYUVJIADtq1KrU8UUkdpQUQhI5rFKUPl/MB2Bmq/PLqzkxia+hJJH4pwPNCaDPF/ODL+bKGF9EYkc9BQmFeg0i8Uk9BRERCainIKFTr0EkfqinICIiAfUUEkD/q0ZV6vgiEjtKCglgX7XkShNfQ0ki4dLwUQK4/tM3uf7T4v+uoaLFF5HYUU8hhkryg7VIF335AQAvtgtnzzms+AW9XupBiJQfJQVJOBpiEik/cZMUzKwH8CRQFfibc25syE2SBFBY70sJQ6Tk4iIpmFlV4P8BFwDZwHIzm+2cWxtuy6JX2iEjiZ6Gn0RKLi6SAtAR2OCc2whgZtOAS4GETApKBPEtlu9PZAIqaNhLw2EST8w5F3YbMLM+QA/n3C3+4xuAM5xzd+QpNwAY4D88GfhXTBsavfpATtiNiGN6fQqn16dwen0KVx84xjnXoLBC8dJTKBbn3CRgUtjtKC0zW+Gc6xB2O+KVXp/C6fUpnF6fwvmvT3pR5eLldwqbgcYRj9P8ZSIiEkPxkhSWA83MrImZHQVcA8wOuU0iIpVOXAwfOecOmNkdwDt4p6ROds6tCblZ5SFhh75iRK9P4fT6FE6vT+GK9frExYFmERGJD/EyfCQiInFASUFERAJKCjFiZj3M7F9mtsHMhofdnnhiZo3NbJGZrTWzNWZ2d9htijdmVtXMVpnZnLDbEo/MrLaZzTSzL81snZmdGXab4oWZDfH/r/5pZq+YWaFz4SspxEDENB49gRbAtWbWItxWxZUDwD3OuRZAJ2CQXp8j3A2sC7sRcexJ4G3nXHPgNPRaAWBmqcBdQAfn3Kl4J/JcU9g2SgqxEUzj4Zz7L5A7jYcAzrktzrlP/fs/4v1Dp4bbqvhhZmlAb+BvYbclHplZLaAr8ByAc+6/zrmd4bYqriQBR5tZElAD+HdhhZUUYiMV2BTxOBt96eXLzNKBtsAn4bYkrowHhgGHwm5InGoCbAWe94fY/mZmx4TdqHjgnNsMjAO+BbYAu5xz7xa2jZKCxA0zqwnMAgY753aH3Z54YGYXAT8451aG3ZY4lgS0AyY659oCPwM6bgeYWR28UYkmQApwjJldX9g2SgqxoWk8imBm1fASwkvOudfCbk8c6QxcYmZZeMOO55nZi+E2Ke5kA9nOudze5Uy8JCFwPvC1c26rc24/8BpwVmEbKCnEhqbxKISZGd548Drn3P+G3Z544py73zmX5k9kdg2w0DlX6J5eZeOc+w7YZGYn+4u6kaDT7peDb4FOZlbD/z/rRhEH4eNimouKrhJN41FanYEbgC/MbLW/7AHn3NwQ2ySJ5U7gJX+nayPwu5DbExecc5+Y2UzgU7yz/FZRxHQXmuZCREQCGj4SEZGAkoKIiASUFEREJKCkICIiASUFEREJKCmIiEhASUFERAL/H39URnBUiZjHAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# let's try calculating the gravitational force between two stars\n", + "\n", + "G = 6.67384e-11 # the gravitational constant\n", + "m1 = q.Measurement(40e4, 2e4, name=\"m1\", unit=\"kg\")\n", + "m2 = q.Measurement(30e4, 10e4, name=\"m2\", unit=\"kg\")\n", + "r = q.Measurement(3.2, 0.5, name=\"distance\", unit=\"m\")\n", + "\n", + "f = G * m1 * m2 / (r ** 2)\n", + "print(f)\n", + "\n", + "f.mc.show_histogram()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, in this case, the mean and standard deviation of the distribution doesn't quite capture the center value and uncertainty we are looking for. We can try a different strategy where we find the mode (most probably value) of the distribution, and a confidence range." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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dRRmxttvv80333xDrVq10tgrZUvDXIhImRkxYgSXXnoprVu3pkqVKjz8cHCl+/Dhw+nbty8tWrSgQ4cO0dWGzZs354477qBbt2788MMPVKxYkfvuu2+3pLAvWrRoQe/evWnevDkVKlTgvvvui648OvPMMxk7diw/+clPGDp0KBdddBF33303VatWZezYsQA8+eSTTJw4kYoVK3LIIYcwderURJ9sttSz+knSpk0bP2AfstOlS/C6l33+bxwZ9Nv2ufDOXcqL7D7ax/iSPMuXL+fYY48t62pIGSrs/4CZLXL3NsVtp5ZCHGW6D7iM+phFJP6UFOIoJyfZ8UUksTRKahzNnh1MSY0vIomllkIc3XFH8HraacmMLyKJpaSQQKn3IgBMKaN6iMiBR91HIiISUVIQkYyoWrVqrOPtyYQJE7j66qvTjrNo0SJatWpFkyZNuPbaaynsNoBJkybRunVrWrVqRYcOHXj33XejZffccw8tW7akRYsWjN4PVw4qKYiIZNCVV17Jgw8+yMqVK1m5ciXPP//8buvk3yX93nvvccsttzBo0CAA3n//fR588EEWLFjAu+++y7PPPsuqVasyWl8lhTj6v/8LpqTGF0nh7txwww20bNmSVq1aMXXqVAAGDx7MjBkzgGAsoUsvvRSA8ePHc/PNNxcaa8iQIdHgehs3bgRg8eLFtG/fntatW9OzZ89oSIwuXbqQf4Prpk2byM7OBoIWQK9evejevTtNmzblxhtvjOI/9NBDHH300bRt25bXXnst7e++fv16vvjiC9q3b4+Z0a9fP55++und1uvQoUM06F/79u3JzQ0eM7N8+XLatWtHlSpVqFChAp07d44GCswUJYU4atYsmJIaX+KnS5fdp/vvD5Zt31748gkTguWbNu2+bB9Mnz6dxYsX8+677zJ79mxuuOEG1q9fT8eOHaNRRtetW8eyZcsAePXVV+nUqdNucb766ivatGnD0qVL6dy5M7fddhsA/fr1409/+hNLliyhVatWUXlxFi9ezNSpU3nvvfeYOnUqa9euZf369QwfPpzXXnuN+fPnR/UpaO7cueTk5Ow2dShkKPp169aRlfXfpwMUNSx3qnHjxnHGGWcA0LJlS1599VXy8vLYvn07M2fO3GXwvkzQ1Udx9I9/BK+/+EUy44ukmD9/Pn379qV8+fLUq1ePzp0789Zbb9Gx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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.632 +/- 0.339 [kg^2⋅m^-2]\n" + ] + } + ], + "source": [ + "# change the mc strategy\n", + "f.mc.use_mode_with_confidence()\n", + "\n", + "# show the histogram again\n", + "f.mc.show_histogram()\n", + "\n", + "# also print the value\n", + "print(f)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the confidence level is 68%, but we can change that." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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3HvvvXTv3p0ffviBypUr8/jjj++XFA5EZmYmffr0oVWrVlSqVInHH388OvPoggsu4Omnn+boo49m6NChXHHFFYwcOZLq1avz9NNPA/DKK6/w3HPPUblyZQ4//HAmTJhQrgebLXFUvzxp3769HxI32enaNXguZCzg/WODftu+lz+wT/k+3UfFiCPpY8WKFZx44ollXQ0pQ/n9GzCzxe7evrD91FKIu1T1B5dRv7KIlC9KCnHXrl284ohIWtMsqXH3zjvBIy5xRCStqaUQd/feGzyfd1484ohIWlNSKIcSr0UAeLGM6iEi6UfdRyIiElFSEJGDonr16rGOV5QxY8YwePDgpOMsXryYNm3a0KxZM2655RbyuwygsOm3b7vtNjIzMznxxBML3D+VlBRERA6in//85zz11FOsWrWKVatW8dZbb+23Tbdu3fjoo4/Iyspi9OjRXHvttQC89957zJs3j6VLl/LPf/6ThQsXMnv27INaXyWFuPvrX4NHXOKIHCB359e//jWtW7emTZs2TJgwAYBBgwYxZcoUIJhL6OqrrwZg9OjR3HnnnfnGGjJkSDS53saNGwHIysqiY8eOtG3blt69e0dTYnTt2pXcC1w3bdpERkYGELQALr30Unr06EHz5s257bbbovjPPvssLVq0oEOHDsybNy/pz75+/Xq2b99Ox44dMTP69+/P5MmT99uuoOm3zYydO3fy/fffs2vXLnbv3k2DBg322z+VNNAcdy1bxiuOlE+5V7Qn6tMHbroJduyACy7Yf/3AgcFj0ya47LJ91x3AlfGTJk0iKyuLjz76iE2bNnHaaafRuXNnOnXqxLvvvsvFF19MTk4O69cH9+1699136du3735xvv32W9q3b8/IkSO55557GDFiBH/+85/p378/jz32GF26dOHuu+9mxIgRPFLExZpZWVl8+OGHVKlShZYtW3LzzTdTqVIlhg0bxuLFi6lZsybnnHMOJ5988n77zpw5kyFDhuxXXq1aNd7LnaI+lJOTQ+PG/707QEHTckP+02+fccYZnHPOOTRs2BB3Z/DgwQf9SnW1FOLu738PHnGJI3KA5s6dS79+/ahYsSINGjSgS5cuLFy4MEoKy5cvp1WrVjRo0ID169czf/58zsznvh8VKlTgpz/9KQBXXnklc+fOZdu2bXz11Vd06dIFgAEDBjBnzpwi69StWzdq1qxJ1apVadWqFZ9//jkLFiyga9eu1K9fn8MOOyx6r7zOOeccsrKy9nvkTQgHqnfv3qxcuZLJkydz1113AcFkeytWrCA7O5ucnBxmzJgRTdd9sKilEHd/+EPwfNFF8Ygj5VNhv+yrVSt8fb16B2XOrEaNGvHVV1/x1ltv0blzZ7Zs2cJLL71E9erVOfLII4vcv6hJ5ypVqsQPP/wAwM6dO/dZlzitd8WKFQ9oKu8DaSk0atSI7Oz/3nG4oGm5EyVOv/3qq6/SsWPHaJD9/PPPZ/78+XTq1KnY9T1QaimksYyhU6OHSFnp1KkTEyZMYO/evWzcuJE5c+ZEU1N37NiRRx55JOpOevjhhws84P3www9MnDgRgBdeeIGzzz6bmjVrUrt27ejX8/PPPx+1GjIyMli8eDFAtF9hTj/9dGbPns3mzZvZvXs3L7/8cr7bHUhLoWHDhtSoUYP3338fd+e5556jV69e+223evXq6KyixOm3jz32WGbPns2ePXvYvXs3s2fPPujdR2opiMhB1bt3b+bPn89JJ52EmfHQQw/xP//zP0CQMP7xj3/QrFkzjjvuOLZs2VJgUjjiiCP44IMPuPfeeznqqKOiAeuxY8dy4403smPHDpo2bcqzzz4LwK9+9Sv69OnDqFGj6Nmz6BtONWzYkOHDh3PGGWdQq1Yt2qVovrAnnniCgQMH8t1333H++edz/vnnA/CXv/wFgBtvvLHA6bcvu+wyZsyYQZs2bTAzevTowUUHubWvqbPjLp8pr/e7ovmF4E5SeafOTrT2/f/bL46kL02dLSWdOlvdRyIiElH3Udw9/3y84ohIWlNSiLtjjolXHBFJa+o+irsJE4JHXOKISFpTSyHunnwyeC7gQppSjyMiaU0tBRERiSgpiEjKrV27ltatW+e77u677+adIm4NO3z4cB5++OEi3yedp9Petm0bF110ESeddBKZmZnR9RcAv/nNb2jdujWtW7eOrtdIFSUFESlV99xzD+cd4reFLc502o8//jitWrXio48+YtasWfzyl7/k+++/Z+rUqSxZsoSsrCwWLFjAww8/zPbt21NWNyUFETko9u7dy3XXXUdmZibdu3fnu+++A2DgwIHRtBNvvPEGJ5xwAqeeeiq33HILF154YbT/8uXL6dq1K02bNuXRRx8t8H3SdTptM+Prr7/G3fnmm2+oU6cOlSpVYvny5XTu3JlKlSpxxBFH0LZt23yTSkkpKcTdxInBIy5xpHzq2nX/xxNPBOt27Mh//ZgxwfpNm/ZfVwyrVq1i0KBBLFu2jFq1avHKK6/ss37nzp3ccMMNvPnmmyxevDg6oOdauXIl06ZN44MPPmDEiBHs3r17v/fInU572bJldOnShREjRgDQv39/HnzwQZYuXUqbNm2i8sJkZWUxYcIEPv74YyZMmMC6detYv349w4YNY968ecydO5fly5fnu+/MmTNp167dfo/8Znst7nTagwcPZsWKFRx99NG0adOGP/3pT1SoUIGTTjqJt956ix07drBp0yZmzpzJunXrivx8xaWzj+KuXr14xREppiZNmkTzB5166qmsXbt2n/UrV66kadOmNGnSBIB+/foxatSoaH3Pnj2pUqUKVapU4aijjuLLL7/c52AK+0+nfemll+Y7nfZPfvKTIuubO502EE2nvWnTpmg6bYCf/vSnfPrpp/vtmztJXipNmzaNdu3aMWPGDD777DN+9KMf0alTJ7p3787ChQs588wzqV+/PmeccQYVK1ZM2fsqKcRd7q+1gQPjEUfKpzKYOjvv9NS53Ucl3b8401un03Tazz77LEOHDsXMaNasGU2aNGHlypV06NCBO++8M7o73eWXX06LFi2KXdeiqPso7saM+e8BPQ5xRFKkZcuWrFmzJmpBlOQsmnSeTvvYY49l+vTpAHz55Zd88sknNG3alL1797J582YAli5dytKlS+nevXuRn6W41FIQkTJx+OGH88QTT9CjRw+OOOIITjvttAOOkc7Tad91110MHDiQNm3a4O48+OCD1KtXj507d0bTi9eoUYNx48ZRqVLqDuWaOjvuUjR1duI2ax8o+j+DlG/lZersb775hurVq+PuDBo0iObNm+fbDSMHrkymzjazIWa2zMz+aWZ/M7OqZtbEzBaY2Wozm2Bmh4XbVgmXV4frMxLi3B6Wf2JmP06mTiJSfjz11FO0a9eOzMxMtm3bxg033FDWVTrklTgpmFkj4Bagvbu3BioCfYEHgZHu3gzYClwT7nINsDUsHxluh5m1CvfLBHoAT5hZ6obSRSS2hgwZQlZWFsuXL2f8+PFUq1atrKt0yEu2I6oScLiZ7QaqAeuBc4HLw/VjgeHAk0Cv8DXARODPFpwq0At40d13Af8ys9VAB2B+knVLD2+8kZIw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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.632 +/- 0.254 [kg^2⋅m^-2]\n" + ] + } + ], + "source": [ + "# change the confidence\n", + "f.mc.confidence = 0.5\n", + "\n", + "# show the histogram again\n", + "f.mc.show_histogram()\n", + "\n", + "# also print the value\n", + "print(f)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, most of the samples are concentrated in the first half of the histogram. In order to increase resolution, the user can manually set the range of the histogram to focus on the region with the most samples." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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yBnVs1zLFinI8uXLmTx5Mp9++ilffvkl5513Hp999hmVK1feZ7uJEyeybt06Vq5cSaVKldi4MRiNfvvtt3P77bcD8NprrzF69GiOPvrocj+O0lJLX6KdeWbqgm4qy5ZiycnJ4eSTT2bw4MGcdNJJXHrppbz99tt07NiRpk2bsnDhQgC2bt1Kr169aN26NR06dGDZsmUAbNmyhW7dupGVlcVVV11F4lxgzz33HO3atSM7O5trrrmGvXv3lqqur776Kv3796datWo0atSIJk2a5Ncv0ZNPPsnw4cOpVCkIj8cee+wB27z44osMGDCgVPVJFbX0JVoxW9uZw97I/1zqB7qqpV8yt94KS5cmN8/sbHj44YNusnr1al566SUmTJjA6aefzgsvvMCCBQuYOXMmI0eOZMaMGYwYMYI2bdowY8YM3nnnHQYOHMjSpUu55557OOussxg+fDhvvPEGTz31FAArVqxgypQpvP/++1StWpXrr7+e559/noEDB+5T9tChQ5k7d+4Bderfvz/Dhu0zcQDr16+nQ4d/P4EzIyOD9esPfOb8559/zpQpU3jllVeoV68ejz76KE2bNs1fv3PnTt58800ef/zxwr+/Q5CCvkS7667g/d1341W2FFujRo1o1aoVAFlZWXTt2hUzo1WrVuTk5ACwYMECXn75ZQDOPfdctmzZwtdff838+fOZPj14XG/Pnj2pUye4d3POnDksWbKE008/HYDvvvsussU9evTopB/P7t27qV69OosXL2b69OlcccUVvPfee/nrX3vtNTp27Fghu3ZAQV8kfRTSIi8r1apVy/9cqVKl/OVKlSqxZ8+eEuXp7gwaNIj777//oNsVp6XfsGFD1q3795yPubm5NGx44JyPGRkZ9OnTB4DevXtz+eWX77N+8uTJFbZrB9SnLyLloFOnTjz//PMAvPvuu9StW5datWrRuXNnXnjhBQD+/Oc/s21b8HiNrl27Mm3atPyLqFu3bmXt2gNnCx49ejRLly494LV/wAe48MILmTx5Mrt37+aLL75g1apVtGvX7oDtevXqlX8imTdvHieddFL+uh07djBv3jwuuuiiUn4jqaOWvoiUubvvvpsrrriC1q1bU6NGDSZNmgTAiBEjGDBgAFlZWZx55pmccMIJALRo0YL77ruPbt268eOPP1K1alWeeOIJTjzxxBLXISsri379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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# try show histogram again\n", + "f.mc.show_histogram(range=(-1,4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you feel like this subset of the distribution is somewhat more representative of the quantity, you can set the range for Monte Carlo simulation to this interval" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.844 +/- 0.421 [kg^2⋅m^-2]\n" + ] + }, + { + "data": { + "image/png": "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7NmTFStWHLJ85cqV1KlTh5deeikZISecTuRK/N5/37s988wqP7XtPX8qvZ8/eVDsT8zLq/K2EqIadZXUsG3bNiZOnMiqVaswM3r06MFPfvITGjVqdMh6Y8eOZfz48QwcOJCFCxcyduxYli1bBsCBAwe4++67ufDCeP90LXhq6Uv8zjwzPEkwTHWtgvz8fE466SRGjBjBCSecwDXXXMPixYvp06cPHTp0KG0lf/PNN1x//fX06tWLbt268eqrr5Y+v2/fvnTv3p3u3bvzvr9zXbZsGTk5OQwZMoSTTjqJa665huqOCPzmm29ywQUX0LhxYxo1asQFF1zAG2+U/wMwM2PXLm+k7507d9KqVavSZdOmTePyyy+nefPm1YolSGrpS/zC1PpNh7qOGZP4I6GuXWHq1MOusnHjRl588UVmzpzJaaedxvPPP897773HggULePjhh3nllVd46KGHOO+885g5cyY7duygV69enH/++TRv3pxFixaRlZXFhg0bGDp0aOl4W2vWrGHt2rW0atWKPn36sHz5cs4666xDtv2rX/2KP/zhD+ViOvvss/nNb35zyLzCwkLatPn+v9ezs7MpLCws99ypU6fSv39/7rzzTg4ePFi6IyosLOTll19m6dKlrFy5MrbXLwUp6Uv87rvPu/UPfTNamOpaRe3ataNz584AdOrUiX79+mFmdO7cmfz8fADeeustFixYwJQpUwDvUtN//etftGrVitGjR5OXl0ft2rX59NNPS8vt1asX2dnZAHTt2pX8/PxySf+uu+7irrvuSmh9pk+fzuOPP87ll1/OvHnzuOGGG1i8eDFjxozh0UcfpVat9O4gUdIXyRSVtMhrSr169Urv16pVq/RxrVq12L9/P+D9kGj+/PmceOKJhzx3woQJtGjRgo8++oiDBw+SlZUVtdzatWuXlhWpKi391q1bl/bNg/fDtpycnHLPzc3N5YknngDgiiuu4MYbbwRg1apVXH311QAUFxezcOFC6tSpw6WXXlr+RUlhSvoSuLhP6kra6N+/P9OmTWPatGmYGWvWrKFbt27s3LmT7OxsatWqRW5uLgcOHKhSuVVp6ffv35/77ruP7du3A97RxyOPPFJuvVatWvGXv/yFnJwclixZQocOHQD47LPPStcZMWIEF198cdolfIjxRK6Z5ZvZ380sz8xW+fMam9kiM9vg3zby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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# try setting the range\n", + "f.mc.set_xrange(-1, 3.5)\n", + "f.mc.use_mean_and_std() # let's see what the mean and std is now\n", + "print(f)\n", + "f.mc.show_histogram()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Print Style and Formatting" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.844 +/- 0.421 [kg^2⋅m^-2] (the value has 3 significant figures)\n", + "0.844 +/- 0.421 [kg^2⋅m^-2] (the uncertainty has 3 significant figures)\n" + ] + } + ], + "source": [ + "# you can specify the precision of the values\n", + "q.set_sig_figs_for_value(3)\n", + "print(\"{} (the value has 3 significant figures)\".format(f))\n", + "q.set_sig_figs_for_error(3)\n", + "print(\"{} (the uncertainty has 3 significant figures)\".format(f))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(8.44 +/- 4.21) * 10^-1 [kg^2⋅m^-2]\n", + "(8.44 \\pm 4.21) * 10^-1 [kg^2⋅m^-2]\n", + "0.844 +/- 0.421 [kg^2⋅m^-2]\n" + ] + } + ], + "source": [ + "# you can change the print formatting\n", + "q.set_print_style(q.PrintStyle.SCIENTIFIC)\n", + "print(f)\n", + "q.set_print_style(q.PrintStyle.LATEX)\n", + "print(f)\n", + "# or reset it to default\n", + "q.set_print_style(q.PrintStyle.DEFAULT)\n", + "print(f)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "default (exponent) style unit printing: kg^2⋅m^-2\n", + "fraction style unit printing: kg^2/m^2\n", + "\n", + "The complete value representation will change accordingly:\n", + "0.844 +/- 0.421 [kg^2/m^2]\n" + ] + } + ], + "source": [ + "# you can change the style of how units are displayed\n", + "q.set_unit_style(q.UnitStyle.EXPONENTS) # this is the default\n", + "print(\"default (exponent) style unit printing: {}\".format(f.unit))\n", + "q.set_unit_style(q.UnitStyle.FRACTION) # more intuitive but sometimes ambiguous\n", + "print(\"fraction style unit printing: {}\".format(f.unit))\n", + "\n", + "# print the complete value\n", + "print(\"\\nThe complete value representation will change accordingly:\\n{}\".format(f))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Correlated Measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sometimes when two series of measurements are correlated, the error propagation should reflect that. It's worth noting that repeated measurements of the same length are not automatically correlated. Whether two measurements are physically correlated is at the discretion of the user. There are two values related to correlated measurements. The \"covariance\" indicates the extent to which two random variables change in tandem, and \"correlation\" is indicates how strongly two variables are related, which is confined between -1 and 1" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.275 +/- 0.135\n" + ] + } + ], + "source": [ + "q.reset_default_configuration() # first reset everything to default\n", + "q.set_sig_figs_for_error(3)\n", + "\n", + "# first let's take two series of measurements\n", + "m1 = q.Measurement([20, 20.2, 20.3, 20.4])\n", + "m2 = q.Measurement([20, 20.1, 19.8, 20.3])\n", + "\n", + "result = m1 + m2\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Covariance: 0.011666666666666523\n", + "Correlation: 0.3281650616569432\n" + ] + } + ], + "source": [ + "# let's say m1 and m2 are measured together, and they might be correlated\n", + "q.set_correlation(m1, m2) # this declares the correlation\n", + "\n", + "# since m1 and m2 are of the same length, QExPy is able to calculate the covariance\n", + "cor = q.get_correlation(m1, m2)\n", + "cov = q.get_covariance(m1, m2)\n", + "print(\"Covariance: {}\\nCorrelation: {}\".format(cov, cor))" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.275 +/- 0.155\n" + ] + } + ], + "source": [ + "# as a result, the error will be recalculated\n", + "result.recalculate() # since something changed, this ensures that everything is updated\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.272 +/- 0.153\n" + ] + } + ], + "source": [ + "# the monte carlo simulated results will also be properly correlated\n", + "result.recalculate()\n", + "q.set_error_method(q.ErrorMethod.MONTE_CARLO)\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.275 +/- 0.180\n", + "40.276 +/- 0.180\n" + ] + } + ], + "source": [ + "# the user can personally set the correlation between two values\n", + "q.set_correlation(m1, m2, 0.8)\n", + "# q.set_covariance(m1, m2, 0.02)\n", + "\n", + "result.recalculate() # first ask that the value is updated\n", + "\n", + "q.set_error_method(q.ErrorMethod.DERIVATIVE)\n", + "print(result)\n", + "q.set_error_method(q.ErrorMethod.MONTE_CARLO)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Measurement Arrays" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "QExPy can also handle a series of measurements with ```MeasurementArray```, which is an array of individual measurements, each with an uncertainty. This is a sub-class of ```numpy.ndarray```, so it can be operated on as one." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "length = [ 1.00 +/- 0.50, 2.00 +/- 0.50, 3.00 +/- 0.50, 4.00 +/- 0.50, 5.00 +/- 0.50 ] (m)\n" + ] + } + ], + "source": [ + "q.reset_default_configuration()\n", + "q.set_sig_figs_for_error(2)\n", + "\n", + "# you can record an array with the same uncertainty throughout\n", + "arr1 = q.MeasurementArray([1, 2, 3, 4, 5], 0.5, name=\"length\", unit=\"m\")\n", + "print(arr1)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 1.0 +/- 0, 2.0 +/- 0, 3.0 +/- 0, 4.0 +/- 0, 5.0 +/- 0 ]\n" + ] + } + ], + "source": [ + "# if the error is left out, it's by default set to 0\n", + "arr2 = q.MeasurementArray([1, 2, 3, 4, 5])\n", + "print(arr2)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "length = [ 1.00 +/- 0.10, 2.00 +/- 0.20, 3.00 +/- 0.30, 4.00 +/- 0.40, 5.00 +/- 0.50 ] (m)\n" + ] + } + ], + "source": [ + "# you can record an array of measurement with distinct uncertainties\n", + "arr3 = q.MeasurementArray(\n", + " [1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4, 0.5], name=\"length\", unit=\"m\")\n", + "print(arr3)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "length_2 = 3.00 +/- 0.30 [m]\n" + ] + } + ], + "source": [ + "# individual measurements can be extracted\n", + "measurement = arr3[2]\n", + "print(measurement)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean: mean of length = 3.00 +/- 0.71 [m]\n", + "Sum: length = 15.00 +/- 0.74 [m]\n", + "Standard Deviation: 1.5811388300841898\n", + "Error Weighted Mean: 1.5600683241601823\n" + ] + } + ], + "source": [ + "# the measurement array has basic statistical uncertainties\n", + "print(\"Mean: {}\".format(arr3.mean()))\n", + "print(\"Sum: {}\".format(arr3.sum()))\n", + "print(\"Standard Deviation: {}\".format(arr3.std()))\n", + "print(\"Error Weighted Mean: {}\".format(arr3.error_weighted_mean()))" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "arr4 = [ 2.00 +/- 0.20, 3.00 +/- 0.30, 4.00 +/- 0.40, 5.00 +/- 0.50 ] (m)\n", + "arr5 = [ 2.00 +/- 0.20, 3.00 +/- 0.30, 4.00 +/- 0.40, 5.00 +/- 0.50, 6.00 +/- 0.60 ] (m)\n", + "arr6 = [ 3.00 +/- 0.30, 4.00 +/- 0.40, 5.00 +/- 0.50, 6.00 +/- 0.60, 7.00 +/- 0.70 ] (m)\n", + "arr7 = [ 6.00 +/- 0.60, 2.00 +/- 0.20, 3.00 +/- 0.30, 4.00 +/- 0.40, 5.00 +/- 0.50 ] (m)\n" + ] + } + ], + "source": [ + "# basic operations with measurement arrays\n", + "arr4 = arr3.delete(0)\n", + "arr4.name = \"arr4\"\n", + "print(arr4)\n", + "arr5 = arr4.append((6, 0.6))\n", + "arr5.name = \"arr5\"\n", + "print(arr5)\n", + "# operations can be chained\n", + "arr6 = arr4.delete(0).append([(6, 0.6),(7, 0.7)])\n", + "arr6.name = \"arr6\"\n", + "print(arr6)\n", + "arr7 = arr4.insert(0, (6, 0.6))\n", + "arr7.name = \"arr7\"\n", + "print(arr7)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "arr7_0 = 6.00 +/- 0.60 [m]\n", + "arr7_1 = 2.00 +/- 0.20 [m]\n", + "arr7_2 = 3.00 +/- 0.30 [m]\n", + "arr7_3 = 4.00 +/- 0.40 [m]\n", + "arr7_4 = 5.00 +/- 0.50 [m]\n" + ] + } + ], + "source": [ + "# the index of variables are calculated accordingly\n", + "for element in arr7:\n", + " print(element)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Math Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "QExPy includes wrappers for some basic math functions, which works on both individual values and measurement arrays. Available functions include basic trig functions such as ```sin```, ```cos```, ```tan```, and ```sec```, ```csc```, and other functions such as ```sqrt```, ```log```, ```exp```." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time = 1.2771 +/- 0.0080 [s]\n" + ] + } + ], + "source": [ + "# for example, try finding out the time it takes for an object to fall 8 meters\n", + "h = q.Measurement(8,0.1)\n", + "g = 9.81\n", + "t = q.sqrt(2*h/g)\n", + "t.name = \"time\"\n", + "t.unit = \"s\"\n", + "print(t)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "log base e of h: 2.079 +/- 0.012\n", + "log base 2 of h: 3.000 +/- 0.018\n" + ] + } + ], + "source": [ + "# the log function can take 1 or 2 arguments\n", + "res = q.log(h) # by default the base is e\n", + "print(\"log base e of h: {}\".format(res))\n", + "res = q.log(2, h) # you can specify the base\n", + "print(\"log base 2 of h: {}\".format(res))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Right now unit propagation with functions is not yet supported. It will be implemented in future versions" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/jupyter/1_Intro_to_Error_Propagation.ipynb b/examples/jupyter/1_Intro_to_Error_Propagation.ipynb deleted file mode 100644 index 3fea59b..0000000 --- a/examples/jupyter/1_Intro_to_Error_Propagation.ipynb +++ /dev/null @@ -1,1112 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Error Propagation with QExPy\n", - "\n", - "The first step is to import the error module for error propagation. We'll import it as \"q\":" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " Loading BokehJS ...\n", - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "(function(global) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - "\n", - " if (typeof (window._bokeh_onload_callbacks) === \"undefined\" || force === true) {\n", - " window._bokeh_onload_callbacks = [];\n", - " window._bokeh_is_loading = undefined;\n", - " }\n", - "\n", - "\n", - " \n", - " if (typeof (window._bokeh_timeout) === \"undefined\" || force === true) {\n", - " window._bokeh_timeout = Date.now() + 5000;\n", - " window._bokeh_failed_load = false;\n", - " }\n", - "\n", - " var NB_LOAD_WARNING = {'data': {'text/html':\n", - " \"
\\n\"+\n", - " \"

\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"

\\n\"+\n", - " \"
    \\n\"+\n", - " \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n", - " \"
  • use INLINE resources instead, as so:
  • \\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n", - " \"from bokeh.resources import INLINE\\n\"+\n", - " \"output_notebook(resources=INLINE)\\n\"+\n", - " \"\\n\"+\n", - " \"
\"}};\n", - "\n", - " function display_loaded() {\n", - " if (window.Bokeh !== undefined) {\n", - " document.getElementById(\"27f5bd5d-be76-4417-8060-775df8356902\").textContent = \"BokehJS successfully loaded.\";\n", - " } else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(display_loaded, 100)\n", - " }\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " window._bokeh_onload_callbacks.forEach(function(callback) { callback() });\n", - " delete window._bokeh_onload_callbacks\n", - " console.info(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(js_urls, callback) {\n", - " window._bokeh_onload_callbacks.push(callback);\n", - " if (window._bokeh_is_loading > 0) {\n", - " console.log(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls == null || js_urls.length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " console.log(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " window._bokeh_is_loading = js_urls.length;\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " var s = document.createElement('script');\n", - " s.src = url;\n", - " s.async = false;\n", - " s.onreadystatechange = s.onload = function() {\n", - " window._bokeh_is_loading--;\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: all BokehJS libraries loaded\");\n", - " run_callbacks()\n", - " }\n", - " };\n", - " s.onerror = function() {\n", - " console.warn(\"failed to load library \" + url);\n", - " };\n", - " console.log(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.getElementsByTagName(\"head\")[0].appendChild(s);\n", - " }\n", - " };var element = document.getElementById(\"27f5bd5d-be76-4417-8060-775df8356902\");\n", - " if (element == null) {\n", - " console.log(\"Bokeh: ERROR: autoload.js configured with elementid '27f5bd5d-be76-4417-8060-775df8356902' but no matching script tag was found. \")\n", - " return false;\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.js\", \"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.js\"];\n", - "\n", - " var inline_js = [\n", - " function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - " \n", - " function(Bokeh) {\n", - " \n", - " document.getElementById(\"27f5bd5d-be76-4417-8060-775df8356902\").textContent = \"BokehJS is loading...\";\n", - " },\n", - " function(Bokeh) {\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " }\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " \n", - " if ((window.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - " inline_js[i](window.Bokeh);\n", - " }if (force === true) {\n", - " display_loaded();\n", - " }} else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!window._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " window._bokeh_failed_load = true;\n", - " } else if (force !== true) {\n", - " var cell = $(document.getElementById(\"27f5bd5d-be76-4417-8060-775df8356902\")).parents('.cell').data().cell;\n", - " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", - " }\n", - "\n", - " }\n", - "\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", - " run_inline_js();\n", - " } else {\n", - " load_libs(js_urls, function() {\n", - " console.log(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " });\n", - " }\n", - "}(this));" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import qexpy as q" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we'll declare two measured values, x and y, with uncertainties, and print them out. We use the Measurement object from qexpy:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false, - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "x = 10 +/- 1\n", - "y = 5 +/- 3\n" - ] - } - ], - "source": [ - "#Our two measured values:\n", - "x = q.Measurement(10,1)\n", - "y = q.Measurement(5,3)\n", - "\n", - "#We can print them out:\n", - "print(\"x =\",x)\n", - "print(\"y =\",y)\n", - "#x.set_correlation(y,0.3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can declare a third object, z, which depends on x and y. The uncertainty in x and y will be correctly propagated to z, so once we have defined z, we can simply print it out with the correct uncertainty:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "z = 15 +/- 3\n" - ] - } - ], - "source": [ - "#We define z\n", - "z = x+y\n", - "#z can now be printed out\n", - "print(\"z =\",z)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note how the uncertainties have been kept to 1 significant figure. In this case, the error in z was obtained by adding the errors in x and y in quadrature. We can change the number of significant figures to confirm that the errors were indeed added in quadrature. We can choose between setting the number of significant figures based on the uncertainty (more common) or based on the central value." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "z = 15.000 +/- 3.162\n" - ] - } - ], - "source": [ - "q.set_sigfigs_error(4) # set sigfigs based on the error\n", - "print(\"z =\",z)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's compare the error in z to what the errors in x and y are when added manually in quadrature. We need to import the sqrt() function from the math package to apply mathematical functions to numbers:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3.1622776601683795\n" - ] - } - ], - "source": [ - "import math as m\n", - "quadrature = m.sqrt(x.std**2+y.std**2)\n", - "print(quadrature)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Math functions\n", - "We can propagate the uncertainties through any operator (+,-,\\*,/) automatically as we showed above. QExPy also knows how to propagate the uncertainty through common mathematical functions. To use mathematical functions on Measurement objects, we need to call the functions from the QExPy package (as opposed to the math package as we did above for 2 numbers)." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "time = 1.277102 +/- 0.007982 seconds\n" - ] - } - ], - "source": [ - "#If an object fell a distance of 8.0 +/0.1 m, how long did it take to fall?\n", - "y = q.Measurement(8,0.1)\n", - "g = 9.81\n", - "t = q.sqrt(2*y/g)\n", - "print(\"time = \",t, \"seconds\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Error in correlated quantities\n", - "If we have two measurements, x and y, that are correlated, then their correlation factor will impact the uncertainty on a quantity that depends on them:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "x and y uncorrelated: z= 15.000 +/- 3.162\n", - "x and y positively correlated: z= 15.000 +/- 3.606\n", - "x and y negatively correlated: z= 15.000 +/- 2.646\n" - ] - } - ], - "source": [ - "x = q.Measurement(10,1)\n", - "y = q.Measurement(5,3)\n", - "z = x+y\n", - "print(\"x and y uncorrelated: z=\",z)\n", - "\n", - "#Now set a correlation factor between x and y:\n", - "x.set_correlation(y,0.5)\n", - "z = x+y\n", - "print(\"x and y positively correlated: z=\",z)\n", - "\n", - "#We can also use the covariance factor instead of the correlation factor:\n", - "x.set_covariance(y,-1.5)\n", - "z = x+y\n", - "print(\"x and y negatively correlated: z=\",z)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we don't specify any correlation factors, then all quantities are assumed to be independent. However, a quantity should not be independent from itself, so QExPy knows how to track the correlation in quantities that depend on common quantities. " - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.000 +/- 0.000\n" - ] - } - ], - "source": [ - "x = q.Measurement(10,1)\n", - "y = x*x\n", - "z = x*x - y #this should be 0 +/- 0, since it's really x^2 - x^2 \n", - "print(z)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "## Statistical measurements\n", - "QExPy can also handle the case when you have repeated measurements of a single quantity and you want to average them together. Suppose that you have measured 5 values of some quantity T. QExPy will automatically assume that those values should be averaged together so that T is given by the mean of the measured values with an uncertainty given by the standard deviation of the values.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "5.3000 +/- 0.5431\n" - ] - } - ], - "source": [ - "T=q.Measurement( [5.6, 4.8, 6.1, 4.9, 5.1 ] )\n", - "print(T)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "T can be used just as any other measurement with uncertainties, and its error will be propagate correctly:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.1849 +/- 0.1214\n" - ] - } - ], - "source": [ - "omega = 2*3.14/T\n", - "print(omega)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we have measured many values, it can sometimes be useful to visualize those measurements in a histogram. QExPy will automatically create a histogram of the values, showing lines corresponding to the mean and the range covered by one standard deviation" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "T.show_histogram()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Multiple measurements (Arrays of Measurements)\n", - "QExPy can also handle the case of having multiple measurements, when each measurement has its own uncertainty. For example, if you have three measurements of a single quantity, in order to get an average value, you should weight each measurement by its uncertainty (or rather the weight should be 1 over the square of the uncertainty). This is handled by the MeasurementArray class:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "g_0 = 9.8000 +/- 0.2000 [s^-2 m^1 ],\n", - "g_1 = 14.000 +/- 3.000 [s^-2 m^1 ],\n", - "g_2 = 9.9000 +/- 0.1000 [s^-2 m^1 ]\n", - "The mean is 11.2333333333\n", - "The error weighted mean is 9.88366 +/- 0.08940\n" - ] - } - ], - "source": [ - "gvals = q.MeasurementArray([(9.8,0.2), (14,3) , (9.9,0.1)], name=\"g\", units='m/s^2') \n", - "print(gvals)\n", - "print(\"The mean is \",gvals.mean)\n", - "print(\"The error weighted mean is \",gvals.error_weighted_mean)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The MeasurementArray object truly is an array of Measurement objects, so individual measurements can be retrieved (remember that the first element in an array in python has index 0 not 1, so element 1 is the second element):" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "g_1 = 14.000 +/- 3.000 [s^-2 m^1 ]\n" - ] - } - ], - "source": [ - "print(gvals[1])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Error propagation methods\n", - "\n", - "### Derivative method (default)\n", - "By default, QExPy propagates the uncertainties using the \"derivative\" method. That is, for a function, $f(x,y)$, that depends on measured quantities $x\\pm\\sigma_x$ and $y\\pm\\sigma_y$, with covariance $\\sigma_{xy}$ between the two measured quantities, the uncertainty in $f$ is given by:\n", - "$$ \\sigma_f = \\sqrt{ \\left(\\frac{\\partial f}{\\partial x} \\sigma_x \\right)^2 + \\left(\\frac{\\partial f}{\\partial y} \\sigma_y \\right)^2 + 2 \\frac{\\partial f}{\\partial x} \\frac{\\partial f}{\\partial y}\\sigma_{xy} }$$\n", - "\n", - "QExPy evaluates the derivatives exactly when propagating the uncertainties using an algorithm called \"automatic differentiation\". This is possible as QExPy internally keeps track of the dependency of quantities on each other, and as an added bonus can also be used to evaluate numerical derivatives exactly. Although the derivative method is commonly taught in undergraduate laboratories, it is only valid when the relative uncertainties in the quantities being propagated are small (e.g. less than ~10% relative uncertainty). This method is thus not strongly encouraged, although it has been made the default because it is so prevalent in undergraduate teaching. \n", - "\n", - "\n", - "### Min-Max method (not recommended)\n", - "This is not the only way to propagate the uncertainty in $f$. For example, the \"Min-Max\" method, is a method to yield a more conservative (larger) estimate of the uncertainty in $f$ and is often used in introductory courses. The Min-Max method defines the central value and uncertainty in $f$ as:\n", - "$$f=\\frac{1}{2}(f^{max}+f^{min})$$\n", - "$$\\sigma_f=\\frac{1}{2}(f^{max}-f^{min})$$\n", - "\n", - "where $f^{max}$ ($f^{min}$) is the maximum (minimum) value that $f$ takes when $x$ and $y$ are varied within their uncertainty range. For example, if $f(x,y)=x+y$, then the maximum and minimum of $f$ are easily found:\n", - "\n", - "$$f^{max} = (x+\\sigma_x)+(y+\\sigma_y)$$\n", - "$$f^{min} = (x-\\sigma_x)-(y-\\sigma_y)$$\n", - "\n", - "The Min-Max method actually requires a numerical approximation to evaluate the values of $f^{max}$ and $f^{min}$ for all but the most simple cases. Hence although it is a good method to introduce the idea of uncertainty propagation, it is not recommended to use this method in any serious calculation (it also does not take correlations into account). \n", - "\n", - "### Monte Carlo method (recommended!)\n", - "Finally, the recommended method to propagate errors is the \"Monte-Carlo\" method, although it is the hardest to understand. The MC method is based on a statiscal understanding of the measurements. In the QExPy implementation, currently, the main assumptions is that the uncertainty in a quantity is given by a \"standard error\"; that is, if $x = 10\\pm 1$, then we *assume* that this error and uncertainty should be interpreted as: \"if we measure $x$ multiple times, we will obtain a set of measurements that are normally distributed with a mean of 10 and a standard deviation of 1\". In other words, we assume that $x$ has a 68% chance of being in the range between 9 and 11.\n", - "\n", - "\n", - "The MC method then uses the assumption that measured quantities are normally distributed and use this to propagate the errors by using Monte Carlo simulation. Suppose that we have measured $x$ and $y$ and wish to determine the central value and uncertainty in $x=x+y$. The Monte Carlo method will generate normally distributed random values for $x$ and $y$ (the random numbers will be correctly correlated if the user has indicated that $x$ and $y$ are correlated), then it will add those random values together, to obtain a set of values for $z$. The mean and standard deviation of the random values for $z$ are taken as the central value and uncertainty in $z$. \n", - "\n", - "## What QExPy actually does\n", - "Although the user appears to choose the method used to propagate the errors, QExPy always uses all three methods behind the scenes and the user only decides which method to print out. This allows QExPy to compare the results behind the scenes, in particular, to inform the users that the uncertainties using a particular method (e.g. derivative) may be inaccurate and to suggest that the user choose a different method. \n", - "\n", - "## Example\n", - "Below, we illustrate an example of using the different methods to propagate the uncertainty in the Coulomb force based on the measurement of two charges, $q_1$ and $q_2$, and the distance between them, $r$. We illustrate how the derivative method does not give the correct answer if the relative uncertainties in the measured quantities are large. \n", - "\n", - "### 1 % relative uncertainty calculation\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Derivative method, F = 18.0000 +/- 0.4409\n", - "Min-Max method, F = 18.0144 +/- 0.7202\n", - "Monte Carlo method, F = 17.9972 +/- 0.4425\n" - ] - } - ], - "source": [ - "#Measurements with 1% relative uncertainties:\n", - "relative_factor = 0.01 \n", - "\n", - "#Measured values\n", - "q1m=1e-6\n", - "q2m=2e-5\n", - "rm=0.1\n", - "\n", - "#Convert to Measurement objects\n", - "q1 = q.Measurement(q1m,relative_factor*q1m)\n", - "q2 = q.Measurement(q2m,relative_factor*q2m)\n", - "r = q.Measurement(rm,relative_factor*rm)\n", - "#Coulomb's constant:\n", - "k = 9e9 \n", - "\n", - "#Define the Force:\n", - "F = k*q1*q2/r**2\n", - "\n", - "#Print out the different errors\n", - "q.set_error_method(\"derivative\")\n", - "print(\"Derivative method, F = \",F)\n", - "q.set_error_method(\"minmax\")\n", - "print(\"Min-Max method, F =\", F)\n", - "q.set_error_method(\"mc\")\n", - "print(\"Monte Carlo method, F =\",F)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 10 % relative uncertainty calculation\n", - "We see that in this case, the Monte Carlo method returns a different uncertainty than the derivative method, because the derivative method is incorrect when the uncertainties are this large." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false, - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Derivative method, F = 18.000 +/- 4.409\n", - "Min-Max method, F = 19.469 +/- 7.420\n", - "Monte Carlo method, F = 18.771 +/- 5.036\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#Measurements with 10% relative uncertainties:\n", - "relative_factor = 0.1 \n", - "\n", - "#Measured values\n", - "q1m=1e-6\n", - "q2m=2e-5\n", - "rm=0.1\n", - "\n", - "#Convert to Measurement objects\n", - "q1 = q.Measurement(q1m,relative_factor*q1m)\n", - "q2 = q.Measurement(q2m,relative_factor*q2m)\n", - "r = q.Measurement(rm,relative_factor*rm)\n", - "#Coulomb's constant:\n", - "k = 9e9 \n", - "\n", - "#Define the Force:\n", - "F = k*q1*q2/r**2\n", - "\n", - "#Print out the different errors\n", - "q.set_error_method(\"derivative\")\n", - "print(\"Derivative method, F = \",F)\n", - "q.set_error_method(\"minmax\")\n", - "print(\"Min-Max method, F =\", F)\n", - "q.set_error_method(\"mc\")\n", - "print(\"Monte Carlo method, F =\",F)\n", - "\n", - "q.plot_engine=\"bokeh\"\n", - "F.show_MC_histogram()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Printing style and formatting\n", - "QExPy can print results in different styles, and the user can assign names and units to variables (although one should not rely on units being correctly propagated). If one assigns and name and units to a variable, the print() function will automatically include those when printing out the variable.\n", - "\n", - "One can also set the \"print style\" to be either \"Default\", \"Scientific\" or \"Latex\". In the scientific and default styles, QExPy will factor out the powers of 10 to make the central value and uncertainty easier to compare. " - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "distance = (10000 +/- 1220)*10^(-3) [m]\n", - "distance = (10000 \\pm 1220)*10^{-3}\\,m\n", - "distance = 10.000 +/- 1.220 [m]\n" - ] - } - ], - "source": [ - "x = q.Measurement(10,1.22,name=\"distance\", units=\"m\")\n", - "q.set_print_style(\"Scientific\")\n", - "print(x)\n", - "q.set_print_style(\"Latex\")\n", - "print(x)\n", - "q.set_print_style(\"Default\")\n", - "print(x)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## Exact derivatives\n", - "Since QExPy needs to be able to evaluate derivatives (by using the automatic differentiation algorithm that exploits the Chain Rule), we can use QExPy to evaluate numerical derivatives exactly. That is, given a function, $f(x,y)$, QExPy can evaluate $\\frac{\\partial f(x,y)}{\\partial x}$ at a given value of $x$ and $y$.\n", - "\n", - "For example, if we have:\n", - "$$x(t) = \\frac{1}{2}gt^2 $$\n", - "\n", - "and we have measured $t=8 \\pm 1$, we can evaluate the exact value of the derivative:\n", - "\n", - "$$\\frac{\\partial x(t)}{\\partial t} = \\frac{d x(t)}{d t} = gt$$\n", - "\n", - "at $t=8 \\pm 1$\n" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "78.48\n" - ] - } - ], - "source": [ - "t = q.Measurement(8,1)\n", - "x = 0.5 * 9.81 * t**2\n", - "print(x.get_derivative(t))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/jupyter/2_Intro_to_Graphing.ipynb b/examples/jupyter/2_Intro_to_Graphing.ipynb deleted file mode 100644 index 3b5492b..0000000 --- a/examples/jupyter/2_Intro_to_Graphing.ipynb +++ /dev/null @@ -1,976 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Plotting with QExPy\n", - "The first step is to import the module, we import it as p.\n", - "We will also import the error module so that we can plot Measurement objects" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " Loading BokehJS ...\n", - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "(function(global) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - "\n", - " if (typeof (window._bokeh_onload_callbacks) === \"undefined\" || force === true) {\n", - " window._bokeh_onload_callbacks = [];\n", - " window._bokeh_is_loading = undefined;\n", - " }\n", - "\n", - "\n", - " \n", - " if (typeof (window._bokeh_timeout) === \"undefined\" || force === true) {\n", - " window._bokeh_timeout = Date.now() + 5000;\n", - " window._bokeh_failed_load = false;\n", - " }\n", - "\n", - " var NB_LOAD_WARNING = {'data': {'text/html':\n", - " \"
\\n\"+\n", - " \"

\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"

\\n\"+\n", - " \"
    \\n\"+\n", - " \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n", - " \"
  • use INLINE resources instead, as so:
  • \\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n", - " \"from bokeh.resources import INLINE\\n\"+\n", - " \"output_notebook(resources=INLINE)\\n\"+\n", - " \"\\n\"+\n", - " \"
\"}};\n", - "\n", - " function display_loaded() {\n", - " if (window.Bokeh !== undefined) {\n", - " document.getElementById(\"0ee72846-1e96-4962-a4d5-76df45cfd883\").textContent = \"BokehJS successfully loaded.\";\n", - " } else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(display_loaded, 100)\n", - " }\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " window._bokeh_onload_callbacks.forEach(function(callback) { callback() });\n", - " delete window._bokeh_onload_callbacks\n", - " console.info(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(js_urls, callback) {\n", - " window._bokeh_onload_callbacks.push(callback);\n", - " if (window._bokeh_is_loading > 0) {\n", - " console.log(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls == null || js_urls.length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " console.log(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " window._bokeh_is_loading = js_urls.length;\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " var s = document.createElement('script');\n", - " s.src = url;\n", - " s.async = false;\n", - " s.onreadystatechange = s.onload = function() {\n", - " window._bokeh_is_loading--;\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: all BokehJS libraries loaded\");\n", - " run_callbacks()\n", - " }\n", - " };\n", - " s.onerror = function() {\n", - " console.warn(\"failed to load library \" + url);\n", - " };\n", - " console.log(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.getElementsByTagName(\"head\")[0].appendChild(s);\n", - " }\n", - " };var element = document.getElementById(\"0ee72846-1e96-4962-a4d5-76df45cfd883\");\n", - " if (element == null) {\n", - " console.log(\"Bokeh: ERROR: autoload.js configured with elementid '0ee72846-1e96-4962-a4d5-76df45cfd883' but no matching script tag was found. \")\n", - " return false;\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.js\", \"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.js\"];\n", - "\n", - " var inline_js = [\n", - " function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - " \n", - " function(Bokeh) {\n", - " \n", - " document.getElementById(\"0ee72846-1e96-4962-a4d5-76df45cfd883\").textContent = \"BokehJS is loading...\";\n", - " },\n", - " function(Bokeh) {\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " }\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " \n", - " if ((window.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - " inline_js[i](window.Bokeh);\n", - " }if (force === true) {\n", - " display_loaded();\n", - " }} else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!window._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " window._bokeh_failed_load = true;\n", - " } else if (force !== true) {\n", - " var cell = $(document.getElementById(\"0ee72846-1e96-4962-a4d5-76df45cfd883\")).parents('.cell').data().cell;\n", - " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", - " }\n", - "\n", - " }\n", - "\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", - " run_inline_js();\n", - " } else {\n", - " load_libs(js_urls, function() {\n", - " console.log(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " });\n", - " }\n", - "}(this));" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import qexpy as q" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The most common scenario in undergraduate laboratories is that we have measured some \"depedent\" quantity, $y$, as we varied some \"independent\" quantity $x$. Let us suppose that we have such a set of measurements, where the $x$ values had negligible (zero) uncertainty, and the y values each have the same uncertainty.\n", - "\n", - "We can represent the data using the MeasurementArray object:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "x = q.MeasurementArray([1,2,3,4,5,6,7], error=[0], name=\"length\", units=\"m\")\n", - "y = q.MeasurementArray([2.1,3.9,6.3,8.1,9.6,11.3,14.6], error=[0.5], name=\"force\", units=\"N\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now make a plot of $y$ versus $x$ and \"show\" it:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "figure = q.MakePlot(x,y)\n", - "figure.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fitting\n", - "QExPy easily handles fitting of data. A few common functions are automatically implemented (linear, polynomial, gaussian), but the used can also specify their own function. The fitting algorithm calls the scipy.optimize package in the background to perform the fit, and the fitted parameters are returned as Measurement objects (with uncertainties and correlations). \n", - "\n", - "A fit to a straight line of the above plot is easily done, and the residuals can also be shown. By default, the fit will show a band around the fitted function computed using 1 standard error in the fitted parameters (and their correlation)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of dataset0 to linear\n", - "Fit parameters:\n", - "dataset0_linear_fit0_fitpars_intercept = 0.0 +/- 0.4,\n", - "dataset0_linear_fit0_fitpars_slope = 1.99 +/- 0.09\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.894]\n", - " [-0.894 1. ]]\n", - "\n", - "chi2/ndof = 4.49/4\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "figure.fit(\"linear\")\n", - "figure.add_residuals()\n", - "figure.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If the user wishes to specify a custom function to fit, then the function can be specified as a python function. The first argument to the function must be the dependent variable, and the second argument must be an array (or tuple) corresponding to the parameters to be fit. \n", - "\n", - "In order to properly fit the user-supplied function, QExPy needs to have a guess for the values of the parameters." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of dataset0 to custom\n", - "Fit parameters:\n", - "dataset0_custom_fit1_fitpars_par0 = 0.5 +/- 0.8,\n", - "dataset0_custom_fit1_fitpars_par1 = 1.7 +/- 0.4,\n", - "dataset0_custom_fit1_fitpars_par2 = 0.04 +/- 0.05\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.924 0.84 ]\n", - " [-0.924 1. -0.977]\n", - " [ 0.84 -0.977 1. ]]\n", - "\n", - "chi2/ndof = 3.94/3\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Define a simple quadratic\n", - "def myModel(x, *parameters):\n", - " return parameters[0]+parameters[1]*x+parameters[2]*x**2\n", - "\n", - "#Carry out the fit, and change the color of the fit function\n", - "figure.fit(model=myModel, parguess=[1,1,2], fitcolor=\"blue\")\n", - "figure.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Showing multiple data sets on one figure\n", - "QExPy can also show multiple data sets on the same plot, and each data set can have its own individual fit.\n", - "\n", - "Suppose that we have a second set of measurement of the y values for the same x values as above. The easiest way to plot them along with the initial values, is to create a DataSet and to add that Dataset to the figure:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "y2 = q.MeasurementArray([2.4,4.2,5.9,8.2,9.2,12.0,13.6], error=[0.5], name=\"force\", units=\"N\")\n", - "dataset = q.XYDataSet(x,y2)\n", - "figure.add_dataset(dataset)\n", - "figure.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/jupyter/3_Load_data_into_plot.ipynb b/examples/jupyter/3_Load_data_into_plot.ipynb deleted file mode 100644 index fe44a21..0000000 --- a/examples/jupyter/3_Load_data_into_plot.ipynb +++ /dev/null @@ -1,2461 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ways to load data into a plot\n", - "\n", - "In this notebook, we look at the different ways to plot data in QExPy. QExPy has several different (equivalent) ways to create a plot based on data, depending on the nature of the data.\n", - "\n", - "First, we must import QExPy:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " Loading BokehJS ...\n", - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "(function(global) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - "\n", - " if (typeof (window._bokeh_onload_callbacks) === \"undefined\" || force === true) {\n", - " window._bokeh_onload_callbacks = [];\n", - " window._bokeh_is_loading = undefined;\n", - " }\n", - "\n", - "\n", - " \n", - " if (typeof (window._bokeh_timeout) === \"undefined\" || force === true) {\n", - " window._bokeh_timeout = Date.now() + 5000;\n", - " window._bokeh_failed_load = false;\n", - " }\n", - "\n", - " var NB_LOAD_WARNING = {'data': {'text/html':\n", - " \"
\\n\"+\n", - " \"

\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"

\\n\"+\n", - " \"
    \\n\"+\n", - " \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n", - " \"
  • use INLINE resources instead, as so:
  • \\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n", - " \"from bokeh.resources import INLINE\\n\"+\n", - " \"output_notebook(resources=INLINE)\\n\"+\n", - " \"\\n\"+\n", - " \"
\"}};\n", - "\n", - " function display_loaded() {\n", - " if (window.Bokeh !== undefined) {\n", - " document.getElementById(\"4f06327e-dcb4-4329-a292-7028a0863866\").textContent = \"BokehJS successfully loaded.\";\n", - " } else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(display_loaded, 100)\n", - " }\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " window._bokeh_onload_callbacks.forEach(function(callback) { callback() });\n", - " delete window._bokeh_onload_callbacks\n", - " console.info(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(js_urls, callback) {\n", - " window._bokeh_onload_callbacks.push(callback);\n", - " if (window._bokeh_is_loading > 0) {\n", - " console.log(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls == null || js_urls.length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " console.log(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " window._bokeh_is_loading = js_urls.length;\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " var s = document.createElement('script');\n", - " s.src = url;\n", - " s.async = false;\n", - " s.onreadystatechange = s.onload = function() {\n", - " window._bokeh_is_loading--;\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: all BokehJS libraries loaded\");\n", - " run_callbacks()\n", - " }\n", - " };\n", - " s.onerror = function() {\n", - " console.warn(\"failed to load library \" + url);\n", - " };\n", - " console.log(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.getElementsByTagName(\"head\")[0].appendChild(s);\n", - " }\n", - " };var element = document.getElementById(\"4f06327e-dcb4-4329-a292-7028a0863866\");\n", - " if (element == null) {\n", - " console.log(\"Bokeh: ERROR: autoload.js configured with elementid '4f06327e-dcb4-4329-a292-7028a0863866' but no matching script tag was found. \")\n", - " return false;\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.js\", \"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.js\"];\n", - "\n", - " var inline_js = [\n", - " function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - " \n", - " function(Bokeh) {\n", - " \n", - " document.getElementById(\"4f06327e-dcb4-4329-a292-7028a0863866\").textContent = \"BokehJS is loading...\";\n", - " },\n", - " function(Bokeh) {\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " }\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " \n", - " if ((window.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - " inline_js[i](window.Bokeh);\n", - " }if (force === true) {\n", - " display_loaded();\n", - " }} else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!window._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " window._bokeh_failed_load = true;\n", - " } else if (force !== true) {\n", - " var cell = $(document.getElementById(\"4f06327e-dcb4-4329-a292-7028a0863866\")).parents('.cell').data().cell;\n", - " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", - " }\n", - "\n", - " }\n", - "\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", - " run_inline_js();\n", - " } else {\n", - " load_libs(js_urls, function() {\n", - " console.log(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " });\n", - " }\n", - "}(this));" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#import the package\n", - "import qexpy as q\n", - "\n", - "#We can choose the method in which the plots are build:\n", - "q.plot_engine = \"bokeh\" #These plots are more interactive,\n", - " #but not as good to save to file\n", - " #This is the default (so no need to specify)\n", - " \n", - "#q.plot_engine = \"mpl\" #These plots are a little easier \n", - " #to manipulate and to save to file - try it out!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data where the x-values have no uncertainty\n", - "Suppose that we have multiple measurements of a quantity $y$ corresponding to some quantity $x$. Further, let us assume that all of the $y$ measurement have the same uncertainty, and the $x$ measurements have no uncertainty. Below, we show several equivalent ways to load the data into a plot.\n", - "\n", - "In each case, we create a Plot Object (with variable names starting with 'fig'), and then use the show() function of the plot object to actually display it. The most convenient way to load data is usually in the form of an array (or a list as they are called in python) - arrays are given by a comma-seperated list of numbers surrounded by square brackets [ ].\n", - "\n", - "### Passing data directly to MakePlot()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#The most direct method is to use MakePlot with the data specified directly.\n", - "\n", - "#We only need to specify the data, but if we give additional parameters\n", - "#QExPy will label the axes for us\n", - "fig1 = q.MakePlot(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [0.9, 1.4, 2.5, 4.2, 5.7, 6., 7.3, 7.1, 8.9, 10.8],\n", - " yerr = 0.5,\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'April20thData')\n", - "#and then we can show the figures:\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Passing MeasurementArray to MakePlot()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false, - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "x measurements:\n", - "1.0 +/- 0.0,\n", - "2.0 +/- 0.0,\n", - "3.0 +/- 0.0,\n", - "4.0 +/- 0.0,\n", - "5.0 +/- 0.0,\n", - "6.0 +/- 0.0,\n", - "7.0 +/- 0.0,\n", - "8.0 +/- 0.0,\n", - "9.0 +/- 0.0,\n", - "10.0 +/- 0.0\n", - "\n", - "y measurements:\n", - "length_0 = 1.6 +/- 0.5 [mm],\n", - "length_1 = 3.1 +/- 0.5 [mm],\n", - "length_2 = 4.0 +/- 0.5 [mm],\n", - "length_3 = 4.6 +/- 0.5 [mm],\n", - "length_4 = 4.6 +/- 0.5 [mm],\n", - "length_5 = 5.4 +/- 0.5 [mm],\n", - "length_6 = 6.9 +/- 0.5 [mm],\n", - "length_7 = 8.8 +/- 0.5 [mm],\n", - "length_8 = 8.3 +/- 0.5 [mm],\n", - "length_9 = 10.6 +/- 0.5 [mm]\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Another option, is to identify our measurements of x as y as two independent\n", - "#arrays of measurements. In this case, we first use the MeasurementArray Object.\n", - "\n", - "#With the MeasurementArray, we can specify the values, the error on those values \n", - "#(0 if unspecified), as well as the name and units\n", - "\n", - "xmes = q.MeasurementArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])\n", - "ymes = q.MeasurementArray([1.6, 3.1, 4., 4.6, 4.6, 5.4, 6.9, 8.8, 8.3, 10.6],\n", - " error= 0.5,\n", - " name = 'length', units='mm')\n", - "\n", - "#Let's print out our MeasurementArrays:\n", - "print(\"x measurements:\")\n", - "print(xmes)\n", - "print(\"\\ny measurements:\")\n", - "print(ymes)\n", - "#As you will note, the x measurements have no name or units, so those\n", - "#are not diplayed, whereas for y, we specified a name and unit\n", - "\n", - "#we can now plot these by giving MakePlot the MeasurementArrays directly, \n", - "#and it will know to get the names and units that we specified:\n", - "fig2 = q.MakePlot(xmes, ymes, data_name=\"April21stData\")\n", - "fig2.show()\n", - "#As you can see, the plot will have an automatic label for the x axis\n", - "#since we never specified what that label should be. We can add our \n", - "#own label by calling:\n", - "#fig2.set_labels(xtitle = \"length [m]\")\n", - "#before the show() command - Try it!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Passing an XYDataSet to MakePlot()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "length_0 = 1.0 +/- 0.0 [m] , force_0 = 0.6 +/- 0.5 [N]\n", - "length_1 = 2.0 +/- 0.0 [m] , force_1 = 1.6 +/- 0.5 [N]\n", - "length_2 = 3.0 +/- 0.0 [m] , force_2 = 3.5 +/- 0.5 [N]\n", - "length_3 = 4.0 +/- 0.0 [m] , force_3 = 4.1 +/- 0.5 [N]\n", - "length_4 = 5.0 +/- 0.0 [m] , force_4 = 4.6 +/- 0.5 [N]\n", - "length_5 = 6.0 +/- 0.0 [m] , force_5 = 5.6 +/- 0.5 [N]\n", - "length_6 = 7.0 +/- 0.0 [m] , force_6 = 6.1 +/- 0.5 [N]\n", - "length_7 = 8.0 +/- 0.0 [m] , force_7 = 7.9 +/- 0.5 [N]\n", - "length_8 = 9.0 +/- 0.0 [m] , force_8 = 8.7 +/- 0.5 [N]\n", - "length_9 = 10.0 +/- 0.0 [m] , force_9 = 9.8 +/- 0.5 [N]\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#A third option, is to use an XYDataSet to represent our paired set of \n", - "#x and y data. In fact, the XYDataSet is how QExPy internally holds\n", - "#the data that are passed when calling MakePlot()\n", - "\n", - "#The datasets can be built either directly from the data or from \n", - "#two measurement arrays, inmuch the same way as we passed the data\n", - "#directly to the plots\n", - "\n", - "\n", - "xy3 = q.XYDataSet( xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [0.6, 1.6, 3.5, 4.1, 4.6, 5.6, 6.1, 7.9, 8.7, 9.8],\n", - " yerr = 0.5,\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'April22ndData')\n", - "\n", - "#Let's print out the dataset to see what it looks like:\n", - "print(xy3)\n", - "\n", - "#We can now build the figure from the dataset directly:\n", - "fig3 = q.MakePlot(xy3)\n", - "fig3.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise:\n", - "Produce a plot from a dataset, where the dataset has been created from 2 MeasurementArrays" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "## Exercise \n", - "\n", - "#In a similar way as we produce a plot from 2 MeasurementArrays, we can \n", - "#produce an XYDataSet from 2 MeasurementArrays()\n", - "\n", - "#xmes = MeasurementArray()\n", - "#ymes = MeasurementArray()\n", - "#xy4 = XYDataSet(xdata = x, ydata = y)\n", - "#fig4 = q.MakePlot(xy4)\n", - "#fig4.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data when the y uncertainty on each point is different\n", - "\n", - "It is often the case that each point has a different uncertainty. In this case, we can specify the yerr argument of the various functions to be an array rather than a single number. The array needs to obviously of the same length as that for the ydata.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#To use different error bars on each point, we specify the yerr argument of MakePlot\n", - "#to be an array instead of a single number\n", - "fig5 = q.MakePlot(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [1., 2.2, 2.7, 4.9, 5.7, 5.5, 7., 8.1, 8.9, 9.7],\n", - " yerr = [0.5, 0.4, 0.5, 0.2, 0.5, 0.5, 0.5, 0.3, 0.5, 0.6],\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'April23rdData')\n", - "fig5.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using MeasurementArray to create a plot with varying uncertainties in y\n", - "\n", - "By specifying the yerr argument as an array, you can use MeasurementArray or XYDataSet to specify uncertainties in y that are different for each point. Note that for MeasurementArray, the argument is called error instead of yerr, since a MeasurementArray is not aware of whether it contains data for x or for y, or for some completely different purpose.\n", - "\n", - "Here we use yet a different method of creating a MeasurementArray for the y values, by passing pairs of values corresponding to the measured y values and their uncertainty:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Define the y measurements as paired values of measurement and uncertainty\n", - "ymes = q.MeasurementArray([\n", - " (1.4, 0.5),\n", - " (3.2, 0.4),\n", - " (2.6, 0.5),\n", - " (3.8, 0.5),\n", - " (5.4, 0.6),\n", - " (6.7, 0.5),\n", - " (7.4, 0.4),\n", - " (7.3, 0.6),\n", - " (8.7, 0.3),\n", - " (10.6, 0.4)],\n", - " name = 'Force',\n", - " units = 'N')\n", - "\n", - "#we will use the same values for x as above, so no need to redefine them\n", - "#let's build a dataset out of the measurement arrays, and then build the figure\n", - "#from the dataset instead of the measurement arrays.\n", - "#When we defined xmes, we didn't give the x values names or units,\n", - "#so let's take the opportunity to name them in the data set:\n", - "xy6 = q.XYDataSet(xdata = xmes, ydata = ymes,\n", - " xname = 'length', xunits='m',\n", - " data_name = 'April24thData')\n", - "fig6 = q.MakePlot(xy6)\n", - "fig6.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data when both x and y have uncertainties:\n", - "\n", - "Adding uncertainties in the x direction is trivial, and the Plot Objects can be generated in the same way, simply by specifying the additional keyword 'xerr' (or 'error' in the case of a MeasurementArray). The uncertainties in x can be specifed as a single number to be applied to all data points, or as an array to have different errors on each point. \n", - "\n", - "### Example: Passing the data as an XYDataSet to MakePlot()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "xy7 = q.XYDataSet(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " xerr = [0.5, 0.6, 0.5, 0.6, 0.5, 0.6, 0.5, 0.5, 0.6, 0.4],\n", - " ydata = [2., 2.3, 2.8, 3.8, 5.2, 5.9, 7.8, 7.7, 8.8, 10.1],\n", - " yerr = [0.4, 0.6, 0.5, 0.4, 0.4, 0.5, 0.5, 0.5, 0.6, 0.5],\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'April25thData')\n", - "\n", - "fig7 = q.MakePlot(xy7)\n", - "fig7.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Plotting multiple data sets on the same figure\n", - "\n", - "Internally, the QExPy Plot objects store a list of the data sets that they need to plot. Everytime that we initialize a Plot Object, we also initialize an internal list of data sets that it can hold. \n", - "\n", - "For example, let us add the data set from the third figure to the first figure, and plot them both together:\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#We defined a dataset called xy3 when we created figure 3.\n", - "#let's add that dataset to figure 1, and show figure 1 again\n", - "fig1.add_dataset(xy3)\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we created figure 2, we did not use a dataset (we used 2 MeasurementArrays). However, when we created the figure, QExPy internally created a data set. Let's add that dataset to figure 1 as well:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.0 +/- 0.0 [m] , length_0 = 1.6 +/- 0.5 [mm]\n", - "2.0 +/- 0.0 [m] , length_1 = 3.1 +/- 0.5 [mm]\n", - "3.0 +/- 0.0 [m] , length_2 = 4.0 +/- 0.5 [mm]\n", - "4.0 +/- 0.0 [m] , length_3 = 4.6 +/- 0.5 [mm]\n", - "5.0 +/- 0.0 [m] , length_4 = 4.6 +/- 0.5 [mm]\n", - "6.0 +/- 0.0 [m] , length_5 = 5.4 +/- 0.5 [mm]\n", - "7.0 +/- 0.0 [m] , length_6 = 6.9 +/- 0.5 [mm]\n", - "8.0 +/- 0.0 [m] , length_7 = 8.8 +/- 0.5 [mm]\n", - "9.0 +/- 0.0 [m] , length_8 = 8.3 +/- 0.5 [mm]\n", - "10.0 +/- 0.0 [m] , length_9 = 10.6 +/- 0.5 [mm]\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Let's retrieve the dataset from figure 2:\n", - "xy2 = fig2.get_dataset()\n", - "#Let's print it out to see if it what we put in\n", - "print(xy2)\n", - "#Now, let's add it to figure 1\n", - "fig1.add_dataset(xy2)\n", - "#the above lines could have been combined into 1:\n", - "#fig1.add_dataset(fig2.get_dataset())\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "At this point, figure 1, now has 3 datasets in it. We can retrieve those data sets individually by specifying their index. We can use either the get_dataset() function above, or directly access the dataset inside of fig1. Here we print out the first and third data set using the two methods:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "length_0 = 1.0 +/- 0.0 [m] , force_0 = 0.9 +/- 0.5 [N]\n", - "length_1 = 2.0 +/- 0.0 [m] , force_1 = 1.4 +/- 0.5 [N]\n", - "length_2 = 3.0 +/- 0.0 [m] , force_2 = 2.5 +/- 0.5 [N]\n", - "length_3 = 4.0 +/- 0.0 [m] , force_3 = 4.2 +/- 0.5 [N]\n", - "length_4 = 5.0 +/- 0.0 [m] , force_4 = 5.7 +/- 0.5 [N]\n", - "length_5 = 6.0 +/- 0.0 [m] , force_5 = 6.0 +/- 0.5 [N]\n", - "length_6 = 7.0 +/- 0.0 [m] , force_6 = 7.3 +/- 0.5 [N]\n", - "length_7 = 8.0 +/- 0.0 [m] , force_7 = 7.1 +/- 0.5 [N]\n", - "length_8 = 9.0 +/- 0.0 [m] , force_8 = 8.9 +/- 0.5 [N]\n", - "length_9 = 10.0 +/- 0.0 [m] , force_9 = 10.8 +/- 0.5 [N]\n", - "\n", - "1.0 +/- 0.0 [m] , length_0 = 1.6 +/- 0.5 [mm]\n", - "2.0 +/- 0.0 [m] , length_1 = 3.1 +/- 0.5 [mm]\n", - "3.0 +/- 0.0 [m] , length_2 = 4.0 +/- 0.5 [mm]\n", - "4.0 +/- 0.0 [m] , length_3 = 4.6 +/- 0.5 [mm]\n", - "5.0 +/- 0.0 [m] , length_4 = 4.6 +/- 0.5 [mm]\n", - "6.0 +/- 0.0 [m] , length_5 = 5.4 +/- 0.5 [mm]\n", - "7.0 +/- 0.0 [m] , length_6 = 6.9 +/- 0.5 [mm]\n", - "8.0 +/- 0.0 [m] , length_7 = 8.8 +/- 0.5 [mm]\n", - "9.0 +/- 0.0 [m] , length_8 = 8.3 +/- 0.5 [mm]\n", - "10.0 +/- 0.0 [m] , length_9 = 10.6 +/- 0.5 [mm]\n", - "\n" - ] - } - ], - "source": [ - "#Print out the first dataset of fig1, it's original dataset, using get_data_set. Note\n", - "#that the first dataset has and index of 0, not 1:\n", - "print(fig1.get_dataset(0))\n", - "\n", - "#Now we print the third (index 2) data set accessing it directly, using square brackets\n", - "print(fig1.datasets[2])\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If a Plot Object has multiple data sets, and the index is not specified when using get_dataset(), then the last dataset is returned (that's why we didn't need to specify the index when got the dataset from fig2). When accessing the dataset directly, then one must specify an index using square brackets.\n", - "\n", - "### Starting from an empty plot\n", - "Now that we know how to add data to a plot, we can generate plots \"from scratch\", without specifying any data, and then adding datasets. Let's plot datasets from figures 6 and 7 onto a fresh figure. We'll also choose our own colours for the datasets and override the defaults" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Here we make a plot with no data\n", - "fig8 = q.MakePlot()\n", - "fig8.add_dataset(fig6.get_dataset(), color='purple')\n", - "fig8.add_dataset(fig7.get_dataset(), color='goldenrod')\n", - "fig8.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Advanced: generating data programatically\n", - "QExPy can understand data that have been generated programatically, if those data are in the form of a list or of a numpy array. Numpy is a very popular computing package in python that can manipulate numbers ver efficiently. Here we show an example of making a plot using random numbers generated in numpy that follow a normal distribution. To do so, we need to import the module for numpy.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of normal_dist to gaussian\n", - "Fit parameters:\n", - "normal_dist_gaussian_fit0_fitpars_mean = 10.0 +/- 0.0,\n", - "normal_dist_gaussian_fit0_fitpars_sigma = -2.0 +/- 0.0,\n", - "normal_dist_gaussian_fit0_fitpars_normalization = 50.01267875546766 +/- 0.0\n", - "\n", - "Correlation matrix: \n", - "[[ 0. 0. 0.]\n", - " [ 0. 0. 0.]\n", - " [ 0. 0. 0.]]\n", - "\n", - "chi2/ndof = 0.00/46\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import numpy as np\n", - "\n", - "#Generate 50 equally spaced values along x, between 0 and 20\n", - "xvalues = np.linspace(0,20,50)\n", - "\n", - "#Define a function that returns the normal distribution:\n", - "def normal(x, mean, sigma, norm):\n", - " return norm*1./np.sqrt(2.*3.14*sigma**2)*np.exp( -((x-mean)/sigma)**2/2)\n", - "\n", - "#Use our function on all of the x values to generate y values\n", - "yvalues = normal(xvalues, mean=10, sigma=2, norm=50)\n", - "\n", - "#Create a plot with constant uncertainties in y \n", - "fig9 = q.MakePlot(xdata=xvalues, ydata=yvalues, yerr=0.5,\n", - " data_name='normal_dist', xname='x',yname='y')\n", - "\n", - "#Just for fun, let's fit the data.\n", - "#This returns some extra text with the results of the fit, and\n", - "#adds a fitted line to the plot\n", - "fig9.fit(\"gaussian\")\n", - "#Note that in this case, because the data are exactly along the fit\n", - "#line, the fitter assigns uncertainties of 0 to the fitted parameters!\n", - "\n", - "#We can overwrite the default color for the plot \n", - "#(which corresponds to dataset 0)\n", - "fig9.datasets_colors[0]='navy'\n", - "fig9.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false, - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of normal_dist to gaussian\n", - "Fit parameters:\n", - "normal_dist_gaussian_fit0_fitpars_mean = 9.97 +/- 0.06,\n", - "normal_dist_gaussian_fit0_fitpars_sigma = 2.01 +/- 0.06,\n", - "normal_dist_gaussian_fit0_fitpars_normalization = 51 +/- 1\n", - "\n", - "Correlation matrix: \n", - "[[ 1.000e+00 -3.739e-08 -1.132e-10]\n", - " [ -3.739e-08 1.000e+00 5.774e-01]\n", - " [ -1.132e-10 5.774e-01 1.000e+00]]\n", - "\n", - "chi2/ndof = 17.36/46\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#If instead, we add some fluctuations to the y values, by using numpy\n", - "#to make some random numbers for us, the fit parameters will have \n", - "#uncertainties:\n", - "\n", - "#Add some randomness to the yvalues (by choosing normally\n", - "#distributed number about the original values)\n", - "yvalues = np.random.normal(yvalues, 0.5)\n", - "fig10 = q.MakePlot(xdata=xvalues, ydata=yvalues, yerr=1.0,\n", - " data_name='normal_dist', xname='x',yname='y')\n", - "fig10.fit(\"gaussian\", parguess=[10,2,50])\n", - "fig10.datasets_colors[0]='darkolivegreen'\n", - "fig10.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/jupyter/4_Interactive_Linear_Fit.ipynb b/examples/jupyter/4_Interactive_Linear_Fit.ipynb deleted file mode 100644 index 393d693..0000000 --- a/examples/jupyter/4_Interactive_Linear_Fit.ipynb +++ /dev/null @@ -1,498 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Interactive linear fit\n", - "\n", - "QExPy provides an interactive interface to fit data to a linear model. This is aimed to give a pedagogic tool for students to understand how to define uncertainties in parameters of a model. It also provides a way to visualize how the correlation between parameters affects the uncertainty on quantities that depend on those parameters.\n", - "\n", - "The interactive plot will first call the internal QExPy routines to perform a linear fit to the data and will initialize the parameters to their best estimates from the fit. The user is then provided with sliders to modify those values to observe the effect and to help understand how the automated fits work.\n", - "\n", - "The interactive plot interface is slightly different depending on the plotting engine that is chosen (bokeh or matlplotlib). The matlpotlib interface is the most straightforward and the recommended one. The main difference with the bokeh interface is that the bokeh interface has to be called using 2 different cells, and the controls cannot be placed in the same cell as the plot.\n", - "\n", - "With some versions of Jupyter, it is possible that the widgets for interacting with the plot do not display, and you may see a warning that you need to enable widgets. If that is the case, you will need to open the Anaconda prompt (or a terminal), and type:\n", - "\n", - "`jupyter nbextension enable --py --sys-prefix widgetsnbextension`\n", - "\n", - "You will then need to **completely shut down Jupyter and restart it** for the change to take effect. You should only need to do this once. \n", - "\n", - "We start by importing the QExPy model and setting the plot engine:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " Loading BokehJS ...\n", - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "(function(global) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - "\n", - " if (typeof (window._bokeh_onload_callbacks) === \"undefined\" || force === true) {\n", - " window._bokeh_onload_callbacks = [];\n", - " window._bokeh_is_loading = undefined;\n", - " }\n", - "\n", - "\n", - " \n", - " if (typeof (window._bokeh_timeout) === \"undefined\" || force === true) {\n", - " window._bokeh_timeout = Date.now() + 5000;\n", - " window._bokeh_failed_load = false;\n", - " }\n", - "\n", - " var NB_LOAD_WARNING = {'data': {'text/html':\n", - " \"
\\n\"+\n", - " \"

\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"

\\n\"+\n", - " \"
    \\n\"+\n", - " \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n", - " \"
  • use INLINE resources instead, as so:
  • \\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n", - " \"from bokeh.resources import INLINE\\n\"+\n", - " \"output_notebook(resources=INLINE)\\n\"+\n", - " \"\\n\"+\n", - " \"
\"}};\n", - "\n", - " function display_loaded() {\n", - " if (window.Bokeh !== undefined) {\n", - " document.getElementById(\"f4233477-d4b8-4fd0-936c-8b799b45b41d\").textContent = \"BokehJS successfully loaded.\";\n", - " } else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(display_loaded, 100)\n", - " }\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " window._bokeh_onload_callbacks.forEach(function(callback) { callback() });\n", - " delete window._bokeh_onload_callbacks\n", - " console.info(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(js_urls, callback) {\n", - " window._bokeh_onload_callbacks.push(callback);\n", - " if (window._bokeh_is_loading > 0) {\n", - " console.log(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls == null || js_urls.length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " console.log(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " window._bokeh_is_loading = js_urls.length;\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " var s = document.createElement('script');\n", - " s.src = url;\n", - " s.async = false;\n", - " s.onreadystatechange = s.onload = function() {\n", - " window._bokeh_is_loading--;\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: all BokehJS libraries loaded\");\n", - " run_callbacks()\n", - " }\n", - " };\n", - " s.onerror = function() {\n", - " console.warn(\"failed to load library \" + url);\n", - " };\n", - " console.log(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.getElementsByTagName(\"head\")[0].appendChild(s);\n", - " }\n", - " };var element = document.getElementById(\"f4233477-d4b8-4fd0-936c-8b799b45b41d\");\n", - " if (element == null) {\n", - " console.log(\"Bokeh: ERROR: autoload.js configured with elementid 'f4233477-d4b8-4fd0-936c-8b799b45b41d' but no matching script tag was found. \")\n", - " return false;\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.js\", \"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.js\"];\n", - "\n", - " var inline_js = [\n", - " function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - " \n", - " function(Bokeh) {\n", - " \n", - " document.getElementById(\"f4233477-d4b8-4fd0-936c-8b799b45b41d\").textContent = \"BokehJS is loading...\";\n", - " },\n", - " function(Bokeh) {\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " }\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " \n", - " if ((window.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - " inline_js[i](window.Bokeh);\n", - " }if (force === true) {\n", - " display_loaded();\n", - " }} else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!window._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " window._bokeh_failed_load = true;\n", - " } else if (force !== true) {\n", - " var cell = $(document.getElementById(\"f4233477-d4b8-4fd0-936c-8b799b45b41d\")).parents('.cell').data().cell;\n", - " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", - " }\n", - "\n", - " }\n", - "\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", - " run_inline_js();\n", - " } else {\n", - " load_libs(js_urls, function() {\n", - " console.log(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " });\n", - " }\n", - "}(this));" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import qexpy as q\n", - "q.plot_engine=\"mpl\" # not strictly necessary, as the plot_engine is ignored for the interactive fit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We then create a Plot Object and call the interactive_linear_fit() function" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\nEREJUwr2RURERETClIJ9EREREZEwpWBfRERERCRMKdgXEREREQlTCvZFRERERMKUgn0RERERkTCl\nYF9EREQ7klzWAAAgAElEQVREJEwp2BcRERERCVMK9kVEREREwpSCfRERERGRMKVgX0REREQkTCnY\nFxEREREJUwr2RURERETClIJ9EREREZEwpWBfRERERCRMKdgXEREREQlTCvZFRERERMKUgn0RERER\nkTClYF9EREREJEwp2BcRERERCVMK9kVEREREwpSCfRERERGRMKVgX0REREQkTLV6sG+MecEYU2yM\n2XLSsf8xxmw3xmwyxvzDGBNXx7V7jTGbjTEbjDFrW7utIiIiIiLhpC1G9ucDk0859hGQYq0dBnwB\n3F/P9ROstWnW2vRWap+IiIiISFhq9WDfWvtv4KtTjn1orfXWfFwF9G3tdoiIiIiIdDTGWtv6lRgz\nAHjbWpsS4tw/gdestS+FOPclUA74gP+11j5bTx0/AH4AkJiYOHLRokUt0/hmqKyspHPnzu1Wv7Qf\n9X3Hpv7v2NT/HZv6v2Nrj/6fMGHCusZkvkS0RWPqYoz5L8ALvFxHkUxrbb4xJgH4yBizveY3Baep\n+SLwLEB6errNyspqjSY3yrJly2jP+qX9qO87NvV/x6b+79jU/x3b2dz/7bYajzHmZmAqcKOt49cL\n1tr8mr+LgX8Ao9usgSIiIiIi57h2CfaNMZOBXwDTrLXH6ijTyRjT5cTPwCRgS6iyIiIiIiJyurZY\nevNVYCVwkTEmzxjzfeCPQBcCqTkbjDHP1JRNNsa8W3NpIpBrjNkIfAq8Y619v7XbKyIiIiISLlo9\nZ99aOyvE4efrKFsATKn5eQ+Q2opNExEREREJa9pBV0REREQkTCnYFxEREREJUwr2RURERETClIJ9\nEREREZEwpWBfRERERCRMKdgXEREREQlTCvZFRERERMKUgn0RERERkTClYF9EREREJEwp2BcRERER\nCVMK9kVEREREwpSCfRERERGRMKVgX0REREQkTCnYFxEREREJUwr2RURERETClIJ9EREREZEwpWBf\nRERERCRMKdgXEREREQlTCvZFRERERMKUgn0RERERkTClYF9EREREJEwp2BcRERERCVMR7d0AERER\nEZFz1aEZM+lz+DBkZbV3U0LSyL6IiIiISJhSsC8iIiIiEqYU7IuIiIiIhCkF+yIiIiIiYUoTdEVE\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig1 = q.MakePlot(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [0.9, 1.4, 2.5, 4.2, 5.7, 6., 7.3, 7.1, 8.9, 10.8],\n", - " yerr = 0.5,\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'mydata')\n", - "\n", - "\n", - "fig1.dimensions_px = [700,500]\n", - "\n", - "#We can specify the error_range parameter as the range over which\n", - "#the parameters can vary (as a fraction of their uncertainty)\n", - "fig1.interactive_linear_fit(error_range=10)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Bokeh version\n", - "The Bokeh version is called a little differently; the main difference is that the interactive widgets need to be displayed in a different output cell. First, we display the fit, and then we interact with it. The interact part will actually interact only with the last Bokeh plot that was made; thus **if you that this in a notebook with additional plots below the interactive one, and you \"Run All\" cells, then the buttons will not work *** (This is why this is the last example in the notebook!)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of mydata to linear\n", - "Fit parameters:\n", - "mydata_linear_fit0_fitpars_intercept = -0.3 +/- 0.4,\n", - "mydata_linear_fit0_fitpars_slope = 1.06 +/- 0.06\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.886]\n", - " [-0.886 1. ]]\n", - "\n", - "chi2/ndof = 9.76/7\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "q.plot_engine=\"bokeh\" # not strickly necessary, as the plot_engine is ignored for the interactive fit\n", - "#show the fit:\n", - "fig1.bk_show_linear_fit()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "#then we create the buttons to interact with it:\n", - "fig1.bk_interact_linear_fit(error_range=10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - }, - "widgets": { - "state": { - "b58ecfcc6c114326a480744d597b2bf5": { - "views": [ - { - "cell_index": 6 - } - ] - }, - "db160d8c00064731a3975d1fe9faab0e": { - "views": [ - { - "cell_index": 3 - } - ] - } - }, - "version": "1.2.0" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/jupyter/5_Fitting_Data.ipynb b/examples/jupyter/5_Fitting_Data.ipynb deleted file mode 100644 index c95b53e..0000000 --- a/examples/jupyter/5_Fitting_Data.ipynb +++ /dev/null @@ -1,751 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Fitting data\n", - "\n", - "QExPy makes it straightforwar to fit data to a model. QExPy has a few built in models (linear, polynomial, gaussian, exponential), but makes it easy for users to provide their own models. \n", - "\n", - "Internally, QExPy fits XYDataSets; this means that one can fit a dataset regardless of whether that dataset is displayed in a Plot Object. XYDataSets can also be fit to multiple models, and we are able to recall the results of the fits to the various models of that single dataset. By default, the Plot Object will only display the last fit for a dataset. \n", - "\n", - "Currently, QExPy uses the least-squares fitting routine from the scipy.optimize package to perform the fit. When the fit results are displayed, the correlation between parameters are taken into account when drawing the error band around the fit line.\n", - "\n", - "As usual, we start by importing the QExPy module and choosing the plot engine.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " Loading BokehJS ...\n", - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "(function(global) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - "\n", - " if (typeof (window._bokeh_onload_callbacks) === \"undefined\" || force === true) {\n", - " window._bokeh_onload_callbacks = [];\n", - " window._bokeh_is_loading = undefined;\n", - " }\n", - "\n", - "\n", - " \n", - " if (typeof (window._bokeh_timeout) === \"undefined\" || force === true) {\n", - " window._bokeh_timeout = Date.now() + 5000;\n", - " window._bokeh_failed_load = false;\n", - " }\n", - "\n", - " var NB_LOAD_WARNING = {'data': {'text/html':\n", - " \"
\\n\"+\n", - " \"

\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"

\\n\"+\n", - " \"
    \\n\"+\n", - " \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n", - " \"
  • use INLINE resources instead, as so:
  • \\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n", - " \"from bokeh.resources import INLINE\\n\"+\n", - " \"output_notebook(resources=INLINE)\\n\"+\n", - " \"\\n\"+\n", - " \"
\"}};\n", - "\n", - " function display_loaded() {\n", - " if (window.Bokeh !== undefined) {\n", - " document.getElementById(\"252da4a5-7ce6-41d5-b411-8ae8d82e0cc3\").textContent = \"BokehJS successfully loaded.\";\n", - " } else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(display_loaded, 100)\n", - " }\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " window._bokeh_onload_callbacks.forEach(function(callback) { callback() });\n", - " delete window._bokeh_onload_callbacks\n", - " console.info(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(js_urls, callback) {\n", - " window._bokeh_onload_callbacks.push(callback);\n", - " if (window._bokeh_is_loading > 0) {\n", - " console.log(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls == null || js_urls.length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " console.log(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " window._bokeh_is_loading = js_urls.length;\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " var s = document.createElement('script');\n", - " s.src = url;\n", - " s.async = false;\n", - " s.onreadystatechange = s.onload = function() {\n", - " window._bokeh_is_loading--;\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: all BokehJS libraries loaded\");\n", - " run_callbacks()\n", - " }\n", - " };\n", - " s.onerror = function() {\n", - " console.warn(\"failed to load library \" + url);\n", - " };\n", - " console.log(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.getElementsByTagName(\"head\")[0].appendChild(s);\n", - " }\n", - " };var element = document.getElementById(\"252da4a5-7ce6-41d5-b411-8ae8d82e0cc3\");\n", - " if (element == null) {\n", - " console.log(\"Bokeh: ERROR: autoload.js configured with elementid '252da4a5-7ce6-41d5-b411-8ae8d82e0cc3' but no matching script tag was found. \")\n", - " return false;\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.js\", \"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.js\"];\n", - "\n", - " var inline_js = [\n", - " function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - " \n", - " function(Bokeh) {\n", - " \n", - " document.getElementById(\"252da4a5-7ce6-41d5-b411-8ae8d82e0cc3\").textContent = \"BokehJS is loading...\";\n", - " },\n", - " function(Bokeh) {\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-0.12.4.min.css\");\n", - " console.log(\"Bokeh: injecting CSS: https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " Bokeh.embed.inject_css(\"https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.4.min.css\");\n", - " }\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " \n", - " if ((window.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - " inline_js[i](window.Bokeh);\n", - " }if (force === true) {\n", - " display_loaded();\n", - " }} else if (Date.now() < window._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!window._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " window._bokeh_failed_load = true;\n", - " } else if (force !== true) {\n", - " var cell = $(document.getElementById(\"252da4a5-7ce6-41d5-b411-8ae8d82e0cc3\")).parents('.cell').data().cell;\n", - " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", - " }\n", - "\n", - " }\n", - "\n", - " if (window._bokeh_is_loading === 0) {\n", - " console.log(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", - " run_inline_js();\n", - " } else {\n", - " load_libs(js_urls, function() {\n", - " console.log(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " });\n", - " }\n", - "}(this));" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import qexpy as q\n", - "q.plot_engine=\"mpl\" # choose bokeh or mpl" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fitting a dataset and then adding it to the plot\n", - "Let's create a dataset and fit it to a linear model. As you can see, the call to the fit() method will output the results of the fit. The fit method can take a variety of arguments that we will explore in this notebook. The fit method also returns a Measurement_Array containing the parameters of the fit." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of xydata to linear\n", - "Fit parameters:\n", - "xydata_linear_fit0_fitpars_intercept = -0.1 +/- 0.3,\n", - "xydata_linear_fit0_fitpars_slope = 0.98 +/- 0.04\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.886]\n", - " [-0.886 1. ]]\n", - "\n", - "chi2/ndof = 4.75/7\n", - "---------------End fit results----------------\n", - "\n" - ] - } - ], - "source": [ - "#Initialize the data set:\n", - "xy1 = q.XYDataSet( xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [0.6, 1.6, 3.5, 4.1, 4.6, 5.6, 6.1, 7.9, 8.7, 9.8],\n", - " yerr = 0.5,\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'xydata')\n", - "\n", - "results = xy1.fit(\"linear\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The fit function returned a Measurement_Array that we stored in the results variable. We can print out those results as well, or use them for other purposes:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All of the results:\n", - "xydata_linear_fit0_fitpars_intercept = -0.1 +/- 0.3,\n", - "xydata_linear_fit0_fitpars_slope = 0.98 +/- 0.04\n", - "-----------------\n", - "Just the first parameter of results:\n", - "xydata_linear_fit0_fitpars_intercept = -0.1 +/- 0.3\n" - ] - } - ], - "source": [ - "print(\"All of the results:\")\n", - "print(results)\n", - "print(\"-----------------\")\n", - "print(\"Just the first parameter of results:\")\n", - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "We can display the dataset by initializing a Plot Object. In this case, since the dataset has a fit associated to it, the Plot Object will automatically display the results of the fit:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\nbKzOzs72PZeenq5jYmL01Vdf7duWk5OjO3TooPft26e11vq1117zXTJYvHixHjJkiDYMQ7tcLj1q\n1Cg9YsQIrbXWW7Zs0b169fL189133+moqChdUlKiDcPQ991330VPb2uttcfj0QcOHPA9njFjhn7p\npZe01lUvGVT8edeuXbpjx46VLhn85je/0VprvW/fPt2hQwedk5Ojz5w5o7t06aI///xzX/8bN27U\npmle9vGszrRp03z7Xbdune7Ro4c2DKNKu7Nnz+qSkhKttdbbt2/XXbp00UePHtVaa/3Tn/5UP/jg\ng9o0TW0Yhp4+fbr+xS9+UeO+qzumR48e1YmJibUe/1//+lc9ZswYbRiGzs3N1dHR0frgwYNa68q/\n7wUFBbpz586+342HHnrId9npfPV1yUDOEAghRD1pToWQDMNg+vTpJCQkkJiYyPHjx3n44YertFu4\ncCHvv/8+gwYN4p577uG9996rVCHQ6/Vy9dVXk5qayttvv03nzp2JiIhg8eLFPP/88yQmJnLVVVfx\n3HPPVTsT4Eq88sorpKen07t3bx555BHee+89bDbr42zGjBm+0sMHDx4kMTGR/v37M336dBYuXEjX\nrl0BePbZZ8nLy2PgwIEkJCTgdrt5+umnr2g8n332Gbfeemut299333306NGD3r17c/311/PMM8/4\n/r4WL17MM888A1iL182bN4/U1FR69erF2bNnq72kUW9qkxr8/VOfZwhMw9Anbh6lj193vS5ds1ab\n1aRKf6GWc4/feOONK96HnCG4cs3tm2pL0ZKO+8GDB3VUVFSVueRNUW2Oe23+zWoNxo4dqzdv3lwv\nfTXE7ztyhqAqbZqceWgG3j17MbKPcPr+aZx5aAbaNP09tFrLysrib3/7m7+HIYS4TM888wzDhw/n\nd7/7XYuqiihgxYoVDBkyxN/DqLMWP8vg5OQ7fH828/Lw7tlb6XnXqtXkjh6DLSKCTh9XnQdaW3v2\n7GH06NF8/fXXdO/eneeff57du3fz5ZdfsnXrVqKiogBr7nGXLl146qmn+OSTT3jqqadwOp3cfvvt\nlfq755572Lt3L263m169erFgwQIiIiJ49NFHyczMJCkpiV69evHxxx/zxBNP8OWXX1JWVkbHjh1Z\nsGBBvS1UIYSoHy+88AIvvPBCo++3IQsh6Xq6BCCahtZ1hqC4+CLb615nqV+/frz88stMmTKFVatW\n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\ngg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig1 = q.MakePlot(xy1)\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding a dataset to a plot, and then fitting it\n", - "As you may have noted from previous notebooks, we can also do things the other way around. We can first create the Plot Object from the data set, and then fit the dataset from the Plot Object. If the Plot Object contains multiple datasets, by default it will fit the last one that was added:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgQAAAFpCAYAAADjgDCPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAH8pJREFUeJzt3X+U1fV95/Hne/g1/hYUaAqcYLOEiQ4GZZKY2DaDBE6a\nxsRu4URrFRMImy0bo23SGrtNml+eZNdkY1qbLAv+WmM4lbgxzak5UOS2bm2sqEQwYOw2GscaGYQJ\nog4wc9/7x1wo4MAMMvd+uXOfj3M4c+/n+7nf73s+Z5h5fT/fX5GZSJKkxtZUdAGSJKl4BgJJkmQg\nkCRJBgJJkoSBQJIkYSCQJEkYCCQdhYjIiDh5gD5TI2LJINf35ohYFxFbImJTRNwaEScMTbWSjoaB\nQNJQmwoMKhAAe4A/zMwW4FzgROCTVapL0hEYCKQGEREtEfFsRLyx8v6zEbEyIp6PiDcc0O8bEXF9\n5fV/rOy9b4iIPztkfd+OiPURsTEi/k9EjK0suhk4u/KZVZW+N0bEwxHx44hYu6+GzHw6Mx+rvC4D\n/wy8sdpjIem1wjsVSo0jIq4AlgKfAf4CeBtwPfBqZn6ucjjgX4FWIICfAO/KzCcj4o+BrwCnZOau\niDgzM7dV1vtFYGRmXhcR7cCNmdl2wHYP7LsYeE9mXnpIbScA64FPZ+b3qzgMkvoxsugCJNVOZv7v\niJgDfA/4jczcGRE3Aw9ExJeA3wdWZ+bWiPgA8GhmPln5+DL6AsE+V0bE5cBo4CTgp0fY9G9FxFLg\nZPr5vRMRI4GVwP2GAakYHjKQGkhEjAbOAbqAiQCZ+Sx9e+YfpG/24OZBrOc3gP8MvDczZwD/FWg+\nTN83Av8DuCwzW4GPHNg3IkYA3wZ2AFe/3u9N0rExEEiN5b8DjwBzgW9FxORK+18AXwf2ZuY/Vdp+\nBJwXEdMq7xcfsJ7TgV8CL0bEGPr+yO+zEzjtgPen0nfy4C8iogn42L4Flfe3Ab3AovQYplQYA4HU\nICLiEqAduCYznwA+B3wnIkZm5t8D3cBf7eufmVvpu1rgbyLiMQ6eAfgh8P/oO0zw98CjByx7HHiy\nchnhqszcCNxN3/kIDwE/O6Dvb9F3mGIG8EjlRMQBZygkDT1PKpRERJwF/CPwHzLzlaLrkVR7zhBI\nDS4iPg88APyRYUBqXM4QSJIkZwgkSZKBQJIkYSCQJEnUyZ0KzzzzzJw6dWrRZRw3Xn75ZU466aSi\ny2g4jnsxHPdiOO7FqMa4P/LII9syc/xA/eoiEEydOpX169cXXcZxo1Qq0d7eXnQZDcdxL4bjXgzH\nvRjVGPeIeGYw/TxkIEmSDASSJMlAIEmSqJNzCA5n586dbN26lb179xZdSk2ddtppbN68eUjXedJJ\nJzF58mSamsyIktSI6jYQ7Ny5kxdeeIFJkyZxwgknEBFFl1QzL730EqeccsqQra9cLvPcc8+xbds2\nJkyYMGTrlSTVj7rdHdy6dSuTJk3ixBNPbKgwUA1NTU1MnDiRX/7yl0WXIkkqSN0Ggr1793LCCSe8\nrs92zl9A5/wFQ1xRfRs1ahQ9PT1FlyFJKkjdBgLAmYEh5FhKUmOr60Aw3EQEu3btOmKfp59+mltv\nvXVQ6/vpT3/K7NmzaWlpobW1lQ9/+MO8+uqrQ1GqJGmYabhAkOUy5R076O3ooHvt/WS5XHRJR+Xp\np5/mtttuG1Tf0aNH87WvfY0tW7bw+OOP88orr3DjjTdWt0BJUl1qqECQ5TLbFy2mZ8uT9D7bwYtX\nLmT7osVDEgq2bNnClClTeOaZvjtEfu5zn+PSSy/lDW94A88///z+fldffTU33HADAPfccw8tLS3M\nnDmTL3zhCwet7/LLL6etrY0ZM2bwO7/zO+zYsQOApUuXsmXLFmbOnMn8+fMB+OQnP8nb3vY23vrW\ntzJnzpz9NUydOpXzzjsP6Dtx8O1vf/v+ZZIkHWjYB4J9JxB2zl/A1rnz6F695qDl3avXsHXuvGM+\nybClpYUbbriBD33oQ6xevZq77rqLZcuWsXDhQpYtWwbArl27WLlyJYsXL+aFF17gox/9KPfeey8b\nNmxgzJgxB63vpptuYv369WzcuJFzzjmHr3zlKwDcfPPNtLS0sGHDBlatWgXAddddx8MPP8yPf/xj\nLrvsMv7kT/7kNfW9+uqr3HLLLXzgAx84pu9TkjQ8DftAcKB8+eXDtL8yJOu/4ooraGlp4ZJLLuGu\nu+7i1FNPZenSpdx666309PRw5513Mm/ePCZMmMBDDz3E+eefz/Tp0wFYsmTJQeu64447mDVrFjNm\nzOCuu+5iw4YNh93ufffdxwUXXEBrays33njja/r29PRw6aWXctFFFxkIJEn9qtsbEw3W+FV373/d\nvfZ+Xrxy4Wv6nP6lL9I856Jj3taePXt44oknOP3003nhhRcAmDJlCm1tbdx7773cfPPN+2cLjuSB\nBx7gm9/8Jg8++CDjx4/fP9vQn2eeeYZrr72Whx9+mLPOOosHH3yQ3/u939u/vLe3l8svv5yxY8fy\njW9845i/R0nS8NRQMwRjZrfTPG/uQW3N8+YyZnb7kKz/U5/6FLNmzWLNmjV87GMfo6OjA4CPf/zj\nXHPNNYwaNYp3vvOdAFxwwQU89thjPPXUUwAsX758/3q6uro47bTTOOOMM9i9eze33HLL/mWnnnrq\nQTcQ2rlzJ6NHj+ZXfuVXKJfLfOtb39q/rFwuc9VVVzFixAhWrFjhpYWSdBzrnL+ASV/8UmHbb6hA\nEE1NjFuxnJEt0xkxZQpn3HE741YsJ4bg/v3f+973KJVKfP3rX+ecc87hs5/9LJdddhk9PT28+93v\nprm5mT/4gz/Y33/ChAksW7aMiy++mPPOO4/u7u79y9773vfypje9iTe/+c28+93v5vzzz9+/7Nxz\nz2XatGm0trYyf/58ZsyYwYIFCzj77LN5xzvewVlnnbW/73333cedd97Jxo0bmTVrFjNnzmTp0qXH\n/L1KkoafyMyiaxhQW1tbrl+//qC2zZs385a3vOV1rW/fCYQHHk6opp/97GdceOGF/Mu//Asnnnji\nMa9vqJ9lsM+xjGkjKJVKtLe3F11Gw3Hci+G4117n/AV0dXUx7e/WDNz5KETEI5nZNlC/YX8OQX9q\nFQQAPvOZz3DLLbfw1a9+dUjCgCRJ1dBQhwyK8PnPf56Ojg4uu+yyokuRJOmwDASSJKm+A0G5zm47\nfDyrh3NJJEnVU7eB4KSTTuK5555jz549/jE7RpnJiy++SHNzc9GlSJIKUrcnFU6ePJlt27bxzDPP\n0NPTU3Q5NdXd3T3kf7ybm5uZPHnykK5TklQ/6jYQNDU1MWHCBCZMmFB0KTVXKpX2P7RIkqShULeH\nDCRJ0tAxEEiSJAOBJElFy3KZ8o4djNy2je6195MFXEVnIJAkqUBZLrN90WJ6tjzJ6M5tvHjlQrYv\nWlzzUGAgkCSpQLvXleheffDzC7pXr2H3ulJN66haIIiIWyJia0RsOqBtXESsiYinKl/HVmv7kiTV\ngz0bN/bbvnfTpn7bq6WaMwS3Ae89pO06YG1mTgPWVt5LktSwRs+Y0W/7qNbWmtZRtUCQmf8AbD+k\n+YPA7ZXXtwOXVGv7kiTVgzGz22meN/egtuZ5cxkzu72mdUQ1b/sbEVOBH2Rma+V9V2aeXnkdwI59\n7/v57BJgCcDEiRNnrVy5smp11ptdu3Zx8sknF11Gw3Hci+G4F8Nxr7FymSnX/ynxyqts+8hVvHLu\nudA0NPvss2fPfiQz2wbqV9idCjMzI+KwaSQzlwHLANra2rK9vb1WpR33SqUSjkftOe7FcNyL4bjX\nXufkyXR1dfH2a64pZPu1vsrghYh4A0Dl69Yab1+SJPWj1oHg+8DCyuuFwL013r4kSepHNS87/A7w\nT8D0iOiIiEXAl4G5EfEU8J7Ke0mSVLCqnUOQmZcdZtGcam1TkiS9Pt6pUJIkGQgkSZKBQJIkYSCQ\nJEkYCCRJEgYCSZJEgbculiRJ/278qrt5olRiWkHbd4ZAkiQZCCRJkoFAkiRhIJAkSRgIJEkSBgJJ\n0jDSOX8BnfMXFF1GXTIQSJIkA4EkSTIQSJIkDASSJAkDgSRJwkAgSZIwEEiSJAwEkiQJA4EkScJA\nIEmSMBBIkiQMBJIkCQOBJEnCQCBJkjAQSJKGiSyXKe/YQW9HB91r7yfL5aJLqisGAklS3ctyme2L\nFtOz5Ul6n+3gxSsXsn3RYkPBUTAQSJLq3u51JbpXrzmorXv1GnavKxVTUB0yEEiS6t6ejRv7bd+7\naVONK6lfBgJJUt0bPWNGv+2jWltrXEn9MhBIkuremNntNM+be1Bb87y5jJndXkxBdchAIEmqe9HU\nxLgVyxnZMp0RU6Zwxh23M27FcqLJP3ODNbLoAiRJGgrR1ETT2LEwdizNcy4qupy6Y3SSJEkGAkmS\nZCCQJEkYCCRJEgYCSZKEgUCSJGEgkCRJGAgkSRIGAkmShIFAkqqic/4COucvKLoMadAMBJIkyUAg\nSZJ8uJEk6RCd8xcwqasL2tuLLuWojV91d9El1C1nCCRJkoFAkiQVFAgi4tqIeCIiNkXEdyKiuYg6\nJElSn5oHgoiYBFwNtGVmKzACuLTWdUiSpH9X1CGDkcAJETESOBH4t4LqkCRJFHCVQWY+FxE3Aj8H\nXgVWZ+bqQ/tFxBJgCcDEiRMplUo1rfN4tmvXLsejAI57Mep13Cd1dQHwRJ3W3tvbW5fjXu+K/HmP\nzKztBiPGAt8FPgR0AXcDqzLzzsN9pq2tLdevX1+jCo9/pVKJ9jq8HKjeOe7FqNdx33eXwnq8DK5z\n/gK6urqY9ndrii6l4VTj5z0iHsnMtoH6FXHI4D3AzzKzMzP3AvcA7yqgDkmSVFFEIPg5cEFEnBgR\nAcwBNhdQhyRVRZbLlHfsoLejg+6195PlctElSQOqeSDIzIeAVcCjwMZKDctqXYckVUOWy2xftJie\nLU/S+2wHL165kO2LFhsKdNwr5CqDzPxsZrZkZmtmXpGZu4uoQ5KG2u51JbpXH3zsvXv1GnavKxVT\nkDRI3qlQkobQno0b+23fu2lTjSt5ffYd7hi5bZuHOxqMgUCShtDoGTP6bR/V2lrjSo7egYc7Rndu\n83BHgzEQSNIQGjO7neZ5cw9qa543lzGz24sp6Ch4uKOxGQgkaQhFUxPjVixnZMt0RkyZwhl33M64\nFcuJpuP/1229H+7Qsan5nQolabiLpiaaxo6FsWNpnnNR0eUMWj0f7tCxO/4jqySpJur5cIeOnYFA\nkgQcfLhjz/gz6+pwh46dhwwkSfvtO9zRE1FXhzt07Ix9kiTJQCBJkgwEkiQJA4EkScJAIEmSMBBI\nkiQMBJIkCe9DIElVMX7V3UWXIB0VZwgkSZKBQJIkechAknSI8avu5olSiWlFF6KacoZAkiQZCCRJ\nkoFAkiRhIJAkSRgIJEkSBgJJkoSBQJIkYSCQJEkYCCRJEgYCSZKEgUCSJGEgkCRJGAgkSRIGAkmS\nhIFAkiQBI4+0MCL+ehDr2J6ZHxuieiRJUgGOGAiAdwCfGaDPdUNUiyRJKshAgeCuzLz9SB0iomUI\n65EkSQU44jkEmfnpgVYwmD6SJOn4dsRAMJi9f2cIJEmqfwNdZXDXINYxmD6SJOk4NtA5BOdGxNYj\nLA9g9xDWI0mSCjBQIHjTINbROxSFSNKhOucvYFJXF7S3F12KNOwdMRBk5jO1KkSSJBXHOxVKkiQD\ngSRJMhBIkiQGGQgi4sSI+GJE3FV53xIRl1S3NEmSVCuDnSH4Jn0nIL618r4D+GxVKpIkSTU32EBw\nbmZeB+wByMxdR/FZSZJ0nBvsH/WDbj4UEc1H8VlJknScG+wf9X+IiOuBMRHRDvw1cO/r3WhEnB4R\nqyJiS0Rsjoh3vt51SZKkYzfYQPCn9N2m+CXgvwH/DPz5MWz3JuCHmdlC33kJm49hXZIk6RgNdOti\nADJzL/Clyr9jEhGnAb8JXFVZ9x4q5yZIkqRiRGYO3CniG8CfZ+b2yvszgD/LzGuOeoMRM4FlwE/o\nmx14BPhEZr58SL8lwBKAiRMnzlq5cuXRbmrY2rVrFyeffHLRZTQcx73GymWmXP+nxCuvsu0jV/HK\nuedCk6cu1Yo/78WoxrjPnj37kcxsG6jfYAPBY5l53kBtgxERbcCPgAsz86GIuAnYmZl/drjPtLW1\n5fr16492U8NWqVSi3Ye91JzjXjtZLrN90WK6V6/Z39Y8by7jViwnDAU14c97Maox7hExqEAw2P9Z\nI/ppG3V0Je3XAXRk5kOV96uA81/nuiQNQ7vXlQ4KAwDdq9ewe12pmIKkBjDYQPBwRNwUEZMiYnJl\nr/7h17PBzPwF8GxETK80zaHv8IEkAbBn48Z+2/du2lTjSqTGMdhAcC1wCvAYfcf8TwaO+vyBA3wc\n+HZEPA7MBG44hnVJGmZGz5jRb/uo1tYaVyI1jgGvMoiIJuDXM/MjQ7XRzNwADHg8Q1JjGjO7neZ5\nc19zDsGY2e3FFSUNcwPOEGRmGfhiDWqRJACiqYlxK5YzsmU6e8afyRl33O4JhVKVDfZ/14aIeHtV\nK5GkA0RTE01jx9Jz5pk0z7nIMCBV2aBuTATMAv4xIp4Cdu1rzExDgiRJw8BgA8HVVa1CkiQVarC3\nLv57gIg4qfL+5SN/QpIk1ZNBHZSLiF+LiB8BLwLbIuLBiPi16pYmSZJqZbBn6fxP+p4/cAJwIvC/\nKm2SjnOd8xfQOX9B0WVIOs4NNhCMz8xb8t/dCoyvZmGSJKl2BhsIygfcapiIeDPQW52SJElSrQ32\nKoPrgQciYkPl/VuBK6pTkiRJqrUjBoKImJaZT2XmDyPiHOAdlUU/ysxt1S9PkiTVwkAzBCuBWRGx\nNjPnAD+oQU2SJKnGBgoEJ0TE7wJvjIj3HbowM/+2OmVJEoxfdTdPlEpMK7oQqQEMFAg+DfwnYCLw\nqUOWJWAgkCRpGDhiIMjMe4F7I+JrmfmHNapJkiTV2KAuOzQMSPUpy2XKO3bQ29FB99r7yXK56JIk\nHad8nqg0TGW5zPZFi+nZ8iS9z3bw4pUL2b5osaFAUr8MBNIwtXtdie7Vaw5q6169ht3rSsUUJOm4\nZiCQhqk9Gzf2275306YaVyKpHhgIpGFq9IwZ/baPam2tcSWS6oGBQBqmxsxup3ne3IPamufNZczs\n9mIKknRcMxBIw1Q0NTFuxXJGtkxnxJQpnHHH7YxbsZxo8r+9pNca7MONJNWhaGqiaexYGDuW5jkX\nFV2OpOOYuwqSJMlAIEmSDASSJAkDgSRJwkAgSZIwEEiSJAwEkiQJ70MgDXvjV91ddAmS6oAzBJIk\nyUAgSZIMBJIkCQOBJEnCQCBJkjAQSJIkDASSJAkDgSRJwkAgSZIwEEiSJAwEkiQJA4EkScJAIEmS\nMBBIkiQMBJIkCQOBJEkCRhZdgFQPOucvYFJXF7S3F12KJFWFMwSSJMlAIEmSCgwEETEiIh6LiB8U\nVYMkSepT5AzBJ4DNBW5fkiRVFBIIImIy8NvA8iK2L0mSDlbUVQZfB/4YOOVwHSJiCbAEYOLEiZRK\npdpUVgd27drleNTYpK4uent7HfcC+PNeDMe9GEWOe80DQUS8H9iamY9ERPvh+mXmMmAZQFtbW7Z7\nudd+pVIJx6O2Ov/yZrq6uhz3AvjzXgzHvRhFjnsRhwwuBD4QEU8DK4GLIuLOAuqQJEkVNQ8Emfnp\nzJycmVOBS4H7M/P3a12HNFhZLlPesYOR27bRvfZ+slwuuiRJGnLeh0A6giyX2b5oMT1bnmR05zZe\nvHIh2xctNhRIGnYKDQSZWcrM9xdZg3Qku9eV6F695qC27tVr2L2uVExBklQlzhBIR7Bn48Z+2/du\n2lTjSiSpugwE0hGMnjGj3/ZRra01rkSSqstAIB3BmNntNM+be1Bb87y5jJndXkxBklQlBgLpCKKp\niXErljOyZTp7xp/JGXfczrgVy4km/+tIGl6KulOhVDeiqYmmsWPpiaB5zkVFlyNJVeFujiRJMhBI\nkiQDgSRJwkAgSZIwEEiSJAwEkiQJA4EkScL7EEiDMn7V3TxRKjGt6EIkqUqcIZAkSQYCSZJkIFAN\ndc5fQOf8BUWXIUnqh4FAkiQZCCRJkoFAkiRhIJAkSRgIJEkSBgJJkoSBQJIkYSCQJEkYCCRJEgYC\nSZKEgUCSJGEgqDud8xcw6YtfKrqMo5blMuUdO+jt6KB77f1kuVx0SZKkAxgIVHVZLrN90WJ6tjxJ\n77MdvHjlQrYvWmwokKTjiIFAVbd7XYnu1WsOautevYbd60rFFCRJeg0Dgapuz8aN/bbv3bSpxpVI\nkg7HQKCqGz1jRr/to1pba1yJJOlwDASqujGz22meN/egtuZ5cxkzu72YgiRJr2EgUNVFUxPjVixn\nZMt0RkyZwhl33M64FcuJJn/8JOl4MbLoAtQYoqmJprFjYexYmudcVHQ5kqRDuIsmSZIMBJIkyUAg\nSZIwEEiSJAwEkiQJA4EkScJAIEmSaNBA0Dl/AZ3zFxRdxlHb9wjhkdu2+QhhSdKQashAUI8OfITw\n6M5tPkJYkjSkDAR1wkcIS5KqyVsX14kjPUK4Xm4FPH7V3UWXIEk6DGcI6oSPEJYkVZOBoE74CGFJ\nUjUZCOrEgY8Q3jP+TB8hLEkaUp5DUEf2PUK4J6JuzhuQJNWHmu9eRsSUiFgXET+JiCci4hO13P6+\na/l7Ozq8ll+SpIoi5pt7gD/KzLOBC4ClEXF2LTZ84LX8vc92eC2/JEkVNQ8Emfl8Zj5aef0SsBmY\nVIttey2/JEn9K/QcgoiYCpwHPNTPsiXAEoCJEydSKpWOeXtjv/99zuyn/cm/+Rt2jKiPk/MmdXXR\n29s7JOOho7Nr1y7HvQCOezEc92IUOe6FBYKIOBn4LnBNZu48dHlmLgOWAbS1tWV7e/sxb7O7t8yL\nq777mvbpF19M8xCsvxY6//Jmurq6GIrx0NEplUqOewEc92I47sUoctwL2S2OiFH0hYFvZ+Y9tdqu\n1/JLktS/Iq4yCGAFsDkzv1bTbR9wLf+IKVO8ll+SpIoi/hJeCFwBXBQRGyr/3lerje+7ln/E5Ek0\nz7nIMCBJEgWcQ5CZ/xeIWm9XkiQdnrvHkiTJQCBJkgwEkiQJH25Ud8avupsnSiWmFV2IJGlYcYZA\nkiQZCCRJUoMeMhi/6u6iS5Ak6bjiDIEkSTIQSJIkA4EkScJAIEmSMBBIkiQMBJIkCQOBJEnCQCBJ\nkjAQSJIkDASSJAkDgSRJwkAgSZIwEEiSJAwEkiQJA4EkScJAIEmSMBBIkiQMBJIkCQOBJEkCIjOL\nrmFAEdEJPFN0HceRM4FtRRfRgBz3YjjuxXDci1GNcX9jZo4fqFNdBAIdLCLWZ2Zb0XU0Gse9GI57\nMRz3YhQ57h4ykCRJBgJJkmQgqFfLii6gQTnuxXDci+G4F6OwcfccAkmS5AyBJEkyENSViJgSEesi\n4icR8UREfKLomhpFRIyIiMci4gdF19IoIuL0iFgVEVsiYnNEvLPomhpBRFxb+f2yKSK+ExHNRdc0\nXEXELRGxNSI2HdA2LiLWRMRTla9ja1WPgaC+9AB/lJlnAxcASyPi7IJrahSfADYXXUSDuQn4YWa2\nAG/F8a+6iJgEXA20ZWYrMAK4tNiqhrXbgPce0nYdsDYzpwFrK+9rwkBQRzLz+cx8tPL6Jfp+QU4q\ntqrhLyImA78NLC+6lkYREacBvwmsAMjMPZnZVWxVDWMkcEJEjAROBP6t4HqGrcz8B2D7Ic0fBG6v\nvL4duKRW9RgI6lRETAXOAx4qtpKG8HXgj4Fy0YU0kLOATuDWyqGa5RFxUtFFDXeZ+RxwI/Bz4Hng\nl5m5utiqGs7EzHy+8voXwMRabdhAUIci4mTgu8A1mbmz6HqGs4h4P7A1Mx8pupYGMxI4H/hmZp4H\nvEwNp04bVeV49QfpC2S/CpwUEb9fbFWNK/suA6zZpYAGgjoTEaPoCwPfzsx7iq6nAVwIfCAingZW\nAhdFxJ3FltQQOoCOzNw3A7aKvoCg6noP8LPM7MzMvcA9wLsKrqnRvBARbwCofN1aqw0bCOpIRAR9\nx1Q3Z+bXiq6nEWTmpzNzcmZOpe/kqvsz0z2mKsvMXwDPRsT0StMc4CcFltQofg5cEBEnVn7fzMGT\nOWvt+8DCyuuFwL212rCBoL5cCFxB317qhsq/9xVdlFQlHwe+HRGPAzOBGwquZ9irzMisAh4FNtL3\nN8I7FlZJRHwH+CdgekR0RMQi4MvA3Ih4ir4Zmy/XrB7vVChJkpwhkCRJBgJJkmQgkCRJGAgkSRIG\nAkmShIFAahgRkZW7XFZr/X8eEaMPeH9bRPyXQXxuakT0VC6jPbvS9uWI+HlErKpWvZIOZiCQNFQ+\nC4wesFf/ujJzZmb+BCAzrwM+M2SVSRqQgUBqQBExPSLui4iHI+LHEfHhA5ZlRFxfWfavEfG7Byz7\n3YjYUnng0PX7Zh0i4uZKlwcre/qnV963RsT9lWe731G5+52k45CBQGowlcfa3gVcm5lvA34duC4i\nWg7otrOy7ArgG5XPTaTvrnUXVx449Oq+zpm5tPLyXZU9/X2PKm4F3gecA8yi785rko5DBgKp8bwZ\neAuwMiI2AA8AYypt+6ysfP0R8KsR0Qy8A3g0M5+qLLtlENv6XmZ2Z+Ye+m6H+6ah+AYkDb2RRRcg\nqeYC2JaZM4/QpxsgM3srs/yv93dF9wGve49hPZKqzBkCqfE8CbwSEVfsa4iIlog4dYDPPQScHxH7\n9vIXHrL8JeC0oStTUi0ZCKQGk5k9wMXApRHxeEQ8AfwVA1whkJkvAB8D/jYiHgPGA3uBVypdvgrc\nf8hJhZLqhE87lDRoEXFKZr5Uef1hYFFm/voxrnMqsD4zzzyk/Srg/Zk5/1jWL2lwnCGQdDSurswA\nbAI+DHx0CNbZC+w59MZEwKeBHUOwfkmD4AyBJElyhkCSJBkIJEkSBgJJkoSBQJIkYSCQJEkYCCRJ\nEvD/AYkeTfuGh6wXAAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#initialize the dataset\n", - "xy2 = q.XYDataSet(xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [2., 2.3, 2.8, 3.8, 5.2, 5.9, 7.8, 7.7, 8.8, 10.1],\n", - " yerr = [0.4, 0.6, 0.5, 0.4, 0.4, 0.5, 0.5, 0.5, 0.6, 0.5],\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'xydata2')\n", - "\n", - "#create the plot object\n", - "fig2 = q.MakePlot(xy2)\n", - "#show the figure, it will have no fit:\n", - "fig2.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now use the fit() command from the Plot Object to perform the fit. Internally, this just calls the fit() method on the dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of xydata2 to degree_2_polynomial\n", - "Fit parameters:\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par0 = 1.0 +/- 0.5,\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par1 = 0.7 +/- 0.2,\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par2 = 0.02 +/- 0.02\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.9 0.792]\n", - " [-0.9 1. -0.969]\n", - " [ 0.792 -0.969 1. ]]\n", - "\n", - "chi2/ndof = 5.79/6\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "image/png": "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\nGImJiWHw4MEsX74c+O8CI4mJiXTv3h2AXr168fbbb5fa+1ZKVeyaBC6X65JqElit1suuSZCVlcXQ\noUP505/+5LtacaHyqElQnB9++IFnn32WxYsXF9km1PUKilOatQxCPj4gmJ/SrnaYMWKkZIwYecmv\nv1h79+6Vpk2b+u5XXa6LqXaoSo+OIQiNyjCGIC8vT5KTk6Vp06Z+lUFHjBghs2bNksTERFm5cqWI\nFD+GaPny5dK2bVtfxbtPP/1UevfuLSL+Ywj27dsnDRo0kL1794qIyI8//ljoHrfL5ZI77rhDxowZ\n46s4WJwxY8bISy+95DvWtGnT5LbbbhMRkdatW8tXX30lIt4xCREREb778XFxcbJly5ZC+8rJyZEb\nbrhBnn322WKPebFVC48dOyZ16tQRl8vle4916tQpVCGwwODBg2XmzJm+x4888oi89tprvscrV66U\n5s2bF1vx8EKBxhC43W5p1aqV72/8ww8/7HsPBZUpz5eQkCBr1671Pb7lllvkiy++EJHCv++vvfaa\nPPLII77HM2fOlMGDB/seHzt2TBISEuT9998vts8EOYYg5B/2wfxU5vLHzz77rMTGxspnn31WavvU\nQBAaGghCozIEgscee0wefPBB2bp1q7Ro0UIOHDjg25aSkiLNmzcvNBj42LFj0qBBA9/Au//93//1\nBYKvv/5aunfvLm63WxwOh/Tr188XCNavXy9t27b17Wfz5s3StGlTyc3NFbfbLXfffbfvA8vtdstv\nfvMbGTVqlO/DsyRjxoyRLl26SHZ2tixZskRuuukmXxnj+vXrS2pqqoh4yyUDRQYCu90uN954ozz5\n5JMXdyKD1Lt370KDCvv06ROw3QcffOA3qLAgPK1Zs0ZatGghq1atuqhjBwoEK1eulKFDhwa9j+ef\nf77QoMJGjRr5/q6f//u+Z88ev0GFH374oYh4y0p37txZ3nnnnRKPp4GgCtNAEBoaCEKjogeCr776\nSjp37ix2u11ERN5//3257rrrxOl0+tq0bdtW/v3vfxd63ezZs+WKK66QpKQkefHFF32BID8/X+64\n4w5p06aN9OzZU5544glfIHA6nXLLLbdIx44dZcSIESLiDSPx8fGSnJwszz33nO8Da968eQJIYmKi\n79vqww8/XOx7GTNmjIwbN06uvfZaiY2NlXHjxkleXp6IeGczxcfHS9euXeXPf/6zNGjQoMhA8M9/\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#fit the data to a second order polynomial:\n", - "fig2.fit(\"pol2\")\n", - "#it will now show the results of the fit\n", - "fig2.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding fits to the same dataset\n", - "Let's now fit the second data set to a linear model. As you can see, QExPy will only display the latest fit to the dataset, even if the dataset still is aware of both fits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of xydata2 to linear\n", - "Fit parameters:\n", - "xydata2_linear_fit1_fitpars_intercept = 0.5 +/- 0.3,\n", - "xydata2_linear_fit1_fitpars_slope = 0.93 +/- 0.05\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.88]\n", - " [-0.88 1. ]]\n", - "\n", - "chi2/ndof = 7.29/7\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "image/png": "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\nuFwufPHFF8jIyMCrr76KlJQUfPDBBxfzv9FLaWkptm3bhtmzZwMAZs+ejW3btuHo0aOXfOyGVFdX\nY9u2bbj66qsbbPv111/j/vvvBwAMGDAAqampWLVqVZ124eHhCA0NBQC43W44HI5L7+EVkWb/NXLk\nSDnX7t27xTCMOtvbgrKyskY/pmEYsnv37kY/bmvy9ddfB7uENilY1/2DDz6QUaNGyRdffCEDBw6U\n06dPi4jIL37xC3nuuedERKS8vFy6du0qJSUlcuTIEenUqZPs3btXRERee+01ASDl5eUiInL06FHP\nsZ966in5xS9+ISLm+zv337jabf/0pz/JrFmz6tRXVVUlQ4cOlfT09PO+j3nz5snbb78tIiLPPvus\n9O/fX06ePCmGYUhaWposXrzYs++//uu/PK9bunSpXHnllZ73vWvXLomLi/PUbLFYZMOGDZ728+fP\nl4ceekh0Xfd6Dy+++KK8+OKLnnb//d//LU8++aSntqSkJCkvLxe32y1paWmeWmvX7cv//M//SHJy\nss+vb7/9tk77LVu2yNChQ722DRkyRLZu3Vqn7bPPPis9evSQxMRESUtL83qf59OnTx+f21esWCF3\n3XWXX8cIDw+X0tJSz/c///nP5c033/TZtqioSJKTkyUqKkpuu+02qa6u9tkOwBbx43dti71lAIA9\nA42I15LI29y5c/HVV1/h5ptvxvr169G+fXsA5if60aNH46mnnsLSpUsxceJExMTEICMjAyNGjMCg\nQYMAAPfddx9+8YtfeI73wQcf4MMPP4TL5UJlZSUGDhxY77lXrVqFd955BxUVFdA0rc5+TdNw++23\nY/z48Zg+ffoFva9JkyahQ4cOAIBRo0YhJyfHZ7vNmzcjJycH1113ndd5S0pKAJifXq+66irPvhUr\nVmDr1q2eT6ldunQBAGRkZKCsrAyffvopAPPTcnJysud1s2bN8nSHz5s3D//61788vQvn88QTT+CJ\nJ57w+31fiIULF+Kpp55CSEgI1q5di5tuugl79uxB586d67RNTU31/D8qLi5GSkoKAKB3797IyMgA\nAKSnp+Omm25q9Dp79OiBrKwsVFZW4o477sBnn32G22+//aKP16IDQXNTewDRDTfcgLfffhv9+vUL\ndllEdBFcLhd++ukndOjQwfNLEADi4uKQmpqK9PR0vPPOO1i8eHGDx1q/fj3+8Ic/YMOGDejatSv+\n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\nAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Here we fit the xy2 dataset to a linear model, and then display it.\n", - "#The following 2 methods are equivalent:\n", - "xy2.fit(\"linear\")\n", - "#or:\n", - "#fig2.fit(\"linear\") #this is equivalent, since fig2 only has the 1 dataset\n", - "fig2.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "results from the first fit\n", - "-----------------Fit results-------------------\n", - "Fit of xydata2 to degree_2_polynomial\n", - "Fit parameters:\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par0 = 1.0 +/- 0.5,\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par1 = 0.7 +/- 0.2,\n", - "xydata2_degree_2_polynomial_fit0_fitpars_par2 = 0.02 +/- 0.02\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.9 0.792]\n", - " [-0.9 1. -0.969]\n", - " [ 0.792 -0.969 1. ]]\n", - "\n", - "chi2/ndof = 5.79/6\n", - "---------------End fit results----------------\n", - "\n", - "results from the second fit\n", - "-----------------Fit results-------------------\n", - "Fit of xydata2 to linear\n", - "Fit parameters:\n", - "xydata2_linear_fit1_fitpars_intercept = 0.5 +/- 0.3,\n", - "xydata2_linear_fit1_fitpars_slope = 0.93 +/- 0.05\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.88]\n", - " [-0.88 1. ]]\n", - "\n", - "chi2/ndof = 7.29/7\n", - "---------------End fit results----------------\n", - "\n" - ] - } - ], - "source": [ - "#however, the dataset knows of both fits:\n", - "print(\"results from the first fit\")\n", - "xy2.print_fit_results(fitindex=0) # results of the first fit\n", - "\n", - "print(\"results from the second fit\")\n", - "xy2.print_fit_results(fitindex=1) # results of the second fit\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Displaying multiple data sets that each have fits\n", - "\n", - "There is no inherent difference in showing multiple datasets in the case where they have fits. If we want to show dataset xy2 with its latest fit on fig1 along with dataset xy and its latest fit, we just add xy2 to fig1:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\nfBkAsGnTJjF9cXExbG1t4eDggOrqamzevFk8ZmNjozOWp7S0FKampnBxcYFGo8Enn3zSatdU/1wj\nR47E/v37ce7cOfG1lJSURt8/btw4vP/++yAiAEBBQQEAYMKECfjwww9RVVUFQDsIuu6sqG+//RYV\nFRVQqVTYunUrhg8frrc89dV2J+h7DB06tEH62bNni8enTZsGZ2dnSKVSbN++HQCwfft29OvXD05O\nza7o+0CSkpIQHBwMS0tLANpuodpy1bYg1RUVFYWNGzcCAC5fvoyUlBSMHj26QborV66guroagDZg\n3bNnD4KDgx+orBwQtBFPT0+kp6cD0P6CX3vtNQwZMgR9+vTRafbKycnBpEmTMGDAAAQHB4sDkoDG\nBxNdu3YNjo6OePXVVxEWFqbzx4Yx1rp2794NmUyGjz76CIGBgVi5ciWmTp0qBuKRkZEwNzfHwoUL\nxfc4OzsjPj4e48ePR79+/XS+1Y8ePRpeXl7w9fVFZGQkwsLCxGMhISHw8/NDUFAQJk2ahODgYDzz\nzDMICAjAwIED0bt371a7rqeeegopKSnioEIfHx9s27YNc+bMQWhoKOLi4sQbkz7/+te/UFZWhqCg\nIISGhoqzG5YuXYrQ0FD0798fISEhGDJkiE5A0L9/f4wcORL+/v7w8PAQ+71nzJiBr776qs0GFQLA\nJ598gnXr1sHX1xfr1q3TCbCeeOIJcYn833//He7u7vjwww+xceNGuLu746effrqvc+7evfueugti\nY2NRXFwMb29vjBs3DvHx8eKsshUrVohlPnr0KCIiIhAaGoqwsDB0794dy5cvv68y1hJqo7vWJgjC\nZgDjAOQTUVC9Y38DsBaAExEVNJdXREQE1d/L4MKFC/D39xefF69YBeX5c/Xf2ipMAgJht3pVs+nq\nzjP29PTEDz/8gKCgIERFRaFHjx7Yvn07ysrK4OXlhWPHjsHHxwcjRozA8uXLMWzYMCgUCsTExGDF\nihUYMWIECgoKxH65TZs24eDBg9ixYwfS09MRHByMHTt2NDvX+F7Ur1Omi9d2N4yOXu9Xr17FY489\nhszMTPFb4MOgLep91qxZiIiIaNGYgYdFQEAAZDJZi6d0t9FeBieJKKK5dG05qPALAP8FoBPqCYLg\nAWAkgOtteO4O55lnnoFEIoGtrS38/f2RlZUFV1dXyGQynWkvtc1rI0aMaHIwkbm5uc4IYMZY+1ux\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig1.add_dataset(xy2)\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Showing fit residuals\n", - "By using the add_residual() method of the Plot Object, we can ask to have residual added to the plot. add_residuals() can be called at any time after initializing the Plot Object, and it will only actually add residuals if there are datasets in the Plot Object which have fits associated with them. To illustrate this, we add a third dataset to fig1, but without fitting it. As you can see, the residuals are only drawn for the first 2 data sets.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\njRpl3N93331HRkYGYWFhhcolJycbs9D379/Pf/7zHyPdY/v27fnyyy/RWmOz2di8ebMxobWsLly4\nQFZWljFJZs6cOcY99O/fv8j6/+tf/8Jms5GUlMS6deuKnHU9ZMgQYmNjjX8bS5cuNRJ9bNq0iTNn\nzpCQkMB3331H48aNSUhIMFq2y0OCRiGEEKKSVVee6pEjR5Z56bbc3FwmTZpEUFAQISEhXLx4kSef\nfLJQuaioKFauXElwcDBjx45lxYoVNG/e3NhvtVrp2rUrw4YN491338XT05PGjRuzYcMGXn75ZSOP\n9fz584sMGu/EokWLiImJwcfHh6effpoVK1ZgMtlDnSlTprBhwwYATp48SUhICAEBAUyaNImoqCha\nt24NwLx587h+/TqBgYEEBQWRlZXFnDlz7qg+69evZ/jw4aUuP378eDp06ICPjw8zZsxg7ty5xvNa\nunQpc+fOBexBe2RkJMOGDaNTp07cuHGjyO7zilbrck+LOye5p2u/2pizVhQkz/juVJY81XfDM1ZK\nkZKSYiw/V1cNGTKE1157rcjWzpLUxNzT0tJYCZRSpKamAvDggw9y4sSJaq6REEKImmru3Ln06dNH\n8lTXQps3b76jgLGmktnTleyLL76osmtZrVYcHeWRCiHE3aQ25qm+m3oxRenVugjjb9FHOHapfOl+\niuPb0o0Xhpat29bb25tNmzYRGBhIv3796NatGz/88AMXLlzgscceY9GiRYB9av+zzz7LmTNnyMjI\nYMyYMbz00kuAfVHW//73v2RnZ9OsWTOWLVtGu3btSEhIwGKxMGnSJL755humTZtmLGoqhBBC3I7k\nqRZlJd3TVezMmTN8++237N27l/fee8+Y+TRhwgSee+45fvrpJ3bv3k10dDRbt24Fbr8o67Vr1+jW\nrRt79uyRgFEIIYQQlabWtTSWtSWwqo0aNQqTyYSHhwedO3fmxIkTtG7dmpiYmAIr6aekpHDkyBEG\nDRp020VZXVxcjGn2QgghhBCVpdYFjTWdi4uL8bODgwNWqxWbzYZSitjY2EIpf/IWZY2NjaV9+/bs\n2LGjQDqjBg0aoJSqsvoLIYQQom6S7ukawM3NjT59+hjjGwHOnj3LpUuXyrwoqxBCCCFEZaj0oFEp\ntUwplaiUOljEvj8qpbRSqllRx9YlUVFRHD58mKCgIIKCgggPDycpKanMi7IKIYQQQlSGquie/hD4\nB7A8/0allBcwGDhTBXWoUvmXGsifLDwmJqZAufzvW7ZsyerVq4s839tvv83bb79tvH/55ZcB+8zs\n/Dk2hRBCCCEqS6W3NGqtvwV+KWLX34D/BWQxJyGEEEKIGq5axjQqpR4Bzmut91XH9YUQQgghRNlU\n+exppZQr8BL2runSlJ8GTANo0aJFoS5eDw8PUlIqZzHvuiY3N5eUlBQyMzMLfc6idkhNTZVnW8vJ\nM6795BnXDTXxOVfHkjsdgfbAvptLxbQB9iilumutL91aWGsdCUQCWCwWfWvy7iNHjuDm5lbZda4T\nUlJScHNzw8XFha5du1Z3dUQliImJ4db/h0TtIs+49pNnXDfUxOdc5UGj1voA4Jn3XimVAFi01lU+\no+PKyFEANF/7SVVfWgghhBDirlIVS+6sBn4A/JRS55RST1T2NYUQQgghRMWqitnTY7TWrbTWTlrr\nNlrr92/Z710drYw1mVKK1NTU25ZJSEggMjKyimokhBCiIlX09/yxY8fo378//v7+BAYGMnnyZDIy\nMm57zNy5c/n4449LPHdMTAxbtmwpVT2qwrp16/jpp58q9JzHjh2jV69e+Pr60qtXL44fP15kufnz\n5+Pp6YnZbMZsNjNjxoxyX3vlypXMnDmzxHLp6emEh4fTqVMn/P392bRpU5Hl4uLiCA0NxWw206VL\nF6ZNm0ZWVla56wl1OCOMttmwXb9O7rlzZH79Ddpmq+4qlYkEjUIIUbuV5Xve2dmZN998k6NHj7J/\n/37S09NZvHjxbY9ZsGAB4eHhJZ67PEFjbm7uHR13O5URNE6fPp0ZM2Zw7NgxZsyYwZNPPlls2QkT\nJhAXF0dcXBwRERGlOr+3t3ex+9atW8eIESNKPMfixYtxd3cnPj6ejRs3MmXKlCL/8PDz8+PHH38k\nLi6OAwcOcO3aNd59991S1bMkdTJo1DYbvzwxBevRn8k9e45rEybyyxNTKiRwPHr0KF5eXpw+fRqw\nL8Q9evRoWrVqxcWLF41yzz33HAsXLgTgs88+w9/fH7PZzCuvvFLgfGPHjsVisRAUFMSjjz7K9evX\nAZgxYwaHDx/GbDYzcuRIAGbOnEm3bt0ICQlh4MCBRh2EEEJUjOK+4zMzM6vse3769OmFvue9vb2N\nCYwmk4nu3buX+Dtg0qRJ/OMf/wDsLWhjxozhwQcfxN/fn4ceeoj09HQOHDjA0qVLWb58OWaz2Uh3\n+8UXX9C7d2/CwsLo1asXP/74I2APMIODg5k8eTJms5no6Ghu3LjB73//e4KCgggJCeGZZ54BIDs7\nm1mzZtG9e3dCQkIYP368EQRNmjSJqVOncu+99+Lr68vUqVPJzs7myy+/ZMOGDSxatAiz2czy5cuL\nuLOySUxMZM+ePYwZMwaAMWPGsGfPHq5cuVLuc5ckKyuLPXv2cO+995ZY9uOPPzaCWR8fHywWC9HR\n0YXK1a9fH2dnZwBycnLIyMjAZKqgcE9rfde8wsLC9K0OHz5caFtREn830nhdGjBQn2vdptDr0oCB\nOvF3I0t1vttZvny57tGjh/7yyy+1r6+vvnHjhn7xxRf1/PnztdZap6Sk6ObNm+vLly/rS5cu6SZN\nmuijR49qrbV+/fXXNaBTUlK01lpfuXLFOO+cOXP0iy++qLXWetu2bfrWzyN/2X/96186PDy8TPVO\nTk7WWpf+MxV3n23btlV3FUQlk2dc+Yr6jtdaV9n3fN4zLu57Pj09XQcEBOj169ff9j4mTpyolyxZ\norXWet68ebpTp076+vXr2maz6UGDBunIyEhj3x//+EfjuPj4eN2zZ0/jvg8ePKi9vLyMOptMJr1j\nxw6j/KRJk/Qzzzyjc3NzC9zvK6+8ol955RWj3P/+7//ql156yahbUFCQTklJ0Tk5OXrQoEFGXfPX\nuyh/+ctfdEhISJGvb7/9tlD5Xbt26YCAgALbOnfurHfv3l2o7Lx583Tr1q11YGCgHjRoUIH7vJ12\n7doVuX3Tpk168uTJRe679f/lhg0b6sTEROP9U089pd94440ijz1//rwOCQnRDRs21I899pjOysq6\nbf2AXboUcVh1LLlT7XRaWjHb06Fx43Kff/z48Xz99deMGDGC7du34+7uzowZM+jTpw9z5sxh5cqV\nDB48GE9PTzZs2EBoaCh+fn4ATJs2jRdffNE41/Lly4mKiiI7O5u0tDR8fX2LvW50dDQRERGkpqZi\ntVrLfR9CCCEKK+o7Hqiy7/m//OUvmEymIr/nrVYro0ePZsCAAQwfPrxM9/XAAw/QqFEjAHr06MGJ\nEyeKLPfll19y4sQJ7r///gLXvXz5MmBvBevVq5exb9OmTezevdto7WrWrBkAGzZsIDk5mbVr1wL2\nVreQkBDjuPDwcBo2bAjAxIkT+fTTT41WytuZPXs2s2fPLvV9l8X06dOZM2cOTk5ObN26lUceeYQj\nR47QtGnTQmUtFovxjC5cuIDZbAagbdu2bNiwAYD169fzyCOPVHg9W7duTVxcHGlpaYwbN47PPvuM\n0aNHl/u7Kj23AAAgAElEQVS8dSZozL+sTubX33BtwsRCZRq99iouAweU+1rZ2dkcOnSIRo0aGf8T\neXl5YbFYWL9+PREREaUap7J9+3beeecdduzYQfPmzVm1alWxx50+fZoXXniB2NhY2rdvz44dO3j8\n8cfLfS9CCCEKKuo7Hqrue37JkiWMGTOm0Pd8bm4uY8eOpXHjxvz9738v8325uLgYPzs4OBQ7kUZr\nzZAhQ4rsGj5y5IgR6JVEa80///lPBgwo/+/d/BYtWsSaNWuK3LdkyRL69OlTYJuXlxfnz58nNzcX\nBwcHcnNzuXDhAl5eXoWOb9mypfHzoEGD8PLy4uDBg/Tt27dQ2V27dhk/e3t7ExcXV2C/zWbjq6++\n4q233gLsf3R8//33AEVOUGrbti2nT5+mefPmAJw5c4b+/fsXeZ95GjRoQHh4OFFRURUSNNbJMY31\n+vfDZfCgAttcBg+iXv9+FXL+WbNmERYWxtatW5k+fTrnzp0D4Nlnn+X555/HycnJ+CusZ8+e7N27\n15ip9d577xnnSUpKwsPDg6ZNm5KVlcWyZcuMfe7u7ty4ccN4n5ycjLOzMy1btsRms7F06dIKuRch\nhBAFFfcdD1XzPd+kSZNC3/M2m41Jkybh4ODA+++/z83kGRXi1noMHjyYzZs3c+jQIWNbbGxssccP\nGzaMv/71r9h7QeHqVfuCKcOHD+fNN980gtOUlBSOHDliHPfJJ5+QlpaG1WplxYoVRnB5a31uNXv2\nbGOiyq2vWwNGwJgNvXr1agBWr15N165djeAsv/Pnzxs/x8XFkZCQYLQgl9XOnTsJCgrC1dUVgIiI\nCKOeRZ1z1KhRxoSW48ePExsby5AhQwqVO3nypDFbOjs7m/Xr1xMUFHRHdbxVnQwalclEk/ffw9Hf\nDwcvL5ou/4gm77+HqoCBouvWrSMmJoa33nqLLl26MG/ePMaMGYPVaqVv3764uLjw9NNPG+U9PT2J\njIzk4YcfpmvXrmRmZhr7hgwZQseOHfH19aVv376EhoYa+4KDg/Hz8yMwMJCRI0cSFBTEqFGjCAgI\noEePHrRv377c9yKEEKKg233HA1XyPT9p0qRC3/PR0dGsXLmSAwcOEBYWVmHLwQA8+uijxMbGGhNh\nfHx8WLlyJU888QQhISF07tz5trNz//a3v5GSkkJgYCAhISEsWLAAsAd3ISEhdOvWjeDgYO67774C\nQWO3bt0YPHgwnTt3xsvLi2nTpgH24QGrVq2qsIkwAEuXLmXJkiX4+vqyZMmSAgH5gw8+aLQavvTS\nS8Z9TJ06lRUrVhRofSyLdevWlalretasWSQlJdGpUyeGDRtGZGSkkRFv7ty5Rp137NiBxWIhJCSE\n0NBQmjRpwp///Oc7quOtVF7kfzewWCw6f3Mv2JvCO3fufEfnq+qMMKdOnaJ3797Ex8cbf1nUJHlp\nBMvzmYqarSampRIVS55x9aqK7/m68IwnTZqExWIp1RjGu1VAQAAxMTF4enoWub8qn7NSarfW2lJS\nuTozprEoVZk+cO7cuSxbtow33nijRgaMQgghyke+50VZHD58uLqrUGZ1OmisSgsWLDCa5IUQQtQ+\nNfF7Pi4ujkmTJhXa/swzzzBlypSqr1Apffjhh9VdBVEECRqFEEKIWspsNheatSvEnaqTE2GEEEII\nIUTZSNAohBBC1CBKqSJzCudXlrzUx44do3///vj7+xMYGMjkyZOLXYOxJN7e3hw8ePCOjq0Kly9f\nZvDgwfj6+hISEsLOnTuLLJeamsqECRMICgrC39+/QJ7uzZs3ExISgtlspkuXLsyZM4fyThr+7rvv\njFSQt5Obm8uMGTPo2LEjY8eOLbA80602btyIv78/nTp1Ijw8nPT0dMD+b8PR0RGz2Wy8rl27Vq76\n56nTQeNTH/zEUx9UbNJzIYQQorKVJWh0dnbmzTff5OjRo+zfv5/09PQCQVJt8qc//Yn777+fY8eO\nERERwbhx44oM+BYuXIizszP79+9n9+7drFixwsiffd9997Fnzx5jzcStW7eycePGEq/t7e1d7L51\n69YxYsSIEs8RFRVFfHw8x48fJyIigvnz55OQkFCoXGpqKlOnTmXjxo3Ex8fj5uZW4Jk2atSowPqU\nRWWsuRN1OmgUQgghKsrRo0fx8vLi9OnTALz88suMHj2azMxMWrVqxcWLF42yzz33HAsXLgTgs88+\nw9/fH7PZzCuvvFLgnGPHjsVisRAUFMSjjz7K9evXAXv2kMOHD2M2m40WrJkzZ9KtWzdCQkIYOHCg\nUQ9vb2+6du0KgMlkonv37sa+4kRGRtK5c2fMZjPBwcEcPXq0UJn4+HgGDhxIcHAwoaGhbN682din\nlGLevHmYzWb8/Pz49NNPjX07d+6kf//+hIWFERYWxueff166D7gU/v3vfzN9+nTAHvzVq1ePW5fq\nA9i3bx8PPPAASikaNGhA3759iYqKAqBhw4Y4ODgAkJmZSXZ2tpEC8U59/vnnPPTQQyWW+/jjj5k6\ndSomk4lGjRoxYsQIPvmk8Eov0dHRWCwWfHx8AHt6w6KyyFS0Ohs02myaG+k5XErKYMexK9hsNWe9\nyprcNSGEEKJo/v7+LFy4kPDwcLZs2WKkBHRxcWHixInGd3Zqaipr1qxhypQpXL58malTp7J+/Xri\n4uKoV69egXO+/fbb7Nq1iwMHDtClSxdef/11wJ49JCAggLi4OCN38+zZs4mNjWXfvn2MGTOmQH7r\nPBkZGSxbtqzEvNSzZs3im2++IS4ujtjYWNq2bVuozNixY3n88cfZv38/K1euZNy4cVy5csXY7+Dg\nQFxcHBs2bGDatGkkJiaSlJTE9OnTWbVqFbt372bTpk08+eSTJCUlFTr/8uXLC3Sx5n8VFSBdu3YN\nrbWR2xrsqffOnj1bqGxYWBhr164lJyeHq1ev8uWXXxYIpHft2kVwcDCenp4MGDCgVAFfcQ4dOkSr\nVq1o3LhxiWXPnDlDu3btSqx/SeWSk5MJDQ0lLCysQDae8qqTQaPNpnlxzV5OJqZyMSmTP0Tt4cU1\ne2tU4FgS6ZoQQoiaZ/z48fj7+zNixAhWrVqFu7s7YG8Z/OCDD7BaraxcuZLBgwfj6enJzp07CQ0N\nNdLG5WU9ybN8+XLCwsIICgpi1apVt50JHR0dTc+ePQkMDGTx4sWFylqtVkaPHs2AAQNKDBoHDBjA\nxIkTWbJkCefPny+07mRKSgpxcXFMnjwZsC9UbTabjS5egCeeeAIAPz8/QkND+fHHH9mxYwenTp1i\n6NChmM1mhg4dilKK+Pj4QnWYMGFCsekAw8PDb1v/ksyePZtmzZphsVgYPXo0/fr1w9Hx1wVlLBYL\n+/fv5+zZs+zevZvt27cXeZ7hw4cbgeyFCxeMny2WX9fJXr9+fZkyv5RXq1atOHfuHHv27CE6OppP\nP/2U999/v0LOXWeCxrzxi0998BPj3tnB9p+vFNi//ecrjHtnR7nHOBbXPXG3dU0IIYQou+zsbA4d\nOkSjRo24fPmysd3LywuLxcL69euJiIgoVYq/7du3884777B582YOHDjAq6++WiAFYX6nT5/mhRde\nYPXq1Rw8eJBly5YVKJubm8vYsWNp3Lgxf//730u89meffcarr75KWloa/fv3Jzo6uhR3XzKtNcHB\nwQUCwLNnzxYIsvKUtaUxb9xeXm5rsLfIeXl5FSrr6upKREQE+/bt46uvvsLBwYGAgIBC5Zo1a8bQ\noUOL7CIG2LBhg3EfrVu3Nn7O3yWeP2h87bXXjHvYtm1bofO1bdu2wO/n4up/u3L16tUzssx4enoy\nduxYvv/++yLrX1Z1JmjMLyPbWqbtZVFc98Td1jUhhBCi7GbNmkVYWBhbt25l+vTpnDt3ztj37LPP\n8vzzz+Pk5ESvXr0A6NmzJ3v37uX48eMABWbLJiUl4eHhQdOmTcnKymLZsmXGPnd3d27cuGG8T05O\nxtnZmZYtW2Kz2QrkTrbZbEyaNAkHBwfef/99lFK3vQer1crJkyfp3r07s2fPZvDgwezdu7dAGTc3\nN8xmMx999BFgT+m7b98+evbsaZT54IMPADh+/Dh79+6lZ8+e3HvvvRw/frxAwBQbG1tk9+mdtDSO\nGjXKuPfvvvuOjIwMwsLCCpVLTk42hmnt37+f//znP0a+8GPHjmGz2QBIS0sjOjqaoKCg235mxblw\n4QJZWVnGJJk5c+YY99C/f/8i6/+vf/0Lm81GUlIS69atK3LW9ZAhQ4iNjTX+3SxdupTHHnsMgMTE\nRHJycgBIT09nw4YNmM3mO6r/rerM4t7vTO5u/Lzj2BX+ELWnUJlZDwVwr2/zcl9r/PjxfP3114wY\nMYLt27fj7u7OjBkz6NOnD3PmzCnQNbFhw4ZCXRP5g73ly5cTFRVFdnY2aWlp+Pr6Fnvd6OhoIiIi\nSE1NxWotHACXpWtCCCFE2axbt46YmBh27tyJi4sL8+bNY8yYMWzbtg1HR0f69u2Li4uLEZyAvSUo\nMjKShx9+mPr16/O73/3O2DdkyBBWrlyJr68vzZo14/777+enn+y9YcHBwfj5+REYGIi/vz9r165l\n1KhRBAQE0KxZMx588EG+/fZbwP67YeXKlQQGBhoBVO/evYmIiCjyPnJzc5k0aRJJSUmYTCa8vLxY\ntGhRoXJRUVE8+eST/O1vf8PR0ZEVK1bQvPmvv0OtVitdu3YlPT2dd99912j92rBhA7NmzeL5558n\nOzubDh06sHHjxhKD2dJYtGgR48aN46OPPqJ+/fqsWLHCmMQyZcoUhg8fzvDhwzl58iSPPfYYjo6O\nuLi4EBUVRevWrQF7y+CHH36Io6Mjubm5PProo3ecPWf9+vVl+n07fvx4du7ciY+PD5mZmcybN4/2\n7dsD9sDwwoULLFiwADc3NyIjIxk2bBi5ubl07dqVt99+G7AHy3PnzsXBwYGcnByGDRtWcTm8tdZ3\nzSssLEzf6vDhw4W2lSQ316ZnRu3WPeZuNl4zo3br3Fxbmc9VlKysLG2xWHSrVq30559/bmz/3e9+\np9euXasDAwP1jh07tNZar1+/Xv/mN78xyly/fl0DOiUlRX/77be6U6dOOjExUWutdVRUlO7bt6/W\nWutt27bp/J9HQkKCbtq0qT558qTWWuvvv/9et2vXzthvtVr1Y489pidOnKhttqLvMzk5WWt9Z5+p\nuDts27atuqsgKpk845rr5MmTulWrVjotLa1c57kbnnHe77G67oEHHtC7du26o2Or8jkDu3Qp4rA6\n2T1tMileH92VDp4NadXIhTfHhvL66K6YTOX/KweK7564W7omhBBCVKy5c+fSp08f3njjjUKTSkTt\ntXnz5iK7x+9WlR40KqWWKaUSlVIH8237q1LqqFJqv1LqP0qpRpVdj1uZTAoPVydaNqrPvb7NKyxg\nzOueeOutt+jSpYvRPWG1WkvsmujatWuBgctDhgyhY8eO+Pr60rdvX0JDQ419+bsmRo4cSVBQkNE1\n0aNHD6M5G37tmjhw4ABhYWGYzeZSDcIWQghRMRYsWMC5c+cYM2ZMdVfFEBcXV+Qkk9tlISktrTUN\nGzasgFqKmkTpClq7p9gLKHU/kAos11oH3tw2GPhGa21VSr0OoLUuPGvjFhaLRd+6SOeRI0fo3Lnz\nHdUtb6Z0/vGOlenUqVP07t2b+Pj4GvmXZkpKCm5ubuX6TEXNFhMTQ79+/aq7GqISyTOu/eQZ1w1V\n+ZyVUru11oWnsN+i0ifCaK2/VUp537JtS763PwIlJ2SsBFUVLIK9a2LZsmXSNSGEEEKIu1JNGNP4\ne6BiFoCqwWpi14QQQgghRGlVevc0wM2Wxk153dP5ts8BLMBvdTEVUUpNA6YBtGjRImzNmjUF9nt4\neNCpU6dKqHXdk5ubi4ODA/Hx8QUm2YjaIzU1VcYZ1XLyjGs/ecZ1Q1U+5/79+9eM7uniKKUmAcOA\ngcUFjABa60ggEuxjGm/t3z9y5Ahubm6VV9E6JG9Mo4uLi5FBRtQuMhaq9pNnXPvJM64bauJzrpag\n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\nIwAAACL9P5YDTBP+46N+AAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#A new dataset\n", - "xy3 = q.XYDataSet( xdata = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " ydata = [0.9, 1.4, 2.5, 4.2, 5.7, 6., 7.3, 7.1, 8.9, 10.8],\n", - " yerr = 0.5,\n", - " xname = 'length', xunits='m',\n", - " yname = 'force', yunits='N',\n", - " data_name = 'xydata3')\n", - "\n", - "fig1.add_dataset(xy3)\n", - "fig1.add_residuals()\n", - "fig1.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fitting to other buit-in functions\n", - "\n", - "QExPy has built in support for fitting to: linear, polynomials (currently up to 9th order), exponential, and gaussian. For example, to fit a dataset to a second order polynomial, we call fit(\"pol2\"): \n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of dataset0 to degree_2_polynomial\n", - "Fit parameters:\n", - "dataset0_degree_2_polynomial_fit0_fitpars_par0 = -0.1 +/- 0.4,\n", - "dataset0_degree_2_polynomial_fit0_fitpars_par1 = -0.1 +/- 0.2,\n", - "dataset0_degree_2_polynomial_fit0_fitpars_par2 = 1.02 +/- 0.03\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.84 0.706]\n", - " [-0.84 1. -0.965]\n", - " [ 0.706 -0.965 1. ]]\n", - "\n", - "chi2/ndof = 5.93/16\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "image/png": "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\nLXehxCsiF5ewsDB27drl7AWaOXOm89zx48fx8/OjatWqpKamukw6qVSpkst46vj4eMqWLUvNmjVx\nOBxMnz7deW737t1UqlSJXr16MXHiRDZu3IjD4aBbt25MnTrVOU4yNTXVOVHi9PpbtGhBcnIyq1ev\nBmD69On07Nkz3/f3ySefkJSUREZGBnPnzqV9+/bO91anTh0AZs+eTWpq6hnruPXWW4mOjna20fTp\n07nnnnvyLBsREUFMTEyeH6cnjAAPPPCA83yfPn24/PLLCQ4O5sMPPwTgww8/JCQkxJmcucv69esJ\nDAx0zgifMmWKM66cXuhT9ezZk3feeQeAXbt2ER0dza233pqr3NatW9mzZw979uxh4cKFBAYGFknC\nCEoaS52iHktXqVIlPvjgA7Zv387mzZtZu3Yt8+bNK9J7XGi++uor6tWrV9JhiMgl6vLLL2fkyJF0\n7dqVkJAQl962W2+9lXr16tGwYUPatm3rfIwM0KxZMxo1akRAQAA9evQgMDCQnj170qRJE6677jpn\nQgZZM6GbN29OcHAwt912G9OnT8fDw4P777+fPn360LZtW5o1a0aLFi2cvVt33nkn0dHRzokwHh4e\nzJ07l4ceeogGDRqwcuVKIiMj831/LVu2pFOnTlx77bVcddVVztneb7/9Nt27d6d58+b88ccfVK1a\n9Yx1VKxYkRkzZtClSxfq16/PiRMnGDVqVKHbuqCmT5/O5MmTadiwIZMnT3ZJwDt37uzsNVy9ejW1\na9dm4sSJvPPOO9SuXZtvvvnmnO65ePHiQj2aHj16NMePH6d+/fp06dKFGTNmOFc1GTNmjEvM7mLc\n1avkDqGhofb0tYZ27NjBtdde63x9fMxLpG//+bzvlZmRiaeXp8uxMk2aUnnsS/le+9lnn/Hss8/i\n4+PD3XffzZgxY0hISODnn38mIiKC+Ph4AMaOHcvtt98OwH/+8x8mTZpE5cqV6dy5M1OmTOHw4cPs\n2bOH0NBQBgwYwHfffceQIUMYOHAgzz33HCtXriQ1NZVmzZoxbdo0KlSoQHx8PE8++SRbt24lJSWF\ndu3aMXHiROcA3/w88sgj1KhRg+eff97l+KlL7oSHhxMcHMyaNWs4evQo99xzD6+//joAv/32G0OH\nDuXQoUN4eXnx+uuvO/8SylmHbOnSpbz//vv897//BbL+0vX392f9+vV89913LFiwgCpVqrBt2zYq\nV67Mp59+Ss2aNcnMzOTpp5/m66+/BrL+cR03bhyenp4MGDAAb29vdu3axe+//85dd91F165defHF\nF/nrr7944okneOyxx4CsxwRffvklAQEBTJgwgYULF5KRkYGPjw/Tpk1zjjs527ppO3bs4ODBg9or\n9RTaO9aV2sOV2sPVxdoeAwYMIDQ0lBEjRhTquou1Pc6mSZMmREVFnXHdy2Lee3qjtTY0v3LqaSxi\nBw8eZPDgwblmxR0/fpxhw4axYMECNm7cyJdffsnQoUM5fvw4W7du5Y033mDNmjVER0dz/PhxlzqP\nHDlCy5Yt2bRpE8OGDePNN9/Ez8+PDRs2sGXLFq688kreeOMNAJ588knatm3Lhg0biImJIS4ursBr\nasXFxfHpp586E9mz2b59O2vWrCEmJoYvvviCL7/8EshaKuK+++5j69atzJs3j759++ZatuDOO+9k\n27Zt7N69G4CPP/6YsLAwrr76agCio6MZP348P//8M02aNGHy5MkAzJgxg5iYGDZt2sSmTZvYvHmz\ny1ifn3/+maVLl7Jjxw7mz5/PvHnzWLlyJT/++CPPPfdcno+a+/XrR3R0NJs3b+aVV15h2LBhBWor\nERGR87F9+/ZiWyi9qFx0E2EK0hNYEOe6mPX69etp3rx5rllxmzZtYvfu3dx2223OssYYfvvtN9as\nWUPnzp2d4ycGDhzI/PnzneV8fHxcxnJ8/vnnxMfHO5dcSE1NJSgoyHluw4YNTJgwAchaQb4gu5ck\nJCTQrVs3Ro4cSUhISL7l+/fvj5eXFxUqVKBXr1589913tG3blpiYGB544AEg66+o4OBg1q1bR9eu\nXZ3Xenl5MXToUKZPn864ceOYMmUKr776qvP89ddf7xyAHBYWxvLly4GspQkGDBhA2bJlgaxxKf/3\nf//HQw89BED37t2dSXqjRo3o3LkzHh4e1KpViypVqvD333/TuHFjl/exceNGXn/9dY4ePYqHhwc7\nd+7M972LiFzKYmJiGDBgQK7jI0aM4P333y/2eKT4XHRJ44XKWkuzZs344Ycfcp1bs2bNWa8tX768\nyzIv1lqmTp3qHFx8+n0WL15M3bp1CxzbyZMn6dKlC506dWLkyJEFvu58DBkyhJCQELp168bx48fp\n0KGD85yPj4/za09PzwKP4zz9uvzqSUtLo0ePHvzwww80b96cf/75h1q1ap3rWxIRuSQEBwfnmgks\n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\npfY4hdrDldrDVWlrjwEDBhAaGsqIESPcUn9pa4+CatKkCVFRUee0TW9xtokxZqO1NjS/cqV+TGOZ\nMmVITk52dv2KuFt6ejpeXqX+V0dERNxs+/btJR1CkSr1//Ndfvnl7Nu3j1q1alGuXLki73EUOZXD\n4eDgwYP4+fmxb9++kg5HRKTIxMTEMGDAgFzHR4wYwfvvv1/s8ciFp9QnjZUqVQKyprqnp6cXWb0p\nKSn4+PgUWX2lndrjf8qXL0+1atVKOgwRkSIVHBx81p3KREp90ghZiWNO8lhUoqKiCAkJKdI6SzO1\nh4iIyKVNs6dFREREJF9KGkVEREQkX0oaRURERCRfShpFREREJF9KGkVEREQkX0oaRURERCRfShpF\nREREJF9KGkVEREQkX0oaRURERCRfShpFREREJF9KGkVEREQkX0oaRURERCRfShpFREREJF9KGkVE\nREQkX0oaRURERCRfShpFREREJF9KGkVEREQkX0oaRURERCRfShpFREREJF9KGkVEREQkX0oaRURE\nRCRfShpFRERELhDW4eBgh5u55vEnSPn2O6zDUdIhOSlpFBEREbkAWIeDow8OIuOXXyl76DBH+vXn\n6IODLpjEUUmjiIiIyAUg9fsoUpYtdzmWsmw5qd9HlUxAp1HSKCIiInIBSIuNzfN4+rZtxRxJ3pQ0\nioiIiJSwzAMHSFm2LM9zZQICijmavClpFBERESlBifMXcLD9zaRv+xmPWrVczvl06oh3u/CSCew0\nXiUdgIiIiMilKOPvfRwfNYrUVavx9L+GyjNn4B0WRur3Ufz6xRc06toV73bhGI8Lo49PSaOIiIhI\nMbLWkjh7NglvjsempFB+4ANUeu5ZPHx8APDp0J5jnh74hIeXbKCnUdIoIiIiUkzS/9jNsSefJD36\nJ7waNKDy+DfxDg0t6bAKREmjiIiIiJs5MjNJ/M8UEif/B5uZSYXhw6k4eiQeZcqUdGgFpqRRRERE\nxI3Stm/n+JMjSY/dRpmAACq/NYGyTZqUdFiFpqRRRERExA0c6ekkRL5J4qxZGC8vKj79FBVGDMfj\nApnYUljFkjQaYyoDM4EAwAIDgV+BjwB/YA9wj7X2WHHEIyIiIuJOqevXc2zUaDL/2E3Z666jyoR/\n4VWnTkmHdV6KK9WdBHxtrW0MBAE7gAjgW2ttA+Db7NciIiIipZYj6STHnnqawz3uwXH4CJUjX6fa\n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\nRhERERFJ6/8D+K0VcFgnedkAAAAASUVORK5CYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#We first create a data set programatically:\n", - "import numpy as np\n", - "xdata = np.linspace(0,8,20)\n", - "ydata = np.random.normal(xdata**2, 0.5)\n", - "xy4 = q.XYDataSet(xdata=xdata, ydata=ydata, yerr=1)\n", - "\n", - "fig3 = q.MakePlot(xy4)\n", - "xy4.fit(\"pol2\")\n", - "fig3.add_residuals()\n", - "fig3.show()\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fitting to custom functions\n", - "\n", - "We can define our own functions to fit to the data. The function must be defined with the following syntax:\n", - "\n", - "```python\n", - "def model (x, *pars):\n", - " return pars[0]*np.sin(pars[1]*x)\n", - "```\n", - "\n", - "A few comments:\n", - " * The first argument, x, is the dependent variable\n", - " * The second argument, \\*pars, is an array of parameters that we want to determine\n", - " * If you use any math function, you need to use the ones provided by numpy (you must import numpy)\n", - " * When calling the fit() function, **you must provide a guess for the parameter values**. In addition to helping the fit converge, this is necessary for the fitter to know how long the \\*pars array really is\n", - " \n", - "Thus the function above corresponds to a sine function where we want to fit for the amplitude (pars[0]) and the frequency (pars[1]).\n", - "\n", - "Below, we give an example of fitting a dataset with a custom function.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of dataset1 to custom\n", - "Fit parameters:\n", - "dataset1_custom_fit0_fitpars_par0 = 5.2 +/- 0.1,\n", - "dataset1_custom_fit0_fitpars_par1 = 0.494 +/- 0.006\n", - "\n", - "Correlation matrix: \n", - "[[ 1. 0.248]\n", - " [ 0.248 1. ]]\n", - "\n", - "chi2/ndof = 2.66/17\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "image/png": "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\nrv8PAemtsWmKGe8AAAAASUVORK5CYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#We first create a data set programatically:\n", - "import numpy as np\n", - "#regularly spaced values along the x axis\n", - "xdata = np.linspace(0,6.28,20)\n", - "#generate data from a sine wave of frequency 0.5 and amplitude 5\n", - "ydata = np.random.normal(5*np.sin(0.5*xdata), 0.5)\n", - "#make a dataset\n", - "xy5 = q.XYDataSet(xdata=xdata, ydata=ydata, yerr=1)\n", - "#initialize the plot\n", - "fig4 = q.MakePlot(xy5)\n", - "\n", - "#define our model with 2 parameters:\n", - "def model (x, *pars):\n", - " return pars[0]*np.sin(pars[1]*x)\n", - "\n", - "#fit the model - we must provide a guess for the parameters\n", - "xy5.fit(model, parguess=[1,1], fitcolor=\"darkgoldenrod\")\n", - "fig4.add_residuals()\n", - "fig4.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/jupyter/6_Advanced_Plotting.ipynb b/examples/jupyter/6_Advanced_Plotting.ipynb deleted file mode 100644 index d165ff9..0000000 --- a/examples/jupyter/6_Advanced_Plotting.ipynb +++ /dev/null @@ -1,1163 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Advanced plotting\n", - "\n", - "In this notebook, we explore some of the more advanced plotting features including:\n", - " - plotting functions\n", - " - controlling the plotting output (placement of labels, axes range, colors, figure size)\n", - " - accessing the underlying matplotlib and bokeh interfaces to fine tune your plots\n", - "\n", - "## Plotting functions\n", - "\n", - "In addition to plotting data sets, QExPy allows you to add functions on your plots. Because QExPy knows how to handle numbers with uncertainties, the parameters of the functions can be Measurement Objects, and the resulting function can be plotted with error bands.\n", - "\n", - "We start by importing the module and choosing the plot engine. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import qexpy as q\n", - "q.plot_engine=\"bokeh\" # choose bokeh or mpl" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To simply plot a function, we can define a Plot Object, and add a function to it. The function must be specified in the same format as a custom function that is being fit (see notebook 5).\n", - "\n", - "We can also choose the number of points to use when plotting the function. By default, this is 100. If a function oscillates rapidly, then this may need to be increased. Of course, increasing this slows down the plotting. " - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#We need to import numpy to define a function properly:\n", - "import numpy as np\n", - "\n", - "#make an empty plot\n", - "fig1 = q.MakePlot()\n", - "\n", - "#This will plot the function very coarsely\n", - "q.settings[\"plot_fcn_npoints\"]=10\n", - "\n", - "#define a python function to plot:\n", - "#let's plot a linear + gaussian:\n", - "def lineargauss(x, *pars):\n", - " offset = pars[0]\n", - " slope = pars[1]\n", - " norm = pars[2]\n", - " mean = pars[3]\n", - " sigma = pars[4]\n", - " \n", - " linear = offset + slope*x\n", - " gaussian = norm/np.sqrt(2*3.14)/sigma*np.exp(-(x-mean)**2/2/sigma**2)\n", - " \n", - " return linear + gaussian\n", - "\n", - "\n", - "\n", - "#We can then add the function, naming it, giving the parameters, and specifying\n", - "#the range over which to plot it\n", - "fig1.add_function(lineargauss, name=\"linear + gauss\", pars = [2,0.5,10,5,1],\n", - " color = 'saddlebrown',\n", - " x_range =[0,20])\n", - "\n", - "#And we can then show it:\n", - "fig1.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "### Plotting a function of parameters with uncertainties (and adding labels and changing the figure size)\n", - "We can replot the same function, but with parameters that have uncertainties. Suppose that we want to see the effect of an uncertainty in the normalization of the gaussian (the third parameters, pars[2]):" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false, - "scrolled": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#Make a new figure:\n", - "fig2 = q.MakePlot()\n", - "\n", - "#Set the range:\n", - "fig2.x_range=[0,10]\n", - "\n", - "#Plot with more points:\n", - "q.settings[\"plot_fcn_npoints\"]=100\n", - "\n", - "\n", - "#Store the paramaters for the function as a MeasurementArray\n", - "#Note that we chose only the third parameter to have a non-zero\n", - "#uncertainty\n", - "pars = q.MeasurementArray( [(2,0),\n", - " (0.5,0),\n", - " (10,2),\n", - " (5,0),\n", - " (1,0)])\n", - "\n", - "#Add the function\n", - "fig2.add_function(lineargauss, pars = pars, name=\"uncertainty in norm\",\n", - " color='darkkhaki',\n", - " x_range =[0,20])\n", - "\n", - "#Let's also change the title of the axes:\n", - "fig2.labels[\"xtitle\"]=\"energy [MeV]\"\n", - "fig2.labels[\"ytitle\"]=\"number of counts\"\n", - "fig2.labels[\"title\"]=\"location of the particle resonnance\"\n", - "\n", - "#Let's change the figure size, by giving dimensions in pixels:\n", - "fig2.dimensions_px = [800,400]\n", - "\n", - "#show the figure\n", - "fig2.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "## Use function plotting to show 2 different fits to the same dataset\n", - "\n", - "By default, QExPy only shows the last fit to a dataset when plotting that dataset. However, we saw earlier that a dataset recalls all functions that were fit to it (and the results). Below, we generate a dataset programatically, then fit it to two functions and compare the results.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-----------------Fit results-------------------\n", - "Fit of Run10_Data to gaussian\n", - "Fit parameters:\n", - "Run10_Data_gaussian_fit0_fitpars_mean = 8.1 +/- 0.2,\n", - "Run10_Data_gaussian_fit0_fitpars_sigma = 1.4 +/- 0.2,\n", - "Run10_Data_gaussian_fit0_fitpars_normalization = 32 +/- 3\n", - "\n", - "Correlation matrix: \n", - "[[ 1.000e+00 -4.736e-08 -4.885e-09]\n", - " [ -4.736e-08 1.000e+00 5.774e-01]\n", - " [ -4.885e-09 5.774e-01 1.000e+00]]\n", - "\n", - "chi2/ndof = 664.12/46\n", - "---------------End fit results----------------\n", - "\n", - "-----------------Fit results-------------------\n", - "Fit of Run10_Data to custom\n", - "Fit parameters:\n", - "Run10_Data_custom_fit1_fitpars_par0 = 0.9 +/- 0.2,\n", - "Run10_Data_custom_fit1_fitpars_par1 = 0.10 +/- 0.01,\n", - "Run10_Data_custom_fit1_fitpars_par2 = 20.6 +/- 0.9,\n", - "Run10_Data_custom_fit1_fitpars_par3 = 8.03 +/- 0.04,\n", - "Run10_Data_custom_fit1_fitpars_par4 = 1.03 +/- 0.05\n", - "\n", - "Correlation matrix: \n", - "[[ 1. -0.848 -0.441 0.09 -0.284]\n", - " [-0.848 1. 0.203 -0.106 0.131]\n", - " [-0.441 0.203 1. -0.022 0.645]\n", - " [ 0.09 -0.106 -0.022 1. -0.014]\n", - " [-0.284 0.131 0.645 -0.014 1. ]]\n", - "\n", - "chi2/ndof = 45.68/44\n", - "---------------End fit results----------------\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "##############################\n", - "#Programatically generate data\n", - "##############################\n", - "\n", - "#Generate some values around the x axis\n", - "xvals = np.linspace(0,20, 50)\n", - "#Generate values of y for those x values, normally distributed\n", - "#about our model:\n", - "#\"true\" parameters of the model:\n", - "true_pars = [1,0.1,20,8,1]\n", - "#the y values with some randomness:\n", - "yvals = np.random.normal(lineargauss(xvals, *true_pars),0.5)\n", - "\n", - "##################\n", - "#Build the dataset\n", - "##################\n", - "xydata = q.XYDataSet(xdata = xvals, ydata = yvals, yerr = 0.5)\n", - "#Set the name of the dataset:\n", - "xydata.name = \"Run10_Data\"\n", - "xydata.xname = \"Energy\"\n", - "xydata.xunits = \"MeV\"\n", - "xydata.yname = \"Counts\"\n", - "\n", - "#######################################\n", - "#Fit the data set to 2 different models\n", - "#######################################\n", - "\n", - "#Let's fit it to a gaussian and to a linear+gaussian\n", - "#to see which one is best:\n", - "\n", - "###Fit to a gaussian\n", - "#For the gaussian, we use the built in model:\n", - "#For a gaussian, it's best to give a guess for the parameters\n", - "gresults = xydata.fit(\"gaussian\", parguess = [8,1,20])\n", - "\n", - "###Fit to our custom model:\n", - "lgresults = xydata.fit(lineargauss, parguess = [1,1,1,8,1])\n", - "\n", - "####################\n", - "#Display the results\n", - "####################\n", - "\n", - "#Build a plot object:\n", - "fig3 = q.MakePlot(xydata)\n", - "#This will automatically display the last fit, so we add a function \n", - "#for the first fit:\n", - "fig3.add_function(xydata.fit_function[0],\n", - " name = 'gaussian',\n", - " pars = xydata.fit_pars[0],\n", - " color = 'blue')\n", - "#The plot is a little busy if we show the fit results, so we don't show them:\n", - "fig3.show_fit_results = False\n", - "#The residuals will only be shown for the last fit\n", - "fig3.add_residuals()\n", - "fig3.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using the underlying bokeh interface\n", - "In the figure above, we would like to see the residuals from the first fit (the blue curve), since by default, QExPy will only show the results of the last fit. We can do this by modifying how we draw the figure. When we call show() on a Plot Object, QExPy will first create a figure, then populate it, then show it. We can modify the figure at any point during the show command, if we tell show() not to populate the figure (as this will erase the figure and start from scratch by default)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#First we populate the figure (with Bokeh, this starts from an empty figure)\n", - "fig3.populate_bokeh_figure()\n", - "#Then, we add the residuals from the first fit (fit index = 0)\n", - "fig3.bk_plot_dataset(xydata,residual=True, fit_index=0, color='blue')\n", - "#And now we show the figure, but ask QExPy not to populate it, since we\n", - "#already did\n", - "fig3.show(populate_figure=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "# Using the backends\n", - "QExPy was designed to enable the user to access and manipulate the plotting backends if needed. This can be done when there is a feature in Bokeh or Matplotlib that isn't implemented in QExPy." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Bokeh\n", - "Bokeh provides a number of useful plotting and graphing features that aren't used by QExPy. When initialize_bokeh_figure() is called, it returns a Bokeh figure object that can be manipulated before plotting. Again, we won't want to populate the figure before we show it, since that would clear all the changes we made to the Bokeh figure. We will do a simple example that involves drawing three annuli. A full reference for Bokeh plotting and figure objects can be found here: http://bokeh.pydata.org/en/latest/docs/reference/plotting.html" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "q.plot_engine='bk'\n", - "#First we make the QExPy plot and set the x and y ranges.\n", - "fig4 = q.MakePlot()\n", - "fig4.x_range=[0,10]\n", - "fig4.y_range=[0,10]\n", - "#Then we populate the Bokeh figure (with Bokeh, this starts from an empty figure)\n", - "bkfigure = fig4.initialize_bokeh_figure()\n", - "#Then we draw the annuli, using figure.annulus from Bokeh.\n", - "bkfigure.annulus(x=[1, 5, 9], y=[1, 5, 9], color=\"#7FC97F\",\n", - " inner_radius=0.2, outer_radius=0.5)\n", - "#And now we show the figure, but ask QExPy not to populate it, since we already did\n", - "fig4.show(populate_figure=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Matplotlib\n", - "The Matplotlib backend can also be accessed for more customizable plotting. We use the same method to access the backend as with Bokeh. For the most part, you will want to access the figure's Axis object, and then display onto that. The documentation for this object can be found here: https://matplotlib.org/api/figure_api.html" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\nUA4AAMAy/x/j0ZQV/kMihAAAAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "q.plot_engine='mpl'\n", - "#First we make the QExPy plot and set the x and y ranges.\n", - "fig5 = q.MakePlot()\n", - "fig5.x_range=[0,10]\n", - "fig5.y_range=[0,10]\n", - "#Then we populate the Matplotlib figure\n", - "mplfigure = fig5.initialize_mpl_figure()\n", - "#Most things in matplotlib are drawn onto a Axis object, so we should get that.\n", - "axis = mplfigure.gca()\n", - "#We will now draw an arbitrary object using Axis.fill_between()\n", - "axis.fill_between([1, 9], [0.5, 7.5], [2.5, 9.5], facecolor='r',\n", - " alpha=0.3, edgecolor = 'none',\n", - " interpolate=True, zorder=0)\n", - "#And now we show the figure, but ask QExPy not to populate it, since we already did\n", - "fig5.show(populate_figure=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/plotting_and_fitting.ipynb b/examples/plotting_and_fitting.ipynb new file mode 100644 index 0000000..0c21f0d --- /dev/null +++ b/examples/plotting_and_fitting.ipynb @@ -0,0 +1,719 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Intro to Plotting and Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import qexpy as q\n", + "import qexpy.plotting as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The plotting module of QExPy is a wrapper for matplotlib.pyplot, developed to interface with QExPy data structures." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# let's start by creating some arrays of measurement\n", + "xdata = q.MeasurementArray(\n", + " [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], unit=\"m\", name=\"length\")\n", + "ydata = q.MeasurementArray(\n", + " [6, 16, 35, 41, 46, 56, 61, 79, 87, 98], error=5, unit=\"N\", name=\"force\")\n", + "\n", + "# now we can add them to a simple plot\n", + "plt.plot(xdata, ydata, name=\"first\")\n", + "# use `figure = plt.plot(xdata, ydata)` to obtain the Plot object instance for further \n", + "# customization. qexpy.plotting keeps a buffer of the latest Plot instance, if you did \n", + "# not assign the return value of plt.plot to anything (like what we are doing here), you \n", + "# can still retrieve the Plot instance using `figure = plt.get_plot()`, as shown below.\n", + "\n", + "# draw the plot to the screen\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see in the plot above, the name and units of the data that's passed in are automatically added to the plot as axis labels. For simple plotting purposes, this is enough. However, if you wish to further customize the plot, you can try to operate directly on the plot object. You will be able to change the title as well as the axis labels yourself. You can also add error bars and legends to the plot." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# retrieve the current plot object\n", + "figure = plt.get_plot()\n", + "\n", + "# As you can see, the error bars are automatically on. \n", + "# If not, we can manually add error bars to the plot\n", + "figure.error_bars() # use `figure.error_bars(false)` to turn off error bars\n", + "\n", + "# we can add a title to the plot\n", + "figure.title = \"Demo Plot\"\n", + "\n", + "# finally draw the plot\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of first to linear\n", + "\n", + "Result Parameter List: \n", + "slope = 9.8 +/- 0.4,\n", + "intercept = -1 +/- 3\n", + "\n", + "Correlation Matrix: \n", + "[[ 1. -0.886]\n", + " [-0.886 1. ]]\n", + "\n", + "chi2/ndof = 4.75/7\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# We can try to add a fit to the plot. The fit function automatically selects the last\n", + "# applicable fit target (a data set or a histogram) on the plot.\n", + "result = figure.fit(model=q.FitModel.LINEAR)\n", + "\n", + "# also add a residuals subplot\n", + "figure.residuals()\n", + "\n", + "# show the plot and the result\n", + "figure.show()\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdUAAAIOCAYAAADuqJeGAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOzdd3hc5Zn38e8zo1GzmtWbe7fl3jHGNiXGmJoECC3YBAwJCS0xZWHZTbIsvEk2ZNlsCKRBWAKG0HuxEcXYBhsbN8kV2+p9RjPS9HneP87ISLaMJXlGM5Luz3XpsnTmlPscC/845zxFaa0RQgghxKkzRboAIYQQor+QUBVCCCFCREJVCCGECBEJVSGEECJEJFSFEEKIEJFQFUIIIUJEQlUI0WNKqcVKqfJI1yFEtJBQFSIKKKUOKaWcSim7UsqqlPpUKXWTUiri/40qpVYopfxKKYdSqlkptU0pdX4P9vOEUuo/wlGjENEi4v/BCiGOukBrnQwMAx4C7gL+EtmSjtqgtU4C0jBqek4pNTjCNQkRdSRUhYgyWmub1vpV4HLgWqVUEYBSKk4p9Rul1BGlVI1S6o9KqYTgZ4uVUuVKqTuVUrVKqSql1MVKqfOUUnuVUo1KqX9pO0ZwX79TSlUGv36nlIrrQm0B4K9AAjDq2M+VUhOUUsXBu+1dSqkLg8tXAVcBdwbveF8LwaUSIupIqAoRpbTWnwHlwMLgooeAscA0YDRQANzfbpNcIL7d8j8BVwMzg/v4V6XUiOC69wLzgvuaCswB7jtZTUqpGOB6wAHsO+YzC/Aa8C6QDfwEeFopNU5r/TjwNPArrXWS1vqCLl8IIfoQCVUholslkK6UUsAq4HatdaPW2g78J/C9dut6gQe01l7gWSAT+G+ttV1rvQvYjRGgYNw1/kJrXau1rgN+DlzzDXXMU0pZgWrgCuASrbXt2HWAJOAhrbVHa70OeD24vhADQkykCxBCfKMCoBHIAhKBLUa+AqAAc7t1G7TW/uD3zuCfNe0+d2KEHkA+cLjdZ4eDy05ko9b69JPUmg+UBR8Rt99vwUm2E6LfkDtVIaKUUmo2RiB9AtRjhOIkrXVa8Cs12HioJyoxGkS1GRpcdioqgSHHtFgeClQEv5cpsUS/J6EqRJRRSqUEu6w8C/yf1npH8O7vT8DDSqns4HoFSqmlPTzMM8B9SqkspVQmxjvY/zvF0jcBrRiNkSxKqcXABcHzAOOueeQpHkOIqCahKkT0eE0pZQfKMBoS/RZY2e7zu4D9wEalVDPwPjCuh8f6D2AzsB3YAXwRXNZjWmsPRoguw7iz/gPwfa11aXCVvwATgy2DXz6VYwkRrZRMUi6EEEKEhtypCiGEECEioSqEEEKEiISqEEIIESISqkIIIUSI9IvBHzIzM/Xw4cMjXUava2lpYdCgQZEuo1+Qaxkach1DR65laITjOm7ZsqVea53V2Wf9IlSHDx/O5s2bI11GrysuLmbx4sWRLqNfkGsZGnIdQ0euZWiE4zoqpQ6f6DN5/CuEEEKEiISqEEIIESISqkIIIUSI9It3qp3xer2Ul5fjcrkiXUrYpKamUlJS0u3t4uPjKSwsxGKxhKEqIYQYuPptqJaXl5OcnMzw4cNpN1VWv2K320lOTu7WNlprGhoaKC8vZ8SIESffQAghRJf128e/LpeLjIyMfhuoPaWUIiMjo1/fwQshRKT021AFJFBPQK6LEEKER78O1e66/LENXP7YhkiXIYQQoo+SUA2jRx55hAkTJjB48GAeeuihLm936NAh/vGPf4SxMiGEEOEgoRr08tYKth6xsumrRhY8tI6Xt1ac8j7/8Ic/8N5779HU1MTdd9993Oc+n6/T7SRUhRCib+q3rX+74+WtFdzz4g48/gAAFVYn97y4A4CLpxf0aJ833XQTBw8eZNmyZVx33XUcOHCA3//+96xYsYL4+Hi2bt3KggULuOiii7j11lsB413nRx99xN13301JSQnTpk3j2muv5fbbbw/NiQohhAirAR2qbe9Ptx6xHg3UNk6vnzv/uZ1nPjvCmhvnd3vff/zjH3n77bf54IMPeP311zt8Vl5ezqefforZbOaCCy7gf//3f1mwYAEOh4P4+HgeeughfvOb3xy3nRBCiG7423KmWa2weH2vHVIe/8JxgXqy5afq0ksvxWw2A7BgwQLuuOMOHnnkEaxWKzExA/r/c4QQok8b0KG65sb5rLlxPgVpCZ1+XpCW0KO71JNpPw3R3XffzZ///GecTicLFiygtLQ05McTQgjROwZ0qLZZvXQcCRZzh2UJFjOrl44L+7EPHDjA5MmTueuuu5g9ezalpaUkJydjt9vDfmwhhBChJaGK0RjpwW9PJtZsXI6CtAQe/PbkHjdS6o7f/e53FBUVMWXKFCwWC8uWLWPKlCmYzWamTp3Kww8/HPYahBBChIa8wAu6eHoBz3x2BCBkj3wPHToEwIoVK1ixYgUATzzxRId1/ud//qfTbdetWxeSGoQQQvQeCdV2wvH+VAghxMAR9se/Sqm/KqVqlVI72y37tVKqVCm1XSn1klIqrd1n9yil9iul9iilloa7PiGEEP3Q9ueg/HNSbTvh4SLj517QG+9UnwDOPWbZe0CR1noKsBe4B0ApNRH4HjApuM0flFJmhBBCiK7a/hy8dgv43SgAW5nxcy8Ea9hDVWv9EdB4zLJ3tdZtY/RtBAqD318EPKu1dmutvwL2A3PCXaMQQoh+ZO0vwOvsuMzrNJaHWTS8U70OWBP8vgAjZNuUB5cdRym1ClgFkJOTQ3FxcYfPU1NT+323FL/f3+NzdLlcx12zgczhcMj1CAG5jqEj17LnFtnK6WyCS20r58MwX9OIhqpS6l7ABzzd3W211o8DjwPMmjVLL168uMPnJSUlJCcnd2+nf1tu/Lnyje6WExF2u7375xgUHx/P9OnTQ1xR31VcXMyxv0Oi++Q6ho5cy1PwWQa01h+3WKUWhv2aRqyfqlJqBXA+cJXWWgcXVwBD2q1WGFwmeqC4uJjzzz8/0mUIIUTvOfQJOJvg2HtVSwKcdX/YDx+RUFVKnQvcCVyotW5t99GrwPeUUnFKqRHAGOCzXikq2FKMw5/0aksxIYQQIVK5Ff5xGSSmw+TLwByLBkgdAhc8AlMuC3sJvdGl5hlgAzBOKVWulPoB8HsgGXhPKbVNKfVHAK31LuA5YDfwNnCz1tof7hrbtxQDQtZSrKWlheXLlzN16lSKiopYs2YNW7ZsYdGiRcycOZOlS5dSVVUFwP79+zn77LOZOnUqM2bM4MCBA2itWb16NUVFRUyePJk1a4xXz22Pha655hrGjx/PVVddRdvN/ttvv8348eOZMWMGL7744inVL4QQfUbdXnjqEjBZYN7NcN6voHAOttQiuH1nrwQq9MI7Va31FZ0s/ss3rP8A8ED4Kmqn7R1q+edfB2obrxNe+TFsebLH71jffvtt8vPzeeMNY3ubzcayZct45ZVXyMrKYs2aNdx777389a9/5aqrruLuu+/mkksuweVyEQgEePHFF9m2bRtffvkl9fX1zJ49mzPOOAOArVu3smnTJsaOHcuCBQtYv349s2bN4oYbbmDdunWMHj2ayy+/vMeXRggh+oymw/DkBeD3wfybYea1kJB28u3CIBpa/0besYF6suVdNHnyZH76059y1113cf755zN48GB27tzJOeecY+ze7ycvLw+73U5FRQWXXHIJYDQiAvjkk0+44oorMJvN5OTksGjRIj7//HNSUlKYM2cOBQUFmEwmpk2bxqFDh0hKSmLEiBGMGTMGgKuvvprHH3/8lM5BCCGimr0G/n4huG0w5yaYfb3x+DdCBnaott2BPlxkPPI9VuqQU2oJPHbsWL744gvefPNN7rvvPs4880wmTZrEhg0bOqzXk24xcXFxR783m834fL5vWFsIIfohZxM8dRE0V8Ks62DujZCUFdGSZJYaMFqEWY6ZUzUELcUqKytJTEzk6quvZvXq1WzatIm6urqjoer1etm1axfJyckUFhby8ssvA+B2u2ltbWXhwoWsWbMGv99PXV0dH330EXPmnHgsjPHjx3Po0CEOHDgAwDPPPHNK9QshRNRy2+Gpb0P9Pph6pfHYNyUv0lUN8DvVNm0vsF/5sfHIN3WIEain+GJ7x44drF69GpPJhMVi4dFHHyUmJoZbbrkFm82Gz+fjtttuY9KkSTz11FPceOON3H///VgsFp5//nkuueQSNmzYwNSpU1FK8atf/Yrc3NwTTmQeHx/P448/zvLly0lMTGThwoX9fgAMIcQA5HXCPy6Hqm0w5XJYcCukDY10VQCor7uI9l2zZs3Smzdv7rCspKSECRMmdG9HA2jwhx5dn35MOtqHhlzH0JFreQI+Dzx7Jex/H4q+DYvugqxxna/7t+VYrVbSbl8f0hKUUlu01rM6+0zuVNvrI2EqhBADUsAPL14P+9+D8efD6XecOFABVr7BtuJiFvdagfJOVQghRF8QCMArN8PuV2DMt2DhzyC3KNJVHadfh2p/eLQdDnJdhBB9itbw1mr48hkYuRjOuBPyp0W6qk7121CNj4+noaFBAuQYWmsaGhqO9oUVQoiopjW8dz98/mcYdhqccRcUzgLV2Tw0kddv36kWFhZSXl5OXV1dpEsJG5fL1aNwjI+Pp7Cw8OQrCiFEpH34K/j0ESicDYvugaHzojZQoR+HqsViYcSIEZEuI6yKi4tl+jYhRP/16e+h+D8hbxosvgeGnw6m6H7AGt3VCSGEGJg+/wu8ey/kTIJFdxvvUqM8UEFCVQghRLTZ9g944w7IHAdn3A1jzgGTOdJVdYmEqhBCiOix80Wj60zGKFh0J4xfBua+86ZSQlUIIUR0KHkdXrgeUofCwjthwoVgtkS6qm6RUBVCCBF5e9+B56+FlHw442dQdAnExEa6qm6TUBVCCBFZ+9fCmqshKccYenDypRATd/LtopCEqhBCiMj56iNjgPzEDCNQp10Blr47OI2EqhBCiMg4/Ck8fRnEp8Lpt8O0K4+f27qPkVAVQgjR+8o+g//7LsQlw4LbYfrVEJsY6apOmYSqEEKI3lXxBTx1iXFXuuA2mHENxA6KdFUhIaEqhBCi91Rug79fBOZYWHArzPw+xCVFuqqQkVAVQgjRO6q2w98vNEZHWnAbzFxhPP7tRyRUhRBChF/1TiNQUXDaLTBrJcSnRLqqkJNQFUIIEV41u+HJC4y5URfcArOv75eBChKqQggheuJvy42vk6nZDU+eDwE/zP9xvw5UkFAVQggRLm2B6vfCaT+GuauMPqn9WN8Z+l8IIUTfUVvSLlBvGRCBCnKnKoQQItRqS+GJtkD9yYAJVJA7VSGEEKFUWwJPLG8XqDdGLFAvf2wDVquTxYt775hypyqEECI0oihQI0XuVIUQQpy6o42SfMZISXNW9etWvicS9jtVpdRflVK1Sqmd7ZalK6XeU0rtC/45OLhcKaUeUUrtV0ptV0rNCHd9Qgghumn7c1D+ORz+BB4ugo9/2y5QbxmwgQq98/j3CeDcY5bdDazVWo8B1gZ/BlgGjAl+rQIe7YX6hBBCdNX25+C1W8DvNn62lcHan4OndUDfobYJe6hqrT8CGo9ZfBHwZPD7J4GL2y3/uzZsBNKUUnnhrlEIIUQXrf0FeJ3HL4+JhTk3DOhAhci9U83RWlcFv68GcoLfFwBl7dYrDy6r4hhKqVUYd7Pk5ORQXFwctmKjlcPhGJDnHQ5yLUNDrmPoROu1XGQrR3WyXLua+XDjF71ezzexWp34/f5evY4Rb6iktdZKKd2D7R4HHgeYNWuWXtybbaajRHFxMQPxvMNBrmVoyHUMnai9llsLjUe+x1CphVFV78tbK/iqeTsev+LejQFWLx3HxdMLwn7cSHWpqWl7rBv8sza4vAIY0m69wuAyIYQQ0WDqFccvsyTAWff3fi0n8PLWCu55cQcefwCACquTe17cwctbwx8nkQrVV4Frg99fC7zSbvn3g62A5wG2do+JhRBCRNJXH8OG30NsMiizsSx1CFzwCEy5LLK1tfPrd/bg9Po7LHN6/fz6nT1hP3bYH/8qpZ4BFgOZSqly4N+Ah4DnlFI/AA4DbX8bbwLnAfuBVmBluOsTQgjRBQc+gGe+ZwzmsPAO2PWyEawr34h0ZR00ONxUWDtpSAVUnmB5KIU9VLXWnTwrAOCsTtbVwM3hrUgIIUS37H0X1lwFiRmw8Kcw/RrY/Vqkq+rA2urhy3IrhxtaSUu0YG31HrdOflpC2OuQYQqFEEKcWMnr8OyVMCgbFt5pBKolPtJVHWVr9bJ+fz2vfVlJjc1NXko8351RSKy5Y7wlWMysXjou7PVEvPWvEEKIKLXzBXjhBkgtgNN/CtOugJi4SFcFGI95d1c1c6i+ldgYRU5KPCZldPbJTomjcHACB+tbAMhNiefuZeN7pfWvhKoQQojjbXsGXvkRpA2DM1YbDZHMlkhXRb3DzfYyK+VWJ/ExZnJS4jAphdaaHRU2Xt9eyYG6FhIsZgYnWkg2e3njjjNIju+d2iVUhRBCdLT5r/D67ZAxBhbdCZMuiXigNrUE35k2tjLIYiYvJR4VDNNdlTZe2VbJwfoWMgbF8r3ZQzh9dCaPrNuH3+Xr1TolVIUQQnxtwx/gnXsgazws+RcYtxzMnURFL7X6bWzxUFLVzIFaB/EWM/ntwnRHhY03d1Sxr9ZBemIs3583jNNGZRBjjlxzIQlVIYQQho//yxjbN6cIltwLY5eCydzrZWitqbW72VFuo9LmJM5sIjfVeGfqD2g2H2rg7Z3VlDU5GZxo4co5Q1k4JhNLBMO0jYSqEEIMdFrDul8aoZo3zRgdaeQSMPV+SNU2u9hyuIl6h5vE2Jijj3l9gQCfHmjgjR1V1Nnd5KXGs3LBcOYOT+/0ztTrD+D1BzABMb14HhKqQggxkAUC8Pbd8NljUDgHzrwPRpwBqrNh88OnscXDl0eslFlbSY6LIS/V6FPqCwTYuD8Ypg43Q9MT+dHiUUwbkna0tW97DpcPh9tLrMXEoNgYvH5FQmzv3W1LqAohxEAV8MOrP4FtT8Pw0+HMf4Mhs3s1UG2tXnZW2jhYZ7wzbbszbfX4+HhfPe+X1NDU6mVYRiI/nj2aqYWpqBOEqc3lJTM5lkXDs8lLjeeFLRX0wiBKHUioCiHEQOTzwEurYNdLMOosOPN+KJjWa4dvdnnZXWljf01Lh36m1lYP7+6u4aN9dbi8AcblJHPNvGFMLug8TO0uL3aXj4zkWL41KofcYCgDrLlxfq9PnyehKoQQA42nFZ77Pux/D8adZ7xDzZ7QK4d2uH2UVDZTWt2MxWwiO9jP1Ob08tbOKj7cW4c/oJk1LJ1vTcpheMag4/ahtcbq9OL0+slJjmPeqIwOYRpJEqpCCDGQuJrhH5fDkQ1Q9B1YfC9kjur2bi5/bANg3A12RavHR0lVMyVVdmJMkJMcj8lkhOm7u6r5YE8d3kCA+SMzuGBKPlnJx4/cFNCaplYPHl+AIemJTMpPJTMpNirCtI2EqhBCDBQtDfB/l0D1Tph2JSy+G9KGhvWQrR4fpVV2SqqaMZkgKykOs8l4zPvOrho+3GuE6Zzh6Vw4NZ+clOPHFfYHNE2tbnwBzaisJCbkpZCWGBvWuntKQlUIIQaC5kr4+0XQ+BXMXAmLVkNybtgOd2yYZiTFEmMyYW318NbOaj7aZzzmnTsig+VT8sjtJEw9vgCNTg9omJCXzNic5F4bbrCnJFSFEKK/azwIT14ILXUwdxWcfgcMygzLoRxuH3uqmymtsqPU12Ha2OLh7WCYBrRm/sgMzpuc1+mdaYvbh93tIy7GxIwhaYzITOrVbjGnQkJVCCH6s5pd8PeLwdsC838M82+GxPSQH8bh9lFa1UxptR2zCTKDj3kbWzy8tbOKj/fVozWcNsoI087emdpdXuxuH4MTY1k4JpOCtISIDjnYExKqQgjRX5V9Dk9/x/h+we0w90aITwnpIVrcRgOk0iojTLOT4jCZFPUON2/trOaT/fXG4YNhmpnUMUy11jS7fLR4fGQmxTF3pNGS12SKnsZH3SGhKoQQ/dH+92HN1WAZBAtug9nXQezx3VN6KqA1rR4/L22twGyCrGTjzrS62cVbO6rYeLARpWDh6EyWFeWScUyYBrTG2urF5fOTlxrP6WMyyU6Oi6qWvD0hoSqEEP3NzhfgxRshKQsW/BRmXAWWhJDsutXj4/EPD/LFESv+gOa37+3h29MLcQ0O8OaOKjYfbiLGpFg0LotzJ+WSPqhjK92vu8VohmUkMik/5bjA7cskVIUQoj/Z/Fd4/Q4YPNxokDT1exBz6t1P2lrzPvPZEV7eVoE/oAFobPHyl/VfoTXExZhYOjGXcybmkJrQsZWuP6BpbHHj10a3mIl5qaQmRndL3p6QUBVCiP5Aa/joN/DBf0DWBFh0F0y4oPO5ULvh2K4xa0tr8fr1cYdOsJh58NuTSYrreDxfIEBTqwd/AMbnJjMuN/q7xZwKCVUhhOjr2s80UzATFt9jjOd7ClOetbh97Kk2wrSta4xJGa15O+P0+jsEqs8foLHVg9YwIS+FcbnJDIrr/5HT/89QCCH6M58HXv4h7PwnDF8IS+6DoXN7PNNMs8tLaVUze6sdmM3BMEXx+aFG3thRdcLt2t6dev0BGlo8mBRMyk9hTE4yibEDJ2oGzpkKIUR/43YYA+MfWAtjl8GSeyFvco92ZXMGZ42pdRgD3Qf7kX5+qJHXtldR3eyiIC2BJeOyWL+/Hk+7R8CxZhMXTMmj2ubCEqOYMTSNkVlJxFv6xoANoSShKoQQfVFLPTz9XajaBpMvhcX/Ahkju72b9mEaG2MiJyWegNZs/KqBt3ZWU2UzwvSmRSOZMXQwJqUYlZXEE58ewhfQDE60cNb4bKYOTWNKQSpD0wcRG9O3BmwIJQlVIYSINn9bzjSrFRav7/zzpsPw1CVgPQIzr4MzfgYp+d06RNt8pvtqvg5Try/AutJa3t1dQ2OL57gwbTNnRDrrSmsJaM29yydSVJBydNaZgU5CVQgh+pKaXUaguu0w74ew4NZujeNrd3kpqbKzt6aZGJMRpi6vnzd3VPF+SS0Ot48x2UlcPXfocRODt7Xk9fk1cRYTibExnDMxJxxn2WdJqAohRJTZVWXD5/OTduwHhz6BZ64wGiEtuB3m3QjxqV3ap83ppbS6mX3VDmLMiuykeFo8Pl7ZVsm60lqcXj+TC1I5ryiXMTnJHbb1BQJHW/2Oz01hTE4Sr28/caOlgUxCVQgh+oLdr8AL10PCYGPYwZnXdmnYQWurh12VzXxV12KEaXIczS4vz39Rbsxl6gswY9hglhflMTQjscO27cN0ILbk7Qm5OkIIEe0++xO8uRoGD4PTb4OpV0LMNw/t1xamB+scxJpNZKfE0dTi4ZnPj/DxvnoC2pjLdFlRLvlpHYcw9PiMx7wq2C1mbE5Kn5l6LdIkVIUQIlppDWt/AZ/8FrInwaI7Yfz53zhKks3pZWeFjYN1LcSaFTkp8dQ2u3ni00NsOtgIypgxZlnR8dOvOT1+rE4PcRYTM4YZ85gOxG4xp0JCVQghopDSGl66EbavgSFzjS4zI8444ShJx7fmjaO8ycnzWw6y5XATFrOJJeOz+NbE4we5t7u82F0+UhIsLBidyZD0RCwnmcd0zY3zQ3au/UlEQ1UpdTtwPaCBHcBKIA94FsgAtgDXaK07HxdLCCH6m+3PMcZTggUfbC+F3Mlw7q8gf2qnoyTZXV5Kq+zsadea91B9C09vOsL2chsJFjPLinI5e0IOKe0GuddaY3N6afX6yUqKY87IDPL68Dym0SJioaqUKgBuASZqrZ1KqeeA7wHnAQ9rrZ9VSv0R+AHwaKTqFEKIXrP9OXj1J8Ti+3pZ/T5o2AsF0zqserQ1b40dszJa8+6vc/DkhkOUVNkZFGvmomn5nDU+u0PjokBA0+T04PEFGJKeyMT8FLKS+v48ptEi0o9/Y4AEpZQXSASqgDOBK4OfPwn8OxKqQoiB4N1/BZ+r4zKfy3ivOuUywGiAVFLVfHQ4waxBcZTW2Pnr+kPsq3WQHB/Dd2YUsGRcdof3oe1nixmTk8S4nGTSEk99SjjRkdJan3ytcB1cqVuBBwAn8C5wK7BRaz06+PkQ4C2tdVEn264CVgHk5OTMfPbZZ3ut7mjhcDhISkqKdBn9glzL0JDr2HNpTTuYuu2+TsfB1yjWnfESTq8fjy+AUmBSipLGAG8f9nOoWZMWC2cNNXNanplY89c70VrjC859mmAxE2cxM5Ce8Ibjd3LJkiVbtNazOvssko9/BwMXASMAK/A8cG5Xt9daPw48DjBr1iy9ePHiMFQZ3YqLixmI5x0Oci1DQ65jD+34J3z8c6oZTC5Nx31cRQYVCSNISDGTGh/Dl+U2Xt9RxeGGVjIGxXL13FwWjM7s0LjI7fXT6PRiMSmKClIZnT0wW/L29u9kJB//ng18pbWuA1BKvQgsANKUUjFaax9QCFREsEYhhAgfrWH97+D9f4f0kTxYfS4PWv5Movq6bWarjuUh72UsS4pj8+Em3t5ZTYXVSVZyHCvmD2feqHRi2rUIbvX4sDm9JMSamTs8nWGZicTFDLwwjZRIhuoRYJ5SKhHj8e9ZwGbgA+C7GC2ArwVeiViFQggRLn4fvPkz2PI3yJsGZ9zF5lfiuLsZ7ox5jnzVQKXO4Fe+y3g/ZhGfvLyTxhYP+Wnx/GDBCOaMSMfc7jmu3eXF7vaRmmDhjLFZFKQlEHOSbjEi9CIWqlrrTUqpfwJfAD5gK8bj3DeAZ5VS/xFc9pdI1SiEEGHhdsDzK2D/ezByCSy5h8bB07hg6kH+8slCXvWc3nF9j5+CtASuCg5y3zZjTPtuMTnJccwblSGzxURYRFv/aq3/Dfi3YxYfBOZEoBwhhAi/5ir4x6VQsxOKvkPT7Dv4sjWbI+VVjMxM4orZQ3hh4x5aiAcU+SlxXD1/OGPbDXKvtcbq9OL0+ilMS2BRYdpxoyOJyIh0lxohhBg4qnfA05eCsxHn1JV8MXQlByqTSLC4SYmLYd2eOt4vqaGVBKaZDrD0W+cxJvvrMA0ENE2tHjx+o4/p5HfzV4AAACAASURBVIJUMpIkTKOJhKoQQvSGfe/D898noGI4NG4Vnw2+FHNgMIMs8F5JLR/sqcXtCzCtMI0r63/HOFVGXfblAPj8ARpbPQQ0jMoaxPjcFAYPkj6m0UhCVQghwu3zv6DfXI07IYsvh36fiiHfxWRO4J1d1Xy0tx5vIMDsYeksm5zLkMGJZL1QCYH2s8UoJuWnMDo7mUFx8s92NJO/HSGECBPt9+F8819I3PIY1qRR7Bl1A4dyv8VbuxtYv/8AAa2ZNzKD8ybnkZsS33FbwO72ymwxfYyEqhBChJjWmvqGRvQL15NdtY7a9Fl8OuIWnqvKZuNnpZiU4rROpl9r62P6YOIDmL1O3phReNLZYkR0kVAVQogQqne4Kd1TyoTiVaTb97Ar63x+6f8+m9b7sJitnDk+m6WTchncbtzdtj6maQmxLBqbxevbq7BanRKofZCEqhBChECDw82OChv2rzZz9tZbiPE28+u4H/OHsvnExQQ4tyiXc9pNv6a1ptnlo8XjIyspjrkjM8gNTr225sb5FBcXR/aERI9IqAohRA9pralzuNlZbqPc6mRU3TrO3XYPVp3I1a6fc8Q7nAum5HDWhBySgg2MAlpjbfXi8vnJT01g4ZhMspJl6rX+QkJVCCG6SWtNrd3NtiNN1NjdJMaYGLPnT8w/9Hu+CIzmjsDtTJ88jpsn5h+dy9Qf0DS2uPEFNMMzBzExL0X6mPZDEqpCCNFFWmvq7G62toWpxUyD1U7ulvuY7yvmrcA8Ph+ygtWzTiMuzmjN6wsEaGzxoDWMz0tmTE4yKfGWCJ+JCBcJVSGEOIm2O9Mvy6xU21zEW8wcbmhl4/YSfu56iFmmvXycdC5JM1dyWvY0UKpDmE7IS2F8XvLRu1bRf8nfsBBCnIDWmiqbi21lVuodbhJizBysb+GNHVVkOPbxRNxvyDDbODLsUtTEG3ElDQ0O2OBGKSNMx+VKmA4k8jcthBCdqG12sflwE/V2NwkWE3ur7by9q5p6h4crU3bw7wkPo8yxlI2+nrIx12IzpWCzOYmzmGTAhgFMQlUIIdppcBiPecubnMTGKHZVNvPOrmqsTi8jMxL5Xf5aTj/yKM7EfA6PvY69+Rdic8eRkgALRmcyJD1R+pcOYF0KVaXUq11YrVFrveLUyhFCiN7X1gBpV6WNsiYnZqXYXm7lnd012F0+xuUks2p+HpeUPUjekTewDS5i18jrOJB+BtmJSZxVmHa0j6kY2Lp6pzoBuP4bPlfA/556OUII0XsCAU1Vs4ttZU00ODxGmJZ9HaaT8lI4f0oeRcktTF1/I8lNu6nIXsS2ETeQOGIO5xbIPKaio66G6r1a6w+/aQWl1M9DUI8QQoRdIKCpsLay9YgNa6uHGJPii8NNvF9Si8NthOkFU/MZnZ1ESsM2pr73I2K8zewt/C72mT/mtLETZeo10akuharW+rlQrCOEEJGk/3Yebl+Ad2b/BZvTCxrWH6ineE8dbl+AKQWpLJuce3Ri8JyDLzJxy7/ijUmiYsrN5J3xI8alZ0f4LEQ06+o71b9hzETUGa21/kHoShJCiNBquzNNcHjwBTSNDjcf7q3nk/31+LXuMJcpgM/rYfgXDzH+8P/hShmBPu0nDJ15JVgSInwmItp19fHv650sGwLcDkibcSFEVDLC1MnWI1asTg8TvYN50jGHN94oQSnFglEZnFuUS3ayMfqR2+fH0VTL4u2ryWn4DP+Q04hftBpGLgKT/FMnTq6rj39faPteKTUS+BfgDOAh4C/hKU0IIXqmrQHSF4ebaGrx0Orxsa60jn+rvZY45ePM8dl8a2Iu6cH3oi6vH6vTQ1brPi754jYsrdUw+TLMC38G2eMifDaiL+lyP1Wl1HjgPmA68GvgJq21L1yFCSFEdwUCmkqbk21HrDS2eHC4fKwtrWXLkSbiYkxcm7yZa5K/4KvZfwLaJgX3kRBr4uzAp+R8egfKbIHTfgLzfgRJ8v5UdE9X36k+D8wE/gvjka8fSGmbqkhr3RiuAoUQ4mT8AU1FUytby6w0O71YW72sLa1lW5mVBIuZ8yfncfaEHBZt+C0A211e7C4fqYkWFo5KY+jW32Da8AgMHm6E6bSrIC4psicl+qSu3qnOxmio9DPgp8Flbb2cNTAyxHUJIcRJ+QOaIw0tbC230uLy0djiYW1JLdsrbCTGmrlwaj5njc9mUFwMWmsON7aC1sRZzMwZmUGexYnphWvh4AcwZB4suA3GnA1mmUVG9ExX36kOD3MdQgjRZT5/gCONrWwrs9Li9lFjc/N+aQ2l1XaS4mK4eFo+Z47PJjE2hoDWNLZ4yDvyKhP9e7DgZer756BmrYQtT0BzJUz+Lpx+B2RPBJksXJyCrj7+zdVaV5/qOkIIcSp8/gCHG1rZWtZEq9tPhdXJe7trOFjfQmqChctmFXLGmCziLWYCWtPQ4sbj08y2v8+4kl+i8Bo7spXB2l9ATALM/wnM+yEk50T25ES/0NXHv28CM0KwjhBCdFv7MG1x+zjS0Mq7u2soa3KSMSiWq+YO5fTRmVjMJgIBTb3DjdcfYHR2EhPzUkl97GHwOY/fcUwcnPEzeX8qQqaroTpVKdUc/L79u1TaLWtGCCFCyOsPcLihhW1lVhwuH1/Vt/Du7hqqbC5yUuJYuWA4c0ekE2My4QsEqLW7CGgYl5PMuLxkUuKD70Zt5Z0fwGWTQBUh1dV3qtLrWQjRazy+AIfqW9hebsXh9rG/1sG7u2uotbvJT4tn1cKRzBo2GJNJ4fUHqLG7UMDE/BTGZCczKO6Yf9oGZUFL7fEHSi3slfMRA0e35lNVRh+aq4ARWutfKqWGAHla68/CUp0QYkBx+/wcrHOwvdyG0+2ntMbO+yU11Ds8DE1P5IeLRjF9aBompXD7/DQ6vMSaFdOGpDEqq5NJwbWGz/8MLXXHH8ySAGfd3zsnJgaM7k5S/gcgAJwJ/BJwYEz5NjvEdQkhBhCX18/+Wgc7Kmy4vD52Vxph2tTqZUTmIK6YM5QpBakopXB5/TS1ekiwmJk7PJ1hmYnExXTyMM3TAq/eAjv/CVkT+Dx+HoVHXiGHRmpVFmWTVzN7ymW9f7KiX+tuqM7VWs9QSm0F0Fo3KaVk/iMhRI+0enzsq7Gzq7KZVq+fneU21pbWYnf5GJOdxIrThjMxLwWlVHD0Iy+D4mNYMDqToemJxJhNne+4bi+suRoa9sKYpbyc/UPu+ciF03vO0VUSPjfz4JAKLp5e0EtnKwaC7oaqVyllJthISSmVhXHn2iNKqTTgz0BRcJ/XAXuANcBw4BBwmda6qafHEEJEnxa3j9LqZkqr7Dg9PraWWflgTx2tHj+T8lJYPiWPsTnG9GsOt49mp5e0xFgWjc2iYHAiZtM39CXd9RK8fLPR33TOjTD/Zn79x/04vR3/qXJ6/fz6nT0SqiKkuhuqjwAvAdlKqQeA72KMB9xT/w28rbX+bvCONxFjsP61WuuHlFJ3A3cDd53CMYQQveDyxzZgtTpZvPjE67SFaUmVHYfby+ZDTXy8rx63L8D0IWmcNzmPEZmDALAHhxJMHxTLWRNzyEuJx/RNYerzwLv3wWePQdowmHeTMdxgfCqV1h2dblJp7aSbjRCnoFuhqrV+Wim1BTgLoxvNxVrrkp4cWCmVijHTzYrgvj2ARyl1EbA4uNqTQDESqkL0aQ63jz3BMG12eth4sJFPDzQQ0Jq5I4zp1wrSEtBaY3N6afH4yE6OY96oDHJT4lEnG+XIWgbPXwsVW2DYAmNAh3bDDeanJVDRSYDmp8n8qCK0lNYnmnu8k5WVmgfs0lrbgz+nABO01pu6fWClpgGPA7uBqcAW4FagQmudFlxHAU1tPx+z/SpgFUBOTs7MZ599trsl9HkOh4OkJOljFwpyLU/dg5uc+P1+7jvt6+sY0EYjJJfXT51L80FZgM9qApiAubkmzh4aQ2aCEZi+gCagNbFmEwmxZmK+6a60nfSGLUwo+S0q4KN05Erqc5cYgzq082mllyd2evC0ewIca4IVRbGclh+d4/zK72RohOM6LlmyZIvWelZnn3U3VLcCM3RwI6WUCdiste72SEpKqVnARmCB1nqTUuq/MQaQ+En7EFVKNWmtB3/TvmbNmqU3b97c3RL6vOLiYhZ/07M20WVyLU+d8fjXyjt3LaPZ5WVPlZ09NXYaWtx8ur+Bzw41EmNSLByTxbmTjLlMAwFNk9ODxxdgWMYgJuWnkJEUd/KDAfh9UPyf8PF/QXIezF4Fs1ZAYnqnq7+8tYIHnvuQep1Cfloiq5eOi+r3qfI7GRrhuI5KqROGanffqSrdLoW11gGlVHf30aYcKG93l/tPjPenNUqpPK11lVIqD+ikx7YQIlqt31/PwToHDQ4Pn+yvZ8vhJiwxJs6ZkMO3JuaQlhiLLxCgzuHC74cxuUmMz0khNbEbd4z2avjndXB4PRTMhvk/hnHngiX+hJtcPL2AZz4bwUhgzY3zT/1EhehEdwPxoFLqFuDR4M8/Ag725MBa62qlVJlSapzWeg/Ge9rdwa9rgYeCf77Sk/0LIXqPtdWDzenFF9Cs31/PJ/vr2V5uIy7GxLKiXM6ZmENyvAWfP0BtswutYEJuCmNzk0k6dvSjkzlYDC9cbwwxOOVyOO0WyJkks8uIqNDdUL0JowXwfRhdYNYSfK/ZQz8Bng62/D0IrARMwHNKqR8AhwHpnS1ElLK2ethV2cyBWju2Vi/WFs0fig8wKNbMRVPzWTI+m6S4GGMowWYXSimKClMZk51MQmw3Rz8N+OHD/wcf/soYdnDhz2DWdZCUFZ6TE6IHuhyqwf6pD2utvxeqg2uttwGdPZc+K1THEEKEXr3Dza5KG4frWyhrdPLql5XU2N0AJMaa+e7MQhaOycLt81PV7CLWrJg+NI2RnQ0l2BXNVfDCdXD4U8ifCfN/COPPN4YaFCKKdDlUtdZ+pdQwpVRssPuLEGIA0VpTZ3ezvdxGhbWVskYnH+6tY1+to8N6rR4///jsCC1uH3NHZjB3eDrDMwcRG3OC0Y9OZt/78NIqcNuNx73zb4GciWDq4f6ECKNuv1MF1iulXgVa2hZqrX8b0qqEEFFDa01Ns5ttZU1UN7s4UNvCh3trOdLoZHCihcRYM60ef4dtvH7NR/vqefA7U7CcaCjBk/F5YN0v4dNHjNa9Z9wJM1fI414R1bobqgeCXyYgOfTlCCGiRSCgqWp28WWZlZpmF3uq7XxQWkuN3U1OShwr5g9n3sh0bnr6i063r7O7ex6oTYfg+ZVQ+QUMmQtzf3TS1r1dIa1+Rbh1d0SlnwMopZKCPzu+eQshRF/j8weosDrZVmal3u6mpMrOB3tqaWgxpl+7adFIZgwx5jJ1uHykJliwOb3H7afHoxXtfAFevRUCPph+Dcy/GbLGS+te0Sd0dz7VIuApID34cz3wfa31rjDUJoToRT5/gMMNrWwta8LW6mNHhTHIvc3pZWTmIK6aO5TJBakANLt8tHh8ZCTFctvZY/jV26UdBqxPsJhZvXRc9wrwtMBbd8HWpyBtKMy6HmZcc8LBHISIRt19/Ps4cIfW+gMApdRi4E/AaSGuSwjRS9qHaWOLh+1lNor31uFw+xifm8z1p49gfK7xtqep1YvL5yc/NYGFYzLJSo5DKcXgxFju/Od2PP4ABWkJ3R+tqHonPL8CGvbDiMXG3enIxRAjM0uKvqW7oTqoLVABtNbFSqlBIa5JCNELvP4Ahxta2FZmpcHhZusRGx/urcPp9TOlMJXlk/MYlZVEQGsaWzx4/CceSvDi6QWMeetyfD4fU+/e2PUitIZNj8F790FMAsxZBXNvgoyRIT5bIXpHT0ZU+leMR8AAV9PDEZWEEJHh8QU4VN/CtvImGh0eth6x8uG+OlzeADOHDmb5lDyGpicSCGjqHW68/gCjspOYmJdCWmII7xxb6uHlH8K+d413prNvgCmXQnxq6I4hRC/rUqgqpZ7SWl8DfIwxefiLwY8+wphYXAgR5VxePwfrHOyosNHU6mXL4UY+2luPxxdg5rDBnD8lj8LBifgDmnqHC19wXN4JeSmkxId4Jpf9a+GlG8HZBBMuNKZqK5wJph4MDCFEFOnqnepMpVQ+xli8SzDmUm0bWF+a5AkRxVxePwfqHGwvt2FtdfPFYSsf7TPCdPbwdJZPyaMgLQF/QBuD3AdgfG4y43KTSQ51mHpd8P6/w6ZHISkXFv7UGGowOTe0xxEiQroaqn/EGOd3JNB+jrW2cJUXIEJEmRa3j321dnZXNtPQ4uHzr4yJwf1aM2d4Ossn55GfloAvEKDW7kJrmJDXw0Huu6K2xJhZpnY3DJ0Pc26CcUtlqEHRr3Tpvxyt9SPAI0qpR7XWPwxzTUKIU9B+LtN6h5uNBxv47KtGlFIsGJXB0km55KTEHw1TgEn5KYzJSSYxNgxhGgjAZ4/Be/eDOc7oKjP3JsgcLX1PRb/T3cEfJFCFiIDLH9sAfPOIQNZWDyVVzeyvdVBnd/PpgQZjLlOziTPHZ/OticbE4MeG6diclO7PGNOJSXmpWK3WjgubK43GSAeLjcZIs34AUy6DhLRTPp4Q0SgM/1sqhOhNDQ43uyqbOdzQQpXNxSf76tleYSPeYmLZ5FzOmWDMZer1B6i2uzAB43NTGJ8XpjvTNrtegtduBa8TJl4C834EBTPALP/siP5LfruF6IOOnTGmosnJR/vqKa22G3OZTsvnzHHZDIqLwe31U21zYYlRzBhyCtOvfZPtz0H556T63fDbiZBSCOWbIHWIMYn49GsgOSe0xxQiCkmoCtGHaK2ptbvZdsSYMeZIg5N1e2r5qr6F1AQLl80q5IwxWcRbzLi8fqpsThIsZuaOTGdYxilMv/ZNtj8Hr90CfrfRFaC5wvjKnghn/xxGLoKYuJPtRYh+QUJViD7C6w/w7q5qqppdHKxt4YM9tZQ1OckYFMtVc4dy+uhMLGYTTo+fKpuLhFgTp43KZGhGYs9ni+mKtb8wHvEey9kEY78VvuMKEYUkVIWIYlprqptdxjCBPj/r9zfwfmkNlVYXuSnxrFwwnLkj0okxmWhx+6h3uBkUH8OC0RkMTU8kJpxh2sZW3vlye3X4jy1ElJFQFSIKtc1luq2sibpmN81OL02tXv6+8TB5qfGsWjiSWcO+nn6t2e1mcGIsi8dlkZ+WiNnUC11V/F746Nd8PQ7MMVILw1+DEFFGQlWIKNJ+LtNGh4e9NXbe2FFJs8sPQFJcDOcV5TJnRDp2l5dml4/M5FjOHplDbko8pt4IU4CaXfDiKqjZCWnDwF5phGwbSwKcdX/v1CJEFJFQFSIKuH1+jjS08mW5FbvLR0lVM2tLjInB28ekw+3j7xsPY3V6OWdiDvNHZZKTYky/1iv8Plj/Oyh+EGLiYca1xswyNbvg1Z+g/W5U6hAjUKdc1js1CRFFJFSFiKBWj4/9tQ52VTbT6vGzq8LG2tLaoxODe3wB7G5fh228fs36/Q385tKpvRemAHV7jEHwK7dCThHMXAmTvwMJgyG3CL74OzarlbTb1/deTUJEGQlVMeBd/tgGrFYnixf33jEdbh+lVc2UVttxeX18WWbjgz1fTwx+w8IRjMtJ5oantnS6fU2zq3fvTjf8D3zwn2CywLQrjXF7c4tkVhkhjiGhKkQvanH7KK1upqTKjsPl5fNDTXyyvx63L8CUglTOm5zH6OwktNY0tXpJTbBgc3qP209+Wi8NQt/+7jR7Esy8FiZfConpvXN8IfoYCVUheoHN6WVvtTHIvcPlZcPBRj7ZX4/Wmjkj0jl3Ui6FgxPRWtPY4sHt8zM0I5HVS8fywBulOL3+o/tKsJhZvXRceAv2++DT/zbenZosMPVK491p3hS5OxXiG0ioChFGDQ43u6uaOVzfSqvHx4aDDXy8rx4NnD46k2VFuWQmxRHQmgaHG48/wLCMQUwuSGXwoFgAkuIs3PnP7Xj8AQrSEli9dBwXTy8IX9HVO+HlH0H1l5AzCaZfC1Pk7lSIrpBQFSLEtNbUOdzsLLdRbnVidxphuvFgA1rDaaMyWD4lzwjTgKbe4cbn1wzPTGRS/tdh2ubi6QU889kR4JtnqTllPg98/F/w8W+Mlr3TrjbuTuXdqRBdJqEqRIgEApoau4svy6zUNrtpbPXw0d46th6xEmNWnD46k6WTcslKbgtTFz4/jMlNYnxuCqkJlsgVX77FmKKtfg/kTTMGwC/6dvfuTle+wbbiYhaHrUghop+EqhCnKBDQVFidfFlmpbHFQ2OLh3WltWyvsJEYa+a8yXmcNT6blARLhzvTcbnJjM9LJjk+gmHqaYF1D8CmRyEuBWZdZ8x5mj1B7k6F6AEJVSF6yOcPUN7UyrYyG80uLw0OD2tLathZ2cygWDOXTC/gzHHZJMSa8bfdmQY0Y3KSmZiXEtkwBdi/Fl67DWxHoHCOMZDDxAsgPjWydQnRh0moigHt5a0VbD1ixeMPsOChdV1qBOTxBTjc0MKX5VZa3X5q7W7eL6mhtNpOUlwM35lRwJJx2cRbzHj9AWqbXaBgXG4yY3MifGcK0NoIb98D25+FQVnG5OEzV0LGaDD1wgD8QvRjEqpiwHp5awX3vLgDjz8AQIXVyT0v7gDoNFhdXj8H6xzsqLDh8Qaosjl5Z3cNB+q+nst00Zgs4oJhWt3swmxSTBmSxqisJBJiI/w4VWvY8Ty8dRe4rDByMcy8DkafBXFJka1NiH4i4qGqlDIDm4EKrfX5SqkRwLNABrAFuEZr7YlkjaJ/+vU7ezr0/wRwev38+p09HULV6fGzr9bOzopm/IEA5U1O3tpZzZHGVtIHxXLVnKGcPsaYy9TnDxwd7WjakDRGZycRb4mCd5ONX8Hrt8PBDyB1CMy+AaZfBWlDoTeHOhSin4t4qAK3AiVASvDn/wc8rLV+Vin1R+AHwKORKk70X5XWTibWbrfc4faxt9pOSXUzAb/mYH0L7+yuptLqIjs5jhWnDWfeiHRiOoQpTC5MZWxOckjDtMddafxe2PgHY4hBgAkXwuzrYchcsMSHrD4hhCGioaqUKgSWAw8AdyhjMNMzgSuDqzwJ/DsSqiIM8tMSqOgkWHNT49n0VQP7qh1oApRU2Xl3dw31Dg/5afHccPoIZg1Px2xSxmNeuwsTKixhetTflht/rnyj69sc2QSv3Qp1JZA1AaZfbQwxmJwT+vqEEAAorU8wwXBvHFypfwIPAsnAz4AVwEat9ejg50OAt7TWRZ1suwpYBZCTkzPz2Wef7a2yo4bD4SApKbrfhT24yQite+b20li13fBppZcndnrwBL5eZjHBZWNMTMk0salGs/aIH5sHhiYrlg4zU5RhwqQUWmt8AeO/ncRYM3Ex5rA+RZ229V4Atk1/4KTrxnjtjDz4d/Kr3sUVm86+oVfSkHO6McdpmPWF38m+Qq5laITjOi5ZsmSL1npWZ59F7E5VKXU+UKu13qKUWtzd7bXWjwOPA8yaNUsv7s0pRqJEcXEx0X7ej+7ZAMDixWEcCaiHFgMTvijnzhe24/VrUhMsnD0hG6eG/9hSg93lZ2xOEtdNzmNiXgpKKTy+AI2tbmJMJqYUpjIyq5femX6VZtT8TX/fWsOXz8K794KzCYadTvyMa5g89lxISAt/jfSN38m+Qq5laPT2dYzk498FwIVKqfOAeIx3qv8NpCmlYrTWPqAQqIhgjaKfCgQ01c0uEmLNFKQlgLeVKSMyeWtnNa0eP5PyU1g+OY+xOckAuL1+mlq9WGIUM4cNZmRWEnExUdAAqU3dHqMh0uH1kDrUGMRh2lUweLg0RBKiF0UsVLXW9wD3AP+/vTuPj6s+7z3+ebRvtuRVlmzjDSNjG2MTs8VATICYhKSQDShJmpL0ktuGJs1NnELS0ty0NNySpU1vyoUkJbRNApQQIJBgCCBIgiFAbOQN22BjW7LlfWTJWmfmuX+ckS3bMsj2GZ058vf9euklzZnRzKOj5avf7/wWMi3VL7n7x8zsv4GPEIwA/iTwcFQ1ytCTSjtbE8GCDXvbu8GdfR097G2HN1/dxtwJVbxvzjimjg66izozYVpalMd500ZyyshyigpyaC5n93549vZgv9P8Ipj5ITj7UzDhbA1EEolALoz+PdxfA/ea2T8Ay4AfRlyPDAHJVJote9p5tbGFfR09pN35zfpd/Gb9LrpTaSoK4YuLZjJxZBlwMEzLi/NZcOooThlZRkF+DoWpO6z5BTz+17BvK9TOg7l/DLM+DOWjo65O5KSVE6Hq7vVAfebjDcA5UdYjQ0ff1Y86utMkU2mefm0Hz2/YDQ7nTR1JU6KDgmQHE0eW0dGdoqWzh7KifC44dRQTcy1MAXa/Ab9cDG88BcPGwfk3Bgvgjz5NKyKJRCwnQlUkbH1XP+pJOql0ml+v2cHzb+zGDN41fQyLZlUzqqKYf1ryGukkbGvpoLykgAtOHcWEETkUpg33Q+NLkOqCb0wIunzzi6DufcG100kLoKgs6ipFBIWqZNHxrKt7olo7e1i/vY3XmveRduhJpfn16u28sGEPZrCwbgzvnT2OqrJgz9L9XUm6k2nygYtOG8OEEWXk5+XQwJ6G++EXnwsCFaCrFTCYeSVc9veacyqSYxSqkhXHuq7uidq7v5vV2/axcVcb+XnG/q4kS1Zt55VNeynMzzsiTNs6k+zr6mFEWRH/+elzeKPhJSaNKg+9rhP2xN9Cz+ELVHgwyleBKpJzFKqSFQNdV/dE7WrrYmVTC1v2tFOYZyTae3h8VTMrm/ZRUpjH5bPHcdnp1QzPbADe1pVkX2cPoyqKuHRqNeOGl5CXZ7wRWkUh6dgbLC3Y1tz//S2aaSaSixSqkhVvt67uiXB3drZ2saKphaZEByUFeTS3dPL4qmbe2LmfYSUFXDW3lovrxlJeHPyIt3cnSXQELdNLTq+mtrIEy8X5m6kk/OFH8PQ/QEciuHaa6mc/icoJFAeiNQAAIABJREFUg16aiLw9hapkxdHW1a2tOv6l8tJpZ3trJ69uSbCjtYvSwny2JTp4tGEbW/Z2MLqiiOvOOYUFp446sDDD/q4kLZ09VJYWcnHdGMZXlZGXS9dM+9rwbLAt2841MGJqsPB9cSXU33poF3BhKVxyS3R1ishRKVQlKxYvquPmB1cc0gVcWpjP4kV1x/xcqbTTtDeYY7q3vZvyony27u3gFw3baEp0UD2smE8tmMy5U0YdGGTU1hl0844oL8r9MN39BjzxN7D2l1A6Mpgec9YnoWYOFBQH104fvjEYrFQ5MQjUOVdHXbWI9EOhKlnRe930yw800J1KM76q9JhH/3Yn02zevZ/ljQk6u1OUFxXQuKeDRxu2srWlk3GVJfzZBVM4Z/LIA4G5r6OHtu4koyqKuGRqNTWZa6Y5qbMlWA3pxTvA8mD6e2Dux2HaQiipPPi4OVfDK/cEHx/LLjUiMugUqpI1V80bz09/vxk4tv1Au5LBHNOGxmCO6fCSAtbsbuWxFdtobumktrKEGy6cyvxJI8jLC3aMaenoYX93knHDS3jnqaOpHl6cm9dM4eB102f+Edr3BKshzbkaZn0wWMxBRGJLoSo5o7MnxcZdmTBNOcOLC1izNcFjK7exs7WL8VWl/M+LpnLWpBHkZQIzCNMeaivLuPC00YwdlsPr3brD67+GJV+BXetgxJSgm/fM62DUNK2GJDIEKFQlcvu7kry+o5VVW1tJe5rhJYWsaNzDr1Y2s3t/N6eMLOOzC6dx5sSqQ8O0K0lNVUnuhynAtoZgzunGeigbHYTpvE8cvG4qIkOCQlUi09Lew5rmFl7fvp+8PKgoKuD5DQmWrGpmb3sPU0eX87FzT+GM8ZUHunJbO3to7UpSPayYC6ePZsywHO7mBUhsCabHNNwXjNqtuwLmfRwmXwAlw6OuTkRCplCVQbe7rYtVW/exafd+igryGFZcwHOv7+TJ1dvZ15nktOoKrn/nFE6vGXYgMHtH844eVsz503L8mikEizf85jvBICRPw+QLYfZHYMb7oGJM1NWJSJYoVGVQuDs727pY2dhCY6KDkoJ8yooKeOa1HTy9dgft3Slm1gQbg9eNG3bg83rnmY4sL+LSmdXU5OqiDb2SXfD778Nz/wSd+6B2brAd2+wPwvDxx79huEb9isSCQlWy6t4bzqN5XydLVjWzY18XZUX5lBfm88Tq7dSv20lXMs1Zp1Txvtk1TB59cO3dtq4k+zp6qMqsgJTTU2MA0ilY8d/w1N/DvkYYNR3O+QyccTWMmgp5+VFXKCKDQKEqWdGTStO0t52Gxn0k2rsZVlJAWVE+j69q5rl1u+hJpzln8kiuOKPmkFWWWjt7aO1MMrK8iHefPpbaytLsh+ndVzA3kYCFvzv2z3WHdUvg118LVkIaXgtn3wBnfQLGzICCotDLFZHcpVCVULV3J9mwcz+rtrbQnUxTWVpIcUEej7y6ld+s30XanfOmjuKKM2qoHn5wxG7vog1jKoo5b9ooxg3P8W5egE3PB2G65cVgRO/c64LFG2rnaX9TkZOUQlVC0daVZG3zPl7b1ooBI8qL2LO/m/9+uZHnN+wG4J1TR/HeM8YdMv2ld9GG6mHFLJg+mrG5PpoXYNur8NTXgzmnxcNh5lUw72Mw8TyN6BU5ySlU5YS0tPfw2vZ9rG9uIz8fRlcUs3lPOz/7w0b+sHkv+XnGRdNHc/mscYyqODgfs7dlWlNZEo+pMQA71warIK1+CArL4LTLYc41MHUhlI2MujoRyQEKVTkuB6fFtFNUYIypKGLdjjbueX4TrzW3UlqYz3tnj+OS06upzOxlCofOM70gLmG6ZwPU/x9YcT/kFcLUi+GMj8D0RZoeIyKHUKjKgPXuY9rQ2MLWlmBazNhhRazcuo87GzawYdd+KksL+eg7JnDR9DGUFh0c8dobpmOHFfPOU2PSzZvYDM99E5b/GDCYtABmXgkz3h+s0Zvr9YvIoFOoyttyd5r3HdzHtLyogOqKYv6wJcEvVwR7mY4qL+Jj557CBaeOpjD/4Bq2vd28Y+OyaANASyM8dzss+6/g9vj5wWL3p38gGN2b6/WLSGQUqnJUvfuYNjS2sGd/MC1m7LBiXty4h1+taKZ5Xyfjhpdw/YLJnDtlJAV9FoSPzQCkhvuh8SUqU13wrRkw+jTY/Dyk0zBhfhCkM68KFm7Qgvci8jYUqnKE7mSaTbv382pjgo7uFMNLCqkeXsILG3fz6Kvb2NnWxcQRpXzmoqm845QRB+aR9m7B1t6TiscApIb74Refg1QXBtC6LXgbMQXmfyro6q2cqDAVkQFTqMoBnT3BPqYrmlroTjojywoZVlzI79/cw6OvbmV7axenjCzjxotP5cwJBxe5d3cSHT109KSYUFXKRRMqc3/XGIAnb4GejiOPJzvh/BsVpiJyzBSqQnt3kvXb21i1dR/uzsjyItLu/O713Tyxupldbd1MHFHKZxdOY+7EqgNhmnYn0d5DVzLFxJFlnDG+8pBpMzlr1+vwm28GrdL+tDYrUEXkuChUT2KtnT2sbW5lbXMrZjCyrIiuZJrHVzbz1Gs7aOtKMm1MOdfMn3jIXqZpd/bu76Y7lWby6HJm11YyojwGy/E1rwxG865+CPILIL8YUl1HPq5ywuDXJiJDgkL1JJRo72bNtn28sbONgjxjdEUx7d1JHmnYyjOv7aSjJ8WcCZW8d9Y4plcf3DHG3dnb3kNnMsW0MRXMqh1OVVkMwrTx5WA077rHgyCdcmEwLcaBp/7u0C7gwlK45JbIShWReFOonkR2tnaxamsLW/a0U5Sfx9hhJbR1Jvn5siaeWbuD7mSad0wawRVzapg44tC1a3tH804aVcaZE6pyP0zd4Y2n4Tffhk2/DVZAmnZJMPjotEVQUR1MjSkbAQ/fiKe6sMqJQaDOuTrq6kUkphSqQ1w6HcwxXdGYYHtrF6WF+YwbXkJrZ5KfvdLIM+t20pNMc/bkkVwxp4bxfXaM6R3N29GTYlxlCRedNoYxw3L8mmkqCWsegd9+B5obgrV5664IpsVMu/jIFZDmXA2v3ENLIkHVF45jlxoRkT4UqkNUTyrNlj3BHNPWzh4qiguorSxlb3s397/cyLPrdh7Yfu39c2qoqTwYpn2vmU4YUcas8cMZU5HDU2MAutuDlY+e/26wElLZaJj9YZj9ETjlPK3NKyKDIrJQNbOJwH8A1QRXt+5y938xs5HAfcBk4E3ganffG1WdcdPZk2LjrmBaTFdPmqrSImoqS9nZ2sXPl73J82/sJu3OuVNGccWcGsb12X4t7c7e9m66k2mmjang9JrhuT8Aaf8u+P334fd3QsfeYF7pWX8aBGrtmVBSGXWFInISibKlmgS+6O5/MLNhwCtm9iTwp8BT7n6bmd0E3AT8dYR1xkIwLaaVVVtbSXuakWXFjCzLo3FvO796sZmX3txDnhkLTg12jOnbjdsbpj1JZ9rYcmbWVFJZVvgWr5YDdr8Bz/8rLP9JMIJ3zOkw92Mw+6Mw5jTtZyoikYgsVN19G7At83Grma0BxgNXAgszD7sHqEeheoRr7lxKItHBO87rYd321mAf08y0mIL8PNbvaOVXK5ppaGqhuCCPS0+v5j0zqw8ZYNQ7mrcrmWLqmApm12YhTO++Inh//WMn/lzusOl3QZiuWwJ5+VAzN9iCbeZVMHIy5Of4PwMiMqTlxDVVM5sMzANeBKozgQvQTNA9LIdJpp20Ow8v23pgH9M8g9eaW/lFw1bWbW+joriAK+fWcnHdWCqKD36r+y4neMrIYDRvTnfzJrth1c9h6b9C8wooLA+2X6u7HOrep3V5RSRnmLtHW4BZBfAscKu7P2hmCXev6nP/Xncf0c/n3QDcAFBdXf2Oe++9d9BqjlIy7XR0p/jnZd3gaT5/Vgm4s3pPmsc3pXhzn1NZBJdMzOedtfkU59sRn592pyg/j9KifArysjv4aO6yrwKwfN6tx/y5hd0t1G5dQu3WxyjuTrC/pIbGcZexfey7SJdUQV54/xO2tbVRUVER2vOdrHQew6NzGY5snMeLL774FXef3999kbZUzawQ+BnwY3d/MHN4u5nVuPs2M6sBdvT3ue5+F3AXwPz5833hwoWDUXIk0mln275Olm/Zy+62bsorC8gveYNURxtvFkzi4VebeGPnfkaVF/Hxc8ex4LDt1yJdTnBj8P/RMX1/ti4PBh6teABS3cHOMVOvoXz2h6kbezp1JcNDL7O+vv7YapR+6TyGR+cyHIN9HqMc/WvAD4E17v7tPnc9AnwSuC3z/uEIyssJqbSzNdHOss0ttHR0M6y4kNrKUtyd/V1Jdu+Hb/96HSPLivjEeZNYcOqoQ7Zfi9VygsnuYH7pi/8PGl+C/CKonQenXhYs2DBiMhTkaO0iIhlRtlQXAJ8AVpjZ8syxrxCE6f1m9mlgE3DSLW/TnUyzec9+GhpbaOtKUllSSE1lKcl0mqUbdvPEqmaaEp3kG1x3zilcOP3IlmnvaN6pY8uZWZPDywnu2wov3w0v/zu074KyUcESgnXvhWnvhopxul4qIrER5ejf3wJHu6B3yWDWkivau5O8vqON1Vv30ZN2RpQGLdOeVJqnX9vBr1ZuY297D7WVJVQPL6aCLt49Y+yBzz9iakxtJZWlOTga1h02Pgsv/RBeeww8DWPqYNaHYNYHYdwszS8VkVjKidG/J7uWjh7WNbeydnsreQYjyooozM87Ikynj63gT86fTFtnD/cs3cT2NHz5Zw18cG4tdeOGB4s25FKYNtwfdOWmuuA7s+HC/xUsXv/SD2DPhmAU76QFcOqlMOMKqDpFXbwiEmsK1Qjtbuti9dZ9vLm7ncJ8Y2xFMXl5RmdPimfWNvPk6u0HwvRTC6YwY9wwXty4h/98YTPJdDBqe8/+bv7jhU186oLJ/Pm7Ts2dbt6G++EXnzu4tVrLFnj0C8HHVZPgzGvh9D+CCWdD+ZhgcXsRkZhTqA4yd2dnaxcNjS1sa+mguCCf6uHF5JnR2tnD06/t4KnXdtDenaKuehjXv3MKp9cMO7Du7oPLmuhOpQ95zp6U8+irzdz83plRfEn9e/izwcjdwxUPhz++Nxh4pFWPRGSIUagOkt7dYhoaE+xo7aK8qIBxw0swMxLt3SxZvZ1n1+2kO5lm3sQqLp89jmljDp1blXZnz/5+ggrYmujo9/igSqdhYz288qP+AxWgqxWqcyj8RURCpFDNslTaadrbzvItLSQ6uhmW2S0Ggu7fx1c185v1u0i7c86Ukbxvdg21fbZfg0MXuh9dUcSutiMD6/DPGVR734RlP4bl/xWM5i0sCzYD7+367atywqCXJyIyWBSqWdKVTLF5dzuvNibo6E5RmRnJC9C8r5NfrtjGixv2gMGCaaO4fPY4xg4rOeQ53J1ERw+dPQfX5h1WUsjND66goyd14HGlhfksXlQ3qF8f3fth9SOw7D+D9XgxGHUqzP04nH4FtDTBk38bDEzqVVgabAIuIjJEKVRD1tGdYv2OVlZt3Ucy7YwsLaSqNBg81LS3g0dXbOXlN/dSkG+8q24Ml88ax8jDFmTou9D9hMzavL2PuWreeAC+/EAD3ak046tKWbyo7sDxrEqngwBd/hNY/RD0tAfzSk+9LFiLd/p7oGoiFGb+OSiphIdvDFqslRODQJ1z0k07FpGTiEI1JPu7kqxtbuW15n3gMKK86MCCDE17O/hFw1Ze2bSXooI8Fs0ax2Uzq4+Y9tJ3BaRJo8qZPb7yiMCFIFin/+oakskkZ970Qva/uF3r4dV74dWfBN27+cUw7gyY9M5gBO+oaf1vAj7nanjlnuDjMHapERHJcQrVE7SvM5hj+lpzK/l5MLK86MBSgVv2tPPYim28smkvxYV5vO+MGi47vZqKkkNPe98wnTqmglm1ObACUmszrPwZvPrTYGcYDEZPhzOvC1Y7Gv8OGDYu2H5NREQAhepx293WxZpt+9i4az+F+XmMqSgmP7Pjyxs723hsxTYaGlsoLcx/yzBNtAfXTKeMKeeM8ZXRhmlHAtb8AhruC7p5PR1sq1Z3BUy9KOjmHT7+YPeuiIgcQqF6DNJpZ3trJyub9rGtpYOSgnyqh5eQZ4a7s7a5lUdXbGXNtlbKi/K5am4t754xlrKiflqm7d10JdNMyXTzRrbQfVcbrHscVvw3vP4UpHugdCRMeVew2tFp74URp0AWdoYRERlqFKoD0JNKs3l3OyuaWmjt7KGsqICazBxTd2dlUwuPrdjG+h1tDC8p4CNnTWBh3RhKCg/tGu2dZ9qTSnPq2ApOj2qh++52eP3JYGu19U9AsjNYlGHiOTDx3GDj75FTgkFIWulIRGTAFKpvoSuZYsPONhoaW+hOphlRVkRNZlqMu7N8S4JHG7by5u52RpQVct05p3DBqaMpKjh0V5W+o3lPHVsRzdq8XW1BgK76+cEgLaqAmjODpQJPe29wzbR8jHaFERE5TgrVflxz51Lau1Ncd+4ppNJpRpYVU1QeBE3anWWbEzy2Yhub97QzuqKIPzlvEu+cNoqC/CPDtKWjh/aeFJNGBVNjwmqZzqqpJJFIvPWDOvbCuiVBkL7xTDC1pagCxs0JBhpNXwRjZ0DF2OwNONKoXxE5iShUj6J3H9PeVmcynealjXv51cptbG3ppHpYMdcvmMy5U0YesjF4r5aOHvZ3JxlfVcrCiVWMrigOr7jM7i+Vvbu/9J3/uW8rrP0lrH44GGyUTgXzRWvnwYT5wWCjMadB+VjI17dfRCRM+qv6FgryjZ5Umt++voslq5rZ1dbN+KpS/scFU5g/eeSB0b693J19nUn2dyepqSzhwtNGH7FK0gnrs/uLQbD7yyM3wppHYe9GaG4IHlc2Ck45H8afDdMvhZHTgq5dBamISNboL+xRpN15as0OlqxqJtHRw9TR5Vx79inMmVBJnh0Zpi0dPXT0pKipLOXC6aMZM6z4wM4yoXrq64cu/QeQ7II1D8PwCUFLdMLZwQpHIyZB+WjNJRURGSQK1X6cufU+1nadz32726mrHsanLwj2Mj08JPteM504oowzJlSG283bV0tTMGK3ZcvRH/PHPw3mkZaN1KhdEZEIKFT78UZ6HJPydvJnl57FjHH9z8/svWY6saqMiydWMirsME12weYXgpG665bA7vWZOwzwIx9fORFq5oRbg4iIHBOFaj/+suSXkEqxo/ojR9y3r6OHtsw104tOG8OYYSGFqTvsfj1YgGHdEti8FJIdYPlQdUowUrd2HqR64IXvBVNiemn3FxGRnKBQPcxDy5q4te1/ssuHM+LBFXxo3njOmzqK1s4eWjuTVA8v5oKwrpm27YSNzwat0Q3PQltzcLx0JFTPCt4mvROqZ8OwGigdEcwhHTsDHr4RT3Vh2v1FRCRnKFT7eGhZU7BXqVcCsGd/N/csfZM97d1ceno1508bTfXwEwjT9j3BNJfXn4Y3nwtapgAFJTBiMkw4B2rPDEbtVp0SjNbtb53dzO4vLYkEVV/43fHVIiIioVOo9nH7krWHbP4N0JNynn99F9/66JnHHqb7d8Gm52FDPbz5W9i1DnDIKwyugU67JGiFnnJ+sH1axRgoqdIgIxGRmFKo9rE10dHv8e37ut4+UN0hsQk2LQ26dDe/EMwbhSBEh4+Hqe+CsacHc0dH18GwsUFXr+aOiogMCfpr3kdtVSlN/QRrbVXpkQ9OdgcLLWz6XRCkjS9B+67gvoLiYM7otHfDmBnBkoAjpwX7j5aNDO4XEZEhR6Hax+JFdfz25//GX3EvtbaLrT6af+ZaLnjPn0NiC2z5fRCiTS/D9tXBNmkQ7PBSOSHY5WXM6cHauiOnBGvqKkRFRE4aCtU+rsr/He8v/AEFqWC6ygTbxW3cQcGv7oae/cGD8gqCFuf4s4LW5+i6YDRu1cRgYFHpCMgf5B1oREQkJyhU+3rq6wcCtVcBKUh1Q937g8FEY06DqkkwvDYI0JKqaLZKu/4xltfXs3DwX1lERI5CodpXS2P/x9NJuOL2IEQL+7m+KiIiAmg36r4qJxz9+PBaBaqIiLwlhWpfl9xyZHBqCUARERkghWpfc66GD3yXbgqDJesrJ8IHvqslAEVEZEByNlTN7HIzW2tmr5vZTYP2wnOuZn3RDFYXnQFfWKlAFRGRAcvJgUpmlg98D7gMaAReMrNH3H31YLz+rJrKwXgZEREZYnK1pXoO8Lq7b3D3buBe4MqIaxIREXlLOdlSBcYDW/rcbgTO7fsAM7sBuAGgurqa+vr60F58biIBwPIQnzMb2traQv26T2Y6l+HQeQyPzmU4Bvs85mqovi13vwu4C2D+/Pm+cOHC8J58YxUAoT5nFtTX1+d8jXGhcxkOncfw6FyGY7DPY652/zYBE/vcnpA5JiIikrNytaX6EjDdzKYQhOm1wHWD9urXPzZoLyUiIkNHToaquyfN7EZgCZAP/Lu7r4q4LBERkbeUk6EK4O6/BH4ZdR0iIiIDlavXVEVERGJHoSoiIhIShaqIiEhIFKoiIiIhUaiKiIiERKEqIiISEoWqiIhISBSqIiIiIVGoioiIhEShKiIiEhJz96hrOGFmthPYFHUdERgN7Iq6iCFC5zIcOo/h0bkMRzbO4yR3H9PfHUMiVE9WZvayu8+Puo6hQOcyHDqP4dG5DMdgn0d1/4qIiIREoSoiIhIShWq83RV1AUOIzmU4dB7Do3MZjkE9j7qmKiIiEhK1VEVEREKiUI0hM5toZs+Y2WozW2Vmn4+6pjgzs3wzW2Zmj0ZdS5yZWZWZPWBmr5nZGjM7P+qa4sjMvpD5vV5pZj81s5Koa4oLM/t3M9thZiv7HBtpZk+a2frM+xHZrEGhGk9J4IvuPhM4D/ismc2MuKY4+zywJuoihoB/AR539xnAmeicHjMzGw98Dpjv7rOBfODaaKuKlR8Blx927CbgKXefDjyVuZ01CtUYcvdt7v6HzMetBH+8xkdbVTyZ2QTgCuAHUdcSZ2ZWCVwE/BDA3bvdPRFtVbFVAJSaWQFQBmyNuJ7YcPfngD2HHb4SuCfz8T3AVdmsQaEac2Y2GZgHvBhtJbH1z8CXgXTUhcTcFGAncHemK/0HZlYedVFx4+5NwDeBzcA2oMXdn4i2qtirdvdtmY+bgepsvphCNcbMrAL4GfBX7r4v6nrixszeD+xw91eirmUIKADOAu5w93nAfrLczTYUZa73XUnwT0otUG5mH4+2qqHDg+kuWZ3yolCNKTMrJAjUH7v7g1HXE1MLgD8yszeBe4F3m9l/RVtSbDUCje7e22PyAEHIyrG5FNjo7jvdvQd4EHhnxDXF3XYzqwHIvN+RzRdTqMaQmRnBtas17v7tqOuJK3e/2d0nuPtkgsEgT7u7WgXHwd2bgS1mVpc5dAmwOsKS4mozcJ6ZlWV+zy9BA75O1CPAJzMffxJ4OJsvplCNpwXAJwhaVsszb++Luig56f0l8GMzawDmAv8YcT2xk2npPwD8AVhB8DdaKysNkJn9FFgK1JlZo5l9GrgNuMzM1hP0BNyW1Rq0opKIiEg41FIVEREJiUJVREQkJApVERGRkChURUREQqJQFRERCYlCVSSHmVlbFp5zbt8pWGb2NTP7Uj+Pm2xmHWa2/Bif/xoze127/sjJSKEqcvKZCwx0XvMb7j73WJ7c3e8D/uyYqxIZAhSqIjFhZovN7CUzazCz/505Njmzd+n3M3twPmFmpZn7zs48drmZ3Z7Zn7MI+DpwTeb4NZmnn2lm9Wa2wcw+d5TXn5zZK/VHZrbOzH5sZpea2e8ye1WeMygnQiSHKVRFYsDM3gNMB84haGm+w8wuytw9Hfieu88CEsCHM8fvBj6TaWmmINiSDbgFuM/d52ZalQAzgEWZ5/+7zNrS/TkV+Fbm8TOA64ALgC8BXwnpyxWJLYWqSDy8J/O2jGAJuxkEYQrBAuy91z1fASabWRUwzN2XZo7/5G2e/zF373L3XQQLjh9te6yN7r7C3dPAKoLNn51gSb3Jx/F1iQwpBVEXICIDYsA33P3OQw4G++l29TmUAkqP4/kPf46j/W3o+7h0n9vpt/gckZOGWqoi8bAE+FRmD13MbLyZjT3ag909AbSa2bmZQ9f2ubsVGJa1SkVOYgpVkRhw9ycIunCXmtkKgp1M3i4YPw18PzMlphxoyRx/hmBgUt+BSiISAu1SIzJEmVmFu7dlPr4JqHH3zx/D508GHnX32cfx2guBL7n7+4/1c0XiTC1VkaHrikxrdCVwIfAPx/j5KaDyeBZ/AP4N2HuMrycSe2qpioiIhEQtVRERkZAoVEVEREKiUBUREQmJQlVERCQkClUREZGQKFRFRERColAVEREJiUJVREQkJENiV4nRo0f75MmToy5j0O3fv5/y8vKoyxgSdC7DofMYHp3LcGTjPL7yyiu73H1Mf/cNiVCdPHkyL7/8ctRlDLr6+noWLlwYdRlDgs5lOHQew6NzGY5snEcz23S0+9T9KyIiEhKFqoiISEgUqiIiIiFRqIqIiIREoSoiIhIShapk1TV3LuWaO5dGXYaIyKBQqIqIiIREoSoiIhIShaqIiEhIFKoiIiIhUaiKiIiERKEqIiISEoWqiIhISBSqIiIiIYk0VM3s381sh5mt7HNspJk9aWbrM+9HRFmjiIjIQEXdUv0RcPlhx24CnnL36cBTmdsiIiI5L9JQdffngD2HHb4SuCfz8T3AVYNalIiIyHEyd4+2ALPJwKPuPjtzO+HuVZmPDdjbe/uwz7sBuAGgurr6Hffee++g1Zwr2traqKioiLqMt/SNFzsAuPnc0ogreWtxOJdxoPMYHp3LcGTjPF588cWvuPv8/u4rCPWVQububmb9pr673wXcBTB//nxfuHDhYJaWE+rr68n1r/uOtcFi+gsXnh9xJW8tDucyDnQew6NzGY7BPo9RX1Ptz3YzqwHIvN8RcT0iIiIDkouCoaZ2AAAUqUlEQVSh+gjwyczHnwQejrAWERGRAYt6Ss1PgaVAnZk1mtmngduAy8xsPXBp5raIiEjOi/Saqrv/8VHuumRQCxEREQlBLnb/ioiIxJJCVUREJCQKVRERkZDk9DxVkcFwzZ1LSSQ60JRAETlRaqmKiIiERKEqIiISEoWqiIhISBSqkjUPLWti2eYEL27cw4LbnuahZU1RlyQiklUKVcmKh5Y1cfODK+hOpQFoSnRw84MrFKwiMqQpVCUrbl+ylo6e1CHHOnpS3L5kbUQVicTHNXcuPbBtosSLptRIVmxN9P8H4WjHRUTCFsV0ObVUJStqq/rflPxox0VEhgKFqmTF4kV1lBbmH3KstDCfxYvqIqpIRCT71P0rWXHVvPEAfPmBBrpTacZXlbJ4Ud2B4yIiQ5FCVbLmqnnj+envNwNw32fOj7gaEZHsU/eviIhISBSqIiIiIVGoiojIsbv7iuBNDpGz11TN7E2gFUgBSXefH21FIiLSa9W2FgBmRVxHrsnZUM242N13RV2EiAwRvS2r6x+Ltg4ZsnI9VCXmbtm9OPPRbyOtQwbB3VcwN5GAhb+LuhKRyORyqDrwhJk5cKe739X3TjO7AbgBoLq6mvr6+tBeeO6yrwKwfN6toT1n2OYu+ypnpFLUc1vUpbylEckkQKjfn7B9ZftN4FBfr9WeTsTcRIJUKpXT3+u5iQQAy3O4RoBEoiPnz2UcfrejOI+5HKoXuHuTmY0FnjSz19z9ud47MyF7F8D8+fN9YZiLO26sAiDU5wzbxioSiURu1wisej74EcvlOlc9X0AymczpGmMhDj+TcfjdBu5YuzTnz2UcfrfHPH8ByWSSMxe+MGivmbOjf929KfN+B/Bz4JxoKxKJkEZaisRCTrZUzawcyHP31szH7wG+HnFZIiKD4pbdi0kmk8B7oy5FjlFOhipQDfzczCCo8Sfu/ni0JYmIiLy1AYWqmS0Alrv7fjP7OHAW8C/uvikbRbn7BuDMbDy3iIhItgz0muodQLuZnQl8EXgD+I+sVSUiIhJDAw3VpLs7cCXwf939e8Cw7JUlMjgeWtbEn7b+BVe1/w0Lbnuah5Y1RV1SbK3a1sKm1lTUZYhEaqDXVFvN7Gbg48BFZpYHFGavLJHse2hZEzc/uIIOrwSgKdHBzQ+uAMi5fV+1JJxIPAy0pXoN0AV82t2bgQnA7VmrSmQQ3L5kLR09h7asOnpS3L5kbUQViUjcDailmgnSb/e5vRldU43Uqm0tJJMpqqIuJMa2JjqO6biIyNt5y1A1s1aC5QKPuAtwdx+elaoipq62k0NtVSlN/QRobZWWKxSR4/OW3b/uPszdh/fzNmyoBqqcPBYvqqO0MP+QY6WF+SxeVBdRRSISd8e0+ENmHd6S3tuZbmCRWOodjHTr/c+yy4dTW1XG4kV1OTdISU4uDy1r4tbWvwh+Jm97Wj+TMTPQxR/+CPgWUAvsACYBa1APqcTcVfPGM/1X/xYsun3T4C26LdKfOI1Il/4NdPTv3wPnAevcfQpwCaC/QCIiIdKI9PgbaKj2uPtuIM/M8tz9GWB+FusSETnpaER6eKJa2GWg11QTZlYBPAf82Mx2APuzV5aIyMlHI9LDEWU3+kBbqlcCHcAXgMcJ1v79QLaKkqFjVk0ls2oqoy5DJBY0Ij0cUXajD3Txh76t0nuyVIuIyElNI9LDEWU3+kBH//ZdBKKIYN3f/ZqrKiISLo1IP3FRdqMPqPu37yIQQCnwYeDfslqZiIjkpob7md79GjO7V8B3ZkPD/VFXdIgou9EHek31AA88BCzKQj0iIpLLGu6HX3yOInowgJYt8IvP5VSwXjVvPN/40BmMsRYMZ3xVKd/40BmD0o0+0O7fD/W5mUcwnaYzKxWJiGRDw/3Q+BKkuoLW1SW3wJyro64qfp76OvQc1rXa0xEcz6HzGVU3+kBbqh/o87YIaCUYEZw1Zna5ma01s9fN7KZsvtYhcrxbAzhQ45z0mtytEQ7+Edv029yuU05crv9MZlpXpLqC2znYuooLb2k8puMnm4GO/r0+24X0ZWb5wPeAy4BG4CUze8TdV2f1hft0awAHf/Egd/4Di0ONcPQ/YpBzdU7vfo1CetR6OV5x+JmMSesqDrYzmnHsPMpxecuWqpn9q5l992hvWazrHOB1d9/g7t3AvWS5ZQy89S9erohDjQAP35j7dcbg2hCQ+70ncfiZPForSq2rY/aN7o/S7kWHHGv3Ir7R/dGIKjqKiHpP3q6l+nLm/QJgJnBf5vZHgWy2GscDW/rcbgTO7fsAM7sBuAGgurqa+vr6E37Rd7U0Bn9cD+MtjTwbwvOHIQ41Arwr1ZXzdZ639CuU9BMGnY99hRf2jI2mqMOM3f4sdWu/d0grMPXQjaxds4Yd1e+KtriMOPxMnlc8mpKuI1tXncWjeSFHauw1dvuzTO9eQyFJOr9xKhumfiJnvtcAvy26kJu64csF91Nru9nqo/in5NX8rujCUP4OhyHK3xtz728P8sMeZPYCcIG7JzO3C4HfuPt5WSnK7CPA5e7+Z5nbnwDOdfcb+3v8/Pnz/eWXX+7vrmPzndlBa+VwlRPhCytP/PnDEIcaIR51fq2Kg9Ov+zL4WmKwq+lfHM5jHGrsvRzR95+owlL4wHdzq/s3BnUeWAKwz4pFpYX5gza6dkCy/DNpZq+4e7/r3w90oNIIoO9CDxWZY9nSBEzsc3tC5lh2XXJL8APcV2FpcDxXxKFGiEedlROO7XgU4tBtGYfv9Zyrg2DKLw5uV07MqaA6IAZd6VFOVxmwCH9vBrqg/m3AMjN7BjDgIuBr2SoKeAmYbmZTCML0WuC6LL5eIPML1v3gX1BID1Y5MfcGrsShRjhYz8M3BoOVcrHOS27pv1WQS2FQOeEo/3HnUPDH6Wfylcwqq9c/Fm0tRxOHf6I4OF0FYNZNv424mn5E+Hsz0BWV7ia4pvlz4EHgfHfP2hrAmW7mG4ElBJuh3+/uq7L1eoeYczXri2awuuiMoJsg1/4wwIEaG/JOz90aIahrwtkw6YLcrDPTeummMOgEzsXWSxxagRCfn8lcF4fekziI8Pfm7Ub/zsi8PwuoJRg8tAWozRzLGnf/pbuf5u7T3P3WbL6WnMRyPQziEPxEt3flkBOXf6JyXYS/N2/X/fu/CEbYfquf+xx4d+gVicghHkot4Nau7wa7lpSUsThVx1VRF9VHlHtXHqtV21oAmBVxHUcVl670OJhzNesf/W6wotIXBm9FpbcMVXe/IfP+4sEpR0T6ikNgvdXelblSY6xEFAYSjgFdUzWzj5rZsMzHf2NmD5rZvOyWJiJRbrY8UFHuXSmSawY6peZv3b3VzC4ALgV+CPy/7JUlIhCPwDraHpWDsXelSK4ZaKj2/qt8BXCXuz9GsFm5RECDQk4ecQisKPeuFMk1Aw3VJjO7E7gG+KWZFR/D58ZKb2C9f99NORlYvdfYdnoljh24xpZrdcbJrJpKJg3Lf/sHRiAOgRWLxQBEBslAF3+4Grgc+Ka7J8ysBlicvbKioUEhkmt6v6e33v9sMPq3qozFi+py7nsd1d6VIrlmoFu/tZvZDuACYD2QzLwfUuIQWHG4xibhyvnVa0TkgIGO/v074K+BmzOHCoH/ylZRUYlDYMXhGpuIyMlqoNdFPwj8EbAfwN23AsOyVVRU4hBYcbjGJiJyshpoqHZ7sEecA5hZefZKik4cAkuDQkREctfbXlM1MwMezYz+rTKz/wF8Cvh+tosbbBoUIiIiJ+JtQ9Xd3cw+SrAO8D6gDrjF3Z/MdnFR0KAQEYnarJpKEolE1GXIcRjolJo/AAl3H3LTaERERMIy0FA9F/iYmW0iM1gJwN3nZKUqERGRGBpoqC7KahUiIiJDwEAXf9iU7UJERETCFMW16SG5fq+IiEgUci5UzexrZtZkZsszb++LuiYREZGBGOg11cH2HXf/ZtRFiIhI/74+6nYA7ou4jlyTq6EqIhI6BYFkW66G6o1m9ifAy8AX3X1v1AXlmq+Pup1EIsGSqAt5O9c/FnUFMkhi8zMpkkWRhKqZ/RoY189dXwXuAP6eYJ3hvwe+RbAs4uHPcQNwA0B1dTX19fWh1feN/K8CcHOIzxm2RKKDVCoV6td90pqymLa2Nipy+FyOSCYBcvr7HYefyURmx6lcrhFgbiKR8+fyzzNLoudyjVGcx0hC1d0vHcjjzOz7wKNHeY67gLsA5s+f7wsXLgytvjvWLgVg4cLzQ3vOsN2xdimJRIIwv+6TWX19fU6fy1XPB7+quVxjHH4m4/C7DcDGqpw/l7EQwXnMue5fM6tx922Zmx8EVkZZj0gu0LVAkeNw/WMsr69n4SC+ZM5NqQH+ycxWmFkDcDHwhagLEpH4e2hZE8s2J3hx4x4W3PY0Dy1rirqko7v+MZbPuzXqKuQ45FxL1d0/EXUNIjK0PLSsiZsfXEF3Kg1AU6KDmx9cAZBzWztKvOViS1VEJFS3L1lLR0/qkGMdPSluX7I2oopkqFKoisiQtzUz6negx0WOl0JVRIa82qrSYzoucrwUqiISivs+cz43n5ubIbV4UR2lhfmHHCstzGfxorqIKpKhKucGKonIke77TI7Pq8xxvYORvvxAA92pNOOrSlm8qE6DlCR0ClUROSlcNW88P/39ZkD/pEj2qPtXREQkJApVERGRkChURUREQqJQjalcHmkpInKyUqiKiIiERKN/+6GRgSIicjzUUhUREQmJQlVERCQkClUREZGQKFRFRERColAVEREJiUJVREQkJApVERGRkEQSqmb2UTNbZWZpM5t/2H03m9nrZrbWzBZFUZ+IiMjxiGrxh5XAh4A7+x40s5nAtcAsoBb4tZmd5u6pwS9RRETk2ETSUnX3Ne6+tp+7rgTudfcud98IvA6cM7jViYiIHJ9cW6ZwPPBCn9uNmWNHMLMbgBsAqqurqa+vz3pxuaatre2k/LqzQecyHLl+HhOJDoCcrrFXrp/LuBjs85i1UDWzXwPj+rnrq+7+8Ik+v7vfBdwFMH/+fF+4cOGJPmXs1NfXczJ+3dmgcxmOXD+Pd6xdCsDChbm/vneun8u4GOzzmLVQdfdLj+PTmoCJfW5PyBwTERHJebk2peYR4FozKzazKcB04PcR1yQiIjIgUU2p+aCZNQLnA4+Z2RIAd18F3A+sBh4HPquRvyIiEheRDFRy958DPz/KfbcCtw5uRSIiIicu17p/RUREYivXptSIiGTNfZ/J/VG/Em9qqYqIiIREoSoiIhIShaqIiEhIFKoiIiIhUaiKiIiERKEqIiISEnP3qGs4YWa2E9gUdR0RGA3sirqIIULnMhw6j+HRuQxHNs7jJHcf098dQyJUT1Zm9rK7z4+6jqFA5zIcOo/h0bkMx2CfR3X/ioiIhEShKiIiEhKFarzdFXUBQ4jOZTh0HsOjcxmOQT2PuqYqIiISErVURUREQqJQFRERCYlCNYbMbKKZPWNmq81slZl9Puqa4szM8s1smZk9GnUtcWZmVWb2gJm9ZmZrzEz7rB0HM/tC5vd6pZn91MxKoq4pLszs381sh5mt7HNspJk9aWbrM+9HZLMGhWo8JYEvuvtM4Dzgs2Y2M+Ka4uzzwJqoixgC/gV43N1nAGeic3rMzGw88DlgvrvPBvKBa6OtKlZ+BFx+2LGbgKfcfTrwVOZ21ihUY8jdt7n7HzIftxL88RofbVXxZGYTgCuAH0RdS5yZWSVwEfBDAHfvdvdEtFXFVgFQamYFQBmwNeJ6YsPdnwP2HHb4SuCezMf3AFdlswaFasyZ2WRgHvBitJXE1j8DXwbSURcSc1OAncDdma70H5hZedRFxY27NwHfBDYD24AWd38i2qpir9rdt2U+bgaqs/liCtUYM7MK4GfAX7n7vqjriRszez+ww91fibqWIaAAOAu4w93nAfvJcjfbUJS53nclwT8ptUC5mX082qqGDg/mkGZ1HqlCNabMrJAgUH/s7g9GXU9MLQD+yMzeBO4F3m1m/xVtSbHVCDS6e2+PyQMEISvH5lJgo7vvdPce4EHgnRHXFHfbzawGIPN+RzZfTKEaQ2ZmBNeu1rj7t6OuJ67c/WZ3n+DukwkGgzzt7moVHAd3bwa2mFld5tAlwOoIS4qrzcB5ZlaW+T2/BA34OlGPAJ/MfPxJ4OFsvphCNZ4WAJ8gaFktz7y9L+qi5KT3l8CPzawBmAv8Y8T1xE6mpf8A8AdgBcHfaC1XOEBm9lNgKVBnZo1m9mngNuAyM1tP0BNwW1Zr0DKFIiIi4VBLVUREJCQKVRERkZAoVEVEREKiUBUREQmJQlVERCQkClWRHGZmbVl4zrl9p2CZ2dfM7Ev9PG6ymXWY2fJjfP5rzOx17fojJyOFqsjJZy4w0HnNb7j73GN5cne/D/izY65KZAhQqIrEhJktNrOXzKzBzP535tjkzN6l38/swfmEmZVm7js789jlZnZ7Zn/OIuDrwDWZ49dknn6mmdWb2QYz+9xRXn9yZq/UH5nZOjP7sZldama/y+xVec6gnAiRHKZQFYkBM3sPMB04h6Cl+Q4zuyhz93Tge+4+C0gAH84cvxv4TKalmYJgSzbgFuA+d5+baVUCzAAWZZ7/7zJrS/fnVOBbmcfPAK4DLgC+BHwlpC9XJLYUqiLx8J7M2zKCJexmEIQpBAuw9173fAWYbGZVwDB3X5o5/pO3ef7H3L3L3XcRLDh+tO2xNrr7CndPA6sINn92giX1Jh/H1yUypBREXYCIDIgB33D3Ow85GOyn29XnUAooPY7nP/w5jva3oe/j0n1up9/ic0ROGmqpisTDEuBTmT10MbPxZjb2aA929wTQambnZg5d2+fuVmBY1ioVOYkpVEViwN2fIOjCXWpmKwh2Mnm7YPw08P3MlJhyoCVz/BmCgUl9ByqJSAi0S43IEGVmFe7elvn4JqDG3T9/DJ8/GXjU3Wcfx2svBL7k7u8/1s8ViTO1VEWGrisyrdGVwIXAPxzj56eAyuNZ/AH4N2DvMb6eSOyppSoiIhIStVRFRERColAVEREJiUJVREQkJApVERGRkChURUREQvL/ASHU4Tx1zzbpAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# we can add multiple datasets to plot\n", + "figure.plot(xdata=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", + " ydata=[3.8, 8.9, 16, 24.8, 35.5, 48.9, 64, 80, 100, 120],\n", + " xerr=0.05, yerr=5, name=\"second\")\n", + "\n", + "# we can also add a line of best fit to the plot\n", + "figure.fit(model=q.FitModel.QUADRATIC)\n", + "\n", + "# we can add turn on legends for the plot since we now have 2 data sets\n", + "figure.legend()\n", + "\n", + "# now show the figure\n", + "figure.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Fitting Module" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The QExPy fitting module supports a few pre-set fit models, as well as any custom fit function the user wish to use. The available pre-set models include linear fit, quadratic fit, general polynomial fit, exponential fit, and gaussian fit. The pre-set models are stored under q.FitModel, To select the fit model, if you're in a Jupyter Notebook environment, simply type \"q.FitModel.\", and press TAB, the available options will appear as a list of autofill suggestions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of XY Dataset to quadratic\n", + "\n", + "Result Parameter List: \n", + "a = 1.004 +/- 0.009,\n", + "b = 2.0 +/- 0.1,\n", + "c = 0.9 +/- 0.2\n", + "\n", + "Correlation Matrix: \n", + "[[ 1. -0.975 0.814]\n", + " [-0.975 1. -0.909]\n", + " [ 0.814 -0.909 1. ]]\n", + "\n", + "chi2/ndof = 1.13/6\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# We can do a simple quadratic fit. \n", + "result = q.fit(\n", + " xdata=[1,2,3,4,5,6,7,8,9,10], xerr=0.5, \n", + " ydata=[3.86,8.80,16.11,24.6,35.71,48.75,64,81.15,99.72,120.94], \n", + " yerr=0.5, model=q.FitModel.QUADRATIC) # or simply type \"quadratic\"\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The parameters of polynomials are organized from highest to lowest power terms. The result above indicates that the function of best fit is 1.004x^2 + 2x + 0.9" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The QExPy fit function is very flexible in accepting fit arguments. The three accepted ways to specify the fit data set are:\n", + "1. Create an XYDataSet object and pass the dataset into the fit function\n", + "2. Pass in a MeasurementArray object for each of xdata and ydata\n", + "3. Pass in two Python lists or numpy arrays for xdata and ydata, specify the xerr or yerr if applicable" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of XY Dataset to linear\n", + "\n", + "Result Parameter List: \n", + "slope = 0.98 +/- 0.04,\n", + "intercept = -0.1 +/- 0.3\n", + "\n", + "Correlation Matrix: \n", + "[[ 1. -0.886]\n", + " [-0.886 1. ]]\n", + "\n", + "chi2/ndof = 4.75/7\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# The traditionoal way (with previous versions of QExPy) of fitting\n", + "xydata = q.XYDataSet(\n", + " xdata=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], xname='length', xunit='m',\n", + " ydata=[0.6, 1.6, 3.5, 4.1, 4.6, 5.6, 6.1, 7.9, 8.7, 9.8], yerr=0.5, \n", + " yname='force', yunit='N')\n", + "\n", + "# the fit function can be called directly from the data set\n", + "result = xydata.fit(\"linear\")\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# You can very easily add a dataset and its fit function to a plot\n", + "figure = plt.plot(xydata)\n", + "figure.plot(result)\n", + "\n", + "# turn on residuals and error bars\n", + "figure.error_bars()\n", + "figure.residuals()\n", + "\n", + "# show the figure\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "slope = 0.98 +/- 0.04\n", + "intercept = -0.1 +/- 0.3\n" + ] + } + ], + "source": [ + "# You can access the fit parameters easily by indexing the result instance:\n", + "slope = result[0]\n", + "intercept = result[1]\n", + "\n", + "# these are both ExperimentalValue instances\n", + "print(slope)\n", + "print(intercept)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of XY Dataset to polynomial\n", + "\n", + "Result Parameter List: \n", + "coeffs_0 = 2.0006 +/- 0.0008,\n", + "coeffs_1 = 0.99 +/- 0.01,\n", + "coeffs_2 = -2.93 +/- 0.06,\n", + "coeffs_3 = 3.86 +/- 0.09\n", + "\n", + "Correlation Matrix: \n", + "[[ 1. -0.989 0.941 -0.795]\n", + " [-0.989 1. -0.979 0.859]\n", + " [ 0.941 -0.979 1. -0.935]\n", + " [-0.795 0.859 -0.935 1. ]]\n", + "\n", + "chi2/ndof = 0.00/5\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# QExPy also supports fitting with higher order polynomials. The degree of a polynomial \n", + "# is the degree of the highest order term. e.g. a quadratic function would be degree-2\n", + "result = q.fit(\n", + " xdata=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", + " ydata=[3.89, 18.01, 58.02, 135.92, 264.01, 453.99, 718.02, 1067.98, 1516.01, 2074], \n", + " model=q.FitModel.POLYNOMIAL, degree=3)\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Advanced Fitting\n", + "\n", + "QExPy supports fitting a custom function to a data set. With any non-polynomial fit models, a list of guesses for the fit parameters needs to be supplied under the keyword argument \"parguess\". Other optional keyword arguments to the fit function includes \"parnames\" and \"parunits\", which are the names and units assigned to the fit parameters, which will show up in the fit results." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of XY Dataset to custom\n", + "\n", + "Result Parameter List: \n", + "mass = 5.1 +/- 0.2 [kg],\n", + "length = 0.500 +/- 0.009 [m]\n", + "\n", + "Correlation Matrix: \n", + "[[1. 0.238]\n", + " [0.238 1. ]]\n", + "\n", + "chi2/ndof = 0.00/17\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# First define a fit model\n", + "def func(x, a, b):\n", + " return a * q.sin(b * x)\n", + "\n", + "# Apply it to the test dataset\n", + "result = q.fit(\n", + " xdata=[0.00,0.33,0.66,0.99,1.32,1.65,1.98,2.31,2.64,2.97,3.31,3.64,3.97,4.30,\n", + " 4.63,4.96,5.29,5.62,5.95,6.28],\n", + " ydata=[0.09,0.41,1.53,2.23,3.76,2.50,3.89,5.33,5.39,4.05,5.08,5.84,4.59,4.50,\n", + " 3.48,3.57,2.20,1.95,0.39,-0.18],\n", + " model=func, parguess=[1, 1], parnames=[\"mass\", \"length\"], parunits=[\"kg\", \"m\"])\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of XY Dataset to custom\n", + "\n", + "Result Parameter List: \n", + "mass = 5.1 +/- 0.2 [kg],\n", + "length = 0.500 +/- 0.009 [m]\n", + "\n", + "Correlation Matrix: \n", + "[[1. 0.238]\n", + " [0.238 1. ]]\n", + "\n", + "chi2/ndof = 0.00/17\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# The QExPy fitting module also has this little feature implemented, where if you leave\n", + "# the \"parname\" field empty, parameter names will be extracted from the signature.\n", + "\n", + "def func(x, mass, length): # define the fit function with the names you want\n", + " return mass * q.sin(length * x)\n", + "\n", + "# try the same fit again\n", + "result = q.fit(\n", + " xdata=[0.00,0.33,0.66,0.99,1.32,1.65,1.98,2.31,2.64,2.97,3.31,3.64,3.97,4.30,\n", + " 4.63,4.96,5.29,5.62,5.95,6.28],\n", + " ydata=[0.09,0.41,1.53,2.23,3.76,2.50,3.89,5.33,5.39,4.05,5.08,5.84,4.59,4.50,\n", + " 3.48,3.57,2.20,1.95,0.39,-0.18],\n", + " model=func, parguess=[1, 1], parunits=[\"kg\", \"m\"])\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Plotting Module" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The plotting module is centered around the Plot class. It is a data structure that holds all the objects to be plotted (data sets, functions, histograms, etc.). When calling the QExPy plot function, a Plot object will be created and returned. The user can add objects to the plot using the same plot function called from the Plot instance. The module also keeps a reference to the last Plot object being operated on, and if the return value of a call to the plot function is not assigned to any variable, by default, the object will be added to the current buffered plot." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plotting Data Sets and Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The QExPy plotting function takes the same types of inputs as the fit function for plotting data sets." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "xydata = q.XYDataSet(\n", + " xdata=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], xname='length', xunit='m',\n", + " ydata=[6, 16, 35, 41, 46, 56, 61, 79, 87, 98], yerr=0.5, \n", + " yname='force', yunit='N')\n", + "\n", + "# You can specify the format string of the plot object. The default for data sets is\n", + "# dots, but if you want them connected in a line, you can use the fmt option\n", + "figure = plt.plot(xydata, \"-o\")\n", + "\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdUAAAFhCAYAAAAm4jCUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deXxU1d3H8c/JnpCEEAKBLBCWEHYIIJtWQau4VbFatVWrra1LH8VqpdXa1m4+taXaap/W1m5aF3AB0VYtWpW6L4SwQ9iXTAiBhCSE7Jnz/HEHCBogwMzcmcn3/Xr5SnLPJPlxDfly7r3nd4y1FhERETl5UW4XICIiEikUqiIiIn6iUBUREfEThaqIiIifKFRFRET8JMbtAk5GRkaGzcvLc7uMoNq/fz/dunVzu4yIofPpPzqX/qXz6V/+PJ9FRUV7rLW9OhoL61DNy8tjyZIlbpcRVIsXL2batGlulxExdD79R+fSv3Q+/cuf59MYs+1IY7r8KyIi4icKVRERET9RqIqIiPiJQlVERMRPFKoiIiJ+ErBQNcb8zRhTYYxZ1e5YujHmdWPMBt/bHr7jxhjzsDFmozFmhTFmXKDqEhERCZRAzlQfA8791LG7gDestfnAG76PAc4D8n3/3QA8EsC6REREAiJgoWqtfRuo+tThi4HHfe8/Dsxsd/wf1vEhkGaM6Ruo2kRERAIh2M0fMq21O33vlwOZvvezgR3tXlfqO7aTTzHG3IAzmyUzM5PFixcHrNhQVFdX1+X+zIGk8+k/Opf+pfPpX8E6n651VLLWWmPMce+Qbq19FHgUYMKECbardRxRlxX/0vn0H51L/9L59K9gnc9gP/2768BlXd/bCt9xD5Db7nU5vmMiIiJhI9ih+hJwre/9a4EX2x3/qu8p4MlATbvLxCIiIidkYbGHU+9/k+v+vZ9T73+ThcWBna8F7PKvMWYuMA3IMMaUAvcC9wPPGmOuB7YBl/te/gpwPrARqAe+Fqi6RESka1hY7OHuBStpaGkDwFPdwN0LVgIwszA7IN8zYKFqrf3yEYbO6uC1FvifQNUiIiJdz5xFJQcD9YCGljbmLCoJWKiqo5KIiESksuqG4zruDwpVERGJOC8tLzviWFZaYsC+b1hvUi4iItJea5uX+19dx1/e3cKAjCR2VjfS2Oo9OJ4YG83sGQUB+/6aqYqISETYU9fE1X/9iL+8u4Vrp/Rn0bfP4P5LR5Ptm5lmpyXyiy+OCtj9VNBMVUREIsDyHdXc9GQRVfubeeBLY7h0fA7gPOU7szA7aM0fFKoiIhLWnv1kBz94cRW9kuOZf/NURmZ3d60WhaqIiISlptY2fvLPNTz90XZOG5zB775cSI9uca7WpFAVEZGwU17TyM1PFVG8vZqbzhjE7BkFREcZt8tSqIqISHj5eEsV33pqKfXNrTxy1TjOGxU6O4UqVEVEJCxYa3n8/a38/OW15KYnMfebk8jPTHG7rMMoVEVEJOQ1NLdxzwsrWVDs4fPDMnnwijGkJsS6XdZnKFRFRCSk7aiq58YnilhbXssdZw/hlumDiQqB+6cdUaiKiEjIemfDbm6dW0yb1/K3a09h+tDebpd0VApVEREJOdZaHvnvJn69qIT83in86Zrx5GV0c7usY1KoiohISKlramX2c8t5dVU5F47uy68uG01SXHjEVXhUKSIiXcKm3XXc+EQRm3fXcc/5w/jG5wZgTGjeP+2IQlVERELC62t2ccczy4iNieLJ6ycxdXCG2yUdN4WqiIi4qs1reeg/63n4zY2MzunOI1ePP7izTLhRqIqIiGtq6lu47ZliFpfs5kvjc/jZzJEkxEa7XdYJU6iKiIgr1pXXcuMTRZRVN/DzmSO5alK/sLp/2hGFqoiIBN1Ly8v43vMrSEmIYd4NUxjfv4fbJfmFQlVERIKmtc3L/a+u4y/vbuGUvB78/qpx9E5JcLssv1GoiohIUFTWNXHL08V8sLmSa6f0554LhhMXE+V2WX6lUBURkYBbvqOam58sonJ/Mw98aQyXjs9xu6SAUKiKiEhAPfvJDn7w4ip6Jccz/+apjMzu7nZJAaNQFRGRgGhqbeMn/1zD0x9t57TBGfzuy4X06BbndlkBpVAVERG/K69p5OaniijeXs1NZwxi9owCokN0uzZ/UqiKiIhffbylim89tZT65lb+cNU4zh/V1+2SgkahKiIifmGt5fH3t/Lzl9eSm57E3G9OIj8zxe2ygkqhKiIiJ62huY17XljJgmIPnx/WmwevGEtqQqzbZQWdQlVERI7bwmIPcxaVUFbdQO/UeKKNYWdtI3ecPYRbpg8mqgvcP+2IQlVERI7LwmIPdy9YSUNLGwC7apsAuOFzA5h1Vr6bpbkuslpZiIhIwM1ZVHIwUNt7eWW5C9WEFoWqiIh0mtdr8VQ3dDhWdoTjXYlCVUREOmXJ1iou/v17RxzPCtONxf1JoSoiIkflqW7g1rnFXPbHD9i9r4mrJ/cjMfbw+EiMjWb2jAKXKgwdelBJREQ61NDcxh//u4k/vb0Ja2HWWfncdMZAkuJimNA//eDTv1lpicyeUcDMwmy3S3adQlVERA5jreWl5WXc/+o6dtY0cuHovtx13lByeiQdfM3MwmyFaAcUqiIictDyHdX89F9rKNq2lxFZqTx0ZSETB6S7XVbYUKiKiAgVtY38alEJzxeVkpEcxy8vHcVl43O7RBN8f1Koioh0YY0tbfz13S384a2NNLd5ufGMgdwyfTApXbDFoD8oVEVEuiBrLYtW7+K+V9awo6qBs4dncs/5w8jL6OZ2aWFNoSoi0sWs3VnLT/+5hg82VzIkM5knr5/EafkZbpcVERSqIiJdRGVdEw+8vp55H28nNTGWn108gi9P7EdMtFoW+ItCVUQkwjW3evnHB1t56I0N1De38dUpeXz78/mkJcW5XVrEUaiKiESwt9ZV8LOX17B5935OH9KLH14wrMttHB5MClURkQi0saKOn7+8hsUluxmQ0Y2/XTeB6QW9MUZLZAJJoSoiEkFq6lv47RvreeKDbSTGRvODC4bx1Sl5xMXovmkwKFRFRCJAa5uXuZ/s4MHXSqhuaOHKU/rxnXOGkJEc73ZpXYpCVUQkzL2/cQ8//dca1pXvY9KAdH70heGMyOrudlldkkJVRCRMbavcz30vr+W1NbvI6ZHII1eN49yRfXTf1EWuhKox5nbgG4AFVgJfA/oC84CeQBFwjbW22Y36RERCWV1TK//35kb+9u4WYqINs2cUcP1pA0iIjXa7tC4v6KFqjMkGZgHDrbUNxphngSuB84HfWGvnGWP+CFwPPBLs+kREQpXXa3l+aSlzFpWwe18TXxyXzffOHUpmaoLbpYmPW5d/Y4BEY0wLkATsBM4EvuIbfxz4MQpVEREAlmyt4if/XMNKTw2F/dL481cnMDY3ze2y5FOMtTb439SY24D7gAbgNeA24ENr7WDfeC7wqrV2ZAefewNwA0BmZub4efPmBa3uUFBXV0dycrLbZUQMnU//0bn0j/fLWpi/voXKRi89E6KYkRfDpmovH5W30SPe8KWCOCb3jSZK902Piz9/PqdPn15krZ3Q0Zgbl397ABcDA4Bq4Dng3M5+vrX2UeBRgAkTJthp06YFoMrQtXjxYrranzmQdD79R+fy5C0s9vDEGytpaLGAobLR8vS6FqINzDpzMDdNG0RSnJ4vPRHB+vl04//O54Et1trdAMaYBcCpQJoxJsZa2wrkAB4XahMRcc2cRSU0tLR95nhGSjx3nFPgQkVyvNxosbEdmGyMSTLOc99nAWuAt4DLfK+5FnjRhdpERFxTVt3Q4fGK2qYgVyInKuihaq39CHgeWIqznCYK53Lu94A7jDEbcZbV/DXYtYmIuKlvWsdP8WalJQa5EjlRrlyct9beC9z7qcObgYkulCMiEhKG902lrLrxsGOJsdHMnqFLv+FCHZZFRELA62t28Z+1FUwZmE62b2aanZbIL744ipmF2S5XJ52lx8hERFy2vbKeO55dxsjsVP7+tYkkxEbraeowpZmqiIiLGlvauOnJIgzwyFXj1WowzGmmKiLiontfXM2anbX87boJ5KYnuV2OnCTNVEVEXPLMJ9t5ZskObpk+mDOHZrpdjviBQlVExAWrPDX88MXVnDq4J7efPcTtcsRPFKoiIkFWU9/Ct55aSnpSHA9fWUh0lPr4RgrdUxURCSKv1/Kd55ZRVt3AMzdOoWdyvNsliR9ppioiEkR/fHsT/1lbwT0XDGN8/x5ulyN+plAVEQmS9zfu4deLSrhwdF+um5rndjkSAApVEZEgKK9p5Na5xQzslcwvLx2N0X6oEUn3VEVEAqylzcv/PL2UhpY2nrl6HN3i9as3Uun/rIhIgP3ilXUUbdvLw18uZHDvFLfLkQDS5V8RkQB6ecVO/vbeFq6bmsdFY7LcLkcCTKEqIhIgGyvq+O7zyynsl8b3zx/mdjkSBApVEZEA2N/Uys1PFhEfG80frhpHXIx+3XYFuqcqIuJn1lruXrCSjbvreOLrk+jbPdHtkiRI9E8nERE/e+LDbby0vIzvnD2E0/Iz3C5HgkihKiLiR8Xb9/Kzf63hzKG9+da0wW6XI0GmUBUR8ZPKuia+9dRSMlMTePDyMUSpUX6Xo3uqIiJ+0Oa1fPuZZVTub2b+TVNJS4pzuyRxgWaqIiJ+8NAbG3hnwx5+ctEIRuV0d7sccYlCVUTkJL1VUsHDb2zgsvE5XHlKrtvliIsUqiIiJ2FHVT23P7OMoX1S+NnFI9Uov4tTqIqInKDGlja+9dRS2tosf7x6PIlx0W6XJC7Tg0oiIifop/9aw0pPDY9eM568jG5ulyMhQDNVEZETML+olKc/2s6NZwzknBF93C5HQoRCVUTkOK0rr+WehSuZNCCd2ecUuF2OhBCFqojIcahtbOHmJ5eSmhDL775SSEy0fo3KIbqnKiLSSdZaZj+3nO1V9cz95mR6pyS4XZKEGP0TS0Skk/78zmYWrd7F3ecNZeKAdLfLkRCkUBUR6YSPNlfyy3+XcN7IPlx/2gC3y5EQpVAVETmGitpGbplbTL/0JH512Wg1eJAj0j1VEZGjaG3zcsvcYvY1tvDE9RNJSYh1uyQJYQpVEZGjmLOohI+3VPGbK8YwtE+q2+VIiNPlXxGRI/j3qnL+9PZmrp7cj0sKc9wuR8KAQlVEpANb9uxn9nPLGZPTnR9eONztciRMKFRFRD6lobmNm58sIjra8PurxhEfo0b50jm6pyoi0o61lnsWrqRk1z7+ft0p5PRIcrskCSOaqYqItDP34x0sWOph1pn5TCvo7XY5EmYUqiIiPitKq/nxS6s5fUgvZp2V73Y5EoYUqiIiQHV9Mzc/uZSM5Dh+e8VYoqPU4EGOn+6pikiX5/Vavv3MMir2NfLcTVNJ7xbndkkSpjRTFZEu7/dvbWRxyW5+9IURjM1Nc7scCWMKVRHp0t7ZsJsH/7OemWOzuHpSP7fLkTCnUBWRLqusuoHb5i0jv3cy//vFUWqULydNoSoiXVJzq5dvPbWU5lYvj1w9nqQ4PWIiJ08/RSLSJd338hqW7ajmD1eNY1CvZLfLkQihUBWRLmFhsYc5i0ooq24gLSmWvfUtfOO0AZw/qq/bpUkEUaiKSMRbWOzh7gUraWhpA2BvfQtRBob1TXG5Mok0uqcqIhFvzqKSg4F6gNfCg69vcKkiiVSuhKoxJs0Y87wxZp0xZq0xZooxJt0Y87oxZoPvbQ83ahORyFJd34ynuqHDsbIjHBc5UW7NVB8C/m2tHQqMAdYCdwFvWGvzgTd8H4uIHLfGljZeXbmTG/6xhFPu+88RX5eVlhjEqqQrCPo9VWNMd+B04DoAa20z0GyMuRiY5nvZ48Bi4HvBrk9EwpPXa/l4axULiz28vHIn+xpb6ZUSz7VT8uieFMsf3tpIQ4v34OsTY6OZPaPAxYolEhlrbXC/oTFjgUeBNTiz1CLgNsBjrU3zvcYAew98/KnPvwG4ASAzM3P8vHnzglV6SKirqyM5WY//+4vOp/+4dS49dV7e97Tywc5Wqhot8dEwITOGKVkxDO8ZRZSvocP7ZS3MX99CZaOlZ4Lh0iGxTM2KDXq9naWfTf/y5/mcPn16kbV2QkdjboTqBOBD4FRr7UfGmIeAWuDW9iFqjNlrrT3qfdUJEybYJUuWBLbgELN48WKmTZvmdhkRQ+fTf4J5LitqG3lpeRkvFHtYXVZLdJThc/kZXFKYzdnDMyOikYN+Nv3Ln+fTGHPEUHXjJ68UKLXWfuT7+Hmc+6e7jDF9rbU7jTF9gQoXahOREFXX1MqiVeUsXObhvY178FoYk9Ode78wnAtHZ9ErJd7tEkWCH6rW2nJjzA5jTIG1tgQ4C+dS8BrgWuB+39sXg12biISW1jYv72zYwwvFHl5bU05ji5fc9ERumT6Yiwuz1QlJQo5b10huBZ4yxsQBm4Gv4TyJ/Kwx5npgG3C5S7WJiIustaworeGFYg//XF5G5f5muifGcum4HC4pzGZ8/x5qfC8hy5VQtdYuAzq6Hn1WsGsRkdCwvbKehcs8LCz2sHnPfuJiovj8sN7MHJvNtILexMWoV42EvvC/my8iYWvv/mZeXrmThcUelmzbC8CkAenccPpAzhvVl+6Joft0rkhHFKoiElSNLW28ua6CF4o9LC6poKXNkt87me+eW8DFY7PJVkMGCWNHDVVjzEud+BpV1trr/FOOiEQir9fy0RanMcMrq5zGDL1T4rluah4zC7MZ3jdV90klIhxrpjoM+MZRxg3we/+VIyLh5sCWap7qBrI/fJPZMwqYWZgNwPpd+1iw1MNLyzyU1TSSFBfNuSP7cElhNlMHZRAdpSCVyHKsUL3HWvvfo73AGPMTP9YjImHk01uqeaobuGv+Ct5aV8GGijrW7HQaM5yen8H3zhsaMY0ZRI7kqD/d1tpnj/UFOvMaEYlMHW2p1tjq5cXlZWrMIF3Sse6p/h04Uh9Da6293v8liUi4ONLWaQZ48ZbTgluMSAg41nWYf3VwLBe4HYj2fzkiEk7SkmLZW9/ymePaUk26qmNd/p1/4H1jzEDg+zjbtt0P/DWwpYlIqKprauXHL61mb30LxkD7fTm0pZp0Zcd8YsAYMxT4AVAIzAFusta2BrowEQlNy3dUc9u8YrZV1XPrmYPJ65nEg69vcJ7+TUs87Olfka7mWPdUnwPGAw/gXPJtA1IPrCez1lYFukARCQ1er+XRdzbz60Ul9EqJZ+43JzN5YE8ALh2fq63KRDj2TPUUnAeV7gS+4zt2YGGZBQYGqC4RCSG7ahu549llvLexkvNG9uEXXxxFWlKc22WJhJxj3VPNC1IdIhKiXl+zi+8+v5zGFi/3f3EUV5ySq+5HIkdwrMu/fay15Sf7GhEJP40tbdz38lqe+HAbw/um8vCXCxncW/uXihzNsS7/vgKM88NrRCSMrCuvZdbcYtbvquObnxvAnTMKiI/RKjqRYzlWqI4xxtT63m9/L5V2x2oRkYhgreUfH2zjvlfWkpoQy+Nfn8gZQ3q5XZZI2DjWPVX901Ski6isa2L28yt4c10F0wt6MedLY8hIVntBkePRqc7Wxnkq4SpggLX2Z8aYXKCvtfbjgFYnIkHxzobd3PHscmrqW7j3C8O5bmqeHkYSOQGd3S7iD4AXOBP4GVCHs+XbKQGqS0SCoLnVy69fK+HRtzeT3zuZf3x9IsP6prpdlkjY6myoTrLWjjPGFANYa/caY7RITSSMbd5dx6x5xazy1HLVpH784ILhJMbpjo/IyehsqLYYY6LxPaRkjOmFM3MVkTBjreW5JaXc+9Jq4mOj+NM145kxoo/bZYlEhM6G6sPAC0BvY8x9wGU4/YBFJIzU1Lfw/RdW8vLKnUwZ2JPfXDGWPt0T3C5LJGJ0KlSttU8ZY4qAs3CW0cy01q4NaGUi4lcfb6ni9meWsau2ke+dO5QbTh9IdJQeRhLxp84+/TsZWG2t/b3v41RjzCRr7UcBrU5ETlprm5eH39zI/725gdz0JObfPJUxuWlulyUSPLU7Sa0pAaYF/Ft19vLvIxzeNamug2MiEmJ2VNVz27xilm6v5tJxOfzk4hEkx3f2r71IGGquh53LwbMESpdA6SdQ62FEbBpc+A2IDuzDeJ3922WsPbQNsbXWa4zR30yREPbS8jLuWbASgIeuHMvFY7XHqUQYrxcqN/oC9BMnRHetBtvmjHfrBT0Hw6CzWN2SxzjrBUIjVDcbY2bhzE4BvgVsDkxJInIy6ppauffF1cxfWsr4/j347RVjyU1PcrsskZO3v/LQDNSzBDxF0FjjjMUmQvogGH6RE6S9R0DWWOieA9Gx1C5eDDGxAS+xs6F6E84TwD/AWVbzBnBDoIoSkROzfEc1s+YVs6Oqnlln5TPrzMHEREe5XZbI8WttgvJVzgz0QJDu3eKMGQPd+0H2BMjId0I0dyL0GAAJ7jYvOWao+tan/sZae2UQ6hGRE+D1Wv709mYeeK2E3inxzLthChMHpLtdlkjnWAt7tzozzwP3QctXQFuzM57YwwnOfpOdEM0aB72GQnImRIXWPxqPGarW2jZjTH9jTJy1tjkYRYlI55XXNHLHs8t4f1MlF4zqy/9eMoruSYG/zCVywhqqoWwplBYdmonWVzpj0XGQPhDyZ0DGYOg1DLLHQ1ouxIT+Bg+dvqcKvGeMeQnYf+CgtfbBgFQlIp3y2upyvjt/BU0tXn516Wi+NCFHjfAltLS1QsVq331Q30x0T8mh8dRsyBzpzEQPXMbtOciZnYahzobqJt9/UUBK4MoRkc5oaG7jvlfW8OSH2xmZncpDVxYyqFey22WJQI2n3X3QImdG2trojMWnQM98GHW5E6B9x0CfkZCSFXKXcU9UZzsq/QTAGJPs+7gukEWJyJGt3VnLrLnFbKio48bTB/KdcwqIi4mMX0gSZpr3Q1nxoadxSz+BfeXOWFQM9MiDgdOcIO01BHImQlo/iIvcp9E721FpJPAEkO77eA/wVWvt6gDWJiLtWGt57P2t/OLVdXRPjOWJ6yfyufxebpclXYXX61y2bR+gFWvB+vZWSc50wnPIuc4sNGcC9BwC3Xq6W3eQdfby76PAHdbatwCMMdOAPwNTA1SXiAALiz3MWVRCWXUDcTFRNLV6OWtob3512Wh6Jof+QxsSxuoq2gWo735os+8iZWySE5zDZzpv+4yGvqOd+6PRXbsvUGf/9N0OBCqAtXaxMaZbgGoSEZxAvXvBShpanO4wTa1eYqMNF47uq0AV/2ppdJawHOhK5FkC1dudMRMFaf2h3xQnQDMGQ+4kZ01ovO7jf9rxdFT6Ic4lYICrUUclkYDZVdvIj15cdTBQD2hps/z6tfVcMi7Hpcok7FkLVZsPv4xbvgq8Lc54Uk8nPPNOdwI0ezxkFEByb6fpghzVUUPVGPOEtfYa4B0gD1jgG3ob+HpgSxPpWuqaWvn3qnIWFnt4b9MeDnXbPlxZdUNwC5PwVl8FnqWHt/dr2OuMxcQ7rf0KznOaKvQeDlmF0D0XYuLcrTtMHWumOt4YkwVcC0zH2Uv1wF91/ZNF5CS1tHl5Z8NuXigu4/U15TS2eOmXnsStZ+Yz7+PtVOxr+sznZKUlulCphIW2Fti1yteVyBeglRt9g8bpg9t3rO8ybj7knOKsCU3o7mrZkeRYofpHnD6/A4El7Y4fCNeBAapLJGJZa1leWsPCYg//XF5G5f5meiTF8qXxucwszGZcvzSMMQzM6HbYPVWAxNhoZs8ocLF6CRnWQs2Ow5sq7Cx2euaCE5Q982H0lU6IZo2FzBGQ3Cdi1oSGoqOGqrX2YeBhY8wj1tqbg1STSETaVrmfhcVlLFzmYcue/cTFRHH2sEwuKczm9CG9PrPWdGahs1Xbgad/s9ISmT2j4OBx6WKa9rW7jFsEpR/D/t3OWFQspA+AQWc5M9CMoc690B79ITbB3bq7mM42f1CgipyAqv3NvLyijBeKPSzdXo0xMHlAT24+YxDnjupDasLRe/TOLMxWiHZF3ja61W2FoscOXcatWMfBu28pfaBXAQy90Lcm9BQnTJO0iYLbuvaCIpEAaGxp4421FbxQ7GFxSQWtXktBZgp3nTeUi8Zk6Z6ofNa+8kO7s3iKwFPEKS31zlhcshOcIy89vLVfajZEBXbDbTl+ClURP/B6LR9uqWRhsYdXV5azr6mVzNR4rj9tADMLsxnW1909HiWENNfDzuWHnsYt/QRqPc5YVLSzJjTvNNbaAQwbOsy3JrQ/xKk1QDhQqIqchHXltbxQ7OGlZWXsrGkkOT6Gc0f24ZLCbCYP7El0lB6S79K8Xufp2/YBums1WN/DZ916ObPPQWc5a0KzJjg9crtlsOu//2XYhGmuli/HT6EqcpzKaxp5abmHF4rLWLuzlpgowxlDevH984fx+WGZJMbpklyXtb/y8PWgniJorHHGYhKh50AY9gUnSDNHOk/kds+BaO1/GykUqiKdsK+xxWnMsMzD+5sqsRYK+6Xx04tHcMEotQ3sklqbnE5EB7oSlS6BvVucMWOgez/InnCotV/ORGfz7QTdCohkClWRIzjQmGHBUg+vr9lFU6uX/j2TmHVmPjMLsxmQoXtcXYa1sHfrofWgniXOfdG2Zmc8sYcTnv0mO0/hZo2DXkOdnVu0JrRLUaiKtGOtZdmOaqcxw4qdVPkaM1xxitOYoTDXacwgEa6xxhegRYdmovWVzlh0nDPjzJ/hzEB7DXPWhKblOm3/pEtTqEqXdGBLNU91A9kfvsl1U/PY39zKwmIPWyvriY+J4uzhhxozxEZrthGx2lqhYk27h4mWwJ71HFwTmprl3P/sOdj5L/cU521iD1fLltCkUJUu59NbqnmqG7jvlbUATB3Uk29NH8y5I4/dmEHCVI3n8D1Cy5ZCi2+TgvgUJzBHfenQmtDMEU6wak2odIJCVbqc+15Z+5kt1QD6pMbz9Dcnu1CRBEzzfihb5muq4LuMu6/cGYuKgR55MOAMp0duryHOw0Rp/SAuydWyJXy5FqrGmGicJv0ea+2FxpgBwDygJ1AEXGOtbXarPoksLW1eFq0u5+/vbWV3Bzu/AOyq7fi4hAmvF/aUtNsndNlnk7MAABrTSURBVAlUrD20JjS5tzP7zD/HCdGcUyBjCHTr6W7dElHcnKneBqwFDjxf/kvgN9baecaYPwLXA4+4VZxEhqr9zcz9eDtPfriNnTWN9O+ZRPfEGGoaWj/zWrUPDDN1uw9vqlC21Gk6DxCb5GxpNvxiJ0j7jIK+oyE1B6J1gU4Cx5WfLmNMDnABcB9wh3EepzwT+IrvJY8DP0ahKido7c5aHntvKwuXeWhq9XLa4Ax+PnMk0wp688/lZdpSLdy0NEL5isNnodXbnDET5VyyzZ3kzEAzBvta+w2A+GR365Yux1hrj/0qf39TY54HfgGkAHcC1wEfWmsH+8ZzgVettSM7+NwbgBsAMjMzx8+bNy9YZYeEuro6kpP1i6IjXmsprmjj9W0trKvyEhcFU7NjOLt/LNnJhz+9+35ZC/PXt1DZ6KVnQhSXDollapYeTDoZfvvZtJbEhnJS9pWQWrue1Nr1JNdtIco6Vxca43pSmzyQfSmDqE0ezL6UwXjjUpx7pBFEf9f9y5/nc/r06UXW2gkdjQX9p9AYcyFQYa0tMsZMO97Pt9Y+CjwKMGHCBDtt2nF/ibC2ePFiutqf+Vhq6lt4dskOHv9gK6V7m8hOS+Tu8/pzxSm5pCXFdfg504Dvo/PpTyd8Luur2u0T6puJNux1xmLinTWhOec5s9DMESRkFZLQPZfeMR3/v40U+tn0r2CdTzf+aXcqcJEx5nwgAeee6kNAmjEmxlrbCuQAHhdqkzCysWIfj72/lflFHhpa2pg4IJ0fXOD0343RutLQ1NYCu1YdWs5S+onTcB4AA92zoe/YdmtCfa39EtNcLVuks4Ieqtbau4G7AXwz1TuttVcZY54DLsN5Avha4MVg1yahz+u1LF5fwd/f28o7G/YQFxPFxWOyuO7UPEZkdXe7PGnPWqjZ0S5Al8DOYqdnLkBCdyc4R1/hvM0qdNaEJvdRaz8JW6F0E+J7wDxjzM+BYuCvLtcjIWRfYwvPF5Xy+Ptb2VpZT2ZqPHeeM4QvT+ynZvYhIrq1Hra87Wsu75uF7q9wBqNiIT3Pt8VZPmQMdVr79egPsQmu1i3iT66GqrV2MbDY9/5mYKKb9Ujo2bpnP4+9v5Xni0qpa2qlsF8ad5xTwHkj+6h1oJu8bbB7XbuncT/htIoSeNfrjKf0cZopDL3AmYXmnOKEaVK6u3WLBFgozVRFAKep/bsb9/DYe1t5s6SCmCjDBaP6ct2pAxibq3trrthXfvhylrKlTrcigLhu0HMw23IuJm9QgbMetM8oSM1Waz/pchSqEjLqm1tZsNTD4+9vZUNFHRnJcdx6Zj5XT+pH71RdIgya5npnW7P2DeZrS50xE+1csu1/qm+f0HzInQw9+rP1/U/I09Oq0sUpVMV1pXvreeKDbcz9eDu1ja2MzE7lgS+N4cIxfYmP0UwnoLxeqNp0aJNtzxJn4+0Drf26ZTjhOWi68zZ7AvQqcI5rCzyRz1CoiiustXy0pYrH3tvKa2vKMcZw7og+fO3UPMb376E9SwOlvurwy7ieJc7eoQAxidBzIAz7ghOgmSMhawx0z4VoNcYQ6QyFqgRVY0sbLy0r4+/vb2XtzlrSkmK58YxBXDO5v3rv+ltrM5SvPDxAqzY7Y8Y4YZk9/lBrvxzfmtCE1KN/XRE5IoWqBEV5TSNPfLiVuR/voGp/MwWZKdz/xVHMLMwmIVaXeE+atU4v3NJ2AbpzObT5NnpK7OFrpjDJuQ+aNQ56DYXkTK0JFfEjhar41cJiD3MWlVBW3UBWWgKXjc9l8579vLpyJ23WcvawTK47NY8pA3vqEu/JaKxxWvu1W9JCfaUzFh3nzDjzz3GCtNcw35rQfk7bPxEJGIWq+M3CYs9hu794qht56I0NxMcYrpuax7VT88hN1+bPx62tFSrW+MLT11Rhz3rAtxlGapbTiahnvm82eorzNrGHq2WLdEUKVfGbOYtKDttO7YD0bvH84MLhLlQUpmrL2j2NW+SsCW1pcMbiU5zAHHWZE6J9xziBmpqlNaEiIUChKn5TVt3Q4fHymsYgVxJGmvdD2bJDl3BLl8C+nc5YVAz0yIMBZ/gu4xY4DxOl9YM4zfhFQpFCVfwmNTGGmobWzxzXU70+Xq9z2bZ9gFasPbQmNLm3E575Zzuz0AOt/bpluFu3iHSaQlX8YpWnhrrGVqIMeNvte58YG83sGQXuFeamut2HL2fxFEHTPmcsNgl6DoLhFzlB2meUcyk3NQei9ddSJFzpb6+ctP1Nrdw6t5iMlHhuPTOfRxZv8j39m8jsGQXMLMx2u8TAa2mE8hWHN1ao3uaMmSjnkm3upENrQnMnQY8BEJ/sbt0i4lcKVTlpP3pxNVsr9/P0NyYzZVBPrp7c3+2SAstap4nCgU22S5c4TRa8Lc54Uroz+8w77VNrQnurtZ9IhFOoyklZWOxh/tJSZp2Vz5RBPd0uJzAa9voCtOjQ/dCGvc5YTLyzJrTgXGcW2ns4ZI9zuhXFxLlbt4gEnUJVTtjWPfu554WVTMxLZ9aZg90uxz/aWmDXqkPLWUqXQOUG36CB7tnOvc/2a0LTB0GitqQTEYWqnKDmVi+3zi0mJjqK3145lphw3DDcWuIbd8OqBYcCdGcxtDY54/Hdnfufo69wAjSr0FkTmtxHrf1EpEMKVTkhv/r3OlZ6avjTNePDZ8lM0z4oKz7UH7f0E6bsr3DGomIhPQ8GneVbEzrU2easR3+I1V6uItI5ClU5bm+VVPCXd7dwzeT+zBjRx+1yOuZtg90lzv3PA0/j7l4H1uuMp/SBXkPY0Otc8ocUQM4EyBjiPGQkInKCFKpyXCpqG7nz2eUM7ZPCPRcMc7ucQ/aVH76cpWyp060IIK6bM/sccYnztu8YZ11oajaet98hf+o0V0sXkcihUJVO83ottz+7jPrmNv7vK4XubdnW0uBsa+a7hItnCdSUOmMm2rlk2/9UJ0B7DoZ+U5xjcd3cqVdEugyFqnTaI//dxHsbK/nlpaMY3DslON/U64WqTYfPQnetAq+vHWK3DCc4B0533mZPcHrkdsvQmlARCTqFqnRK0bYqHnx9PReO7svlE3ID9432VzpP4h5s71cEjdXOWEwi9BwIQy9wlrRkjoCssc6a0OjYwNUkItJJClU5ppqGFmbNXUZWWgL/+8VR/ttcvLXZ6UTUvj9u1WZnzBgnLLPH+S7j5kPuRKfRQkKqf76/iIifKVTlqKy13L1gBbtqG3nupimkJpzgjNBapxfuwaYKnzj3RduanfHEHr5mCpN8a0LHQe9hkJypNaEiEjYUqnJUcz/ewSsry7nrvKEU9uvR+U9srAHPUt8stAg8n8D+Pc5YdBykDzi0xdnBNaH9nLZ/IiJhSqEqR7R+1z5+8s/VfC4/gxs+N/DIL2xrhd1rfc3lffdDd5cAvj3gUrKcnrgHL+P6WvtpTaiIRBiFqnSoobmNW55eSkpCDA9cPoaoqHb3UWvL2i1nKXLWhLY0OGNxKU5rv1GXHVoTmjkSUrMgyqUlOCIiQaJQlQ797OU1rN9Vxz++OprelUWw4lBrP/btdF4UFQ098mDAGU6AZgyBnIm+NaFJrtYvIuIGhaoc4vVC5QZe/WAZT3+UzI2pH3D6c9eAbXPGu/V2ZqEH7oXmnOLsF9otw926RURChEK1K9u/5/CuRJ6llDbG8b2mXzAmajN39ngbMi5yZqGZI501oak5EK0fGxGRjui3Y1fR2uSsCS395FCQVm9zxkwUdO9HS84kZu34IrYtkd9dcxqxeddBfJA6J4mIRACFaiSy1mmicGCPUM8SJ1APrAlNSndmn3mnHbYm9Lfv7WXp6k08/OVC+hVkuftnEBEJQwrVSNBQfXiAln4CDXudsZh4pwvRkBnOfdDewyB7vNOtKCbu4Jd4f+Me/vDfTVw+IYeLxihQRUROhEI1zBhvK5QtO7ScpXQJVG44MArds51lLJ9eE5qYdsSvWVnXxLefWcbAjG78+KIRwfmDiIhEIIVqKLMWaj2H7oN6ijittAje9l3GjU91nr4dfYVvTWghZA5zmi10srWf12u587nlVDe08NjXJpIUpx8JEZETpd+goaRpH5QVH94ft26XMxYVC+l5lGVOJ3dAAWQUOEtaevSD2MQT/pZ/e28Lb5Xs5qcXj2B4lhrVi4icDIWqW7xtTiu/A/dAS5fA7nVgvc54ch9nFlpw3mFrQjd9vILcadP8UsLK0hp++e91nDM8k2sm9/fL1xQR6coUqsGyb9fhAVq2FJr3O2Nx3SB9MIy4xLmM22c09B0NqdkBa+1X19TKrXOXkpEcz68uG+2/7dxERLowhWogtDQ425q1fxq3ptQZM9FOG7/+p/oeJhoMuZMhPc8J1yD54cJVbK+qZ94NU0hLijv2J4iIyDEpVE+W1wtVm9oF6BLYtQq8rc54twwnOAdOd95mT4BeQ6BbL2cjbhfMLyrlhWIPt39+CBMHaKcYERF/Uager/qqQ0tZDixraax2xmISoOcgGHqBcx80czhkFTprQqNPcHNvP9u8u44fvriKSQPSueXMwW6XIyISURSqR9PaDLtWOnuEHuiPW7XZGTMGUnMhe1y7NaETnUYLCaH5FG1Taxu3zi0mLiaK3145lugo3UcVEfEnheoB1jq9cA8uZ1kCO5cdau2XkOY8jZs76bDWfiRndnpNqNvuf3Udq8tq+fNXJ9C3+4kvwxERkY4pVAEW3Aib/uPs2gIQHQfpAw5tcdZrqNPar0d/p+1fGHpj7S7+/t5Wrpuax9nDM90uR0QkIilUwQnK3sM/29ovKTIe4imvaeTO55YzvG8qd5031O1yREQilkIV4KKH3a4gYNq8lm8/U0xTq5fffaWQhNjArHsVERGFasT7w1sb+XBzFXMuG82gXslulyMiEtHC4wkbOSGfbK3iN/9Zz8Vjs7hsfI7b5YiIRDyFaoSqrm/mtrnF5KYn8fOZI9WGUEQkCHT5NwJZa/ne/BXsrmti/s1TSUkIjcYTIiKRLugzVWNMrjHmLWPMGmPMamPMbb7j6caY140xG3xvewS7tkjx5EfbWbR6F9+dMZTROUfenFxERPzLjcu/rcB3rLXDgcnA/xhjhgN3AW9Ya/OBN3wfy3Fau7OWn/1rDWcM6cX1pw1wuxwRkS4l6KFqrd1prV3qe38fsBbIBi4GHve97HFgZrBrC3f1za3cOreY7omxPHD5GKLUhlBEJKiMtda9b25MHvA2MBLYbq1N8x03wN4DH3/qc24AbgDIzMwcP2/evKDVGwrq6upITu54aczfVjXxTmkrd05IYESG1qN2xtHOpxwfnUv/0vn0L3+ez+nTpxdZayd0NObag0rGmGRgPvBta21t+6dTrbXWGNNh2ltrHwUeBZgwYYKdNm1aEKoNHYsXL6ajP/M/l5fxdmkx35o2iP85V12TOutI51OOn86lf+l8+lewzqcrS2qMMbE4gfqUtXaB7/AuY0xf33hfoMKN2sLRjqp6vr9gJYX90rj97CFulyMi0mW58fSvAf4KrLXWPthu6CXgWt/71wIvBru2cNTS5uXWucVg4OErC4mN1tJjERG3uHH591TgGmClMWaZ79j3gfuBZ40x1wPbgMtdqC3sPPDaepbtqOb3XxlHbnqS2+WIiHRpQQ9Va+27wJEeSz0rmLWEu3c27OaP/93ElyfmcsHovm6XIyLS5elaYZjava+J259ZTn7vZH504Qi3yxEREdSmMCx5vZbvPLecfY0tPPWNSSTGafmMiEgo0Ew1DP3l3c28vX43P7xwOAV9UtwuR0REfDRTDRMLiz3MWVSCp7oBWMfonFSumtTP7bJERKQdzVTDwMJiD3cvWOkLVMf6XXW8uKzMxapEROTTFKphYM6iEhpa2g471tjiZc6iEpcqEhGRjihUw0BZuxlqZ46LiIg7dE81hFlr+ccH2zjSlgdZaYlBrUdERI5OoRqiKuua+O7zK3hjXQXD+qSwpXI/jS3eg+OJsdHMnlHgYoUiIvJpCtUQ9M6G3dzx7HJq6lu49wvDuW5qHi8uKzv49G92WiKzZxQwszDb7VJFRKQdhWoIaW718uvXSnj07c0M7p3MP74+kWF9UwGYWZjNzMJsbQclIhLCFKohYvPuOmbNK2aVp5arJvXjBxcMV6ckEZEwo1B1mbWW55aUcu9Lq4mPjeJP14xnxog+bpclIiInQKHqopr6Fr6/cCUvr9jJlIE9+c0VY+nTPcHtskRE5AQpVF3y8ZYqbn9mGbtqG/nuuQXcePogoqOOtCOeiIiEA4VqkLW2eXn4zY3835sbyE1P4vmbpzI2N83tskRExA8UqkG0o6qebz+zjKJte/niuGx+evFIkuP1v0BEJFLoN3qQvLS8jHsWrATgoSvHcvFYrTEVEYk0CtUAq2tq5d4XVzN/aSnj+qXx0JWF5KYnuV2WiIgEgEI1gJbvqOa2ecVsr6pn1pmDmXVWPjHR2sNARCRSKVQDwOu1/OntzTzwWgm9U+KZ+83JTBrY0+2yREQkwBSqflZe08gdzy7j/U2VnD+qD7+4ZDTdk2LdLktERIJAoepHr60u57vzV9DU4uWXl47i8gm5GKO1pyIiXYVC1Q8amtu475U1PPnhdkZkpfLwlwsZ1CvZ7bJERCTIFKonae3OWmbNLWZDRR3f/NwA7pxRQHyMGuGLiHRFCtUTZK3lsfe38otX15GaEMs/vj6R04f0crssERFxkUL1BOypa2L2c8t5q2Q3Zw7tza8uG01GcrzbZYmIiMsUqsfpv+t3851nl1Pb2MKPvzCca6fm6WEkEREBFKqd1tTaxpx/l/CXd7eQ3zuZJ66fyLC+qW6XJSIiIUSh2gmbdtcxa24xq8tquWZyf+65YBgJsXoYSUREDqdQPQprLc98soOf/HMN8bFRPHrNeM4Z0cftskREJEQpVI+gpr6Fu19YwSsry5k6qCcPXj6WPt0T3C5LRERCmEK1Ax9truT2Z5ZRsa+Ju84byg2fG0hUlB5GEhGRo+vyobqw2MOcRSWUVTfQNy2BkVmp/GdtBf3Sk5h/81TG5Ka5XaKIiISJLh2qC4s93L1gJQ0tbQCUVTdSVt3IxLwe/O1rE0mO79KnR0REjlOX3txzzqKSg4Hanqe6UYEqIiLHrUuHall1w3EdFxEROZouHapZaYnHdVxERORounSozp5RQOKnmjgkxkYze0aBSxWJiEg469I3DmcWZgMcfPo3Ky2R2TMKDh4XERE5Hl06VMEJVoWoiIj4Q5e+/CsiIuJPClURERE/UaiKiIj4iUJVRETETxSqIiIifqJQFRER8ROFqoiIiJ8oVEVERPxEoSoiIuInClURERE/UaiKiIj4ibHWul3DCTPG7Aa2uV1HkGUAe9wuIoLofPqPzqV/6Xz6lz/PZ39rba+OBsI6VLsiY8wSa+0Et+uIFDqf/qNz6V86n/4VrPOpy78iIiJ+olAVERHxE4Vq+HnU7QIijM6n/+hc+pfOp38F5XzqnqqIiIifaKYqIiLiJwpVERERP1GohgFjTK4x5i1jzBpjzGpjzG1u1xQJjDHRxphiY8y/3K4l3Blj0owxzxtj1hlj1hpjprhdUzgzxtzu+7u+yhgz1xiT4HZN4cQY8zdjTIUxZlW7Y+nGmNeNMRt8b3sE4nsrVMNDK/Ada+1wYDLwP8aY4S7XFAluA9a6XUSEeAj4t7V2KDAGndcTZozJBmYBE6y1I4Fo4Ep3qwo7jwHnfurYXcAb1tp84A3fx36nUA0D1tqd1tqlvvf34fzCyna3qvBmjMkBLgD+4nYt4c4Y0x04HfgrgLW22Vpb7W5VYS8GSDTGxABJQJnL9YQVa+3bQNWnDl8MPO57/3FgZiC+t0I1zBhj8oBC4CN3Kwl7vwW+C3jdLiQCDAB2A3/3XU7/izGmm9tFhStrrQf4NbAd2AnUWGtfc7eqiJBprd3pe78cyAzEN1GohhFjTDIwH/i2tbbW7XrClTHmQqDCWlvkdi0RIgYYBzxirS0E9hOgS2tdge9e38U4/1jJAroZY652t6rIYp21pAFZT6pQDRPGmFicQH3KWrvA7XrC3KnARcaYrcA84ExjzJPulhTWSoFSa+2BqyfP44SsnJjPA1ustbuttS3AAmCqyzVFgl3GmL4AvrcVgfgmCtUwYIwxOPer1lprH3S7nnBnrb3bWptjrc3DeQDkTWutZgInyFpbDuwwxhT4Dp0FrHGxpHC3HZhsjEny/d0/Cz345Q8vAdf63r8WeDEQ30ShGh5OBa7BmVEt8/13vttFibRzK/CUMWYFMBb4X5frCVu+Gf/zwFJgJc7vabUsPA7GmLnAB0CBMabUGHM9cD9wtjFmA87VgPsD8r3VplBERMQ/NFMVERHxE4WqiIiInyhURURE/EShKiIi4icKVRERET9RqIqEKGNMXQC+5tj2y7GMMT82xtzZwevyjDENxphlx/n1rzDGbNTOP9JVKVRFupaxQGfXOG+y1o49ni9urX0G+MZxVyUSIRSqImHAGDPbGPOJMWaFMeYnvmN5vr1L/+zbe/M1Y0yib+wU32uXGWPm+PbljAN+ClzhO36F78sPN8YsNsZsNsbMOsL3z/PtlfqYMWa9MeYpY8znjTHv+fannBiUEyES4hSqIiHOGHMOkA9MxJlpjjfGnO4bzgd+b60dAVQDl/qO/x240TfTbANnSzbgR8Az1tqxvlklwFBghu/r3+vrM92RwcADvtcPBb4CnAbcCXzfT39ckbCmUBUJfef4/ivGaV03FCdMwWm8fuC+ZxGQZ4xJA1KstR/4jj99jK//srW2yVq7B6fJ+JG2xNpirV1prfUCq3E2fLY4rfTyTuDPJRJxYtwuQESOyQC/sNb+6bCDzt66Te0OtQGJJ/D1P/01jvR7of3rvO0+9h7lc0S6FM1URULfIuDrvv10McZkG2N6H+nF1tpqYJ8xZpLv0JXthvcBKQGrVKSLU6iKhDhr7Ws4l3A/MMasxNnB5FjBeD3wZ9+SmG5Aje/4WzgPJrV/UElE/ES71IhEIGNMsrW2zvf+XUBfa+1tx/H5ecC/rLUjT+B7TwPutNZeeLyfKxLuNFMViUwX+Gajq4DPAT8/zs9vA7qfSPMH4A/A3uP8fiIRQTNVERERP9FMVURExE8UqiIiIn6iUBUREfEThaqIiIifKFRFRET85P8BFzc8PRVsnTQAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# You can also add callable functions to a Plot\n", + "\n", + "def func(x):\n", + " return 20 + x * 3\n", + "\n", + "figure.plot(func)\n", + "\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# You can also plot a function with parameters\n", + "def func2(x, *pars):\n", + " return pars[0] + x * pars[1]\n", + "\n", + "# You can specify an xrange for the plot, and also when plotting a function with \n", + "# parameters, you have to specify the parameters values too.\n", + "figure.plot(func2, xrange=(4,8), pars=[-10,8])\n", + "\n", + "figure.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plotting Histograms" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The QExPy plotting module is also capable of plotting histograms." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# first let's generate a bunch of random numbers\n", + "import numpy as np\n", + "samples = np.random.normal(5, 0.5, 10000)\n", + "\n", + "# Let's plot it out as a histogram. Note that the return values include the array\n", + "# of counts, the bin edges, followed by the Plot object.\n", + "n, bins, figure = plt.hist(samples, bins=100)\n", + "\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdUAAAFhCAYAAAAm4jCUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deXxdd3nv+8+z9ixtjbal2HESZ3AGEiAkJgOkqZ2QEmiD6S1t4bSUtvSkPaXc08LrtPTcc3tb+uo5tOdSKGFow1DCZTCBAhkamgQnJnEgcezMnmInnmNLtqxZ2vPv/rGWZNmRrWlLaw/f9+ull/dee9Cjbe396Pes3+/5mXMOERERmTsv7ABERERqhZKqiIhImSipioiIlImSqoiISJkoqYqIiJRJNOwAABYvXuxWrFgRagzDw8M0NjaGGsNcKP5wKf5wKf5w1Vv8W7ZsOeacWzLZbRWRVFesWMHmzZtDjWHDhg2sXr061BjmQvGHS/GHS/GHq97iN7N9p7tN5V8REZEyUVIVEREpEyVVERGRMlFSFRERKRMlVRERkTJRUhURESkTJVUREZEymTKpmlnSzDaZ2fNmttXM/iY4/nUz22NmzwVfVwbHzcw+Z2a7zewFM7tqvn8IERGRSjCd5g9Z4Cbn3JCZxYCNZvbj4Lb/5pz7/in3fxewMvi6FvhS8K+IiEhNm3Kk6nxDwdVY8HWmnc3XAt8IHvck0GpmS+ceqoiISGUz586UH4M7mUWALcBFwBecc39hZl8Hrscfya4HPuGcy5rZ/cCnnHMbg8euB/7CObf5lOe8HbgdoLOz8+p169aV76eahaGhIdLpdKgxzIXiD5fiD5fiD1e9xb9mzZotzrlVk97onJv2F9AKPApcASwFDEgAdwF/FdznfuCGCY9ZD6w60/NeffXVLmyPPvpo2CHMieIPl+IPl+IPV73FD2x2p8lnM5r965zrC5Lqrc65w8HzZ4F/Ba4J7nYIOGfCw5YHx0RERGradGb/LjGz1uByCrgF2DF2ntTMDHgv8FLwkHuB3wlmAV8H9DvnDs9L9CIiIhVkOrN/lwJ3BedVPeBu59z9ZvaImS3BLwE/B/xRcP8HgHcDu4ER4PfKH7aITMdtd2wcv3zfR28IMRKR+jBlUnXOvQC8ZZLjN53m/g74yNxDExERqS7qqCQiIlImSqoiIiJloqQqIiJSJkqqIiIiZaKkKiIiUiZKqiIiImUynXWqIlIDJq5ZPdXaziFWL1woIjVLSVVEJnVqElbzCJGpqfwrIiJSJkqqIiIiZaKkKiIiUiZKqiIiImWipCoiIlImSqoiIiJloiU1InXKOUeuWGI0V6RYcvzw2UOkYh4NsSgXn9U05eO15Ebk9ZRURepMyTlGcwX6RwsMZQvkCiU+P2Q0791JYzxCezrOtSvaGczkaYhHiXgGnLl5hIj4lFRF6ki+WOTIQJahTIFCyY0fH8wbg32j/pVu2Ly3l2QsQjpRpDERJRWPYFhIUYtUDyVVkTqRyRfZ1zNCrlgCIGJGUzJKYyLCe88eYdGKKxjM5nlqz3F+9koPQ9kCI7kCzfkSbY1xGhNKrCJTUVIVqQPDuQL7e0YolBwRz2hriNEQi1B0/rnVphgkYh6JWIJffuNSrlnRzpd++grZQom+0TzFksNI0JiIgBKryGkpqYrUuIHRPAd6Ryg5iEWMJekEGBScIxmNkIh5RD3jtiuXkS+UyBRKvHCgj/aGGJlCid6RPIPZAp6BWYKGuBKryOkoqYrUsKFMgf3HR3BAIuqxuDGOM2iIR0lEPTw7kRybk7Hxy0ubk3z2Jy8TyRUpOUf/aIH+TAEMljQlScUiIfw0IpVPSVWkRvWN5Njf6yfUhniEtoYYYDQno8QiZ16i7nlGPBohFvFIRD2cg4GMP2PYI0tnS3LK5xCpR3pXiNQg5xx/8u1nKZYc8YjRmophZrSkYjNKhmZGQzzC4qYETQn/b/De0Ty9IzlKzk3xaJH6o6QqUoO+8vgeNu4+hgFtDXGiEY+WVGx8zenMBIk1HachKPseH84xkiuUNWaRWqCkKlJjXjrUzz88uAOAtoYYsahHUzJ60vnTmTKMxmSUJU0JIgb5ouPYUI58sDxHRHxKqiI1JJMv8sffeoZ80XHdBe1BA4e5JdQxhtGUirIonQBgMFOgbySHUxlYZJwmKonUkH/6yS72Hx+hoynBzZd18N1NB6d9DnU6bQgNY3E6ETSGKNIznKMhHqExEZvysSL1QCNVkRrRPZjha0/sAeBX37KM8xc1koyX/y0e8YylLUm8CWXgYkmjVRFQUhWpGf/rgR1kCyWuOLuZC5akuf7CxfPWVjAVj5xUBh7KatKSCKj8K1ITdh4e4EfPHQKgqz/D1zbu4ZtP7p+372f4nZkGR/NkCiV6hrL0jeRobYjP2/cUqQYaqYrUgL+5fxvOQWM8QioeJRad/7d2xDM6mv3R6nCuyGMvH5337ylS6ZRURarc47uO8rNXekhEPdKJKA0LuE1bcyo23rLwW0/tp3c4tyDfV6RSKamKVLFSyfHJ+7YBcOPKxaTikQVtH2gYZwWj1c37enl0Z9eCfW+RSqSkKlLFHtx6hF3dQ7SkYlx3wSIa4gs/TSKd9EfHxZLjW08doGcou+AxiFQKJVWRKvaxu58PLjm+tWn/LNsQzpXR2ZzEgGf39/KTbRqtSv2aMqmaWdLMNpnZ82a21cz+Jjh+vpk9ZWa7zey7ZhYPjieC67uD21fM748gUp+e2d/LaL6Imb8LTZjbsaUTEa67cBElB+uePkD/SD60WETCNJ2Raha4yTn3ZuBK4FYzuw74e+AzzrmLgF7gw8H9Pwz0Bsc/E9xPRMrsi4/uBvwZv42JWFlaEc6e8efvvAQDnj/Yx6a9PSHGIhKeKZOq8w0FV2PBlwNuAr4fHL8LeG9weW1wneD2m81CfbeL1JwDx0dYv6MbgMaEv+F42P7qnq2k4hFKDj723efJFophhySy4Gw6zbDNLAJsAS4CvgD8b+DJYDSKmZ0D/Ng5d4WZvQTc6pw7GNz2CnCtc+7YKc95O3A7QGdn59Xr1q0r3081C0NDQ6TT6VBjmAvFH66Fjv+b27L8ZH+BK9oc71kBcz2V2hor0pefe/l47yB8c5fREHX8w40p0vGFKUnr9ydc9Rb/mjVrtjjnVk1227SmCjrnisCVZtYK/BC4dNrf/fTPeSdwJ8CqVavc6tWr5/qUc7JhwwbCjmEuFH+4FjL+/pE8f7T+JwB05RP86EhyzhOU1nb2cU9X65xjc84RiwwyUoCfD3fwt++4Am8BJk/p9ydciv+EGdWMnHN9wKPA9UCrmY0l5eXAoeDyIeAcgOD2FkAnWETK5Bs/30smX+KiJWkaE9GQZvxOzsxob/RbFT66o5uugUzIEYksrOnM/l0SjFAxsxRwC7AdP7m+L7jbh4B7gsv3BtcJbn/EacNFkbLIFUrc9fO9AFx/ob9faqVZlI7jGbzWn+H+F14LOxyRBTWdkepS4FEzewF4GnjYOXc/8BfAx8xsN7AI+Gpw/68Ci4LjHwM+Uf6wRerT+u1dHBvK0dGU4NoL2olFKmeUOiZiHi0pf3/Ve58/TP+oltdI/ZjynKpz7gXgLZMcfxW4ZpLjGeDXyxKdiJzkW0/5O89cdW4bb17eCgvU43emFqcT9I/m2fpaP5te7eGWy88KOySRBRH+PHwRmZbD/aP87JVjeOaXfpe1NoQd0mklYxGuOb+dkoPvPL2fQrEUdkgiC0JJVaRK3P30QUoOLj2rmbeuaK+oCUqT+c83XADA03t7ea1fE5akPiipilSBUsnx3c1+6fetK9o4d1FjyBFN7abLOuhoSjCYKXDPs4emfoBIDVBSFakCT77aw2t9GZqTUW5cuZh0YuF3o5kpM2PtlcsAeHh7FyO5QsgRicw/JVWRKvDtTcEEpfPauHRZS8jRTN9vX3ceANteG+DlI4MhRyMy/5RURSpc/2ieh7b626lde347nU2JkCOavvMWNfLGs1solBzrnj6AlqxLrVNSFalw9zx7iFyxxPmLG3nbhYuIRqrrbfvrq5YD8LNXjtGnLeGkxlXXu1OkDo2Vfled18Z5VTBB6VS/dtVy4lGP/cdHec/nN3LbHf6XSC1SUhWpYC93DbLjyCDJqMcvrFxMa0M87JBmrDERZfXFSwDoG8mrBCw1TUlVpIKNLUW5bFkzb1pePROUTvWfrj0XgJFcgbwaQUgNq/x5+SJ1yjnHj57zG9JfdW4rS1tTIUc0ezeuXELUMwolR/9oniVNkZNKwPd99IYQoxMpHyVVkQr1/MF+DvWNkk5EuOmSDhLRytuR5kxOPW/akorRM5xjYDTP4nQCs8ruCCUyGyr/ilSoe57zS7+XL2vhwo6mkKOZu/ZGf+ea0XyJnErAUqOUVEUqULHkuO95v/S7akUbi9PVN0HpVMlYlHjEw4G2g5OapaQqUoGe2tPDsaEcrakYqy9eUnVrU0+nJeWfcRoY1SxgqU218U4VqTH3POuPUt+4vIXzFlff2tTTaWvwS8CZfImsSsBSg5RURSpMrlDigZcOA3DNinYWN1ZPW8KpJGJR4lG/BDygErDUICVVkQrz+K6jDGYKLEnHufHixXgVvm/qTLUkVQKW2qWkKlJhfhQ0fHjTOa2c0147pd8xKgFLLVNSFakgmXyR9Tu6AbjugvbxBFRLTioBj+TCDkekrNT8QSREpzZI+NN3rGQkV6SjKcHbL1xcsw0SWlJRjg7mGMgUWNKkErDUDo1URSrIj186AsDly5o5u7Uh5Gjmj0rAUqs0UhWpEM45frJtbDPyRbRMUvqtlS3TElG/BJwrlOjXHqtSQzRSFakQw7kifaN5Whti3LByUdjhzLuxRhCDmQIFjValRiipilSIsXWbly2t7dLvmNZUUAIuFOkezIQcjUh5qPwrUgGccwxk/KR69bmttAal31op904mEY0Qixj5omP99m4+eP2KsEMSmTONVEUqQCZfIl90NMYjrLm0o2Zn/U5kZjQHjSAe3talRhBSE5RURSrAQMZfr3nJWU2cW4MNH06nJSgBv3ion95hrVmV6qekKlIB+kcLAFx5TivtjdW/zdt0NcSjeAa9I3me3NMTdjgic6akKhKybKFItlDCgHdc1kGkxnr9nomZ0ZT0R6sPvHgk5GhE5k5JVSRkA8EoNRH1uGBJU8jRLLyxpTXP7OtlMKM1q1LdlFRFQjY267chHmFRun5Kv2OaEjHiEY/X+jNsPdQfdjgic6KkKhKiQrHESK4IQFMySixSf29JzzOuvaAdgPuePxxyNCJzU3/vYJEKMpj1S7/xiJGM1++y8Xe/8SwAntp7nEy+GHI0IrOnpCoSorEuSslYhGgdTVA61TsvX4oZvHp0iL3HhsMOR2TWlFRFQpIvlhgKRqrpRBSvDho+nE57Y5w3nt1CycH9L7wWdjgiszZlUjWzc8zsUTPbZmZbzey/Bsf/2swOmdlzwde7JzzmL81st5ntNLN3zucPIFKtNu/tpeQg4hnpRP2Wfsf80hs6AXjilR6KJXVXkuo0nZFqAfi4c+4NwHXAR8zsDcFtn3HOXRl8PQAQ3PZ+4HLgVuCLZhaZh9hFqtr6Hf42b8moR7QOJyid6tYr/POqOw4PcnQwG3I0IrMz5TvZOXfYOfdMcHkQ2A6cfYaHrAXWOeeyzrk9wG7gmnIEK1JLxvZObYhHUE6FC5ekOas5yWi+yOO7joYdjsis2EyaWJvZCuAx4ArgY8DvAgPAZvzRbK+ZfR540jn3zeAxXwV+7Jz7/inPdTtwO0BnZ+fV69atm+vPMidDQ0Ok0+lQY5gLxR+umcZ/ZLjEJx4fJRFx/NkbIRok1Ys6Tn6O3d1D5QzztFpjRfry4RWUxn7ub2zN8siBAu881+MDb0hN+/H19vtTaeot/jVr1mxxzq2a7LZpn8gxszTwb8CfOucGzOxLwN8CLvj308DvT/f5nHN3AncCrFq1yq1evXq6D50XGzZsIOwY5kLxh2um8X914x5gG2YRfnSkcXx96n2/ccNJ9/v0Am39trazj3u6Whfke01m7Od2Z3XxyNc380J/nE9ff8O0zzXX2+9PpVH8J0yr6GRmMfyE+i3n3A8AnHNdzrmic64EfJkTJd5DwDkTHr48OCYigfXb/dJvKl7fS2lOdf2Fi4lHPQ73Z9jxmrorSfWZzuxfA74KbHfO/eOE40sn3O1XgZeCy/cC7zezhJmdD6wENpUvZJHqNpjJs2nPcQy/i1I97J06XclYhLeuaAPg319Sg32pPtOprbwd+CDwopk9Fxz778AHzOxK/PLvXuAPAZxzW83sbmAb/szhjzjn1CJFJLBx1zEKJcfZrSlSMS2lOdUtl3XyxO4ennq1h3yxVJetG6V6TfmOds5tBCb7U/qBMzzm74C/m0NcIjXptjs2cqB3BPAb6bc2xEKOqPLccvlZ/PV929jdPUzXQIblbQ1hhyQybfoTUGQBOecYzPhdlBrjkbruonQ6Z7emWLGogVyxxPpt3WGHIzIjqj2JLKBMvkSx5PCMSWe23rZAs30r3ZpLO/jXJ/byyM4ufudt5/Gezz9x0u33ffSG0zxSJFwaqYosoMGs30A/EY0Qi+rtdzrvvNzvrvTSoYHxTdxFqoHe1SILaKz0m4pFiGgpzWmtOq+NxkSEnuEczx/oDTsckWlTUhVZIP2j+fENyZuTUWzS+X8CEI14vO2CRQD8x9aukKMRmT4lVZEF8rPdxwCIRYxEXHtMTOUdwa41W/YdZybtVEXCpKQqskAe3enPZE1G1UVpOtZc0gHAq8eGyRVLIUcjMj1KqiILwDnHhp3+ziuNdb4h+XR1NCe5cEkj+eKJZUgilU5JVWQB7OwapHswGyylUel3um68eAmAkqpUDa1TFVkAY6NULaWZ3Knrc8fWod7yhk7+9Ym9ZPJFiiWnGdNS8fTuFlkAj+4IzqfGPCWGGVh1XjvJqEeh5BjNa7QqlU8jVZF5NpQtsGVfb7ArTUxLaaZh4sg1GjEowMBogXRCvZKlsmmkKjLPntjt70pzVkuSVEznU2eqKekn0uFcQUtrpOIpqYrMs58GS2ku6WzyR10yI81BUs3mS+S1tEYqnMq/IvPoVz73ODu7BgHYfmSAjqZkyBFVn3jUIxYx8kXHULZIe1SjfalcGqmKzKNcoUS+6LDT7Eoj09MUnEsdzORDjkTkzJRURebRYNafsZqIeMS1lGbWmlL+6HQ0X6Sk86pSwfQuF5lHQ0HTgqR2pZmTdNwfqeaLjky+GHI0IqenpCoyT3KFEkM5P6k2JSNaSjMHnmc0BJsQDKi7klQwJVWRebJ533Gcg4hnpOI6nzpXTUn/NRzOFiiVVAKWyqSkKjJPHnvZ3+otGfWIenqrzdXYetVMvkjfSC7kaEQmp3e6yDzZEKxPbYjrfGo5JKN+i8eSg837esMOR2RSSqoi8+DoYJYdR/z1qemkSr/lYGbjO/z8ZFtXyNGITE5JVWQebNzt70oTjxgJNSsom7Hzqs8c6CNXUHclqTxKqiLz4KfBVm/JWISoSr9lM9ZQf++xYQ73j4YcjcjrKamKlFmp5Hhslz9JqTEexUxJtVxiEY9EsBXc2HZ6IpVESVWkzLYfGeD4cI50IkpjQqXfchs7Rz32h4tIJVFSFSmzsaU0FyxpJKbWhGU3tmvNS4f6GcqqEYRUFr3jRcrspy/7ZckrlrVoKc08aIhHiEc9ugezvHxkIOxwRE6ipCpSRqO5IluCNZSrL1ms1oTzwDPj6nPbAHhwq5bWSGVRUhUpo6f29JAvOjqbE1y2tCXscGrWTZd2APDUnuMU1bJQKoiSqkgZPR5MnlnZkWZROh5yNLVrzaVLAHi5a5Djw9mQoxE5QUlVpIx+GrQm3HlkkN/8lydDjqZ2XbgkzZJ0nJFckSdf7Qk7HJFxSqoiZdI1kGH30WGME51/ZH6YGW+7aDEAj+w4GnI0IicoqYrM0W13bGR39xC/9sWfARCLesTVmnDe3RycV33+QB86qyqVYsqkambnmNmjZrbNzLaa2X8Njreb2cNmtiv4ty04bmb2OTPbbWYvmNlV8/1DiFSCwWweGNvqTbN+59sNK5dgwL7jIwzn1AdYKsN0RqoF4OPOuTcA1wEfMbM3AJ8A1jvnVgLrg+sA7wJWBl+3A18qe9QiFcY5GMoWAUgn1JpwIbQ3xrm4s4liybGjR00gpDJMmVSdc4edc88ElweB7cDZwFrgruBudwHvDS6vBb7hfE8CrWa2tOyRi1SQrlEolhye+c0JZGHceLF/XnVrTzHkSER85tz0z0aY2QrgMeAKYL9zrjU4bkCvc67VzO4HPuWc2xjcth74C+fc5lOe63b8kSydnZ1Xr1u3bu4/zRwMDQ2RTqdDjWEuFH94dncP8ezREv9+IMKb2h23raDqWj60xor05avnj4GLOvzfle09Rf7+6QxLGxx/9wuNeFVaIajm33+ov/jXrFmzxTm3arLbpj1F0czSwL8Bf+qcG5hY3nLOOTOb0VwB59ydwJ0Aq1atcqtXr57Jw8tuw4YNhB3DXCj+8Hz6jo0MDfQBcCAT556uVNV1Ulrb2cc9Xa1hhzFt9/3GDQBcXyjy2Wcf4vBIiZbz38TVKxaFHNnsVPPvPyj+iaY1+9fMYvgJ9VvOuR8Eh7vGyrrBv2P7MB0Czpnw8OXBMZGaVCo59g/5l5uSsapLqNUsEY1wVdCy8KFt2gpOwjed2b8GfBXY7pz7xwk33Qt8KLj8IeCeCcd/J5gFfB3Q75w7XMaYRSrKcK5A0RlRz0jFqqeEWivGuittUstCqQDTGam+HfggcJOZPRd8vRv4FHCLme0C3hFcB3gAeBXYDXwZ+OPyhy1SOca2H0vEPKIRjVIX2ppL/PWqL3cP0quWhRKyKc+pBhOOTvdJcfMk93fAR+YYl0jVGMz4SbUhFqnaiTLV7KKONK0J6MsW2bT3OO9+47KwQ5I6po5KInPQPZAhWygR85xaE4bEzLh8kV92X79dLQslXEqqInMwtivNuWnUmjBEVyz2/6B57kAvmbzWrEp4lFRF5uDxXf7IaEXaqTVhiMZGqvt6RjgyMBpyNFLPlFRFZqlUcuMj1QuaUWvCEDUnjJUdaQolx6PatUZCpKQqMks7jgzSM5zDM1iSVEIN240X+0trHtt1lJl0ihMpJyVVkVkaK/0mohFM76TQjW0Ft/XQwPgyJ5GFpo8CkVl67GU/qSZjnnooVYCrV7SRiHp0D2bZeXgg7HCkTimpisxCJl/k6b29AFpKUyES0QhXn+e3LHxYLQslJEqqIrOwac9xcsUSHU0JUjEl1Uqx+hL/vOpTe3vUslBCoaQqMgtj51Mv6kgTUWvCinFTcF51V9cQPWpZKCFQUhWZhZ8G51OvPKeViJbSVIwLl6RZnI4znCuy6dWesMOROqSkKjJD3QMZXu4aIurZeLlRwrW7e4jb7tjIez7/BPmiX/Zdv0PnVWXhKamKzNDG3X7Dh+VtDZzb3hhyNHKq5mDi2PMH+tWyUBacZliIzNBY6ffSpU20NcZCjqY+3XbHxpOur+08cTmdiGL4LQtf6xvlgiXphQ1O6ppGqiIzUCq58fWp113QTkJN9CtONOJxyVlNFJ1j/fausMOROqOkKjID2w4P0DuSJ52Ict35i8IOR05jrGXh47uOqWWhLCiVf0WmMLHU2D2YAeCijkY6mpNhhSRTuPnSDu587FW2HR5gIFOgJaUyvSwMjVRFZmAw4/eUvXxZC636oK5Ybzm3jWTM49hQjm2v9YUdjtQRJVWRaSqWHCO5Igb84sVL8LR/asWKRz3euqIdgIe2ammNLBwlVZFpGg52PjmrJcllS5tDjkamMtZd6em9x8kXSyFHI/VC51RFpmkwm/f/zeT5o29uwVMnpYp206Ud/M1929jVPcTRwQzLWhvCDknqgEaqItM0dj61IR5RQq0C5y1qZFlrkmyhxMZdx8IOR+qEkqrINGQLRfJFhwFNCRV4qsUvrPSX1jyy42jIkUi9UFIVmYahYJQaj3rE1PCharwjOK/60qH+8XPiIvNJSVVkGgaDD+RkzCOqWb9V420XLSbiGYf6RtlzdDjscKQOqI4lMgXn3PgopykRxXQ+teJNbNiRiHqM5Io8uO0wVyxvCTEqqQcaqYpMYSRXpOQg4hkNOp9addLB/9nPXzlOqaSWhTK/lFRFpjCY8ZfSJKMeUU9vmWrTnPQ7X+3sGuT4cDbkaKTW6RNCZAoDwSSlVDxCRO+YqpOMebQ1xBjMFNi8tzfscKTG6SNC5Ay6BjJkC343nnQyCuh8arUxM0rBTjWf+MGLr9uLVaSclFRFzmBsQ/J41COppTRVqykoAY/ki+MJVmQ+KKmKnMGjO/xm7KmoR0RLaarWWMOOXKFErqA+wDJ/lFRFTqNQLLFxt9/erjERVWvCKhaNeCSj/sfdwGg+5GiklimpipzGcwf6GMwUiJjRqKU0Va8p2P92KFvQ0hqZN0qqIqexYad/PjUR84hFNEqtds1J/w+jTL5I70gu5GikVimpipzG+h1dgL8rjc6nVr9ULELEoOjg6T3Hww5HatSUSdXMvmZm3Wb20oRjf21mh8zsueDr3RNu+0sz221mO83snfMVuMh8OjqYZfvhQSKeBR15lFSrnU0o4z+0/UjI0Uitms5I9evArZMc/4xz7srg6wEAM3sD8H7g8uAxXzQzrUOQqvNYsJTm3PYGEjH9CteKse5Kz+7vZzRXDDkaqUVTJlXn3GPAdGsla4F1zrmsc24PsBu4Zg7xiYTikZ3+UppLz2rSrjQ1pCk4r7qvZ5j9Pdq1RsrP3DQWQpvZCuB+59wVwfW/Bn4XGAA2Ax93zvWa2eeBJ51z3wzu91Xgx86570/ynLcDtwN0dnZevW7dujL8OLM3NDREOp0ONYa5UPzlU3KOjz4ywnAe/sc1MShMPamlNVakL1+9I9p6iv/rO+HgsHH7FVHetjwxz5FNTyX9/s9GvcW/Zs2aLc65VZPdNtt1Al8C/hZwwb+fBn5/Jk/gnLsTuBNg1apVbvXq1bMMpTw2bNhA2DHMheIvny37ehl+8Ge0pGLccP11/LfvvzDlY9Z29nFPV+sksZsAAB9hSURBVOsCRDc/6in+rJcBsjw31MwnbrwOrwIqEZX0+z8biv+EWc3+dc51OeeKzrkS8GVOlHgPAedMuOvy4JhI1Vi/3Z/1e3Fnms7mZMjRSLm1BOdVtx8Z0K41UnazSqpmtnTC1V8FxmYG3wu838wSZnY+sBLYNLcQRRbWVzfuAWDPsWE++LWnQo5Gym3irjVPaWmNlNl0ltR8B/g5cImZHTSzDwP/YGYvmtkLwBrgzwCcc1uBu4FtwH8AH3HOaYqdVI3X+kbJFkoYfr9Y01KammNmvP2ixQA8tLUr5Gik1kx5TtU594FJDn/1DPf/O+Dv5hKUSFgeCRrox6OeltLUsF96Qyf3v3CY5w72MZwtqA2llI06KolM8JPgfGoypl1patnqSzuImLH/+Ai7uwbDDkdqiJKqSCCTL/LzV3oASCdj2pWmhv3Wl58iEfNwDj78jc1hhyM1RElVJPDzV3rIFkpEPaMxrtJvrRtrsD+cLWiPVSkbJVWRwMTSb9TTW6PWNQdbwWXyJboHMiFHI7VCnxwigHOO9dv9SUoN8ajOp9aBRDRCLGI4TvxBJTJXSqoiwM6uQY4MZGiIR0gnVPqtF2O9gB/Z0a2Ny6UslFRFOLGU5qKONPGokmq9aAlKwFtfG6BvNB9yNFILlFRFgIe3+eW/N57drF1p6khjPIpn0DOcY/NedVeSuVNSlbp3fDjH8wf68Ax+8eIOTEtp6obZ2Cb08OOXDoccjdQCJVWpe+u3d1Fy/obkly1tDjscWWBjs4Cf3d/HSK4QcjRS7ZRUpe49uPUIAFcsa2FxujL215SF0xzsWrO3Z4Tb7tjIbXdsDDkiqWZKqlLXRnNFHt91DIC3r1xESk0f6k7EM1JBn+d+TVaSOVJSlbq2cfcxsoUSZzUnWXVue9jhSEiaUye6KzmnpTUye9qaQerOxPLegd4RAN6wrJmOFm1IXq9aUjG6BrJk8iVyRbUslNnTSFXqlnOOwYw/MeWtK9rGe8FK/UlEI8QjHg6VgGVulFSlbo3kihRLjtZUjBsuWqylNHVurAQ8mClQ0GhVZklJVerWQMYfkVy6tIllramQo5GwtY432C9ybCgbcjRSrZRUpS4558bLfG85p5W2hnjIEUnYkrEIUc8ouRNtK0VmSklV6lK2UCJfdJjB6ks68NSasO6Z2XiD/Ye2dqnBvsyKkqrUpbHSbzIaYcXixpCjkUrR2uCXgF841M/xYZWAZeaUVKUuDYz6s35TMY9FjSr9im+swf7x4RxP7VGDfZk5JVWpO/liidF8EYCmVJRoRG8D8U0sAT/w4mE1gpAZ08I8qTtjE5QSUY9kLKper3KSllSM/tECz+7vYyBTGN9zVWQ69Ce61J2xpNoQj2jvVHmdpmSMeNTjtf4MLxzsCzscqTJKqlJXugcyjOSC0m8yiqeGD3IKz4zrL1gEwD3PHQo5Gqk2SqpSV8a2eUtEvfGdSURO9StvWgrA03t6x2eKi0yHkqrUlfteOAxAKh7RBCU5rX99Yi8A+46PsPVgf7jBSFXRRCWpG8eGsmze6y+TaEqo9CunF/GMxkSE4WyRP/72Myxvaxi/7b6P3hBiZFLp9Ke61I2HtnZRckHpV5uRyxRaU/765eFsgaK6K8k0KalK3bj/hdcAv+FDTKVfmcLYUppc0TGaK4QcjVQLfbJIXegdzvHkqz14BulkTKVfmVLEM9IJv6LRpz1WZZqUVKUuPLzNL/2e295Ig0q/Mk0qActMKalKXbj3eb/0++blLSr9yrQ1p2IYKgHL9OnTRWreuz77GBt3HwNgy75elX5l2sZmAQP0qgQs06CkKjVvrC1hPOrRkFDpV2amtUElYJm+KZOqmX3NzLrN7KUJx9rN7GEz2xX82xYcNzP7nJntNrMXzOyq+QxeZDp6R4Jev7GISr8yY81JvwScLzpGVAKWKUznE+brwK2nHPsEsN45txJYH1wHeBewMvi6HfhSecIUmZ0Dx0dO2uZNpV+ZKb8E7PfJ6R3JaTs4OaMpk6pz7jHg1N161wJ3BZfvAt474fg3nO9JoNXMlpYrWJGZui+YoJSMeTSo16/MUluDv2Z1JFukb0TnVuX0bDp/dZnZCuB+59wVwfU+51xrcNmAXudcq5ndD3zKObcxuG098BfOuc2TPOft+KNZOjs7r163bl15fqJZGhoaIp1OhxrDXCj+yf2PjSMcHHL82vklLmubv1Fqa6xIX756k7biP7NCCT7zImSLxv99bZwL28q7x6rev+Gaafxr1qzZ4pxbNdltc+7965xzZjbjeohz7k7gToBVq1a51atXzzWUOdmwYQNhxzAXiv+EsU3HM/kiB4ccBmwZaOTlXLwszz+ZtZ193NPVOm/PP98U/9SS8RGyo3me6Gvh99a+Fa+Me/Hq/RuucsY/21kbXWNl3eDf7uD4IeCcCfdbHhwTWXC9IznA35EmGdPeETI37UEJeMv+ProHMyFHI5Vqtkn1XuBDweUPAfdMOP47wSzg64B+59zhOcYoMmPOOfrHZv3GI0TLOKqQ+tSYiBL1jP7RPD9+6UjY4UiFms6Smu8APwcuMbODZvZh4FPALWa2C3hHcB3gAeBVYDfwZeCP5yVqkSmM5IrkSw7P/G3eTLN+ZY7MbLzJ/r+/eJh8sRRyRFKJpqyJOec+cJqbbp7kvg74yFyDEpmrvrHSbyxCQrN+pUzaGmP0DOd48WA/B3pHuGBx9U7OkfmhlfBSc0rOje8q0piIEo1olCrlkYpFuXBJI9lCiR9s0XQReT0lVak5g5kCJQdRz2hKRjGUVKV83vuWswFYv72LTNBYRGSMkqrUnOPDfum3IR4hHtWvuJTXb646BzPY2TXIttcGwg5HKow+caSmdA1kGMr6/VnTyahm/UrZdTQnufrcNkoOvrNpv9oWykmUVKWm/OCZgwAkoh7pRBRU+pV58IFr/OX4j+8+Nr5hgwgoqUoNcc6x7ukDADQmtCONzJ9fefMyGhMRjvRnWL9da1blBLWZkao31pZwJFdgX88InkFzUjvSyPxJRCO8+4qlfG/LQf5tyyHe+5bl+iNOAI1UpYaMTVBKxdSWUObfh962AoBnD/Sx79hwuMFIxdAnj9SEUsnRH6xNTSe1NlXm3xVnt3BRR5rd3UP85pefZFlLCoD7PnpDyJFJmJRUpSb0Z/KUHMQ889sSaoKSzJOx0w0Awxn/D7nB0QLFJkdEs83rnsq/UhNOXpuqtoSyMNoaExiQK5bGl3JJfVNSlaqXyRcZyRUxoCkV02hBFkwk6NoF/laDDq1ZrXcq/0pVmliC6xnOAv6+qQ1xjVJlYbU3xhnIFBjOFsgXtHNNvdNIVapaseToCxbfpxNR4lrWIAssnYgSixgld+I0hNQvfQJJVesbzfkTlCJB83ytTZUFZma0N8YB6B8tjM9Cl/qk8q9ULeccPUP+yKAxHiWhCUoSkkWNcboHsuSKJX75c4/T1hAfv01LbOqLRqpStUZyRbKFkt9BKRXVBCUJTcTzaE7FAL8ErCb79UtJVarWyROUVHSRcC1J+6PT0VyRUe2zWreUVKUqFYol+kf9dYFNCXVQkvCl4lFSMQ8HHBvKgpbX1CUlValKPSP+udRE1KMpGVMHJakIi9MJAIYyRXJaXlOXlFSl6mTyxRMTlBIR4lH9GktlaEnFiHpG0Tl6R7S8ph7p00iqzj3PHaJYckQ9ozkZ0xZvUjEmLq/pGy1QLKkEXG+UVKWqlEqOL214BfAX3afUQUkqzOJ03O8HXCgxkNGa1XqjpCpV5dGd3ewd24g8FSXq6VdYKkvE82gJltf0DGXJaCZwXdEnklSVL04YpWoZjVSqjiZ/wtJovsQTu4+FHI0sJCVVqRrPHehjy75ef8Zvyu+3KlKJErHI+O41X3n8Vc0EriNKqlI1/jkYpa46r410PApaRiMVbGy0+vTeXp4/0BtyNLJQVD+TijRxazeAL/ynq3ho2xE8g5su6+CHzx4KKTKR6WmIR2mIRxjJFfnnn77KVee1q5VmHdBIVarCHY/uouTgjWe3cOPKJWr2IFWhs9kfrW7cfYyXuwZCjkYWgpKqVLxcocQPnjmEATdf1sm5ixrCDklkWtKJGG9Y2ky2UOKfN7xKSetWa56SqlS8roEMxZLjTctbWHNJh7Z4k6ry0ZsuAmD9jm729AyHHI3MNyVVqWjZQpG+0Txm8I7LOlnZmQ47JJEZufWKszh/cSND2QKfW79Lo9Uap6QqFa1rwN/e7crlrdx0WQfJmEapUl3MjP/2SxcD8NDWLnbq3GpN0+xfqVjZfJH+Ub/N22t9o/z5919Qn1+pSu9641IuOWs3O48M8tmf7OKLv3W1ZgLXKI1UpWJ1DWYASMUiNKfUOF+ql5nxiVsvBeDRHUd56VBfyBHJfJnTSNXM9gKDQBEoOOdWmVk78F1gBbAX+A3nnFY+y4xk8sXxTcibU1ES2t5NqtTENdepWITRfJHPPLyLr3xoFdGIfq9rTTn+R9c45650zq0Krn8CWO+cWwmsD66LzMjhfn+U2hCP0JyMYRqlSg1Y2pIE4PHdx9i8V2ONWjQffyatBe4KLt8FvHcevofUsJ++fJShbAHD3/RZo1SpFY2JKG+7cBHFkuOzP3lZPYFrkDk3++ndZrYH6AUc8C/OuTvNrM851xrcbkDv2PVTHns7cDtAZ2fn1evWrZt1HOUwNDREOl29yzWqPf5jvf305SOUHHx5OxzNGKuXlnj70urondQaK9KXr96ZyYp/4RwbhX/Z7l/+8KVww4p01b9/6y3+NWvWbJlQnT3JXJPq2c65Q2bWATwMfBS4d2ISNbNe51zbmZ5n1apVbvPmzbOOoxw2bNjA6tWrQ41hLqo9/q/cfT/3dLXSM5zltb4METOWtiZobYhXRUvCtZ193NP1ur8dq4biX1j7j4/QP5onHvXY/H/dzDNP/ayq37/V/vkz0/jN7LRJdU51NefcoeDfbuCHwDVAl5ktDb7xUqB7Lt9D6kex5MbXpTanoqQTsapIqCIztbQliWd+C847f/pq2OFIGc06qZpZo5k1jV0Gfgl4CbgX+FBwtw8B98w1SKkP3YN+O8JYxGhJRolpZqTUqFjEY0nab7b/pZ++wtbDQ9x2x8bX7c4k1WcuS2o6gR8GszKjwLedc/9hZk8Dd5vZh4F9wG/MPUypdccz0DOUA/zJSQ2JWMgRicyvJU1xekdy5IqOJ46AizpVZmrArJOqc+5V4M2THO8Bbp5LUFJfnHM8cMCf7ZaKebSmYuo2IzXPzGNZa5K9PaM81Q3ntBdoSuqPyWqn+pqE7nubD7B30PAMWhvi6u8rdaMpGSediFB0xpH+DMWSlthUOyVVCVX3YIa//Xd/fUFzMqZGD1J3lrWmSHiOTKFE92CWuazIkPCpob6E6v+5ZyuDmQIXNDlKsSjxqBKq1JdENMIt58D9+6BnOMc7Pr3hpDkF9330hhCjk5nSSFVC85NtXfz4pSPEIsY7lzsakzHQRA2pQ29u9+cTOAeH+lQGrmZKqhKK/tE8//2HLwJw0yUdtKdMu9BI3TKDc9obMBgvA0t1UlKVBeec48+//zzdg1nObk3yG6vO0fhU6l4iGqGj2V+72jOUYyRXCDkimQ0lVVlw39m0nwe3dpGIerz/redw7YWLwg5JpCIsScf9MjBwsHdUZeAqpKQqC2pX1yB/c982wN9b8r4XDvOBO58MOSqRymDmcU57A55BtlDitb4MpZJmA1cTJVVZMJl8kf/yrWfIFkqkYh7tjXFt6yZyikQ0wrLWFAB9o3nWPb0/5IhkJrSkRubFZD1MD/WOcHwkT8Qz2tNxGuIRNNtX5PXaGuIMZwv0juT5Hz96ia9t3EMq7n9ca4lNZdMwQRbE8eEsx0fyALQ3xGhRkweRM1rWmiQR9Sg52K/zq1VDSVXm3VC2wKG+DAAtqShtjXEinn71RM7EM4/zFjWMbxF3sHdU3ZaqgD7ZZF5lC0X29QwD0BiP0N6Y0HlUkWlKRCMsb/PPrw5kChzuzyixVjh9usm8KZYce4+NUHKQiHosSsdp1HlUkRlpScXpbArWrw7n+Ncn9oQckZyJkqrMi5Jz7OsZJlcsEfWMxek4TTqPKjIrHc0J2hr8fsD/88c7ePClwyFHJKejpCpllyuU2NczwnCuiGewqDFOSyquNoQis2Ysa02STkQoFB0f+97zPLu/N+ygZBJKqlJWhWKJj37nGYayBcxgUToeTExSQhWZC888zmtvIBnzGM4Wed+XfsZzSqwVR0lVyqZYcnzs7ud4cGsXhj9CXdSYIBbRr5lIOXiex4pFjcSjHkUHv/O1TUqsFUafdlIW+WKJj9/9HPc+f5h4xGNRY5zFaSVUkXKLRTwuWNxIIuoxkCkosVYYfeLJnA1lC/ze15/mR8+9RixifPC6c1nUpIQqMl9iEY/zFzdyUUeagUyBD35tE0+92hN2WIKSqsxR92CG3/jnn7Nx1zEa4hE+fMP5/P4N5xNXQhWZV7GIx91/eB0Xd6YZDBLr9zarT3DY9Mkns7a7e4hf/cITbDs8QFtDjI+suZDfv+F8zm5rCDs0kbrQ3pjge394PdesaCNXKPHn33+R//XAdjWICJGSqszKvc+/xns+v5FDfRmWtiT5+C9dzG9ft4KOpmTYoYnUlZaGON/8g2v59auX44B/eexV/vM3NjOa1SbnYdAuNTIjmXyRT963jW9v8stMl53VxEdWX8g7Lj+LZCwScnQi9eXk3aAcf/nuS/nfD+7kJ9u7ueWzj/GF37qKNy9vDS2+eqSRqkzbK0eH+D+++DO+vWk/ETPefcVZfHLt5bzzjUuVUEVCZ/zhjRfy5Q9ezaLGOAd7R/m1L/6MLz66W+XgBaSRqkwpVyjxzz99hTse2UW+6GhNxYhFPF45Nswn798edngiEhgbuS5JJ8jkiwznivzDgzv5/KO7Wd6W4qE/+8WQI6x9GqnKGW3ee5x3/9Pj/OPDL5MvOlKxCE3JKB3NCc3wFalQsajH+Usa6WxOYMBIrsiuriH+3wd3kitoX9b5pJGqTOrA8RF+5Y6N9I/6G4tHzGhriNKeThCPeph2mhGpaIbR0ZSkKRnlUF+G0VyRzz+6mx88e5BPrr2Cmy/t0AYX80BJVcbddsdGCsUS3YNZeoZz48cb4xEWNyVIJ6Jqii9SZVKxKBcuaeT4cI5cocRrfRn+4K7NvOnsFv781ku4YeWSsEOsKUqqAvgj09f6RukdyVEK5jQkYx7tDXGag3OoIlKdDGNRY4JCqUTJOXpH8rxwqJ/f/uomrjynlY/fcjE3rFyskWsZKKnWMeccT+/t5WsbX+WhbV3jyTQe9WhriNHWECcWMbSpuEhtiHoeZ7c2sCRdpGswSyZf5LkDfXzwa5s4r72BD71tBe9btZzmZCzsUKuWkmod2t8zwg+fPcQPnj3Ivp4RADzzR6atqRgtqRgxnTcVqVnxaIRz2hr40m9fxR3rd/PvLx5m3/ERPnn/Nv7hwR2864ql/NpVy/nUj7efNHq976M3hBh1dVBSrQPOOXZ3D7F+RzcPb+tiy74TO1p4Bg3xCG2NcdKJKFFPI1ORerG8rYG/f9+b+O+/fCnf23yQ72zazytHh/nhs4f44bOHiHhGSypKSypOQ1xr0adDSbWKnNw95WRrO4f49ITb88USI7kiQ9kCg5k8+eKJxd+xiHHJWU10DWRoTcWIRyOagCRSh079THn4z36RzXuP852nD/DYy0fpGc5xfDjP8eE8ZvC7X9vETZd1cN0Fi7hoSRrP0+fGqZRUa0Cp5DgwBMeG/HMkI7ki2VPWonkGbzy7havPa2Ptm5dxYWcTH7jzyZAiFpFKtPYLT4xfPqs5QVMySu9InuFsgULJseHlo2x4+Sjgf6bcsHIJbz2vjdLxApf2Z/x1sXX+B7qSapUoFEvkCiVywb/Zgp84s3n/2NbDBmTG72/4C8ATUY+mRJSmZJS7/+h6ElGVcERkap7n0ZT0aErGcDgy+SIDowUGMwWyhSIlB4+9fJTHgiT7mS3raUnFuPSsJi7sSHPB4kZWLGpkxeIGlrWmaIjXR7qZt5/SzG4F/gmIAF9xzn1qvr5XJTm1nHK6E/vOOTL5EoOZPAOZPL0jeT5+9/MUS45CqUSh6Lj2gna6B7IcGchwuD9DsXT6/p0dScdQKUo84tGYiPjnRyPeSWVdJVQRmQ3DSMWipGJROpuDz69CkeFsgaFskfZonv3DRv9onqf2HOepPcdf9xytDTGWtaRY1ppkcTrB4nSCJU0JFqXjtKbitDbEaG3wJ0o2xqNVW1qel6RqZhHgC8AtwEHgaTO71zm3bT6+36mODWXZc2wYAOf8XwA3dhkXHPMvl4Lbnz9aoLjdX1ZSco5SyVF0jmLJUXKOYgmKpRKFkn+sUHTki/71fLFEPhhBvtY3Ssn536PkHL//9afHS7IjuQLDWf/fwYxfTjmTB148ctJ1z/wp8dGIEfWMZCxCKu6RjEb49bMHuacrPV8vqYjIOLMTSXZxGtZ29vHDw0185jffwvMH+9hxeJBd3YMcGcjQM5SjbzRP34j/te3wwLS+h2fgmRHxDDN409mtpOIRkjGPZCxCIuqRiEaIRz1iEY941CMeMaIRj6jnf0ZGIx6XLW3i6vPa5/kVOWG+RqrXALudc68CmNk6YC2wIEn1vV94goO9ozN/4JbNZY/lkR3dp73NAAt+cTzP8MxvB+h5RsTspF+WRNQjEtxntrNzzzTRSURkLjzP4+Pfe/6kY4lohGWtKc5qTpAvOrLB4CNfLPH2ixZzfDjHwGh+fNDh/+uf2hob4IwNPjbtff3odzraG+Oc3ZpasOVANh9bApnZ+4BbnXN/EFz/IHCtc+5PJtznduD24OolwM7yBuFFvETDtIdurpBvsWisf5InmmYGc8Eg2B8D43DgSv4wORgv+5dLOH98HOzHVK7/gMXAsTI9VxgUf7gUf7gU/+uYYWaYeZiZmeeNH8MMY/yyceLyqc9Syo0MUSpOtWP7TOM/zzk3aX/H0M4cO+fuBO4M6/ufysw2l/KZVWHHMVtmttk5p/hDovjDpfjDpfhPmK+GroeAcyZcXx4cExERqVnzlVSfBlaa2flmFgfeD9w7T99LRESkIsxL+dc5VzCzPwEexF9S8zXn3Nb5+F5lVDGl6FlS/OFS/OFS/OFS/IF5magkIiJSj7RJpoiISJkoqYqIiJRJ3SRVM0ua2SYze97MtprZ30xyn4SZfdfMdpvZU2a2YuEjndw04/9dMztqZs8FX38QRqxnYmYRM3vWzO6f5LaKff3HTBF/Rb/+ZrbXzF4MYntdpxPzfS54/V8ws6vCiPN0phH/ajPrn/D6/1UYcZ6OmbWa2ffNbIeZbTez60+5vdJf/6nir9jX38wumRDXc2Y2YGZ/esp9yvL610eHY18WuMk5N2RmMWCjmf3YOTdxq5YPA73OuYvM7P3A3wO/GUawk5hO/ADfndhkowL9V2A70DzJbZX8+o85U/xQ+a//Gufc6Ra5vwtYGXxdC3wp+LeSnCl+gMedc7+yYNHMzD8B/+Gce1+wKqLhlNsr/fWfKn6o0NffObcTuBLG2+geAn54yt3K8vrXzUjV72fkhoKrseDr1Flaa4G7gsvfB242q4x9jKYZf0Uzs+XALwNfOc1dKvb1h2nFX+3WAt8IfteeBFrNbGnYQdUCM2sBbgS+CuCcyznn+k65W8W+/tOMv1rcDLzinNt3yvGyvP51k1RhvHT3HNANPOyce+qUu5wNHAB/WRDQDyxa2ChPbxrxA/xaULr4vpmdM8ntYfos8OdA6TS3V/Trz9TxQ2W//g54yMy2mN8m9FTjr3/gYHCsUkwVP8D1wSmSH5vZ5QsZ3BTOB44C/xqcPviKmTWecp9Kfv2nEz9U7us/0fuB70xyvCyvf10lVedc0Tl3JX6Hp2vM7IqwY5qJacR/H7DCOfcm4GFOjPpCZ2a/AnQ757aEHctsTDP+in39Azc4567CL3N9xMxuDDugGZoq/mfwe7K+GbgD+NFCB3gGUeAq4EvOubcAw8Anwg1pRqYTfyW//gAEZev3AN+br+9RV0l1TFC2eBS49ZSbxtsrmlkUaAF6Fja6qZ0ufudcj3MuG1z9CnD1Qsd2Bm8H3mNme4F1wE1m9s1T7lPJr/+U8Vf4649z7lDwbzf++aRrTrlLRbcXnSp+59zA2CkS59wDQMzMFi94oJM7CBycUF36Pn6SmqiSX/8p46/w13/Mu4BnnHNdk9xWlte/bpKqmS0xs9bgcgp/r9cdp9ztXuBDweX3AY+4CumOMZ34T6n/vwd/Qk1FcM79pXNuuXNuBX755RHn3G+fcreKff2nE38lv/5m1mhmTWOXgV8CXjrlbvcCvxPMgrwO6HfOHV7gUCc1nfjN7Kyxc/Bmdg3+51tF/FHmnDsCHDCzS4JDN/P6rTAr9vWfTvyV/PpP8AEmL/1CmV7/epr9uxS4K5j55QF3O+fuN7NPApudc/fin4T//8xsN3Ac/8OzUkwn/v/TzN4DFPDj/93Qop2mKnr9J1VFr38n8MPgMy8KfNs59x9m9kcAzrl/Bh4A3g3sBkaA3wsp1slMJ/73Af/FzArAKPD+SvmjLPBR4FtBCfJV4Peq6PWHqeOv6Nc/+GPsFuAPJxwr++uvNoUiIiJlUjflXxERkfmmpCoiIlImSqoiIiJloqQqIiJSJkqqIiIiZaKkKiIiUiZKqiIiImXy/wNb6xOn/jx5ugAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------- Fit Results -------------------\n", + "Fit of histogram to gaussian\n", + "\n", + "Result Parameter List: \n", + "normalization = 405 +/- 3,\n", + "mean = 5.011 +/- 0.004,\n", + "std = 0.497 +/- 0.004\n", + "\n", + "Correlation Matrix: \n", + "[[1.000e+00 1.785e-06 5.774e-01]\n", + " [1.785e-06 1.000e+00 2.826e-06]\n", + " [5.774e-01 2.826e-06 1.000e+00]]\n", + "\n", + "chi2/ndof = 0.00/96\n", + "\n", + "--------------- End Fit Results -----------------\n" + ] + } + ], + "source": [ + "# now let's try adding a fit to the histogram\n", + "result = figure.fit(model=q.FitModel.GAUSSIAN, parguess=[100, 5, 0.5])\n", + "\n", + "figure.show()\n", + "print(result)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/qexpy.egg-info/PKG-INFO b/qexpy.egg-info/PKG-INFO deleted file mode 100644 index 86ce0d3..0000000 --- a/qexpy.egg-info/PKG-INFO +++ /dev/null @@ -1,18 +0,0 @@ -Metadata-Version: 1.1 -Name: qexpy -Version: 0.3.8 -Summary: Package to handle error analysis and data plotting aimed - at undergraduate physics. -Home-page: https://github.com/Queens-Physics/QExPy -Author: Connor Kapahi and Prof. Ryan Martin -Author-email: ryan.martin@queensu.ca -License: GNU GLP v3 -Download-URL: https://github.com/Queens-Physics/QExPy/tarball/0.3.7 -Description: UNKNOWN -Keywords: physics,laboratories,labs,undergraduate,data analysis,uncertainties,plotting,error analysis,error propagation,uncertainty propagation -Platform: UNKNOWN -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: Science/Research -Classifier: Topic :: Scientific/Engineering :: Physics -Classifier: License :: OSI Approved :: GNU General Public License (GPL) -Classifier: Programming Language :: Python diff --git a/qexpy.egg-info/SOURCES.txt b/qexpy.egg-info/SOURCES.txt deleted file mode 100644 index 0cc4267..0000000 --- a/qexpy.egg-info/SOURCES.txt +++ /dev/null @@ -1,15 +0,0 @@ -setup.cfg -qexpy/__init__.py -qexpy/_test.py -qexpy/defaults.py -qexpy/error.py -qexpy/error_operations.py -qexpy/fitting.py -qexpy/plot_utils.py -qexpy/plotting.py -qexpy/utils.py -qexpy.egg-info/PKG-INFO -qexpy.egg-info/SOURCES.txt -qexpy.egg-info/dependency_links.txt -qexpy.egg-info/requires.txt -qexpy.egg-info/top_level.txt \ No newline at end of file diff --git a/qexpy.egg-info/dependency_links.txt b/qexpy.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/qexpy.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/qexpy.egg-info/requires.txt b/qexpy.egg-info/requires.txt deleted file mode 100644 index de7b88c..0000000 --- a/qexpy.egg-info/requires.txt +++ /dev/null @@ -1,6 +0,0 @@ -numpy -matplotlib -ipywidgets -scipy>=0.17 -bokeh>=0.12.1 -pandas \ No newline at end of file diff --git a/qexpy.egg-info/top_level.txt b/qexpy.egg-info/top_level.txt deleted file mode 100644 index f2fc5cb..0000000 --- a/qexpy.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -qexpy diff --git a/qexpy/__init__.py b/qexpy/__init__.py index 14ac344..12483ea 100644 --- a/qexpy/__init__.py +++ b/qexpy/__init__.py @@ -1,52 +1,54 @@ +"""Python library for scientific data analysis""" + +# +# _oo0oo_ +# o8888888o +# 88" . "88 +# (| -_- |) +# 0\ = /0 +# ___/`---'\___ +# .' \\| |// '. +# / \\||| : |||// \ +# / _||||| -:- |||||- \ +# | | \\\ - /// | | +# | \_| ''\---/'' |_/ | +# \ .-\__ '-' ___/-. / +# ___'. .' /--.--\ `. .'___ +# ."" '< `.___\_<|>_/___.' >' "". +# | | : `- \`.;`\ _ /`;.`/ - ` : | | +# \ \ `_. \_ __\ /__ _/ .-` / / +# =====`-.____`.___ \_____/___.-`___.-'===== +# `=---=' +# +# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# 佛祖保佑 永无BUG +# -# Check the python interpretter version import sys -if sys.version_info[0] < 3: # No reason to assume a future Python 4 will break compatability. - raise ImportError("Error: QExPy is only supported on Python 3. Please upgrade your interpretter.\n" - "If you're using Anaconda, you can download the correct version here:\n" - "https://www.continuum.io/downloads") - - -#Whether to use Bokeh or matplotlib: -plot_engine="bokeh" -plot_engine_synonyms = {"bokeh":["bokeh", "Bokeh", "Bk", "bk", "Bo", "bo", "B", "b"], - "mpl":["mpl","matplotlib","MPL","Mpl","Matplotlib", "M","m"]} -quick_MC = False - -#Default parameters for things: -from qexpy.defaults import settings - -#Error propagation -from qexpy.error import Measurement, MeasurementArray, ExperimentalValue, \ - set_print_style, set_sigfigs_centralvalue, set_sigfigs_error, set_sigfigs, set_error_method, show_histogram, \ - sqrt, sin, cos, tan, sec, csc, cot, log, exp, e, asin, acos, atan - -#Plotting and fitting -from qexpy.plotting import Plot, MakePlot -from qexpy.fitting import XYDataSet, XYFitter, DataSetFromFile -from qexpy.plot_utils import bk_plot_dataset, bk_add_points_with_error_bars,\ - bk_plot_function - - -__version__ = '1.0.4' - -# The following will initialize bokeh if running in a notebook, -# and hacks the _nb_loaded variable which is required for all plots -# to show when Run All is used in a notebook. This bug arrived in -# bokeh 12.1, hopefully they get rid of it... - -import qexpy.utils as qu -import bokeh.io as bi - -from qexpy.utils import get_data_from_file - -if qu.in_notebook(): - - qu.mpl_output_notebook() # calls matplotlib inline - - bi.output_notebook() - qu.bokeh_ouput_notebook_called = True - '''This hack is required as there is a bug in bokeh preventing it - from knowing that it was in fact loaded. - ''' - bi._nb_loaded = True + +__version__ = '3.0.0' + +from .utils import load_data_from_file + +from .settings import ErrorMethod, PrintStyle, UnitStyle, SigFigMode +from .settings import get_settings, reset_default_configuration +from .settings import set_sig_figs_for_value, set_sig_figs_for_error, set_error_method, \ + set_print_style, set_unit_style, set_monte_carlo_sample_size, set_plot_dimensions + +from .data import Measurement, MeasurementArray, XYDataSet +from .data import get_covariance, set_covariance, get_correlation, set_correlation +from .data import sqrt, exp, sin, sind, cos, cosd, tan, tand, sec, secd, cot, cotd, \ + csc, cscd, asin, acos, atan, log, log10, pi, e +from .data import std, mean, sum # pylint: disable=redefined-builtin +from .data import reset_correlations + +from .fitting import fit, FitModel + +# Check the python interpreter version +if sys.version_info[0] < 3: # pragma: no coverage + raise ImportError( + "Error: QExPy is only supported on Python 3. Please upgrade your interpreter. " + "If you're using Anaconda, you can download the correct version here: " + "https://www.continuum.io/downloads") diff --git a/qexpy/_test.py b/qexpy/_test.py deleted file mode 100644 index 8e27b6f..0000000 --- a/qexpy/_test.py +++ /dev/null @@ -1,378 +0,0 @@ -import math as m -import qexpy.error as e -import qexpy.plotting as p -import unittest -import numpy as np - -class TestError(unittest.TestCase): - def test_single_measurement(self): - '''Tests creating a Measurement from a single measurement with uncertainty - ''' - x = e.Measurement(10, 1) - self.assertEqual(x.mean, 10) - self.assertEqual(x.std, 1) - x.mean = 3 - x.std = 0.1 - self.assertEqual(x.mean, 3) - self.assertEqual(x.std, 0.1) - - def test_multiple_measurements(self): - '''Tests creating a Measurement from a multiple measurements - ''' - x = e.Measurement([9, 10, 11]) - self.assertEqual(x.mean, 10) - self.assertEqual(x.std, 1) - - def test_measurement_array(self): - '''Tests creating a MeasurementArray from multiple measurements - ''' - x = e.MeasurementArray([9, 10, 11], error=1) - self.assertEqual(x.mean, 10) - self.assertEqual(x.std(), 1) - -class TestCovariance(unittest.TestCase): - def test_array_covariance(self): - '''Tests covariance calculated from the data arrays - from the two Measurement objects. - ''' - x = e.Measurement([1, 2, 3, 4, 5]) - y = e.Measurement([2, 4, 6, 8, 10]) - - self.assertEqual(x.get_covariance(y), 5) - - def test_set_covariance(self): - '''Tests setting covariance between two objects. - ''' - x = e.Measurement(10, 1) - y = e.Measurement(20, 2) - x.set_covariance(y, 2) - - self.assertEqual(x.get_covariance(y), 2) - - def test_propagated_covariance(self): - '''Tests the propagation of correlation between two - objects through the derivative method. - ''' - x = e.Measurement(10, 1) - y = e.Measurement(20, 2) - x.set_covariance(y, 2) - - result = x*y+x - - self.assertEqual(result.get_covariance(y), 82) - - def test_array_correlation(self): - '''Tests covariance calculated from the data arrays - from the two Measurement objects. - ''' - x = e.Measurement([1, 2, 3, 4, 5]) - y = e.Measurement([2, 4, 6, 8, 10]) - x.get_covariance(y) - - self.assertAlmostEqual(x.get_correlation(y), 1, places=7) - - def test_set_correlation(self): - '''Tests setting covariance between two objects. - ''' - x = e.Measurement([1, 2, 3, 4]) - y = e.Measurement([2, 3, 4, 1]) - - result = x*y+x - result.get_covariance(y) - self.assertAlmostEqual(x.get_correlation(y), -.2) - - def test_propagated_correlation(self): - '''Tests setting covariance between two objects. - ''' - x = e.Measurement(10, 1) - y = e.Measurement(20, 2) - x.set_correlation(y, 0.5) - - self.assertEqual(x.get_correlation(y), 0.5) - -class TestFunctions(unittest.TestCase): - - def test_measurement_elementary(self): - '''Tests elementary operations on Measurement objects - ''' - x = e.Measurement(2, 0.01) - y = e.Measurement(5, 0.2) - - self.assertEqual(x+y, e.Measurement(7, 0.2)) - self.assertEqual(y+x, e.Measurement(7, 0.2)) - self.assertEqual(x-y, e.Measurement(-3, .2)) - self.assertEqual(y-x, e.Measurement(3, .2)) - self.assertEqual(x*y, e.Measurement(10, 0.4)) - self.assertEqual(y*x, e.Measurement(10, 0.4)) - self.assertEqual(x/y, e.Measurement(.4, .02)) - self.assertEqual(y/x, e.Measurement(2.5, 0.1)) - self.assertEqual(x**y, e.Measurement(32, 5)) - self.assertEqual(y**x, e.Measurement(25, 2)) - - def test_array_elementary(self): - '''Tests elementary operations on Measurement objects - ''' - x = e.MeasurementArray([4, 9, 2], error = [0.1, 0.2, 0.3]) - y = e.MeasurementArray([2, 3, 3], error = 0.1) - - self.assertListEqual((x+y).means.tolist(), [6, 12, 5]) - self.assertListEqual((y+x).means.tolist(), [6, 12, 5]) - self.assertListEqual((x-y).means.tolist(), [2, 6, -1]) - self.assertListEqual((y-x).means.tolist(), [-2, -6, 1]) - self.assertListEqual((x*y).means.tolist(), [8, 27, 6]) - self.assertListEqual((y*x).means.tolist(), [8, 27, 6]) - self.assertListEqual((x/y).means.tolist(), [2, 3, 2/3]) - self.assertListEqual((y/x).means.tolist(), [0.5, 1/3, 3/2]) - self.assertListEqual((x**y).means.tolist(), [16, 729, 8]) - self.assertListEqual((y**x).means.tolist(), [16, 19683, 9]) - - def test_measurement_functions(self): - '''Tests mathematical functions on Measurement objects - ''' - x = e.Measurement(3.2, 0.01) - y = e.Measurement(0.23, 0.04) - - self.assertEqual(e.sin(x), m.sin(x.mean)) - self.assertEqual(e.cos(x), m.cos(x.mean)) - self.assertEqual(e.tan(x), m.tan(x.mean)) - self.assertEqual(e.csc(x), 1/m.sin(x.mean)) - self.assertEqual(e.sec(x), 1/m.cos(x.mean)) - self.assertEqual(e.cot(x), 1/m.tan(x.mean)) - self.assertEqual(e.exp(x), m.exp(x.mean)) - self.assertEqual(e.log(x), m.log(x.mean)) - self.assertEqual(e.asin(y), m.asin(y.mean)) - self.assertEqual(e.acos(y), m.acos(y.mean)) - self.assertEqual(e.atan(x), m.atan(x.mean)) - - def test_measurement_comparisons(self): - '''Tests comparisons of Measurement objects - ''' - x = e.Measurement(3.2, 0.01) - y = e.Measurement(0.23, 0.04) - z = e.Measurement(0.23, 0.01) - - self.assertFalse(x == y) - self.assertFalse(y == x) - self.assertTrue(y == z) - self.assertTrue(z == y) - - self.assertFalse(x <= y) - self.assertFalse(y >= x) - self.assertTrue(y >= z) - self.assertTrue(z <= y) - - self.assertTrue(x > y) - self.assertTrue(y < x) - self.assertFalse(y > z) - self.assertFalse(z < y) - - def test_array_functions(self): - '''Tests mathematical functions on Measurement objects - ''' - x = e.MeasurementArray([4, 9, 2], error = [0.1, 0.2, 0.3]) - y = e.MeasurementArray([0.3, 0.56, 0.2], error = 0.01) - - self.assertEqual(e.sin(x).means.tolist(), np.sin(x.means).tolist()) - self.assertEqual(e.cos(x).means.tolist(), np.cos(x.means).tolist()) - self.assertEqual(e.tan(x).means.tolist(), np.tan(x.means).tolist()) - self.assertEqual(e.csc(x).means.tolist(), (1/np.sin(x.means)).tolist()) - self.assertEqual(e.sec(x).means.tolist(), (1/np.cos(x.means)).tolist()) - self.assertEqual(e.cot(x).means.tolist(), (1/np.tan(x.means)).tolist()) - self.assertEqual(e.exp(x).means.tolist(), np.exp(x.means).tolist()) - self.assertEqual(e.log(x).means.tolist(), np.log(x.means).tolist()) - self.assertEqual(e.asin(y).means.tolist(), np.arcsin(y.means).tolist()) - self.assertEqual(e.acos(y).means.tolist(), np.arccos(y.means).tolist()) - self.assertEqual(e.atan(x).means.tolist(), np.arctan(x.means).tolist()) - - def test_derivative(self): - '''Tests derivative of functions of Measurement objects - ''' - x = e.Measurement(3, 0.4) - y = e.Measurement(12, 1) - - self.assertEqual((x+y).get_derivative(y), 1) - self.assertEqual((x-y).get_derivative(x), 1) - self.assertEqual((x*y).get_derivative(y), x.mean) - self.assertEqual((x/y).get_derivative(x), 1/y.mean) - self.assertEqual((x**y).get_derivative(x), y.mean*x.mean**(y.mean-1)) - self.assertEqual(e.sin(x).get_derivative(x), m.cos(x.mean)) - self.assertEqual(e.cos(x).get_derivative(x), -m.sin(x.mean)) - self.assertEqual(e.tan(x).get_derivative(x), m.cos(x.mean)**-2) - self.assertEqual(e.exp(x).get_derivative(x), m.exp(x.mean)) - -class TestArrayOps(unittest.TestCase): - def test_append(self): - '''Tests appending new values to a MeasurementArray. - ''' - x = e.MeasurementArray([3, 2], 1) - x = x.append(1) - - to_append = e.Measurement(4, 1) - x = x.append(to_append) - - self.assertEqual(x[2], 1) - self.assertEqual(x[3], to_append) - - def test_insert(self): - '''Tests inserting new values into a MeasurementArray. - ''' - x = e.MeasurementArray([3, 1], 1) - x = x.insert(1, 2) - - to_insert = e.Measurement(4, 1) - x = x.insert(2, to_insert) - - self.assertEqual(x[1], 2) - self.assertEqual(x[2], to_insert) - - def test_delete(self): - '''Tests inserting new values into a MeasurementArray. - ''' - x = e.MeasurementArray([3, 2, 1], 1) - x = x.delete(1) - - self.assertEqual(len(x), 2) - -class TestFitting(unittest.TestCase): - def test_linear_fit(self): - ''' Test of plotting fit - ''' - X = e.MeasurementArray([1, 2, 3, 4, 5], [0.1]) - Y = e.MeasurementArray([3, 5, 7, 9, 11], [0.05]) - - figure = p.MakePlot(xdata = X, ydata = Y) - slope, intercept = figure.fit('linear', print_results=False) - - self.assertEqual(slope, 2) - self.assertEqual(intercept, 1) - - def test_polynomial_fit(self): - ''' Test of plotting fit - ''' - X = e.MeasurementArray([-2, -1, 0, 1, 2], [0.1]) - Y = 3*X**2+2*X+1 - - figure = p.MakePlot(xdata = X, ydata = Y) - figure.fit('pol2', print_results=False) - par0 = figure.get_dataset().xyfitter[0].fit_pars[0] - par1 = figure.get_dataset().xyfitter[0].fit_pars[1] - par2 = figure.get_dataset().xyfitter[0].fit_pars[2] - - self.assertAlmostEqual(par0, 1, places=7) - self.assertAlmostEqual(par1, 2, places=7) - self.assertAlmostEqual(par2, 3, places=7) - - def test_gaussian_fit(self): - ''' Test of plotting fit - ''' - X = e.MeasurementArray([-1, -1/3, 1/3, 1], [0.1]) - mean = 0.1 - std = 0.5 - norm = .5 - Y = norm*(2*m.pi*std**2)**(-0.5)*np.exp(-0.5*(X-mean)**2/std**2) - - figure = p.MakePlot(xdata = X, ydata = Y) - figure.fit('gauss', print_results=False) - - par0 = figure.get_dataset().xyfitter[0].fit_pars[0] - par1 = figure.get_dataset().xyfitter[0].fit_pars[1] - par2 = figure.get_dataset().xyfitter[0].fit_pars[2] - - self.assertAlmostEqual(par0, mean, places=7) - self.assertAlmostEqual(par1, std, places=7) - self.assertAlmostEqual(par2, norm, places=7) - -class TestMisc(unittest.TestCase): - def test_unit_propagation(self): - '''Tests unit propagation of Measurements - ''' - L = e.Measurement(12, 1, name='Distance', units='m') - v = e.Measurement(5, 0.1, name='Velocity', units=['m', 1, 's', -1]) - t = L/v - self.assertEqual(L.units, {'m': 1}) - self.assertEqual(v.units, {'s': -1, 'm': 1}) - self.assertEqual(t.units, {'s': 1}) - - x = e.Measurement(2, 0.3, name='Length', units='m') - x2 = x + L - self.assertEqual(x2.units, {'m': 1}) - - L = v*t - self.assertEqual(L.units, {'m': 1}) - - def test_unit_parsing(self): - '''Tests parsing of unit strings. - ''' - test1 = e.Measurement(10, 1, units='kg*m/s^2') - test2 = e.Measurement(10, 1, units='kg^1m^1s^-2') - test3 = e.Measurement(10, 1, units='kg^1*m^1/s^2') - - units = {'kg':1, 'm':1, 's':-2} - - self.assertEqual(test1.units, units) - self.assertEqual(test2.units, units) - self.assertEqual(test3.units, units) - - def test_printing(self): - '''Test of printing methods and sigfigs. - ''' - # Test of standard printing without figs ################################## - x = e.Measurement(12563.2, 1.637) - e.set_print_style('Latex') - self.assertEqual(x.__str__(), '(12563 \pm 2)') - - x = e.Measurement(156.2, 12) - e.set_print_style('Default') - self.assertEqual(x.__str__(), '160 +/- 10') - - x = e.Measurement(1360.2, 16.9) - e.set_print_style('Sci') - self.assertEqual(x.__str__(), '(136 +/- 2)*10^(1)') - - # Test of figs set on central value ####################################### - e.set_print_style('Default', 3) - x = e.Measurement(12.3, 0.1, name='x') - self.assertEqual(x.__str__(), 'x = 12.3 +/- 0.1') - - e.set_print_style('Latex', 4) - x = e.Measurement(12.3, 0.156, name='x') - self.assertEqual(x.__str__(), 'x = (1230 \pm 16)*10^{-2}') - - e.set_print_style('Sci', 5) - x = e.Measurement(123.456, 0.789, name='x') - self.assertEqual(x.__str__(), 'x = (12346 +/- 79)*10^(-2)') - - # Test of figs set on uncertainty ######################################### - x = e.Measurement(12.35, 0.1237) - e.set_print_style('Default') - e.set_sigfigs_error() - self.assertEqual(x.__str__(), '12.350 +/- 0.124') - - x = e.Measurement(120, 0.1237795) - e.set_print_style('Latex') - e.set_sigfigs_error(5) - self.assertEqual(x.__str__(), '(12000000 \pm 12378)*10^{-5}') - - x = e.Measurement(12.38, 0.1237) - e.set_print_style('Sci') - e.set_sigfigs_error(1) - self.assertEqual(x.__str__(), '(124 +/- 1)*10^(-1)') - - - def test_public_methods(self): - '''Test of public methods to return Measurement object attributes. - ''' - x = e.Measurement(10, 1, name='x', units='m') - y = e.Measurement(13, 2, name='y', units=['m', 1]) - a = x+y - - d = e.Measurement(21, 1, name='Distance', units='m') - t = e.Measurement(7, 2, name='Interval', units='s') - v = d/t - - self.assertEqual(x.mean, 10) - self.assertEqual(y.std, 2) - self.assertEqual(a.get_derivative(x), 1) - self.assertEqual(a.name, 'x+y') - self.assertEqual(a.get_units_str(), 'm') - self.assertTrue(v.get_units_str() == 'm^1 s^-1 ' or v.get_units_str() == 's^-1 m^1 ') diff --git a/qexpy/data/__init__.py b/qexpy/data/__init__.py new file mode 100644 index 0000000..b4f0777 --- /dev/null +++ b/qexpy/data/__init__.py @@ -0,0 +1,9 @@ +"""This package contains the data structures and operations for experimental values""" + +from .data import MeasuredValue as Measurement +from .datasets import ExperimentalValueArray as MeasurementArray, XYDataSet +from .data import get_covariance, set_covariance, get_correlation, set_correlation +from .data import reset_correlations +from .operations import sqrt, exp, sin, sind, cos, cosd, tan, tand, sec, secd, cot, cotd, \ + csc, cscd, asin, acos, atan, log, log10, pi, e +from .operations import std, mean, sum_ as sum # pylint: disable=redefined-builtin diff --git a/qexpy/data/data.py b/qexpy/data/data.py new file mode 100644 index 0000000..96cfa45 --- /dev/null +++ b/qexpy/data/data.py @@ -0,0 +1,1014 @@ +"""Module containing the core data structures for experimental values + +This module defines ExperimentalValue and all its sub-classes. They serve as a container for +quantities recorded in an experiment, or calculated in subsequent data analysis, with error +propagation and other features (such as unit propagation) built-in. + +""" + +import uuid +import warnings +import numpy as np + +from abc import ABC, abstractmethod +from typing import Dict, List, Union +from numbers import Real +from collections import namedtuple + +from qexpy.utils import IllegalArgumentError, UndefinedActionError +from qexpy.settings import ErrorMethod + +import qexpy.utils as utils +import qexpy.settings as sts +import qexpy.settings.literals as lit + +from . import operations as op +from . import utils as dut + +ARRAY_TYPES = list, np.ndarray + +# A simple data structure to store a value-uncertainty pair +ValueWithError = namedtuple("ValueWithError", "value, error") + +# A sub-tree in an expression tree representing a formula. The "operator" is the root node of +# the sub-tree, and the "operands" is a list of branches. The leaf nodes of a complete tree +# are individual ExperimentalValue instances +Formula = namedtuple("Formula", "operator, operands") + +# A data structure to store the correlation between two values. +Correlation = namedtuple("Correlation", "correlation, covariance") + + +class ExperimentalValue(ABC): + """Base class for quantities with a value and an uncertainty + + The ExperimentalValue is a container for an individual quantity involved in an experiment + and subsequent data analysis. Each quantity has a value and an uncertainty (error), and + optionally, a name and a unit. ExperimentalValue instances can be used in calculations + just like any other numerical variable in Python. The result of such calculations will be + wrapped in ExperimentalValue instances, with the properly propagated uncertainties. + + Examples: + >>> import qexpy as q + + >>> a = q.Measurement(302, 5) # The standard way to initialize an ExperimentalValue + + >>> # Access the basic properties + >>> a.value + 302 + >>> a.error + 5 + >>> a.relative_error # This is defined as error/value + 0.016556291390728478 + + >>> # These properties can be changed + >>> a.value = 303 + >>> a.value + 303 + >>> a.relative_error = 0.05 + >>> a.error # The error and relative_error are connected + 15.15 + + >>> # You can specify the name or the units of a value + >>> a.name = "force" + >>> a.unit = "kg*m^2/s^2" + + >>> # The string representation of the value will include the name and units + >>> print(a) + force = 300 +/- 20 [kg⋅m^2⋅s^-2] + + >>> # You can also specify how you want the values or the units to be printed + >>> q.set_print_style(q.PrintStyle.SCIENTIFIC) + >>> q.set_unit_style(q.UnitStyle.FRACTION) + >>> q.set_sig_figs_for_error(2) + >>> print(a) + force = (3.03 +/- 0.15) * 10^2 [kg⋅m^2/s^2] + + """ + + # Static register that stores references to all instantiated values in a session. + _register = {} # type: Dict[uuid.UUID, "ExperimentalValue"] + + # Static database that stores all correlations between measurements. The key of this + # database is the UUIDs of the two measurements concatenated in natual order. + _correlations = {} # type: Dict[str, Correlation] + + def __init__(self, unit: str = "", name: str = "", save=True): + """Constructor for ExperimentalValue""" + + # Stores each unit string and their powers. + if unit is not None and not isinstance(unit, str): + raise TypeError("The unit provided is not a string!") + self._unit = utils.parse_unit_string(unit) if unit else {} # type: Dict[str, int] + + # The name of this quantity if given + if name is not None and not isinstance(name, str): + raise TypeError("The name provided is not a string!") + self._name = name # type: str + + # Each instance is given a unique ID for easy reference + self._id = uuid.uuid4() # type: uuid.UUID + + if save: # save this value in the register + self._register[self._id] = self + + def __str__(self): + name_string = "{} = ".format(self.name) if self.name else "" + unit_string = " [{}]".format(self.unit) if self.unit else "" + return "{}{}{}".format(name_string, self.print_value_error(), unit_string) + + def __repr__(self): + return "{}({})".format(self.__class__.__name__, self.print_value_error()) + + @property + @abstractmethod + def value(self): + """float: The center value of this quantity""" + raise NotImplementedError + + @property + @abstractmethod + def error(self): + """float: The uncertainty of this quantity""" + raise NotImplementedError + + @property + @abstractmethod + def relative_error(self): + """float: The ratio of the uncertainty to its center value""" + raise NotImplementedError + + @property + def std(self): + """float: The standard deviation of this quantity""" + return self.error # usually the standard deviation is the error + + @property + def name(self): + """str: The name of this quantity""" + return self._name + + @name.setter + def name(self, new_name: str): + if not isinstance(new_name, str): + raise TypeError( + "Cannot set name of value to \"{}\"".format(type(new_name).__name__)) + self._name = new_name + + @property + def unit(self): + """str: The unit of this quantity""" + return utils.construct_unit_string(self._unit) if self._unit else "" + + @unit.setter + def unit(self, new_unit: str): + if not isinstance(new_unit, str): + raise TypeError( + "Cannot set unit of value to \"{}\"".format(type(new_unit).__name__)) + self._unit = utils.parse_unit_string(new_unit) if new_unit else {} + + @utils.check_operand_type("==") + def __eq__(self, other): + return self.value == dut.wrap_in_experimental_value(other).value + + def __neg__(self): + return DerivedValue(Formula(lit.NEG, [self])) + + @utils.check_operand_type(">") + def __gt__(self, other): + return self.value > dut.wrap_in_experimental_value(other).value + + @utils.check_operand_type(">=") + def __ge__(self, other): + return self.value >= dut.wrap_in_experimental_value(other).value + + @utils.check_operand_type("<") + def __lt__(self, other): + return self.value < dut.wrap_in_experimental_value(other).value + + @utils.check_operand_type("<=") + def __le__(self, other): + return self.value <= dut.wrap_in_experimental_value(other).value + + @utils.check_operand_type("pow") + def __pow__(self, power): + if isinstance(power, ARRAY_TYPES): + return power.__rpow__(self) + return DerivedValue(Formula(lit.POW, [self, dut.wrap_in_experimental_value(power)])) + + @utils.check_operand_type("pow") + def __rpow__(self, other): + return DerivedValue(Formula(lit.POW, [ + dut.wrap_in_experimental_value(other), self])) + + @utils.check_operand_type("+") + def __add__(self, other): + if isinstance(other, ARRAY_TYPES): + return other.__radd__(self) + return DerivedValue(Formula(lit.ADD, [self, dut.wrap_in_experimental_value(other)])) + + @utils.check_operand_type("+") + def __radd__(self, other): + return DerivedValue(Formula(lit.ADD, [ + dut.wrap_in_experimental_value(other), self])) + + @utils.check_operand_type("-") + def __sub__(self, other): + if isinstance(other, ARRAY_TYPES): + return other.__rsub__(self) + return DerivedValue(Formula(lit.SUB, [self, dut.wrap_in_experimental_value(other)])) + + @utils.check_operand_type("-") + def __rsub__(self, other): + return DerivedValue(Formula(lit.SUB, [ + dut.wrap_in_experimental_value(other), self])) + + @utils.check_operand_type("*") + def __mul__(self, other): + if isinstance(other, ARRAY_TYPES): + return other.__rmul__(self) + return DerivedValue(Formula(lit.MUL, [self, dut.wrap_in_experimental_value(other)])) + + @utils.check_operand_type("*") + def __rmul__(self, other): + return DerivedValue(Formula(lit.MUL, [ + dut.wrap_in_experimental_value(other), self])) + + @utils.check_operand_type("/") + def __truediv__(self, other): + if isinstance(other, ARRAY_TYPES): + return other.__rtruediv__(self) + return DerivedValue(Formula(lit.DIV, [self, dut.wrap_in_experimental_value(other)])) + + @utils.check_operand_type("/") + def __rtruediv__(self, other): + return DerivedValue(Formula(lit.DIV, [ + dut.wrap_in_experimental_value(other), self])) + + @abstractmethod + def derivative(self, other: "ExperimentalValue") -> float: + """Calculates the derivative of this quantity with respect to another + + The derivative of any value with respect to itself is 1, and for unrelated values, + the derivative is always 0. This method is typically called from a DerivedValue, + to find out its derivative with respect to one of the measurements it's derived from. + + Args: + other (ExperimentalValue): the target for finding the derivative + + """ + raise NotImplementedError + + # pylint: disable=no-self-use,unused-argument + def get_covariance(self, other: "ExperimentalValue") -> float: + """Gets the covariance between this value and another value""" + return 0 # default covariance is 0 + + def set_covariance(self, other: "ExperimentalValue", cov: float = None): + """Sets the covariance between this value and another value + + The covariance between two variables is by default 0. Users can set the covariance + between two measurements to any value, and it will be taken into account during error + propagation. When two measurements are recorded as arrays of repeated measurements of + the same length, users can leave the covariance term empty, and let QExPy calculate + the covariance between them. You should only do this when these two quantities are + measured at the same time, and can be related physically. + + Examples: + >>> import qexpy as q + >>> a = q.Measurement(5, 0.5) + >>> b = q.Measurement(6, 0.3) + + >>> # The user can manually set the covariance between two values + >>> a.set_covariance(b, 0.135) + >>> a.get_covariance(b) + 0.135 + + >>> # The correlation factor is calculated behind the scene as well + >>> a.get_correlation(b) + 0.9 + + >>> # The user can ask QExPy to calculate the covariance if applicable + >>> a = q.Measurement([1, 1.2, 1.3, 1.4]) + >>> b = q.Measurement([2, 2.1, 3, 2.3]) + >>> a.set_covariance(b) # this will declare that a and b are indeed correlated + >>> a.get_covariance(b) + 0.0416667 + + """ + raise UndefinedActionError("Cannot set covariance between non-measurements.") + + # pylint: disable=no-self-use,unused-argument + def get_correlation(self, other: "ExperimentalValue") -> float: + """Gets the correlation between this value and another value""" + return 0 # default correlation is 0 + + def set_correlation(self, other: "ExperimentalValue", corr: float = None): + """Sets the correlation between this value and another value + + The correlation factor is a value between -1 and 1. This method can be used the same + way as set_covariance. + + See Also: + :py:func:`ExperimentalValue.set_covariance` + + """ + raise UndefinedActionError("Cannot set correlation between non-measurements.") + + def print_value_error(self) -> str: + """Helper method that prints the value-error pair in proper format""" + return utils.get_printer()(self.value, self.error) + + @staticmethod + def get(variable_id: uuid.UUID) -> "ExperimentalValue": + """Retrieves a value from the register using its UUID""" + return ExperimentalValue._register[ + variable_id] if variable_id in ExperimentalValue._register else None + + +class Constant(ExperimentalValue): + """A value with no uncertainty""" + + def __init__(self, value, **kwargs): + super().__init__(**kwargs, save=False) + self._value_error = ValueWithError(value, 0) + + @property + def value(self) -> float: + return self._value_error.value + + @property + def error(self) -> 0: + return 0 # pragma: no cover + + @property + def relative_error(self) -> 0: + return 0 # pragma: no cover + + def derivative(self, other: "ExperimentalValue") -> 0: + return 0 # the derivative of a constant with respect to anything is 0 + + +class MeasuredValue(ExperimentalValue): + """Container for user-recorded values with uncertainties + + The MeasuredValue represents a single measurement recorded in an experiment. This class + is given an alias "Measurement" for backward compatibility and for a more intuitive user + interface. On the top level of this package, this class is imported as "Measurement". + + Args: + data (Real|List): The center value of the measurement + error (Real|List): The uncertainty on the value + + Keyword Args: + unit (str): The unit of this value + name (str): The name of this value + + """ + + def __new__(cls, data, error=None, **kwargs): # pylint: disable=unused-argument + if isinstance(data, Real): + instance = super().__new__(cls) + elif isinstance(data, ARRAY_TYPES): + instance = super().__new__(RepeatedlyMeasuredValue) + else: + raise IllegalArgumentError("Invalid data type to record a measurement!") + return instance + + def __init__(self, data, error=None, **kwargs): + if error is not None and not isinstance(error, Real): + raise IllegalArgumentError("Invalid data type to record an uncertainty!") + unit = kwargs.get("unit", "") + name = kwargs.get("name", "") + save = kwargs.get("save", True) + super().__init__(unit, name, save=save) + self._value, self._error = float(data), float(error) if error else 0.0 + + @property + def value(self): + return self._value + + @value.setter + def value(self, value: Real): + if not isinstance(value, Real): + raise TypeError("Cannot assign a {} to the value!".format(type(value).__name__)) + self._value = value + + @property + def error(self): + return self._error + + @error.setter + def error(self, error: Real): + if not isinstance(error, Real): + raise TypeError("Cannot assign a {} to the error!".format(type(error).__name__)) + if error < 0: + raise ValueError("The error must be a positive real number!") + self._error = error + + @property + def relative_error(self): + return self.error / self.value if self.value != 0 else 0. + + @relative_error.setter + def relative_error(self, relative_error: Real): + if not isinstance(relative_error, Real): + raise TypeError( + "Cannot assign a {} to the error!".format(type(relative_error).__name__)) + if relative_error < 0: + raise ValueError("The error must be a positive real number!") + new_error = self.value * float(relative_error) + self._error = new_error + + def derivative(self, other: "ExperimentalValue") -> float: + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError( + "You can only find derivative with respect to another ExperimentalValue") + # Derivative of a measurement with respect to anything other than itself is 0 + return 1 if self._id == other._id else 0 + + def get_covariance(self, other: "ExperimentalValue") -> float: + """Gets the covariance of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Cannot find covariance for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + return 0 # only covariance between measurements is supported. + + if self.std == 0 or other.std == 0: + return 0 # constants don't correlate with anyone + if self._id == other._id: + # The covariance between a measurement and itself is the variance + return self.std ** 2 + + id_string = "_".join(sorted([str(self._id), str(other._id)])) + if id_string in ExperimentalValue._correlations: + return ExperimentalValue._correlations[id_string].covariance + + return 0 + + def set_covariance(self, other: "ExperimentalValue", cov: float = None): + """Sets the covariance of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Cannot set covariance for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + raise IllegalArgumentError("Only covariance between measurements is supported.") + + if self.std == 0 or other.std == 0: + raise ArithmeticError("Cannot set covariance for values with 0 errors") + if cov is None: + raise IllegalArgumentError( + "The covariance is not provided, and cannot be calculated!") + + corr = cov / (self.std * other.std) + # check that the result makes sense + if corr > 1 or corr < -1: + raise ValueError("The covariance: {} is non-physical".format(cov)) + + # register the correlation between these measurements + id_string = "_".join(sorted([str(self._id), str(other._id)])) + correlation_record = Correlation(corr, cov) + ExperimentalValue._correlations[id_string] = correlation_record + + def get_correlation(self, other: "ExperimentalValue") -> float: + """Gets the correlation factor of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Can't find correlation for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + return 0 # only covariance between measurements is supported. + + if self.std == 0 or other.std == 0: + return 0 # constants don't correlate with anyone + if self._id == other._id: + return 1 # values have unit correlation with themselves + + id_string = "_".join(sorted([str(self._id), str(other._id)])) + if id_string in ExperimentalValue._correlations: + return ExperimentalValue._correlations[id_string].correlation + return 0 + + def set_correlation(self, other: "ExperimentalValue", corr: float = None): + """Sets the correlation factor of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Cannot set correlation for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + raise IllegalArgumentError("Only covariance between measurements is supported.") + + if self.std == 0 or other.std == 0: + raise ArithmeticError("Cannot set correlation for values with 0 errors") + if corr is None: + raise IllegalArgumentError( + "The correlation factor is not provided, and cannot be calculated!") + + # check that the result makes sense + if corr > 1 or corr < -1: + raise ValueError("The correlation factor: {} is non-physical".format(corr)) + cov = corr * (self.std * other.std) + + # register the correlation between these measurements + id_string = "_".join(sorted([str(self._id), str(other._id)])) + correlation_record = Correlation(corr, cov) + ExperimentalValue._correlations[id_string] = correlation_record + + +class RepeatedlyMeasuredValue(MeasuredValue): + """Container for a MeasuredValue recorded as an array of repeated measurements + + This class is instantiated if an array of values is used to record a Measurement of a + single quantity with repeated takes. By default, the mean of the array is used as the + value of this quantity, and the standard error (error on the mean) is the uncertainty. + The reason for this choice is because the reason for taking multiple measurements is + usually to minimize the uncertainty on the quantity, not to find out the uncertainty on + a single measurement (which is what standard deviation is). + + Examples: + >>> import qexpy as q + + >>> # The most common way of recording a value with repeated measurements is to only + >>> # give the center values for the measurements + >>> a = q.Measurement([9, 10, 11]) + >>> print(a) + 10.0 +/- 0.6 + + >>> # There are other statistical properties of the array of measurements + >>> a.std + 1 + >>> a.error_on_mean + 0.5773502691896258 + + >>> # You can choose to use the standard deviation as the uncertainty + >>> a.use_std_for_uncertainty() + >>> a.error + 1 + + >>> # You can also specify individual uncertainties for the measurements + >>> a = q.Measurement([10, 11], [0.1, 1]) + >>> print(a) + 10.5 +/- 0.5 + >>> a.error_weighted_mean + 10.00990099009901 + >>> a.propagated_error + 0.09950371902099892 + + >>> # You can choose which statistical properties to be used as the value/error + >>> a.use_error_weighted_mean_as_value() + >>> a.use_propagated_error_for_uncertainty() + >>> q.set_sig_figs_for_error(4) + >>> print(a) + 10.00990 +/- 0.09950 + + """ + + def __init__(self, data: List, error: Union[List, Real] = None, **kwargs): + """Constructor of a RepeatedlyMeasuredValue""" + + # Check validity of inputs + if isinstance(error, ARRAY_TYPES) and len(error) != len(data): + raise ValueError("The lengths of uncertainties and data do not match") + + # pylint: disable=cyclic-import + from .datasets import ExperimentalValueArray + + # Initialize raw data and its uncertainties. Internally, the raw data is implemented + # as an ExperimentalValueArray. However, in principle, the ExperimentalValueArray + # should only be used for an array of measurements of different quantities. + self._raw_data = ExperimentalValueArray(data, error, save=False, **kwargs) + + # Calculate its statistical properties + self._mean = self._raw_data.mean().value + self._std = self._raw_data.std(ddof=1) + self._error_on_mean = self._raw_data.error_on_mean() + + # Call parent constructor with mean and error on mean as value and uncertainty + super().__init__(self._mean, self._error_on_mean, **kwargs) + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value: Real): + if not isinstance(new_value, Real): + raise TypeError( + "Cannot assign a {} to the value!".format(type(new_value).__name__)) + warnings.warn( + "You are trying to override the value calculated from an array of repeated " + "measurements. This value is now considered a single Measurement.") + self.__class__ = MeasuredValue + self._value = new_value + + @property + def raw_data(self): + """np.ndarray: The raw data that was used to generate this measurement""" + return self._raw_data.values if all( + x.error == 0 for x in self._raw_data) else self._raw_data + + @property + def std(self): + """float: The standard deviation of the raw data""" + return self._std + + @property + def error_on_mean(self): + """float: The error on the mean or the standard error""" + return self._error_on_mean + + @property + def mean(self): + """float: The mean of raw measurements""" + return self._mean + + @property + def error_weighted_mean(self): + """float: Error weighted mean if individual errors are specified""" + return self._raw_data.error_weighted_mean() + + @property + def propagated_error(self): + """float: Error propagated with errors passed in if present""" + return self._raw_data.propagated_error() + + def use_std_for_uncertainty(self): + """Sets the uncertainty of this value to the standard deviation""" + self._error = self._std + + def use_error_on_mean_for_uncertainty(self): + """Sets the uncertainty of this value to the error on the mean""" + self._error = self._error_on_mean + + def use_error_weighted_mean_as_value(self): + """Sets the value of this object to the error weighted mean""" + error_weighted_mean = self.error_weighted_mean + if not np.isnan(error_weighted_mean): + self._value = error_weighted_mean + else: # pragma: no cover + warnings.warn("The error weighted mean is not valid") + + def use_propagated_error_for_uncertainty(self): + """Sets the uncertainty of this object to the weight propagated error""" + propagated_error = self.propagated_error + if not np.isnan(propagated_error): + self._error = propagated_error + else: # pragma: no cover + warnings.warn("The propagated error is not valid") + + def set_covariance(self, other: "ExperimentalValue", cov: float = None): + """Sets the covariance of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Cannot set covariance for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + raise IllegalArgumentError("Only covariance between measurements is supported.") + + if cov is None and isinstance(other, RepeatedlyMeasuredValue): + try: + cov = utils.calculate_covariance(self.raw_data, other.raw_data) + except ValueError: + cov = None + + super().set_covariance(other, cov) + + def set_correlation(self, other: "ExperimentalValue", corr: float = None): + """Sets the correlation factor of this value with another value""" + + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError("Cannot set correlation for non-QExPy defined values") + if not isinstance(other, MeasuredValue): + raise IllegalArgumentError("Only covariance between measurements is supported.") + + if corr is None and isinstance(other, RepeatedlyMeasuredValue): + try: + cov = utils.calculate_covariance(self.raw_data, other.raw_data) + corr = cov / (self.std * other.std) + except ValueError: + corr = None + + super().set_correlation(other, corr) + + def show_histogram(self, **kwargs) -> tuple: # pragma: no cover + """Plots the raw measurement data in a histogram + + See Also: + This works the same as the :py:func:`~qexpy.fitting.fitting.hist` function in + the plotting module of QExPy + + """ + import qexpy.plotting as plotting # pylint:disable=cyclic-import + values, bins, figure = plotting.hist(self.raw_data, **kwargs) + figure.show() + return values, bins, figure + + +class DerivedValue(ExperimentalValue): + """Result of calculations performed with ExperimentalValue instances + + This class is automatically instantiated when the user performs calculations with other + ExperimentalValue instances. It is created with the properly propagated uncertainties and + units. The two available methods for error propagation are the derivative method, and the + Monte Carlo method. + + Internally, a DerivedValue preserves information on how it is calculated, so the user is + able to make use of that information. For example, the user can find the derivative of + a DerivedValue with respect to another ExperimentalValue that this value is derived from. + + Examples: + >>> import qexpy as q + + >>> # First let's create some standard measurements + >>> a = q.Measurement(5, 0.2) + >>> b = q.Measurement(4, 0.1) + >>> c = q.Measurement(6.3, 0.5) + >>> d = q.Measurement(7.2, 0.5) + + >>> # Now we can perform operations on them + >>> result = q.sqrt(c) * d - b / q.exp(a) + >>> result + DerivedValue(18 +/- 1) + >>> result.value + 18.04490478513969 + >>> result.error + 1.4454463754287323 + + >>> # By default, the standard derivative method is used, but it can be changed + >>> q.set_error_method(q.ErrorMethod.MONTE_CARLO) + >>> result.value + 18.03203135268583 + >>> result.error + 1.4116412532654283 + >>> # If we want this value to use a different error method from the global default + >>> result.error_method = "derivative" # this only affects this value alone + >>> result.error + 1.4454463754287323 + >>> # If we want to reset the error method for this value and use the global default + >>> result.reset_error_method() + >>> result.error + 1.4116412532654283 + + """ + + def __init__(self, formula: Formula): + """Constructor for a DerivedValue""" + + # The error method used for error propagation of this value + self.__error_method = ErrorMethod.AUTO # type: ErrorMethod + + # The expression tree representing how this value is derived. + self._formula = formula # type: Formula + + # The objects used to evaluate the formula with the appropriate error methods + self.__evaluators = { + lit.DERIVATIVE: op.DerivativeEvaluator(), + lit.MONTE_CARLO: op.MonteCarloEvaluator() + } # type: Dict[str, op.Evaluator] + + super().__init__(save=True) + + self._unit = op.propagate_units(formula) + + @property + def value(self): + return self.__get_value_error_pair().value + + @value.setter + def value(self, new_value: Real): + if not isinstance(new_value, Real): + raise TypeError( + "Cannot assign a {} to the value".format(type(new_value).__name__)) + warnings.warn( + "You are trying to override the calculated value of a derived quantity. This " + "value is casted to a regular Measurement") + error = self.error + self.__class__ = MeasuredValue # casting it to MeasuredValue + self.value, self.error = new_value, error + + @property + def error(self): + return self.__get_value_error_pair().error + + @error.setter + def error(self, new_error: Real): + if not isinstance(new_error, Real): + raise TypeError( + "Cannot assign a {} to the error!".format(type(new_error).__name__)) + if new_error < 0: + raise ValueError("The error must be a positive real number!") + warnings.warn( + "You are trying to override the propagated error of a derived quantity. This " + "value is casted to a regular Measurement") + value = self.value + self.__class__ = MeasuredValue # casting it to MeasuredValue + self.value, self.error = value, new_error + + @property + def relative_error(self): + return self.error / self.value if self.value != 0 else 0. + + @relative_error.setter + def relative_error(self, relative_error: Real): + if not isinstance(relative_error, Real): + raise TypeError( + "Cannot assign a {} to the error!".format(type(relative_error).__name__)) + if relative_error < 0: + raise ValueError("The error must be a positive real number!") + new_error = self.value * float(relative_error) + warnings.warn( + "You are trying to override the propagated relative error of a derived quantity." + " This value is casted to a regular Measurement") + value = self.value + self.__class__ = MeasuredValue # casting it to MeasuredValue + self.value, self.error = value, new_error + + @property + def error_method(self): + """ErrorMethod: The default error method used for this value + + QExPy currently supports two different methods of error propagation, the derivative + method, and the Monte-Carlo method. The user can change the global default which + applies to all values, or set the error method of this single quantity if it is to + be different from the global settings. + + """ + if self.__error_method == ErrorMethod.AUTO: + return sts.get_settings().error_method + return self.__error_method + + @error_method.setter + def error_method(self, new_error_method: Union[ErrorMethod, str]): + if isinstance(new_error_method, ErrorMethod): + self.__error_method = new_error_method + elif new_error_method in [lit.MONTE_CARLO, lit.DERIVATIVE]: + self.__error_method = ErrorMethod(new_error_method) + else: + raise ValueError("Invalid error method!") + + @property + def mc(self): + """dut.MonteCarloSettings: The settings object for customizing Monte Carlo""" + evaluator = self.__evaluators[lit.MONTE_CARLO] + assert isinstance(evaluator, op.MonteCarloEvaluator) + evaluator.regenerate_samples(self._formula) + return evaluator.settings + + def reset_error_method(self): + """Resets the default error method for this value to follow the global settings""" + self.__error_method = ErrorMethod.AUTO + + def recalculate(self): + """Recalculates the value + + A DerivedValue instance preserves information on how the value was derived. If values + of the original measurements are changed, and you wish to update the derived value + using the exact same formula, this method can be used. + + Examples: + + >>> import qexpy as q + + >>> a = q.Measurement(5, 0.2) + >>> b = q.Measurement(4, 0.1) + + >>> c = a + b + >>> c + DerivedValue(9.0 +/- 0.2) + + >>> # Now we change the value of a + >>> a.value = 8 + >>> c.recalculate() + >>> c + DerivedValue(12.0 +/- 0.2) + + """ + for evaluator in self.__evaluators.values(): + evaluator.clear() + + def derivative(self, other: ExperimentalValue) -> float: + if not isinstance(other, ExperimentalValue): + raise IllegalArgumentError( + "You can only find derivative with respect to another ExperimentalValue") + return 1 if self._id == other._id else op.differentiate(self._formula, other) + + def show_error_contributions(self): # pragma: no cover + """Displays measurements' contribution to the final uncertainty""" + import matplotlib.pyplot as plt + evaluator = self.__evaluators[lit.DERIVATIVE] + assert isinstance(evaluator, op.DerivativeEvaluator) + evaluator.evaluate(self._formula) + measurements, contributions = evaluator.measurements, evaluator.error_contributions + names = list(var.name if var.name else "var_{}".format(idx) + for idx, var in enumerate(measurements)) + plt.bar(list(range(len(measurements))), contributions, tick_label=names) + plt.title("Error Contributions") + plt.show() + + def __get_value_error_pair(self) -> ValueWithError: + """Gets the value-error pair for the current specified error method""" + error_method = self.error_method.value + return self.__evaluators[error_method].evaluate(self._formula) + + +def get_covariance(var1: ExperimentalValue, var2: ExperimentalValue) -> float: + """Finds the covariances between two ExperimentalValue instances + + Args: + var1, var2 (ExperimentalValue): the two values to find covariance between + + Returns: + The covariance between var1 and var2 + + See Also: + :py:func:`ExperimentalValue.get_covariance` + + """ + + if any(not isinstance(var, ExperimentalValue) for var in [var1, var2]): + raise IllegalArgumentError( + "Cannot find covariance between non-QExPy defined variables") + + # As of now, only covariance between measurements are supported. + if isinstance(var1, MeasuredValue) and isinstance(var2, MeasuredValue): + return var1.get_covariance(var2) + + return 0 + + +def set_covariance(var1: ExperimentalValue, var2: ExperimentalValue, cov: Real = None): + """Sets the covariance between two measurements + + Args: + var1, var2 (ExperimentalValue): the two values to set covariance between + + See Also: + :py:func:`ExperimentalValue.set_covariance` + + Examples: + >>> import qexpy as q + >>> a = q.Measurement(5, 0.5) + >>> b = q.Measurement(6, 0.3) + + >>> # The user can manually set the covariance between two values + >>> q.set_covariance(a, b, 0.135) + >>> q.get_covariance(a, b) + 0.135 + + """ + + if any(not isinstance(var, ExperimentalValue) for var in [var1, var2]): + raise IllegalArgumentError( + "Cannot set covariance between non-QExPy defined variables") + + var1.set_covariance(var2, cov) + + +def get_correlation(var1: ExperimentalValue, var2: ExperimentalValue) -> float: + """Finds the correlation between two ExperimentalValue instances + + Args: + var1, var2 (ExperimentalValue): the two values to find correlation between + + Returns: + The correlation factor between var1 and var2 + + See Also: + :py:func:`ExperimentalValue.get_correlation` + + """ + + if any(not isinstance(var, ExperimentalValue) for var in [var1, var2]): + raise IllegalArgumentError( + "Cannot find correlation between non-QExPy defined variables") + + # As of now, only covariance between measurements are supported. + if isinstance(var1, MeasuredValue) and isinstance(var2, MeasuredValue): + return var1.get_correlation(var2) + + return 0 + + +def set_correlation(var1: MeasuredValue, var2: MeasuredValue, corr: Real = None): + """Sets the correlation factor between two MeasuredValue objects + + Args: + var1, var2 (ExperimentalValue): the two values to set correlation between + + See Also: + :py:func:`ExperimentalValue.set_correlation` + + """ + if any(not isinstance(var, ExperimentalValue) for var in [var1, var2]): + raise IllegalArgumentError( + "Cannot set correlation between non-QExPy defined variables") + + var1.set_correlation(var2, corr) + + +def reset_correlations(): + """resets all correlation settings""" + ExperimentalValue._correlations.clear() # pylint: disable=protected-access + + +def get_variable_by_id(variable_id: uuid.UUID) -> ExperimentalValue: + """Internal method used to retrieve an ExperimentalValue instance with its ID""" + return ExperimentalValue.get(variable_id) diff --git a/qexpy/data/datasets.py b/qexpy/data/datasets.py new file mode 100644 index 0000000..f81a43a --- /dev/null +++ b/qexpy/data/datasets.py @@ -0,0 +1,588 @@ +"""Defines data structures for collections of individual measurements""" + +import re +import warnings + +import numpy as np +import math as m + +from typing import List # pylint: disable=unused-import +from numbers import Real + +from qexpy.utils import IllegalArgumentError + +from . import data as dt +from . import utils as dut + +import qexpy.utils as utils + +ARRAY_TYPES = np.ndarray, list + + +class ExperimentalValueArray(np.ndarray): + """An array of experimental values, alias: MeasurementArray + + An ExperimentalValueArray (MeasurementArray) represents a series of ExperimentalValue + objects. It is implemented as a sub-class of numpy.ndarray. This class is given an alias + "MeasurementArray" for more intuitive user interface. + + Args: + *args: The first argument is an array of real numbers representing the center values + of the measurements. The second argument (if present) is either a positive real + number or an array of positive real numbers of the same length as the data array, + representing the uncertainties on the measurements. + + Keyword Args: + data (List): an array of real numbers representing the center values + error (Real|List): the uncertainties on the measurements + relative_error (Real|List): the relative uncertainties on the measurements + unit (str): the unit of the measurement + name (str): the name of the measurement + + Examples: + >>> import qexpy as q + + >>> # We can instantiate an array of measurements with two lists + >>> a = q.MeasurementArray([1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4, 0.5]) + >>> a + ExperimentalValueArray([MeasuredValue(1.0 +/- 0.1), + MeasuredValue(2.0 +/- 0.2), + MeasuredValue(3.0 +/- 0.3), + MeasuredValue(4.0 +/- 0.4), + MeasuredValue(5.0 +/- 0.5)], dtype=object) + + >>> # We can also create an array of measurements with a single uncertainty. + >>> # As usual, if the error is not specified, they will be set to 0 by default + >>> a = q.MeasurementArray([1, 2, 3, 4, 5], 0.5, unit="m", name="length") + >>> a + ExperimentalValueArray([MeasuredValue(1.0 +/- 0.5), + MeasuredValue(2.0 +/- 0.5), + MeasuredValue(3.0 +/- 0.5), + MeasuredValue(4.0 +/- 0.5), + MeasuredValue(5.0 +/- 0.5)], dtype=object) + + >>> # We can access the different statistical properties of this array + >>> print(np.sum(a)) + 15 +/- 1 [m] + >>> print(a.mean()) + 3.0 +/- 0.7 [m] + >>> a.std() + 1.5811388300841898 + + >>> # Manipulation of a MeasurementArray is also very easy. We can append or insert + >>> # into the array values in multiple formats + >>> a = a.append((7, 0.2)) # a measurement can be inserted as a tuple + >>> print(a[5]) + length = 7.0 +/- 0.2 [m] + >>> a = a.insert(0, 8) # if error is not specified, it is set to 0 by default + >>> print(a[0]) + length = 8 +/- 0 [m] + + >>> # The same operations also works with array-like objects, in which case they are + >>> # concatenated into a single array + >>> a = a.append([(10, 0.1), (11, 0.3)]) + >>> a + ExperimentalValueArray([MeasuredValue(8.0 +/- 0), + MeasuredValue(1.0 +/- 0.5), + MeasuredValue(2.0 +/- 0.5), + MeasuredValue(3.0 +/- 0.5), + MeasuredValue(4.0 +/- 0.5), + MeasuredValue(5.0 +/- 0.5), + MeasuredValue(7.0 +/- 0.2), + MeasuredValue(10.0 +/- 0.1), + MeasuredValue(11.0 +/- 0.3)], dtype=object) + + >>> # The ExperimentalValueArray object is vectorized just like numpy.ndarray. You + >>> # can perform basic arithmetic operations as well as functions with them and get + >>> # back ExperimentalValueArray objects + >>> a = q.MeasurementArray([0, 1, 2], 0.5) + >>> a + 2 + ExperimentalValueArray([DerivedValue(2.0 +/- 0.5), + DerivedValue(3.0 +/- 0.5), + DerivedValue(4.0 +/- 0.5)], dtype=object) + >>> q.sin(a) + ExperimentalValueArray([DerivedValue(0.0 +/- 0.5), + DerivedValue(0.8 +/- 0.3), + DerivedValue(0.9 +/- 0.2)], dtype=object) + + See Also: + numpy.ndarray + + """ + + # pylint: disable=no-member,arguments-differ,too-many-function-args + + def __new__(cls, *args, **kwargs): + """Constructor for an ExperimentalValueArray + + __new__ is used instead of __init__ for object initialization. This is required for + subclassing the numpy.ndarray type. + + """ + + data = kwargs.pop("data", args[0] if args else None) + + if not isinstance(data, ARRAY_TYPES): + raise TypeError("You have not provided valid data to initialize the array.") + data = np.asarray(data) + + error = kwargs.pop("error", args[1] if len(args) > 1 else None) + relative_error = kwargs.pop("relative_error", None) + + error_array = _get_error_array_helper(data, error, relative_error) + + if all(isinstance(x, dt.ExperimentalValue) for x in data): + if error is None and relative_error is None: + error_array = None + return ExperimentalValueArray.__wrap(data, error_array=error_array, **kwargs) + + if not all(isinstance(x, Real) for x in data): + raise TypeError("Some values in the array are not real numbers") + + values = list( + dt.MeasuredValue(val, err, **kwargs) for val, err in zip(data, error_array)) + for index, meas in enumerate(values): + meas.name = "{}_{}".format(meas.name, index) if meas.name else "" + + # Initialize the instance to a numpy.ndarray + obj = np.asarray(values, dtype=dt.ExperimentalValue).view(ExperimentalValueArray) + + # Added so that subclasses of this are of the correct type + obj.__class__ = cls + + return obj + + def __str__(self): + value_errors = ", ".join(variable.print_value_error() for variable in self) + name = "{} = ".format(self.name) if self.name else "" + unit = " ({})".format(self.unit) if self.unit else "" + return "{}[ {} ]{}".format(name, value_errors, unit) + + def __setitem__(self, key, value): + if isinstance(value, Real): + self[key].value = value + else: + super().__setitem__( + key, dut.wrap_in_measurement(value, unit=self.unit, name=self.name)) + if self.name: + self[key].name = "{}_{}".format(self.name, key) + + def __pow__(self, power): + if isinstance(power, ARRAY_TYPES): + return super().__pow__(power) + return super().__pow__(dut.wrap_in_experimental_value(power)) + + def __rpow__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__rpow__(other) + return super().__rpow__(dut.wrap_in_experimental_value(other)) + + def __add__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__add__(other) + return super().__add__(dut.wrap_in_experimental_value(other)) + + def __radd__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__radd__(other) + return super().__radd__(dut.wrap_in_experimental_value(other)) + + def __sub__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__sub__(other) + return super().__sub__(dut.wrap_in_experimental_value(other)) + + def __rsub__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__rsub__(other) + return super().__rsub__(dut.wrap_in_experimental_value(other)) + + def __mul__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__mul__(other) + return super().__mul__(dut.wrap_in_experimental_value(other)) + + def __rmul__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__rmul__(other) + return super().__rmul__(dut.wrap_in_experimental_value(other)) + + def __truediv__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__truediv__(other) + return super().__truediv__(dut.wrap_in_experimental_value(other)) + + def __rtruediv__(self, other): + if isinstance(other, ARRAY_TYPES): + return super().__rtruediv__(other) + return super().__rtruediv__(dut.wrap_in_experimental_value(other)) + + def __array_finalize__(self, obj): + """wrap up array initialization""" + if obj is None or not (self.shape and isinstance(self[0], dt.ExperimentalValue)): + return # Skip if this is not a regular array of ExperimentalValue objects + if hasattr(obj, "name"): + name = getattr(obj, "name", "") + else: + name = getattr(self, "name", "") + # re-index the names of the measurements + for index, measurement in enumerate(self): + measurement.name = "{}_{}".format(name, index) if name else "" + + @property + def name(self): + """str: Name of this array of values + + A name can be given to this data set, and each measurement within this list will be + named in the form of "name_index". For example, if the name is specified as "length", + the items in this array will be named "length_0", "length_1", "length_2", ... + + """ + return re.sub(r"_[0-9]+$", "", self[0].name) + + @name.setter + def name(self, new_name: str): + if not isinstance(new_name, str): + raise TypeError("Cannot set name to \"{}\"!".format(type(new_name).__name__)) + for index, measurement in enumerate(self): + measurement.name = "{}_{}".format(new_name, index) + + @property + def unit(self): + """str: The unit of this array of values + + It is assumed that the set of data that constitutes one ExperimentalValueArray have + the same unit, which, when assigned, is given too all the items of the array. + + """ + return self[0].unit + + @unit.setter + def unit(self, unit_string: str): + if not isinstance(unit_string, str): + raise TypeError("Cannot set unit to \"{}\"!".format(type(unit_string).__name__)) + new_unit = utils.parse_unit_string(unit_string) if unit_string else {} + for data in self: + data._unit = new_unit + + @property + def values(self): + """np.ndarray: An array consisting of the center values of each item""" + return np.asarray(list(data.value for data in self)) + + @property + def errors(self): + """np.ndarray: An array consisting of the uncertainties of each item""" + return np.asarray(list(data.error for data in self)) + + def append(self, value) -> "ExperimentalValueArray": + """Adds a value to the end of this array and returns the new array + + Args: + value: The value to be appended to this array. This can be a real number, a pair + of value and error in a tuple, an ExperimentalValue instance, or an array + consisting of any of the above. + + Returns: + The new ExperimentalValueArray instance + + """ + value = dut.wrap_in_value_array(value, unit=self.unit, name=self.name) + result = np.append(self, value).view(ExperimentalValueArray) + for index, measurement in enumerate(result): + measurement.name = "{}_{}".format(self.name, index) + measurement.unit = self.unit + return result + + def insert(self, index: int, value) -> "ExperimentalValueArray": + """adds a value to a position in this array and returns the new array + + Args: + index (int): the position to insert the value + value: The value to be inserted into this array. This can be a real number, a + pair of value and error in a tuple, an ExperimentalValue instance, or an + array consisting of any of the above. + + Returns: + The new ExperimentalValueArray instance + + """ + value = dut.wrap_in_value_array(value, unit=self.unit, name=self.name) + result = np.insert(self, index, value).view(ExperimentalValueArray) + for idx, measurement in enumerate(result): + measurement.name = "{}_{}".format(self.name, idx) + measurement.unit = self.unit + return result + + def delete(self, index: int) -> "ExperimentalValueArray": + """deletes the value on the requested position and returns the new array + + Args: + index (int): the index of the value to be deleted + + Returns: + The new ExperimentalValueArray instance + + """ + result = np.delete(self, index).view(ExperimentalValueArray) + for idx, measurement in enumerate(result): + measurement.name = "{}_{}".format(self.name, idx) + return result + + def mean(self, **_) -> "dt.ExperimentalValue": # pylint:disable=arguments-differ + """The mean of the array""" + result = np.mean(self.values) + error = self.error_on_mean() + name = "mean of {}".format(self.name) if self.name else "" + return dt.MeasuredValue(float(result), error, unit=self.unit, name=name) + + def std(self, ddof=1, **_) -> float: # pylint:disable=arguments-differ + """The standard deviation of this array""" + return float(np.std(self.values, ddof=ddof)) + + def sum(self, **_) -> "dt.ExperimentalValue": # pylint:disable=arguments-differ + """The sum of the array""" + result = np.sum(self.values) + error = np.sqrt(np.sum(self.errors ** 2)) + return dt.MeasuredValue(float(result), float(error), unit=self.unit, name=self.name) + + def error_on_mean(self) -> float: + """The error on the mean of this array""" + return self.std() / m.sqrt(self.size) + + def error_weighted_mean(self) -> float: + """The error weighted mean of this array""" + if any(err == 0 for err in self.errors): + warnings.warn( + "One or more errors are 0, the error weighted mean cannot be calculated.") + return np.nan + weights = np.asarray(list(1 / (err ** 2) for err in self.errors)) + return float(np.sum(weights * self.values) / np.sum(weights)) + + def propagated_error(self) -> float: + """The propagated error from the error weighted mean calculation""" + if any(err == 0 for err in self.errors): + warnings.warn( + "One or more errors are 0, the propagated error cannot be calculated.") + return np.nan + weights = np.asarray(list(1 / (err ** 2) for err in self.errors)) + return 1 / np.sqrt(np.sum(weights)) + + @classmethod + def __wrap(cls, data, **kwargs): + """if an array of ExperimentalValue objects are passed in, simply wrap it""" + + error_array = kwargs.get("error_array", None) + + if error_array is not None: + for x, err in zip(data, error_array): + x.error = err # update the errors if specified + + name = kwargs.get("name", None) + unit = kwargs.get("unit", None) + + if name is not None: + for idx, x in enumerate(data): + x.name = "{}_{}".format(name, idx) + if unit is not None: + for x in data: + x.unit = unit + + obj = data.view(ExperimentalValueArray) + obj.__class__ = cls + return obj + + +class XYDataSet: + """A pair of ExperimentalValueArray objects + + QExPy is capable of multiple ways of data handling. One typical case in experimental data + analysis is for a pair of data sets, which is usually plotted or fitted with a curve. + + Args: + xdata (List|np.ndarray): an array of values for x-data + ydata (List|np.ndarray): an array of values for y-data + + Keyword Args: + xerr (Real|List): the uncertainty on x data + yerr (Real|List): the uncertainty on y data + xunit (str): the unit of the x data set + yunit (str): the unit of the y data set + xname (str): the name of the x data set + yname (str): the name of the y data set + + Examples: + + >>> import qexpy as q + + >>> a = q.XYDataSet(xdata=[0, 1, 2, 3, 4], xerr=0.5, xunit="m", xname="length", + >>> ydata=[3, 4, 5, 6, 7], yerr=[0.1,0.2,0.3,0.4,0.5], + >>> yunit="kg", yname="weight") + >>> a.xvalues + array([0, 1, 2, 3, 4]) + >>> a.xerr + array([0.5, 0.5, 0.5, 0.5, 0.5]) + >>> a.yerr + array([0.1, 0.2, 0.3, 0.4, 0.5]) + >>> a.xdata + ExperimentalValueArray([MeasuredValue(0.0 +/- 0.5), + MeasuredValue(1.0 +/- 0.5), + MeasuredValue(2.0 +/- 0.5), + MeasuredValue(3.0 +/- 0.5), + MeasuredValue(4.0 +/- 0.5)], dtype=object) + + """ + + def __init__(self, *args, **kwargs): + + xunit = kwargs.get("xunit", "") + yunit = kwargs.get("yunit", "") + xname = kwargs.get("xname", "") + yname = kwargs.get("yname", "") + + self._name = kwargs.get("name", "") + + xerr = kwargs.get("xerr", None) + yerr = kwargs.get("yerr", None) + + xdata = kwargs.pop("xdata", args[0] if len(args) >= 2 else None) + ydata = kwargs.pop("ydata", args[1] if len(args) >= 2 else None) + + xdata = XYDataSet.__wrap_data(xdata, xerr, name=xname, unit=xunit) + ydata = XYDataSet.__wrap_data(ydata, yerr, name=yname, unit=yunit) + + if len(xdata) != len(ydata): + raise ValueError("The length of xdata and ydata don't match!") + + self.xdata = xdata # type: ExperimentalValueArray + self.ydata = ydata # type: ExperimentalValueArray + + @property + def name(self): + """str: The name of this data set""" + return self._name if self._name else "XY Dataset" + + @name.setter + def name(self, new_name: str): + if not isinstance(new_name, str): + raise TypeError("Cannot set name to \"{}\"".format(type(new_name).__name__)) + self._name = new_name + + @property + def xvalues(self): + """np.ndarray: The values of the x data set""" + return self.xdata.values + + @property + def xerr(self): + """np.ndarray: The errors of the x data set""" + return self.xdata.errors + + @property + def yvalues(self): + """np.ndarray: The values of the y data set""" + return self.ydata.values + + @property + def yerr(self): + """np.ndarray: The errors of the x data set""" + return self.ydata.errors + + @property + def xname(self): + """str: Name of the xdata set""" + return self.xdata.name + + @xname.setter + def xname(self, name): + if not isinstance(name, str): + raise TypeError("Cannot set xname to \"{}\"".format(type(name).__name__)) + self.xdata.name = name + + @property + def xunit(self): + """str: Unit of the xdata set""" + return self.xdata.unit + + @xunit.setter + def xunit(self, unit): + if not isinstance(unit, str): + raise TypeError("Cannot set xunit to \"{}\"".format(type(unit).__name__)) + self.xdata.unit = unit + + @property + def yname(self): + """str: Name of the ydata set""" + return self.ydata.name + + @yname.setter + def yname(self, name): + if not isinstance(name, str): + raise TypeError("Cannot set yname to \"{}\"".format(type(name).__name__)) + self.ydata.name = name + + @property + def yunit(self): + """str: Unit of the ydata set""" + return self.ydata.unit + + @yunit.setter + def yunit(self, unit): + if not isinstance(unit, str): + raise TypeError("Cannot set yunit to \"{}\"".format(type(unit).__name__)) + self.ydata.unit = unit + + def fit(self, model, **kwargs): + """Fits the current dataset to a model + + See Also: + The fit function in the fitting module of QExPy + + """ + import qexpy.fitting as fitting # pylint: disable=cyclic-import + return fitting.fit(self, model, **kwargs) + + @staticmethod + def __wrap_data(data, error, unit, name) -> ExperimentalValueArray: + """Wraps the data set into ExperimentalValueArray objects""" + + if isinstance(data, ExperimentalValueArray): + if name: + data.name = name + if unit: + data.unit = unit + if error is not None: + error_array = _get_error_array_helper(data, error, None) + for x, e in zip(data, error_array): + x.error = e + return data + if isinstance(data, ARRAY_TYPES): + return ExperimentalValueArray(data, error, unit=unit, name=name) + + raise IllegalArgumentError("Cannot create XYDataSet with the given arguments.") + + +def _get_error_array_helper(data, error, rel_error): + """Helper method that produces an error array for an ExperimentalValueArray""" + + if error is None and rel_error is None: + error_array = [0.0] * len(data) + elif isinstance(error, Real): + error_array = [float(error)] * len(data) + elif isinstance(error, ARRAY_TYPES) and all(isinstance(err, Real) for err in error): + if len(error) != len(data): + raise ValueError("The length of the error data arrays don't match.") + error_array = np.asarray(error) + elif isinstance(rel_error, Real): + error_array = float(rel_error) * abs(data) + elif isinstance(rel_error, ARRAY_TYPES) and all(isinstance(e, Real) for e in rel_error): + if len(rel_error) != len(data): + raise ValueError("The length of the relative error and data arrays don't match.") + error_array = rel_error * abs(data) + else: + raise TypeError("The error or relative error provided is invalid!") + + if any(err < 0 for err in error_array): + raise ValueError("The uncertainty of any measurement cannot be negative!") + + return error_array diff --git a/qexpy/data/operations.py b/qexpy/data/operations.py new file mode 100644 index 0000000..8a64d66 --- /dev/null +++ b/qexpy/data/operations.py @@ -0,0 +1,566 @@ +"""Defines arithmetic and math operations with ExperimentalValue objects""" + +import itertools +import warnings +import numpy as np + +from abc import ABC, abstractmethod +from typing import Dict, Callable, List, Set, Generator +from numbers import Real +from collections import OrderedDict + +from qexpy.utils import UndefinedOperationError, UndefinedActionError +from uuid import UUID + +import qexpy.utils as utils +import qexpy.settings as sts +import qexpy.settings.literals as lit + +from . import data as dt # pylint: disable=cyclic-import +from . import datasets as dts # pylint: disable=cyclic-import +from . import utils as dut + +pi, e = np.pi, np.e + +ARRAY_TYPES = np.ndarray, list + + +class Evaluator(ABC): + """Used to calculate the value and uncertainty of a derived value""" + + @abstractmethod + def evaluate(self, formula: "dt.Formula") -> "dt.ValueWithError": + """Evaluates a formula with the proper error method""" + raise NotImplementedError + + @abstractmethod + def clear(self): + """Clears the buffered results in this evaluator""" + raise NotImplementedError + + +class DerivativeEvaluator(Evaluator): + """The calculator that uses the derivative method to propagate errors""" + + def __init__(self): + self.result = () # type: dt.ValueWithError + self.measurements = [] + self.error_contributions = [] + + def evaluate(self, formula: "dt.Formula") -> "dt.ValueWithError": + if not self.result: + self.result = self.__evaluate(formula) + return self.result + + def clear(self): + self.result = () + self.measurements = [] + self.error_contributions = [] + + def __evaluate(self, formula: "dt.Formula"): + """Executes an operation with propagated results using the derivative method + + This is also known as the method of adding quadratures. It also takes into account + the covariance between measurements if they are specified. It is only valid when the + relative uncertainties in the quantities are small (less than ~10%) + + """ + + # Execute the operation + result_value = _evaluate_formula(formula) + + # Find measurements that this formula is derived from + source_meas_ids = _find_source_measurement_ids(formula) # type: Set[UUID] + sources = list(dt.get_variable_by_id(_id) for _id in source_meas_ids) + + # record source measurements + self.measurements = sources + + # Find the quadrature terms + quads = list(map(lambda x: (x.error * differentiate(formula, x)) ** 2, sources)) + + # Handle covariance between measurements + covariance_terms = DerivativeEvaluator.__find_cov_terms(formula, sources) + + # Calculate the result + result_sums = sum(quads) + sum(covariance_terms) + if result_sums < 0: # pragma: no cover + raise UndefinedActionError( + "The error propagated for the given operation is negative. This is likely " + "to be incorrect! Check your values, maybe you have unphysical covariance.") + + # record error contributions + if result_sums > 0: + self.error_contributions = np.array([quad / result_sums for quad in quads]) + else: + self.error_contributions = np.zeros(len(quads)) + + result_error = np.sqrt(result_sums) + + return dt.ValueWithError(result_value, result_error) + + @staticmethod + def __find_cov_terms(_formula: "dt.Formula", _measurements: List) -> Generator: + """Finds the contributing covariance terms for the quadrature method""" + for var1, var2 in itertools.combinations(_measurements, 2): + corr = dt.get_correlation(var1, var2) + # Re-calculate the covariance between two measurements, because in the case of + # repeated measurements, sometimes the covariance is calculated from the raw + # measurement array, which is closely coupled with the standard deviation of the + # raw samples. This is misleading because with repeated measurements, we use the + # error on the mean, not the standard deviation of the raw measurements, as the + # uncertainty on the quantity. Essentially, with repeatedly measured values, we + # are ignoring the array of raw measurements, and treating its value and error + # as the mean and standard deviation just like we would with any other single + # measurements. This would make the most physical sense. + cov = corr * var1.error * var2.error + if cov != 0: + yield 2 * cov * differentiate(_formula, var1) * differentiate(_formula, var2) + + +class MonteCarloEvaluator(Evaluator): + """The calculator that uses the Monte Carlo method to propagate errors""" + + def __init__(self): + self.raw_samples = np.empty(0) + self.values = {} + self.settings = dut.MonteCarloSettings(self) + + @property + def samples(self): + """np.ndarray: the raw samples of this simulation""" + if not self.settings.xrange: + return self.raw_samples + xrange = self.settings.xrange + return np.ma.masked_outside(self.raw_samples, xrange[0], xrange[1], copy=False) + + def evaluate(self, formula: "dt.Formula") -> "dt.ValueWithError": + + self.regenerate_samples(formula) + + strategy = self.settings.strategy + + if strategy == lit.MC_CUSTOM not in self.values: + strategy = lit.MC_MEAN_AND_STD + self.settings.use_mean_and_std() + + if strategy == lit.MC_MEAN_AND_STD not in self.values: + result = dt.ValueWithError(np.mean(self.samples), np.std(self.samples, ddof=1)) + self.values[strategy] = result + + if strategy == lit.MC_MODE_AND_CONFIDENCE not in self.values: + n, bins = np.histogram(self.samples, bins=100) + value, error = utils.find_mode_and_uncertainty(n, bins, self.settings.confidence) + self.values[strategy] = dt.ValueWithError(value, error) + + return self.values[strategy] + + def regenerate_samples(self, formula): + """generates raw samples if none is present""" + if not self.raw_samples.size: + self.raw_samples = self.__compute_samples(formula) + + def clear(self): + self.raw_samples = np.empty(0) + self.values.clear() + + def show_histogram(self, bins=100, **kwargs): # pragma: no cover + """Shows the distribution of the Monte Carlo simulated samples""" + + samples = self.samples + if "range" in kwargs: + xrange = kwargs.pop('range') + samples = np.ma.masked_outside(samples, xrange[0], xrange[1], copy=False) + + import matplotlib.pyplot as plt + n, edges, _ = plt.hist(samples, bins=bins, **kwargs) + + if self.settings.strategy == lit.MC_MODE_AND_CONFIDENCE: + value, error = utils.find_mode_and_uncertainty(n, edges, self.settings.confidence) + value_label = "mode = {:.2f}".format(value) + plt.title("MC with {:.1f}% confidence".format(self.settings.confidence * 100)) + else: + value, error = np.mean(samples), np.std(samples, ddof=1) + value_label = "mean = {:.2f}".format(value) + plt.title("MC highlighting mean and standard deviation") + + plt.plot([value, value], [0, max(n)], "r", label=value_label) + + low, high = value - error, value + error + plt.plot([low, low], [0, max(n)], "r--", label="low bound = {:.2f}".format(low)) + plt.plot([high, high], [0, max(n)], "r--", label="high bound = {:.2f}".format(high)) + + plt.legend() + plt.show() + + def __compute_samples(self, formula: "dt.Formula") -> np.ndarray: + """Executes an operation with propagated results using the Monte-Carlo method + + For each original measurement that the formula is derived from, generate a normally + distributed random data set with the mean and standard deviation of that measurement. + Evaluate the formula with each sample, and return the final sample set + + """ + + sample_size = self.settings.sample_size + + # Find measurements that this formula is derived from + source_meas_ids = _find_source_measurement_ids(formula) # type: Set[UUID] + source_measurements = list(dt.get_variable_by_id(_id) for _id in source_meas_ids) + + # Each source measurement is assigned a set of normally distributed values with the + # mean and standard deviation of the measurement's center value and uncertainty. + data_sets = {} # type: Dict[UUID, np.ndarray] + + # Generate a sample matrix with 0 mean and unit variance, correlated if applicable + sample_set = dut.generate_offset_matrix(source_measurements, sample_size) + for _id, sample in zip(source_meas_ids, sample_set): + # Apply each sample to the desired mean and standard deviation of the measurement + data_sets[_id] = _generate_random_data_set(_id, sample) + + result_data_set = _evaluate_formula(formula, data_sets) + + # Check the quality of the result data + assert isinstance(result_data_set, np.ndarray) + + # First remove undefined values + result_data_set = result_data_set[np.isfinite(result_data_set)] + + if len(result_data_set) / sts.get_settings().monte_carlo_sample_size < 0.9: + # If over 10% of the results calculated are invalid + warnings.warn( + "Over 10 percent of the random samples generated for the Monte Carlo " + "simulation falls outside the domain on which the function is defined. " + "Check the error or the standard deviation of the measurements passed in, " + "it is possible that the domain of this function is too narrow compared to " + "the standard deviation of the measurements.") + + # return the result data set + return result_data_set + + +def differentiate(formula: "dt.Formula", variable: "dt.ExperimentalValue") -> float: + """Find the derivative of a formula with respect to a variable""" + return __differentiator(formula.operator)(variable, *formula.operands) + + +def propagate_units(formula: "dt.Formula") -> Dict[str, dict]: + """Calculate the correct units for the formula""" + + operator = formula.operator + operands = formula.operands + + # the power operator is different, treat separately + if operator == lit.POW and isinstance(operands[1], dt.Constant): + power = operands[1].value + return OrderedDict([ + (unit, count * power) for unit, count in operands[0]._unit.items()]) + + if all(operand._unit or isinstance(operand, dt.Constant) for operand in operands): + return utils.operate_with_units(operator, *(operand._unit for operand in operands)) + + # If there are non-constant values with unknown units, the units of the final result + # should also stay unknown. This is to avoid getting non-physical units. + return {} + + +@utils.vectorize +def sqrt(x): + """square root""" + return _execute(lit.SQRT, x) + + +@utils.vectorize +def exp(x): + """e raised to the power of x""" + return _execute(lit.EXP, x) + + +@utils.vectorize +def sin(x): + """sine of x in rad""" + return _execute(lit.SIN, x) + + +@utils.vectorize +def sind(x): + """sine of x in degrees""" + return sin(x / 180 * np.pi) + + +@utils.vectorize +def cos(x): + """cosine of x in rad""" + return _execute(lit.COS, x) + + +@utils.vectorize +def cosd(x): + """cosine of x in degrees""" + return cos(x / 180 * np.pi) + + +@utils.vectorize +def tan(x): + """tan of x in rad""" + return _execute(lit.TAN, x) + + +@utils.vectorize +def tand(x): + """tan of x in degrees""" + return tan(x / 180 * np.pi) + + +@utils.vectorize +def sec(x): + """sec of x in rad""" + return _execute(lit.SEC, x) + + +@utils.vectorize +def secd(x): + """sec of x in degrees""" + return sec(x / 180 * np.pi) + + +@utils.vectorize +def csc(x): + """csc of x in rad""" + return _execute(lit.CSC, x) + + +@utils.vectorize +def cscd(x): + """csc of x in degrees""" + return csc(x / 180 * np.pi) + + +@utils.vectorize +def cot(x): + """cot of x in rad""" + return _execute(lit.COT, x) + + +@utils.vectorize +def cotd(x): + """cot of x in degrees""" + return cot(x / 180 * np.pi) + + +@utils.vectorize +def asin(x): + """arcsine of x""" + return _execute(lit.ASIN, x) + + +@utils.vectorize +def acos(x): + """arccos of x""" + return _execute(lit.ACOS, x) + + +@utils.vectorize +def atan(x): + """arctan of x""" + return _execute(lit.ATAN, x) + + +@utils.vectorize +def log(*args): + """log with a base and power + + If two arguments are provided, returns the log of the second with the first on base + If only one argument is provided, returns the natural log of that argument + + """ + if len(args) == 2: + return _execute(lit.LOG, args[0], args[1]) + if len(args) == 1: + return _execute(lit.LN, args[0]) + raise TypeError("Invalid number of arguments for log().") + + +@utils.vectorize +def log10(x): + """log with base 10 for a value""" + return _execute(lit.LOG10, x) + + +def mean(array): + """The mean of an array""" + if isinstance(array, dts.ExperimentalValueArray): + return array.mean() + return np.mean(array) + + +def sum_(array): # avoid built-in function "sum" + """The sum of an array""" + if isinstance(array, dts.ExperimentalValueArray): + return array.sum() + return np.sum(array) + + +def std(array, ddof=1): + """The standard deviation of an array""" + if isinstance(array, dts.ExperimentalValueArray): + return array.std() + return np.std(array, ddof=ddof) + + +def _evaluate_formula(formula, samples: Dict[UUID, np.ndarray] = None): + """Evaluates a Formula with original values of measurements or sample values + + This function evaluates the formula with the original measurements by default. If a set + of samples are passed in, the formula will be evaluated with the sample values. + + Args: + formula (Union[dt.Formula, dt.ExperimentalValue]): the formula to be evaluated + samples (Dict): an np.ndarray of samples assigned to each source measurements's ID. + + """ + np.seterr(all="ignore") # ignore runtime warnings + if samples and isinstance(formula, dt.MeasuredValue) and formula._id in samples: + # Use the value in the sample instead of its original value if specified + return samples[formula._id] + if isinstance(formula, dt.DerivedValue): + return _evaluate_formula(formula._formula, samples) + if isinstance(formula, (dt.MeasuredValue, dt.Constant)): + return formula.value + + operands = (_evaluate_formula(variable, samples) for variable in formula.operands) + return OPERATIONS[formula.operator](*operands) + + +def _find_source_measurement_ids(formula) -> Set[UUID]: + """Find IDs of all measurements that the given formula is derived from""" + + if isinstance(formula, dt.Formula): + return set.union( + *(_find_source_measurement_ids(operand) for operand in formula.operands)) + if isinstance(formula, dt.MeasuredValue): + return {formula._id} + if isinstance(formula, dt.DerivedValue): + return _find_source_measurement_ids(formula._formula) + return set() + + +def _generate_random_data_set(measurement_id: UUID, offsets: np.ndarray): + """Generate random simulated measurements for each MeasuredValue + + This method simply applies the desired mean and standard deviation to the random + sample set with 0 mean and unit variance + + """ + + measurement = dt.get_variable_by_id(measurement_id) + + # The error is used here instead of std even in the case of repeatedly measured values, + # because the value used is the mean of all measurements, not the value of any single + # measurement, thus it is more accurate. + _std = measurement.error + + center_value = measurement.value + return offsets * _std + center_value + + +def _execute(operator: str, *operands) -> "dt.DerivedValue": + """Execute a math function on numbers or ExperimentalValue instances""" + + # For functions such as sqrt, sin, cos, ..., if a simple real number is passed in, a + # simple real number should be returned instead of a Constant object. + if all(isinstance(x, Real) for x in operands): + return OPERATIONS[operator](*operands) + + try: + # wrap all operands in ExperimentalValue objects + values = list(dut.wrap_in_experimental_value(x) for x in operands) + except TypeError: + raise UndefinedOperationError(operator, operands, "real numbers") + + # Construct a DerivedValue object with the operator and operands + return dt.DerivedValue(dt.Formula(operator, list(values))) + + +def __differentiator(operator: str) -> Callable: + """Gets the derivative formula for an operator + + The differentiator is a lambda function that calculates the derivative of an expression + with respect to a target value. The first argument is a reference to the target value, + and the rest of the arguments are the operands for this operation. + + Usage: + differentiator("MUL")(x, a, b) would return the derivative of the expression "a * b" + with respect to the value x + + Args: + operator (str): the operator of this expression + + """ + return DIFFERENTIATORS[operator] + + +def __pow_differentiator(o, x, a): + """The differentiator for a power function is a little more complicated + + This is added because the derivative of f(x)^g(x) includes a term that involves taking + the log of f(x). This should not be necessary if g(x) is a constant. In some cases where + f(x) is smaller than 0, the log would return nan, which makes the entire expression nan. + This should not need to happen if d/dx of g(x) is 0, which should eliminate the nan term. + Since in Python nan * 0 returns nan instead of 0, this helper function is written so that + this can happen. + + """ + leading = x.value ** (a.value - 1) + first = a.value * x.derivative(o) + second = x.value * np.log(x.value) * a.derivative(o) if a.derivative(o) != 0 else 0 + return leading * (first + second) + + +OPERATIONS = { + lit.NEG: lambda x: -x, + lit.ADD: lambda a, b: a + b, + lit.SUB: lambda a, b: a - b, + lit.MUL: lambda a, b: a * b, + lit.DIV: lambda a, b: a / b, + lit.SQRT: np.sqrt, + lit.EXP: np.exp, + lit.SIN: np.sin, + lit.COS: np.cos, + lit.TAN: np.tan, + lit.ASIN: np.arcsin, + lit.ACOS: np.arccos, + lit.ATAN: np.arctan, + lit.SEC: lambda x: 1 / np.cos(x), + lit.CSC: lambda x: 1 / np.sin(x), + lit.COT: lambda x: 1 / np.tan(x), + lit.POW: lambda x, a: x ** a, + lit.LOG: lambda base, x: np.log(x) / np.log(base), + lit.LOG10: np.log10, + lit.LN: np.log +} + +DIFFERENTIATORS = { + lit.NEG: lambda o, x: -x.derivative(o), + lit.ADD: lambda o, a, b: a.derivative(o) + b.derivative(o), + lit.SUB: lambda o, a, b: a.derivative(o) - b.derivative(o), + lit.MUL: lambda o, a, b: a.derivative(o) * b.value + b.derivative(o) * a.value, + lit.DIV: lambda o, a, b: (b.value * a.derivative(o) - a.value * b.derivative(o)) / ( + b.value ** 2), + lit.SQRT: lambda o, x: 1 / 2 / np.sqrt(x.value) * x.derivative(o), + lit.EXP: lambda o, x: np.exp(x.value) * x.derivative(o), + lit.SIN: lambda o, x: np.cos(x.value) * x.derivative(o), + lit.COS: lambda o, x: -np.sin(x.value) * x.derivative(o), + lit.TAN: lambda o, x: 1 / (np.cos(x.value)) ** 2 * x.derivative(o), + lit.ASIN: lambda o, x: 1 / np.sqrt(1 - x.value ** 2) * x.derivative(o), + lit.ACOS: lambda o, x: -1 / np.sqrt(1 - x.value ** 2) * x.derivative(o), + lit.ATAN: lambda o, x: 1 / (1 + x.value ** 2) * x.derivative(o), + lit.SEC: lambda o, x: np.tan(x.value) / np.cos(x.value) * x.derivative(o), + lit.CSC: lambda o, x: -1 / (np.tan(x.value) * np.sin(x.value)) * x.derivative(o), + lit.COT: lambda o, x: -1 / (np.sin(x.value) ** 2) * x.derivative(o), + lit.POW: __pow_differentiator, # see function definition and comment above + lit.LOG: lambda o, b, x: ((np.log(b.value) * x.derivative(o) / x.value) - ( + b.derivative(o) * np.log(x.value) / b.value)) / (np.log(b.value) ** 2), + lit.LOG10: lambda o, x: x.derivative(o) / (np.log(10) * x.value), + lit.LN: lambda o, x: x.derivative(o) / x.value +} diff --git a/qexpy/data/utils.py b/qexpy/data/utils.py new file mode 100644 index 0000000..9029f67 --- /dev/null +++ b/qexpy/data/utils.py @@ -0,0 +1,234 @@ +"""Utility methods for the data module""" +import warnings + +import numpy as np + +from numbers import Real + +from . import data as dt, datasets as dts # pylint: disable=cyclic-import + +import qexpy.settings.literals as lit +import qexpy.settings as sts +import qexpy.utils as utils + +ARRAY_TYPES = np.ndarray, list + + +class MonteCarloSettings: + """The object for customizing the Monte Carlo error propagation process""" + + def __init__(self, evaluator): + self.__evaluator = evaluator + self.__settings = { + lit.MONTE_CARLO_SAMPLE_SIZE: 0, + lit.MONTE_CARLO_STRATEGY: lit.MC_MEAN_AND_STD, + lit.MONTE_CARLO_CONFIDENCE: 0.68, + lit.XRANGE: () + } + + @property + def sample_size(self): + """int: The Monte Carlo sample size""" + default_size = sts.get_settings().monte_carlo_sample_size + set_size = self.__settings[lit.MONTE_CARLO_SAMPLE_SIZE] + return set_size if set_size else default_size + + @sample_size.setter + def sample_size(self, new_size: int): + if not isinstance(new_size, int) or new_size < 0: + raise ValueError("The sample size has to be a positive integer") + self.__settings[lit.MONTE_CARLO_SAMPLE_SIZE] = new_size + self.__evaluator.clear() + + def reset_sample_size(self): + """reset the sample size to default""" + self.__settings[lit.MONTE_CARLO_SAMPLE_SIZE] = 0 + + @property + def confidence(self): + """float: The confidence level for choosing the mode of a Monte Carlo distribution""" + return self.__settings[lit.MONTE_CARLO_CONFIDENCE] + + @confidence.setter + def confidence(self, new_level: float): + if not isinstance(new_level, Real): + raise TypeError("The MC confidence level has to be a number") + if new_level > 1 or new_level < 0: + raise ValueError("The MC confidence level has to be a number between 0 and 1") + self.__settings[lit.MONTE_CARLO_CONFIDENCE] = new_level + if lit.MC_MODE_AND_CONFIDENCE in self.__evaluator.values: + self.__evaluator.values.pop(lit.MC_MODE_AND_CONFIDENCE) + + @property + def xrange(self): + """tuple: The x-range of the simulation + + This is really the y-range, which means it's the range of the y-values to show, + but also this is the x-range of the histogram. + + """ + return self.__settings[lit.XRANGE] + + def set_xrange(self, *args): + """set the range for the monte carlo simulation""" + + if not args: + self.__settings[lit.XRANGE] = () + else: + new_range = (args[0], args[1]) if len(args) > 1 else args + utils.validate_xrange(new_range) + self.__settings[lit.XRANGE] = new_range + + self.__evaluator.values.clear() + + def use_mode_with_confidence(self, confidence=None): + """Use the mode of the distribution with a confidence coverage for this value""" + self.__settings[lit.MONTE_CARLO_STRATEGY] = lit.MC_MODE_AND_CONFIDENCE + if confidence: + self.confidence = confidence + + def use_mean_and_std(self): + """Use the mean and std of the distribution for this value""" + self.__settings[lit.MONTE_CARLO_STRATEGY] = lit.MC_MEAN_AND_STD + + def use_custom_value_and_error(self, value, error): + """Manually set the value and uncertainty for this quantity + + Sometimes when the distribution is not typical, and you wish to see for yourself what + the best approach is to choose the center value and uncertainty for this quantity, + use this method to manually set these values. + + """ + self.__settings[lit.MONTE_CARLO_STRATEGY] = lit.MC_CUSTOM + if not isinstance(value, Real): + raise TypeError("Cannot assign a {} to the value!".format(type(value).__name__)) + if not isinstance(error, Real): + raise TypeError("Cannot assign a {} to the error!".format(type(error).__name__)) + if error < 0: + raise ValueError("The error must be a positive real number!") + self.__evaluator.values[self.strategy] = dt.ValueWithError(value, error) + + @property + def strategy(self): + """str: the strategy used to extract value and error from a histogram""" + return self.__settings[lit.MONTE_CARLO_STRATEGY] + + def show_histogram(self, bins=100, **kwargs): # pragma: no cover + """Shows the distribution of the Monte Carlo simulated samples""" + self.__evaluator.show_histogram(bins, **kwargs) + + def samples(self): + """The raw samples generated in the Monte Carlo simulation + + Sometimes when the distribution is not typical, you might wish to do your own analysis + with the raw samples generated in the Monte Carlo simulation. This method allows you + to access a copy of the raw data. + + """ + return self.__evaluator.raw_samples.copy() + + +def generate_offset_matrix(measurements, sample_size): + """Generates offsets from mean for each measurement + + Each sample set generated has 0 mean and unit variance. Then covariance is applied to the + set of samples using the Chelosky algorithm. + + Args: + measurements (List[dt.ExperimentalValue]): a set of measurements to simulate + sample_size (int): the size of the samples + + Returns: + A N row times M column matrix where N is the number of measurements to simulate and + M is the requested sample size for Monte Carlo simulations. Each row of this matrix + is an array of random values with 0 mean and unit variance + + """ + + offset_matrix = np.vstack( + [np.random.normal(0, 1, sample_size) for _ in measurements]) + offset_matrix = correlate_samples(measurements, offset_matrix) + return offset_matrix + + +def correlate_samples(variables, sample_vector): + """Uses the Chelosky algorithm to add correlation to random samples + + This method finds the Chelosky decomposition of the correlation matrix of the given list + of measurements, then applies it to the sample vector. + + The sample vector is a list of random samples, each entry correspond to each variable + passed in. Each random sample, corresponding to each entry, is an array of random numbers + with 0 mean and unit variance. + + Args: + variables (List[dt.ExperimentalValue]): the source measurements + sample_vector (np.ndarray): the list of random samples to apply correlation to + + Returns: + The same list sample vector with correlation applied + + """ + + corr_matrix = np.array( + [[dt.get_correlation(row, col) for col in variables] for row in variables]) + if np.count_nonzero(corr_matrix - np.diag(np.diagonal(corr_matrix))) == 0: + return sample_vector # if no correlations are present + + try: + chelosky_decomposition = np.linalg.cholesky(corr_matrix) + result_vector = np.dot(chelosky_decomposition, sample_vector) + return result_vector + except np.linalg.linalg.LinAlgError: # pragma: no cover + warnings.warn( + "Fail to generate a physical correlation matrix for the values provided, using " + "uncorrelated samples instead. Please check that the covariance or correlation " + "factors assigned to the measurements are physical.") + return sample_vector + + +def wrap_in_experimental_value(operand) -> "dt.ExperimentalValue": + """Wraps a variable in an ExperimentalValue object + + Wraps single numbers in a Constant, number pairs in a MeasuredValue. If the argument + is already an ExperimentalValue instance, return directly. If the + + """ + + if isinstance(operand, Real): + return dt.Constant(operand) + if isinstance(operand, dt.ExperimentalValue): + return operand + if isinstance(operand, tuple) and len(operand) == 2: + return dt.MeasuredValue(operand[0], operand[1]) + raise TypeError( + "Cannot parse a {} into an ExperimentalValue".format(type(operand).__name__)) + + +def wrap_in_measurement(value, **kwargs) -> "dt.ExperimentalValue": + """Wraps a value in a Measurement object""" + + if isinstance(value, Real): + return dt.MeasuredValue(value, 0, **kwargs) + if isinstance(value, tuple) and len(value) == 2: + return dt.MeasuredValue(*value, **kwargs) + if isinstance(value, dt.ExperimentalValue): + value.name = kwargs.get("name", "") + value.unit = kwargs.get("unit", "") + return value + + raise TypeError( + "Elements of a MeasurementArray must be convertible to an ExperimentalValue") + + +def wrap_in_value_array(operand, **kwargs) -> np.ndarray: + """Wraps input in an ExperimentalValueArray""" + + # wrap array times in numpy arrays + if isinstance(operand, dts.ExperimentalValueArray): + return operand + if isinstance(operand, ARRAY_TYPES): + return np.asarray([wrap_in_measurement(value, **kwargs) for value in operand]) + + # wrap single value times in array + return np.asarray([wrap_in_measurement(operand, **kwargs)]) diff --git a/qexpy/defaults.py b/qexpy/defaults.py deleted file mode 100644 index fe3a776..0000000 --- a/qexpy/defaults.py +++ /dev/null @@ -1,26 +0,0 @@ -#Default parameters - -import matplotlib.pyplot as plt -#Matplotlib needs to know the dpi to convert between -#actual size and pixels -screen_dpi = plt.gcf().get_dpi() -if screen_dpi == 0: - screen_dpi = 100 -plt.close() - -import bokeh.palettes as bpal - -settings = { - "plot_fig_x_px": 600, #x dimension of figures in pixels - "plot_fig_y_px": 400, #y dimension of figures in pixels - "plot_fig_title_ftsize": 11, #font size for figure titles - "plot_fig_xtitle_ftsize": 11, #font size for x axis label - "plot_fig_ytitle_ftsize": 11, #font size for y axis label - "plot_fig_fitres_ftsize": 11, #font size for fit results - "plot_fig_leg_ftsize": 11, #font size for legends - "plot_screen_dpi": screen_dpi, #default dpi used by matplotlib - "plot_color_palette": bpal.Set1_9+bpal.Set2_8, #color palette to choose colors from - "plot_fcn_npoints": 100, #number of points to use for plotting functions and error bands - "fit_max_fcn_calls": -1, #max number of function calls when fitting before giving up (-1 default) - } - diff --git a/qexpy/error.py b/qexpy/error.py deleted file mode 100644 index 86da1b4..0000000 --- a/qexpy/error.py +++ /dev/null @@ -1,3145 +0,0 @@ -import numpy as np -#used for the histograms, will remove when move bokeh histo to -#to use Plot(): -import bokeh.plotting as bp -import bokeh.io as bi -#used for array and number types: -import qexpy.utils as qu -#used to check plot_engine: -import qexpy as q -import re -import pandas as pd - - -class ExperimentalValue: - '''Root class of objects which contains a mean and standard deviation. - From this class, objects with properties pertaining to their use or - formulation can be instanced. (ie. the result of an operation of - measured values, called Function and Measurement respectivly) - ''' - error_method = "Derivative" # Default error propogation method - print_style = "Default" # Default printing style - mc_trial_number = 10000 # number of trial in Monte Carlo simulation - minmax_n = 100 # grid size in MinMax calculation - - figs = None - figs_on_uncertainty = False - register = {} - formula_register = {} - id_register = {} - - # Defining common types under single arrayclear - from numpy import int64, float64, ndarray, int32, float32 - CONSTANT = qu.number_types #(int, float, int64, float64, int32, float32) - ARRAY = qu.array_types #(list, tuple, ndarray) - - def __init__(self, *args, name=None): - '''Creates a variable that contains a mean, standard deviation, - and name for inputted data. - ''' - self.der = [0, 0] - self.MinMax = [0, 0] - self.MC = [0, 0] - - self.info = { - 'ID': '', 'Formula': '', 'Method': '', 'Data': [],\ - 'Function': { - 'operation': (), 'variables': ()}, } - - if len(args) ==1: - if isinstance(args[0], qu.array_types): - data = np.ndarray(len(args[0])) - self.info['Data'] = data - for index in range(len(args[0])): - data[index] = args[0][index] - self.mean = data.mean() - self.std = data.std(ddof=1) - self.error_on_mean = 0 if data.size==0 else self.std/np.sqrt(data.size) - else: - raise TypeError('''Input must be either a single array of values, - or the central value and uncertainty in one measurement''') - elif len(args)==2: - if isinstance(args[0], qu.number_types) and isinstance(args[1], qu.number_types): - self.mean = float(args[0]) - data = np.ndarray(1) - error_data = np.ndarray(1) - data[0] = self.mean - self.info['Data'] = data - self.std = float(args[1]) - elif isinstance(args[0], qu.array_types) and isinstance(args[1], qu.array_types): - raise TypeError('''Input must be either a single array of values, - or the central value and uncertainty in one measurement. - - The feature of passing a list of measurements and their corresponding - uncertanties is now deprecated. Please use a MeasurementArray insted. - More info: - https://github.com/Queens-Physics/qexpy/blob/master/examples/jupyter/1_Intro_to_Error_Propagation.ipynb''') - else: - raise TypeError('''Input must be either a single array of values, - or the central value and uncertainty in one measurement''') - else: - raise TypeError('''Input must be either a single array of values, - or the central value and uncertainty in one measurement''') - - ExperimentalValue.id_register[id(self)] = self - self.print_style = ExperimentalValue.print_style - - if name is not None: - self.user_name = True - else: - self.user_name = False - - self._units = {} - self.MC_list = None - '''The list of values generated by the Monte Carlo simulation.''' - -############################################################################### -# Methods for printing or returning Measurement paramters -############################################################################### - - def __str__(self): - '''Method called when printing measurement objects.''' - if self.user_name: - string = self.name+' = ' - else: - string = '' - - if ExperimentalValue.print_style == "Latex": - string += _tex_print(self) - elif ExperimentalValue.print_style == "Default": - string += _def_print(self) - elif ExperimentalValue.print_style == "Scientific": - string += _sci_print(self) - - unit_string = self.get_units_str() - if unit_string != '': - if ExperimentalValue.print_style == "Latex": - string += '\,'+unit_string - else: - string += ' ['+unit_string+']' - - return string - - def print_mc_error(self): - '''Prints the result of a Monte Carlo error propagation. - - The purpose of this method is to easily compare the results of a - Monte Carlo propagation with whatever method is chosen. - - .. code-block:: python - - q.set_sigfigs(4) - x = q.Measurement(4, 0.5) - y = q.Measurement(9.8, 0.1) - z = x**2*y - - z.print_mc_error() - - .. nboutput:: ipython3 - - 159.90 +/- 39.04 - ''' - if self.print_style == "Latex": - string = _tex_print(self, method="Monte Carlo") - elif self.print_style == "Default": - string = _def_print(self, method="Monte Carlo") - elif self.print_style == "Scientific": - string = _sci_print(self, method="Monte Carlo") - print(string) - - def print_min_max_error(self): - '''Prints the result of a Min-Max method error propagation. - - The purpose of this method is to easily compare the results of a - Min-Max propagation with whatever method is chosen to confirm that - the Min-Max is the upper bound of the error. - - .. code-block:: python - - q.set_sigfigs(4) - x = q.Measurement(4, 0.5) - y = q.Measurement(9.8, 0.1) - z = x**2*y - - z.print_min_max_error() - - .. nboutput:: ipython3 - - 159.65 +/- 40.82 - ''' - if self.print_style == "Latex": - string = _tex_print(self, method="Min Max") - elif self.print_style == "Default": - string = _def_print(self, method="Min Max") - elif self.print_style == "Scientific": - string = _sci_print(self, method="Min Max") - print(string) - - def print_deriv_error(self): - '''Prints the result of a derivative method error propagation. - - The purpose of this method is to easily compare the results of a - derivative propagation with whatever method is chosen. - - .. code-block:: python - - q.set_sigfigs(4) - x = q.Measurement(4, 0.5) - y = q.Measurement(9.8, 0.1) - z = x**2*y - - z.print_deriv_error() - - .. nboutput:: ipython3 - - 156.80 +/- 39.23 - ''' - if self.print_style == "Latex": - string = _tex_print(self, method="Derivative") - elif self.print_style == "Default": - string = _def_print(self, method="Derivative") - elif self.print_style == "Scientific": - string = _sci_print(self, method="Derivative") - print(string) - - def get_derivative(self, variable=None): - '''Function to find the derivative of a measurement or measurement like - object. By default, derivative is with respect to itself, which will - always yeild 1. Operator acts by acessing the self.derivative - dictionary and returning the value stored there under the specific - variable inputted (ie. differentiating with respect to variable = ???) - - :param variable: The variable that the derivative is to be taken with respect to. - :type variable: Measurement - - :returns: the numerical value of the derivative with respect to the variable - :rtype: float - - .. code-block:: python - - x = q.Measurement(4, 0.5) - y = 2*(x**2) - - print('dy/dx =', y.get_derivative(x)) - - .. nboutput:: ipython3 - - dy/dx = 16.0 - ''' - if variable is not None \ - and type(variable) is not Measurement: - print('''The derivative of a Measurement with respect to anything - other than a Measurement is zero.''') - return 0 - - elif variable is None: - raise TypeError('''The object must be differentiated with respect to another - Measurement.''') - - if variable.info['ID'] not in self.derivative: - self.derivative[variable.info['ID']] = 0 - - derivative = self.derivative[variable.info["ID"]] - return derivative - - @property - def mean(self): - '''The mean of a Measurement object. - - :setter: Sets the mean of the Measurement. - :getter: Returns the mean of the Measurement. - :type: float - - .. code-block:: python - - x = q.Measurement(0, 1) - - x.mean = 10 - print(x) - - .. nboutput:: ipython3 - - 10 +/- 1 - ''' - return self._mean - - @mean.setter - def mean(self, mean): - '''Sets the mean of a Measurement object. - ''' - if(type(mean) in ExperimentalValue.CONSTANT): - self.der[0] = mean - self.MinMax[0] = mean - self.MC[0] = mean - self._mean = mean - else: - print("Mean must be a number") - self._mean = 0 - - @property - def std(self): - '''Gets the standard deviation of a Measurement object. - - :setter: Sets the error of the Measurement. - :getter: Returns the error of the Measurement. - :type: float - - .. code-block:: python - - x = q.Measurement(5, 0) - - x.std = 0.5 - print(x) - - .. nboutput:: ipython3 - - 5.0 +/- 0.5 - ''' - return self._std - - @std.setter - def std(self, std): - '''Sets the standard deviation of a Measurement object. - ''' - if(type(std) in ExperimentalValue.CONSTANT): - self.der[1] = std - self.MinMax[1] = std - self.MC[1] = std - self._error_on_mean = std/np.sqrt(len(self.get_data_array())) - self._std = std - else: - print("Standard deviation must be a number") - self._std = 0 - - @property - def error_on_mean(self): - '''The error on the mean of a Measurement object. - - :setter: Sets the error on the mean of the Measurement object. Also updates the error on the Measurement. - :getter: Returns the error on the mean of the Measurement. - :type: float - - .. code-block:: python - - x = q.Measurement(5, 0) - - x.error_on_mean = 0.5 - print('Error on mean:', x.error_on_mean) - - .. nboutput:: ipython3 - - Error on mean: 0.5 - ''' - if self._error_on_mean: - return self._error_on_mean - else: - print("Error: error on mean not calculated") - return 0 - - @error_on_mean.setter - def error_on_mean(self, error_on_mean): - '''Sets the error on the mean of a Measurement object. - ''' - if(type(error_on_mean) in ExperimentalValue.CONSTANT): - self._error_on_mean = error_on_mean - self._std = error_on_mean*np.sqrt(len(self.get_data_array())) - else: - print("Error on mean must be a number") - self._error_on_mean = 0 - - @property - def name(self): - '''The name of a Measurement object. - - :setter: Sets the name of the Measurement object. - :getter: Returns the name of the Measurement object. - :type: str - - .. code-block:: python - - x = q.Measurement(5, 0.2, name='mass') - - x.name = 'length' - print(x) - - .. nboutput:: ipython3 - - length = 5.0 +/- 0.2 - ''' - return self._name - - @name.setter - def name(self, name): - '''Sets the name of a Measurement object. - ''' - if isinstance(name, str): - self._name = name - else: - print("Name must be a string") - self._name = 'unnamed_var%d' % (Measurement.id_number) - - @property - def units(self): - '''The units of a Measurement object. - - :setter: Sets the units of the Measurement object. - :getter: Returns the name of the Measurement object. - :type: str - - .. code-block:: python - - x = q.Measurement(5, 0.2, name='length') - - x.units = 'm' - print(x) - - .. nboutput:: ipython3 - - length = 5.0 +/- 0.2 [m] - ''' - return self._units - - @units.setter - def units(self, units): - '''Sets the units of a Measurement object. - ''' - if units is not None: - if type(units) is str: - self._units = _parse_units(units) - elif type(units) is dict: - self._units = units - else: - for i in range(len(units)//2): - self._units[units[2*i]] = units[2*i+1] - - @property - def relative_error(self): - '''Gets the relative error (error/mean) of a Measurement object. - - :setter: Sets the error of the Measurement to a fraction of it's mean. - :getter: Returns the error as a fraction of the mean - (ie. 0.1 corresponds to a 10% error). - :type: float - - .. code-block:: python - - x = q.Measurement(5, 0) - - x.relative_error = 0.1 - print(x) - - .. nboutput:: ipython3 - - 5.0 +/- 0.5 - ''' - return self.std/self.mean if self.mean !=0 else 0. - - @relative_error.setter - def relative_error(self, rel_error): - '''Sets the relative error (error/mean) of a Measurement object. - ''' - if(type(rel_error) in qu.number_types): - self.std = self.mean*rel_error - else: - print("Relative error must be a number") - - def get_data_array(self): - '''Returns the underlying data array used to create the Measurement object. - - :returns: A python list of the data used to create the Measurement object. - :rtype: list - - .. code-block:: python - - x = q.Measurement([11, 12, 13]) - - print(x.get_data_array()) - - .. nboutput:: ipython3 - - [ 11. 12. 13.] - ''' - if self.info['Data'] is None: - print('No data array exists.') - return None - return self.info['Data'] - - def get_units_str(self): - '''Gets the units of the associated Measurement. - - :returns: The units of the Measurement. - :rtype: str - - .. code-block:: python - - mass = q.Measurement(4, 0.5, units='kg') - accel = q.Measurement(9.8, 0.1, units='m/s^2') - force = mass*accel - - print(force.get_units_str()) - - .. nboutput:: ipython3 - - s^-2 kg^1 m^1 - ''' - unit_string = '' - unit_dict = self.units - if unit_dict != {}: - for key in unit_dict: - if unit_dict[key] == 1 and len(unit_dict.keys()) is 1: - unit_string = key + unit_string - else: - unit_string += key+'^%d' % (unit_dict[key]) - unit_string += ' ' - - if unit_string == '': - unit_string = 'unitless' - return unit_string - - def show_histogram(self, bins=50, color="#036564", title=None, output='inline'): - '''Creates a histogram of the inputted data using Bokeh or mpl. - - :param bins: The number of bins for the histogram. - :type bins: int - :param color: The color of the bars. - :type color: str - :param title: The title that will appear at the top of the histogram. - :type title: str - :param output: How the histogram is to be output. Can be 'inline' or 'file'. - :type output: str - - :returns: The Plot object used to create the histogram. - :rtype: Plot - - .. bokeh-plot:: - :source-position: above - - import qexpy as q - import numpy as np - - rand_norm = np.random.normal(10, 1, 1000) - x = q.Measurement(rand_norm) - - x.show_histogram(title='Mean 10, std. 1 random normal') - ''' - if self.info['Data'] is None: - print("no data to histogram") - return None - - if type(title) is str: - hist_title = title - elif title is None: - hist_title = self.name+' Histogram' - else: - print('Histogram title must be a string.') - hist_title = self.name+' Histogram' - - data_arr = self.info['Data'] - - data = q.XYDataSet(xdata = data_arr, is_histogram = True, data_name=hist_title) - fig = q.MakePlot() - fig.add_dataset(data, color=color) - fig.x_range = [min(data_arr)*.95,max(data_arr)*1.05] - fig.y_range = [0,max(data.ydata)*1.2] - - mean = self.mean - std = self.std - fig.add_line(x=mean, dashed=False, color='red') - fig.add_line(x=mean+std, dashed=True, color='red') - fig.add_line(x=mean-std, dashed=True, color='red') - - fig.show(output=output) - return fig - - def show_MC_histogram(self, bins=50, color="#036564", title=None, output='inline'): - '''Creates and shows a Bokeh plot of a histogram of the values - calculated by Monte Carlo error simulation. - - :param bins: The number of bins for the histogram. - :type bins: int - :param color: The color of the bars. - :type color: str - :param title: The title that will appear at the top of the histogram. - :type title: str - :param output: How the histogram is to be output. Can be 'inline' or 'file'. - :type output: str - - :returns: The Plot object used to create the histogram. - :rtype: Plot - - .. bokeh-plot:: - :source-position: above - - import qexpy as q - - x = q.Measurement(10, 0.2) - y = q.Measurement(8, 0.4) - z = y**2*x - - z.show_MC_histogram(title='Monte Carlo histogram') - ''' - MC_data = self.MC_list - if MC_data is None: - print("no MC data to histogram") - return None - - if type(title) is str: - hist_title = title - elif title is None: - hist_title = self.name+' Histogram' - else: - print('Histogram title must be a string.') - hist_title = self.name+' Histogram' - - data = q.XYDataSet(xdata = MC_data, is_histogram = True, data_name=hist_title) - fig = q.MakePlot() - fig.add_dataset(data, color = color) - fig.x_range = [min(MC_data)*.95,max(MC_data)*1.05] - fig.y_range = [0,max(data.ydata)*1.2] - - # Adds a line at the mean and at the range corresponding to 68% coverage. - MC_mean, MC_std = self.MC - fig.add_line(x=MC_mean, dashed=False, color='red') - fig.add_line(x=MC_mean+MC_std, dashed=True, color='red') - fig.add_line(x=MC_mean-MC_std, dashed=True, color='red') - - fig.show(output=output) - return fig - - def show_error_contribution(self, color="#036564", title=None, output='inline'): - '''Creates and shows a Bokeh or mpl plot of a histogram of the relative - contribution of individual measurements to the variance of a calculated value. - - :param color: The color of the bars. - :type color: str - :param title: The title that will appear at the top of the histogram. - :type title: str - :param output: How the histogram is to be output. Can be 'inline' or 'file'. - :type output: str - - :returns: The Plot object used to create the histogram. - :rtype: Plot - - .. bokeh-plot:: - :source-position: above - - import qexpy as q - - w = q.Measurement(0.0255, 0.0001, name='width') - t = q.Measurement(0.0050, 0.0001, name='thickness') - m = q.Measurement(0.156, 0.001, name='mass') - P = q.Measurement(6.867, 0, name='P') - - E = 4*P/(m**2*w*t) - - E.show_error_contribution(title='Error contribution') - ''' - terms = {} - formula = self.info['Formula'] - - # For each measurement that goes into the calculation, evaluate the calculation - # at that measurement +/- the std. Take the output of that and do .5*(output-mean)^2. - # Add the +std and -std term. - # This process is described in this paper: http://pubs.acs.org/doi/abs/10.1021/ed1004307 - for i in self.root: - maxx = formula - minn = formula - name = "" - for j in self.root: - meas = q.error.ExperimentalValue.register[j] - if j == i: - name = meas.name - maxx = maxx.replace(j, str(meas.mean+meas.std)) - minn = minn.replace(j, str(meas.mean-meas.std)) - else: - maxx = maxx.replace(j, str(meas.mean)) - minn = minn.replace(j, str(meas.mean)) - terms[name] = .5*(eval(maxx)-self.mean)**2+.5*(eval(minn)-self.mean)**2 - - N = len(terms) - names = [] - vals = [] - - for k, v in sorted(terms.items()): - names.append(k) - vals.append(v) - - # Change the absolute terms into relative terms. - summ = sum(vals) - for index in range(N): - vals[index] = vals[index]/summ - - # Add spacing to make the histogram look like a bar chart. - new_vals = [] - new_names = [] - for index in range(N): - new_vals.append(vals[index]) - new_vals.append(0) - new_names.append('') - new_names.append(names[index]) - new_vals = new_vals[0:-1] - - if title is not None: - plot_title = title - else: - plot_title = 'Relative contribution to variance of {}'.format(self.name) - - data = q.XYDataSet(xdata=np.arange(2*N-1), ydata=new_vals, is_histogram = True, bins=N, - data_name=plot_title) - - # Populates the mpl figure in case it is plotted. - fig = q.MakePlot() - fig.add_dataset(data, color=color) - fig.x_range = [-1,2*N-1] - fig.y_range = [0,1] - fig.set_labels(xtitle="", ytitle="") - fig.populate_mpl_figure() - fig.mplfigure_main_ax.axes.set_xticklabels(new_names) - fig.mplfigure_main_ax.axes.grid(False, which='both', axis='x') - - # Populates the boken figure in case it is plotted. - # The messy stuff comes from the fact that mpl boxes, - # mpl labels and bokeh boxes are 0-indexed, but mpl labels are 1 indexed. - fig.axes['xscale'] = 'auto' - fig.datasets[0].ydata = np.insert(fig.datasets[0].ydata, [0, 0, 2*N-1], [0, 0, 0]) - fig.datasets[0].xdata = np.append(fig.datasets[0].xdata, [2*N-1, 2*N, 2*N+1]) - fig.x_range = new_names+[''] - fig.populate_bokeh_figure() - fig.bkfigure.xgrid.grid_line_color = None - fig.bkfigure.xaxis.major_tick_line_color = None - fig.bkfigure.xaxis.minor_tick_line_color = None - - # Will use whatever plotting engine is in use. - fig.show(populate_figure=False, refresh = True) - - return fig - -############################################################################### -# Methods for Correlation and Covariance -############################################################################### - - def _find_covariance(x, y): - '''Uses the data from which x and y were generated to calculate - covariance and add this informaiton to x and y. - - Requires data arrays to be stored in the .info of both objects - and that these arrays are of the same length, as the covariance is - only defined in these cases. - - :param x: A Measurement with an underlying data array the same length as y. - :type x: Measurement - :param y: A Measurement with an underlying data array the same length as x. - :type y: Measurement - ''' - try: - x.covariance[y.info['ID']] - return - except KeyError: - pass - - data_x = x.info["Data"] - data_y = y.info["Data"] - - if data_x is None or data_y is None: - raise TypeError( - "Data arrays must exist for both quantities " + - "to define covariance.") - - if len(data_x) != len(data_y): - raise TypeError('Lengths of data arrays must be equal to\ - define a covariance') - - sigma_xy = np.sum((data_x-x.mean)*(data_y-y.mean)) - nmin1 = len(data_x)-1 - if nmin1 != 0: - sigma_xy /= nmin1 - x.covariance[y.info['ID']] = sigma_xy - y.covariance[x.info['ID']] = sigma_xy - - factor = sigma_xy - if x.std != 0: - factor /= x.std - if y.std != 0: - factor /= y.std - - x.correlation[y.info['ID']] = factor - y.correlation[x.info['ID']] = factor - - def set_correlation(self, y, factor): - '''Manually set the correlation between two quantities - - Given a correlation factor, the covariance and correlation - between two variables is added to both objects. - - :param y: The variable to set the correlation with respect to. - :type y: str - :param factor: The correlation term between the two variables. - Must be between -1 and 1 inclusive. - :type factor: float - - .. code-block:: python - - x = q.Measurement(4, 0.1, units='m') - y = q.Measurement(12, 0.2, units='m') - x.set_correlation(y, 0.8) - - print(x.get_correlation(y)) - - .. nboutput:: ipython3 - - 0.8 - ''' - if factor > 1 or factor < -1: - raise ValueError('Correlation factor must be between -1 and 1.') - - x = self - rho_xy = factor - sigma_xy = rho_xy*x.std*y.std - - x.correlation[y.info['ID']] = factor - y.correlation[x.info['ID']] = factor - - x.covariance[y.info['ID']] = sigma_xy - y.covariance[x.info['ID']] = sigma_xy - - def set_covariance(self, y, sigma_xy): - '''Manually set the covariance between two quantities - - Given a covariance value, the covariance and correlation - between two variables is added to both objects. - - :param y: The variable to set the covariance with respect to. - :type y: Measurement - :param sigma_xy: The covariance term between the two variables. - :type sigma_xy: float - - .. code-block:: python - - x = q.Measurement(4, 0.1, units='m') - y = q.Measurement(12, 0.2, units='m') - x.set_covariance(y, 1.2) - - print(x.get_covariance(y)) - - .. nboutput:: ipython3 - - 1.2 - ''' - x = self - factor = sigma_xy - if x.std != 0: - factor /= x.std - if y.std != 0: - factor /= y.std - - x.correlation[y.info['ID']] = factor - y.correlation[x.info['ID']] = factor - - x.covariance[y.info['ID']] = sigma_xy - y.covariance[x.info['ID']] = sigma_xy - - def get_covariance(self, y): - '''Returns the covariance of the object and a specified variable. - - This funciton checks for the existance of a data array in each - object and that the covariance of the two objects is not already - specified. In each case, the covariance is returned, unless - the data arrays are of different lengths or do not exist, in that - case a covariance of zero is returned. - - :param y: The variable to get the covariance with respect to. - :type y: Measurement - - :returns: The covariance term between the two variables. - :rtype: float - - .. code-block:: python - - x = q.Measurement([11, 12, 13]) - y = q.Measurement([11, 12.1, 12.9]) - - print('xy covariance: ', x.get_covariance(y)) - - .. nboutput:: ipython3 - - xy covariance: 0.95 - ''' - if isinstance(self, Constant) or isinstance(y, Constant): - return 0 - if self.info['ID'] in y.covariance: - return self.covariance[y.info['ID']] - - elif self.info["Data"] is not None \ - and y.info["Data"] is not None\ - and len(self) == len(y) and len(self) != 1: - ExperimentalValue._find_covariance(self, y) - var = self.covariance[y.info['ID']] - return var - elif isinstance(self, Function): - term = 0 - for root in self.root: - root_obj = ExperimentalValue.register[root] - partial_der = self.derivative[root] - term += partial_der*y.get_covariance(root_obj) - if term: - self.set_covariance(y, term) - return term - elif isinstance(y, Function): - term = 0 - for root in y.root: - root_obj = ExperimentalValue.register[root] - partial_der = y.derivative[root] - term += partial_der*self.get_covariance(root_obj) - if term: - self.set_covariance(y, term) - return term - return 0 - - def get_correlation(self, y): - '''Returns the correlation factor of two measurements. - - Using the covariance, or finding the covariance if not defined, - the correlation factor of two measurements is returned. - - :param y: The variable to get the covariance with respect to. - :type y: Measurement - - :returns: The correlation term between the two variables. - :rtype: float - - .. code-block:: python - - x = q.Measurement([11, 12, 13]) - y = q.Measurement([11, 12.4, 12.6]) - - print('xy correlation: ', x.get_correlation(y)) - - .. nboutput:: ipython3 - - xy correlation: 0.917662935482 - ''' - x = self - if x.std == 0 or y.std == 0: - return 0 - - if y.get_covariance(x) is not 0: - pass - else: - # ExperimentalValue._find_covariance(x, y) - return 0 - - sigma_xy = x.covariance[y.info['ID']] - sigma_x = x.std - sigma_y = y.std - - factor = sigma_xy - factor /= (x.std*y.std) - - return factor - -############################################################################### -# Methods for Naming and Units -############################################################################### - - def rename(self, name=None, units=None): - '''Used to rename an object or change its units. - - :param name: The new name of the Measurement. - :type name: str - :param units: The new units of the Measurement. - :type units: str - - .. code-block:: python - - mass = q.Measurement(4, 0.5, units='kg') - mass.rename(units='g', name='mass') - - print(mass) - .. nboutput:: ipython3 - - mass = 4.0 +/- 0.5 [g] - ''' - if name is not None: - self.name = name - self.user_name = True - - if units is not None: - self.units = units - - def _update_info(self, operation, *args, func_flag=False): - ''' - Update the formula, name and method of an object. - - Function to update the name, formula and method of a value created - by a measurement operation. The name is updated by combining the - names of the object acted on with another name using whatever - operation is being performed. The function flag is to change syntax - for functions like sine and cosine. Method is updated by acessing - the class property. - - :param operation: A mathematical operation on 1 or 2 Measurement objects. - :type operation: Function - :param *args: The 1 or 2 Measurement objects being operated upon. - :type *args: tuple - :param func_flag: Whether the operation is a mathematical function. - (ie. sqrt, power, sin... are functions but +,-,**... are not). - :type func_flag: bool - ''' - import qexpy.error_operations as op - - if len(args) is 1: - var1 = args[0] - var2 = None - elif len(args) is 2: - var1 = args[0] - var2 = args[1] - - op_string = {op.sin: 'sin', op.cos: 'cos', op.tan: 'tan', op.sqrt: 'sqrt', - op.csc: 'csc', op.sec: 'sec', op.cot: 'cot', - op.exp: 'exp', op.log: 'log', op.add: '+', - op.sub: '-', op.mul: '*', op.div: '/', op.power: '**', - 'neg': '-', op.asin: 'asin', op.acos: 'acos', - op.atan: 'atan', } - - if func_flag == False and var2 is not None: - self.rename(var1.name+op_string[operation]+var2.name) - self.user_name = False - self.info['Formula'] = '('+ var1.info['Formula'] + ')' + \ - op_string[operation] + '(' + var2.info['Formula'] + ')' - self.info['Function']['variables'] += (var1, var2), - self.info['Function']['operation'] += operation, - ExperimentalValue.formula_register.update( - {self.info["Formula"]: self.info["ID"]}) - self.info['Method'] += "Errors propagated by " +\ - self.error_method + ' method.\n' - for root in var1.root: - if root not in self.root: - self.root += (root,) - for root in var2.root: - if root not in self.root: - self.root += (root,) - - if var1.units == var2.units and op_string[operation] in ('+', '-'): - self.units = var1.units - - elif op_string[operation] is '*': - for key in var1.units: - self._units[key] = var1.units[key] - for key in var2.units: - if key in var1.units: - self._units[key] = var1.units[key] + var2.units[key] - if self.units[key] == 0: - del self._units[key] - else: - self._units[key] = var2.units[key] - - elif op_string[operation] is '/': - for key in var1.units: - self._units[key] = var1.units[key] - for key in var2.units: - if key in var1.units: - self._units[key] = var1.units[key] - var2.units[key] - if self.units[key] == 0: - del self._units[key] - else: - self._units[key] = -var2.units[key] - - elif func_flag == True and var2 is None: - self.rename(op_string[operation]+'('+var1.name+')') - self.user_name = False - self.info['Formula'] = op_string[operation] + '(' + \ - var1.info['Formula'] + ')' - self.info['Function']['variables'] += (var1,), - self.info['Function']['operation'] += operation, - self.info['Method'] += "Errors propagated by " + \ - self.error_method + ' method.\n' - ExperimentalValue.formula_register.update( - {self.info["Formula"]: self.info["ID"]}) - for root in var1.root: - if root not in self.root: - self.root += var1.root - self.units = '' - - else: - #TODO double check with Connor, but I think it was a bug above and we have to check == True - # not is True, since 1 could also be True... - print('Something went wrong in update_info') - -############################################################################### -# Operations on measurement objects -############################################################################### - - ########################################################################### - # ARITHMETIC OPERATIONS - # Called whenever an operation (+, -, /, *, **) is encountered - # Call operation_wrap() in error_operations.py - ########################################################################### - - def __add__(self, other): - '''Handles addition with Measurements. - - Ensures that the two objects can be added and then sends them to - error_operations.operation_wrap, which handles the addition and - error propagation. Called with: Measurement+other - - :param other: The object to be added. - :type other: Measurement, constant, array - - :returns: The sum with propagated mean and error. - :rtype: Measurement - ''' - #TODO: is this the correct implementation??? or should ARRAy create an ndarray??? - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.add, self, other[i]) - #result.append(op.operation_wrap(op.add, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other+self - else: - return op.operation_wrap(op.add, self, other) - - def __radd__(self, other): - '''Handles addition with Measurements. - - Ensures that the two objects can be added and then sends them to - error_operations.operation_wrap, which handles the addition and - error propagation. Called with: other+Measurement - - :param other: The object to be added. - :type other: Measurement, constant, array - - :returns: The sum with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.add, self, other[i]) - #result.append(op.operation_wrap(op.add, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other+self - else: - return op.operation_wrap(op.add, self, other) - - def __mul__(self, other): - '''Handles multiplication with Measurements. - - Ensures that the two objects can be multiplied and then sends them to - error_operations.operation_wrap, which handles the multiplication and - error propagation. Called with: Measurement*other - - :param other: The object to be multiplied. - :type other: Measurement, constant, array - - :returns: The product with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.mul, self, other[i]) - #result.append(op.operation_wrap(op.mul, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other*self - else: - return op.operation_wrap(op.mul, self, other) - - def __rmul__(self, other): - '''Handles multiplication with Measurements. - - Ensures that the two objects can be multiplied and then sends them to - error_operations.operation_wrap, which handles the multiplication and - error propagation. Called with: other*Measurement - - :param other: The object to be multiplied. - :type other: Measurement, constant, array - - :returns: The product with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.mul, self, other[i]) - #result.append(op.operation_wrap(op.mul, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other*self - else: - return op.operation_wrap(op.mul, self, other) - - def __sub__(self, other): - '''Handles subtraction with Measurements. - - Ensures that the object can be subtracted and then sends them to - error_operations.operation_wrap, which handles the subtraction and - error propagation. Called with: Measurement-other - - :param other: The object to be subtracted. - :type other: Measurement, constant, array - - :returns: The difference with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.sub, self, other[i]) - #result.append(op.operation_wrap(op.sub, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return self-other - else: - return op.operation_wrap(op.sub, self, other) - - def __rsub__(self, other): - '''Handles subtraction with Measurements. - - Ensures that the object can be subtracted and then sends them to - error_operations.operation_wrap, which handles the subtraction and - error propagation. Called with: other-Measurement - - :param other: The object that the Measurement will be subtracted from. - :type other: Measurement, constant, array - - :returns: The difference with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.sub, other[i], self) - #result.append(op.operation_wrap(op.sub, value, self)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other-self - else: - return op.operation_wrap(op.sub, other, self) - - def __truediv__(self, other): - '''Handles division with Measurements. - - Ensures that the object can be divided by and then sends them to - error_operations.operation_wrap, which handles the division and - error propagation. Called with: Measurement/other - - :param other: The object that the Measurement will be divided by. - :type other: Measurement, constant, array - - :returns: The quotient with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.div, self, other[i]) - #result.append(op.operation_wrap(op.div, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return self/other - else: - return op.operation_wrap(op.div, self, other) - - def __rtruediv__(self, other): - '''Handles division with Measurements. - - Ensures that the object can be divided by and then sends them to - error_operations.operation_wrap, which handles the division and - error propagation. Called with: other/Measurement - - :param other: The object that the Measurement will divide. - :type other: Measurement, constant, array - - :returns: The quotient with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.div, self, other[i]) - #result.append(op.operation_wrap(op.div, value, self)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other/self - else: - return op.operation_wrap(op.div, other, self) - - def __pow__(self, other): - '''Handles exponentiation with Measurements. - - Ensures that the object can be raised to the power of the other - and then sends them to error_operations.operation_wrap, which - handles the exponentiation and error propagation. - Called with: Measurement**other - - :param other: The power that the Measurement will be raised to. - :type other: Measurement, constant, array - - :returns: The result with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.power, self, other[i]) - #result.append(op.operation_wrap(op.power, self, value)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return self**other - else: - return op.operation_wrap(op.power, self, other) - - def __rpow__(self, other): - '''Handles exponentiation with Measurements. - - Ensures that the object can be raised to the power of the other - and then sends them to error_operations.operation_wrap, which - handles the exponentiation and error propagation. - Called with: other**Measurement - - :param other: The object that will be raised to the power of the - value of the Measurement. - :type other: Measurement, constant, array - - :returns: The result with propagated mean and error. - :rtype: Measurement - ''' - import qexpy.error_operations as op - if type(other) in ExperimentalValue.ARRAY: - result = Measurement_Array(len(other)) - for i in range(result.size): - result[i]=op.operation_wrap(op.power, self, other[i]) - #result.append(op.operation_wrap(op.power, value, self)) - return result - elif type(self) in ExperimentalValue.CONSTANT and\ - type(other) in ExperimentalValue.CONSTANT: - return other**self - else: - return op.operation_wrap(op.power, other, self) - - # Calls neg() in error_operations, which returns the negative of the value - def __neg__(self): - '''Returns the negative of a Measurement object. - - :returns: The negative of the Measurement. - :rtype: Measurement - ''' - import qexpy.error_operations as op - return op.neg(self) - - # Returns the length of the ExperimentalValue - def __len__(self): - '''Returns the length of a the array used to create the Measurement object. - - :returns: The length of the data array used to create the Measurement. - :rtype: int - ''' - return self.info['Data'].size - - ########################################################################### - # COMPARISON OPERATIONS - # Called whenever a comparison (>, <, >=, ==, ...) is made - # Makes the relevant comparison and return a boolean - ########################################################################### - def __eq__(self, other): - '''Checks if two Measurements are the same. - - :returns: True if the means of the two Measurements are the same, - False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean == other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean == other.mean - - def __req__(self, other): - '''Checks if two Measurements are the same. - - :returns: True if the means of the two Measurements are the same, - False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean == other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean == other.mean - - def __gt__(self, other): - '''Checks if a Measurement is greater than another Measurement. - - :returns: True if the mean of the Measurement is greater than the mean - of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean > other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean > other.mean - - def __rgt__(self, other): - '''Checks if a Measurement is less than another Measurement. - - :returns: True if the mean of the Measurement is less than the mean - of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean < other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean < other.mean - - def __ge__(self, other): - '''Checks if a Measurement is greater than or equal to another Measurement. - - :returns: True if the mean of the Measurement is greater than or equal to - the mean of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean >=other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean >= other.mean - - def __rge__(self, other): - '''Checks if a Measurement is less than or equal to another Measurement. - - :returns: True if the mean of the Measurement is less than or equal to - the mean of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean <= other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean <= other.mean - - def __lt__(self, other): - '''Checks if a Measurement is less than another Measurement. - - :returns: True if the mean of the Measurement is less than the mean - of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean < other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean < other.mean - - def __rlt__(self, other): - '''Checks if a Measurement is greater than another Measurement. - - :returns: True if the mean of the Measurement is greater than the mean - of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean > other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean > other.mean - - def __le__(self, other): - '''Checks if a Measurement is less than or equal to another Measurement. - - :returns: True if the mean of the Measurement is less than or equal to - the mean of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean <= other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean <= other.mean - - def __rle__(self, other): - '''Checks if a Measurement is greater than or equal to another Measurement. - - :returns: True if the mean of the Measurement is greater than or equal to - the mean of the other Measurement, False otherwise. - :rtype: bool - ''' - if type(other) in ExperimentalValue.CONSTANT: - return self.mean >= other - else: - try: - other.type - except AttributeError: - raise TypeError - else: - return self.mean >= other.mean - - def sqrt(x): - return sqrt(x) - - def log(x): - return log(x) - - def exp(x): - return exp(x) - - def e(x): - return exp(x) - - def sin(x): - return sin(x) - - def cos(x): - return cos(x) - - def tan(x): - return tan(x) - - def csc(x): - return csc(x) - - def sec(x): - return sec(x) - - def cot(x): - return cot(x) - - def asin(x): - return asin(x) - - def acos(x): - return acos(x) - - def atan(x): - return atan(x) - -############################################################################### -# Miscellaneous Methods -############################################################################### - - def _check_der(self, b): - ''' - Checks for a derivative with respect to b, else zero is defined as - the derivative. - - Checks the existance of the derivative of an object in the - dictionary of said object with respect to another variable, given - the variable itself, then checking for the ID of said variable - in the .derivative dictionary. If non exists, the deriviative is - assumed to be zero. - - :param b: The variable to check the derivative with respect to. - :type b: Measurement - ''' - for key in b.derivative: - if key in self.derivative: - pass - else: - self.derivative[key] = 0 - - -############################################################################### -# Measurement Sub-Classes -############################################################################### - - -class Measurement(ExperimentalValue): - ''' - Subclass of ExperimentalValue, specified by the user and treated as variables - or arguments of functions when propagating error. Stores the mean and - standard deviation of an experimentally measured value. - - :param name: The name of the Measurement object. - :type name: str - :param units: The units of the Measurement. - :type units: str - ''' - id_number = 0 - - def __init__(self, *args, name=None, units=None): - super().__init__(*args, name=name) - if name is not None: - self.name = name - else: - self.name = 'unnamed_var%d' % (Measurement.id_number) - - if units is not None: - if type(units) is str: - self.units = _parse_units(units) - elif type(units) is dict: - self.units = units - else: - for i in range(len(units)//2): - self._units[units[2*i]] = units[2*i+1] - - self.type = "ExperimentalValue" - self.info['ID'] = 'var%d' % (Measurement.id_number) - self.info['Formula'] = 'var%d' % (Measurement.id_number) - Measurement.id_number += 1 - self.derivative = {self.info['ID']: 1} - self.covariance = {self.info['ID']: self.std**2} - self.correlation = {self.info['ID']: 1} - ExperimentalValue.register.update({self.info["ID"]: self}) - self.root = (self.info["ID"],) - self.der = [self.mean, self.std] - self.MC = [self.mean, self.std] - self.MinMax = [self.mean, self.std] - -class Function(ExperimentalValue): - ''' - Subclass of ExperimentalValue, which are measurements created by operations or - functions of other Measurement type objects. They contain the mean and propagated - error of the function. - ''' - id_number = 0 - - def __init__(self, *args, name=None): - super().__init__(*args, name=name) - - self.name = 'obj%d' % (Function.id_number) - self.info['ID'] = 'obj%d' % (Function.id_number) - self.type = "Function" - Function.id_number += 1 - self.derivative = {self.info['ID']: 1} - ExperimentalValue.register.update({self.info["ID"]: self}) - self.covariance = {self.info['ID']: self.std**2} - self.correlation = {self.info['ID']: 1} - self.root = () - self.der = None - #These are set by super() - #self.MC = None - #self.MinMax = None - self.error_flag = False - - -class Constant(ExperimentalValue): - ''' - Subclass of ExperimentalValue, not neccesarily specified by the user, - called when a consant (int, float, etc.) is used in operation with a - measurement. This class is called before calculating operations to - ensure objects can be combined. The mean of a constant is the specified - value, the standard deviation is zero, and the derivarive with respect - to anything is zero. - ''' - def __init__(self, arg, name=None, units=None): - super().__init__(arg, 0) - - if name is not None: - self.name = name - else: - self.name = '%d' % (arg) - - if units is not None: - if type(units) is str: - self._units[units] = 1 - else: - for i in range(len(units)//2): - self._units[units[2*i]] = units[2*i+1] - - self.info['ID'] = 'Constant' - self.info["Formula"] = '%f' % arg - self.derivative = {} - self.info["Data"] = np.array([arg]) - self.type = "Constant" - self.covariance = {self.name: 0} - self.root = () - - -class Measurement_Array(np.ndarray): - '''A numpy-based array of Measurement objects. It is better to call - MeasurementArray() and let it create a Measurement_Array object - behind the scenes.''' - id_number = 0 - error_method = "Derivative" - def __new__(subtype, shape, dtype=Measurement, buffer=None, offset=0, - strides=None, order=None, name = None, units = None, error_method='Derivative'): - obj = np.ndarray.__new__(subtype, shape, dtype, buffer, offset, strides, - order) - if name is not None: - obj.name = name - else: - obj.name = 'unnamed_arr%d' % (Measurement_Array.id_number) - - obj.error_method = error_method - - obj._units = {} - if units is not None: - obj.units = units - - Measurement_Array.id_number += 1 - - return obj - - def __array_finalize__(self, obj): - '''Sets the name and units of the MeasurementArray during creation. - ''' - if obj is None: return - self.units = getattr(obj, 'units', None) - self.name = getattr(obj, 'name', None) - - def __array_wrap__(self, out_arr, context=None): - '''Used to make sure that numpy functions work on MeasurementArrays - and they return MeasurementArrays. - ''' - # then just call the parent - return np.ndarray.__array_wrap__(self, out_arr, context) - - def append(self, meas): - '''Appends a value to the end of a MeasurementArray. - - :param meas: A measurement to be appended to the MeasurementArray. - :type meas: Measurement, constant - - :returns: A MeasurementArray with the new value added to the end. - :rtype: Measurement_Array - - .. code-block:: python - - import qexpy as q - - x = q.MeasurementArray([12, 10], error=0.5) - y = q.Measurement(11, 0.2) - x = x.append(y) - - print(x) - - .. nboutput:: ipython3 - - 12.0 +/- 0.5, - 10.0 +/- 0.5, - 11.0 +/- 0.2 - ''' - data_name = "{}_{}".format(self.name,len(self)) - if type(meas) in ExperimentalValue.CONSTANT: - meas = Constant(meas) - if isinstance(meas, ExperimentalValue): - if self[0].user_name: - meas.rename(name=data_name) - meas.units = self.units - return self.__array_wrap__(np.append(self, meas)) - else: - print("Object to append must be a Measurement or a number") - return self - - def insert(self, pos, meas): - '''Inserts a value in a position in a MeasurementArray. - - :param pos: The 0-indexed position in the MeasurementArray that the - measurement will be inserted into. Must be between 0 and the - length of the MeasurementArray. - :type pos: int - :param meas: A measurement to be inserted into the MeasurementArray. - :type meas: Measurement, constant - - :returns: A MeasurementArray with the new value inserted into the position - specified by the pos parameter. - :rtype: Measurement_Array - - .. code-block:: python - - import qexpy as q - - x = q.MeasurementArray([12, 11], error=0.5) - y = q.Measurement(10, 0.2) - x = x.insert(1, y) - - print(x) - - .. nboutput:: ipython3 - - 12.0 +/- 0.5, - 10.0 +/- 0.2 - 11.0 +/- 0.5 - ''' - if pos not in range(len(self)): - print("Index for inserting out of bounds.") - return self - data_name = "{}_{}".format(self.name,len(self)) - if type(meas) in ExperimentalValue.CONSTANT: - meas = Constant(meas) - if isinstance(meas, ExperimentalValue): - if (self[0] and self[0].user_name): - meas.rename(name=data_name) - meas.units = self.units - result = np.insert(self, pos, meas) - for index in range(len(result)): - result[index].rename(name="{}_{}".format(self.name,index)) - return self.__array_wrap__(result) - else: - print("Object to insert must be a Measurement or a number") - return self - - def delete(self, pos): - '''Removes an element from a MeasurementArray. - - :param pos: The 0-indexed position in the MeasurementArray of the - Measurement that will be deleted. Must be between 0 and the - length of the MeasurementArray. - :type pos: int - - :returns: A MeasurementArray with the Measurement at the specified position - removed. - :rtype: Measurement_Array - - .. code-block:: python - - import qexpy as q - - x = q.MeasurementArray([12, 10, 11], error=0.5) - x = x.delete(1) - - print(x) - - .. nboutput:: ipython3 - - 12.0 +/- 0.5, - 11.0 +/- 0.5 - ''' - if pos not in range(len(self)): - print("Index for deleting out of bounds.") - return self - result = np.delete(self, pos) - for index in range(len(result)): - result[index].name = "{}_{}".format(self.name,index) - return self.__array_wrap__(result) - - def show_table(self, latex=False): - '''Prints the data of the Measurement_Array in a formatted table. - - :param latex: Whether to print the data using Latex formatting. - :type show: bool - - .. code-block:: python - - import qexpy as q - - mass = q.MeasurementArray([4.2, 4.1, 4.3], error=0.5, name='mass') - - mass.show_table() - - .. nboutput:: ipython3 - - mass - 4.2 +/- 0.5 - 4.1 +/- 0.5 - 4.3 +/- 0.5 - ''' - - x = self.means - stds = self.stds - - if latex: - s = ('\\begin{table}[htp]\n' - '\\begin{center}\n' - '\\begin{tabular}{|c|} \hline\n') - err_const = np.all(stds == stds[0]) - xtitle = '{\\bf '+self.name+'} ('+(self.get_units_str() if self.get_units_str() else 'units')+')' - xtitle += ' $\pm$ ' + str(stds[0]) if err_const else '' - s += xtitle + ' \\\\ \hline\hline \n' - for ind in range(len(x)): - s += str(x[ind]) + ('' if err_const else ' $\pm$ '+str(stds[ind])) + ' \\\\ \hline \n' - s += ('\end{tabular}\n' - '\end{center}\n' - '\caption{} \n' - '\end{table}') - print(s) - - else: - data = [] - for ind in range(len(x)): - data.append([str(x[ind]) + ' +/- ' + str(stds[ind])]) - df = pd.DataFrame(data, columns=[self.name], index=['']*len(x)) - print(df) - - @property - def means(self): - '''Returns a numpy array with the means of each value in the MeasurementArray, - as calculated by the method (der, MC, MinMax). - - :getter: Returns a numpy array of the means of the Measurements - in the MeasurementArray. - :type: numpy.ndarray - - .. code-block:: python - - x = q.MeasurementArray([11, 12, 13], error=0.5) - - print(x.means) - - .. nboutput:: ipython3 - - [ 11. 12. 13.] - ''' - if self.size == 0: - return np.ndarray(0) - - means = np.ndarray(shape=self.shape) - - for index, item in np.ndenumerate(self): - if item is not None and isinstance(item, ExperimentalValue): - if self.error_method == "MinMax": - means[index] = item.MinMax[0] - elif self.error_method == "MC": - means[index] = item.MC[0] - else: - means[index] = item.mean - else: - means[index] = 0 - return means - - @property - def stds(self): - '''Returns a numpy array with the standard deviations of each value - in the MeasurementArray, as calculated by the method (der, MC, MinMax). - - :setter: Sets the errors of the Measurement_Array to either a single - value if a number is provided, or to different errors if a - list is provided. - :getter: Returns a numpy array of the standard deviations of - the Measurements in the MeasurementArray. - :type: numpy.ndarray - - .. code-block:: python - - x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8]) - - print(x.stds) - - .. nboutput:: ipython3 - - [ 0.1 0.1 0.8] - ''' - if self.size == 0: - return np.ndarray(0) - - stds = np.ndarray(shape=self.shape) - - for index, item in np.ndenumerate(self): - if item is not None: - if self.error_method == "MinMax": - stds[index] = 9 - elif self.error_method == "MC": - stds[index] = item.MC[1] - else: - stds[index] = item.std - else: - stds[index] = 0 - return stds - - @stds.setter - def stds(self, error): - '''Sets the standard deviations of each value in the MeasurementArray, - either to the same value for all Measurements or to a different value - for each Measurement. - ''' - n = self.size - - if isinstance(error, qu.number_types):#MA([,,,,], error = x) - for i in range(n): - self[i].std=error - - elif isinstance(error, qu.array_types):#MA([,,,,], error = []) - if len(error)==n:#MA([,,,,], error = [,,,,]) - for i in range(n): - self[i].std=error[i] - - elif len(error)==1: #MA([,,,,], error = [x]) - for i in range(n): - self[i].std=error[0] - else: - print("Error list must be the same length as the original list") - - @property - def mean(self): - '''Gets the mean of the means of the Measurements in the MeasurementArray. - - :getter: Returns the mean of the means of all the Measurements in the - MeasurementArray. - :type: numpy.ndarray - - .. code-block:: python - - x = q.MeasurementArray([11, 12, 13], error=0.5) - - print(x.mean) - - .. nboutput:: ipython3 - - 12.0 - ''' - nparr = self.means - self._mean = nparr.mean() - return self._mean - - @property - def error_weighted_mean(self): - '''Gets the error weighted mean and error of a MeasurementArray. - The error weighted mean gives more weight to more precise Measurements. - - :getter: Returns the weighted mean of all the Measurements in the - MeasurementArray. - :type: tuple - - .. code-block:: python - - x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8]) - - # Will be less than 12, since 11 +/- 0.1 is more precise than 13 +/- 0.8 - print(x.error_weighted_mean) - - .. nboutput:: ipython3 - - 11.51 +/- 0.07 - ''' - means = self.means - stds = self.stds - stds2 = stds**2 - sumw2=0 - mean=0 - - #Needs to be a loop to catch the case of std == 0 - for i in range(means.size): - if stds[i] == 0.0: - continue - w2 = 1./stds2[i] - mean += w2*means[i] - sumw2 += w2 - - self._error_weighted_mean = (0. if sumw2==0 else mean/sumw2) - self._error_weighted_std = (0. if sumw2==0 else np.sqrt(1./sumw2)) - - return Measurement(self._error_weighted_mean, self._error_weighted_std) - - def std(self, ddof=1): - '''Calculates the standard deviation of the means of - all the Measurements in the MeasurementArray. - - :param ddof: The degrees of freedom of the MeasurementArray. - :type ddof: int - - :returns: The standard deviation of the means of all the - Measurements in the MeasurementArray. - :rtype: numpy.ndarray - - .. code-block:: python - - x = q.MeasurementArray([11, 12, 13], error=[0.1, 0.1, 0.8]) - - print(x.std(ddof=1)) - - .. nboutput:: ipython3 - - 1.0 - ''' - nparr = self.means - return nparr.std(ddof=ddof) - - @property - def units(self): - '''The units of a Measurement_Array. - - :setter: Sets the units of the Measurement object. - :getter: Returns the name of the Measurement object. - :type: str - - .. code-block:: python - - x = q.MeasurementArray(5, 0.2, name='length') - - x.units = 'm' - print(x) - - .. nboutput:: ipython3 - - 11.0 +/- 0.1 [m], - 12.0 +/- 0.1 [m], - 13.0 +/- 0.1 [m] - ''' - return self._units - - @units.setter - def units(self, units): - '''Sets the units of a Measurement_Array. - ''' - if units is not None: - if type(units) is str: - self._units = _parse_units(units) - elif type(units) is dict: - self._units = units - else: - for i in range(len(units)//2): - self._units[units[2*i]] = units[2*i+1] - for mes in self: - if mes: - if type(units) is str: - mes._units = _parse_units(units) - elif type(units) is dict: - mes._units = units - else: - for i in range(len(units)//2): - mes._units[units[2*i]] = units[2*i+1] - - def get_units_str(self): - '''Generates a string representation of the units. - - :returns: A string representation of the units of the MeasurementArray. - :rtype: str - - .. code-block:: python - - import qexpy as q - - mass = q.MeasurementArray([4.2, 4.1, 4.3], error=0.5, units='kg') - accel = q.MeasurementArray([9.8, 9.9, 9.5], error=0.1, units='m/s^2') - force = mass*accel - - print(force.get_units_str()) - - .. nboutput:: ipython3 - - kg^1 s^-2 m^1 - ''' - - return self[0].get_units_str() - - def __str__(self): - '''Returns a string representation of the MeasurementArray. - - :returns: A representation of the MeasurementArray. - :rtype: str - ''' - theString='' - for i in range(self.size): - theString += self[i].__str__() - if i != self.size-1: - theString += ',\n' - return theString - -def MeasurementArray(data, error=None, name=None, units=None, error_method='Derivative'): - '''Function to easily construct a Measurement_Array object. It is better to call this - function and let it create the Measurement_Array object behind the scenes. - - :param error: Either a single value representing the error on all - the Measurements in the MeasurementArray, or a list of errors - corresponding to all the Measurements in the MeasurementArray. - :type error: constant, array - :param name: A string of the name of the MeasurementArray. - :type name: str - :param units: A string of the units of the MeasurementArray. - :type units: str - :param error_method: Which method to use when propagating errors through - calculations on the MeasurementArray. - Can be 'Monte Carlo', 'Min Max' or'Derivative' - :type error_method: str - - :returns: A Measurement_Array object. - :rtype: Measurement_Array - ''' - - array = Measurement_Array(0, name=name, units=units, error_method=error_method) - user_name= True if name != None else False - - if error is None: #MA(data) - if isinstance(data, Measurement_Array):# MA(MA) - #TODO should this be a copy?? This will just - #make array be a reference to data... - array = data - array.units = units - #allow the name to be updated: - if name is not None: - array.name = name - - elif isinstance(data, qu.array_types): #MA([...]) - n = len(data) - array.resize(n) - if isinstance(data[0], qu.array_types) and len (data[0]) == 2: #MA([ (,), (,), (,)]) - for i in range(n): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(data[i][0],data[i][1], units=units, name=data_name) - array[i].user_name=user_name - - elif isinstance(data[0], qu.number_types): #MA([,,,]) - for i in range(n): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(float(data[i]),0., units=units, name=data_name) - array[i].user_name=user_name - elif isinstance(data[0], ExperimentalValue): #MA([Measurement,Measurement,Measurement,]) - for i in range(n): - data[i].rename(units=units) - array[i]=data[i] - array[i].user_name=user_name - else: - print("unsupported type for data") - - elif isinstance(data, qu.int_types): #MA(n) - array.resize(data) - for i in range(data): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(0.,0., units=units, name=data_name) - array[i].user_name=user_name - else: - print("unsupported type for data") - - else: #error is not None - if isinstance(data, Measurement_Array): - array = data - array.stds = error - array.units = units - #allow the name to be updated: - if name is not None: - array.name = name - - - elif isinstance(data, qu.array_types): #MA([], error = ...) - n = len(data) - array.resize(n) - - if isinstance(data[0], qu.number_types):#MA([,,,,], error = ...) - - if isinstance(error, qu.number_types):#MA([,,,,], error = x) - for i in range(n): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(float(data[i]),error, units=units, name=data_name) - array[i].user_name=user_name - elif isinstance(error, qu.array_types):#MA([,,,,], error = []) - if len(error)==len(data):#MA([,,,,], error = [,,,,]) - for i in range(n): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(float(data[i]),error[i], units=units, name=data_name) - array[i].user_name=user_name - elif len(error)==1: #MA([,,,,], error = [x]) - for i in range(n): - data_name = "{}_{}".format(array.name,i) - array[i]=Measurement(float(data[i]),error[0], units=units, name=data_name) - array[i].user_name=user_name - else: - print("error array must be same length as data") - else: - print("unsupported type for error") - - else: # data[0] must be a float - print("unsupported type for data:", type(data[0])) - - elif isinstance(data, qu.number_types): #MA(x,error=...) - array.resize(1) - if isinstance(error, qu.number_types):#MA(x, error = y) - data_name = "{}_{}".format(array.name,0) - array[0]=Measurement(float(data),error, units=units, name=data_name) - array[0].user_name=user_name - elif isinstance(error, qu.array_types) and len(error)==1:#MA(x, error = [u]) - data_name = "{}_{}".format(array.name,0) - array[0]=Measurement(float(data),error[0], units=units, name=data_name) - array[0].user_name=user_name - else: - print("unsupported type for error") - else: - print("unsupported type of data") - - return array - - - -############################################################################### -# Mathematical Functions -# These are called for functions in the form: error.func(ExperimentalValue) -# They call operation_wrap() in the error_operations.py file -############################################################################### - -ExperimentalValue.ARRAY = ExperimentalValue.ARRAY +(Measurement_Array,) - -def sqrt(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sqrt, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sqrt, x[index], func_flag=True) - return result - else: - return op.operation_wrap(op.sqrt, x, func_flag=True) - -def sin(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return np.ndarray(0, dtype=float) - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sin, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sin, x[index], func_flag=True) - #result = [] - #for value in x: - #result.append(op.operation_wrap(op.sin, value, func_flag=True)) - - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.sin(x) - else: - return op.operation_wrap(op.sin, x, func_flag=True) - - -def cos(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.cos, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.cos, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.cos, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.cos(x) - else: - return op.operation_wrap(op.cos, x, func_flag=True) - - -def tan(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.tan, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.tan, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.tan, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.tan(x) - else: - return op.operation_wrap(op.tan, x, func_flag=True) - - -def sec(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sec, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.sec, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.sec, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return 1/m.cos(x) - else: - return op.operation_wrap(op.sec, x, func_flag=True) - - -def csc(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.csc, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.csc, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.csc, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return 1/m.sin(x) - else: - return op.operation_wrap(op.csc, x, func_flag=True) - - -def cot(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.cot, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.cot, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.cot, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return 1/m.tan(x) - else: - return op.operation_wrap(op.cot, x, func_flag=True) - - -def log(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.log, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.log, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.log, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.log(x) - else: - return op.operation_wrap(op.log, x, func_flag=True) - - -def exp(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.exp, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.exp, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.exp, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.exp(x) - else: - return op.operation_wrap(op.exp, x, func_flag=True) - - -def e(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.exp, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.exp, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.exp, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.exp(x) - else: - return op.operation_wrap(op.exp, x, func_flag=True) - - -def asin(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.asin, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.asin, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.asin, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.asin(x) - else: - return op.operation_wrap(op.asin, x, func_flag=True) - - -def acos(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.acos, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.acos, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.acos, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.acos(x) - else: - return op.operation_wrap(op.acos, x, func_flag=True) - - -def atan(x): - import qexpy.error_operations as op - if type(x) in ExperimentalValue.ARRAY: - if len(x) <1: - return [] - if isinstance(x[0],ExperimentalValue): - result = Measurement_Array(len(x)) - for index in range(len(x)): - result[index]=op.operation_wrap(op.atan, x[index], func_flag=True) - else: - result = np.ndarray(len(x), dtype=type(x[0])) - for index in range(len(x)): - result[index]=op.operation_wrap(op.atan, x[index], func_flag=True) - #result = [] - #for value in x: - # result.append(op.operation_wrap(op.atan, value, func_flag=True)) - return result - elif type(x) in ExperimentalValue.CONSTANT: - import math as m - return m.atan(x) - else: - return op.operation_wrap(op.atan, x, func_flag=True) - - -############################################################################### -# Printing Methods -############################################################################### - - -def set_print_style(style=None, sigfigs=None): - '''Change style ("default","latex","scientific") of printout for - Measurement objects. - - The default style prints as the user might write a value, that is - 'x = 10 +/- 1'. - - Latex style prints in the form of 'x = (10\pm 1)\e0' which is ideal for - pasting values into a Latex document as will be the case for lab reports. - - The scientific style prints the value in reduced scientific notation - such that the error is a single digit, 'x = (10 +/- 1)*10**0'. - - :param style: The style that the Measurement objects are printed with. - Can be 'Latex', 'Scientific' or 'Default'. - :type style: str - :param sigfigs: The number of significant figures to use when printing. - :type sigfigs: int - ''' - latex = ("Latex", "latex", 'Tex', 'tex',) - Sci = ("Scientific", "Sci", 'scientific', 'sci', 'sigfigs',) - Default = ('default', 'Default',) - - if sigfigs is not None: - set_sigfigs(sigfigs) - ExperimentalValue.figs_on_uncertainty = False - - if style in latex: - ExperimentalValue.print_style = "Latex" - elif style in Sci: - ExperimentalValue.print_style = "Scientific" - elif style in Default: - ExperimentalValue.print_style = "Default" - else: - print('''A style must be a string of either: Scientific notation, - Latex, or the default style. Using default.''') - ExperimentalValue.print_style = "Default" - - -def set_error_method(chosen_method): - '''Choose the method of error propagation to be used. Enter a string. - - Function to change default error propogation method used in - measurement functions. - - :param chosen_method: The method to be used to propagate errors. - Can be 'Monte Carlo', 'Min Max' or 'Default'. - :type chosen_method: str - ''' - mc_list = ( - 'MC', 'mc', 'montecarlo', 'Monte Carlo', 'MonteCarlo', - 'monte carlo',) - min_max_list = ('Min Max', 'MinMax', 'minmax', 'min max',) - derr_list = ('Derivative', 'derivative', 'diff', 'der', 'Default', - 'default',) - - if chosen_method in mc_list: - ExperimentalValue.error_method = "Monte Carlo" - elif chosen_method in min_max_list: - ExperimentalValue.error_method = "Min Max" - elif chosen_method in derr_list: - ExperimentalValue.error_method = "Derivative" - else: - print("Method not recognized, using derivative method.") - ExperimentalValue.error_method = "Derivative" - - -def set_sigfigs_error(sigfigs=3): - '''Change the number of significant figures shown in print() - based on the number of sig figs in the error. - - :param sigfigs: The number of significant figures to use when printing. - :type sigfigs: int - ''' - if type(sigfigs) is None: - ExperimentalValue.figs = None - elif type(sigfigs) is int and sigfigs > 0: - ExperimentalValue.figs = sigfigs - ExperimentalValue.figs_on_uncertainty = True - else: - raise TypeError('''Specified number of significant figures must be - and interger greater than zero.''') - - -def set_sigfigs_centralvalue(sigfigs=3): - '''Change the number of significant figures shown in print() - based on the number of sig figs in the central value. - - :param sigfigs: The number of significant figures to use when printing. - :type sigfigs: int - ''' - if type(sigfigs) is None: - ExperimentalValue.figs = None - elif sigfigs > 0 and type(sigfigs) is int: - ExperimentalValue.figs = sigfigs - ExperimentalValue.figs_on_uncertainty = False - else: - raise TypeError('''Specified number of significant figures must be - an interger greater than zero.''') - - -def set_sigfigs(sigfigs=3): - '''Change the number of significant figures shown in print() - based on the number of sig figs in the error. - - :param sigfigs: The number of significant figures to use when printing. - :type sigfigs: int - ''' - set_sigfigs_error(sigfigs) - - -def _return_exponent(value): - '''Calculates the exponent of the argument in reduced scientific notation. - - :param value: The value to get the exponent for. - :type value: float - - :returns: The exponent of the order of magnitude of the value. - :rtype: int - ''' - value = abs(value) - flag = True - i = 0 - - while(flag): - if value == 0: - flag = False - elif value == float('inf'): - return float("inf") - elif value < 1: - value *= 10 - i -= 1 - elif value >= 10: - value /= 10 - i += 1 - elif value >= 1 and value < 10: - flag = False - return i - - -def _return_print_values(variable, method): - '''Function for returning the correct mean and std for the method - selected. - - :param variable: The variable we want to find the mean+/-error for - :type variable: Measurement - :param method: The method used to propagate error we want to find - the mean and error for. - :type method: str - - :returns: The mean and standard deviation of the variable. - :rtype: tuple - ''' - if isinstance(variable, Constant): - return (variable.mean, 0,) - if ExperimentalValue.error_method == 'Derivative': - [mean, std] = variable.der - elif ExperimentalValue.error_method == 'Monte Carlo': - [mean, std] = variable.MC - elif ExperimentalValue.error_method == 'Min Max': - [mean, std] = variable.MinMax - - if method is not None: - if method == 'Derivative': - [mean, std] = variable.der - elif method == 'Monte Carlo': - [mean, std] = variable.MC - elif method == 'Min Max': - [mean, std] = variable.MinMax - return (mean, std,) - - -def _tex_print(self, method=None): - '''Creates string used by __str__ in a style useful for printing in Latex, - as a value with error, in brackets multiplied by a power of ten. (ie. - 15+/-0.3 is (150 \pm 3)\e-1. Where Latex parses \pm as +\- and \e as - *10**-1) - - :param method: The method used to propagate error we want to find - the mean and error for. - :type method: str - - :returns: The mean+/-error of the variable, formatted using tex style. - :rtype: str - ''' - mean, std = _return_print_values(self, method) - - if ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == False: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(mean) - mean = int(round(mean*10**(figs - i), 0)) - std = int(round(std*10**(figs - i), 0)) - - if i - figs != 0: - return "(%d \pm %d)*10^{%d}" % (mean, std, i - figs) - else: - return "(%d \pm %d)" % (mean, std) - - elif ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == True: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(std) - mean = int(round(mean*10**(figs - i), 0)) - std = int(round(std*10**(figs - i), 0)) - - if i - figs != 0: - return "(%d \pm %d)*10^{%d}" % (mean, std, i - figs) - else: - return "(%d \pm %d)" % (mean, std) - - else: - i = _return_exponent(std) - mean = int(round(mean*10**-i, 0)) - std = int(round(std*10**-i, 0)) - - if i != 0: - return "(%d \pm %d)*10^{%d}" % (mean, std, i) - else: - return "(%d \pm %d)" % (mean, std) - - -def _def_print(self, method=None): - '''Returns string used by __str__ as two numbers representing mean and error - to either the first non-zero digit of error or to a specified number of - significant figures. - - :param method: The method used to propagate error we want to find - the mean and error for. - :type method: str - - :returns: The mean+/-error of the variable, formatted using the default style. - :rtype: str - ''' - method = self.error_method if not method else method - mean, std = _return_print_values(self, method) - - if ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == False: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(mean) - - decimal_places = figs - i - if decimal_places > 0: - n = '%d' % (decimal_places) - n = "%."+n+"f" - else: - n = '%.0f' - std = float(round(std, decimal_places)) - mean = float(round(mean, decimal_places)) - return n % (mean)+" +/- "+n % (std) - - elif ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == True: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(std) - - decimal_places = figs - i - if decimal_places > 0: - n = '%d' % (decimal_places) - n = "%."+n+"f" - else: - n = '%.0f' - std = float(round(std, decimal_places)) - mean = float(round(mean, decimal_places)) - return n % (mean)+" +/- "+n % (std) - - else: - if mean == float('inf') and std == float('inf'): - return "inf +/- inf" - - if mean == float('inf'): - return "inf" - - if std == 0: - return str(mean)+" +/- "+str(std) - - i = _return_exponent(std) - - if i < 0: - n = '%d' % (-i) - n = "%."+n+"f" - else: - n = '%.0f' - - mean = float(round(mean, -i)) - if std == float('inf'): - return n % (mean)+" +/- inf" - - std = float(round(std, -i)) - return n % (mean)+" +/- "+n % (std) - - -def _sci_print(self, method=None): - '''Returns string used by __str__ as two numbers representing mean and - error, each in scientific notation to a specified numebr of significant - figures, or 3 if none is given. - - :param method: The method used to propagate error we want to find - the mean and error for. - :type method: str - - :returns: The mean+/-error of the variable, formatted using scientific notation. - :rtype: str - ''' - mean, std = _return_print_values(self, method) - - if ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == False: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(mean) - mean = int(round(mean*10**(figs - i), 0)) - std = int(round(std*10**(figs - i), 0)) - - if i - figs != 0: - return "(%d +/- %d)*10^(%d)" % (mean, std, i - figs) - else: - return "(%d +/- %d)" % (mean, std) - - elif ExperimentalValue.figs is not None and\ - ExperimentalValue.figs_on_uncertainty == True: - - if mean == float('inf'): - return "inf" - - figs = ExperimentalValue.figs - 1 - i = _return_exponent(std) - mean = int(round(mean*10**(figs - i), 0)) - std = int(round(std*10**(figs - i), 0)) - - if i - figs != 0: - return "(%d +/- %d)*10^(%d)" % (mean, std, i - figs) - else: - return "(%d +/- %d)" % (mean, std) - - else: - i = _return_exponent(std) - mean = int(round(mean*10**-i, 0)) - std = int(round(std*10**-i, 0)) - - if i != 0: - return "(%d +/- %d)*10^(%d)" % (mean, std, i) - else: - return "(%d +/- %d)" % (mean, std) - - -############################################################################### -# Random Methods -############################################################################### - -def _parse_units(unit_str): - '''Parses the string representation of an objects units and - breaks it into it's constituent parts and their powers. - - :param unit_str: A string representation of an object's units. - :type unit_str: str - - :returns: A dictionary with the unit names as keys and their powers as values. - :rtype: dict - ''' - unit_dict = {} - div_split = unit_str.split("/") - for index in range(len(div_split)): - mult_split = re.findall('[a-zA-Z]+\^[+-]?\d+|[a-zA-Z]+', div_split[index]) # regex that finds [string^(+/- number)] or [string] - for term2 in mult_split: - pow_split = term2.split("^") - power_factor = -1 if index else 1 - power = power_factor*int(pow_split[1]) if len(pow_split)>1 else power_factor*1 - unit_dict[pow_split[0]] = power - return unit_dict - -def show_histogram(data, title=None, output='inline', bins=50, color="#036564"): - '''Creates a histogram of the inputted data using Bokeh or mpl. - - :param title: The title that will appear at the top of the histogram. - :type title: str - :param output: How the histogram is to be output. Can be 'inline' or 'file'. - :type output: str - - :returns: The Plot object used to create the histogram. - :rtype: Plot - ''' - if type(data) not in ExperimentalValue.ARRAY: - print('''Input histogram data must be an array''') - return - - if type(title) is str: - hist_title = title - elif title is None: - hist_title = 'Histogram' - else: - print('Histogram title must be a string.') - hist_title = 'Histogram' - - mean, std = _variance(data) - - xy_data = q.XYDataSet(xdata = data, is_histogram = True, data_name=hist_title) - fig = q.MakePlot() - fig.add_dataset(xy_data, color = color) - fig.x_range = [min(data)*.95,max(data)*1.05] - fig.y_range = [0,max(xy_data.ydata)*1.2] - - # Draws lines at the mean and location of the mean +/- standard deviation. - fig.add_line(x=mean, dashed=False, color='red') - fig.add_line(x=mean+std, dashed=True, color='red') - fig.add_line(x=mean-std, dashed=True, color='red') - - fig.show() - return fig - - -def numerical_partial_derivative(func, var, *args): - '''Returns the parital derivative of a dunction with respect to var. - - This function wraps the inputted function to become a function - of only one variable, the derivative is taken with respect to said - variable. - - :param func: The function to take the derivative of. - :type func: - :param var: The variable to take the derivative with respect to. - :type var: Measurement - - :returns: The derivative of the function with respect to the specified variable. - :rtype: float - ''' - def restrict_dimension(x): - partial_args = list(args) - partial_args[var] = x - return func(*partial_args) - return numerical_derivative(restrict_dimension, args[var]) - - -def numerical_derivative(function, point, dx=1e-10): - '''Calculates the first order derivative of a function. - - :param function: The function to take the derivative of. - :type function: Function - :param point: The point on the function to take the derivative at. - :type point: float - - :returns: The derivative of the function. - :rtype: float - ''' - return (function(point+dx)-function(point))/dx - - -def _variance(*args, ddof=1): - '''Uses a more sophisticated variance calculation to speed up - calculation of mean and standard deviation. - - :param *args: The variables we want to find the variance of. - :type *args: tuple - :param ddof: The degrees of freedom of the system. - :type ddof: int - - :returns: The mean and standard deviation of a data array. - :rtype: tuple - ''' - args = args[0] - Sum = 0 - SumSq = 0 - N = len(args) - for i in range(N): - Sum += args[i] - SumSq += args[i]*args[i] - - std = ((SumSq-Sum**2/N)/(N-1))**(1/2) - mean = Sum/N - - return (mean, std, ) - -def _weighted_variance(mean, std, ddof=1): - '''Calculates the variance weighted mean and standard deviation. - - :param mean: The mean of the variable. - :type mean: float - :param std: The standard deviation of the variable. - :type std: float - :param ddof: The degrees of freedom of the system. - :type ddof: int - - :returns: The variance weighted mean and standard deviation. - :rtype: tuple - ''' - from math import sqrt - - w = np.power(std, -2) - w_mean = sum(np.multiply(w, mean))/sum(w) - w_std = 1/sqrt(sum(w)) - return (w_mean, w_std) - - -def reset_variables(): - '''Resets the ID number, directories and methods to their original values. - Useful in Jupyter Notebooks if variables were unintentionally repeated. - ''' - Measurement.id_number = 0 - Function.id_number = 0 - ExperimentalValue.register = {} - ExperimentalValue.formula_register = {} - ExperimentalValue.error_method = "Derivative" - ExperimentalValue.mc_trial_number = 10000 - ExperimentalValue.print_style = "Default" - ExperimentalValue.figs = 3 diff --git a/qexpy/error_operations.py b/qexpy/error_operations.py deleted file mode 100644 index a6f5f52..0000000 --- a/qexpy/error_operations.py +++ /dev/null @@ -1,769 +0,0 @@ -import numpy as np -import math as m -import qexpy.utils as qu -from qexpy.error import Measurement_Array, Measurement, Constant, Function -import qexpy as q - -CONSTANT = qu.number_types -ARRAY = qu.array_types +(Measurement_Array,) -MEASUREMENT = (Measurement, Constant, Function) - -############################################################################### -# Mathematical operations -############################################################################### - - -def neg(x): - '''Returns the negitive of a Measurement object - ''' - import qexpy.error as e - - x, = check_values(x) - result_derivative = {} - for key in x.derivative: - result_derivative[key] = -x.derivative[key] - result = e.Function(-x.mean, x.std) - result.derivative.update(result_derivative) - result._update_info('neg', x, func_flag=1) - result.error_flag = True - return result - - -def add(a, b): - '''Returns a Measurement object that is the sum of two other Measurements. - - The sum can be taken by multiple methods, specified by the measurement - class variable measurement.error_method. The derivative of this new object - is also specifed by applying the chain rule to the input and the - derivative of the inputs. - ''' - if type(a) in ARRAY or type(b) in ARRAY: - import numpy as np - return np.add(a, b) - - elif type(a) in CONSTANT: - if type(b) in CONSTANT: - return a+b - else: - return a+b.mean - - elif type(a) and type(b) in ARRAY: - result = [] - for i in range(len(a)): - result.append(a[i] + b[i]) - return result - - else: - if type(b) in CONSTANT: - return a.mean+b - else: - return a.mean+b.mean - - -def sub(a, b): - '''Returns a Measurement object that is the subtraction of two Measurements. - ''' - if type(a) in ARRAY or type(b) in ARRAY: - import numpy as np - return np.subtract(a, b) - - if type(a) in CONSTANT: - if type(b) in CONSTANT: - return a-b - else: - return a-b.mean - - else: - if type(b) in CONSTANT: - return a.mean-b - else: - return a.mean-b.mean - - -def mul(a, b): - '''Returns the product of two values with propagated errors. - ''' - if type(a) in ARRAY or type(b) in ARRAY: - import numpy as np - return np.multiply(a, b) - - if type(a) in CONSTANT: - if type(b) in CONSTANT: - return a*b - else: - return a*b.mean - - else: - if type(b) in CONSTANT: - return a.mean*b - else: - return a.mean*b.mean - - -def div(a, b): - '''Returns the quotient of two values with propagated errors. - ''' - if type(a) in ARRAY or type(b) in ARRAY: - import numpy as np - return np.divide(a, b) - - if type(a) in CONSTANT: - if type(b) in CONSTANT: - return a if b ==0 else a/b - else: - return a if b ==0 else a/b.mean - - else: - if type(b) in CONSTANT: - return a.mean if b ==0 else a.mean/b - else: - return a.mean if b.mean ==0 else a.mean/b.mean - - -def power(a, b): - '''Returns the power of two values with propagated errors. - ''' - if type(a) in ARRAY or type(b) in ARRAY: - import numpy as np - return np.power(a, b) - - if type(a) in CONSTANT: - if type(b) in CONSTANT: - return a**b - else: - return a**b.mean - - else: - if type(b) in CONSTANT: - return a.mean**b - else: - return a.mean**b.mean - - -############################################################################### -# Mathematical Functions -############################################################################### - -def sqrt(x): - '''Returns the square root of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.sqrt(x) - elif type(x) in ARRAY: - return np.sqrt(x) - elif isinstance(x,MEASUREMENT): - return m.sqrt(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def sin(x): - '''Returns the sine of a Measurement with propagated errors. - ''' - import math as m - if type(x) in CONSTANT: - return m.sin(x) - elif type(x) in ARRAY: - return np.sin(x) - elif isinstance(x,MEASUREMENT): - return m.sin(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def asin(x): - '''Returns the arctangent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.asin(x) - elif type(x) in ARRAY: - return np.arcsin(x) - elif isinstance(x,MEASUREMENT): - return m.asin(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def cos(x): - '''Returns the cosine of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.cos(x) - elif type(x) in ARRAY: - return np.cos(x) - elif isinstance(x,MEASUREMENT): - return m.cos(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def acos(x): - '''Returns the arctangent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.acos(x) - elif type(x) in ARRAY: - return np.arccos(x) - elif isinstance(x,MEASUREMENT): - return m.acos(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def tan(x): - '''Returns the tangent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.tan(x) - elif type(x) in ARRAY: - return np.tan(x) - elif isinstance(x,MEASUREMENT): - return m.tan(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def atan(x): - '''Returns the arctangent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.atan(x) - elif type(x) in ARRAY: - return np.arctan(x) - elif isinstance(x,MEASUREMENT): - return m.atan(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def sec(x): - '''Returns the secant of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return 0. if m.cos(x) ==0 else 1./m.cos(x) - elif type(x) in ARRAY: - return 1./np.cos(x) - elif isinstance(x,MEASUREMENT): - return 0. if m.cos(x.mean) ==0 else 1./m.cos(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def csc(x): - '''Returns the cosecant of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return 0. if m.sin(x) ==0 else 1./m.sin(x) - elif type(x) in ARRAY: - return 1./np.sin(x) - elif isinstance(x,MEASUREMENT): - return 0. if m.sin(x.mean) ==0 else 1./m.sin(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def cot(x): - '''Returns the cotangent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return 0. if m.tan(x) ==0 else 1./m.tan(x) - elif type(x) in ARRAY: - return 1./np.tan(x) - elif isinstance(x,MEASUREMENT): - return 0. if m.tan(x.mean) ==0 else 1./m.tan(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def exp(x): - '''Returns the exponent of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.exp(x) - elif type(x) in ARRAY: - return np.exp(x) - elif isinstance(x,MEASUREMENT): - return m.exp(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -def log(x): - '''Returns the natural logarithm of a Measurement with propagated errors. - ''' - import math as m - - if type(x) in CONSTANT: - return m.log(x) - elif type(x) in ARRAY: - return np.log(x) - elif isinstance(x,MEASUREMENT): - return m.log(x.mean) - else: - raise TypeError("Unsupported type: "+str(type(x))) - -############################################################################### -# Error Propagation Methods -############################################################################### - - -def find_minmax(function, *args): - ''' - e.Function to use Min-Max method to find the best estimate value - and error on a given function - ''' - import numpy as np - N=Measurement.minmax_n - np.seterr(all='ignore') - if len(args) is 1: - x = args[0] - #vals = np.linspace(x.mean-x.std, x.mean + x.std, N) - vals = np.linspace(x.MinMax[0]-x.MinMax[1], x.MinMax[0] + x.MinMax[1], N) - results = function(vals) - - elif len(args) is 2: - a = args[0] - b = args[1] - results = np.ndarray(shape=(N,N)) - a_vals = np.linspace(a.MinMax[0]-a.MinMax[1], a.MinMax[0] + a.MinMax[1], N) - b_vals = np.linspace(b.MinMax[0]-b.MinMax[1], b.MinMax[0] + b.MinMax[1], N) - - for i in range(N): - for j in range(N): - results[i][j]= function(a_vals[i], b_vals[j]) - else: - print("unsupported number of parameters") - results = np.ndarray(0) - - np.seterr(all='raise') - min_val = results.min() - max_val = results.max() - mid_val = (max_val + min_val)/2. - err = (max_val-min_val)/2. - return [mid_val, err] - - -def monte_carlo(func, *args): - '''Uses a Monte Carlo simulation to determine the mean and standard - deviation of a function. - - Inputted arguments must be measurement type. Constants can be used - as 'normalized' quantities which produce a constant row in the - matrix of normally randomized values. - ''' - # 2D array - import numpy as np - import qexpy.error as e - - _np_func = {add: np.add, sub: np.subtract, mul: np.multiply, - div: np.divide, power: np.power, log: np.log, - exp: np.exp, sin: np.sin, cos: np.cos, sqrt: np.sqrt, - tan: np.tan, atan: np.arctan, - csc: lambda x: np.divide(1, np.sin(x)), - sec: lambda x: np.divide(1, np.cos(x)), - cot: lambda x: np.divide(1, np.tan(x)), - asin: np.arcsin, acos: np.arccos, atan: np.arctan, - } - - # Domains of functions that have undefined points in their domain. - # Returns True if the function is undefined at x. - exclude = {csc: lambda x: x%m.pi == 0, - sec: lambda x: x%m.pi == m.pi/2, - cot: lambda x: x%m.pi == 0, - div: lambda x: x == 0, - log: lambda x: x < 0, - sqrt: lambda x: x < 0, - acos: lambda x: (x > 1) | (x < -1), - asin: lambda x: (x > 1) | (x < -1)} - - N = len(args) - n = e.ExperimentalValue.mc_trial_number - value = np.zeros((N, n)) - result = np.zeros(n) - rand = np.zeros((N, n)) - - if N == 1: - if args[0].MC_list is None: - if args[0].std == 0: - args[0].MC_list = np.empty(n) - args[0].MC_list.fill(args[0].mean) - else: - args[0].MC_list = np.random.normal(args[0].mean, args[0].std, n) - if func in exclude: - value[0] = _truncate_normal(args[0].MC_list, args[0].MC[0], args[0].MC[1], exclude[func]) - else: - value[0] = args[0].MC_list - result = _np_func[func](*value) - else: - if q.quick_MC or args[0].get_correlation(args[1]) < 0.001: - if func == power: - if args[1].std == 0 and float(args[1].mean).is_integer(): # x can be anything - if args[0].MC_list is None: - if args[0].std == 0: - args[0].MC_list = np.empty(n) - args[0].MC_list.fill(args[0].mean) - else: - args[0].MC_list = np.random.normal(args[0].mean, args[0].std, n) - value[0] = args[0].MC_list - if args[1].MC_list is None: - args[1].MC_list = np.empty(n) - args[1].MC_list.fill(args[1].mean) - value[1] = args[1].MC_list - else: - if args[0].mean < 0: - return([np.nan, np.nan], result) - elif args[0].MC_list is not None: - value[0] = _truncate_normal(args[0].MC_list, args[0].MC[0], args[0].MC[1], exclude[log]) - else: - args[0].MC_list = _generate_excluded_normal(args[0].MC[0], args[0].MC[1], n, exclude[log]) - value[0] = args[0].MC_list - if args[1].MC_list is None: - if args[1].std == 0: - args[1].MC_list = np.empty(n) - args[1].MC_list.fill(args[1].mean) - else: - args[1].MC_list = np.random.normal(args[1].mean, args[1].std, n) - value[1] = args[1].MC_list - else: - if args[0].MC_list is None: - if args[0].std == 0: - args[0].MC_list = np.empty(n) - args[0].MC_list.fill(args[0].mean) - else: - args[0].MC_list = np.random.normal(args[0].mean, args[0].std, n) - if args[1].MC_list is None: - if args[1].std == 0: - args[1].MC_list = np.empty(n) - args[1].MC_list.fill(args[1].mean) - else: - args[1].MC_list = np.random.normal(args[1].mean, args[1].std, n) - value[0] = args[0].MC_list - if func == div: - value[1] = _truncate_normal(args[1].MC_list, args[1].MC[0], args[1].MC[1], exclude[div]) - else: - value[1] = args[1].MC_list - result = _np_func[func](*value) - else: # Cholesky decomp - result = _correlated_MC(func, *args) - data = np.mean(result) - error = np.std(result, ddof=1) - return ([data, error], result,) - -def _correlated_MC(func, *args): - '''Uses Cholesky decomposition to generate correlated random normal - variables. Then does the calculation with those values and returns the - result. - - :param func: The function being applied to the arguments. - :type func: function - :param *args: A tuple of the arguments of the function. - :type *args: tupe - ''' - funcs = {'log': np.log, - 'exp': np.exp, 'sin': np.sin, 'cos': np.cos, 'sqrt': np.sqrt, - 'tan': np.tan, atan: np.arctan, - 'csc': lambda x: np.divide(1, np.sin(x)), - 'sec': lambda x: np.divide(1, np.cos(x)), - 'cot': lambda x: np.divide(1, np.tan(x)), - 'asin': np.arcsin, 'acos': np.arccos, 'atan': np.arctan, - } - - import qexpy.error as e - n = e.ExperimentalValue.mc_trial_number - roots = () - for arg in args: - for root in arg.root: - if root not in roots: - roots += (root,) - N = len(roots) - C = np.ndarray(shape=(N, N)) - for row in range(N): - row_obj = e.ExperimentalValue.register[roots[row]] - for col in range(N): - col_obj = e.ExperimentalValue.register[roots[col]] - C[row][col] = row_obj.get_covariance(col_obj)/((row_obj.std if row_obj.std else 1)*(col_obj.std if col_obj.std else 1)) - - try: - U = np.linalg.cholesky(C).transpose() #need transpose to make upper diagonal - except np.linalg.linalg.LinAlgError: - #print("Problem with correlated Monte Carlo, ignoring correlation.") - U = np.linalg.cholesky(np.identity(N)).transpose() - - rand = np.zeros((n, N)) - #3 rows of random numbers - for i in range(N): - rand_row = np.random.normal(0,1,n) - rand_col = rand_row.reshape((-1, 1)) - rand[:, i] = rand_row - #combine - rc=np.empty_like(rand) - for i in range(n): - rc[i] = np.dot(rand[i],U) - #Get the columns back out - - root_dict={} - for j in range(N): - meas = e.ExperimentalValue.register[roots[j]] - root_dict[roots[j]] = rc[:,j]*meas.std+meas.mean - root_dict.update(funcs) - res = eval(args[0].info['Formula'], root_dict)*(rc[:,-1]*args[1].std+args[1].mean) - - return res - -def _truncate_normal(distrib, mean, sigma, exclude): - ''' Truncates a normal distribution so that it fits within the range - of a function. - - :param distrib: An array containing random normal values. - :type distrib: np.ndarray - :param mean: The mean of the random normal distribution. - :type mean: float - :param sigma: The standard deviation of the random normal distribution. - :type sigma: float - :param exclude: A function that returns whether to exclude a value. - :type std: function - - :returns: A truncated normal distribution. - :rtype: np.ndarray - ''' - x = distrib - if np.any(exclude(x)): - x[exclude(x)] = _generate_excluded_normal(mean, sigma, len(x[exclude(x)]), exclude) - return x - -def _generate_excluded_normal( mean, sigma, n, exclude): - '''Recursively generates random normal variables that are undefined - on some part of their codomain. Returns the normal distribution and - the underlying standard normal distribution that was used to create it. - - :param mean: The mean of the random normal distribution. - :type mean: float - :param sigma: The standard deviation of the random normal distribution. - :type sigma: float - :param sigma: The length of the normal distribution to be generated. - :type sigma: int - :param exclude: A function that returns whether to exclude a value. - :type std: function - - :returns: A truncated normal distribution. - :rtype: np.ndarray - ''' - x = np.random.normal(mean, sigma, n) # mean=mean, std=std random normal distribution - if np.any(exclude(x)): - x[exclude(x)] = _generate_excluded_normal(mean, sigma, len(x[exclude(x)]), exclude) - return x - -############################################################################### -# Methods for Propagation -############################################################################### - - -def operation_wrap(operation, *args, func_flag=False): - '''e.Function wrapper to convert existing, constant functions into functions - which can handle measurement objects and return an error propagated by - derivative, min-max, or Monte Carlo method. - ''' - import qexpy.error as e - - args = check_values(*args) - - if len(args) > 1: - args[0].get_covariance(args[1]) - - if func_flag is False: - args[0]._check_der(args[1]) - args[1]._check_der(args[0]) - - df = {} - for key in args[0].derivative: - df[key] = diff[operation](key, *args) - - if check_formula(operation, *args, func_flag=func_flag) is not None: - return check_formula(op_string[operation], *args, func_flag=func_flag) - - mean = operation(*args) - std = dev(*args, der=df) - result = e.Function(mean, std) - result.der = [mean, std] - result.MinMax = find_minmax(operation, *args) - result.MC, result.MC_list = monte_carlo(operation, *args) - - #TODO: This is wrong: should not keep changing the mean and std - # the error method should only change this on a print!!! - - # Derivative Method - #if e.ExperimentalValue.error_method == "Derivative": - # pass - - # By Min-Max method - #elif e.ExperimentalValue.error_method == "Min Max": - # (mean, std, ) = result.MinMax - - # Monte Carlo Method - #elif e.ExperimentalValue.error_method == 'Monte Carlo': - # (mean, std, ) = result.MC - - #else: - # print('''Error method not properly set, please set to derivatie, Monte - # Carlo, or Min-Max. Derivative method used by default.''') - - if func_flag is False and args[0].info["Data"] is not None\ - and args[1].info['Data'] is not None\ - and args[0].info['Data'].size == args[1].info['Data'].size: - d1 = args[0].info["Data"] - d2 = args[1].info["Data"] - result.info['Data'] = np.ndarray(d1.size, dtype=type(d1[0])) - for i in range(d1.size): - result.info["Data"][i] = operation(d1[i],d2[i]) - - elif args[0].info["Data"] is not None and func_flag is True: - result.info["Data"] = (operation(args[0].info["Data"])) - - result.derivative.update(df) - result._update_info(operation, *args, func_flag=func_flag) - - for root in result.root: - result.get_covariance(e.ExperimentalValue.register[root]) - - return result - -diff = { - sqrt: lambda key, x: (0. if x.mean ==0 else 0.5/m.sqrt(x.mean)*x.derivative[key]), - sin: lambda key, x: m.cos(x.mean)*x.derivative[key], - cos: lambda key, x: -m.sin(x.mean)*x.derivative[key], - tan: lambda key, x: m.cos(x.mean)**-2*x.derivative[key], - sec: lambda key, x: m.tan(x.mean)*m.cos(x.mean)**-1*x.derivative[key], - csc: lambda key, x: -(m.tan(x.mean)*m.sin(x.mean))**-1 * - x.derivative[key], - - cot: lambda key, x: -m.sin(x.mean)**-2*x.derivative[key], - exp: lambda key, x: m.exp(x.mean)*x.derivative[key], - log: lambda key, x: (0. if x.mean ==0 else 1./x.mean*x.derivative[key]), - add: lambda key, a, b: a.derivative[key] + b.derivative[key], - sub: lambda key, a, b: a.derivative[key] - b.derivative[key], - mul: lambda key, a, b: a.derivative[key]*b.mean + - b.derivative[key]*a.mean, - - div: lambda key, a, b: (a.derivative[key]*b.mean - - b.derivative[key]*a.mean) / (1. if b.mean==0 else b.mean**2), - - power: lambda key, a, b: a.mean**b.mean*( - b.derivative[key]*( 0. if a.mean<=0. else m.log(a.mean) ) + - b.mean/(1. if a.mean ==0 else a.mean)*a.derivative[key]), - - asin: lambda key, x: (1-x.mean**2)**(-1/2)*x.derivative[key], - acos: lambda key, x: -(1-x.mean**2)**(-1/2)*x.derivative[key], - atan: lambda key, x: 1/(1 + x.mean**2)*x.derivative[key], - } - -op_string = {sin: 'sin', cos: 'cos', tan: 'tan', csc: 'csc', sec: 'sec', sqrt: 'sqrt', - cot: 'cot', exp: 'exp', log: 'log', add: '+', sub: '-', - mul: '*', div: '/', power: '**', asin: 'asin', acos: 'acos', - atan: 'atan', } - - -#def dev(*args, der=None, manual_args=None): -def dev(*args, der=None): - '''Returns the standard deviation of a function of N arguments. - - Using the tuple of variables, passed in each operations that composes a - function, the standard deviation is calculated by the derivative error - propagation formula, including the covariance factor between each pair - of variables. The derivative dictionary of a function must be passes by - the der argument. - ''' - import qexpy.error as e - - - std = 0 - roots = () - - for arg in args: - for i in range(len(arg.root)): - if arg.root[i] not in roots: - roots += (arg.root[i], ) - - # Partial derivative times uncertainty terms - for root in roots: - std += (der[root]*e.ExperimentalValue.register[root].std)**2 - - # Covariance terms - for i in range(len(roots)): - for j in range(len(roots)-i-1): - cov = e.ExperimentalValue.register[roots[i]].get_covariance( - e.ExperimentalValue.register[roots[j + 1 + i]]) - std += 2*der[roots[i]]*der[roots[j + 1 + i]]*cov - - if std >= 0: - std = std**(0.5) - else: - print("Warning: variance from derivative method (error_operations:dev) is negative.") - print("Setting error to zero, this is likely incorrect!!!") - print("Maybe you have an unphysical covariance?") - std = 0 - - return std - - - -def check_values(*args): - '''Checks that the arguments are measurement type, otherwise a measurement - is returned. - - All returned values are of measurement type, if values need to be - converted, this is done by calling the normalize function, which - outputs a measurement object with no standard deviation. - ''' - import qexpy.error as e - - val = () - for arg in args: - if type(arg) in CONSTANT: - val += (e.Constant(arg), ) - else: - val += (arg, ) - return val - - -def check_formula(operation, a, b=None, func_flag=False): - '''Checks if quantity being calculated is already in memory - - Using the formula string created for each operation as a key, the - register of previously calculated operations is checked. If the - quantity does exist, the previously calculated object is returned. - ''' - import qexpy.error as e - - op_string = { - sin: 'sin', cos: 'cos', tan: 'tan', csc: 'csc', sec: 'sec', sqrt: 'sqrt', - cot: 'cot', exp: 'exp', log: 'log', add: '+', sub: '-', - mul: '*', div: '/', power: '**', 'neg': '-', asin: 'asin', - acos: 'acos', atan: 'atan', } - - op = op_string[operation] - - # check_formula is not behanving properly, requires overwrite, disabled - return None - - if func_flag is False: - if a.info["Formula"] + op + b.info["Formula"] in \ - e.ExperimentalValue.formula_register: - ID = e.ExperimentalValue.formula_register[ - a.info["Formula"] + op + b.info["Formula"]] - return e.ExperimentalValue.register[ID] - - else: - if op + '(' + a.info["Formula"] + ')' in\ - e.ExperimentalValue.formula_register: - ID = e.ExperimentalValue.formula_register[ - op + '(' + a.info["Formula"] + ')'] - return e.ExperimentalValue.register[ID] diff --git a/qexpy/fitting.py b/qexpy/fitting.py deleted file mode 100644 index e1d60fe..0000000 --- a/qexpy/fitting.py +++ /dev/null @@ -1,784 +0,0 @@ -import scipy.optimize as sp -import numpy as np -import qexpy as q -import qexpy.error as qe -import qexpy.utils as qu -import pandas as pd -from math import pi -import re -import warnings - -ARRAY = qu.array_types - - -def Rlinear(x, *pars): - '''Linear function p[0]+p[1]*x - ''' - return pars[0]+pars[1]*x - -def Rpolynomial(x, *pars): - '''Function for a polynomial of nth order, requiring n pars, - p[0]+p[1]*x+p[2]x^2+...''' - #Using Horner's method https://en.wikipedia.org/wiki/Horner%27s_method - #should be faster - result = 0 - for par in reversed(pars): - result = par + result * x - return result - #Straight up polynomal: - # poly = 0 - # for i in range(len(pars)): - # poly += pars[i]*x**i - # return poly - -def Rexp(x, *pars): - '''Function for a decaying exponential p[0]*exp(-x*p[1])''' - return (0 if pars[1]==0 else pars[0]*np.exp(-x*pars[1]) ) - -def Rgauss(x, *pars): - '''Function for a Gaussian p[2]*Gaus(p[0],p[1])''' - mean = pars[0] - std = pars[1] - norm = pars[2] - return (0 if std==0 else norm*(2*pi*std**2)**(-0.5)*np.exp(-0.5*(x-mean)**2/std**2)) - -class XYFitter: - '''A class to fit an XYDataSet to a function/model using scipy.optimize - - :param model: The model (linear, gaussian...) to be fit to the data. - :type model: Function - :param parguess: Guesses for the parameters of the fit. - :type parguess: list - :param name: The name of the fit. - :type name: str - :param sigmas: The number of sigmas to show in the error band. - :type sigmas: int - ''' - - def __init__(self, model = None, parguess=None, name=None, sigmas=1.0): - self.xydataset=None - self.initialize_fit_function(model, parguess, name) - self.sigmas = sigmas - - def set_fit_func(self, func, npars, funcname=None, parguess=None): - '''Set the fit function and the number of parameter, given - the expected number of parameters, npars - - :param func: The fit function. - :type func: Function - :param npars: The number of parameters of the fit function. - :type npars: int - :param funcname: The name of the fit function. - :type funcname: str - :param parguess: The best guesses for the values of the parameters of the fit. - :type parguess: list - ''' - - self.fit_function=func - self.fit_npars=npars - self.fit_function_name="custom" if funcname is None else funcname - if parguess is None or len(parguess)!=npars: - self.parguess = npars*[1] - else: - self.parguess = parguess - - #self.fit_pars = MeasurementArray(self.fit_npars) - - def initialize_fit_function(self, model=None, parguess=None, name=None): - '''Set the model and parameter guess. - - :param model: The fit function. - :type model: Function - :param parguess: The best guesses for the values of the parameters of the fit. - :type parguess: list - :param name: The name of the fit function. - :type name: str - ''' - - wlinear = ('linear', 'Linear', 'line', 'Line',) - wgaussian = ('gaussian', 'Gaussian', 'Gauss', 'gauss', 'normal',) - wexponential = ('exponential', 'Exponential', 'exp', 'Exp',) - - if model is None: - fit_name = name if name else 'linear' - self.set_fit_func(func=Rlinear,npars=2,funcname=fit_name,parguess=parguess) - - elif isinstance(model, str): - if model in wlinear: - fit_name = name if name else 'linear' - self.set_fit_func(func=Rlinear,npars=2,funcname=fit_name,parguess=parguess) - elif model in wgaussian: - fit_name = name if name else 'gaussian' - self.set_fit_func(func=Rgauss,npars=3,funcname=fit_name,parguess=parguess) - elif model in wexponential: - fit_name = name if name else 'exponential' - self.set_fit_func(func=Rexp,npars=2,funcname=fit_name,parguess=parguess) - elif 'pol' in model or 'Pol' in model: - match = re.findall('[0-9]+$', model) - if len(match): - degree = int(match[0])+1 - else: - print('''Please provide the degree of the polynomial at the end of the string, - using linear by default.''') - degree = 2 - fit_name = name if name else 'degree_{}_polynomial'.format(degree-1) - self.set_fit_func(func=Rpolynomial,npars=degree,funcname=fit_name,parguess=parguess) - else: - print("Unrecognized model string: "+model+", defaulting to linear") - fit_name = name if name else 'linear' - self.set_fit_func(func=Rlinear,npars=2,funcname=fit_name,parguess=parguess) - else: - import inspect - if not inspect.isfunction(model): - print("Error: model function should be in form: def model(x, *pars)") - return - argspec = inspect.getargspec(model) - if len(argspec[0])!=1: - print("Error: model function should be in form: def model(x, *pars)") - return - if argspec[1] is None: - print("Error: model function should be in form: def model(x, *pars)") - return - if parguess is None: - print("Error: must specify a guess for a custom function") - return - - self.set_fit_func(func=model, npars=len(parguess), funcname="custom", parguess=parguess) - - @property - def sigmas(self): - '''Returns the mean of a Measurement object. - - :setter: Sets the number of sigmas to show in the error band. - :getter: Returns the number of sigmas shown in the error band. - :type: int - ''' - return self._sigmas - - @sigmas.setter - def sigmas(self, sigma): - '''Sets the mean of a Measurement object. - ''' - if(type(sigma) in qe.ExperimentalValue.CONSTANT and float(sigma).is_integer()): - self._sigmas = sigma - else: - print("Simgas parameter must be a whole number, using sigmas=1") - self._sigmas = 1.0 - - def fit(self, dataset, fit_range=None, fit_count=0, name=None): - ''' Perform a fit of the fit_function to a data set. - - :param dataset: The dataset to be fit. - :type dataset: XYDataSet - :param fit_range: The range to plot the fit on. - :type fit_range: list - :param fit_count: The number of the fit. - :type fit_count: int - :param name: The name of the fit function. - :type name: str - - :returns: The parameters of the fit. - :rtype: Measurement_Array - ''' - if self.fit_function is None: - print("Error: fit function not set!") - return - - #Grab the data - xdata = dataset.xdata - ydata = dataset.ydata - xerr = dataset.xerr - yerr = dataset.yerr - - nz = np.count_nonzero(yerr) - if nz < ydata.size and nz != 0: - print("Warning: some errors on data are zero, switching to MC errors") - dataset.y.error_method="MC" - yerr = dataset.y.stds - #now, check again - nz = np.count_nonzero(yerr) - if nz < ydata.size and nz != 0: - yerr[yerr == 0] = ydata[yerr == 0]/1000000 - #We're ok, modify the errors in the dataset to be the MC ones - else: - dataset.yerr = yerr - - #If user specified a fit range, reduce the data: - if type(fit_range) in ARRAY and len(fit_range) is 2: - indices = np.where(np.logical_and(xdata>=fit_range[0], xdata<=fit_range[1])) - xdata=xdata[indices] - ydata=ydata[indices] - xerr=xerr[indices] - yerr=yerr[indices] - - #if the x errors are not zero, convert them to equivalent errors in y - #TODO: check the math on this... - - #The maximum number of function evaluations - maxfev = 200 *(xdata.size+1) if q.settings["fit_max_fcn_calls"] == -1 else q.settings["fit_max_fcn_calls"] - try: - warns = [] - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", sp.OptimizeWarning) - self.fit_pars, self.fit_pcov = sp.curve_fit(self.fit_function, xdata, ydata, - sigma=yerr, p0=self.parguess, - maxfev=maxfev) - warns = w - if len(warns) > 0 and (self.fit_function==Rlinear or self.fit_function==Rpolynomial): - p, V = np.polyfit(x=xdata, y=ydata, deg=self.fit_npars-1, cov=True, w=1/yerr) - self.fit_pars = p[::-1] - self.fit_pcov = np.flipud(np.fliplr(V)) - - self.fit_pars_err = np.sqrt(np.diag(self.fit_pcov)) - except RuntimeError: - print("Error: Fit could not converge; are the y errors too small? Is the function defined?") - print("Is the parameter guess good?") - return None - - # Use derivative method to factor x error into fit - if xerr.nonzero()[0].size: - yerr_eff = np.sqrt((yerr**2 + np.multiply(xerr, num_der(lambda x: self.fit_function(x, *self.fit_pars), xdata))**2)) - - try: - warns = [] - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", sp.OptimizeWarning) - self.fit_pars, self.fit_pcov = sp.curve_fit(self.fit_function, xdata, ydata, - sigma=yerr_eff, p0=self.parguess, - maxfev=maxfev) - warns = w - if len(warns) == 1 and (self.fit_function == Rlinear or self.fit_function == Rpolynomial): - p, V = np.polyfit(x=xdata, y=ydata, deg=self.fit_npars-1, cov=True, w=1/yerr) - self.fit_pars = p[::-1] - self.fit_pcov = np.flipud(np.fliplr(V)) - - self.fit_pars_err = np.sqrt(np.diag(self.fit_pcov)) - except RuntimeError: - print("Error: Fit could not converge; are the y errors too small? Is the function defined?") - print("Is the parameter guess good?") - return None - - #this should already be true, but let's be sure: - self.fit_npars=self.fit_pars.size - - #This is to catch the case of scipy.optimize failing to determin - #the covariance matrix - - for i in range(self.fit_npars): - if self.fit_pars_err[i] == float('inf') or self.fit_pars_err[i] == float('nan'): - #print("Warning: Error for fit parameter",i,"cannot be trusted") - self.fit_pars_err[i] = 0 - for j in range(self.fit_npars): - if self.fit_pcov[i][j] == float('inf') or self.fit_pcov[i][j] == float('nan'): - #print("Warning: Covariance between parameters",i,j,"cannot be trusted") - self.fit_pcov[i][j]=0. - - - parnames = dataset.name+"_"+self.fit_function_name+"_fit{}".format(fit_count)+"_fitpars" - self.fit_parameters = qe.MeasurementArray(self.fit_npars,name=parnames) - - for i in range(self.fit_npars): - if self.fit_function_name is 'gaussian': - if i is 0: - name = 'mean' - elif i is 1: - name = 'sigma' - elif i ==2: - name = 'normalization' - elif self.fit_function_name is 'linear': - if i is 0: - name = 'intercept' - elif i is 1: - name = 'slope' - elif self.fit_function_name is 'exponential': - if i is 0: - name = 'amplitude' - elif i is 1: - name = 'decay-constant' - else: - name = 'par%d' % (i) - name = parnames +"_"+name - self.fit_parameters[i]= qe.Measurement(self.fit_pars[i], self.fit_pars_err[i], name=name) - - for i in range(self.fit_npars): - for j in range(i+1, self.fit_npars): - self.fit_parameters[i].set_covariance(self.fit_parameters[j], - self.fit_pcov[i][j]) - - #Calculate the residuals: - yfit = self.fit_function(dataset.xdata, *self.fit_pars) - self.fit_yres = qe.MeasurementArray( (dataset.ydata-yfit), dataset.yerr) - #Calculate the chi-squared: - self.fit_chi2 = 0 - for i in range(xdata.size): - if self.fit_yres[i].std !=0: - self.fit_chi2 += (self.fit_yres[i].mean/self.fit_yres[i].std)**2 - self.fit_ndof = self.fit_yres.size-self.fit_npars-1 - - return self.fit_parameters - - - -def DataSetFromFile(filename, xcol=0, ycol=1, xerrcol=2, yerrcol=3, delim= ' ', - data_name=None, xname=None, xunits=None, yname=None, yunits=None, - is_histogram=False): - '''Create an XYDataSet from a file, where the data is organized into 4 columns delimited - by delim. User can specify which columns contain what information, the default is - x,y,xerr,yerr. User MUST specify if xerr or yerr are missing by setting those columns to - 'None', and the method will automatically assign error of zero. - - :param filename: The name of the file to open. - :type filename: str - :param xcol: The index of the column containing the x data. - :type xcol: int - :param ycol: The index of the column containing the y data. - :type ycol: int - :param xerrcol: The index of the column containing the x error data. - :type xerrcol: int - :param yerrcol: The index of the column containing the y error data. - :type yerrcol: int - :param delim: The delimiter in the text file. - :type delim: str - :param data_name: The name of the data. - :type param: str - :param xname: The name of the x axis. - :type xname: str - :param xunits: The units of the x values. - :type xunits: str - :param yname: The name of the y axis. - :type yname: str - :param yunits: The units of the y values. - :type yunits: str - :param is_histogram: Whether the data is a histogram. - :type is_histogram: bool - - :returns: An XYDataSet containing the x and y data. - :rtype: XYDataSet - ''' - data = np.loadtxt(filename, delimiter=delim) - xdata = data[:,xcol] - - if not is_histogram: - _ydata = data[:,ycol] - if xerrcol!= None: - xerrdata = data[:,xerrcol] - else: - xerrdata = np.zeros(xdata.size) - - if yerrcol!= None: - yerrdata = data[:,yerrcol] - else: - yerrdata = np.zeros(ydata.size) - else: - _ydata=None - - return XYDataSet(xdata, ydata=_ydata, xerr=xerrdata, yerr=yerrdata, data_name=data_name, - xname=xname, xunits=xunits, yname=yname, yunits=yunits, - is_histogram=is_histogram) - -class XYDataSet: - '''An XYDataSet contains a paired set of Measurement_Array Objects, - typically, a set of x and y values to be used for a Plot, as well - as a method to fit that dataset. If the data set is fit multiple times - the various fits are all recorded in a list of XYFitter objects. - One can also construct an XYDataSet from histogram data, which then - gets converted to equivalent X and Y measurements. - - :param xdata: The data to be plotted on the x axis. - :type xdata: array, Measurement_Array, XYDataSet - :param ydata: The data to be plotted on the y axis. - :type ydata: array, Measurement_Array - :param xerr: The error on the data on the x axis. - :type xerr: array - :param yerr: The error on the data on the y axis. - :type yerr: array - :param data_name: The name of the data to be plotted. - :type data_name: str - :param xname: The name x axis. - :type xname: str - :param xunits: The units of the x axis. - :type xname: str - :param yname: The name y axis. - :type xname: str - :param yunits: The units of the y axis. - :type yunits: str - :param is_histogram: Whether the data is a histogram. - :type is_histogram: bool - :param bins: The number of bins to plot the histogram data in. - :type bins: int - ''' - - #So that each dataset has a unique name (at least by default): - unnamed_data_counter=0 - - def __init__(self, xdata, ydata=None, xerr=None, yerr=None, data_name=None, - xname=None, xunits=None, yname=None, yunits=None, - is_histogram=False, bins=50): - '''Use MeasurementArray() to initialize a dataset''' - if(data_name is None): - self.name = "dataset{}".format(XYDataSet.unnamed_data_counter) - '''A string of the name of the dataset. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - data = q.XYDataSet(x, y) - data.name = 'x vs. y' - print('Name:', data.name) - - .. nboutput:: ipython3 - - Name = x vs. y - ''' - XYDataSet.unnamed_data_counter += 1 - else: - self.name=data_name - - if ydata is None and not is_histogram: - print("Error, if ydata is not given, explicitly specify that this is a histogram") - - elif ydata is None: - #this is a histogram - self.hist_data=xdata - hist, edges = np.histogram(xdata, bins=bins) - self.hist_bins=edges - _xdata = edges[:-1] - _xerr = np.zeros(_xdata.size) - _ydata = hist - _yerr = np.sqrt(hist) - else: - _xdata=xdata - _xerr=xerr - _ydata=ydata - _yerr=yerr - - self.x = qe.MeasurementArray(_xdata,error=_xerr, name=xname, units=xunits) - self.xdata = self.x.means - self.xerr = self.x.stds - self.xunits = self.x.get_units_str() - self.xname = self.x.name - - self.y = qe.MeasurementArray(_ydata,error=_yerr, name=yname, units=yunits) - self.ydata = self.y.means - self.yerr = self.y.stds - self.yunits = self.y.get_units_str() - self.yname = self.y.name - - self.is_histogram=is_histogram - self.bins = bins - - if self.x.size != self.y.size: - print("Error: x and y data should have the same number of points") - #TODO raise an error! - else: - self.npoints = self.x.size - - self.xyfitter = [] - self.fit_pars = [] #stored as Measurement_Array - self.fit_pcov = [] - self.fit_pcorr = [] - self.fit_function = [] - self.fit_function_name = [] - self.fit_npars =[] - self.fit_yres = [] - self.fit_chi2 = [] - self.fit_ndof = [] - self.fit_color = [] - self.nfits=0 - - def fit(self, model=None, parguess=None, fit_range=None, print_results=True, fitcolor=None, name=None, sigmas=1): - '''Fit a data set to a model using XYFitter. Everytime this function - is called on a data set, it adds a new XYFitter to the dataset. This - is to allow multiple functions to be fit to the same data set. - - :param model: The model (linear, gaussian...) to be fit to the data. - :type model: Function - :param parguess: A guess for the values of the parameter of the fit. - :type parguess: list - :param fit_range: The range to plot the fit on. - :type fit_range: list - :param print_results: Whether to print the results of the fit. - :type print_results: bool - :param fitcolor: The color of the fit. - :type fitcolor: str - :param name: The name of the fit. - :type name: str - :param sigmas: The number of sigmas to show in the error band. - :type sigmas: int - - :returns: The parameters of the fit. - :rtype: Measurement_Array - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.fit('linear') - - fig = q.MakePlot(xy) - fig.show() - ''' - fitter = XYFitter(model=model, parguess=parguess, name=name, sigmas=sigmas) - fit_pars = fitter.fit(self, fit_range=fit_range, fit_count=self.nfits) - if(fit_pars is not None): - self.xyfitter.append(fitter) - self.fit_pars.append(fit_pars) - self.fit_pcov.append(fitter.fit_pcov) - self.fit_pcorr.append(cov2corr(fitter.fit_pcov)) - self.fit_npars.append(fit_pars.size) - self.fit_yres.append(fitter.fit_yres) - self.fit_function.append(fitter.fit_function) - self.fit_function_name.append(fitter.fit_function_name) - self.fit_chi2.append(fitter.fit_chi2) - self.fit_ndof.append(fitter.fit_ndof) - self.fit_color.append(fitcolor) # colour of the fit function - self.nfits += 1 - - if print_results: - self.print_fit_results() - - return self.fit_pars[-1] - else: - return None - - def show_table(self, latex=False): - '''Prints the data of the Plot in a formatted table. - - :param latex: Whether to print the data using Latex formatting. - :type show: bool - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.show_table() - ''' - x = self.x - y = self.y - - if latex: - s = ('\\begin{table}[htp]\n' - '\\begin{center}\n' - '\\begin{tabular}{|c|c|} \hline\n') - xerr_const = np.all(x.stds == x.stds[0]) - yerr_const = np.all(y.stds == y.stds[0]) - xtitle = '{\\bf '+x.name+'} ('+(x.get_units_str() if x.get_units_str() else 'units')+')' - xtitle += ' $\pm$ ' + str(x.stds[0]) if xerr_const else '' - ytitle = '{\\bf '+y.name+'} ('+(y.get_units_str() if y.get_units_str() else 'units')+')' - ytitle += ' $\pm$' + str(y.stds[0]) if yerr_const else '' - s += xtitle + ' & ' + ytitle + ' \\\\ \hline\hline \n' - for ind in range(len(x)): - s += str(self.xdata[ind]) + ('' if xerr_const else ' $\pm$ '+str(self.xerr[ind])) + ' & ' - s += str(self.ydata[ind]) + ('' if yerr_const else ' $\pm$ '+str(self.yerr[ind])) + ' \\\\ \hline \n' - s += ('\end{tabular}\n' - '\end{center}\n' - '\caption{} \n' - '\end{table}') - print(s) - - else: - data = [] - for ind in range(len(x)): - data.append([str(self.xdata[ind]) + ' +/- ' + str(self.xerr[ind]), - str(self.ydata[ind]) + ' +/- ' + str(self.yerr[ind])]) - df = pd.DataFrame(data, columns=[x.name, y.name], index=['']*len(x)) - print(df) - - def print_fit_results(self, fitindex=-1): - '''Prints the results of a fit. Includes the data name, fit name, fit - parameter values, correlation matrix and chi-squared. - - :param fitindex: The index of the fit to print. - :type fitindex: int - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.fit("linear") - xy.print_fit_results() - ''' - if self.nfits == 0: - print("no fit results to print") - return - print("-----------------Fit results-------------------") - print("Fit of ",self.name," to ", self.fit_function_name[fitindex]) - print("Fit parameters:") - print(self.fit_pars[fitindex]) - print("\nCorrelation matrix: ") - print(np.array_str(self.fit_pcorr[fitindex], precision=3)) - print("\nchi2/ndof = {:.2f}/{}".format(self.fit_chi2[fitindex],self.fit_ndof[fitindex])) - print("---------------End fit results----------------\n") - - def __str__(self): - theString="" - for i in range(self.xdata.size): - theString += str(self.x[i])+" , "+str(self.y[i])+"\n" - return theString - - def save_textfile(self, filename="dataset.txt", delim=','): - '''Save the data set to a file. - - :param filename: The name of the text file. - :type filename: str - :param delim: The delimiter between entries in the text file. - :type delim: str - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.fit("linear") - xy.save_textfile() - ''' - data = np.ndarray(shape=(self.xdata.size,4)) - data[:,0]=self.xdata - data[:,1]=self.ydata - data[:,2]=self.xerr - data[:,3]=self.xerr - np.savetxt(filename, data, fmt='%.4f', delimiter=delim) - - def clear_fits(self): - '''Remove all fit records. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.fit('linear') - xy.clear_fits() - - fig = q.MakePlot(xy) - fig.show() - ''' - self.xyfitter = [] - self.fit_pars = [] - self.fit_function = [] - self.fit_function_name = [] - self.fit_npars =[] - self.yres = [] - self.nfits=0 - - def get_x_range(self, margin=0): - '''Get range of the x data, including errors and a specified margin. - - :param margin: The margin at either side of the graph. - :type margin: float - - :returns: The x range of the plot. - :rtype: list - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.get_x_range() - ''' - if self.is_histogram: - return [self.xdata.min()-margin,\ - self.xdata.max()+margin] - else: - return [self.xdata.min()-self.xerr.max()-margin,\ - self.xdata.max()+self.xerr.max()+margin] - - def get_y_range(self, margin=0): - '''Get range of the y data, including errors and a specified margin. - - :param margin: The margin at the top and bottom of the graph. - :type margin: float - - :returns: The y range of the plot. - :rtype: list - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.get_y_range() - ''' - if self.is_histogram: - return [self.ydata.min()-margin,\ - self.ydata.max()+margin] - else: - return [self.ydata.min()-self.yerr.max()-margin,\ - self.ydata.max()+self.yerr.max()+margin] - - def get_yres_range(self, margin=0, fitindex=-1): - '''Get range of the y residuals, including errors and a specified margin. - - :param margin: The margin at the top and bottom of the graph. - :type margin: float - :param fitindex: The index of the fit get the residual range for. - :type fitindex: int - - :returns: The y range of the residuals. - :rtype: list - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - xy = q.XYDataSet(x, y) - xy.fit("linear") - xy.get_yres_range() - ''' - return [self.fit_yres[fitindex].means.min()-self.yerr.max()-margin,\ - self.fit_yres[fitindex].means.max()+self.yerr.max()+margin] - -def cov2corr(pcov): - '''Return a correlation matrix given a covariance matrix. - - :param pcov: A covariance matrix. - :type pcov: np.ndarray - - :returns: A correlation matrix corresponding to the covariance matrix. - :rtype: np.ndarray - ''' - sigmas = np.sqrt(np.diag(pcov)) - dim = sigmas.size - pcorr = np.ndarray(shape=(dim,dim)) - - for i in range(dim): - for j in range(dim): - pcorr[i][j]=pcov[i][j] - if sigmas[i] == 0 or sigmas[j] == 0: - pcorr[i][j] = 0. - else: - pcorr[i][j] /= (sigmas[i]*sigmas[j]) - return pcorr - - - -def num_der(function, point, dx=1e-10): - ''' - Returns the first order derivative of a function. - Used in combining xerr and yerr. Used to include - x errors in XYFitter. - - :param function: The function to take the derivative of. - :type function: Function - :param point: The point at which to take the derivative. - :type point: float - :param dx: The width of the interval that the numerical derivative is evaluated on. - :type dx: float - ''' - import numpy as np - point = np.array(point) - return np.divide(function(point+dx)-function(point), dx) diff --git a/qexpy/fitting/__init__.py b/qexpy/fitting/__init__.py new file mode 100644 index 0000000..b82b787 --- /dev/null +++ b/qexpy/fitting/__init__.py @@ -0,0 +1,4 @@ +"""This package contains fitting functions for data sets""" + +from .utils import FitModel +from .fitting import fit diff --git a/qexpy/fitting/fitting.py b/qexpy/fitting/fitting.py new file mode 100644 index 0000000..f439eb4 --- /dev/null +++ b/qexpy/fitting/fitting.py @@ -0,0 +1,291 @@ +"""This module contains curve fitting functions""" + +import inspect +import numpy as np +import scipy.optimize as opt + +from typing import Callable +from inspect import Parameter +from collections import namedtuple +from qexpy.utils.exceptions import IllegalArgumentError +from .utils import FitModelInfo, FitParamConstraints + +import qexpy.data.data as dt +import qexpy.data.datasets as dts +import qexpy.settings.literals as lit +import qexpy.utils as utils + +from . import utils as fut + +# container for the raw outputs of a fit +RawFitResults = namedtuple("RawFitResults", "popt, perr, pcov") + +# container for fit results +FitResults = namedtuple("FitResults", "func, params, residuals, chi2, pcorr") + +ARRAY_TYPES = np.ndarray, list + + +class XYFitResult: + """Stores the results of a curve fit""" + + def __init__(self, **kwargs): + """Constructor for an XYFitResult object""" + + self._dataset = kwargs.pop("dataset") + self._model = kwargs.pop("model") + self._xrange = kwargs.pop("xrange") + + result_func = kwargs.pop("res_func") + result_params = kwargs.pop("res_params") + pcorr = kwargs.pop("pcorr") + + y_fit_res = result_func(self._dataset.xdata) + self._ndof = len(y_fit_res) - len(result_params) - 1 + + y_err = self._dataset.ydata - y_fit_res + + chi2 = sum( + (res.value / err) ** 2 for res, err in zip(y_err, self._dataset.yerr) if err != 0) + + self._result = FitResults(result_func, result_params, y_err, chi2, pcorr) + + def __getitem__(self, index): + return self._result.params[index] + + def __str__(self): + header = "----------------- Fit Results -------------------" + fit_type = "Fit of {} to {}\n".format(self._dataset.name, self._model.name) + res_params = map(str, self._result.params) + res_param_str = "Result Parameter List: \n{}\n".format(",\n".join(res_params)) + corr_matrix = np.array_str(self._result.pcorr, precision=3) + corr_matrix_str = "Correlation Matrix: \n{}\n".format(corr_matrix) + chi2_ndof = "chi2/ndof = {:.2f}/{}\n".format(self._result.chi2, self._ndof) + ending = "--------------- End Fit Results -----------------" + return "\n".join( + [header, fit_type, res_param_str, corr_matrix_str, chi2_ndof, ending]) + + @property + def dataset(self): + """dts.XYDataSet: The dataset used for this fit""" + return self._dataset + + @property + def fit_function(self): + """Callable: The function that fits to this data set""" + return self._result.func + + @property + def params(self): + """List[dt.ExperimentalValue]: The fit parameters of the fit function""" + return self._result.params + + @property + def residuals(self): + """dts.ExperimentalValueArray: The residuals of the fit""" + return self._result.residuals + + @property + def chi_squared(self): + """dt.ExperimentalValue: The goodness of fit represented as chi^2""" + return self._result.chi2 + + @property + def ndof(self): + """int: The degree of freedom of this fit function""" + return self._ndof + + @property + def xrange(self): + """tuple: The xrange of the fit""" + return self._xrange + + +def fit(*args, **kwargs) -> XYFitResult: + """Perform a fit to a data set + + The fit function can be called on an XYDataSet object, or two arrays or MeasurementArray + objects. QExPy provides 5 builtin fit models, which includes linear fit, quadratic fit, + general polynomial fit, gaussian fit, and exponential fit. The user can also pass in a + custom function they wish to fit their dataset on. For non-polynomial fit functions, the + user would usually need to pass in an array of guesses for the parameters. + + Args: + *args: An XYDataSet object or two arrays to be fitted. + + Keyword Args: + model: the fit model given as the string or enum representation of a pre-set model + or a custom callable function with parameters. Available pre-set models include: + "linear", "quadratic", "polynomial", "exponential", "gaussian" + xrange (tuple|list): a pair of numbers indicating the domain of the function + degrees (int): the degree of the polynomial if polynomial fit were chosen + parguess (list): initial guess for the parameters + parnames (list): the names of each parameter + parunits (list): the units for each parameter + dataset: the XYDataSet instance to fit on + xdata : the x-data of the fit + ydata: the y-data of the fit + xerr: the uncertainty on the xdata + yerr: the uncertainty on the ydata + + Returns: + XYFitResult: the result of the fit + + See Also: + :py:class:`~qexpy.data.XYDataSet` + + """ + + result = __try_fit_to_xy_dataset(*args, **kwargs) + if result: + return result + + result = __try_fit_to_xdata_and_ydata(*args, **kwargs) + if result: + return result + + raise IllegalArgumentError( + "Unable to execute fit. Please make sure the arguments provided are correct.") + + +def fit_to_xy_dataset(dataset: dts.XYDataSet, model, **kwargs) -> XYFitResult: + """Perform a fit on an XYDataSet object""" + + fit_model = fut.prepare_fit_model(model) + + if fit_model.name == lit.POLY: + # By default, the degree of a polynomial fit model is 3, because if it were 2, the + # quadratic fit model would've been chosen. The number of parameters is the degree + # of the fit model plus one. (e.g. a degree-1, or linear fit, has 2 params) + new_constraints = FitParamConstraints(kwargs.get("degrees", 3) + 1, False, False) + fit_model = FitModelInfo(fit_model.name, fit_model.func, new_constraints) + + param_info, fit_model = fut.prepare_param_info(fit_model, **kwargs) + + xrange = kwargs.get("xrange", None) + if xrange and utils.validate_xrange(xrange): + x_to_fit = dataset.xdata[(xrange[0] <= dataset.xdata) & (dataset.xdata < xrange[1])] + y_to_fit = dataset.ydata[(xrange[0] <= dataset.xdata) & (dataset.xdata < xrange[1])] + else: + x_to_fit = dataset.xdata + y_to_fit = dataset.ydata + + yerr = y_to_fit.errors if any(err > 0 for err in y_to_fit.errors) else None + + if fit_model.name in [lit.POLY, lit.LIN, lit.QUAD]: + raw_res = __polynomial_fit( + x_to_fit, y_to_fit, fit_model.param_constraints.length - 1, yerr) + else: + raw_res = __curve_fit( + fit_model.func, x_to_fit, y_to_fit, param_info.parguess, yerr) + + # wrap the parameters in MeasuredValue objects + def wrap_param_in_measurements(): + par_res = zip(raw_res.popt, raw_res.perr, param_info.parunits, param_info.parnames) + for param, err, unit, name in par_res: + yield dt.MeasuredValue(param, err, unit=unit, name=name) + + params = list(wrap_param_in_measurements()) + + pcorr = utils.cov2corr(raw_res.pcov) + __correlate_fit_params(params, raw_res.pcov) + + # wrap the result function with the params + result_func = __combine_fit_func_and_fit_params(fit_model.func, params) + + return XYFitResult(dataset=dataset, model=fit_model, res_func=result_func, + res_params=params, pcorr=pcorr, xrange=xrange) + + +def __try_fit_to_xy_dataset(*args, **kwargs): + """Helper function to parse the inputs to a call to fit() for a single XYDataSet""" + + dataset = kwargs.pop("dataset", args[0] if args else None) + model = kwargs.pop("model", args[1] if len(args) > 1 else None) + + if isinstance(dataset, dts.XYDataSet) and model: + return fit_to_xy_dataset(dataset, model, **kwargs) + + return None + + +def __try_fit_to_xdata_and_ydata(*args, **kwargs): + """Helper function to parse the inputs to a call to fit() for separate xdata and ydata""" + + xdata = kwargs.pop("xdata", args[0] if args else None) + ydata = kwargs.pop("ydata", args[1] if len(args) > 1 else None) + model = kwargs.pop("model", args[2] if len(args) > 2 else None) + + if not isinstance(xdata, dts.ExperimentalValueArray): + xdata = np.asarray(xdata) if isinstance(xdata, ARRAY_TYPES) else np.empty(0) + + if not isinstance(ydata, dts.ExperimentalValueArray): + ydata = np.asarray(ydata) if isinstance(ydata, ARRAY_TYPES) else np.empty(0) + + if xdata.size and ydata.size and model: + return fit_to_xy_dataset(dts.XYDataSet(xdata, ydata, **kwargs), model, **kwargs) + + return None + + +def __polynomial_fit(xdata, ydata, degrees, yerr) -> RawFitResults: + """perform a polynomial fit with numpy.polyfit""" + + weights = 1 / yerr if yerr is not None else None + popt, pcov = np.polyfit(xdata.values, ydata.values, degrees, cov=True, w=weights) + perr = np.sqrt(np.diag(pcov)) + return RawFitResults(popt, perr, pcov) + + +def __curve_fit(fit_func, xdata, ydata, parguess, yerr) -> RawFitResults: + """perform a regular curve fit with scipy.optimize.curve_fit""" + + try: + popt, pcov = opt.curve_fit( + fit_func, xdata.values, ydata.values, p0=parguess, sigma=yerr) + + # adjust the fit by factoring in the uncertainty on x + if any(err > 0 for err in xdata.errors): + func = __combine_fit_func_and_fit_params(fit_func, popt) + yerr = 0 if yerr is None else yerr + adjusted_yerr = np.sqrt( + yerr ** 2 + xdata.errors * utils.numerical_derivative(func, xdata.errors)) + + # re-calculate the fit with adjusted uncertainties for ydata + popt, pcov = opt.curve_fit( + fit_func, xdata.values, ydata.values, p0=parguess, sigma=adjusted_yerr) + + except RuntimeError: # pragma: no cover + + # Re-write the error message so that it can be more easily understood by the user + raise RuntimeError( + "Fit could not converge. Please check that the fit model is well defined, and " + "that the parameter guess as well as the y-errors are appropriate.") + + # The error on the parameters + perr = np.sqrt(np.diag(pcov)) + + return RawFitResults(popt, perr, pcov) + + +def __combine_fit_func_and_fit_params(func: Callable, params) -> Callable: + """wraps a function with params to a function of x""" + + result_func = utils.vectorize(lambda x: func(x, *params)) + + # Change signature of the function to match the actual signature + sig = inspect.signature(result_func) + new_sig = sig.replace(parameters=[Parameter("x", Parameter.POSITIONAL_ONLY)]) + result_func.__signature__ = new_sig + + return result_func + + +def __correlate_fit_params(params, corr): + """Apply correlation to the list of parameters with the covariance matrix""" + + for index1, param1 in enumerate(params): + for index2, param2 in enumerate(params[index1 + 1:]): + if param1.error == 0 or param2.error == 0: # pragma: no cover + continue + param1.set_covariance(param2, corr[index1][index2 + index1 + 1]) diff --git a/qexpy/fitting/utils.py b/qexpy/fitting/utils.py new file mode 100644 index 0000000..f45fa00 --- /dev/null +++ b/qexpy/fitting/utils.py @@ -0,0 +1,203 @@ +"""Utility functions for the fit module""" +import functools +import inspect +import warnings + +from collections import namedtuple + +# Contains the name, callable fit function, and the constraints on the fit parameters +from enum import Enum +from numbers import Real +from inspect import Parameter +from typing import Callable, List + +from qexpy.data import operations as op +from qexpy.settings import literals as lit +from qexpy.utils import IllegalArgumentError + +# Contains the name, callable function, and parameter constraints on a fit model +FitModelInfo = namedtuple("FitModelInfo", "name, func, param_constraints") + +# Contains constraints on fit parameters, including the number of params required, a flag +# indicating if there is a variable position argument in the fit function, which indicates +# that the actual number of fit parameters might be higher than what it looks, and a flag +# stating if guesses are required to execute this fit +FitParamConstraints = namedtuple("FitParamConstraints", "length, var_len, guess_required") + +# Contains the parameter information used in a fit +FitParamInfo = namedtuple("FitParamInfo", "parguess, parnames, parunits") + + +class FitModel(Enum): + """QExPy supported pre-set fit models""" + LINEAR = lit.LIN + QUADRATIC = lit.QUAD + POLYNOMIAL = lit.POLY + GAUSSIAN = lit.GAUSS + EXPONENTIAL = lit.EXPO + + +def prepare_fit_model(model) -> FitModelInfo: + """Prepares the fit model and fit function for a fit + + Args: + model: the fit model as is passed into the fit function + + Returns: + model (FitModelInfo): the fit model and all information related to it + + """ + + # First find the name and the callable fit function for the model + if isinstance(model, str) and model in FITTERS: + name, func = model, FITTERS[model] + elif isinstance(model, FitModel): + name, func = model.value, FITTERS[model.value] + elif callable(model): + name, func = "custom", model + else: + raise ValueError( + "Invalid fit model specified! The fit model can be one of the following: " + "one of the pre-set fit models in the form of a string or chosen from the " + "q.FitModel enum, or a custom callable fit function") + + # Now find the number of parameters this fit function has + params = list(inspect.signature(func).parameters.values()) + + if any(arg.kind in [Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD] for arg in params): + raise ValueError("The fit function should not have keyword arguments") + + # If the last param is variable positional, the actual number of params may be higher + var_pos_present = params[-1].kind == Parameter.VAR_POSITIONAL + + # The first argument of the fit function is the variable, only the rest are parameters + nr_of_params = len(params) - 1 + + if nr_of_params == 0: + raise ValueError("The number of parameters in the given fit model is 0!") + + guess_required = name not in [lit.LIN, lit.QUAD, lit.POLY] + constraints = FitParamConstraints(nr_of_params, var_pos_present, guess_required) + + return FitModelInfo(name, func, constraints) + + +def prepare_param_info(model: FitModelInfo, **kwargs) -> (FitParamInfo, FitModelInfo): + """Prepares the parameter information for a fit function + + Args: + model (FitModelInfo): the fit model used for this fit + + Keyword Args: + parguess: the vector of parameter guesses + parnames: the vector of parameter names + parunits: the vector of parameter units + + Returns: + The first return is a FitParamInfo which includes: parguess, parnames, parunits, + all read from kwargs and validated against the fit model and constraints. The last + return value would be the updated FitModelInfo based on all the parameter info. + + """ + + constraints = model.param_constraints + + # check if guess parameters are provided + parguess = kwargs.get("parguess", None) + if constraints.guess_required and parguess is None: + warnings.warn( + "You have not provided any guesses of parameters for a {} fit. For this type " + "of fitting, it is recommended to specify parguess".format(model.name)) + + validate_param_info(parguess, "parguess", constraints) + + if parguess is not None: + # The length of the parguess vector dictates the number of parameters + constraints = FitParamConstraints(len(parguess), False, True) + model = FitModelInfo(model.name, model.func, constraints) + + if parguess and any(not isinstance(guess, Real) for guess in parguess): + raise TypeError("The guess parameters provided are not real numbers!") + + parnames = kwargs.get("parnames", prepare_param_names(model)) + validate_param_info(parnames, "parnames", constraints) + if parnames and any(not isinstance(name, str) for name in parnames): + raise TypeError("The parameter names provided are not strings!") + + parunits = kwargs.get("parunits", [""] * constraints.length) + validate_param_info(parunits, "parunits", constraints) + if parunits and any(not isinstance(unit, str) for unit in parunits): + raise TypeError("The parameter units provided are not strings!") + + return FitParamInfo(parguess, parnames, parunits), model + + +def validate_param_info(info, info_name: str, constraints: FitParamConstraints): + """Validates the param information is valid and matches the fit model""" + + if not info: + return # skip if there's nothing to check + + if not isinstance(info, (list, tuple)): + raise IllegalArgumentError("\"{}\" has to be a list or a tuple.".format(info_name)) + + if constraints.var_len and len(info) < constraints.length: + raise ValueError( + "The length of \"{}\" ({}) doesn't match the number of parameters in the fit " + "function ({} or higher)".format(info_name, len(info), constraints.length)) + + if not constraints.var_len and len(info) != constraints.length: + raise ValueError( + "The length of \"{}\" ({}) doesn't match the number of parameters in the fit " + "function (expecting {})".format(info_name, len(info), constraints.length)) + + +def prepare_param_names(model: FitModelInfo): + """Finds the default param names for pre-set fit models""" + + if model.name in DEFAULT_PARNAMES: + return DEFAULT_PARNAMES.get(model.name) + + nr_of_params = model.param_constraints.length + + # check the function signature for custom functions + par_names = get_param_names_from_signature(model.func, nr_of_params) + + return par_names if par_names else [""] * nr_of_params + + +def get_param_names_from_signature(func: Callable, nr_of_params: int) -> List: + """Inspect the signature of the custom function for parameter names""" + + # get all arguments to the function except for the first one (the variable) + params = list(inspect.signature(func).parameters.values())[1:] + + # the last parameter could be variable, so we process the rest of the parameters first + param_names = list(param.name for param in params[:-1]) + + # now process the last parameter + if params[-1].kind == Parameter.VAR_POSITIONAL: + left_overs = nr_of_params - len(param_names) # how many params left to be filled + last_params = list("{}_{}".format(params[-1].name, idx) for idx in range(left_overs)) + else: + last_params = [params[-1].name] + + param_names.extend(last_params) + + return param_names + + +FITTERS = { + lit.LIN: lambda x, a, b: a * x + b, + lit.QUAD: lambda x, a, b, c: a * x ** 2 + b * x + c, + lit.POLY: lambda x, *coeffs: functools.reduce(lambda a, b: a * x + b, reversed(coeffs)), + lit.EXPO: lambda x, c, a: c * op.exp(-a * x), + lit.GAUSS: lambda x, norm, mean, std: norm / op.sqrt( + 2 * op.pi * std ** 2) * op.exp(-1 / 2 * (x - mean) ** 2 / std ** 2) +} + +DEFAULT_PARNAMES = { + lit.LIN: ["slope", "intercept"], + lit.EXPO: ["amplitude", "decay constant"], + lit.GAUSS: ["normalization", "mean", "std"] +} diff --git a/qexpy/plot_utils.py b/qexpy/plot_utils.py deleted file mode 100644 index 9643dd4..0000000 --- a/qexpy/plot_utils.py +++ /dev/null @@ -1,188 +0,0 @@ -import numpy as np -import qexpy.error as qe -import qexpy.utils as qu - -CONSTANT = qu.number_types -ARRAY = qu.array_types - -def bk_plot_dataset(figure, dataset, residual=False, color='black', fit_index=-1): - '''Given a bokeh figure, this will add data points with errors from a dataset. - - :param figure: The Bokeh figure that the dataset is being added to. - :type figure: bokeh.plotting.figure - :param dataset: The dataset to be added to the figure. - :type dataset: XYDataSet - :param residual: Whether the dataset is residual data. - :type residual: bool - :param color: The color of the dataset. - :type color: str - :param fit_index: The index of the fit. - :type fit_index: int - ''' - - xdata = dataset.xdata - xerr = dataset.xerr - data_name = dataset.name - - index = fit_index if fit_index < dataset.nfits else -1. - - if residual is True and dataset.nfits>0: - ydata = dataset.fit_yres[index].means - yerr = dataset.fit_yres[index].stds - data_name=None - else: - ydata = dataset.ydata - yerr = dataset.yerr - - bk_add_points_with_error_bars(figure, xdata, ydata, xerr, yerr, color, data_name) - -def bk_add_points_with_error_bars(figure, xdata, ydata, xerr=None, yerr=None, color='black', data_name='dataset'): - '''Add data points to a bokeh plot. If the errors are given as numbers, - the same error bar is assume for all data points. - - :param figure: The Bokeh figure that the dataset is being added to. - :type figure: bokeh.plotting.figure - :param xdata: The data to be plotted on the x axis. - :type xdata: array, Measurement_Array - :param ydata: The data to be plotted on the y axis. - :type ydata: array, Measurement_Array - :param xerr: The error on the x data. - :type xerr: array, Measurement_Array - :param yerr: The error on the y data. - :type yerr: array, Measurement_Array - :param color: The color of the points to be plotted. - :type color: str - :param data_name: The name of the data set. - :type data_name: str - ''' - - - _xdata, _ydata, _xerr, _yerr = make_np_arrays(xdata,ydata,xerr,yerr) - - if _xdata.size != _ydata.size: - print("Error: x and y data must have the same number of points") - return None - - #Draw points: - figure.circle(_xdata, _ydata, color=color, size=4, legend=data_name) - - if isinstance(_xerr,np.ndarray) or isinstance(_yerr,np.ndarray): - #Add error bars - for i in range(_xdata.size): - - xcentral = [_xdata[i], _xdata[i]] - ycentral = [_ydata[i], _ydata[i]] - - #x error bar, if the xerr argument was not none - if xerr is not None: - xends = [] - if _xerr.size == _xdata.size and _xerr[i]>0: - xends = [_xdata[i]-_xerr[i], _xdata[i]+_xerr[i]] - elif _xerr.size == 1 and _xerr[0]>0: - xends = [_xdata[i]-_xerr[0], _xdata[i]+_xerr[0]] - else: - pass - - if len(xends)>0: - figure.line(xends,ycentral, color=color) - #winglets on x error bar: - figure.rect(x=xends, y=ycentral, height=5, width=0.2, - height_units='screen', width_units='screen', - color=color) - - #y error bar - if yerr is not None: - yends=[] - if _yerr.size == _ydata.size and _yerr[i]>0: - yends = [_ydata[i]-_yerr[i], _ydata[i]+_yerr[i]] - elif _yerr.size == 1 and _yerr[i]>0: - yends = [_ydata[i]-_yerr[0], _ydata[i]+_yerr[0]] - else: - pass - if len(yends)>0: - figure.line(xcentral, yends, color=color) - #winglets on y error bar: - figure.rect(x=xcentral, y=yends, height=0.2, width=5, - height_units='screen', width_units='screen', - color=color) - -def make_np_arrays(*args): - '''Return a tuple where all of the arguments have been converted into - numpy arrays.''' - np_tuple=() - for arg in args: - if isinstance(arg,np.ndarray): - np_tuple = np_tuple +(arg,) - elif isinstance(arg, list): - np_tuple = np_tuple +(np.array(arg),) - elif isinstance(arg, qu.number_types): - np_tuple = np_tuple +(np.array([arg]),) - else: - np_tuple = np_tuple +(None,) - return np_tuple - -def bk_plot_function(figure, function, xdata, pars=None, n=100, legend_name=None, color='black', sigmas=1, errorbandfactor=1.0): - '''Plot a function evaluated over the range of xdata - xdata only needs 2 values - The function can be either f(x) or f(x, *pars). In the later case, if pars is - a Measurement_Array (e.g. the parameters from a fit), then an error band is also - added to the plot, corresponding to varying the parameters within their uncertainty. - The errorbandfactor can be used to choose an error band that is larger than 1 standard - deviation. - - :param figure: The Bokeh figure that the dataset is being added to. - :type figure: bokeh.plotting.figure - :param function: The function to be plotted. - :type function: Function - :param xdata: The range on which to plot the function. - :type xdata: list - :param pars: The parameters of the function being plotted. - :type pars: Measurement_Array, array - :param n: The number of points on the range to evaluate the function at. - :type n: int - :param legend_name: The name of the function. - :type legend_name: str - :param color: The color of the function. - :type color: str - :param sigmas: The number of sigmas to show in the error band. - :type sigmas: int - :param errorbandfactor: The stretch factor of the error band. - :type errorbandfactor: float - - :returns: The fit function and error band if applicable. - :rtype: bokeh.models.renderers.GlyphRenderer - ''' - - xvals = np.linspace(min(xdata), max(xdata), n) - - if pars is None: - fvals = function(xvals) - elif isinstance(pars, qe.Measurement_Array): - #TODO see if this can be sped up more - recall = qe.Measurement.minmax_n - qe.Measurement.minmax_n=1 - fmes = function(xvals, *(pars)) - fvals = fmes.means - qe.Measurement.minmax_n=recall - elif isinstance(pars,(list, np.ndarray)): - fvals = function(xvals, *pars) - else: - print("Error: unrecognized parameters for function") - pass - line = figure.line(xvals, fvals, legend=legend_name, line_color=color) - - #Add error band - if isinstance(pars, qe.Measurement_Array): - for i in range(1, int(sigmas)+1): - ymax = fmes.means+i*errorbandfactor*fmes.stds - ymin = fmes.means-i*errorbandfactor*fmes.stds - - patch = figure.patch(x=np.append(xvals,xvals[::-1]),y=np.append(ymax,ymin[::-1]), - fill_alpha=0.3/(sigmas-0.3*(i-1)), - fill_color=color, - line_alpha=0.0, - legend=legend_name) - - return line, patch - else: - return line, None - diff --git a/qexpy/plotting.py b/qexpy/plotting.py deleted file mode 100644 index 1ade764..0000000 --- a/qexpy/plotting.py +++ /dev/null @@ -1,1539 +0,0 @@ -import numpy as np -import qexpy as q -import qexpy.error as qe -import qexpy.utils as qu -import qexpy.fitting as qf -import qexpy.plot_utils as qpu - -import bokeh.plotting as bp -import bokeh.io as bi -import bokeh.layouts as bl -import bokeh.models as mo - -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec - -import pandas as pd - -from ipywidgets import interact - -CONSTANT = qu.number_types -ARRAY = qu.array_types - - - -def MakePlot(xdata=None, ydata=None, xerr=None, yerr=None, data_name=None, - dataset=None, xname=None, xunits=None, yname=None, yunits=None): - '''Use this function to create a plot object, by providing either arrays - corresponding to the x and y data, Measurement_Arrays for x and y, or - an XYDataSet. If providing a dataset, it can be specified as either the - x argument or the dataset argument. - - :param xdata: The data to be plotted on the x axis. - :type xdata: array, Measurement_Array, XYDataSet - :param ydata: The data to be plotted on the y axis. - :type ydata: array, Measurement_Array - :param xerr: The error on the data on the x axis. - :type xerr: array - :param yerr: The error on the data on the y axis. - :type yerr: array - :param data_name: The name of the data to be plotted. - :type data_name: str - :param dataset: An XYDataSet to be plotted. - :type dataset: XYDataSet - :param xname: The name x axis. - :type xname: str - :param xunits: The units of the x axis. - :type xname: str - :param yname: The name y axis. - :type xname: str - :param yunits: The units of the y axis. - :type yunits: str - ''' - - if xdata is None and ydata is None and dataset is None: - return Plot(None) - - elif xdata is not None and ydata is None: - #assume that x is a dataset: - if isinstance(xdata, qf.XYDataSet): - if xname is not None and isinstance(xname, str): - xdata.xname = xname - if yname is not None and isinstance(yname, str): - xdata.yname = yname - if xunits is not None and isinstance(xunits, str): - xdata.xunits = xunits - if yunits is not None and isinstance(yunits, str): - xdata.yunits = yunits - if data_name is not None and isinstance(data_name, str): - xdata.name = data_name - return Plot(xdata) - else: - print("Must specify x AND y or dataset, returning empty plot") - return Plot(None) - - elif dataset is not None: - if xname is not None and isinstance(xname, str): - dataset.xname = xname - if yname is not None and isinstance(yname, str): - dataset.yname = yname - if xunits is not None and isinstance(xunits, str): - dataset.xunits = xunits - if yunits is not None and isinstance(yunits, str): - dataset.yunits = yunits - if data_name is not None and isinstance(data_name, str): - dataset.name = data_name - return Plot(dataset) - - elif (xdata is not None and ydata is not None): - ds = qf.XYDataSet(xdata, ydata, xerr=xerr, yerr=yerr, data_name=data_name, - xname=xname, xunits=xunits, yname=yname, yunits=yunits) - return Plot(dataset = ds) - - else: - return Plot(None) - -class Plot: - '''Object for plotting and fitting datasets built on - Measurement_Arrays - - The Plot object holds a list of XYDataSets, themselves containing - pairs of MeasurementArrays holding x and y values to be plotted and - fitted. The Plot object uses bokeh or matplotlib to display the data, - along with fit functions, and user-specified functions. One should configure - the various aspects of the plot, and then call the show() function - which will actually build the plot object and display it. - - :param dataset: The dataset to be plotted. - :type dataset: XYDataSet - ''' - - def __init__(self, dataset=None): - ''' - Constructor to make a plot based on a dataset. - - :param dataset: The dataset to be plotted. - :type dataset: XYDataSet - ''' - - #Colors to be used for coloring elements automatically - self.color_palette = q.settings["plot_color_palette"] - self.color_count = 0 - - #Dimensions of the figure in pixels - self.dimensions_px = [ q.settings["plot_fig_x_px"], - q.settings["plot_fig_y_px"] ] - #Screen dots per inch, required for mpl - self.screen_dpi = q.settings["plot_screen_dpi"] - - #Where to save the plot - self.save_filename = 'myplot.html' - '''A string of what the name of the plot will be when saved. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - fig = q.MakePlot(x, y) - fig.fit('linear') # Will save file as 'linear_fit.html' - fig.show() - ''' - - #How big to draw error bands on fitted functions - self.errorband_sigma = 1 - '''An integer of the number of sigmas to show in the error band. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - fig = q.MakePlot(x, y) - fig.errorband_sigma = 2 # Will draw 2 sigmas on the fit - fig.fit('linear') - - fig.show() - ''' - #whether to show residuals - self.show_residuals=False - #whether to include text labels on plot with fit parameters - self.show_fit_results=True - '''A boolean of whether to show the results from fitting the data. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6, 7, 8], error=0.1) - - fig = q.MakePlot(x, y) - fig.show_fit_results = False # Will prevent the linear fit from printing results - - fig.show() - ''' - self.fit_results_x_offset=0 - self.fit_results_y_offset=0 - - #location of legend - self.bk_legend_location = "top_left" - '''A string of the location of the legend when using Bokeh to plot. - Acceptable values can be found here: http://bokeh.pydata.org/en/latest/docs/user_guide/styling.html#inside-the-plot-area - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6, 7, 8], error=0.1) - q.plot_engine = 'bokeh' - - fig = q.MakePlot(x, y) - fig.bk_legend_location = 'top_right' # Moves the legend to the top right - fig.show() - ''' - self.bk_legend_orientation = "vertical" - self.mpl_legend_location = "upper left" - '''A string of the location of the legend when using MatPlotLib to plot. - Acceptable values can be found here: https://matplotlib.org/api/legend_api.html#matplotlib.legend.Legend - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6, 7, 8], error=0.1) - q.plot_engine = 'mpl' - - fig = q.MakePlot(x, y) - fig.mpl_legend_location = 'upper right' # Moves the legend to the top right - fig.show() - ''' - self.mpl_show_legend = True - - #The data to be plotted are held in a list of datasets: - self.datasets=[] - #Each data set has a color, so that the user can choose specific - #colors for each dataset - self.datasets_colors=[] - - #Functions to be plotted are held in a list of functions - self.user_functions = [] - self.user_functions_count=0 - self.user_functions_pars = [] - self.user_functions_names = [] - self.user_functions_colors = [] - self.user_functions_sigmas = [] - - #Add margins to the range of the plot - self.x_range_margin = 0.5 - self.y_range_margin = 0.5 - #Default range for the axes - self.x_range = [0,1] - '''A list of the minimum x value and the maximum x value. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6, 7, 8], error=0.1) - - fig = q.MakePlot(x, y) - fig.x_range = [0, 5] # Will plot from x=0 to x=5 - fig.show() - ''' - self.y_range = [0,1] - '''A list of the minimum y value and the maximum y value. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6, 7, 8], error=0.1) - - fig = q.MakePlot(x, y) - fig.y_range = [4, 9] # Will plot from y=4 to y=9 - fig.show() - ''' - self.yres_range = [0,0.1] - '''A list of the minimum y value and the maximum y value for the residuals. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - fig = q.MakePlot(x, y) - fig.fit('linear') - fig.show_residuals = True - fig.yres_range = [-0.5, 0.5] # Will plot residuals from y=-0.5 to y=0.5 - fig.show() - ''' - - #Labels for axes - self.axes = {'xscale': 'linear', 'yscale': 'linear'} - self.labels = { - 'title': "y as a function of x", - 'xtitle': "x", - 'ytitle': "y"} - - self.lines = {'x':[], - 'y':[]} - - if dataset != None: - self.datasets.append(dataset) - self.datasets_colors.append(self._get_color_from_palette()) - self.initialize_from_dataset(dataset) - else: - self.initialized_from_dataset = False - - def show_table(self, latex=False): - '''Prints the data of the Plot in a formatted table. - - :param latex: Whether to print the data using Latex formatting. - :type show: bool - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - figure.show_table() - ''' - dataset = self.datasets[-1] - dataset.show_table(latex=latex) - - def initialize_from_dataset(self, dataset): - '''Initialize axes labels and ranges from the dataset''' - self.labels = { - 'title': dataset.name, - 'xtitle': dataset.xname +' ['+dataset.xunits+']', - 'ytitle': dataset.yname +' ['+dataset.yunits+']'} - - #Get the range from the dataset (will include the margin) - self.set_range_from_dataset(dataset) - self.initialized_from_dataset = True - - def _get_color_from_palette(self): - '''Automatically select a color from the palette and increment - the color counter''' - self.color_count += 1 - if self.color_count>len(self.color_palette): - self.color_count = 1 - return self.color_palette[self.color_count-1] - - def check_datasets_color_array(self): - '''Make sure that color array is the same length as dataset array''' - if len(self.datasets) == len(self.datasets_colors): - return - elif len(self.datasets) > len(self.datasets_colors): - for i in range(len(self.datasets_colors), len(self.datasets)): - self.datasets_colors.append(self._get_color_from_palette()) - elif len(self.datasets_colors) > len(self.datasets): - while len(self.datasets_colors) != len(self.datasets): - self.datasets_colors.pop() - else: pass - - def check_user_functions_color_array(self): - '''Make sure that color array is the same length as function array''' - if len(self.user_functions) == len(self.user_functions_colors): - return - elif len(self.user_functions) > len(self.user_functions_colors): - for i in range(len(self.user_functions_colors), len(self.user_functions)): - self.user_functions_colors.append(self._get_color_from_palette()) - elif len(self.user_functions_colors) > len(self.user_functions): - while len(self.user_functions_colors) != len(self.user_functions): - self.user_functions_colors.pop() - else: pass - - def set_range_from_datasets(self): - '''Make sure the x and y range can accomodate all datasets. - Expands the range if needed. - ''' - for ds in self.datasets: - xr = ds.get_x_range(self.x_range_margin) - yr = ds.get_y_range(self.y_range_margin) - self.x_range = [min(xr[0], self.x_range[0]), max(xr[1], self.x_range[1])] - self.y_range = [min(yr[0], self.y_range[0]), max(yr[1], self.y_range[1])] - - def set_range_from_dataset(self, dataset): - '''Use a dataset to set the range for the figure. - Adds margins of 5%. of the range. - ''' - xr = dataset.get_x_range() - self.x_range_margin = (xr[1]-xr[0])*0.05 - xr_margin = dataset.get_x_range(self.x_range_margin) - - yr = dataset.get_y_range() - self.y_range_margin = (yr[1]-yr[0])*0.05 - yr_margin = dataset.get_y_range(self.y_range_margin) - - self.x_range = xr_margin - self.y_range = yr_margin - - def set_yres_range_from_fits(self): - '''Set the range for the residual plot, based on all datasets that - have a fit.''' - for dataset in self.datasets: - if dataset.nfits > 0: - yr = dataset.get_yres_range(self.y_range_margin) - self.yres_range = [min(yr[0], self.yres_range[0]), max(yr[1], self.yres_range[1])] - - def fit(self, model=None, parguess=None, fit_range=None, print_results=None, - datasetindex=-1, fitcolor=None, name=None, sigmas=None): - '''Fit a dataset to model - calls XYDataSet.fit and returns a - Measurement_Array of fitted parameters - - :param model: The fit function - :type model: Function - :param parguess: Guesses for the parameters of the fit. - :type parguess: list - :param fit_range: The range over which the fit function is to be shown. - :type fit_range: list - :param print_results: Whether to display the results of the fit. - :type print_results: bool - :param datasetindex: The index of the XYDataSet to be fit. - :type datasetindex: int - :param fitcolor: The color of the fit. - :type fitcolor: str - :param name: The name of the fit. - :type name: str - :param sigmas: How many sigmas to include in the error band. - :type sigmas: int - - :returns: The parameters of the fit - :rtype: Measurement_Array - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - figure.fit('linear') - - figure.show() - ''' - if sigmas == None: - sigmas = self.errorband_sigma - - if print_results == None: - print_results = self.show_fit_results - results = self.datasets[datasetindex].fit(model, parguess, fit_range, fitcolor=fitcolor, name=name, print_results=print_results, sigmas=sigmas) - return results - - def print_fit_parameters(self, dataset=-1): - if len(self.datasets)<1: - print("No datasets") - return - if self.datasets[-1].nfits>0: - print("Fit parameters:\n"+str(self.datasets[dataset].fit_pars[-1])) - else: - print("Datasets have not been fit") - - def get_dataset(self, index=-1): - if len(self.datasets) > 0: - if index < len(self.datasets) -1: - return self.datasets[index] - else: - return None - else: - return None - -############################################################################### -# User Methods for adding to Plot Objects -############################################################################### - - def add_residuals(self): - '''Add a subfigure with residuals to the main figure when plotting. - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - figure.fit('linear') - figure.add_residuals() - - figure.show() - ''' - self.set_yres_range_from_fits() - for ds in self.datasets: - if ds.nfits > 0: - self.show_residuals = True - return - - - def add_function(self, function, pars = None, name=None, color=None, x_range=None, sigmas=None): - '''Add a user-specifed function to the list of functions to be plotted. - - All datasets are functions when populate_bokeh_figure is called - - usually when show() is called. - - :param function: The function to be added to the Plot. - :type function: Function - :param pars: The parameters of the function. - :type pars: list, array, Measurement_Array - :param name: The name of the function. - :type name: str - :param color: The color of the function. - :type color: str - :param x_range: The range on which the function is to be plotted. - :type x_range: array - :param sigmas: How many sigmas to include in the error band. - :type sigmas: int - - .. code-block:: python - - def func(x, *pars): - return pars[0] + pars[1]*x - - figure = q.MakePlot() - - # This function is not related to any data. - figure.add_function(func, name="Function", pars = [1, 5], - color = 'saddlebrown', x_range =[-10,10]) - - figure.show() - ''' - - if sigmas == None: - sigmas = self.errorband_sigma - - if x_range is not None: - if not isinstance(x_range, ARRAY): - print("Error: x_range must be specified as an array of length 2") - elif len(x_range) != 2: - print("Error: x_range must be specified as an array of length 2") - else: - self.x_range[0]=x_range[0]-self.x_range_margin - self.x_range[1]=x_range[1]+self.x_range_margin - - xvals = np.linspace(self.x_range[0],self.x_range[1], 100) - - #check if we should change the y-axis range to accomodate the function - fvals = np.zeros(xvals.size) - ferr = fvals - if not isinstance(pars, np.ndarray) and pars == None: - fvals = function(xvals) - elif isinstance(pars, qe.Measurement_Array) : - recall = qe.Measurement.minmax_n - qe.Measurement.minmax_n=1 - fmes = function(xvals, *(pars)) - fvals = fmes.means - ferr = fmes.stds - qe.Measurement.minmax_n=recall - elif isinstance(pars,(list, np.ndarray)): - fvals = function(xvals, *pars) - else: - print("Error: Not a recognized format for parameter") - return - - fmax = (fvals+ferr).max() + self.y_range_margin - fmin = (fvals-ferr).min() - self.y_range_margin - - if fmax > self.y_range[1]: - self.y_range[1]=fmax - if fmin < self.y_range[0]: - self.y_range[0]=fmin - - self.user_functions.append(function) - self.user_functions_pars.append(pars) - self.user_functions_sigmas.append(sigmas) - fname = "userf_{}".format(self.user_functions_count) if name==None else name - self.user_functions_names.append(fname) - self.user_functions_count +=1 - - if color is None: - self.user_functions_colors.append(self._get_color_from_palette()) - else: - self.user_functions_colors.append(color) - - def add_dataset(self, dataset, color=None, name=None): - '''Add a dataset to the Plot object. All datasets are plotted - when populate_bokeh_figure is called - usually when show() is called - - :param dataset: The dataset to be added to the plot. - :type dataset: XYDataSet - :param color: The color of the dataset. - :type color: str - :param name: The name of the dataset. - :type name: str - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - xy = q.XYDataSet(x, y) - - fig = q.MakePlot() - fig.add_dataset(xy) - fig.show() - ''' - self.datasets.append(dataset) - - if len(self.datasets) < 2: - self.initialize_from_dataset(self.datasets[0]) - else: - self.set_range_from_dataset(dataset) - - if color is None: - self.datasets_colors.append(self._get_color_from_palette()) - else: - self.datasets_colors.append(color) - if name != None: - self.datasets[-1].name=name - - self.set_range_from_datasets() - - self.set_yres_range_from_fits() - - def add_line(self, x=None, y=None, color='black', dashed=False): - '''Add a vertical or horizontal line to the Plot object. - - :param x: The x position of the line to be drawn - (x or y position can be specified, but not both). - :type x: float - :param y: The y position of the line to be drawn - (x or y position can be specified, but not both). - :type y: float - :param color: The color of the line to be drawn. - :type color: str - :param dashed: Whether to draw a dashed line. - :type dashed: bool - - .. code-block:: python - - figure = q.MakePlot() - - # Adds a vertical line at x = 10 - figure.add_line(x=10) - - figure.show() - ''' - x_pos = x - y_pos = y - if bool(x_pos) == bool(y_pos): # Can't have both x and y data - print('''Lines must be given either an x or a y value, but not both.''') - if type(x_pos) in CONSTANT: - x_line_data = {'pos':float(x_pos), 'color':color, 'dashed':dashed} - self.lines['x'].append(x_line_data) - elif type(y_pos) in CONSTANT: - y_line_data = {'pos':float(y_pos), 'color':color, 'dashed':dashed} - self.lines['y'].append(y_line_data) - else: - print('''Line input must be a number.''') - -# -############################################################################### -# Methods for changing parameters of Plot Object -############################################################################### - - def set_plot_range(self, x_range=None, y_range=None): - '''Set the range for the figure. - - :param x_range: The x range of the graph. - :type x_range: array - :param y_range: The y range of the graph. - :type y_range: array - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - figure.set_plot_range(x_range=[0, 5], y_range=[4, 9]) - - figure.show() - ''' - if type(x_range) in ARRAY and len(x_range) is 2: - self.x_range = x_range - elif x_range is not None: - print('''X range must be a list containing a minimum and maximum - value for the range of the plot.''') - - if type(y_range) in ARRAY and len(y_range) is 2: - self.y_range = y_range - elif y_range is not None: - print('''Y range must be a list containing a minimum and maximum - value for the range of the plot.''') - - - def set_labels(self, title=None, xtitle=None, ytitle=None): - '''Change the labels for plot axis, datasets, or the plot itself. - - Method simply overwrites the automatically generated names used in - the Bokeh plot. - - :param title: The title of the plot. - :type title: str - :param xtitle: The title of the x axis. - :type xtitle: str - :param ytitle: The title of the y axis. - :type ytitle: str - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - figure.set_labels("x vs y", "x_data", "y_data") - - figure.show() - ''' - if title is not None: - self.labels['title'] = title - - if xtitle is not None: - self.labels['xtitle'] = xtitle - - if ytitle is not None: - self.labels['ytitle'] = ytitle - - - def resize_plot_px(self, width=None, height=None): - - if width is None: - width = 600 - if height is None: - height = 400 - self.dimensions_px = [width, height] - - - def show(self, output='inline', populate_figure=True, refresh=True): - ''' - Show the figure, will call one of the populate methods - by default to build a figure. - - :param output: How the histogram is to be output. Can be 'inline' or 'file'. - :type output: str - :param populate_figure: Whether the figure needs to be populated. - :type populate_figure: bool - :param refresh: Whether the mpl figure should be refreshed. - :type refresh: bool - - .. code-block:: python - - x = q.MeasurementArray([1, 2, 3, 4], error=0.5) - y = q.MeasurementArray([5, 6.2, 7, 7.8], error=0.1) - - figure = q.MakePlot(x, y) - - figure.show() - ''' - if q.plot_engine in q.plot_engine_synonyms["bokeh"]: - self.set_bokeh_output(output) - if populate_figure: - bp.show(self.populate_bokeh_figure(), notebook_handle=True) - else: - bp.show(self.bkfigure, notebook_handle=True) - - elif q.plot_engine in q.plot_engine_synonyms["mpl"]: - self.set_mpl_output(output) - if populate_figure: - self.populate_mpl_figure(refresh=refresh) - if output == 'file': - plt.savefig(self.user_save_filename, bbox_inches='tight') - plt.show() - - else: - print("Error: unrecognized plot engine") - -############################################################################### -# Methods for Returning or Rendering Matplotlib -############################################################################### - - def set_mpl_output(self, output='inline'): - '''Choose where to output (in a notebook or to a file)''' - #TODO not tested, the output notebook part does not work - - if output == 'file' or not qu.in_notebook(): - #Prompt user for filename - self.user_save_filename = input('Enter a filename: ') - #TODO Decide what to do about this - #plt.savefig(self.save_filename, bbox_inches='tight') - elif not qu.mpl_ouput_notebook_called: - qu.mpl_output_notebook() - # This must be the first time calling output_notebook, - # keep track that it's been called: - qu.mpl_ouput_notebook_called = True - else: - pass - - - def populate_mpl_figure(self, refresh = True): - '''Thia is the main function to populate the matplotlib figure. It will create - the figure, and then draw all of the data sets, their residuals, their fits, - and any user-supplied functions''' - - - if not hasattr(self, 'mplfigure_main_ax') or refresh == True: - self.initialize_mpl_figure() - - #Plot the data sets - self.check_datasets_color_array() - for dataset, color in zip(self.datasets, self.datasets_colors): - self.mpl_plot_dataset(dataset, color, show_fit_function=True, - show_residuals=self.show_residuals) - - #Add a box with results from the fits - if self.show_fit_results: - self.mpl_show_fit_results_box(self.datasets) - - - #Now add any user defined functions: - #The range over which to plot the functions: - xvals = [self.x_range[0]+self.x_range_margin, - self.x_range[1]-self.x_range_margin] - self.check_user_functions_color_array() - for func, pars, fname, color, sigmas in zip(self.user_functions, - self.user_functions_pars, - self.user_functions_names, - self.user_functions_colors, - self.user_functions_sigmas): - - self.mpl_plot_function(function=func, xdata=xvals,pars=pars, n=q.settings["plot_fcn_npoints"], - legend_name= fname, color=color, sigmas=sigmas) - # Adds lines to the plot - if self.lines['x'] or self.lines['y']: - self.mpl_add_lines(self.lines) - - if self.mpl_show_legend: - self.mplfigure_main_ax.legend(loc=self.mpl_legend_location, - fontsize = q.settings["plot_fig_leg_ftsize"]) - - def initialize_mpl_figure(self): - '''Build a matplotlib figure with the desired size to draw on''' - - #Create the main figure object - self.mplfigure = plt.figure(figsize=(self.dimensions_px[0]/self.screen_dpi, - self.dimensions_px[1]/self.screen_dpi)) - - #If we're showing residuals, create the axes differently, - #create axes for the residuals, and label them - if self.show_residuals: - self.mplfigure_main_ax = self.mplfigure.add_axes([0,0.35,1.,0.65]) - self.mplfigure_res_ax = self.mplfigure.add_axes([0,0,1.0,0.3], - sharex=self.mplfigure_main_ax) - self.set_yres_range_from_fits() - self.mplfigure_res_ax.set_ylim([self.yres_range[0], self.yres_range[1]]) - self.mplfigure_res_ax.set_xlabel(self.labels['xtitle'], - fontsize=q.settings["plot_fig_xtitle_ftsize"]) - self.mplfigure_res_ax.set_ylabel("Residuals", - fontsize=q.settings["plot_fig_ytitle_ftsize"]) - self.mplfigure_res_ax.grid() - - else: - #TODO The latter fixes the issues of axis not showing when - #PyPlot is shown outside of notebook. Need to translate to - # residual case and decide if this is wanted. - - #self.mplfigure_main_ax = self.mplfigure.add_axes([0,0,1.,1.]) - self.mplfigure_main_ax = self.mplfigure.add_subplot(111) - - #Regardless of residuals, create the main axes - self.mplfigure_main_ax.axis([self.x_range[0], self.x_range[1], - self.y_range[0], self.y_range[1]]) - self.mplfigure_main_ax.set_xscale(self.axes['xscale']) - self.mplfigure_main_ax.set_yscale(self.axes['yscale']) - - self.mplfigure_main_ax.set_xlabel(self.labels['xtitle'], - fontsize=q.settings["plot_fig_xtitle_ftsize"]) - self.mplfigure_main_ax.set_ylabel(self.labels['ytitle'], - fontsize=q.settings["plot_fig_ytitle_ftsize"]) - self.mplfigure_main_ax.set_title(self.labels['title'], - fontsize=q.settings["plot_fig_title_ftsize"]) - self.mplfigure_main_ax.grid() - - return self.mplfigure - - def mpl_add_lines(self, lines): - '''Adds vertical and horizontal lines to an mpl plot.''' - if not hasattr(self, 'mplfigure_main_ax'): - self.initialize_mpl_figure() - - for x_data in lines['x']: - dashed = 'dashed' if x_data['dashed'] else 'solid' - self.mpl_plot([x_data['pos']]*2, self.y_range, color=x_data['color'], ls=dashed, lw=1, zorder=5) - - for y_data in lines['y']: - dashed = 'dashed' if y_data['dashed'] else 'solid' - self.mpl_plot(self.x_range, [y_data['pos']]*2, color=y_data['color'], ls=dashed, lw=1, zorder=5) - - def mpl_show_fit_results_box(self, datasets=None, add_space = True): - '''Show a box with the fit results for the given list of datasets. If - datasets==None, it will use self.datasets''' - - if not hasattr(self, 'mplfigure_main_ax'): - self.initialize_mpl_figure() - - if datasets == None: - datasets = self.dataset - - #Add some space to plot for the fit results: - if add_space: - newy = self.y_range[1] - pix2y = newy / self.mplfigure_main_ax.patch.get_window_extent().height - #TODO textheight should depend on font size - textheight = 25 - pixelcount = 0 - for dataset in datasets: - if dataset.nfits > 0: - pixelcount += dataset.fit_npars[-1] * textheight - newy += pixelcount * pix2y - self.mplfigure_main_ax.axis([self.x_range[0], self.x_range[1], - self.y_range[0], newy]) - - - textfit = "" - for ds in datasets: - if ds.nfits == 0: - continue - for i in range(ds.fit_npars[-1]): - short_name = ds.fit_pars[-1][i].__str__().split('_') - textfit += short_name[0]+"_"+short_name[-1]+"\n" - textfit += "\n" - - start_x = 0.99 + self.fit_results_x_offset - start_y = 0.99 + self.fit_results_y_offset - - an = self.mplfigure_main_ax.annotate(textfit,xy=(start_x, start_y), - fontsize=q.settings["plot_fig_fitres_ftsize"], - horizontalalignment='right',verticalalignment='top', - xycoords = 'axes fraction', - bbox=dict(facecolor='white', alpha=0.0, edgecolor='none')) - return an - - - def mpl_plot_dataset(self, dataset, color='black', show_fit_function=True, show_residuals=True, fit_index = -1): - '''Add a dataset, its fit function and its residuals to the main figure. - It is better to use add_function() and to let populate_mpl_plot() actually - add the function. - ''' - - index = fit_index if fit_index < dataset.nfits else -1 - - if not hasattr(self, 'mplfigure_main_ax'): - if show_residuals: - if dataset.nfits > 0: - self.show_residuals = True - self.initialize_mpl_figure() - - - if dataset.is_histogram: - if hasattr(dataset, 'hist_data'): - self.mplfigure_main_ax.hist(dataset.hist_data, bins=dataset.hist_bins, - label=dataset.name, color=color, alpha=0.7) - else: - self.mplfigure_main_ax.bar(dataset.xdata, dataset.ydata, width = dataset.xdata[-1]-dataset.xdata[-2], - label=dataset.name, color=color, alpha=0.7) - - else: - self.mplfigure_main_ax.errorbar(dataset.xdata, dataset.ydata, - xerr=dataset.xerr,yerr=dataset.yerr, - fmt='o',color=color,markeredgecolor = 'none', - label=dataset.name) - if dataset.nfits > 0 and show_fit_function: - self.mpl_plot_function(function=dataset.fit_function[index], - xdata=dataset.xdata, - pars=dataset.fit_pars[index], n=q.settings["plot_fcn_npoints"], - legend_name=dataset.fit_function_name[index], - color=color if dataset.fit_color[index] is None else dataset.fit_color[index], - sigmas=dataset.xyfitter[index].sigmas) - - if self.show_residuals and hasattr(self, 'mplfigure_res_ax') and show_residuals: - self.mplfigure_res_ax.errorbar(dataset.xdata, dataset.fit_yres[index].means, - xerr=dataset.xerr,yerr=dataset.yerr, - fmt='o',color=color,markeredgecolor = 'none') - - - - def mpl_plot_function(self, function, xdata, pars=None, n=100, - legend_name=None, color='black', sigmas=None): - '''Add a function to the main figure. It is better to use add_function() and to - let populate_mpl_plot() actually add the function. - - The function can be either f(x) or f(x, *pars), in which case, if *pars is - a Measurement_Array, then error bands will be drawn - ''' - if sigmas == None: - sigmas = self.errorband_sigma - - if not hasattr(self, 'mplfigure_main_ax'): - self.initialize_mpl_figure() - - xvals = np.linspace(min(xdata), max(xdata), n) - - if pars is None: - fvals = function(xvals) - elif isinstance(pars, qe.Measurement_Array): - recall = qe.Measurement.minmax_n - qe.Measurement.minmax_n=1 - fmes = function(xvals, *pars) - fvals = fmes.means - ferr = fmes.stds - qe.Measurement.minmax_n=recall - elif isinstance(pars,(list, np.ndarray)): - fvals = function(xvals, *pars) - else: - print("Error: unrecognized parameters for function") - pass - - self.mplfigure_main_ax.plot(xvals,fvals, color=color, label = legend_name, zorder=5) - - if isinstance(pars, qe.Measurement_Array): - for i in range(1, int(sigmas)+1): - fmax = fvals + i*ferr - fmin = fvals - i*ferr - self.mplfigure_main_ax.fill_between(xvals, fmin, fmax, facecolor=color, - alpha=0.3/(sigmas-0.3*(i-1)), edgecolor = 'none', - interpolate=True, zorder=0) - - def interactive_linear_fit(self, error_range=5, randomize = False, - show_chi2 = True, show_errors=True, - x_range=None, y_range=None): - '''Fits the last dataset to a linear function and displays the - result as an interactive fit - ''' - - if not qu.in_notebook(): - print("Can only use this feature in a notebook, sorry") - return - - if len(self.datasets) >1: - print("Warning: only using the last added dataset, and clearing previous fits") - - dataset = self.datasets[-1] - color = self.datasets_colors[-1] - - dataset.clear_fits() - dataset.fit("linear") - - func = dataset.fit_function[-1] - pars = dataset.fit_pars[-1] - parmeans = pars.means - fname = "linear" - - #Reset the range - self.x_range = [0,1] - self.y_range = [0,1] - self.set_range_from_dataset(dataset) - - #Extend the x range to 0 if needed - if self.x_range[0] > -0.5: - self.x_range[0] = -0.5 - - #make sure the y range is large enough - xvals = np.array([self.x_range]) - fvals = dataset.fit_function[-1](xvals, *parmeans) - fmaxvals = fvals+error_range*max(dataset.yerr) - fminvals = fvals-error_range*max(dataset.yerr) - self.y_range[0] = fminvals.min() - self.y_range[1] = fmaxvals.max() - - #Set fit range to user if specified - if x_range is not None: - self.x_range = x_range - if y_range is not None: - self.y_range = y_range - - off_min = pars[0].mean-error_range*pars[0].std - off_max = pars[0].mean+error_range*pars[0].std - off_step = (off_max-off_min)/50. - - slope_min = pars[1].mean-error_range*pars[1].std - slope_max = pars[1].mean+error_range*pars[1].std - slope_step = (slope_max-slope_min)/50. - - o = pars[0].mean if not randomize else np.random.uniform(off_min, off_max, 1) - oe = pars[0].std if not randomize else np.random.uniform(0, error_range*pars[0].std , 1) - s = pars[1].mean if not randomize else np.random.uniform(slope_min, slope_max, 1) - se = pars[1].std if not randomize else np.random.uniform(0, error_range*pars[1].std , 1) - c = dataset.fit_pcorr[-1][0][1] if not randomize else np.random.uniform(-1, 1, 1) - - if show_errors: - @interact(offset=(off_min, off_max, off_step), - offset_err = (0, error_range*pars[0].std, error_range*pars[0].std/50.), - slope=(slope_min, slope_max, slope_step), - slope_err = (0, error_range*pars[1].std, error_range*pars[1].std/50.), - correlation = (-1,1,0.05) - ) - - def update(offset=o, offset_err=oe, slope=s, - slope_err=se, correlation=c): - - fig = plt.figure(figsize=(self.dimensions_px[0]/self.screen_dpi, - self.dimensions_px[1]/self.screen_dpi)) - - ax = fig.add_axes([0,0,1.,1.]) - - xvals = np.linspace(self.x_range[0], self.x_range[1], 20) - - omes = qe.Measurement(offset,offset_err, name="offset") - smes = qe.Measurement(slope,slope_err, name="slope") - omes.set_correlation(smes,correlation) - - recall = qe.Measurement.minmax_n - qe.Measurement.minmax_n=1 - fmes = omes + smes*xvals - qe.Measurement.minmax_n=recall - fvals = fmes.means - ferr = fmes.stds - - fmax = fvals + ferr - fmin = fvals - ferr - - ax.errorbar(dataset.xdata, dataset.ydata, - xerr=dataset.xerr,yerr=dataset.yerr, - fmt='o',color=color,markeredgecolor = 'none', - label=dataset.name) - - ax.plot(xvals,fvals, color=color, label ="linear fit") - ax.fill_between(xvals, fmin, fmax, facecolor=color, - alpha=0.3, edgecolor = 'none', - interpolate=True) - - - start_x = 0.99 + self.fit_results_x_offset - start_y = 0.99 + self.fit_results_y_offset - - #calculate chi2 - xdata = dataset.xdata - ydata = dataset.ydata - yerr = dataset.yerr - ymodel = offset+slope*xdata - yres = ydata-ymodel - chi2 = 0 - ndof = 0 - for i in range(xdata.size): - if yerr[i] != 0: - chi2 += (yres[i]/yerr[i])**2 - ndof += 1 - ndof -= 3 #2 parameters, -1 - - textfit=str(omes)+"\n"+str(smes)+\ - ("\n chi2/ndof: {:.3f}/{}".format(chi2, ndof) if show_chi2 else "") - - ax.annotate(textfit, - xy=(start_x, start_y), xycoords = 'axes fraction', - fontsize=q.settings["plot_fig_fitres_ftsize"], - horizontalalignment='right', verticalalignment='top', - bbox=dict(facecolor='white', alpha=0.0, edgecolor='none')) - - ax.axis([self.x_range[0], self.x_range[1], - self.y_range[0], self.y_range[1]]) - ax.set_xlabel(self.labels['xtitle'], - fontsize=q.settings["plot_fig_xtitle_ftsize"]) - ax.set_ylabel(self.labels['ytitle'], - fontsize=q.settings["plot_fig_ytitle_ftsize"]) - ax.set_title(self.labels['title'], - fontsize=q.settings["plot_fig_title_ftsize"]) - ax.legend(loc=self.mpl_legend_location, - fontsize = q.settings["plot_fig_leg_ftsize"]) - ax.grid() - plt.show() - else: #no errors - @interact(offset=(off_min, off_max, off_step), - slope=(slope_min, slope_max, slope_step) - ) - - def update(offset=o, slope=s): - - fig = plt.figure(figsize=(self.dimensions_px[0]/self.screen_dpi, - self.dimensions_px[1]/self.screen_dpi)) - - ax = fig.add_axes([0,0,1.,1.]) - - xvals = np.linspace(self.x_range[0], self.x_range[1], 20) - fvals = offset+slope*xvals - - ax.errorbar(dataset.xdata, dataset.ydata, - xerr=dataset.xerr,yerr=dataset.yerr, - fmt='o',color=color,markeredgecolor = 'none', - label=dataset.name) - - ax.plot(xvals,fvals, color=color, label ="linear fit") - - start_x = 0.99 + self.fit_results_x_offset - start_y = 0.99 + self.fit_results_y_offset - - #calculate chi2 - xdata = dataset.xdata - ydata = dataset.ydata - yerr = dataset.yerr - ymodel = offset+slope*xdata - yres = ydata-ymodel - chi2 = 0 - ndof = 0 - for i in range(xdata.size): - if yerr[i] != 0: - chi2 += (yres[i]/yerr[i])**2 - ndof += 1 - ndof -= 3 #2 parameters, -1 - - textfit="offset = {:.2f}\nslope = {:.2f}".format(offset, slope)+\ - ("\n chi2/ndof: {:.3f}/{}".format(chi2, ndof) if show_chi2 else "") - - ax.annotate(textfit, - xy=(start_x, start_y), xycoords = 'axes fraction', - fontsize=q.settings["plot_fig_fitres_ftsize"], - horizontalalignment='right', verticalalignment='top', - bbox=dict(facecolor='white', alpha=0.0, edgecolor='none')) - - ax.axis([self.x_range[0], self.x_range[1], - self.y_range[0], self.y_range[1]]) - ax.set_xlabel(self.labels['xtitle'], - fontsize=q.settings["plot_fig_xtitle_ftsize"]) - ax.set_ylabel(self.labels['ytitle'], - fontsize=q.settings["plot_fig_ytitle_ftsize"]) - ax.set_title(self.labels['title'], - fontsize=q.settings["plot_fig_title_ftsize"]) - ax.legend(loc=self.mpl_legend_location, - fontsize = q.settings["plot_fig_leg_ftsize"]) - ax.grid() - plt.show() - -###Some wrapped matplotlib functions - def mpl_plot(self, *args, **kwargs): - '''Wrapper for matplotlib plot(), typically to plot a line''' - if not hasattr(self, 'mplfigure'): - self.initialize_mpl_figure() - - plt.plot(*args, **kwargs) - - def mpl_error_bar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, - elinewidth=None, capsize=None, barsabove=False, lolims=False, - uplims=False, xlolims=False, xuplims=False, errorevery=1, - capthick=None, hold=None, data=None, **kwargs): - '''Wrapper for matplotlib error_bar(), adds points with error bars ''' - if not hasattr(self, 'mplfigure'): - self.initialize_mpl_figure() - - plt.error_bar(self, x, y, yerr, xerr, fmt, ecolor, - elinewidth, capsize, barsabove, lolims, - uplims, xlolims, xuplims, errorevery, - capthick, hold, data, **kwargs) - - def mpl_hist(self,x, bins=10, range=None, normed=False, weights=None, - cumulative=False, bottom=None, histtype='bar', align='mid', - orientation='vertical', rwidth=None, log=False, color=None, - label=None, stacked=False, hold=None, data=None, **kwargs): - '''Wrapper for matplotlib hist(), creates a histogram''' - if not hasattr(self, 'mplfigure'): - self.initialize_mpl_figure() - - plt.hist(x, bins, range, normed, weights, cumulative, bottom, - histtype, align, orientation, rwidth, log, color, - label, stacked, hold, data, **kwargs) - - -############################################################################### -# Methods for Returning or Rendering Bokeh -############################################################################### - - def set_bokeh_output(self, output='inline'): - '''Choose where to output (in a notebook or to a file)''' - - if output == 'file' or not qu.in_notebook(): - bi.output_file(self.save_filename, - title=self.labels['title']) - elif not qu.bokeh_ouput_notebook_called: - bi.output_notebook() - # This must be the first time calling output_notebook, - # keep track that it's been called: - qu.bokeh_ouput_notebook_called = True - else: - pass - - def populate_bokeh_figure(self): - '''Main method for building the plot - this creates the Bokeh figure, - and then loops through all datasets (and their fit functions), as - well as user-specified functions, and adds them to the bokeh figure''' - - #create a new bokeh figure - - #expand the y-range to accomodate the fit results text - yrange_recall = self.y_range[1] - - if self.show_fit_results: - pixelcount = 0 - for dataset in self.datasets: - if dataset.nfits > 0: - pixelcount += dataset.fit_npars[-1] * 25 - self.y_range[1] += pixelcount * self.y_range[1]/self.dimensions_px[1] - - - self.initialize_bokeh_figure(residuals=False) - self.y_range[1] = yrange_recall - - # create the one for residuals if needed - if self.show_residuals: - self.initialize_bokeh_figure(residuals=True) - - #plot the datasets and their latest fit - legend_offset=0 - self.check_datasets_color_array() - for dataset, color in zip(self.datasets, self.datasets_colors): - self.bk_plot_dataset(dataset, residual=False, color=color, show_fit_function=True) - if dataset.nfits>0: - if self.show_fit_results: - legend_offset = self.bk_plot_fit_results_text_box(dataset, legend_offset) - legend_offset += 3 - if self.show_residuals: - self.bk_plot_dataset(dataset, residual=True, color=color) - - #Now add any user defined functions: - #The range over which to plot the functions: - if type(self.x_range[0]) in CONSTANT: - xvals = [self.x_range[0]+self.x_range_margin, - self.x_range[1]-self.x_range_margin] - else: - xvals = [0, len(self.x_range)] - - self.check_user_functions_color_array() - for func, pars, fname, color, sigmas in zip(self.user_functions, - self.user_functions_pars, - self.user_functions_names, - self.user_functions_colors, - self.user_functions_sigmas): - - self.bk_plot_function(function=func, xdata=xvals,pars=pars, n=q.settings["plot_fcn_npoints"], - legend_name= fname, color=color, sigmas=sigmas) - - # Adds lines to the plot - if self.lines['x'] or self.lines['y']: - self.bk_add_lines(self.lines) - - #Specify the location of the legend (must be done after stuff has been added) - self.bkfigure.legend.location = self.bk_legend_location - self.bkfigure.legend.orientation = self.bk_legend_orientation - - if self.show_residuals: - # Gridplot moved modules, as far as we can tell - if (hasattr(bi, 'gridplot')): - self.bkfigure = bi.gridplot([[self.bkfigure], [self.bkres]]) - else: - self.bkfigure = bl.gridplot([[self.bkfigure], [self.bkres]]) - - return self.bkfigure - - def initialize_bokeh_figure(self, residuals=False): - '''Create the bokeh figure with desired labeling and axes''' - if residuals==False: - self.bkfigure = bp.figure( - tools='save, pan, box_zoom, wheel_zoom, reset', - toolbar_location="above", - width=self.dimensions_px[0], height=self.dimensions_px[1], - y_axis_type=self.axes['yscale'], - y_range=self.y_range, - x_axis_type=self.axes['xscale'], - x_range=self.x_range, - title=self.labels['title'], - x_axis_label=self.labels['xtitle'], - y_axis_label=self.labels['ytitle'], - ) - return self.bkfigure - else: - self.set_yres_range_from_fits() - self.bkres = bp.figure( - width=self.dimensions_px[0], height=self.dimensions_px[1]//3, - tools='save, pan, box_zoom, wheel_zoom, reset', - toolbar_location="above", - y_axis_type='linear', - y_range=self.yres_range, - x_range=self.bkfigure.x_range, - x_axis_label=self.labels['xtitle'], - y_axis_label='Residuals' - ) - return self.bkres - - def bk_add_lines(self, lines): - '''Adds vertical and horizontal lines to a Bokeh plot.''' - if not hasattr(self, 'bkfigure'): - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - - for x_data in lines['x']: - dashed = 'dashed' if x_data['dashed'] else 'solid' - self.bkfigure.line([x_data['pos']]*2, self.y_range, line_color=x_data['color'], line_dash=dashed) - - for y_data in lines['y']: - dashed = 'dashed' if y_data['dashed'] else 'solid' - self.bkfigure.line(self.x_range, [y_data['pos']]*2, line_color=y_data[color], line_dash=dashed) - - def bk_plot_fit_results_text_box(self, dataset, yoffset=0): - '''Add a text box with the fit parameters from the last fit to - the data set''' - if not hasattr(self, 'bkfigure'): - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - - offset = yoffset - start_x = self.dimensions_px[0]-5 + self.fit_results_x_offset - start_y = self.dimensions_px[1]-30-offset + self.fit_results_y_offset - - for i in range(dataset.fit_npars[-1]): - #shorten the name of the fit parameters - short_name = dataset.fit_pars[-1][i].__str__().split('_') - short_name = short_name[0]+"_"+short_name[-1] - if i > 0: - offset += 18 - tbox = mo.Label(x=start_x, y=start_y-offset, - text_align='right', - text_baseline='top', - text_font_size='11pt', - x_units='screen', - y_units='screen', - text=short_name, - render_mode='css', - background_fill_color='white', - background_fill_alpha=0.7) - self.bkfigure.add_layout(tbox) - return offset - - def bk_plot_dataset(self, dataset, residual=False, color='black', show_fit_function=True, fit_index = -1): - '''Add a dataset to the bokeh figure for the plot - it is better to - use add_dataset() to add a dataset to the Plot object and let - populate_bokeh_figure take care of calling this function''' - - index = fit_index if fit_index < dataset.nfits else -1 - - if residual == True: - if not hasattr(self, 'bkres'): - self.bkres = self.initialize_bokeh_figure(residuals=True) - return qpu.bk_plot_dataset(self.bkres, dataset, residual=True, color=color, fit_index = index) - - if not hasattr(self, 'bkfigure'): - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - - if dataset.is_histogram: - if hasattr(dataset, 'hist_data'): - hist, bins = np.histogram(dataset.hist_data, dataset.bins) - self.bkfigure.quad(top=hist, bottom=0, left=bins[:-1], right=bins[1:], - color=color, alpha=0.7, legend=dataset.name) - else: - width = dataset.xdata[-1]-dataset.xdata[-2] - self.bkfigure.quad(top=dataset.ydata, bottom=0, left=dataset.xdata-width/2, - right = dataset.xdata+width/2, color=color, alpha=0.7, legend=dataset.name) - else: - qpu.bk_plot_dataset(self.bkfigure, dataset, residual=False, color=color) - if dataset.nfits > 0 and show_fit_function: - self.bk_plot_function(function=dataset.fit_function[index], xdata=dataset.xdata, - pars=dataset.fit_pars[index], n=q.settings["plot_fcn_npoints"], - legend_name=dataset.name+"_"+dataset.fit_function_name[index], - color=color if dataset.fit_color[index] is None else dataset.fit_color[index], - sigmas=dataset.xyfitter[index].sigmas) - - def bk_add_points_with_error_bars(self, xdata, ydata, xerr=None, yerr=None, - color='black', data_name='dataset'): - '''Add a set of data points with error bars to the main figure -it is better - to use add_dataset if the data should be treated as a dataset that can be fit''' - if not hasattr(self, 'bkfigure'): - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - return qpu.bk_add_points_with_error_bars(self.bkfigure, xdata, ydata, xerr=xerr, - yerr=yerr, color=color, - data_name=data_name) - - def bk_plot_function(self, function, xdata, pars=None, n=100, - legend_name=None, color='black', sigmas=None): - '''Add a function to the main figure. It is better to use add_function() and to - let populate_bokeh_plot() actually add the function. - - The function can be either f(x) or f(x, *pars), in which case, if *pars is - a Measurement_Array, then error bands will be drawn - ''' - if sigmas == None: - sigmas = self.errorband_sigma - - if not hasattr(self, 'bkfigure'): - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - return qpu.bk_plot_function(self.bkfigure, function, xdata, pars=pars, n=n, - legend_name=legend_name, color=color, sigmas=sigmas) - - def bk_show_linear_fit(self, output='inline'): - '''Fits the last dataset to a linear function and displays the - result. The fit parameters are not displayed as this function is - designed to be used in conjunction with bk_interarct_linear_fit()''' - - - if len(self.datasets) >1: - print("Warning: only using the last added dataset, and clearing previous fits") - - dataset = self.datasets[-1] - color = self.datasets_colors[-1] - - dataset.clear_fits() - dataset.fit("linear") - - func = dataset.fit_function[-1] - pars = dataset.fit_pars[-1] - fname = "linear" - - #Extend the x range to 0 - if self.x_range[0] > -0.5: - self.x_range[0] = -0.5 - self.y_range[0] = dataset.fit_function[-1](self.x_range[0], *pars.means) - - self.bkfigure = self.initialize_bokeh_figure(residuals=False) - - self.bk_plot_dataset(dataset, residual=False,color=color, show_fit_function=False) - - xvals = [self.x_range[0]+self.x_range_margin, - self.x_range[1]-self.x_range_margin] - - line, patches = self.bk_plot_function( function=func, xdata=xvals, - pars=pars, n=100, legend_name= fname, - color=color) - - #stuff that is only needed by interact_linear_fit - self.linear_fit_line = line - self.linear_fit_patches = patches - self.linear_fit_pars = pars - self.linear_fit_corr = dataset.fit_pcorr[-1][0][1] - - #Specify the location of the legend - self.bkfigure.legend.location = self.bk_legend_location - self.show(output=output,populate_figure=False) - - def bk_interact_linear_fit(self, error_range = 2): - '''After show_linear_fit() has been called, this will display - sliders allowing the user to adjust the parameters of the linear - fit - only works in a notebook, require ipywigets''' - - if not qu.in_notebook(): - print("Can only use this feature in a notebook, sorry") - return - - - off_mean = self.linear_fit_pars[0].mean - off_std = self.linear_fit_pars[0].std - off_min = off_mean-error_range*off_std - off_max = off_mean+error_range*off_std - off_step = (off_max-off_min)/50. - - slope_mean = self.linear_fit_pars[1].mean - slope_std = self.linear_fit_pars[1].std - slope_min = slope_mean-error_range*slope_std - slope_max = slope_mean+error_range*slope_std - slope_step = (slope_max-slope_min)/50. - - - @interact(offset=(off_min, off_max, off_step), - offset_err = (0, error_range*off_std, error_range*off_std/50.), - slope=(slope_min, slope_max, slope_step), - slope_err = (0, error_range*slope_std, error_range*slope_std/50.), - correlation = (-1,1,0.05) - ) - def update(offset=off_mean, offset_err=off_std, slope=slope_mean, slope_err=slope_std, correlation=self.linear_fit_corr): - - recall = qe.Measurement.minmax_n - qe.Measurement.minmax_n=1 - omes = qe.Measurement(offset,offset_err) - smes = qe.Measurement(slope,slope_err) - omes.set_correlation(smes,correlation) - xdata = np.array(self.linear_fit_line.data_source.data['x']) - fmes = omes+ smes*xdata - qe.Measurement.minmax_n=recall - - ymax = fmes.means+fmes.stds - ymin = fmes.means-fmes.stds - - self.linear_fit_line.data_source.data['y'] = fmes.means - self.linear_fit_patches.data_source.data['y'] = np.append(ymax,ymin[::-1]) - - bi.push_notebook() diff --git a/qexpy/plotting/__init__.py b/qexpy/plotting/__init__.py new file mode 100644 index 0000000..b274577 --- /dev/null +++ b/qexpy/plotting/__init__.py @@ -0,0 +1,3 @@ +"""This package contains plotting functions""" + +from .plotting import plot, hist, show, new_plot, get_plot diff --git a/qexpy/plotting/plotobjects.py b/qexpy/plotting/plotobjects.py new file mode 100644 index 0000000..2e2f62b --- /dev/null +++ b/qexpy/plotting/plotobjects.py @@ -0,0 +1,476 @@ +"""Contains definitions for objects to be drawn on plot""" + +import numpy as np +import inspect + +from abc import ABC, abstractmethod +from matplotlib.pyplot import Axes +from qexpy.utils.exceptions import IllegalArgumentError, UndefinedActionError +from qexpy.fitting.fitting import XYFitResult + +import qexpy.data.data as dt +import qexpy.utils as utils +import qexpy.settings.settings as sts +import qexpy.settings.literals as lit +import qexpy.data.datasets as dts + +from . import plotting as plt # pylint: disable=cyclic-import,unused-import + + +class ObjectOnPlot(ABC): + """A container for anything to be plotted""" + + def __init__(self, *args, **kwargs): + """Constructor for ObjectOnPlot""" + + # process format string + fmt = kwargs.pop("fmt", args[0] if args and isinstance(args[0], str) else None) + if fmt and not isinstance(fmt, str): + raise TypeError("The fmt provided is not a string!") + self._fmt = fmt + + # process color + color = kwargs.pop("color", None) + if color and not isinstance(color, str): + raise TypeError("The color provided is not a string!") + self._color = color + + # add the rest to the object + label = kwargs.pop("label", "") + if label and not isinstance(label, str): + raise TypeError("The label of this plot object is not a string!") + self.label = label + + @property + def fmt(self): + """str: The format string to be used in PyPlot""" + return self._fmt + + @property + def color(self): + """str: The color of the object""" + return self._color + + @color.setter + def color(self, new_color: str): + if not new_color: + return + if not isinstance(new_color, str): + raise TypeError("The color has to be a string.") + self._color = new_color + + @abstractmethod + def show(self, ax: Axes, plot: "plt.Plot"): + """Draw the object itself onto the given axes""" + raise NotImplementedError + + +class FitTarget(ABC): # pylint: disable=too-few-public-methods + """Interface for anything to which a fit can be applied""" + + @property + @abstractmethod + def fit_target_dataset(self): + """dts.XYDataSet: The target dataset instance to apply the fit to""" + raise NotImplementedError + + +class ObjectWithRange(ABC): # pylint: disable=too-few-public-methods + """Interface for anything with an xrange""" + + @property + @abstractmethod + def xrange(self): + """tuple: The xrange of the object""" + raise NotImplementedError + + +class XYObjectOnPlot(ObjectOnPlot, ObjectWithRange): + """A container for objects with x and y values to be drawn on a plot""" + + def __init__(self, *args, **kwargs): + """Constructor for XYObjectOnPlot""" + + xrange = kwargs.pop("xrange", ()) + if xrange: + utils.validate_xrange(xrange) + self._xrange = xrange + + xname = kwargs.pop("xname", "") + if not isinstance(xname, str): + raise TypeError("The xname provided is not a string!") + self._xname = xname + + yname = kwargs.pop("yname", "") + if not isinstance(yname, str): + raise TypeError("The yname provided is not a string!") + self._yname = yname + + xunit = kwargs.pop("xunit", "") + if not isinstance(xunit, str): + raise TypeError("The xunit provided is not a string!") + self._xunit = xunit + + yunit = kwargs.pop("yunit", "") + if not isinstance(yname, str): + raise TypeError("The yunit provided is not a string!") + self._yunit = yunit + + # save the plot kwargs + self.plot_kwargs = {k: v for k, v in kwargs.items() if k in PLOT_VALID_KWARGS} + self.err_kwargs = {k: v for k, v in kwargs.items() if k in ERRORBAR_VALID_KWARGS} + + super().__init__(*args, **kwargs) + + @property + @abstractmethod + def xvalues(self): + """np.ndarray: The array of x-values to be plotted""" + raise NotImplementedError + + @property + @abstractmethod + def yvalues(self): + """np.ndarray: The array of y-values to be plotted""" + raise NotImplementedError + + @property + def xrange(self): + """tuple: The range of values to be plotted""" + return self._xrange + + @xrange.setter + def xrange(self, new_range: tuple): + if new_range: + utils.validate_xrange(new_range) + self._xrange = new_range + + @property + def xname(self): + """str: The name of the x-axis""" + return self._xname + + @property + def xunit(self): + """str: The unit of the x-axis""" + return self._xunit + + @property + def yname(self): + """str: The name of the x-axis""" + return self._yname + + @property + def yunit(self): + """str: The unit of the x-axis""" + return self._yunit + + +class XYDataSetOnPlot(XYObjectOnPlot, FitTarget): + """A wrapper for an XYDataSet to be plotted""" + + def __init__(self, *args, **kwargs): + + # set the data set object + if args and isinstance(args[0], dts.XYDataSet): + self.dataset = args[0] + fmt = kwargs.pop("fmt", args[1] if len(args) >= 2 else "") + else: + self.dataset = dts.XYDataSet(*args, **kwargs) + fmt = kwargs.pop("fmt", "") + + label = kwargs.pop("label", self.dataset.name) + fmt = fmt if fmt else "o" + + # call super constructors + XYObjectOnPlot.__init__(self, label=label, fmt=fmt, **kwargs) + + def show(self, ax: Axes, plot: "plt.Plot"): + if not plot.plot_settings[lit.ERROR_BAR]: + ax.plot( + self.xvalues, self.yvalues, self.fmt, color=self.color, + label=self.label, **self.plot_kwargs) + else: + ax.errorbar( + self.xvalues, self.yvalues, self.yerr, self.xerr, fmt=self.fmt, + color=self.color, label=self.label, **self.plot_kwargs, **self.err_kwargs) + + @property + def xrange(self): + if not self._xrange: + return min(self.dataset.xvalues), max(self.dataset.xvalues) + return self._xrange + + @property + def xvalues(self): + if self._xrange: + return self.dataset.xvalues[self.__get_indices_from_xrange()] + return self.dataset.xvalues + + @property + def yvalues(self): + if self._xrange: + return self.dataset.yvalues[self.__get_indices_from_xrange()] + return self.dataset.yvalues + + @property + def xerr(self): + """np.ndarray: the array of x-value uncertainties to show up on plot""" + if self._xrange: + return self.dataset.xerr[self.__get_indices_from_xrange()] + return self.dataset.xerr + + @property + def yerr(self): + """np.ndarray: the array of y-value uncertainties to show up on plot""" + if self._xrange: + return self.dataset.yerr[self.__get_indices_from_xrange()] + return self.dataset.yerr + + @property + def xname(self): + return self.dataset.xname + + @property + def xunit(self): + return self.dataset.xunit + + @property + def yname(self): + return self.dataset.yname + + @property + def yunit(self): + return self.dataset.yunit + + @property + def fit_target_dataset(self) -> dts.XYDataSet: + return self.dataset + + def __get_indices_from_xrange(self): + low, high = self._xrange + return (low <= self.dataset.xvalues) & (self.dataset.xvalues < high) + + +class FunctionOnPlot(XYObjectOnPlot): + """This is the wrapper for a function to be plotted""" + + def __init__(self, *args, **kwargs): + """Constructor for FunctionOnPlot""" + + func = args[0] if args else None + + # check input + if not callable(func): + raise IllegalArgumentError("The function provided is not a callable object!") + + # this checks if the xrange of plot is specified by user or auto-generated + self.xrange_specified = "xrange" in kwargs + + self.pars = kwargs.pop("pars", []) + + self.error_method = kwargs.pop("error_method", None) + + self._ydata = None # buffer for calculated y data + + parameters = inspect.signature(func).parameters + if len(parameters) > 1 and not self.pars: + raise ValueError( + "For a function with parameters, a list of parameters has to be supplied.") + + if len(parameters) == 1: + self.func = func + elif len(parameters) > 1: + self.func = lambda x: func(x, *self.pars) # pylint:disable=not-callable + else: + raise ValueError("The function supplied does not have an x-variable.") + + XYObjectOnPlot.__init__(self, *args, **kwargs) + + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + + def show(self, ax: Axes, plot: "plt.Plot"): + xvalues = self.xvalues + yvalues = self.yvalues + ax.plot( + xvalues, yvalues, self.fmt if self.fmt else "-", color=self.color, + label=self.label, **self.plot_kwargs) + yerr = self.yerr + if yerr.size > 0 and plot.plot_settings[lit.ERROR_BAR]: + max_vals = yvalues + yerr + min_vals = yvalues - yerr + ax.fill_between( + xvalues, min_vals, max_vals, edgecolor='none', color=self.color, + alpha=0.3, interpolate=True, zorder=0) + + @property + def xrange(self): + return self._xrange + + @xrange.setter + def xrange(self, new_range: tuple): + if new_range: + utils.validate_xrange(new_range) + self._xrange = new_range + self._ydata = None # clear y data since it would need to be re-calculated + + @property + def xvalues(self): + if not self.xrange: + raise UndefinedActionError("The domain of this function cannot be found.") + return np.linspace(self.xrange[0], self.xrange[1], 100) + + @property + def ydata(self): + """The raw y data of the function""" + if self._ydata: + return self._ydata + if not self.xrange: + raise UndefinedActionError("The domain of this function cannot be found.") + result = self.func(self.xvalues) + derived_values = (res for res in result if isinstance(res, dt.DerivedValue)) + if self.error_method: + for value in derived_values: + value.error_method = self.error_method + return result + + @property + @sts.use_mc_sample_size(10000) + def yvalues(self): + simplified_result = list( + res.value if isinstance(res, dt.DerivedValue) else res for res in self.ydata) + return np.asarray(simplified_result) + + @property + @sts.use_mc_sample_size(10000) + def yerr(self): + """The array of y-value uncertainties to show up on plot""" + errors = np.asarray(list( + res.error if isinstance(res, dt.DerivedValue) else 0 for res in self.ydata)) + return errors if errors.size else np.empty(0) + + +class XYFitResultOnPlot(ObjectOnPlot): + """Wrapper for an XYFitResult to be plotted""" + + def __init__(self, *args, **kwargs): + """Constructor for an XYFitResultOnPlot""" + + result = args[0] if args else None + + # check input + if not isinstance(result, XYFitResult): + raise IllegalArgumentError("The fit result is not an XYFitResult instance") + + # initialize object + ObjectOnPlot.__init__(self, **kwargs) + self.fit_result = result + + xrange = result.xrange if result.xrange else ( + min(result.dataset.xvalues), max(result.dataset.xvalues)) + + self.func_on_plot = FunctionOnPlot( + result.fit_function, xrange=xrange, error_method=lit.MONTE_CARLO, **kwargs) + self.residuals_on_plot = XYDataSetOnPlot( + result.dataset.xdata, result.residuals, **kwargs) + + # pylint: disable=protected-access + def show(self, ax: Axes, plot: "plt.Plot"): + if not self.color: + datasets = (obj for obj in plot._objects if isinstance(obj, XYDataSetOnPlot)) + color = next(( + obj.color for obj in datasets if obj.dataset == self.fit_result.dataset), "") + self.color = color if color else plot._color_palette.pop(0) + self.func_on_plot.show(ax, plot) + if plot.res_ax: + self.residuals_on_plot.show(plot.res_ax, plot) + + @property + def color(self): + return self._color + + @color.setter + def color(self, new_color: str): + if not new_color: + return + if not isinstance(new_color, str): + raise TypeError("The color has to be a string.") + self._color = new_color + self.func_on_plot.color = new_color + self.residuals_on_plot.color = new_color + + @property + def dataset(self): + """dts.XYDataSet: The dataset that the fit is associated with""" + return self.fit_result.dataset + + +class HistogramOnPlot(ObjectOnPlot, FitTarget, ObjectWithRange): + """Represents a histogram to be drawn on a plot""" + + def __init__(self, *args, **kwargs): + """Constructor for histogram on plots""" + + ObjectOnPlot.__init__(self, **kwargs) + + if args and isinstance(args[0], dts.ExperimentalValueArray): + self.samples = args[0] + else: + self.samples = dts.ExperimentalValueArray(*args, **kwargs) + + self.kwargs = {k: v for k, v in kwargs.items() if k in HIST_VALID_KWARGS} + + hist_kwargs = {k: v for k, v in kwargs.items() if k in NP_HIST_VALID_KWARGS} + self.n, self.bin_edges = np.histogram(self.samples.values, **hist_kwargs) + + self._xrange = self.bin_edges[0], self.bin_edges[-1] + + def show(self, ax: Axes, plot: "plt.Plot"): + ax.hist(self.sample_values, **self.kwargs) + + @property + def sample_values(self): + """np.ndarray: The values of the samples in this histogram""" + return self.samples.values + + @property + def fit_target_dataset(self) -> dts.XYDataSet: + bins = self.bin_edges + xvalues = [(bins[i] + bins[i + 1]) / 2 for i in range(len(bins) - 1)] + return dts.XYDataSet(xvalues, self.n, name="histogram") + + @property + def xrange(self) -> (float, float): + return self._xrange + + +# Valid keyword arguments for pyplot.plot() +PLOT_VALID_KWARGS = [ + "agg_filter", "alpha", "animated", "antialiased", "clip_box", "clip_on", "clip_path", + "lw", "contains", "dash_capstyle", "dash_joinstyle", "dashes", "drawstyle", "figure", + "fillstyle", "gid", "in_layout", "linestyle", "linewidth", "marker", "ls", "ds", + "markeredgecolor", "markeredgewidth", "markerfacecolor", "markersize", "mfc", "mew", + "markerfacecoloralt", "markevery", "path_effects", "picker", "pickradius", "rasterized", + "sketch_params", "snap", "solid_capstyle", "solid_joinstyle", "transform", "url", + "visible", "zorder", "aa", "c", "ms", "mfcalt", "mec" +] + +# Valid keyword arguments for pyplot.errorbar() +ERRORBAR_VALID_KWARGS = [ + "ecolor", "elinewidth", "capsize", "capthick", "barsabove", "lolims", "uplims", + "xlolims", "xuplims", "errorevery" +] + +# Valid keyword arguments for pyplot.hist() +HIST_VALID_KWARGS = [ + "bins", "range", "density", "weights", "cumulative", "bottom", "align", "histtype", + "orientation", "rwidth", "log", "stacked", "agg_filter", "alpha", "animated", + "antialiased", "aa", "capstyle", "clip_box", "clip_on", "clip_path", "color", + "contains", "edgecolor", "ec", "facecolor", "fc", "figure", "fill", "gid", "hatch", + "in_layout", "joinstyle", "label", "linestyle", "ls", "linewidth", "lw", "path_effects", + "picker", "rasterized", "sketch_params", "snap", "transform", "url", "visible", "zorder" +] + +# Valid keyword arguments for numpy.histogram() +NP_HIST_VALID_KWARGS = ["bins", "range", "density", "weights"] diff --git a/qexpy/plotting/plotting.py b/qexpy/plotting/plotting.py new file mode 100644 index 0000000..a8a6083 --- /dev/null +++ b/qexpy/plotting/plotting.py @@ -0,0 +1,364 @@ +"""This file contains function definitions for plotting""" + +import matplotlib.pyplot as plt + +from typing import List +from qexpy.utils.exceptions import IllegalArgumentError, UndefinedActionError +from .plotobjects import ObjectOnPlot, XYObjectOnPlot, XYDataSetOnPlot, FunctionOnPlot, \ + XYFitResultOnPlot, HistogramOnPlot, FitTarget, ObjectWithRange + +import qexpy.utils as utils +import qexpy.fitting as ft +import qexpy.settings as sts +import qexpy.settings.literals as lit + + +class Plot: + """The data structure used for a plot""" + + # points to the latest Plot instance that's created + current_plot_buffer = None # type: Plot + + def __init__(self): + self._objects = [] # type: List[ObjectOnPlot] + self._plot_info = { + lit.TITLE: "", + lit.XNAME: "", + lit.YNAME: "", + lit.XUNIT: "", + lit.YUNIT: "" + } + self.plot_settings = { + lit.LEGEND: False, + lit.ERROR_BAR: True, + lit.RESIDUALS: False, + lit.PLOT_STYLE: lit.DEFAULT, + } + self._color_palette = ["C{}".format(idx) for idx in range(20)] + self._xrange = () + self.main_ax = None + self.res_ax = None + + def plot(self, *args, **kwargs): + """Adds a data set or function to the plot + + See Also: + :py:func:`.plot` + + """ + new_obj = self.__create_object_on_plot(*args, **kwargs) + self._objects.append(new_obj) + + def hist(self, *args, **kwargs): + """Adds a histogram to the plot + + See Also: + :py:func:`.hist` + + """ + + new_obj = HistogramOnPlot(*args, **kwargs) + + # add color to the histogram + color = kwargs.pop("color", self._color_palette.pop(0)) + new_obj.color = color + + self._objects.append(new_obj) + + return new_obj.n, new_obj.bin_edges + + def fit(self, *args, **kwargs): + """Plots a curve fit to the last data set added to the figure + + The fit function finds the last data set or histogram added to the Plot and apply a + fit to it. This function takes the same arguments as QExPy fit function, and the same + keyword arguments as in the QExPy plot function in configuring how the line of best + fit shows up on the plot. + + See Also: + :py:func:`~qexpy.fitting.fit` + :py:func:`.plot` + + """ + + fit_targets = list(_obj for _obj in self._objects if isinstance(_obj, FitTarget)) + target = next(reversed(fit_targets), None) + + if not target: + raise UndefinedActionError("There is no dataset in this plot to be fitted.") + + result = ft.fit(target.fit_target_dataset, *args, **kwargs) + color = kwargs.pop( + "color", target.color if isinstance(target, ObjectOnPlot) else "") + obj = self.__create_object_on_plot(result, color=color, **kwargs) + + if isinstance(target, HistogramOnPlot) and isinstance(obj, XYFitResultOnPlot): + target.kwargs["alpha"] = 0.8 + obj.func_on_plot.plot_kwargs["lw"] = 2 + + self._objects.append(obj) + return result + + def show(self): + """Draws the plot to output""" + + self.__setup_figure_and_subplots() + + # set the xrange of functions to plot using the range of existing data sets + xrange = self.xrange + for obj in self._objects: + if isinstance(obj, FunctionOnPlot) and not obj.xrange_specified: + obj.xrange = xrange + + for obj in self._objects: + obj.show(self.main_ax, self) + + self.main_ax.set_title(self.title) + self.main_ax.set_xlabel(self.xlabel) + self.main_ax.set_ylabel(self.ylabel) + self.main_ax.grid() + + if self.res_ax: + self.res_ax.set_xlabel(self.xlabel) + self.res_ax.set_ylabel("residuals") + self.res_ax.grid() + + if self.plot_settings[lit.LEGEND]: + self.main_ax.legend() # show legend if requested + + plt.show() + + def legend(self, new_setting=True): + """Add or remove legend to plot""" + self.plot_settings[lit.LEGEND] = new_setting + + def error_bars(self, new_setting=True): + """Add or remove error bars from plot""" + self.plot_settings[lit.ERROR_BAR] = new_setting + + def residuals(self, new_setting=True): + """Add or remove subplot to show residuals""" + self.plot_settings[lit.RESIDUALS] = new_setting + + @property + def title(self): + """str: The title of this plot, which will appear on top of the figure""" + return self._plot_info[lit.TITLE] + + @title.setter + def title(self, new_title: str): + if not isinstance(new_title, str): + raise TypeError("The new title is not a string!") + self._plot_info[lit.TITLE] = new_title + + @property + def xname(self): + """str: The name of the x data, which will appear as x label""" + if self._plot_info[lit.XNAME]: + return self._plot_info[lit.XNAME] + xy_objects = (obj for obj in self._objects if isinstance(obj, XYObjectOnPlot)) + return next((obj.xname for obj in xy_objects if obj.xname), "") + + @xname.setter + def xname(self, name): + if not isinstance(name, str): + raise TypeError("Cannot set xname to \"{}\"".format(type(name).__name__)) + self._plot_info[lit.XNAME] = name + + @property + def yname(self): + """str: The name of the y data, which will appear as y label""" + if self._plot_info[lit.YNAME]: + return self._plot_info[lit.YNAME] + xy_objects = (obj for obj in self._objects if isinstance(obj, XYObjectOnPlot)) + return next((obj.yname for obj in xy_objects if obj.yname), "") + + @yname.setter + def yname(self, name): + if not isinstance(name, str): + raise TypeError("Cannot set yname to \"{}\"".format(type(name).__name__)) + self._plot_info[lit.YNAME] = name + + @property + def xunit(self): + """str: The unit of the x data, which will appear on the x label""" + if self._plot_info[lit.XUNIT]: + return self._plot_info[lit.XUNIT] + xy_objects = (obj for obj in self._objects if isinstance(obj, XYObjectOnPlot)) + return next((obj.xunit for obj in xy_objects if obj.xunit), "") + + @xunit.setter + def xunit(self, unit): + if not isinstance(unit, str): + raise TypeError("Cannot set xunit to \"{}\"".format(type(unit).__name__)) + self._plot_info[lit.XUNIT] = unit + + @property + def yunit(self): + """str: The unit of the y data, which will appear on the y label""" + if self._plot_info[lit.YUNIT]: + return self._plot_info[lit.YUNIT] + xy_objects = (obj for obj in self._objects if isinstance(obj, XYObjectOnPlot)) + return next((obj.yunit for obj in xy_objects if obj.yunit), "") + + @yunit.setter + def yunit(self, unit): + if not isinstance(unit, str): + raise TypeError("Cannot set yunit to \"{}\"".format(type(unit).__name__)) + self._plot_info[lit.YUNIT] = unit + + @property + def xlabel(self): + """str: The xlabel of the plot""" + return self.xname + "[{}]".format(self.xunit) if self.xunit else "" + + @property + def ylabel(self): + """str: the ylabel of the plot""" + return self.yname + "[{}]".format(self.yunit) if self.yunit else "" + + @property + def xrange(self): + """tuple: The x-value domain of this plot""" + if not self._xrange: + objs = list(obj for obj in self._objects if isinstance(obj, ObjectWithRange)) + low_bound = min(obj.xrange[0] for obj in objs if obj.xrange) + high_bound = max(obj.xrange[1] for obj in objs if obj.xrange) + return low_bound, high_bound + return self._xrange + + @xrange.setter + def xrange(self, new_range): + utils.validate_xrange(new_range) + self._xrange = new_range + + def __create_object_on_plot(self, *args, **kwargs) -> "ObjectOnPlot": + """Factory method for creating ObjectOnPlot instances""" + + color = kwargs.pop("color", None) + + try: + # The color of an XYFitResult will be dynamically determined at show time unless + # explicitly specified by the user. No selecting from the color palette just yet. + return XYFitResultOnPlot(*args, color=color, **kwargs) + except IllegalArgumentError: + pass + + try: + color = color if color else self._color_palette.pop(0) + return FunctionOnPlot(*args, color=color, **kwargs) + except IllegalArgumentError: + pass + + try: + color = color if color else self._color_palette.pop(0) + return XYDataSetOnPlot(*args, color=color, **kwargs) + except IllegalArgumentError: + pass + + # if everything has failed + raise IllegalArgumentError("Invalid combination of arguments for plotting.") + + def __setup_figure_and_subplots(self): + """Create the mpl figure and subplots""" + + has_residuals = self.plot_settings[lit.RESIDUALS] + + width, height = sts.get_settings().plot_dimensions + + if has_residuals: + height = height * 1.5 + + figure = plt.figure(figsize=(width, height), constrained_layout=True) + + if has_residuals: + gs = figure.add_gridspec(3, 1) + main_ax = figure.add_subplot(gs[:-1, :]) + res_ax = figure.add_subplot(gs[-1:, :]) + else: + main_ax = figure.add_subplot() + res_ax = None + + self.main_ax, self.res_ax = main_ax, res_ax + + +def plot(*args, **kwargs) -> Plot: + """Plots a dataset or a function + + Adds a dataset or a function to a Plot, and returns the Plot object. This is a wrapper + around the matplotlib.pyplot.plot function, so it takes all the keyword arguments that is + accepted by the pyplot.plot function, as well as the pyplot.errorbar function. + + By default, error bars are not displayed. If you want error bars, it can be turned on in + the Plot object. + + Args: + *args: The first arguments can be an XYDataSet object, two separate arrays for xdata + and ydata, a callable function, or an XYFitResult object. The function also takes + a string at the end of the list of arguments as the format string. + + See Also: + :py:class:`~qexpy.data.XYDataSet`, + `plot() `_, + `errorbar() `_ + + """ + + plot_obj = __get_plot_obj() + + # invoke the instance method of the Plot to add objects to the plot + plot_obj.plot(*args, **kwargs) + + return plot_obj + + +def hist(*args, **kwargs) -> tuple: + """Plots a histogram with a data set + + Args: + *args: the ExperimentalValueArray or arguments that creates an ExperimentalValueArray + + See Also: + `hist() `_ + + """ + + plot_obj = __get_plot_obj() + + # invoke the instance method of the Plot to add objects to the plot + values, bin_edges = plot_obj.hist(*args, **kwargs) + + return values, bin_edges, plot_obj + + +def show(plot_obj=None): + """Draws the plot to output + + The QExPy plotting module keeps a buffer on the last plot being operated on. If no + Plot instance is supplied to this function, the buffered plot will be shown. + + Args: + plot_obj (Plot): the Plot instance to be shown. + + """ + if not plot_obj: + plot_obj = Plot.current_plot_buffer + plot_obj.show() + + +def get_plot(): + """Gets the current plot buffer""" + return Plot.current_plot_buffer + + +def new_plot(): + """Clears the current plot buffer and start a new one""" + Plot.current_plot_buffer = Plot() + + +def __get_plot_obj(): + """Helper function that gets the appropriate Plot instance to draw on""" + + # initialize buffer if not initialized + Plot.current_plot_buffer = Plot() + return Plot.current_plot_buffer diff --git a/qexpy/settings/__init__.py b/qexpy/settings/__init__.py new file mode 100644 index 0000000..bd1bcb4 --- /dev/null +++ b/qexpy/settings/__init__.py @@ -0,0 +1,6 @@ +"""Package containing configurations for processing and displaying data""" + +from .settings import ErrorMethod, PrintStyle, UnitStyle, SigFigMode +from .settings import get_settings, reset_default_configuration +from .settings import set_sig_figs_for_value, set_sig_figs_for_error, set_error_method, \ + set_print_style, set_unit_style, set_monte_carlo_sample_size, set_plot_dimensions diff --git a/qexpy/settings/literals.py b/qexpy/settings/literals.py new file mode 100644 index 0000000..88a90cf --- /dev/null +++ b/qexpy/settings/literals.py @@ -0,0 +1,85 @@ +"""String literals for common settings + +It is highly recommended that developers use and update the constants in this file, which can +be used when accessing common dictionary entries. This improves readability. + +""" + +# value types for error propagation +DERIVATIVE = "derivative" +MONTE_CARLO = "monte-carlo" +MC_MEAN_AND_STD = "monte-carlo-mean-and-std" +MC_MODE_AND_CONFIDENCE = "monte-carlo-mode_and_confidence" +MC_CUSTOM = "monte-carlo-custom" +MONTE_CARLO_STRATEGY = "mc-strategy" +MONTE_CARLO_CONFIDENCE = "confidence" + +# data fields +COVARIANCE = "covariance" +CORRELATION = "correlation" +VALUES = "values" + +# settings +ERROR_METHOD = "error_method" +PRINT_STYLE = "print_style" +UNIT_STYLE = "unit_style" +SIG_FIGS = "significant_figures" +SIG_FIG_MODE = "mode" +SIG_FIG_VALUE = "value" +MONTE_CARLO_SAMPLE_SIZE = "monte_carlo_sample_size" + +LATEX = "latex" +SCIENTIFIC = "scientific" +FRACTION = "fraction" +EXPONENTS = "exponents" +SET_TO_VALUE = "set_to_value" +SET_TO_ERROR = "set_to_error" + +# operators +OPERATOR = "operator" +OPERANDS = "operands" +NEG = "neg" +ADD = "add" +SUB = "sub" +MUL = "mul" +DIV = "div" +SQRT = "sqrt" +SIN = "sin" +COS = "cos" +TAN = "tan" +SEC = "sec" +CSC = "csc" +COT = "cot" +POW = "pow" +EXP = "exp" +LOG = "log" +LOG10 = "log10" +LN = "ln" +ASIN = "asin" +ACOS = "acos" +ATAN = "atan" + +# fitting +LIN = "linear" +QUAD = "quadratic" +POLY = "polynomial" +GAUSS = "gaussian" +EXPO = "exponential" + +# plotting +TITLE = "title" +XNAME = "xname" +YNAME = "yname" +XUNIT = "xunit" +YUNIT = "yunit" +XRANGE = "xrange" + +LEGEND = "legend" +ERROR_BAR = "error_bar" +RESIDUALS = "residuals" +PLOT_STYLE = "plot_style" +PLOT_DIMENSIONS = "plot_dimensions" + +# miscellaneous +DEFAULT = "default" +AUTO = "auto" diff --git a/qexpy/settings/settings.py b/qexpy/settings/settings.py new file mode 100644 index 0000000..e6ae5f7 --- /dev/null +++ b/qexpy/settings/settings.py @@ -0,0 +1,258 @@ +"""Holds all global configurations and Enum types for common options""" + +import functools +from enum import Enum +from typing import Union + +from . import literals as lit + + +class ErrorMethod(Enum): + """Preferred method of error propagation""" + DERIVATIVE = lit.DERIVATIVE + MONTE_CARLO = lit.MONTE_CARLO + AUTO = lit.AUTO + + +class PrintStyle(Enum): + """Preferred format for the string representation of values""" + DEFAULT = lit.DEFAULT + LATEX = lit.LATEX + SCIENTIFIC = lit.SCIENTIFIC + + +class UnitStyle(Enum): + """Preferred format for the string representation of units""" + FRACTION = lit.FRACTION + EXPONENTS = lit.EXPONENTS + + +class SigFigMode(Enum): + """Preferred method to choose number of significant figures""" + AUTOMATIC = lit.AUTO + VALUE = lit.SET_TO_VALUE + ERROR = lit.SET_TO_ERROR + + +class Settings: + """The settings object, implemented as a singleton""" + + __instance = None + + @staticmethod + def get_instance(): + """Gets the Settings singleton instance""" + if not Settings.__instance: + Settings.__instance = Settings() + return Settings.__instance + + def __init__(self): + self.__config = { + lit.ERROR_METHOD: ErrorMethod.DERIVATIVE, + lit.PRINT_STYLE: PrintStyle.DEFAULT, + lit.UNIT_STYLE: UnitStyle.EXPONENTS, + lit.SIG_FIGS: { + lit.SIG_FIG_MODE: SigFigMode.AUTOMATIC, + lit.SIG_FIG_VALUE: 1 + }, + lit.MONTE_CARLO_SAMPLE_SIZE: 100000, + lit.PLOT_DIMENSIONS: (6.4, 4.8) + } + + @property + def error_method(self) -> ErrorMethod: + """ErrorMethod: The preferred error method for derived values + + There are three possible error methods, keep in mind that all three methods are used + to calculate the values behind the scene. The options are found under q.ErrorMethod + + """ + return self.__config[lit.ERROR_METHOD] + + @error_method.setter + def error_method(self, new_method: Union[ErrorMethod, str]): + if isinstance(new_method, ErrorMethod): + self.__config[lit.ERROR_METHOD] = new_method + elif new_method in [lit.MONTE_CARLO, lit.DERIVATIVE]: + self.__config[lit.ERROR_METHOD] = ErrorMethod(new_method) + else: + raise ValueError("Invalid error method!") + + @property + def print_style(self) -> PrintStyle: + """PrintStyle: The preferred format to display a value with an uncertainty + + The three available formats are default, latex, and scientific. The options are found + under q.PrintStyle + + """ + return self.__config[lit.PRINT_STYLE] + + @print_style.setter + def print_style(self, style: Union[PrintStyle, str]): + if isinstance(style, PrintStyle): + self.__config[lit.PRINT_STYLE] = style + elif isinstance(style, str) and style in [lit.DEFAULT, lit.LATEX, lit.SCIENTIFIC]: + self.__config[lit.PRINT_STYLE] = PrintStyle(style) + else: + raise ValueError("Invalid print style!") + + @property + def unit_style(self) -> UnitStyle: + """UnitStyle: The preferred format to display a unit string + + The supported unit styles are "fraction" and "exponents. Fraction style is the more + intuitive way of showing units, looks like kg*m^2/s^2, whereas the exponent style + shows the same unit as kg^1m^2s^-2, which is more accurate and less ambiguous. + + """ + return self.__config[lit.UNIT_STYLE] + + @unit_style.setter + def unit_style(self, style: Union[UnitStyle, str]): + if isinstance(style, UnitStyle): + self.__config[lit.UNIT_STYLE] = style + elif isinstance(style, str) and style in [lit.FRACTION, lit.EXPONENTS]: + self.__config[lit.UNIT_STYLE] = UnitStyle(style) + else: + raise ValueError("Invalid unit style!") + + @property + def sig_fig_mode(self) -> SigFigMode: + """SigFigMode: The standard for choosing number of significant figures + + Supported modes are VALUE and ERROR. When the mode is VALUE, the center value of the + quantity will be displayed with the specified number of significant figures, and the + uncertainty will be displayed to match the number of decimal places of the value, and + vice versa for the ERROR mode. + + """ + return self.__config[lit.SIG_FIGS][lit.SIG_FIG_MODE] + + @property + def sig_fig_value(self) -> int: + """int: The default number of significant figures""" + return self.__config[lit.SIG_FIGS][lit.SIG_FIG_VALUE] + + @sig_fig_value.setter + def sig_fig_value(self, new_value: int): + if isinstance(new_value, int) and new_value > 0: + self.__config[lit.SIG_FIGS][lit.SIG_FIG_VALUE] = new_value + else: + raise ValueError("The number of significant figures must be a positive integer") + + def set_sig_figs_for_value(self, new_sig_figs: int): + """Sets the number of significant figures to show for all values""" + self.sig_fig_value = new_sig_figs + self.__config[lit.SIG_FIGS][lit.SIG_FIG_MODE] = SigFigMode.VALUE + + def set_sig_figs_for_error(self, new_sig_figs: int): + """Sets the number of significant figures to show for uncertainties""" + self.sig_fig_value = new_sig_figs + self.__config[lit.SIG_FIGS][lit.SIG_FIG_MODE] = SigFigMode.ERROR + + @property + def monte_carlo_sample_size(self) -> int: + """int: The default sample size used in Monte Carlo error propagation""" + return self.__config[lit.MONTE_CARLO_SAMPLE_SIZE] + + @monte_carlo_sample_size.setter + def monte_carlo_sample_size(self, size: int): + if isinstance(size, int) and size > 0: + self.__config[lit.MONTE_CARLO_SAMPLE_SIZE] = size + else: + raise ValueError("The sample size has to be a positive integer") + + @property + def plot_dimensions(self) -> (float, float): + """The default dimensions of a plot in inches""" + return self.__config[lit.PLOT_DIMENSIONS] + + @plot_dimensions.setter + def plot_dimensions(self, new_dimensions: (float, float)): + if not isinstance(new_dimensions, tuple) or len(new_dimensions) != 2: + raise ValueError("The plot dimensions must be a tuple with two entries") + if any(not isinstance(num, (int, float)) or num <= 0 for num in new_dimensions): + raise ValueError("The dimensions of the plot must be numeric") + self.__config[lit.PLOT_DIMENSIONS] = new_dimensions + + def reset(self): + """Resets all configurations to their default values""" + self.__config[lit.ERROR_METHOD] = ErrorMethod.DERIVATIVE + self.__config[lit.PRINT_STYLE] = PrintStyle.DEFAULT + self.__config[lit.SIG_FIGS][lit.SIG_FIG_MODE] = SigFigMode.AUTOMATIC + self.__config[lit.SIG_FIGS][lit.SIG_FIG_VALUE] = 1 + self.__config[lit.UNIT_STYLE] = UnitStyle.EXPONENTS + self.__config[lit.MONTE_CARLO_SAMPLE_SIZE] = 10000 + self.__config[lit.PLOT_DIMENSIONS] = (6.4, 4.8) + + +def get_settings() -> Settings: + """Gets the settings singleton instance""" + return Settings.get_instance() + + +def reset_default_configuration(): + """Resets all configurations to their default values""" + get_settings().reset() + + +def set_error_method(new_method: Union[ErrorMethod, str]): + """Sets the preferred error propagation method for values""" + get_settings().error_method = new_method + + +def set_print_style(new_style: Union[PrintStyle, str]): + """Sets the format to display the value strings for ExperimentalValues""" + get_settings().print_style = new_style + + +def set_unit_style(new_style: Union[UnitStyle, str]): + """Change the format for presenting units""" + get_settings().unit_style = new_style + + +def set_sig_figs_for_value(new_sig_figs: int): + """Sets the number of significant figures to show for all values""" + get_settings().set_sig_figs_for_value(new_sig_figs) + + +def set_sig_figs_for_error(new_sig_figs: int): + """Sets the number of significant figures to show for uncertainties""" + get_settings().set_sig_figs_for_error(new_sig_figs) + + +def set_monte_carlo_sample_size(size: int): + """Sets the number of samples for a Monte Carlo simulation""" + get_settings().monte_carlo_sample_size = size + + +def set_plot_dimensions(new_dimensions: (float, float)): + """Sets the default dimensions of a plot""" + get_settings().plot_dimensions = new_dimensions + + +def use_mc_sample_size(size: int): + """Wrapper decorator that temporarily sets the monte carlo sample size""" + + def set_monte_carlo_sample_size_wrapper(func): + """Inner wrapper decorator""" + + @functools.wraps(func) + def inner_wrapper(*args): + # preserve the original sample size and set the sample size to new value + temp_size = get_settings().monte_carlo_sample_size + set_monte_carlo_sample_size(size) + + # run the function + result = func(*args) + + # restores the original sample size + set_monte_carlo_sample_size(temp_size) + + # return function output + return result + + return inner_wrapper + + return set_monte_carlo_sample_size_wrapper diff --git a/qexpy/utils.py b/qexpy/utils.py deleted file mode 100644 index bf62d9f..0000000 --- a/qexpy/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -def in_notebook(): - '''Simple function to check if module is loaded in a notebook''' - try: - __IPYTHON__ - return True - except NameError: - return False - -#Global variable to keep track of whether the output_notebook command was run -bokeh_ouput_notebook_called = False -mpl_ouput_notebook_called = False - -def mpl_output_notebook(): - from IPython import get_ipython - ipython = get_ipython() - #ipython.magic('matplotlib inline') - mpl_ouput_notebook_called = True - -def get_data_from_file(path, delim=','): - '''Reads data from a file, splitting rows at the delimiter. - Returns data in a 2-dimensional numpy array. - ''' - import csv - with open(path, newline='') as openfile: - reader = csv.reader(openfile, delimiter=delim) - data=[] - for row in reader: - for col in range(len(row)): - append = True - try: - row[col] = float(row[col]) - except ValueError: - append=False - break - if append: - data.append(row) - ret = np.transpose(np.array(data, dtype=float)) - return ret - -#These are used for checking whether something is an instance of -#an array or a number. -import numpy as np -number_types = (int, float, np.int8, np.int16, np.int32, np.int64,\ - np.uint8, np.uint16, np.uint32, np.uint64,\ - np.float16, np.float32, np.float64\ - ) -int_types = (int, np.int8, np.int16, np.int32, np.int64,\ - np.uint8, np.uint16, np.uint32, np.uint64\ - ) -array_types = (tuple, list, np.ndarray) \ No newline at end of file diff --git a/qexpy/utils/__init__.py b/qexpy/utils/__init__.py new file mode 100644 index 0000000..7ddc8ed --- /dev/null +++ b/qexpy/utils/__init__.py @@ -0,0 +1,15 @@ +"""Package containing utility functions mostly for internal use""" + +from .utils import load_data_from_file +from .utils import vectorize, check_operand_type, validate_xrange +from .utils import numerical_derivative, calculate_covariance, cov2corr, \ + find_mode_and_uncertainty +from .exceptions import IllegalArgumentError, UndefinedActionError, UndefinedOperationError +from .units import parse_unit_string, construct_unit_string, operate_with_units +from .printing import get_printer + +import sys +import IPython + +if "ipykernel" in sys.modules: # pragma: no cover + IPython.get_ipython().magic("matplotlib inline") diff --git a/qexpy/utils/exceptions.py b/qexpy/utils/exceptions.py new file mode 100644 index 0000000..f2e9194 --- /dev/null +++ b/qexpy/utils/exceptions.py @@ -0,0 +1,26 @@ +"""Definitions for internal exceptions in QExPy""" + + +class QExPyBaseError(Exception): + """The base error type for QExPy""" + + +class IllegalArgumentError(QExPyBaseError): + """Exception for invalid arguments""" + + +class UndefinedActionError(QExPyBaseError): + """Exception for undefined system states or function calls""" + + +class UndefinedOperationError(UndefinedActionError): + """Exception for undefined arithmetic operations between values""" + + def __init__(self, op, got, expected): + """Defines the standard format for the error message""" + + got_types = " and ".join("\'{}\'".format(type(x).__name__) for x in got) + message = "\"{}\" is undefined with operands of type(s) {}. " \ + "Expected: {}".format(op, got_types, expected) + + super().__init__(message) diff --git a/qexpy/utils/printing.py b/qexpy/utils/printing.py new file mode 100644 index 0000000..22a1bc9 --- /dev/null +++ b/qexpy/utils/printing.py @@ -0,0 +1,173 @@ +"""Utility methods for generating the string representation of value-error pairs""" + +import math as m + +from typing import Callable +from qexpy.settings import PrintStyle, SigFigMode + +import qexpy.settings as sts + + +def get_printer(print_style: PrintStyle = None) -> Callable[[float, float], str]: + """Gets the printer for the given print style + + If the print style is not specified, the global setting will be used. + + Args: + print_style (PrintStyle): The desired print style. + + Returns: + A printer function that takes two numbers as inputs for value and uncertainty and + returns the string representation of the value-error pair + + """ + if not print_style: + print_style = sts.get_settings().print_style + if print_style == PrintStyle.SCIENTIFIC: + return __scientific_printer + if print_style == PrintStyle.LATEX: + return __latex_printer + return __default_printer + + +def __default_printer(value: float, error: float, latex=False) -> str: + """Prints out the value and uncertainty in its default format""" + + pm = r"\pm" if latex else "+/-" + + if value == 0 and error == 0: + return "0 {} 0".format(pm) + if m.isinf(value): + return "inf {} inf".format(pm) + + # Round the values based on significant digits + rounded_value, rounded_error = __round_values_to_sig_figs(value, error) + + # Check if the number of decimals matches the requirement of significant figures + decimals = __find_number_of_decimals(rounded_value, rounded_error) + + # Construct the string to return + value_string = "{:.{num}f}".format(rounded_value, num=decimals) + error_string = "{:.{num}f}".format(rounded_error, num=decimals) if error != 0 else "0" + return "{} {} {}".format(value_string, pm, error_string) + + +def __latex_printer(value: float, error: float) -> str: + """Prints out the value and uncertainty in latex format""" + return __scientific_printer(value, error, latex=True) + + +def __scientific_printer(value: float, error: float, latex=False) -> str: + """Prints out the value and uncertainty in scientific notation""" + + pm = r"\pm" if latex else "+/-" + + if value == 0 and error == 0: + return "0 {} 0".format(pm) + if m.isinf(value): + return "inf {} inf".format(pm) + + # Find order of magnitude + order = m.floor(m.log10(value)) + if order == 0: + return __default_printer(value, error, latex) + + # Round the values based on significant digits + rounded_value, rounded_error = __round_values_to_sig_figs(value, error) + + # Convert to scientific notation + converted_value = rounded_value / (10 ** order) + converted_error = rounded_error / (10 ** order) + + # Check if the number of decimals matches the requirement of significant figures + decimals = __find_number_of_decimals(converted_value, converted_error) + + # Construct the string to return + value_string = "{:.{num}f}".format(converted_value, num=decimals) + error_string = "{:.{num}f}".format(converted_error, num=decimals) if error != 0 else "0" + return "({} {} {}) * 10^{}".format(value_string, pm, error_string, order) + + +def __round_values_to_sig_figs(value: float, error: float) -> (float, float): + """Rounds the value and uncertainty based on sig-fig settings + + This method works by first finding the order of magnitude for the error, or the value, + depending on the sig-fig settings, and calculates a value called back-off. For example, + to round 12345 to 3 significant figures, log10(12345) would return 4, which is the order + of magnitude of the number. The formula for the back-off is: + + back-off = order_of_magnitude - significant_digits + 1. + + In this case, the back-off would 4 - 3 + 1 = 2. With the back-off, we first divide 12345 + by 10^2, which results in 123.45, then round it to 123, before multiplying the back-off, + which produces 12300 + + Args: + value (float): the value of the quantity to be rounded + error (float): the uncertainty to be rounded + + Returns: + the rounded results for this pair + + """ + + sig_fig_mode = sts.get_settings().sig_fig_mode + sig_fig_value = sts.get_settings().sig_fig_value + + def is_valid(number): + return not m.isinf(number) and not m.isnan(number) and number != 0 + + # Check any of the inputs are invalid for the following calculations + if sig_fig_mode in [SigFigMode.AUTOMATIC, SigFigMode.ERROR] and not is_valid(error): + return value, error # do no rounding if the error is 0 or invalid + if sig_fig_mode == SigFigMode.VALUE and not is_valid(value): + return value, error # do no rounding if the value is 0 or invalid + + # First find the back-off value for rounding + if sig_fig_mode in [SigFigMode.AUTOMATIC, SigFigMode.ERROR]: + order_of_error = m.floor(m.log10(error)) + back_off = 10 ** (order_of_error - sig_fig_value + 1) + else: + order_of_value = m.floor(m.log10(value)) + back_off = 10 ** (order_of_value - sig_fig_value + 1) + + # Then round the value and error to the same digit + rounded_error = round(error / back_off) * back_off + rounded_value = round(value / back_off) * back_off + + # Return the two rounded values + return rounded_value, rounded_error + + +def __find_number_of_decimals(value: float, error: float) -> int: + """Finds the correct number of decimal places to show for a value-error pair + + This method checks the settings for significant figures and tweaks the already rounded + value and error to having the correct number of significant figures. For example, if the + value of a variable is 5.001, and 3 significant figures is requested. After rounding, the + value would become 5. However, if we want it to be represented as 5.00, we need to find + the proper number of digits after the decimal. + + The implementation is similar to that of the rounding algorithm described in the method + above. The key is to start counting significant figures from the most significant digit, + which is calculated by finding the order of magnitude of the value. + + See Also: + __round_values_to_sig_figs + + """ + + sig_fig_mode = sts.get_settings().sig_fig_mode + sig_fig_value = sts.get_settings().sig_fig_value + + def is_valid(number): + return not m.isinf(number) and not m.isnan(number) and number != 0 + + # Check if the current number of significant figures satisfy the settings + if sig_fig_mode in [SigFigMode.AUTOMATIC, SigFigMode.ERROR]: + order = m.floor(m.log10(error)) if is_valid(error) else m.floor(m.log10(value)) + else: + order = m.floor(m.log10(value)) if is_valid(value) else m.floor(m.log10(error)) + + number_of_decimals = - order + sig_fig_value - 1 + return number_of_decimals if number_of_decimals > 0 else 0 diff --git a/qexpy/utils/units.py b/qexpy/utils/units.py new file mode 100644 index 0000000..d498b4b --- /dev/null +++ b/qexpy/utils/units.py @@ -0,0 +1,321 @@ +"""Internal module used for unit parsing and propagation""" + +import re +import warnings + +from typing import Dict, List, Union +from collections import namedtuple, OrderedDict +from qexpy.settings import UnitStyle +from copy import deepcopy +from fractions import Fraction + +import qexpy.settings as sts +import qexpy.settings.literals as lit + +# The standard character used in a dot multiply expression +DOT_STRING = "⋅" + +# A sub-tree in a binary expression tree representing a unit expression. The "operator" is +# the root node of the sub-tree, and the "left" and "right" points to the two branches. The +# leaf nodes of a unit expression tree are either unit strings or their powers. +Expression = namedtuple("Expression", "operator, left, right") + + +def parse_unit_string(unit_string: str) -> Dict[str, int]: + """Decodes the string representation of a set of units + + This function parses the unit string into a binary expression tree, evaluate the tree to + find all units present in the string and their powers, which is then stored in a Python + dictionary object. + + The units are parsed to the following rules: + 1. Expressions enclosed in brackets are evaluated first + 2. A unit with its power (e.g. "m^2") are always evaluated together + 3. Expressions connected with implicit multiplication are evaluated together + + For example, "kg*m^2/s^2A^2" would be decoded to: {"kg": 1, "m": 2, "s": -2, "A": -2} + + Args: + unit_string (str): The string to be parsed + + Returns: + A dictionary object that stores the power of each unit in the expression + + """ + tokens = __parse_unit_string_to_list(unit_string) + ast = __construct_expression_tree_with_list(tokens) + return __evaluate_unit_tree(ast) + + +def construct_unit_string(units: Dict[str, int]) -> str: + """Constructs the string representation of a set of units + + Units can be displayed in two different formats: Fraction and Exponents. The function + retrieves the global settings for unit styles and construct the string accordingly. + + Args: + units (dict): A dictionary object representing a set of units + + Returns: + The string representation of the units + + """ + unit_string = "" + if sts.get_settings().unit_style == UnitStyle.FRACTION: + unit_string = __construct_unit_string_as_fraction(units) + if sts.get_settings().unit_style == UnitStyle.EXPONENTS: + unit_string = __construct_unit_string_with_exponents(units) + return unit_string + + +def operate_with_units(operator, *operands): + """perform an operation with two sets of units""" + + result = UNIT_OPERATIONS[operator](*operands) if operator in UNIT_OPERATIONS else {} + # filter for non-zero values + return OrderedDict([(unit, count) for unit, count in result.items() if count != 0]) + + +def __parse_unit_string_to_list(unit_string: str) -> List[Union[str, List]]: + """Parse a unit string into a list of tokens + + A token can be a single unit, an operator such as "*" or "/" or "^", a number indicating + the power of a unit, or a list of tokens grouped together. For example, kg*m/s^2A^2 would + be parsed into: ["kg", "*", "m", "/", [["s", "^", "2"], "*", ["A", "^", "2"]]] + + """ + + unit_string = unit_string.replace("⋅", "*") # replace dots with multiplication sign + + raw_tokens_list = [] # The raw list of tokens + tokens_list = [] # The final list of tokens + + token_pattern = re.compile(r"[a-zA-Z]+(\^-?[0-9]+)?|/|\*|\(.*?\)") + bracket_enclosed_expression_pattern = re.compile(r"\(.*?\)") + unit_with_exponent_pattern = re.compile(r"[a-zA-Z]+\^-?[0-9]+") + operator_pattern = re.compile(r"[/*]") + + # Check if the input only consists of valid token strings + if not re.fullmatch(r"({})+".format(token_pattern.pattern), unit_string): + raise ValueError("\"{}\" is not a valid unit".format(unit_string)) + + # For every token found, process it and append it to the list + for result in token_pattern.finditer(unit_string): + token = result.group() + if bracket_enclosed_expression_pattern.fullmatch(token): + # If the token is a bracket enclosed expression, recursively parse the content of + # that bracket and append it to the tokens list as a list + raw_tokens_list.append(__parse_unit_string_to_list(token[1:-1])) + elif unit_with_exponent_pattern.fullmatch(token): + # Group a unit with exponent together and append to the list as a whole + unit_and_exponent = token.split("^") + raw_tokens_list.append([unit_and_exponent[0], "^", unit_and_exponent[1]]) + else: + raw_tokens_list.append(token) + + # At this stage, except for when an explicit bracket is present, no grouping of tokens + # has occurred yet. The following code checks for expressions connected with implicit + # multiplication, and groups them together (also adding a multiplication operator). The + # following flag keeps track of if there is an operator present between the current token + # and the last expression being processed, if not, assume implicit multiplication. + preceding_operator_exists = True + + for token in raw_tokens_list: + if preceding_operator_exists: + tokens_list.append(token) + preceding_operator_exists = False + elif isinstance(token, str) and operator_pattern.fullmatch(token): + tokens_list.append(token) + preceding_operator_exists = True + else: + # When there is no preceding operator, and the current token is not an operator, + # add multiplication sign, and group this item with the previous one. + last_token = tokens_list.pop() + tokens_list.append([last_token, "*", token]) + preceding_operator_exists = False + + return tokens_list + + +def __construct_expression_tree_with_list(tokens: List[Union[str, List]]) -> Expression: + """Build a binary expression tree with a list of tokens + + The algorithm to construct the tree is called recursive descent, which made use of two + stacks. The operator stack and the operand stack. For each new token, if the token is an + operator, it is compared with the current top of the operator stack. The operator stack + is maintained so that the top of the stack has higher priority in order of operations + compared to the rest of the stack. If the current top has higher priority compared to the + operator being processed, it is popped from the stack, used to build a sub-tree with the + top two operands in the operand stack, and pushed into the operand stack. + + For details regarding this algorithm, see the reference below. + + Reference: + Parsing Expressions by Recursive Descent - Theodore Norvell (C) 1999 + https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm + + Args: + tokens (list): The list of tokens to process. + + Returns: + The expression tree representing the set of units. For more details regarding the + structure of the tree, see top of this file where the Expression type is defined. + + """ + + # Initialize the two stacks + operand_stack = [] # type: List[Union[Expression, str]] + operator_stack = ["base"] # type: List[str] + + # Define the order of operations + precedence = { + "base": 0, + "*": 1, + "/": 1, + "^": 2 + } + + def __construct_sub_tree_and_push_to_operand_stack(): + right = operand_stack.pop() + left = operand_stack.pop() + operator = operator_stack.pop() + operand_stack.append(Expression(operator, left, right)) + + # Push all tokens into the two stacks, make sub-trees if necessary + for token in tokens: + top_of_operators = operator_stack[-1] + if isinstance(token, list): + # Recursively make sub-tree with grouped expressions + operand_stack.append(__construct_expression_tree_with_list(token)) + elif token in precedence and precedence[token] > precedence[top_of_operators]: + operator_stack.append(token) # Push the higher priority operator on top + elif token in precedence and precedence[token] <= precedence[top_of_operators]: + # If an operator with lower precedence is being processed, make a sub-tree + # with the current top of the operator stack and push it to the operands. + __construct_sub_tree_and_push_to_operand_stack() + operator_stack.append(token) # This operator becomes the new top + else: + operand_stack.append(token) + + # Create the final tree from all the tokens and sub-trees left in the stacks + while len(operator_stack) > 1: + __construct_sub_tree_and_push_to_operand_stack() + + return operand_stack[0] if operand_stack else Expression("", "", "") + + +def __evaluate_unit_tree(tree: Expression) -> Dict[str, int]: + """Construct a unit dictionary object from an expression tree + + Args: + tree (Expression): the expression tree to be evaluated + + Returns: + All units in the tree and their powers stored in a dictionary object + + """ + units = OrderedDict() + if isinstance(tree, Expression) and tree.operator == "^": + # When a unit with an exponent is found, add it to the dictionary object + units[tree.left] = int(tree.right) + elif isinstance(tree, Expression) and tree.operator in ["*", "/"]: + for unit, exponent in __evaluate_unit_tree(tree.left).items(): + units[unit] = exponent + for unit, exponent in __evaluate_unit_tree(tree.right).items(): + start_exponent_from = units[unit] if unit in units else 0 + plus_or_minus = 1 if tree.operator == "*" else -1 + units[unit] = start_exponent_from + plus_or_minus * exponent + else: # just a string then count it + units[tree] = 1 + return units + + +def __construct_unit_string_as_fraction(units: Dict[str, int]) -> str: + """Construct a unit string in the fraction format""" + + numerator_units = ["{}{}".format( + unit, __power_num2str(power)) for unit, power in units.items() if power > 0] + denominator_units = ["{}{}".format( + unit, __power_num2str(-power)) for unit, power in units.items() if power < 0] + + numerator_string = DOT_STRING.join(numerator_units) if numerator_units else "1" + denominator_string = DOT_STRING.join(denominator_units) + + if not denominator_units: + return numerator_string if numerator_units else "" + if len(denominator_units) > 1: + # For multiple units in the denominator, use brackets to avoid ambiguity + return "{}/({})".format(numerator_string, denominator_string) + + return "{}/{}".format(numerator_string, denominator_string) + + +def __construct_unit_string_with_exponents(units: Dict[str, int]) -> str: + """Construct a unit string in the exponent format""" + unit_strings = ["{}{}".format( + unit, __power_num2str(power)) for unit, power in units.items()] + return DOT_STRING.join(unit_strings) + + +def __power_num2str(power) -> str: + """Construct a string for the power of a unit""" + + fraction = Fraction(power).limit_denominator(10) + if fraction.numerator == 1 and fraction.denominator == 1: + return "" # do not print power of 1 as it's implied + if fraction.denominator == 1: + return "^{}".format(str(fraction.numerator)) + return "^({})".format(str(fraction)) + + +def __neg(units): + return deepcopy(units) + + +def __add_and_sub(units_var1, units_var2): + if units_var1 and units_var2 and units_var1 != units_var2: + warnings.warn("You're trying to add/subtract two values with mismatching units.") + return OrderedDict() + if not units_var1: # If any of the two units are empty, use the other one + return deepcopy(units_var2) + return deepcopy(units_var1) + + +def __mul(units_var1, units_var2): + units = OrderedDict() + for unit, exponent in units_var1.items(): + __update_unit_exponent_count_in_dict(units, unit, exponent) + for unit, exponent in units_var2.items(): + __update_unit_exponent_count_in_dict(units, unit, exponent) + return units + + +def __div(units_var1, units_var2): + units = OrderedDict() + for unit, exponent in units_var1.items(): + __update_unit_exponent_count_in_dict(units, unit, exponent) + for unit, exponent in units_var2.items(): + __update_unit_exponent_count_in_dict(units, unit, -exponent) + return units + + +def __sqrt(units): + new_units = OrderedDict() + for unit, exponent in units.items(): + new_units[unit] = exponent / 2 + return new_units + + +def __update_unit_exponent_count_in_dict(unit_dict, unit_string, change): + current_count = 0 if unit_string not in unit_dict else unit_dict[unit_string] + unit_dict[unit_string] = current_count + change + + +UNIT_OPERATIONS = { + lit.NEG: __neg, + lit.ADD: __add_and_sub, + lit.SUB: __add_and_sub, + lit.MUL: __mul, + lit.DIV: __div, + lit.SQRT: __sqrt +} diff --git a/qexpy/utils/utils.py b/qexpy/utils/utils.py new file mode 100644 index 0000000..935a9f4 --- /dev/null +++ b/qexpy/utils/utils.py @@ -0,0 +1,115 @@ +"""Miscellaneous utility functions""" + +import functools +import csv + +import numpy as np + +from typing import Callable +from numbers import Real +from .exceptions import UndefinedOperationError + + +def check_operand_type(operation): + """wrapper decorator for undefined operation error reporting""" + + def check_operand_type_wrapper(func): + + @functools.wraps(func) + def operation_wrapper(*args): + try: + return func(*args) + except TypeError: + raise UndefinedOperationError(operation, got=args, expected="real numbers") + + return operation_wrapper + + return check_operand_type_wrapper + + +def vectorize(func): + """vectorize a function if inputs are arrays""" + + @functools.wraps(func) + def wrapper_vectorize(*args): + if any(isinstance(arg, np.ndarray) for arg in args): + return np.vectorize(func)(*args) + if any(isinstance(arg, list) for arg in args): + return np.vectorize(func)(*args).tolist() + return func(*args) + + return wrapper_vectorize + + +def validate_xrange(xrange): + """validates that an xrange is legal""" + + if not isinstance(xrange, (tuple, list)) or len(xrange) != 2: + raise TypeError("The \"xrange\" should be a list or tuple of length 2") + + if any(not isinstance(value, Real) for value in xrange): + raise TypeError("The \"xrange\" must be real numbers") + + if xrange[0] > xrange[1]: + raise ValueError("The low bound of xrange is higher than the high bound") + + return True + + +@vectorize +def numerical_derivative(function: Callable, x0: Real, dx=1e-5): + """Calculates the numerical derivative of a function with respect to x at x0""" + return (function(x0 + dx) - function(x0 - dx)) / (2 * dx) + + +def calculate_covariance(arr_x, arr_y): + """Calculates the covariance of two arrays""" + if len(arr_x) != len(arr_y): + raise ValueError("Cannot calculate covariance for arrays of different lengths.") + return 1 / (len(arr_x) - 1) * sum( + ((x - np.mean(arr_x)) * (y - np.mean(arr_y)) for x, y in zip(arr_x, arr_y))) + + +def cov2corr(pcov: np.ndarray) -> np.ndarray: + """Calculate a correlation matrix from a covariance matrix""" + std = np.sqrt(np.diag(pcov)) + return pcov / np.outer(std, std) + + +def find_mode_and_uncertainty(n, bins, confidence) -> (float, float): + """Find the mode and uncertainty with a confidence of a histogram distribution""" + number_of_samples = sum(n) + max_idx = n.argmax() + value = (bins[max_idx] + bins[max_idx + 1]) / 2 + count = n[max_idx] + low_idx, high_idx = max_idx, max_idx + while count < confidence * number_of_samples: + low_idx -= 1 + high_idx += 1 + count += n[low_idx] + n[high_idx] + error = (bins[high_idx] + bins[high_idx + 1]) / 2 - value + return value, error + + +def load_data_from_file(filepath: str, delimiter=",") -> np.ndarray: + """Reads arrays of data from a file + + The file should be structured like a csv file. The delimiter can be replaced with other + characters, but the default is comma. The function returns an array of arrays, one for + each column in the table of numbers. + + Args: + filepath (str): The name of the file to read from + delimiter (str): The delimiter that separates each row + + Returns: + A 2-dimensional np.ndarray where each array is a column in the file + + """ + with open(filepath, newline='') as openfile: + reader = csv.reader(openfile, delimiter=delimiter) + # read file into array of rows + rows_of_data = list([float(entry) for entry in row] for row in reader) + # transpose data into array of columns + result = np.transpose(np.array(rows_of_data, dtype=float)) + return result diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8eee58e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +numpy +matplotlib +scipy +IPython +pylint +pytest +coverage +jupyterlab +sphinx +nbsphinx +nbsphinx_link +sphinx_autodoc_typehints +sphinx_rtd_theme diff --git a/ryantest.py b/ryantest.py deleted file mode 100644 index 80364bf..0000000 --- a/ryantest.py +++ /dev/null @@ -1,6 +0,0 @@ -import qexpy as q -q.plot_engine='mpl' - -fig = q.MakePlot(xdata=[1,2,3,4,5],ydata=[2,4,5,8,11], yerr=1) -fig.fit("pol2") -fig.show() \ No newline at end of file diff --git a/setup.py b/setup.py index 0766f56..004be4f 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,16 @@ -from distutils.core import setup +from setuptools import setup, find_packages setup( name='qexpy', - packages=['qexpy'], - version='1.0.4', - description='''Package to handle error analysis and data plotting aimed - at undergraduate physics.''', - author='Connor Kapahi and Prof. Ryan Martin', - author_email='ryan.martin@queensu.ca', + packages=find_packages(), + version='3.0.0', + description='''Package to handle error analysis and data plotting aimed at undergraduate physics.''', + author='Astral Cai, Connor Kapahi, Prof. Ryan Martin', + author_email='ryan.martin@queensu.ca, astral.cai@queensu.ca', license='GNU GLP v3', url='https://github.com/Queens-Physics/QExPy', - download_url='https://github.com/Queens-Physics/QExPy/tarball/0.3.7', - keywords=['physics', 'laboratories', 'labs', 'undergraduate', - 'data analysis', 'uncertainties', 'plotting', 'error analysis', - 'error propagation', 'uncertainty propagation'], + keywords=['physics', 'laboratories', 'labs', 'undergraduate', 'data analysis', 'uncertainties', 'plotting', + 'error analysis', 'error propagation', 'uncertainty propagation'], classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', @@ -21,5 +18,9 @@ 'License :: OSI Approved :: GNU General Public License (GPL)', 'Programming Language :: Python', ], - install_requires=['numpy','matplotlib', 'ipywidgets', 'scipy>=0.17', 'bokeh>=0.12.1', 'pandas'], + install_requires=['numpy', 'matplotlib', 'scipy', 'IPython'], + extras_require={ + 'dev': ['pylint', 'pytest'], + 'doc': ['jupyterlab', 'sphinx', 'nbsphinx', 'nbsphinx_link', 'sphinx_autodoc_typehints', 'sphinx_rtd_theme'] + } ) diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000..fbff3d8 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,12 @@ +[run] +branch = True + +omit = + */docs/* + */plotting/* + +[report] +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..70492dc --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +adopts = -v diff --git a/tests/resources/data_for_test_load_data.csv b/tests/resources/data_for_test_load_data.csv new file mode 100644 index 0000000..587c271 --- /dev/null +++ b/tests/resources/data_for_test_load_data.csv @@ -0,0 +1,30 @@ +1,0.5,1.00,0.02 +2,0.5,2.30,0.02 +3,0.5,3.48,0.02 +4,0.5,4.60,0.02 +5,0.5,5.70,0.02 +6,0.5,6.78,0.02 +7,0.5,7.85,0.02 +8,0.5,8.90,0.02 +9,0.5,9.95,0.02 +10,0.5,11.00,0.02 +11,0.5,12.04,0.02 +12,0.5,13.08,0.02 +13,0.5,14.11,0.02 +14,0.5,15.15,0.02 +15,0.5,16.18,0.02 +16,0.5,17.20,0.02 +17,0.5,18.23,0.02 +18,0.5,19.26,0.02 +19,0.5,20.28,0.02 +20,0.5,21.30,0.02 +21,0.5,22.32,0.02 +22,0.5,23.34,0.02 +23,0.5,24.36,0.02 +24,0.5,25.38,0.02 +25,0.5,26.40,0.02 +26,0.5,27.41,0.02 +27,0.5,28.43,0.02 +28,0.5,29.45,0.02 +29,0.5,30.46,0.02 +30,0.5,31.48,0.02 diff --git a/tests/test_datasets.py b/tests/test_datasets.py new file mode 100644 index 0000000..c6a7d4f --- /dev/null +++ b/tests/test_datasets.py @@ -0,0 +1,194 @@ +"""Unit tests for taking arrays of measurements""" + +import pytest +import qexpy as q +import numpy as np + +from qexpy.utils.exceptions import IllegalArgumentError + +from qexpy.data.datasets import ExperimentalValueArray + + +class TestExperimentalValueArray: + """tests for the ExperimentalValueArray class""" + + def test_record_measurement_array(self): + """tests for recording a measurement array in different ways""" + + a = q.MeasurementArray([1, 2, 3, 4, 5]) + assert isinstance(a, ExperimentalValueArray) + assert all(a.values == [1, 2, 3, 4, 5]) + assert all(a.errors == [0, 0, 0, 0, 0]) + assert str(a) == "[ 1 +/- 0, 2 +/- 0, 3 +/- 0, 4 +/- 0, 5 +/- 0 ]" + + with pytest.raises(TypeError): + q.MeasurementArray(1) + + b = q.MeasurementArray([1, 2, 3, 4, 5], 0.5) + assert all(b.errors == [0.5, 0.5, 0.5, 0.5, 0.5]) + c = q.MeasurementArray([1, 2, 3, 4, 5], relative_error=0.1) + assert c.errors == pytest.approx([0.1, 0.2, 0.3, 0.4, 0.5]) + d = q.MeasurementArray([1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4, 0.5]) + assert all(d.errors == [0.1, 0.2, 0.3, 0.4, 0.5]) + e = q.MeasurementArray([1, 2, 3, 4, 5], relative_error=[0.1, 0.2, 0.3, 0.4, 0.5]) + assert e.errors == pytest.approx([0.1, 0.4, 0.9, 1.6, 2.5]) + + with pytest.raises(ValueError): + q.MeasurementArray([1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4]) + with pytest.raises(ValueError): + q.MeasurementArray([1, 2, 3, 4, 5], relative_error=[0.1, 0.2, 0.3, 0.4]) + with pytest.raises(TypeError): + q.MeasurementArray([1, 2, 3, 4, 5], '1') + with pytest.raises(TypeError): + q.MeasurementArray([1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4, '0.5']) + with pytest.raises(TypeError): + q.MeasurementArray([1, 2, 3, 4, '5']) + with pytest.raises(ValueError): + q.MeasurementArray([1, 2, 3, 4, 5], -0.5) + with pytest.raises(ValueError): + q.MeasurementArray([1, 2, 3, 4, 5], [0.5, 0.5, 0.5, 0.5, -0.5]) + + f = q.MeasurementArray(data=[1, 2, 3, 4], error=0.5, name="test", unit="m") + assert f.name == "test" + assert f.unit == "m" + assert str(f) == "test = [ 1.0 +/- 0.5, 2.0 +/- 0.5, 3.0 +/- 0.5, 4.0 +/- 0.5 ] (m)" + assert str(f[0]) == "test_0 = 1.0 +/- 0.5 [m]" + assert str(f[-1]) == "test_3 = 4.0 +/- 0.5 [m]" + + g = q.MeasurementArray( + [q.Measurement(5, 0.5), q.Measurement(10, 0.5)], name="test", unit="m") + assert str(g[-1]) == "test_1 = 10.0 +/- 0.5 [m]" + + h = q.MeasurementArray([q.Measurement(5, 0.5), q.Measurement(10, 0.5)], error=0.1) + assert str(h[-1]) == "10.0 +/- 0.1" + + def test_manipulate_measurement_array(self): + """tests for manipulating a measurement array""" + + a = q.MeasurementArray([1, 2, 3, 4], 0.5, name="test", unit="m") + a = a.append(q.Measurement(5, 0.5)) + assert str(a[-1]) == "test_4 = 5.0 +/- 0.5 [m]" + a = a.insert(1, (1.5, 0.5)) + assert str(a[1]) == "test_1 = 1.5 +/- 0.5 [m]" + assert str(a[-1]) == "test_5 = 5.0 +/- 0.5 [m]" + a = a.delete(1) + assert str(a[1]) == "test_1 = 2.0 +/- 0.5 [m]" + + with pytest.raises(TypeError): + a.name = 1 + with pytest.raises(TypeError) as e: + a.unit = 1 + assert str(e.value) == "Cannot set unit to \"int\"!" + + a.name = "speed" + a.unit = "m/s" + assert a.name == "speed" + assert a.unit == "m⋅s^-1" + assert str(a[4]) == "speed_4 = 5.0 +/- 0.5 [m⋅s^-1]" + + a = a.append(6) + assert str(a[5]) == "speed_5 = 6 +/- 0 [m⋅s^-1]" + + a[3] = 10 + assert str(a[3]) == "speed_3 = 10.0 +/- 0.5 [m⋅s^-1]" + a[4] = (10, 0.6) + assert str(a[4]) == "speed_4 = 10.0 +/- 0.6 [m⋅s^-1]" + + with pytest.raises(TypeError): + a[2] = 'a' + + b = q.MeasurementArray([5, 6, 7], 0.5) + b[-1] = (8, 0.5) + + a = a.append(b) + assert str(a[-1]) == "speed_8 = 8.0 +/- 0.5 [m⋅s^-1]" + + a = a.append([8, 9, 10]) + assert str(a[-1]) == "speed_11 = 10 +/- 0 [m⋅s^-1]" + + def test_calculations_with_measurement_array(self): + """tests for calculating properties of a measurement array""" + + a = q.MeasurementArray([1, 2, 3, 4, 5]) + assert a.mean() == 3 + assert a.std() == pytest.approx(1.58113883008419) + assert a.sum() == 15 + assert a.error_on_mean() == pytest.approx(0.707106781186548) + + with pytest.warns(UserWarning): + assert np.isnan(a.error_weighted_mean()) + with pytest.warns(UserWarning): + assert np.isnan(a.propagated_error()) + + b = q.MeasurementArray([1, 2, 3, 4, 5], [0.1, 0.2, 0.3, 0.4, 0.5]) + assert b.error_weighted_mean() == pytest.approx(1.5600683241601823) + assert b.propagated_error() == pytest.approx(0.08265842980736918) + + +class TestXYDataSet: + """tests for the XYDataSet class""" + + def test_construct_data_set(self): + """test for various ways to construct a data set""" + + with pytest.raises(ValueError): + q.XYDataSet([0, 1, 2, 3, 4], [0, 1, 2, 3]) + + with pytest.raises(IllegalArgumentError): + q.XYDataSet(0, 0) + + dataset = q.XYDataSet([0, 1, 2, 3, 4], [0, 0.2, 0.5, 0.8, 1.3], + xerr=0.1, yerr=[0.1, 0.1, 0.1, 0.1, 0.5], name="test", + xname="time", xunit="s", yname="distance", yunit="m") + assert dataset.xname == "time" + assert dataset.xunit == "s" + assert dataset.yname == "distance" + assert dataset.yunit == "m" + assert dataset.name == "test" + + assert all(dataset.xvalues == [0, 1, 2, 3, 4]) + assert all(dataset.xerr == [0.1, 0.1, 0.1, 0.1, 0.1]) + assert all(dataset.yvalues == [0, 0.2, 0.5, 0.8, 1.3]) + assert all(dataset.yerr == [0.1, 0.1, 0.1, 0.1, 0.5]) + + a = q.MeasurementArray([1, 2, 3, 4, 5]) + b = q.MeasurementArray([10, 20, 30, 40, 50]) + dataset = q.XYDataSet(a, b, xerr=0.5, yerr=0.5, name="test", + xname="x", yname="y", xunit="m", yunit="s") + assert dataset.name == "test" + assert all(dataset.xerr == [0.5, 0.5, 0.5, 0.5, 0.5]) + assert str(dataset.xdata[0]) == "x_0 = 1.0 +/- 0.5 [m]" + + c = q.MeasurementArray([1, 2, 3, 4, 5]) + d = q.MeasurementArray([10, 20, 30, 40, 50]) + dataset = q.XYDataSet(c, d) + assert all(dataset.xerr == [0, 0, 0, 0, 0]) + assert str(dataset.xdata[0]) == "1 +/- 0" + + def test_manipulate_data_set(self): + """tests for changing values in a data set""" + + dataset = q.XYDataSet([0, 1, 2, 3, 4], [0, 0.2, 0.5, 0.8, 1.3]) + dataset.name = "test" + assert dataset.name == "test" + dataset.xname = "x" + assert dataset.xname == "x" + dataset.xunit = "m" + assert dataset.xunit == "m" + assert str(dataset.xdata[0]) == "x_0 = 0 +/- 0 [m]" + dataset.yname = "y" + assert dataset.yname == "y" + dataset.yunit = "s" + assert dataset.yunit == "s" + assert str(dataset.ydata[0]) == "y_0 = 0 +/- 0 [s]" + + with pytest.raises(TypeError): + dataset.name = 1 + with pytest.raises(TypeError): + dataset.xname = 1 + with pytest.raises(TypeError): + dataset.xunit = 1 + with pytest.raises(TypeError): + dataset.yname = 1 + with pytest.raises(TypeError): + dataset.yunit = 1 diff --git a/tests/test_error_propagation.py b/tests/test_error_propagation.py new file mode 100644 index 0000000..b7b4410 --- /dev/null +++ b/tests/test_error_propagation.py @@ -0,0 +1,182 @@ +"""Tests for different error propagation methods""" + +import pytest +import qexpy as q + +from qexpy.data.data import ExperimentalValue, MeasuredValue +from qexpy.utils.exceptions import IllegalArgumentError + + +class TestDerivedValue: + """tests for the derived value class""" + + @pytest.fixture(autouse=True) + def reset_environment(self): + """resets all default configurations""" + q.get_settings().reset() + q.reset_correlations() + + def test_derivative_method(self): + """tests error propagation using the derivative method""" + + a = q.Measurement(5, 0.5) + b = q.Measurement(2, 0.2) + + res = q.sqrt((a + b) / 2) + assert res.error == pytest.approx(0.0719622917128924443443) + assert str(res) == "1.87 +/- 0.07" + + def test_monte_carlo_method(self): + """tests error propagation using the monte carlo method""" + + q.set_error_method(q.ErrorMethod.MONTE_CARLO) + + a = q.Measurement(5, 0.5) + b = q.Measurement(2, 0.2) + + res = q.sqrt((a + b) / 2) + assert res.error == pytest.approx(0.071962291712, abs=1e-2) + assert str(res) == "1.87 +/- 0.07" + + res.mc.sample_size = 10000000 + assert res.mc.samples().size == 10000000 + assert res.error == pytest.approx(0.071962291712, abs=1e-3) + + res.mc.reset_sample_size() + assert res.mc.sample_size == 10000 + + with pytest.raises(ValueError): + res.mc.sample_size = -1 + + G = 6.67384e-11 # the gravitational constant + m1 = q.Measurement(40e4, 2e4, name="m1", unit="kg") + m2 = q.Measurement(30e4, 10e4, name="m2", unit="kg") + r = q.Measurement(3.2, 0.5, name="distance", unit="m") + + f = G * m1 * m2 / (r ** 2) + + f.mc.confidence = 0.68 + assert f.mc.confidence == 0.68 + + f.mc.use_mode_with_confidence() + assert f.value == pytest.approx(0.68, abs=0.15) + assert f.error == pytest.approx(0.36, abs=0.15) + + with pytest.raises(ValueError): + f.mc.confidence = -1 + with pytest.raises(TypeError): + f.mc.confidence = '1' + + f.mc.use_mode_with_confidence(0.3) + assert f.error == pytest.approx(0.15, abs=0.15) + + f.mc.confidence = 0.3 + assert f.error == pytest.approx(0.15, abs=0.15) + + f.mc.use_mean_and_std() + assert f.value == pytest.approx(0.848, abs=0.15) + assert f.error == pytest.approx(0.435, abs=0.15) + + f.mc.sample_size = 10000000 + f.mc.set_xrange(-1, 4) + + assert f.value == pytest.approx(0.848, abs=0.05) + assert f.error == pytest.approx(0.435, abs=0.05) + + with pytest.raises(TypeError): + f.mc.set_xrange('1') + with pytest.raises(ValueError): + f.mc.set_xrange(4, 1) + + f.mc.set_xrange() + assert f.mc.xrange == () + + f.mc.use_custom_value_and_error(0.8, 0.4) + assert f.value == 0.8 + assert f.error == 0.4 + + with pytest.raises(TypeError): + f.mc.use_custom_value_and_error('a', 0.4) + with pytest.raises(TypeError): + f.mc.use_custom_value_and_error(0.8, 'a') + with pytest.raises(ValueError): + f.mc.use_custom_value_and_error(0.8, -0.5) + + f.recalculate() + assert f.value == pytest.approx(0.848, abs=0.15) + assert f.error == pytest.approx(0.435, abs=0.15) + + k = q.Measurement(0.01, 0.1) + res = q.log(k) + with pytest.warns(UserWarning): + assert res.value != pytest.approx(-4.6) + + def test_correlated_measurements(self): + """tests error propagation for correlated measurements""" + + a = q.Measurement(5, 0.5) + b = q.Measurement(2, 0.2) + + q.set_covariance(a, b, 0.08) + + res = q.sqrt((a + b) / 2) + assert res.error == pytest.approx(0.08964214570007952299766) + + res.error_method = q.ErrorMethod.MONTE_CARLO + assert res.error == pytest.approx(0.0896421457001, abs=1e-2) + + def test_manipulate_derived_value(self): + """unit tests for the derived value class""" + + a = q.Measurement(5, 0.5) + b = q.Measurement(2, 0.5) + + res = a + b + assert res.value == 7 + assert res.error == pytest.approx(0.7071067811865476) + assert res.relative_error == pytest.approx(0.1010152544552210749) + + assert res.derivative(a) == 1 + + with pytest.raises(IllegalArgumentError): + res.derivative(1) + + res.error_method = q.ErrorMethod.MONTE_CARLO + assert res.error_method == q.ErrorMethod.MONTE_CARLO + + res.error_method = "derivative" + assert res.error_method == q.ErrorMethod.DERIVATIVE + + res.reset_error_method() + assert res.error_method == q.ErrorMethod.DERIVATIVE + + with pytest.raises(ValueError): + res.error_method = "hello" + + with pytest.raises(TypeError): + res.value = 'a' + with pytest.raises(TypeError): + res.error = 'a' + with pytest.raises(ValueError): + res.error = -1 + with pytest.raises(TypeError): + res.relative_error = 'a' + with pytest.raises(ValueError): + res.relative_error = -1 + + with pytest.warns(UserWarning): + res.value = 6 + assert res.value == 6 + assert isinstance(res, MeasuredValue) + + res = a + b + with pytest.warns(UserWarning): + res.error = 0.5 + assert res.error == 0.5 + assert isinstance(res, MeasuredValue) + + res = a + b + with pytest.warns(UserWarning): + res.relative_error = 0.5 + assert res.relative_error == 0.5 + assert isinstance(res, MeasuredValue) diff --git a/tests/test_fitting.py b/tests/test_fitting.py new file mode 100644 index 0000000..a803e43 --- /dev/null +++ b/tests/test_fitting.py @@ -0,0 +1,195 @@ +"""Tests for the fitting sub-package""" + +import pytest +import qexpy as q +import numpy as np + +from qexpy.data.datasets import XYDataSet, ExperimentalValueArray +from qexpy.utils.exceptions import IllegalArgumentError + + +class TestFitting: + """tests for fitting functions to datasets""" + + def test_fit_result(self): + """tests for the fit result object""" + + a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + b = [5.14440433, 7.14299315, 9.19825169, 11.04786137, 12.98168509, + 15.33559568, 16.92760861, 18.80124373, 21.34893411, 23.16547138] + + result = q.fit(a, b, model=q.FitModel.LINEAR) + + slope, intercept = result.params[0], result.params[1] + assert slope.value == pytest.approx(2, abs=slope.error) + assert intercept.value == pytest.approx(3, abs=intercept.error) + assert result[0].value == pytest.approx(2, abs=result[0].error) + assert result[1].value == pytest.approx(3, abs=result[1].error) + + assert slope.name == "slope" + assert intercept.name == "intercept" + + assert callable(result.fit_function) + test = result.fit_function(3) + assert test.value == pytest.approx(9, abs=0.2) + + residuals = result.residuals + assert all(residuals < 0.3) + + assert result.ndof == 7 + assert result.chi_squared == pytest.approx(0) + + assert isinstance(result.dataset, XYDataSet) + assert all(result.dataset.xdata == a) + assert all(result.dataset.ydata == b) + + assert str(result) + + b[-1] = 50 + + result = q.fit(a, b, model="linear", xrange=(0, 10)) + assert result.xrange == (0, 10) + assert result[0].value == pytest.approx(2, abs=0.15) + assert result[1].value == pytest.approx(3, abs=0.15) + + def test_polynomial_fit(self): + """tests for fitting to a polynomial""" + + a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + b = [5.82616885, 10.73323542, 18.53401063, 27.16662982, 37.99711327, + 51.41386193, 66.09297228, 83.46407479, 102.23573159, 122.8573845] + + result = q.fit(a, b, model=q.FitModel.QUADRATIC) + + assert len(result.params) == 3 + assert result[0].value == pytest.approx(1, rel=0.15) + assert result[1].value == pytest.approx(2, rel=0.15) + assert result[2].value == pytest.approx(3, rel=0.15) + + a = q.MeasurementArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + b = q.MeasurementArray( + [9.96073312, 31.18583676, 78.11727423, 161.58352404, 298.7038423, 494.25761959, + 766.3146814, 1123.59437138, 1578.30697946, 2142.70591363]) + + result = q.fit(a, b, model=q.FitModel.POLYNOMIAL) + + assert len(result.params) == 4 + assert result[0].value == pytest.approx(2, rel=0.3) + assert result[1].value == pytest.approx(1, rel=0.3) + assert result[2].value == pytest.approx(4, rel=0.3) + assert result[3].value == pytest.approx(3, rel=0.4) + + b = [20.32132071, 64.27190108, 189.14762997, 469.97259457, 999.96248493, + 1899.41641639, 3312.43244643, 5411.38221041, 8379.45187783, 12439.47094005] + + dataset = q.XYDataSet(a, b, yerr=0.2) + result = q.fit(dataset, model=q.FitModel.POLYNOMIAL, degrees=4) + + assert len(result.params) == 5 + assert result[0].value == pytest.approx(1, rel=0.3) + assert result[1].value == pytest.approx(2, rel=0.3) + assert result[2].value == pytest.approx(4, rel=0.3) + assert result[3].value == pytest.approx(3, rel=0.4) + assert result[4].value == pytest.approx(10, rel=0.4) + + with pytest.raises(IllegalArgumentError): + q.fit(1, 2) + + def test_gaussian_fit(self): + """tests for fitting to a gaussian distribution""" + + data = np.random.normal(20, 10, 10000) + n, bins = np.histogram(data) + centers = [(bins[i] + bins[i + 1]) / 2 for i in range(len(bins) - 1)] + + result = q.fit(centers, n, model="gaussian", parguess=[10000, 18, 9]) + assert result[1].value == pytest.approx(20, rel=0.3) + assert result[2].value == pytest.approx(10, rel=0.3) + + def test_exponential_fit(self): + """tests for fitting to an exponential function""" + + a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + b = [3.06297029e+00, 1.84077449e+00, 1.12743994e+00, 6.70756404e-01, 4.13320658e-01, + 2.46274429e-01, 1.48775210e-01, 9.22527208e-02, 5.51925037e-02, 3.39932113e-02, + 2.01913759e-02, 1.24795552e-02, 7.67283714e-03, 4.55000537e-03, 2.75573044e-03, + 1.72345608e-03, 1.00990816e-03, 6.21266331e-04, 3.75164648e-04, 2.26534182e-04] + + result = q.fit(a, b, model=q.FitModel.EXPONENTIAL, parguess=[4.5, 0.45]) + assert result[0].value == pytest.approx(5, rel=0.1) + assert result[1].value == pytest.approx(0.5, rel=0.1) + + assert result[0].name == "amplitude" + assert result[1].name == "decay constant" + + def test_custom_fit(self): + """tests for fitting to a custom function""" + + arr1 = [0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5, 5., + 5.5, 6., 6.5, 7., 7.5, 8., 8.5, 9., 9.5, 10.] + arr2 = [0.84323953, 1.62678942, 2.46301834, 2.96315268, 3.64614702, + 4.11468579, 4.61486981, 4.76099487, 5.04725257, 4.9774383, + 4.85234697, 4.55749775, 4.13774772, 3.64002414, 3.01167174, + 2.26087356, 1.48618986, 0.71204259, -0.0831691, -0.9100453] + + def model(x, a, b): + return a * q.sin(b * x) + + result = q.fit(arr1, arr2, model=model, parguess=[4, 0.5], parunits=["m", "kg"]) + assert result[0].name == "a" + assert result[1].name == "b" + assert result[0].unit == "m" + assert result[1].unit == "kg" + assert result[0].value == pytest.approx(5, rel=0.5) + assert result[1].value == pytest.approx(0.5, rel=0.5) + + with pytest.raises(ValueError): + q.fit(arr1, arr2, model="model", parguess=[4, 0.5]) + + with pytest.warns(UserWarning): + q.fit(arr1, arr2, model=model) + + with pytest.raises(TypeError): + q.fit(arr1, arr2, model=model, parguess=['a', 0.5]) + + with pytest.raises(TypeError): + q.fit(arr1, arr2, model=model, parguess=[4, 0.5], parnames=[1, 2]) + + with pytest.raises(TypeError): + q.fit(arr1, arr2, model=model, parguess=[4, 0.5], parunits=[1, 2]) + + with pytest.raises(IllegalArgumentError): + q.fit(arr1, arr2, model=model, parguess=4) + + with pytest.raises(ValueError): + q.fit(arr1, arr2, model=model, parguess=[4, 0.5], parunits=["m"]) + + def func(x, **kwargs): + return kwargs.get("a") * q.sin(kwargs.get("b") * x) # pragma: no cover + + with pytest.raises(ValueError): + q.fit(arr1, arr2, model=func, parguess=[4, 0.5]) + + def func2(x): + return x # pragma: no cover + + with pytest.raises(ValueError): + q.fit(arr1, arr2, model=func2, parguess=[4, 0.5]) + + def func3(x, *args): + return args[0] * q.sin(args[1] * x) + + dataset = q.XYDataSet(arr1, arr2, xerr=0.05, yerr=0.05) + result = dataset.fit(model=func3, parguess=[4, 0.5], parnames=["arr1", "arr2"]) + assert result[0].name == "arr1" + assert result[1].name == "arr2" + + with pytest.raises(ValueError): + dataset.fit(model=func3, parguess=[4, 0.5], parnames=["arr1"]) + + def func4(x, a, *args): + return a + args[0] * q.sin(args[1] * x) # pragma: no cover + + with pytest.raises(ValueError): + with pytest.warns(UserWarning): + q.fit(arr1, arr2, model=func4, parnames=["arr1"]) diff --git a/tests/test_measurements.py b/tests/test_measurements.py new file mode 100644 index 0000000..84c71a0 --- /dev/null +++ b/tests/test_measurements.py @@ -0,0 +1,260 @@ +"""Unit tests for recording individual and arrays of measurements""" + +import pytest + +import numpy as np +import qexpy as q + +from qexpy.data.data import RepeatedlyMeasuredValue, MeasuredValue, UndefinedActionError +from qexpy.data.datasets import ExperimentalValueArray +from qexpy.utils.exceptions import IllegalArgumentError + + +class TestMeasuredValue: + """Tests for a single measurement""" + + @pytest.fixture(autouse=True) + def reset_environment(self): + """restores all default configurations before each testcase""" + q.get_settings().reset() + + def test_measurement(self): + """tests for single measurements""" + + a = q.Measurement(5) + assert a.value == 5 + assert a.error == 0 + assert str(a) == "5 +/- 0" + assert repr(a) == "MeasuredValue(5 +/- 0)" + + b = q.Measurement(5, 0.5) + assert b.value == 5 + assert b.error == 0.5 + assert b.relative_error == 0.1 + assert b.std == 0.5 + assert str(b) == "5.0 +/- 0.5" + assert repr(b) == "MeasuredValue(5.0 +/- 0.5)" + + c = q.Measurement(12.34, 0.05, name="energy", unit="kg*m^2*s^-2") + assert str(c) == "energy = 12.34 +/- 0.05 [kg⋅m^2⋅s^-2]" + + q.set_sig_figs_for_error(2) + assert str(c) == "energy = 12.340 +/- 0.050 [kg⋅m^2⋅s^-2]" + q.set_sig_figs_for_value(4) + assert str(c) == "energy = 12.34 +/- 0.05 [kg⋅m^2⋅s^-2]" + q.set_unit_style(q.UnitStyle.FRACTION) + assert str(c) == "energy = 12.34 +/- 0.05 [kg⋅m^2/s^2]" + + assert c.derivative(c) == 1 + assert c.derivative(b) == 0 + + with pytest.raises(IllegalArgumentError): + c.derivative(1) + + with pytest.raises(IllegalArgumentError): + q.Measurement('12.34') + + with pytest.raises(IllegalArgumentError): + q.Measurement(12.34, '0.05') + + with pytest.raises(TypeError): + q.Measurement(5, unit=1) + + with pytest.raises(TypeError): + q.Measurement(5, name=1) + + def test_measurement_setters(self): + """tests for changing values in a measured value""" + + a = q.Measurement(12.34, 0.05) + a.value = 50 + assert a.value == 50 + a.error = 0.02 + assert a.error == 0.02 + a.relative_error = 0.05 + assert a.relative_error == 0.05 + assert a.error == 2.5 + a.name = "energy" + assert a.name == "energy" + a.unit = "kg*m^2/s^2" + assert a.unit == "kg⋅m^2⋅s^-2" + + with pytest.raises(TypeError): + a.value = '1' + + with pytest.raises(TypeError): + a.error = '1' + + with pytest.raises(ValueError): + a.error = -1 + + with pytest.raises(TypeError): + a.relative_error = '1' + + with pytest.raises(ValueError): + a.relative_error = -1 + + with pytest.raises(TypeError): + a.name = 1 + + with pytest.raises(TypeError): + a.unit = 1 + + def test_repeated_measurement(self): + """test recording repeatedly measured values""" + + a = q.Measurement([10, 9.8, 9.9, 10.1, 10.2]) + assert isinstance(a, RepeatedlyMeasuredValue) + assert a.value == 10 + assert a.error == pytest.approx(0.070710730438880) + assert a.std == pytest.approx(0.158114) + a.use_std_for_uncertainty() + assert a.error == pytest.approx(0.158114) + + assert isinstance(a.raw_data, np.ndarray) + assert not isinstance(a.raw_data, ExperimentalValueArray) + + b = q.Measurement([10, 9.8, 9.9, 10.1, 10.2], [0.5, 0.3, 0.1, 0.2, 0.2]) + assert isinstance(b, RepeatedlyMeasuredValue) + assert b.mean == 10 + assert b.value == 10 + assert b.error == pytest.approx(0.070710730438880) + + assert isinstance(b.raw_data, ExperimentalValueArray) + assert all(b.raw_data == [10, 9.8, 9.9, 10.1, 10.2]) + + with pytest.raises(ValueError): + q.Measurement([10, 9.8, 9.9, 10.1, 10.2], [0.5, 0.3, 0.1, 0.2]) + + def test_repeated_measurement_setters(self): + """test that the setters for repeated measurements behave correctly""" + + a = q.Measurement([10, 9.8, 9.9, 10.1, 10.2], [0.5, 0.3, 0.1, 0.2, 0.2]) + assert isinstance(a, RepeatedlyMeasuredValue) + a.use_error_weighted_mean_as_value() + assert a.error_weighted_mean == 9.971399730820997 + assert a.value == 9.971399730820997 + a.use_error_on_mean_for_uncertainty() + assert a.error_on_mean == pytest.approx(0.070710678118654) + assert a.error == pytest.approx(0.070710678118654) + a.use_propagated_error_for_uncertainty() + assert a.propagated_error == 0.0778236955614928 + assert a.error == 0.0778236955614928 + + with pytest.raises(TypeError): + a.value = '15' + + with pytest.warns(UserWarning): + a.value = 15 + + assert not isinstance(a, RepeatedlyMeasuredValue) + assert isinstance(a, MeasuredValue) + assert a.value == 15 + + def test_correlation_for_repeated_measurements(self): + """test covariance and correlation settings between repeated measurements""" + + a = q.Measurement([0.8, 0.9, 1, 1.1]) + b = q.Measurement([2, 2.2, 2.1, 2.3]) + + assert q.get_correlation(a, b) == 0 + assert q.get_covariance(a, b) == 0 + + q.set_correlation(a, b) + assert q.get_correlation(a, b) == pytest.approx(0.8) + assert q.get_covariance(a, b) == pytest.approx(0.01333333333) + + q.set_correlation(a, b, 0) + assert q.get_correlation(a, b) == 0 + assert q.get_covariance(a, b) == 0 + + a.set_covariance(b) + assert q.get_correlation(a, b) == pytest.approx(0.8) + assert q.get_covariance(a, b) == pytest.approx(0.01333333333) + + q.set_covariance(a, b, 0) + assert q.get_correlation(a, b) == 0 + assert q.get_covariance(a, b) == 0 + + d = a + b + with pytest.raises(IllegalArgumentError): + a.get_covariance(0) + with pytest.raises(IllegalArgumentError): + a.get_correlation(0) + with pytest.raises(IllegalArgumentError): + a.set_covariance(0, 0) + with pytest.raises(IllegalArgumentError): + a.set_correlation(0, 0) + with pytest.raises(IllegalArgumentError): + a.set_covariance(d, 0) + with pytest.raises(IllegalArgumentError): + a.set_correlation(d, 0) + + c = q.Measurement([0, 1, 2]) + with pytest.raises(IllegalArgumentError): + q.set_covariance(a, c) + with pytest.raises(IllegalArgumentError): + q.set_correlation(a, c) + + def test_correlation_for_single_measurements(self): + """test covariance and correlation between single measurements""" + + a = q.Measurement(5, 0.5) + b = q.Measurement(6, 0.2) + c = q.Measurement(5) + d = a + b + + assert a.get_covariance(a) == 0.25 + assert a.get_correlation(a) == 1 + assert a.get_covariance(c) == 0 + assert a.get_correlation(c) == 0 + + assert d.get_covariance(a) == 0 + assert d.get_correlation(a) == 0 + assert a.get_covariance(d) == 0 + assert a.get_correlation(d) == 0 + assert q.get_covariance(a, d) == 0 + assert q.get_correlation(a, d) == 0 + + def test_illegal_correlation_settings(self): + """test illegal correlation and covariance settings""" + + a = q.Measurement(5, 0.5) + b = q.Measurement(6, 0.2) + c = q.Measurement(5) + d = a + b + + with pytest.raises(IllegalArgumentError): + q.set_correlation(a, 0, 0) + with pytest.raises(IllegalArgumentError): + q.set_covariance(a, 0, 0) + with pytest.raises(IllegalArgumentError): + a.set_correlation(0, 0) + with pytest.raises(IllegalArgumentError): + a.set_covariance(0, 0) + with pytest.raises(UndefinedActionError): + d.set_correlation(a, 0) + with pytest.raises(UndefinedActionError): + d.set_covariance(a, 0) + with pytest.raises(IllegalArgumentError): + a.set_covariance(d, 0) + with pytest.raises(IllegalArgumentError): + a.set_correlation(d, 0) + with pytest.raises(ArithmeticError): + a.set_covariance(c, 1) + with pytest.raises(ArithmeticError): + a.set_correlation(c, 1) + + with pytest.raises(IllegalArgumentError): + a.get_correlation(0) + with pytest.raises(IllegalArgumentError): + a.get_covariance(0) + with pytest.raises(IllegalArgumentError): + q.get_correlation(a, 0) + with pytest.raises(IllegalArgumentError): + q.get_covariance(a, 0) + + with pytest.raises(ValueError): + q.set_covariance(a, b, 100) + with pytest.raises(ValueError): + q.set_correlation(a, b, 2) diff --git a/tests/test_operations.py b/tests/test_operations.py new file mode 100644 index 0000000..9b30625 --- /dev/null +++ b/tests/test_operations.py @@ -0,0 +1,331 @@ +"""Tests for operations between experimental values""" + +import pytest +import qexpy as q + +from qexpy.data.data import ExperimentalValue +from qexpy.utils.exceptions import UndefinedOperationError + + +class TestArithmetic: + """tests for basic arithmetic operations""" + + def test_value_comparison(self): + """tests for comparing values""" + + a = q.Measurement(4, 0.5, unit="m") + b = q.Measurement(10, 2, unit="m") + c = q.Measurement(10, 1) + + assert a < b + assert a <= b + assert b >= a + assert a > 2 + assert 3 < b + assert a == 4 + assert 10 == b + assert b == c + + def test_elementary_operations(self): + """tests for elementary arithmetic operations""" + + a = q.Measurement(4, 0.5, unit="m") + b = q.Measurement(10, 2, unit="m") + + c = a + b + assert c.value == 14 + assert c.error == pytest.approx(2.0615528128088303) + assert str(c) == "14 +/- 2 [m]" + + c2 = a + 2 + assert c2.value == 6 + assert c2.error == 0.5 + assert str(c2) == "6.0 +/- 0.5 [m]" + + c3 = 5 + a + assert c3.value == 9 + assert c3.error == 0.5 + + c4 = a + (10, 2) + assert c4.value == 14 + assert c4.error == pytest.approx(2.0615528128088303) + assert str(c4) == "14 +/- 2" + + c5 = -a + assert str(c5) == "-4.0 +/- 0.5 [m]" + + h = b - a + assert h.value == 6 + assert h.error == pytest.approx(2.0615528128088303) + assert str(h) == "6 +/- 2 [m]" + + h1 = a - 2 + assert h1.value == 2 + assert h1.error == 0.5 + assert str(h1) == "2.0 +/- 0.5 [m]" + + h2 = 5 - a + assert h2.value == 1 + assert h2.error == 0.5 + + f = q.Measurement(4, 0.5, unit="kg*m/s^2") + d = q.Measurement(10, 2, unit="m") + + e = f * d + assert e.value == 40 + assert e.error == pytest.approx(9.433981132056603) + assert str(e) == "40 +/- 9 [kg⋅m^2⋅s^-2]" + + e1 = f * 2 + assert e1.value == 8 + assert str(e1) == "8 +/- 1 [kg⋅m⋅s^-2]" + + e2 = 2 * f + assert e2.value == 8 + + s = q.Measurement(10, 2, unit="m") + t = q.Measurement(4, 0.5, unit="s") + + v = s / t + assert v.value == 2.5 + assert v.error == pytest.approx(0.5896238207535377) + assert str(v) == "2.5 +/- 0.6 [m⋅s^-1]" + + v1 = 20 / s + assert v1.value == 2 + assert str(v1) == "2.0 +/- 0.4 [m^-1]" + + v2 = s / 2 + assert v2.value == 5 + + with pytest.raises(UndefinedOperationError): + s + 'a' + + k = q.Measurement(5, 0.5, unit="m") + + m = k ** 2 + assert str(m) == "25 +/- 5 [m^2]" + + n = 2 ** k + assert n.value == 32 + assert n.error == pytest.approx(11.09035488895912495) + assert str(n) == "30 +/- 10" + + def test_vectorized_arithmetic(self): + """tests for arithmetic with experimental value arrays""" + + a = q.MeasurementArray([1, 2, 3, 4, 5], 0.5, unit="s") + + res = a + 2 + assert all(res.values == [3, 4, 5, 6, 7]) + assert all(res.errors == [0.5, 0.5, 0.5, 0.5, 0.5]) + assert res.unit == "s" + + res = 2 + a + assert all(res.values == [3, 4, 5, 6, 7]) + assert all(res.errors == [0.5, 0.5, 0.5, 0.5, 0.5]) + assert res.unit == "s" + + res = a + (2, 0.5) + assert all(res.values == [3, 4, 5, 6, 7]) + + res = (2, 0.5) + a + assert all(res.values == [3, 4, 5, 6, 7]) + + res = q.Measurement(2, 0.5) + a + assert all(res.values == [3, 4, 5, 6, 7]) + + res = a + [1, 2, 3, 4, 5] + assert all(res.values == [2, 4, 6, 8, 10]) + + res = [1, 2, 3, 4, 5] + a + assert all(res.values == [2, 4, 6, 8, 10]) + + res = a - 1 + assert all(res.values == [0, 1, 2, 3, 4]) + + res = 10 - a + assert all(res.values == [9, 8, 7, 6, 5]) + + res = q.Measurement(10, 0.5) - a + assert all(res.values == [9, 8, 7, 6, 5]) + + res = a - [1, 2, 3, 4, 5] + assert all(res.values == [0, 0, 0, 0, 0]) + + res = [1, 2, 3, 4, 5] - a + assert all(res.values == [0, 0, 0, 0, 0]) + + res = a * 2 + assert all(res.values == [2, 4, 6, 8, 10]) + + res = 2 * a + assert all(res.values == [2, 4, 6, 8, 10]) + + res = q.Measurement(2, 0.5) * a + assert all(res.values == [2, 4, 6, 8, 10]) + + b = q.MeasurementArray([10, 20, 30, 40, 50], 0.5, unit="m") + + res = b * a + assert all(res.values == [10, 40, 90, 160, 250]) + assert res.unit == "m⋅s" + + res = [1, 2, 3, 4, 5] * a + assert all(res.values == [1, 4, 9, 16, 25]) + + res = a / 2 + assert all(res.values == [0.5, 1, 1.5, 2, 2.5]) + + res = 2 / a + assert all(res.values == [2, 1, 2 / 3, 2 / 4, 2 / 5]) + + res = q.Measurement(2, 0.5) / a + assert all(res.values == [2, 1, 2 / 3, 2 / 4, 2 / 5]) + + res = b / a + assert all(res.values == [10, 10, 10, 10, 10]) + assert res.unit == "m⋅s^-1" + + res = [1, 2, 3, 4, 5] / a + assert all(res.values == [1, 1, 1, 1, 1]) + + res = a ** 2 + assert all(res.values == [1, 4, 9, 16, 25]) + + res = 2 ** a + assert all(res.values == [2, 4, 8, 16, 32]) + + res = q.Measurement(2, 0.5) ** a + assert all(res.values == [2, 4, 8, 16, 32]) + + res = a ** [2, 2, 2, 2, 2] + assert all(res.values == [1, 4, 9, 16, 25]) + assert res.unit == "s^2" + + res = [2, 2, 2, 2, 2] ** a + assert all(res.values == [2, 4, 8, 16, 32]) + + def test_composite_operations(self): + """tests combining several operations""" + + d = q.Measurement(5, 0.1, unit="m") + m = q.Measurement(10, 0.5, unit="kg") + t = q.Measurement(2, 0.1, unit="s") + + v = d / t + e = 1 / 2 * m * (v ** 2) + + assert e.value == 31.25 + assert e.error == pytest.approx(3.7107319021993495716) + assert e.unit == "kg⋅m^2⋅s^-2" + + res = (d ** 2) ** (1 / 3) + assert res.unit == "m^(2/3)" + + +class TestMathFunctions: + """tests for math function wrappers""" + + def test_math_functions(self): + """tests for math functions on single values""" + + a = q.Measurement(4, 0.5, unit="m") + + res = q.sqrt(a) + assert res.value == 2 + assert res.error == 0.125 + assert res.unit == "m^(1/2)" + assert q.sqrt(4) == 2 + + with pytest.raises(UndefinedOperationError): + q.sqrt("a") + + res = q.exp(a) + assert res.value == pytest.approx(54.598150033144239) + assert res.error == pytest.approx(27.299075016572120) + + res = q.log(a) + assert res.value == pytest.approx(1.3862943611198906) + assert res.error == 0.125 + + res = q.log(2, a) + assert res.value == 2 + assert res.error == pytest.approx(0.1803368801111204) + + res = q.log10(a) + assert res.value == pytest.approx(0.6020599913279624) + assert res.error == pytest.approx(0.0542868102379065) + + with pytest.raises(TypeError): + q.log(2, a, 2) + + def test_trig_functions(self): + """tests for trigonometric functions""" + + a = q.Measurement(0.7853981633974483) + b = q.Measurement(45) + c = q.Measurement(0.5) + + res = q.sin(a) + assert res.value == pytest.approx(0.7071067811865475244) + + res = q.sind(b) + assert res.value == pytest.approx(0.7071067811865475244) + + res = q.cos(a) + assert res.value == pytest.approx(0.7071067811865475244) + + res = q.cosd(b) + assert res.value == pytest.approx(0.7071067811865475244) + + res = q.tan(a) + assert res.value == pytest.approx(1) + + res = q.tand(b) + assert res.value == pytest.approx(1) + + res = q.sec(a) + assert res.value == pytest.approx(1.4142135623730950488) + + res = q.secd(b) + assert res.value == pytest.approx(1.4142135623730950488) + + res = q.csc(a) + assert res.value == pytest.approx(1.4142135623730950488) + + res = q.cscd(b) + assert res.value == pytest.approx(1.4142135623730950488) + + res = q.cot(a) + assert res.value == pytest.approx(1) + + res = q.cotd(b) + assert res.value == pytest.approx(1) + + res = q.asin(c) + assert res.value == pytest.approx(0.523598775598298873077) + + res = q.acos(c) + assert res.value == pytest.approx(1.047197551196597746154) + + res = q.atan(c) + assert res.value == pytest.approx(0.463647609000806116214) + + def test_vectorized_functions(self): + """tests for functions on experimental value arrays""" + + a = q.MeasurementArray([1, 2, 3, 4, 5]) + assert q.mean(a) == 3 + assert q.std(a) == pytest.approx(1.58113883008419) + assert q.sum(a) == 15 + + b = [1, 2, 3, 4, 5] + + res = q.mean(b) + + assert res == 3 + assert not isinstance(res, ExperimentalValue) + + assert q.std(b) == pytest.approx(1.58113883008419) + assert q.sum(b) == 15 diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..ff8dcc6 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,93 @@ +"""Unit tests for the settings sub-package""" + +import pytest + +from qexpy.settings.settings import ErrorMethod, PrintStyle, UnitStyle, SigFigMode + +import qexpy.settings.literals as lit +import qexpy.settings.settings as sts + + +class TestSettings: + """Unit tests for global settings""" + + def test_settings(self): + """test change and get settings""" + + sts.set_unit_style(lit.FRACTION) + assert sts.get_settings().unit_style == UnitStyle.FRACTION + sts.set_unit_style(UnitStyle.EXPONENTS) + assert sts.get_settings().unit_style == UnitStyle.EXPONENTS + + sts.set_print_style(PrintStyle.SCIENTIFIC) + assert sts.get_settings().print_style == PrintStyle.SCIENTIFIC + sts.set_print_style(lit.DEFAULT) + assert sts.get_settings().print_style == PrintStyle.DEFAULT + + sts.set_error_method(ErrorMethod.MONTE_CARLO) + assert sts.get_settings().error_method == ErrorMethod.MONTE_CARLO + sts.set_error_method(lit.DERIVATIVE) + assert sts.get_settings().error_method == ErrorMethod.DERIVATIVE + + sts.set_plot_dimensions((8, 4)) + assert sts.get_settings().plot_dimensions == (8, 4) + + sts.set_monte_carlo_sample_size(10000) + assert sts.get_settings().monte_carlo_sample_size == 10000 + + sts.set_sig_figs_for_error(4) + assert sts.get_settings().sig_fig_value == 4 + assert sts.get_settings().sig_fig_mode == SigFigMode.ERROR + sts.set_sig_figs_for_value(3) + assert sts.get_settings().sig_fig_value == 3 + assert sts.get_settings().sig_fig_mode == SigFigMode.VALUE + + def test_invalid_settings(self): + """tests for rejecting invalid settings""" + + with pytest.raises(ValueError): + sts.set_sig_figs_for_value(-1) + + with pytest.raises(ValueError): + # noinspection PyTypeChecker + sts.set_sig_figs_for_error(0.5) + + with pytest.raises(ValueError): + sts.set_monte_carlo_sample_size(-1) + + with pytest.raises(ValueError): + sts.set_plot_dimensions((0, 0)) + + with pytest.raises(ValueError): + sts.set_error_method(lit.DEFAULT) + + with pytest.raises(ValueError): + sts.set_print_style(lit.DERIVATIVE) + + with pytest.raises(ValueError): + sts.set_unit_style(lit.DERIVATIVE) + + with pytest.raises(ValueError): + sts.set_plot_dimensions(10) + + def test_reset_error_configurations(self): + """test for reset all configurations to default""" + + sts.reset_default_configuration() + assert sts.get_settings().error_method == ErrorMethod.DERIVATIVE + assert sts.get_settings().sig_fig_mode == SigFigMode.AUTOMATIC + assert sts.get_settings().sig_fig_value == 1 + assert sts.get_settings().monte_carlo_sample_size == 10000 + assert sts.get_settings().unit_style == UnitStyle.EXPONENTS + assert sts.get_settings().plot_dimensions == (6.4, 4.8) + + def test_use_mc_sample_size(self): + """test for temporarily setting monte-carlo sample size""" + + @sts.use_mc_sample_size(100) + def test_func(): + assert sts.get_settings().monte_carlo_sample_size == 100 + + sts.set_monte_carlo_sample_size(10000) + test_func() + assert sts.get_settings().monte_carlo_sample_size == 10000 diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..9862591 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,296 @@ +"""Unit tests for the utility sub-package""" + +import os +import pytest +import numpy as np + +from collections import OrderedDict + +import qexpy.settings.settings as sts +import qexpy.settings.literals as lit +import qexpy.utils.utils as utils +import qexpy.utils.printing as printing +import qexpy.utils.units as units + +from qexpy.utils.exceptions import UndefinedOperationError + + +class TestDecorators: + """Unit tests for various decorators in utils""" + + def test_check_operand_type(self): + """test the operand type checker""" + + @utils.check_operand_type("test") + def test_func(_): + raise TypeError("test error") + + @utils.check_operand_type("+") + def test_func2(_, __): + raise TypeError("test error") + + with pytest.raises(UndefinedOperationError) as e: + test_func('a') + + exp = "\"test\" is undefined with operands of type(s) 'str'. Expected: real numbers" + assert str(e.value) == exp + + with pytest.raises(UndefinedOperationError) as e: + test_func2('a', 1) + exp = "\"+\" is undefined with operands of type(s) 'str' and 'int'. " \ + "Expected: real numbers" + assert str(e.value) == exp + + def test_vectorize(self): + """test the vectorize decorator""" + + @utils.vectorize + def test_func(a): + return a + 2 + + assert test_func([1, 2, 3]) == [3, 4, 5] + assert test_func(1) == 3 + assert all(test_func(np.array([1, 2, 3])) == [3, 4, 5]) + + +class TestUtils: + """Unit tests for the utils sub-module""" + + def test_validate_xrange(self): + """tests the range validator""" + + with pytest.raises(TypeError): + utils.validate_xrange(0) + with pytest.raises(TypeError): + utils.validate_xrange((0,)) + with pytest.raises(TypeError): + utils.validate_xrange((0, '1')) + with pytest.raises(ValueError): + utils.validate_xrange((1, 0)) + assert utils.validate_xrange((10.5, 20.5)) + + def test_numerical_derivative(self): + """test the numerical derivative""" + + assert pytest.approx(1.9726023611141572335938, utils.numerical_derivative( + lambda x: x ** 2 * np.sin(x), 2)) + + def test_calculate_covariance(self): + """test the covariance calculator""" + + with pytest.raises(ValueError): + utils.calculate_covariance([1, 2, 3], [1, 2, 3, 4]) + + assert pytest.approx(utils.calculate_covariance([1, 2, 3, 4], [4, 3, 2, 1]), - 5 / 3) + assert pytest.approx(utils.calculate_covariance( + np.array([1, 2, 3, 4]), np.array([4, 3, 2, 1])), - 5 / 3) + + def test_cov2corr(self): + """test converting covariance matrix to correlation matrix""" + + m = np.array([[1, 2, 3, 4], [4, 3, 2, 1], [2, 3, 2, 3]]) + assert utils.cov2corr(np.cov(m)) == pytest.approx(np.corrcoef(m)) + + def test_load_data_from_file(self): + """test loading an array from a data file""" + + curr_path = os.path.abspath(os.path.dirname(__file__)) + filename = os.path.join(curr_path, "./resources/data_for_test_load_data.csv") + data = utils.load_data_from_file(filename) + assert len(data) == 4 + for data_set in data: + assert len(data_set) == 30 + assert data[2, 8] == 9.95 + + def test_find_mode_and_uncertainty(self): + """test finding most probably value and uncertainty from distribution""" + + samples = np.random.normal(0, 1, 10000) + n, bins = np.histogram(samples, bins=100) + mode, error = utils.find_mode_and_uncertainty(n, bins, 0.68) + assert mode == pytest.approx(0, abs=0.5) + assert error == pytest.approx(1, abs=0.5) + + +class TestPrinting: + """Unit tests for the printing sub-module""" + + @pytest.fixture(autouse=True) + def reset_environment(self): + """Before method that resets all configurations""" + sts.get_settings().reset() + + def test_default_print(self): + """Tests the default print format""" + + # Printing in default format + default_printer = printing.get_printer() + assert default_printer(0.0, 0.0) == "0 +/- 0" + assert default_printer(np.inf, 0.0) == "inf +/- inf" + assert default_printer(2, 1) == "2 +/- 1" + assert default_printer(2123, 13) == "2120 +/- 10" + assert default_printer(2.1, 0.5) == "2.1 +/- 0.5" + assert default_printer(2.12, 0.18) == "2.1 +/- 0.2" + + # Printing with significant figure specified for error + sts.set_sig_figs_for_error(2) + assert default_printer(0.0, 0.0) == "0 +/- 0" + assert default_printer(2, 1) == "2.0 +/- 1.0" + assert default_printer(2, 0) == "2.0 +/- 0" + assert default_printer(2.1, 0.5) == "2.10 +/- 0.50" + assert default_printer(2.12, 0.22) == "2.12 +/- 0.22" + assert default_printer(2.123, 0.123) == "2.12 +/- 0.12" + + # Printing with significant figure specified for value + sts.set_sig_figs_for_value(2) + assert default_printer(0.0, 0.0) == "0 +/- 0" + assert default_printer(2, 1) == "2.0 +/- 1.0" + assert default_printer(0, 0.5) == "0.00 +/- 0.50" + assert default_printer(1231, 0.5) == "1200 +/- 0" + assert default_printer(123, 12) == "120 +/- 10" + + def test_scientific_print(self): + """Tests printing in scientific notation""" + + # Printing in default format + scientific_printer = printing.get_printer(sts.PrintStyle.SCIENTIFIC) + assert scientific_printer(0.0, 0.0) == "0 +/- 0" + assert scientific_printer(np.inf, 0.0) == "inf +/- inf" + assert scientific_printer(2.1, 0.5) == "2.1 +/- 0.5" + assert scientific_printer(2.12, 0.18) == "2.1 +/- 0.2" + assert scientific_printer(2123, 13) == "(2.12 +/- 0.01) * 10^3" + assert scientific_printer(0.012312, 0.00334) == "(1.2 +/- 0.3) * 10^-2" + assert scientific_printer(120000, 370) == "(1.200 +/- 0.004) * 10^5" + + # Printing with significant figure specified for error + sts.set_sig_figs_for_error(1) + assert scientific_printer(100, 500) == "(1 +/- 5) * 10^2" + + sts.set_sig_figs_for_error(2) + assert scientific_printer(0.0, 0.0) == "0 +/- 0" + assert scientific_printer(2.1, 0.5) == "2.10 +/- 0.50" + assert scientific_printer(2.12, 0.18) == "2.12 +/- 0.18" + assert scientific_printer(2123, 13) == "(2.123 +/- 0.013) * 10^3" + assert scientific_printer(0.012312, 0.00334) == "(1.23 +/- 0.33) * 10^-2" + assert scientific_printer(120000, 370) == "(1.2000 +/- 0.0037) * 10^5" + + # Printing with significant figure specified for value + sts.set_sig_figs_for_value(2) + assert scientific_printer(0.0, 0.0) == "0 +/- 0" + assert scientific_printer(2.1, 0.5) == "2.1 +/- 0.5" + assert scientific_printer(2.12, 0.18) == "2.1 +/- 0.2" + assert scientific_printer(2123, 13) == "(2.1 +/- 0.0) * 10^3" + assert scientific_printer(0.012312, 0.00334) == "(1.2 +/- 0.3) * 10^-2" + assert scientific_printer(120000, 370) == "(1.2 +/- 0.0) * 10^5" + + def test_latex_print(self): + """Test printing in latex format""" + + latex_printer = printing.get_printer(sts.PrintStyle.LATEX) + + # Printing in default format + assert latex_printer(2.1, 0.5) == r"2.1 \pm 0.5" + assert latex_printer(2.12, 0.18) == r"2.1 \pm 0.2" + assert latex_printer(2123, 13) == r"(2.12 \pm 0.01) * 10^3" + assert latex_printer(0.012312, 0.00334) == r"(1.2 \pm 0.3) * 10^-2" + assert latex_printer(120000, 370) == r"(1.200 \pm 0.004) * 10^5" + + # Printing with significant figure specified for error + sts.set_sig_figs_for_error(2) + assert latex_printer(2.1, 0.5) == r"2.10 \pm 0.50" + assert latex_printer(2.12, 0.18) == r"2.12 \pm 0.18" + assert latex_printer(2123, 13) == r"(2.123 \pm 0.013) * 10^3" + assert latex_printer(0.012312, 0.00334) == r"(1.23 \pm 0.33) * 10^-2" + assert latex_printer(120000, 370) == r"(1.2000 \pm 0.0037) * 10^5" + + # Printing with significant figure specified for value + sts.set_sig_figs_for_value(2) + assert latex_printer(2.1, 0.5) == r"2.1 \pm 0.5" + assert latex_printer(2.12, 0.18) == r"2.1 \pm 0.2" + assert latex_printer(2123, 13) == r"(2.1 \pm 0.0) * 10^3" + assert latex_printer(0.012312, 0.00334) == r"(1.2 \pm 0.3) * 10^-2" + assert latex_printer(120000, 370) == r"(1.2 \pm 0.0) * 10^5" + + +@pytest.fixture() +def resource(): + yield { + "joule": OrderedDict([("kg", 1), ("m", 2), ("s", -2)]), + "pascal": OrderedDict([("kg", 1), ("m", -1), ("s", -2)]), + "coulomb": OrderedDict([("A", 1), ("s", 1)]), + "random-denominator": OrderedDict([("A", -1), ("s", -1)]), + "random-complicated": OrderedDict([ + ("kg", 4), ("m", 2), ("Pa", 1), ("L", -3), ("s", -2), ("A", -2)]) + } + + +class TestUnits: + """Unit tests for the units sub-module""" + + @pytest.fixture(autouse=True) + def reset_environment(self): + sts.get_settings().reset() + + def test_parse_unit_string(self, resource): + """tests for parsing unit strings into dictionary objects""" + + joule = dict(resource["joule"]) + assert units.parse_unit_string("kg*m^2/s^2") == joule + assert units.parse_unit_string("kg^1m^2s^-2") == joule + + pascal = dict(resource['pascal']) + assert units.parse_unit_string("kg/(m*s^2)") == pascal + assert units.parse_unit_string("kg/m^1s^2") == pascal + assert units.parse_unit_string("kg^1m^-1s^-2") == pascal + + coulomb = dict(resource['coulomb']) + assert units.parse_unit_string("A*s") == coulomb + + denominator = dict(resource['random-denominator']) + assert units.parse_unit_string("A^-1s^-1") == denominator + + complicated = dict(resource['random-complicated']) + assert units.parse_unit_string("kg^4m^2Pa^1L^-3s^-2A^-2") == complicated + assert units.parse_unit_string("kg^4m^2Pa/L^3s^2A^2") == complicated + assert units.parse_unit_string("(kg^4*m^2*Pa)/(L^3*s^2*A^2)") == complicated + + with pytest.raises(ValueError): + units.parse_unit_string("m2kg4/A2") + + def test_construct_unit_string(self, resource): + """tests for building a unit string from a dictionary object""" + + assert units.construct_unit_string(resource['joule']) == "kg⋅m^2⋅s^-2" + assert units.construct_unit_string(resource['pascal']) == "kg⋅m^-1⋅s^-2" + assert units.construct_unit_string(resource['coulomb']) == "A⋅s" + assert units.construct_unit_string(resource['random-denominator']) == "A^-1⋅s^-1" + assert units.construct_unit_string( + resource['random-complicated']) == "kg^4⋅m^2⋅Pa⋅L^-3⋅s^-2⋅A^-2" + + sts.set_unit_style(sts.UnitStyle.FRACTION) + assert units.construct_unit_string(resource['joule']) == "kg⋅m^2/s^2" + assert units.construct_unit_string(resource['pascal']) == "kg/(m⋅s^2)" + assert units.construct_unit_string(resource['coulomb']) == "A⋅s" + assert units.construct_unit_string(resource['random-denominator']) == "1/(A⋅s)" + assert units.construct_unit_string( + resource['random-complicated']) == "kg^4⋅m^2⋅Pa/(L^3⋅s^2⋅A^2)" + + def test_unit_operations(self, resource): + """tests for operating with unit propagation""" + + joule = resource['joule'] + assert units.operate_with_units(lit.NEG, joule) == {'kg': 1, 'm': 2, 's': -2} + assert units.operate_with_units(lit.DIV, {}, joule) == {'kg': -1, 'm': -2, 's': 2} + + pascal = resource['pascal'] + assert units.operate_with_units(lit.ADD, pascal, pascal) == pascal + assert units.operate_with_units(lit.ADD, {}, pascal) == pascal + assert units.operate_with_units(lit.SUB, pascal, pascal) == pascal + assert units.operate_with_units(lit.SUB, {}, pascal) == pascal + + with pytest.warns(UserWarning): + assert units.operate_with_units(lit.ADD, pascal, joule) == {} + + assert units.operate_with_units(lit.MUL, pascal, joule) == {'kg': 2, 'm': 1, 's': -4} + assert units.operate_with_units(lit.DIV, joule, pascal) == {'m': 3} + assert units.operate_with_units(lit.SQRT, joule) == {'kg': 1 / 2, 'm': 1, 's': -1}