pax_global_header00006660000000000000000000000064141223710660014514gustar00rootroot0000000000000052 comment=d5110c0301602eb23bb899b98053fa5815adc833 symfit-0.5.4/000077500000000000000000000000001412237106600130355ustar00rootroot00000000000000symfit-0.5.4/.github/000077500000000000000000000000001412237106600143755ustar00rootroot00000000000000symfit-0.5.4/.github/workflows/000077500000000000000000000000001412237106600164325ustar00rootroot00000000000000symfit-0.5.4/.github/workflows/deploy_pypi.yml000066400000000000000000000012771412237106600215210ustar00rootroot00000000000000name: PyPi deploy release on: [release] jobs: build-n-publish: name: Build and publish Python 🐍 distributions 📦 to PyPI runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Fetch all history for all tags and branches run: git fetch --prune --unshallow - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Build run: | pip install wheel pip install pbr python setup.py sdist bdist_wheel - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.PYPI_API_TOKEN }}symfit-0.5.4/.github/workflows/deploy_pypi_test.yml000066400000000000000000000013751412237106600225570ustar00rootroot00000000000000name: PyPi deploy test on: [push] jobs: build-n-publish: name: Build and publish Python 🐍 distributions 📦 to TestPyPI runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Fetch all history for all tags and branches run: git fetch --prune --unshallow - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Build run: | pip install wheel pip install pbr python setup.py sdist bdist_wheel - name: Publish distribution 📦 to Test PyPI uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/symfit-0.5.4/.github/workflows/generate_docs.yml000066400000000000000000000010171412237106600217560ustar00rootroot00000000000000name: docs on: [push, pull_request] jobs: build-docs: name: Build and generate docs runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Build docs run: | sudo apt-get install pandoc pip install --upgrade -r requirements_docs.txt pip install --upgrade .[all] mkdir docs_build sphinx-build -nW -b html docs docs_buildsymfit-0.5.4/.github/workflows/test.yaml000066400000000000000000000013061412237106600202750ustar00rootroot00000000000000name: pytest on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install wheel pip install pytest pip install -r requirements.txt pip install matplotlib pip install -e . - name: Test with coverage run: pytestsymfit-0.5.4/.github/workflows/test_and_coveralls.yml000066400000000000000000000014311412237106600230270ustar00rootroot00000000000000name: pytest_and_coveralls on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install wheel pip install coveralls pip install pytest pip install -r requirements.txt pip install matplotlib pip install -e . - name: Test with coverage run: | coverage run --source symfit -m py.test - name: Upload to Coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | pip install --upgrade coveralls coveralls --service=githubsymfit-0.5.4/.gitignore000066400000000000000000000001071412237106600150230ustar00rootroot00000000000000*.pyc .idea .cache .pytest_cache build docs/_build docs/_doctrees dist symfit-0.5.4/.readthedocs.yml000066400000000000000000000010701412237106600161210ustar00rootroot00000000000000# .readthedocs.yml # 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: configuration: docs/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: - requirements: requirements_docs.txt - method: pip path: . extra_requirements: - all symfit-0.5.4/.zenodo.json000066400000000000000000000003451412237106600153060ustar00rootroot00000000000000{ "creators": [ { "orcid": "0000-0002-8646-7693", "name": "Martin Roelfs" }, { "orcid": "0000-0001-9273-4850", "name": "Peter C Kroon" } ] } symfit-0.5.4/AUTHORS.rst000066400000000000000000000004461412237106600147200ustar00rootroot00000000000000Credits ======= ``symfit`` is written and maintained by Martin Roelfs. Contributors ------------ The following wonderful people contributed directly or indirectly to this project: - Peter C Kroon (@pckroon) Please add yourself here alphabetically when you submit your first pull request. symfit-0.5.4/CONTRIBUTING.rst000066400000000000000000000043271412237106600155040ustar00rootroot00000000000000How To Contribute ================= ``symfit`` needs more contributors to help it become even better. In particular I would like help in the packaging department, as I am new to distributing code in a cross-platform way. Even if you don't know much about this yourself, it would be great if you could help provide information about on which OS and Python versions ``symfit`` works and on which it doesn't. Just open an issue_! To make participation as pleasant as possible, this project adheres to the `Code of Conduct`_ by the Python Software Foundation. Here are a few hints and rules to get you started: - The core principle of ``symfit`` as that the way people use this package is beatiful, easy, and Pythonic. Under the hood ugly code is sometimes inevitable and therefore forgivable, but the users should never have to interact with the package in such a way. If you are unsure your proposed code adheres to this just add a pull request or open an issue_ to have a discussion about it. If your pull request can indeed be worked into the project, great! Otherwise, no harm done. - Add yourself to the AUTHORS.rst_ file in an alphabetical fashion. Every contribution is valuable and shall be credited. - Please submit as many fixes for typos and grammar bloopers as you can, as I am known to make many of them. - Don’t *ever* break backward compatibility. If it ever *has* to happen for higher reasons, please open an issue_ to have a discussion about it first. - *Always* add tests and docs for your code. This is a hard rule; patches with missing tests or documentation won’t be merged. If a feature is not tested or documented, it doesn’t exist. - Obey `PEP 8`_ and `PEP 257`_. - Write `good commit messages`_. Thank you for considering to contribute to ``symfit``! If you have any question or concerns, feel free to reach out to me. .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ .. _`PEP 257`: http://www.python.org/dev/peps/pep-0257/ .. _`good commit messages`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html .. _`Code of Conduct`: http://www.python.org/psf/codeofconduct/ .. _AUTHORS.rst: https://github.com/tBuLi/symfit/blob/master/AUTHORS.rst .. _issue: https://github.com/tBuLi/symfit/issues symfit-0.5.4/LICENSE000066400000000000000000000020341412237106600140410ustar00rootroot00000000000000Copyright 2020 Martin Roelfs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.symfit-0.5.4/MANIFEST000066400000000000000000000004531412237106600141700ustar00rootroot00000000000000# file GENERATED by distutils, do NOT edit setup.py symfit/__init__.py symfit/api.py symfit/distributions.py symfit/core/__init__.py symfit/core/argument.py symfit/core/fit.py symfit/core/leastsqbound.py symfit/core/operators.py symfit/core/support.py symfit/tests/__init__.py symfit/tests/tests.py symfit-0.5.4/README.rst000066400000000000000000000051341412237106600145270ustar00rootroot00000000000000.. image:: https://zenodo.org/badge/24005390.svg :target: https://zenodo.org/badge/latestdoi/24005390 .. image:: https://coveralls.io/repos/github/tBuLi/symfit/badge.svg?branch=master :target: https://coveralls.io/github/tBuLi/symfit?branch=master Please cite this DOI if ``symfit`` benefited your publication. Building this has been a lot of work, and as young researchers your citation means a lot to us. Martin Roelfs & Peter C Kroon, symfit. doi:10.5281/zenodo.1133336 Documentation ============= http://symfit.readthedocs.org Project Goals ============= The goal of this project is simple: to make fitting in Python pythonic. What does pythonic fitting look like? Well, there's a simple test. If I can give you pieces of example code and don't have to use any additional words to explain what it does, it's pythonic. .. code-block:: python from symfit import parameters, variables, Fit, Model import numpy as np xdata = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) ydata = np.array([2.3, 3.3, 4.1, 5.5, 6.7]) yerr = np.array([0.1, 0.1, 0.1, 0.1, 0.1]) a, b = parameters('a, b') x, y = variables('x, y') model = Model({y: a * x + b}) fit = Fit(model, x=xdata, y=ydata, sigma_y=yerr) fit_result = fit.execute() Cool right? So now that we have done a fit, how do we use the results? .. code-block:: python import matplotlib.pyplot as plt yfit = model(x=xdata, **fit_result.params)[y] plt.plot(xdata, yfit) plt.show() .. figure:: http://symfit.readthedocs.org/en/latest/_images/linear_model_fit.png :width: 600px :alt: Linear Fit Need I say more? How about I let another code example do the talking? .. code-block:: python from symfit import parameters, Fit, Equality, GreaterThan x, y = parameters('x, y') model = 2 * x * y + 2 * x - x**2 - 2 * y**2 constraints = [ Equality(x**3, y), GreaterThan(y, 1), ] fit = Fit(- model, constraints=constraints) fit_result = fit.execute() I know what you are thinking. "What if I need to fit to a system of Ordinary Differential Equations?" .. code-block:: python from symfit import variables, Parameter, ODEModel, Fit, D import numpy as np tdata = np.array([10, 26, 44, 70, 120]) adata = 10e-4 * np.array([44, 34, 27, 20, 14]) a, b, t = variables('a, b, t') k = Parameter('k', 0.1) model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: 54 * 10e-4, b: 0.0}) fit = Fit(ode_model, t=tdata, a=adata, b=None) fit_result = fit.execute() For more fitting delight, check the docs at http://symfit.readthedocs.org. symfit-0.5.4/docs/000077500000000000000000000000001412237106600137655ustar00rootroot00000000000000symfit-0.5.4/docs/Makefile000066400000000000000000000151761412237106600154370ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build DOCTREESDIR = _doctrees # 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 $(DOCTREESDIR) $(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 gettext 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 " 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)" 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/symfit.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/symfit.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/symfit" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/symfit" @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." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." symfit-0.5.4/docs/_static/000077500000000000000000000000001412237106600154135ustar00rootroot00000000000000symfit-0.5.4/docs/_static/callable_numerical_model.png000066400000000000000000000677561412237106600231240ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 3.0.0, http://matplotlib.org/ IDATxwxe$@z AP*JS 誸}\EY]YzUE(RbdH mf?$2I&dJfGgD*dltDDDDĽEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?("""gEDDDQ3 """"~FPDDD(@?x3ʉ' d2y;"""牉! ?ĉz"""r;F= P0ވ#f3%H}EDD?/ωo?("""gEDDDf鮸]`` AAA~BDD&Rt|ӹpႧ1DGGSvmOwEDDD(j%%%@bbb]_l69u)))nom4 .j%66POw#ԩCZHMM%??OwIDDD&Q/"""5EDDD@<RCI q]! ӽY """Yk`ݳ`>q*$ \|WLϘ1[o06mСC9x`6yyy<4nܘu2x`?^}m6/111ԩS={rW~мysyRm߾PRSS=+Ik`n<3q^lO<_),,;$''ɓYr%K./$;;VxkwfΜɮ]o߾?%fq!?6JLLLd׮]=yd&OL\\[DDěY- Zc{SE{jxuJ]ϙ3Mg={6 ,O>,\X6l@~fo?{yŋyǝ9.XHϝ~$WܹsK,X@ZZSN`ذal޼޽{:""-;u_)60b^%^1x,6l={(((;,iC۶mپ}{Q5ѣD~p?<ӧO',, w̟?ýrmIv0fcʔ)~m j׮M J$##},ֹ呗Wrm6:IKHWSjnٻw/6lQF<%'%%yf'RDDFS&cYsWNupҤI|w|嗕l{fƌL6yLzJHH۷gŊ{]V'z%ΚjL{/|\Vo'|5kְi&7o^xTT믥gff5@Š^3uTJ~;VS%&&oҧOzHM⬩ۀ@|9O E]+fcҤIX7_N:QV-֯__Xzz:kWk'**kٲeK &<<ԏСAAA4Κ56]Ic`|t? <,^իWV2jA:uGᩧQF4lؐ~vڕ hӦ 3f`ذaL&&O/L֭iݺ5/23SXh'NtWDD)M:@nQocWBvNqfgϞ3g> GPP#FŋݛsxKv<3\x'믿ҥK⋒ݮjr)fϞYrvc޽мysV\ɭފ^Ø<:0u{(YiP_ &tpUl&""2OHHzX57oW^iӆ9sХKjDDAv63_ySAp>k .R@q={bZ=  R/= @N&4iVCX{,TG@ct ųƮ]?KPDDD\NXx/eALG4u2 """:)`H(ȁD9DxW~OPDDD\Xz?B|jt@c81U~>x,кQԹA """ninY\V<6 $ {އι8H^STt W a$7!oCFMgH%#=uUF<_ :=Cg)P """ u{PW ̿'OQiHZ_ >msD(fT7#"" O: , _ 6 _0? ^{i\VDDԋt޽ 6e\3GC\F#RFywJ=}vBCCIMMPDDBq]7fP^s$LZtrq7CϋhНl6(ZDvUrm٘tZDD,aQnW*_`a>xW) 7{(J<wtŮ:믿^湼< ԩSڵb""'`P8}6 {%N(v-Z'rחzfҫW/Ǝމ3 R!,ƭ&yW$ Rjr)fϞYre6_}˖-㦛nbժUQ&]vʩ0O5@u`}غu+zM6X2mnvVz'""n-,@6ƴox{%N(%zp'"ҾEA^Dw06|m^ (KV ,3AWjZAq9@xC^0rԮ!J^SΩ#V|ꈸT"""?=`6aR熿J?sx缏T3>zli7sofɢ=gSt1|_DMX{<CgAWn/;W ̿]VZ\p=_!""fdo`j>vT"ԯ_LBCC1Ll6.\@ff&'0PDDGynxlذ٣T&h6mJAAS jժ?00ڏt{^LE7_Q=@8` PW0^$ ѩ@!ˑX7 t{' ˩zW8y$| ͫC!88.]/W+6N鳈Kfpk C$!a0['0>͛GXXsOt8y$ӧOk׮8pF{8cƌ2EDDjӰ`d|gv3} yodBm&&˵iӆ}[oU999lْgy)S7KVVUz?:n?A&0v%Dtj LDD_m8x ˖-k֭Kv8tPm vRtW8scƭƭ<+a|ٳԩ۷kvADDD {w?#5jS"fggo>@JJ #--l>G{޽{3s̒맟~-[7|ýދlf0"""5̹rNӽP^1{n.Z*^7~xΝ ҥKl==9ӧK?ѣ9}4M4!118}W8>m0z)ԩ^I u@j-";(l-V}`^h)`c|!#8 F-Q(xo'fal^5"""RfM [_7Lɳ(""-Vi=۸"txHEEDDV/}&R@wZ u;dz8#p aB{]_i """=  B_{,i; \ XDDĕq9xYvpWhr]a,V$"EEDD\^dlhv 6Ԯ:ID.)`WZ4zW)&of; 0ߵ=aj7\APDDU9l, <|HcCpKMD@WJ #Cxtcyp|<A9\n&)Z`hsw-aFOC?tl&;B|w-Ivqe_[$"PqpS0_SĜu& q,X8A!0jQg@6CMn&Q=@qFEDD!;Cp8^ t|e57mJl6dql&"",=Φak m.=_\nވN6}kPDDĵ27}OB8|5LF6wA$"WIPDD#iq+X2 ¢JJ??q@OٻBO-%P~~Ɵx;bq]YCbH} 7Ԫc/'(""ő5w=[uy6qr^sX|pEDĿ8|E1KPDD|GӾers…YZ"kEDD}[&^F.^)4(""^\˴7ao^ŸHQ-`s:[$_94."APDDjn)h_>c!hp߿~VwNO:xUGDDBPDD<*[*y |(Xyg BV>">F@DDijK-͌vU_K\['RDPDD}Aa@Jx|X8|qgjœ_ JT$,X6R@@ znߵ}R ""Rs\^*'`}pr?Ԫ #C>P#,nb:xW"5x/q⇈THPDDӁ1ClTe^j(ΔdϥiXP^q@>;ށϟlf jtĎuә6ܒǢ#BxqPFWJq%agSL_n:-2rp/{g(""ޡ"|0e\ ebĭ,V&c\c&ck!(""5_?~X al{0iYM3lـ\vu_Hv6(rDpT"|jډs)Hu|,NCD,!4m^88EDf:̽Q7HDGP$ c7px@yvK@EH>(OJ 0ċʄK IDAT%(Haᓧf<3cq Ž#g\hf=БӼQ!zzHP '?N_pgqm@jfSd6 ++pOwGDfZ u;dzR g)ߴgBў*.|q9}{֭[4h111L&VZU\AA>,ڵnݺ0n8N8Q=_z%L&S(/q5F[7>zgS`F V(3K1999oߞ3gy… ݻ^x{b ~'\}oFK~{Wt_DĿ%|_|,2ʼ<9\3*,żb 0`"""X~}z-:wLZZ-Z(AAAq%= 9均0fv3K1,L&ׯݡC!>>QFqQ7PDOn/;gOҟřݱX+F"77{1cTK.̟?뮻'O2}tvʁhԨ呗Wrm6}ұvy;gʵ;&k(hqfw&Fjwީ>|8ڵO>|'̛7̘1X_Dԋtn;gg.M|Œ\&,˺N豸FAJJ ׯuҮ];:TnSUsرv[Dķu  Nřpq;t6m*w "yyyt޽6W""SQ=w ~?gɢQ_~řUNÇKSRRطo 6$&&{w{cXaÆԮ]޽{3l0&MO?͠AhѢL>ED-y 1UHLghRyŽgM"޽)S0~x^z%֬Y@JnӦM#Gp?ѣ9}4M4!118+w9x||}a͓`-pGώHSb7%#"5bQn16{ׅ/ 6 [_3!@-'0,V_>۫Fo/UZw_vyjyz|ܸQ/I{kԶ+M A NԶ\v^! v9t|IlL,᤺{6ly 6l\ jVɑrͯS.0g{ s4oPVMygOPD% 6ù\JaQeuAia'ltM.uFtD/J e vW21C:P+P{T]MuAuD\8 K` NK \ݩmwvuәpoh[<+jOsywQL% @0Hj]6کoE#E;}A^s(쎩mXmL[\fMoBN;{n9o1M#xWkziJ~.o(""+Xv?\"Zˡ s,ʩmٙrԴl@zV.;S:m=;꿿Ph5"4^޺1&(H-):ӷuK^S9O;S^2ϗ3]y{a>E3)]ED>6Mm3^N4̱3m,W)Yt\*熦<Ԋ[4pyq ""RVX[H^m\;Wřs|C#B-o3Q蛻)%#+ ٔ',om<惧>ஶLLjɍ1n8NPDDJ;{! ?hp XQz͝3;E`%0aJwmqtSJ"ٙbGg>I-i4-(""d#|4.b6xIqfw6Yt,3:nJmV3gKuSO:ED[ΥQ+8| nwgvm@lr(3Fwncw\KL:.8W.?wS/~3q˹~ 0R/n6xS43(S9t֊?*.boGkco ѩStrM("o)rWUNJ3B{ێr|&| %g9EDMiϡ -b v/SB=ex|{ śabV\UEM8ٟ(TG\Wc9jL}1d&4jEJi6-?bN5Nm 0qOfLيu=/ ""hhY>jL}ҾÅfQ!֫DR&MZm$oo:wdz[byǵ4o>wy[2h bbb0LZ6^zԩCϞ=9p@}w'$$N:m6W}e -W𘪕nwhv&?'6/Œ$QXD|ßjcͷ'm<`ϢN@=m$סmJb0''C1|2Ͽkgܹ\wuL>}rAXl'Owޡ[n 0dZh$"Y%U^o7 26{ԟyrmZʪ¬G8z:zAiT/#glX&+W2tPa<ɫ?n>]tcǎ̚5nC2c b6 ++j~2 8-78@ϩC -|86sZ3kz7}{`ERRR;,y,88=z}v0??={sϕz;dW^^yyy%f @D3y;s|}dz-,ޙ{[pl|4WG_q m!ORFFK#DFFj5Ob}M1cӦMfE/9㘷ax=a10j4-`שޖ™|#Bxkչ!t:8bWic*=榪:u*SL)6ک/"rf`TY-F rq]gW?3̹6Ğc35>FE :Ү2#|7nL```Ѿ^rp܊HU瘷|i;ϸi Ԫ9'PH՝m),39Z6ˤ^tS AZ)0>>(֯_7 kl«j5kצSN_aÆ<~z ~crNò0A߿@' 2rywL# <٫ng6^9|puJJ aÆhтɓ'/ӺukZn/Lhh(cƌ)yM޽6l&M`ʔ);[nn{4~ߺ9cWne昷f4;h^`֖#|8#ϓI}CJ,8WݻwTr]o̝;gy/2qD~Wt_|Q#G8}tȑ#9s _HOOm۶|Źg9cW#z[jX9 r0z)4y9r*w6aվ_X^֪xITGHċ+صQǼm–W`Kp\m~H))g~xTm9Z37ȩdm4 s Qv:Lc`!9вG*9!!dd7KTDc#X3Yv!A<-]C%qƌ)vbo|)C`"h# 0&,kwѺ1dԭa<=q*u?wl̨ꨥHeTTHRD*UO]m°BpXůB0BxKhc&m(|cFuF -VQ/5FED\'0mqgx_@Mbӏ{Ns(5_ =[roԲ?"\35JX٨%a BDu⿰~t0/~{%9ǿJaT/q]&lЛQ+⠾FJ} ?94JXt}kPD] kШZMtH9e7i\,p}dOjEstŜRVxR'Hu)8|qݺ1~ [l1 k^{C$U Qn8w箦|K`I^ED!qی;=z t6f_(cq5`RV H9:* TEMPDN>P.  C<+0k=AQVԫ]^u+ƌ]m8TTEMPD:- [^d1sa(yw<ѫ[4p{oͬ:٘U1oBKw|KNHeEwY-+l)ϟaBιTٞ_Ihu0+ۘ-R L5hH c^1oOxz<~`q 37f3`HfLْ֑5ʷ,V˫6bՇc`X8ƨ_[rb)fn<̞_hbxGKi\=,[}5,"j1F*9 N>mzc_Ngx㨔jc݁ ^ )s[cyGKկϝ[62 "[R-fΑ;vvYŸ z#xu [0NX=Bbec~t_p|etf)W>{I1ovG3P Z^j)g]Ξr-s~9w0v[~5Gʂ~r "[}_h2Z=l6&lx lV gLKʸN8ΙS YMm=J< ߍ,&]Mh5Eķ^*wV?ɫceowkJxN8Yk ͹,ؑ/S8@LDْ `1>R^|Fu_)? =B@- 6Sf4ќnꔔGΰut gL9ϜRgТa(zdx2;3*IU(oJlzq$5j"䟇h#5o%%e<ȑ3l9:Ss-_oUz<ԒA7XzՄV+`/APD|W@`^,u\7wEB6痔!*;֑i۫rLϺȻ[dgyVyW+E@9K8+؋P > ?o3'B߿@`\R Te1|/)/!>OjE6M1*OGC8t&g{ "Ҿ1OZua[vx6,)SC9r:EySL96Wef/\YV0#"kWٴmeUP?Y\s e8q"ovΝC=T/X%(l6|cWd7]~\zSS)Yp!SLpQqvv6qqqX,:t_Wnf7TD&y5z(R/8#6__Ւ2b#n(Kh??dvs3&lɵMz/_qˑ,T E1>WZŹsxmӦMΝKv0[n|nݺ呗Wrm6uq6Kgl˸KU9RRR,S3>ځ9ђ؆%;s͑>c|.Ξ=SnDKuFǎy뭷x7}݌36mS+".bNmKJx#7q4 `mqT4-k(.7Pf6y 1GǗ :WWD*("S/ұv-n+Q$$ .@E~w. ssolne q|3 ⡮Pxԭ]{TEDRj5Snx̥?mLK5d46XW;{_mۻq(ln廏Gy|TLVIdM[հ([ݭbkP}PVbV-.Jê(J İIX9'rB&3+Ι{ι3s.VoïwI!"G4=LdO<;k2}:mN^0 rt*mܵi,tť]eP'bg[DNID2ƛ<|lÀꇠ`uIzK%SPdm}iY3[o397o{~y qs-"c3 [HpLQQIIIH9+ݒ|i_SaŽs[&L|uǵ ܯ˾ǝN|^-ŧ3Hp=ʼnD:ݿ6~D,),%{ \5.l ؑ- -,>͟'{9YfKKZW@RD[S"ҘOv." װz+;n0iWprg)Lݫ}߯ݕTuDD "8 @~p3m*;wkA ] _|GynNsO16|c8J=zSemM; 1 0isKæ|ZٹճT3!Dz,\ +ĵoHVLwm=NfM^lcM E`|qJ/hso.}yskaVZ_~Q^q;׭a5{V>M,"@{̍2k%VYmw_V3W\@_g%L1;QէUDDQ(" cMx8sLC^~e 2dGu۾#$)))s"99cǎ~9DU2 z-1H ϵsvPڕ^y%_p39,;*#.l;wf}}"bp̘1UݻwoL׮]_ʌ3ܾfU|s8 %te7MX ~iI3'kij^ܹlճ̩h{߰&Z[=۩E1UH`m͛7w>kjj*5MVx∋k_aBݒim9f0ypTybeMAq:\[瞿}Q#>TasNw^i\Ʊ>x\'SZ~xnvy1fw^qKfq_q:O n Ns_ؽ}"b <@NN|\wu8nvzݜy{ "RMX~w|9r6mp饗'йsg:ľ}ڧ0}t.\HZZo)`"m[ʕbYU+(a}'!o@>yPq0GhE(2V*v'xal`n??X6õApLQQIIIAC$Nns_gW^ȳU'PtNr׫`}pp,6B}Ɏ1sVv=3nÄ>r.iZBla13 N ryecJ~ ޛodmz 6d?0?pip6^K㸸~ lgHw>ӠQv[MM6x|ΟӀ,U:3+g^yȽpsy kJ05'cё2njũ3naR;N{J]'ɳHQD*f_=):UZS4l%-iOA|rDoY4ښ3slaҋgx4Pib&5){MO,13{|O~ l(ɳD:M7QV5y35Yi1l'V.7 }l/QvWD^qzuqJ,"&5 -ԧ=7~\65YKcv5Jg5geipCB+kh F rpѓ<~k*rz\#{u4M'Y0P0V.PӀ 6)*ar+a>0}rjb$a=,AbZ5e-DcwQAs8>?zN#|It[$^8 3/pj!~} .QK>?Z:+"Ei_ H 2l򇘗i ~gdW9\Q}^ |uIK'>6*=j>SA n#>&FV)) .ny4 ܯ (9]Naqc9Ôh*("?EÉIӥ=:uf|'vĦpsZ$pj)VtiݜIqm6JJInOv`|46xNyEDC` va9Zi4(t}kπ[\{ȕ&mT".@AL8|yx \k R9q `{'Vx\xUF5f.IAz)9~k䪏籪y cbyqFٷhkΎt4Z2d>t^Bai&i_Lop0fmyKp+"| CX FKN.#\[;-r f+1*~&Um4lyx)9 g}8Le eEv̳uڤrg4&^ 6t`""r!]*FX-j%o Xa ޙ\7Zdx > #ť1z[hkEGv~k^D$2 $YY =_5 ؊gR7j,pV}$۩"G lI}frTs+"ZBcAʔkѿpzӆ%kLe@0=l_yAy)DYOK| @YPryc9~ok{}k4ۆ8<{[OG`Нf`/\~""M+Yʅ^Uk?7 61Q-d 0_e<"""!@%r٣`.ʂ –Oag0W`[;'5}+""!#,,Hbb"m۶eĉ|^_ꫯb|>R41;^jGxs}_RIi0:{W:Vm@bjΣ]""bb 8'')S0p@y$//͛{|]RRR@1>^Êݴi戝+(">g :F<C8 i""\zuǯ m۶e֭1l6&.oEe>ZG\.>ǻOAY6nHSвc%%%tܙ:p5װm۶@tOYaݔQylGnn6pmH#36lzخ{s=СCپ};ݺusRJKK;_dZ9j3qoMϛe0^[|@DD) J SLgÆ tN'cĈ̟?mٳg3gΜ:#9x5k v fS{/+V`ڵ v;d<U}߿]b5p~'op DD$"a{,[_9rssݻ6qqqy*gJ͂a Y&""laN2%K|r)(( 99fɓi߾=YYY̙3K/nݺp8?>,\0h?g+LګEDDLX9W^y۷?o~ HNNo߾[v'IDATK.$Pݖ/c<| yVəED$b&@" UzmӐ;WcI/\""L0c%c~١/y@PDD$)gJlHh>4!aF"D ODD X:"""(UWII͔0"""RJQZp椇FG?"""(J81/n>"""^(T%o?P|뚷1$` :J޹kO= ҇7~EDDˆ6H`*x8d_,`,sQDDDΉ@ < R/'1qsPj+&:J3h ZDDDP(c5hKhMU>:YDDJ`׫AUpw><(ɳH) 4ð9g(?a.c<^*FIEDDJԏ.%a|g˶,]x~<4b?7n~0Av=)ɳHP(ָ8J<: vr?~Ð ֯;.+Xp8HNN`w8+^sEBEǵG EDD(b^hf5s Ճ?8;J}zS(g9+ =_ޮ'{xrY5|EDDBxk8u˓jj퟈HP)/<;= l|`% :MG Oӻ svWgL2nkP _`$6۬ >~z.]JFFqqqdddlٲia~}k\4֜MjWxRy\S"""a%lF~mM /СCy饗3f yyytԩN͛7s7c1i$-[ 7 4hP~7 EEEU_oxݱZ/WwEDDpll,g͚5L45k0a <5k0}c}CxNxG\\:I!fgwEDD"1cv `,Z}q]w0ydڷo_#gĈ̛7 &|raÆ`&{T*񲈈C7x#G=W^|t}a2do<ڵ+ovT]]i$a0GY]?R0 [? ]""""b@PDDD$(0 EDDD"@PDDD$(0 EDDD"@Rp *p8uv#(..cǎAWqq1FP،HtrAlvAǎٿ?IIIvH=n\z0 IKKnplNv$idzƥ=n|x#u%2^PDDD$D͞={v;!EEE1rH5kX7>ǍKo{6DMD""""FHQ("""a^xt߿?ׯvFVV$11m2qD`w+eeea٘6mZV8JVHHHO>lݺ5 <#ӬY3t{Ngd[qƑfޫa̞=45kȑ#/0L6~m۶1|pƌþ}ݵÔ)SOXf dffrĉ`w-,}g,ZJX9~8C%&&UVG?`w-l̛7_| sNz)?kM։'Y`z)y,Xg}Fjj*W]uiR6h Gvvvձ=z0qDسtaڶmKNN#FvwJII ^O>5qqq\vel#GPQQAJJJ)))W0 f̘aիWVz-n쮄dgg3c z!l}G\\'Ov̙3)**{DEEQQQO<7쮅%=o޽RXRl6[dža9& 7uTaÆ`w%߿￟>`w',9N ܹs۷/_~% 7`ɒ%ٓ\MFZZ~{tk\ CT֭3WXXX_E0{/+V`ݺut! +[nW`ݺu,XRæ]vddd8֣G.]̚5n ޽{w^6T l׮]qKkCTll,g͚55Y!CW0 NʻLzzzv~򓟰crss ~3rssСC/ڵk;wRɓ'k.ZWVVFNN~6c n6 Yh㮻 v”)SXd ˗/'11j599f͚w!11Κ͛ӪU+ӧ3dΝ 7-[Xh- vƸqx'ԩ={d۶m<3qZURR7|S8??\ZlIN6msΥ[nt֍s璐-^CB… Ν;F~`w)ln^y`w-]ven+Wz2ݻ- vŠ0~SNF||ѥK6JKKݵ&kڵn~a4}Q#55Ո3Faر#3("""aPDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"@PDDD$(0 EDDD"lA=/[wIENDB`symfit-0.5.4/docs/_static/fourier_series.png000066400000000000000000000424071412237106600211550ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 2.2.3, http://matplotlib.org/#D IDATxyxTd$aK  (D"XDB[j"nZQ[7Z\DE vd̜JfLyk.33g xr~~\acDY‹aHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHaHa>|ھ}7n,eu8 Paaڶm(gH`JII: pnݪ[%Hqƒ@Gj@)))NDXeþ$3Ńo#p@!p@!p@!p@!p@!p@@IY "z$oI/IOrVH_=) RV{iׁc{I."-Mw(p0+::@rKbi X3 [؆4?-Y'% ϾS*̓λ|2 ~e;QJJJR~~j>):^m`>6M\]#=EH7eK ~@ 8Sv?WJI)uoHѱRNzq>|$ (f+sv w皭{J $|BŁY>NrtLY$54Ҏ ⮃#`)@poR;yC~ƙO " 8/7tn\jQn{TrD:п?J1ygAss7& wJAzi$* MR_6;ƚH=,:_`M aԾ *`WŇͪ[&%*J#zJ b#ڏ[O顶ҧdكҶB)3m$QgK$[c2 ?_*9T -O~Jb[;Ņdc??ZyKvC_WSj}y|鬑RQUZ_t]RӎR^B)qؾ[[h>ɗFJk']>CB0Oz<V''+k+|?IȏIII?0+UFRVfg]xf'Iʷ{'FUI+-~z_yՉ`aHCڶܟ"Rݿ7\.ikRb-7l,@0܈7ӥw(1Hj/x i}_!{} ԫ'/!;sx$(bu$$U|WX?R\כ[v xL ℰD "#%40\i-.:;G*>hV$ xdx;ڞE=iXs H+RƭsX3 w'G #bCp8Wx)G# M >u;mSb;stItpg燋D!eVG 0\D/K+%4dճۖ\hu$zZY?$Lj&ݺX:"?~~ۨ8| }z" rcSC$%.|wh9h]<`& ڞy}HɧJQLI:N;M>lߴi.2_˗/=ܣ/={v# k*]th# C{WKo\$ߙupRk@J&e =^f„ ?~yAAI s3v8IRmH{RpX"jj5zvu߽֡&#!D2V䡽TrVs¢r;*EWߠv}*cٞc֭[+//ܱ]v)::Z͛75qqq Gx"LQW& b-FbӆJ uo%TQF7b#%H|N.?*àZ!>}('fsQ޽+*T#"HҨi[DyKO'J9-X[J٫m6 b XBeŊ͕dߎ1ѣe?^W֌3K/;$~k0.:n#}ՑXeWQW)-a]:DzҥK5p`\6Wo+;vAIJKKSvvƍ{Nm۶O?M *mqpF\R~R4k$J.Jz8_ݙҥHms&4'K!/%bu4epjWW^ycF.*1WJO2w +n(VIq>iFcژႉVGxI <RLhjVOpi數}̶3eC|t5ߎrJ41q@ `Ao_n`|&]")Uzdz*M?WZ8s43SZ\X\\iOҾ ijJ~+]h9P5y̅ F.7ϯ4 As).Ij}j\iRA;Z7}xc Kup@FקR}`>~uwJK|E7[OiBT @5ʪcy_s';meRc]LvfҦҬͅ!@ `l*!H*H+mYp(1 (8;*LmX.޺^JJœg؝;F:].:[7{mI,l UR vt%R1_sZ}O R"}:FБ@5xhoxd۾3[:[b&RtQks&O lHPܵ-\"@FQ8nsҞY d> M^o} D@5zyI33-B>QVe0fJ? }@,jAzU/I+x$P"\RCP `X@p7j ́v|1HY_i'Bs}8sePi]>X|Ah jt„!`FQ@p7jA`x<ȁ_@v@zj.J`@5JB:ԬJI{m T# 8S1HG;@0 F MCs] T(m`<%HX.iʩOPC$P00E @ %^C_Gj6n["mBJk@-@ʪR$H. *B0w3BY(ѫok3~-8AJN4+==] ,)Sk׮JHHPJJƍ0E >7Vo\O5zpl3p̙;vNs=W? ~Iǝ֌3Էo_[N7t$' s" tZt$^iZp'6ɓ'kȑ5jw)S(%%EӦMEs_;*##CÇҥK9H&;K!"8`X˖-SFFFZpaׯ-[%KH6nܨl]~!@&`1[ ٳG^W'''+//\wuڽ{'0TZZ[nEw}wxx XAm~䎕RΑ~=8A9va/Ciԩ|J$#%%%|$v0 Qkǰ $em`\rKWN7F?A[$4~xeffwӧ^xjђ#F]vʒ$  0 U E$ *ԩ/Iy?[@ao] ྍG㥨MR\D'PR%@!*p#=b@Db Tl+nr1@ 5.ʗJ(";@a'ftҼB T,,Az-C@G'{Mڳ@"J-qw-s\Rˮ! !`M]`#$PjpӉ! *Q&JkӤFu@DYMHW$ȕB TS `\cԫCCP9i TX&wI+4@#Jj K޹1Q@1*QTR6\ߕcvN8*;@tMVb> #Q@1 7*`" TE B CD$P OM|:Qz0DuGԴ;>A0Du"DAsi U0DuGq#X)0D0TV!JԨcyI)*;6>KZ$t֨uD;V ifVWHFЕ% Mƈ 8^P@%>r@ZTH@*n'9Lz0GuG(V["5$uXW+j}[@%k#I0EcpԩJKKS||ӵ`*?pƌ6m(>>^ݻwWvvvɼ>C%^CRDcp̙;v&N˗4hrss+3fճgO=z/IG쩬 ttKcn7W>S4ЂlٳG^W'''+//lܸQf͒Uvv^C=Tdee)))HII  rT:RPo",r=7 ce|>Zj^xA4qDM6O0A֭[?QTUhl1ErUvu\UL6m#;ps޽T\\^ "@[̹_JjoLhjAdPw*==]999oߊ{Z~|Ju֩M6&l` §;""E(IǏ׿/͘1CWָq㔛ѣGKF &Ͽ[w^qZn>#=3fUR @' 6\JlcQdPwaÆi޽4ivء={*;;%77WQQyJJ̙qƩW^j׮u]V} "9 I?"qaXD}UPP$+11pG?И7iX 緍 .>$m]"yK, *PĿKKϟgaT$PA#%4ڞiaTYokRaR! 796%5("@t4[@pL`X T 8-m'm[*8*@;DIWP ,M N:I?mfe8 I*(V1:X ٳm6vmzwԱcG 4HfRIIp0Ou\4[6o\wq/_%KK.T۶m5n8Va?Ҍ6 A1w;vhΜ93gni2 IDAT.2ZJ=zГO>iuxĽ ]^ܛ::K͞=[oաC;7nvءW_}UsM408#4$S9QBmڴõd~ǝs%I&D hwZ$Z O>kFӴiSmڴ)QpE ,`W&V=o\Tyh $"fD/}$[*lԪ4hon @K^2W-1JXHϯ`W@J'N;G{$Q`_$ I~JKv%9l>1ڞ.u< IR| @įPas}oyJp`O_cHI~$+,`S$iL!*E G:R%IE4`o$ЦUYF슻l'*孔|^^"w7ζ`W{S`OJN4+==] ,z-\. :481 D)؝mn3gرc5qD-_\נA[l٢;SS"礻6K* m9rFݻkʔ)JIIѴi*} 7ܠԩSQW9[$Zl222… +}ݤIԲeK92!DG-8Z`-np[ܳg^ONNV^^^K/iŊ5~#^PPpbaHO.%|^jGUTؓ~u\q1I*,,{jѢE$#%%1Ⱦҁ\i2Q+a0})[T[h!}\o׮]U%iÆ ڼy?7h]V;w>u&L HY'k!0l*==]999+srr4dȐ֭V\YؽޫB=S&uqqq nrI;,}"+33S{V>} /(77WG$1BڵSVVճgroҤ$w,/$Æ ޽{5i$رC={Tvv:t0'v**9s='u uTn]bavAm())IJLL:5oR~#uPMh% m XYݎ 3@Ӿ-`8w88 M8 gٷQڿ?f8NT/iqY*6gTGbgsPrVe@V3@ר? Qs=/p 7f0 g(.m[&XJhRiFTs8CVҨϤ]JO7H8;-AS`c(FT ;KQijO- 6͗|XjO NԪG皏4'G4' `oyґ5>F;{hGioM} yo} CVG S`_$/]O L}q/@I/ >M:F/1 CŬӮUߕ%e?1=5"0[*E%eK 7@ tQ^R HKr"*qr/@{itiZ,0F]%}rUͶ+ChNA~O: Nʨh )X~N|Ԓ 4@6ǯ9Tw9gsB/ ޸Ð^,=\ \KU 7@qpJOjѵ/P,`[Kwlbkr*`9~`/.Դ E lF=lZz<)'|0F@3son'| #hV9@ph3KJ9KAS`s$Q+׵u4@w9{p [%SNUZZ㕞 Tz/iӦjڴ."-Y$I?#WxJ 3.7sL;V'Nտ 4H_j/h"*##C۶m s0/%m[VKC& s[,LC:]ObHqq-[ᄏ -\F8|JJJԬYJx} /(77WG$1BڵSVV$so;5RF,>Gr4I {jҤIڱczluAoSNUqqr׹3tu'5zGM\TPP$+11pg1 j3lIw]f cvm6޹Q:Q'qP}*mBrR)usk`oAb[J>{EV\&`s$GUi 5Dh@k)I[ IP|[Z-Re= AH/\)yKWem`bp@K#Ȋ~]G.(P CZT#$P $ꇝ?J9S*) A 0@P|HjԨR#1W8 w:;4p򷅬WF@{&./Cpt"破otdԢk: cHRRbt ?t";ZjwfH߂U;@8 L o$DJۖ@B""'CV$$"WbQay" |^i{Ҽǭp *;]$[i|0@-{&;[ DmOz Zv[ ~?"lE%VY H#յZ`'K猖JQȕUp@Y'iУa}K0@.I`Tou$mx} I, "aHn&}-Ȓm ۖU$pHrIǛ B\du4KiZ?ift:k>2Wϸl&{ź-?F^ך>.yX:<Dڹ ;{tߤaՑ6i{fHRѯXw\՜  kZH[2ݾ<! {]mP먊}/S~W>YDpFWb=0$őҷ/J-Nz :W]WJ%?J^-=#pnt_Y6?.-*m\\h' srMhNCX_,{ERnRRaĶfpfG'~i|aK)Nj"Y)WJ &ЬRHBrJ>C:?p|Tץ权^K~: [9Knsx'vGؒ8& p*E|t'VGfv`sbwDŽ4 |]r ~'Kƅ=-5M3+̭dӐ~:Wj:-]%4Stt7ͭpbz^%R[ i7>9\]VGb>>]qvXi:į˅ H1 sÐ> m*gQQRL%o>)nti2@Zfd6n^Rͪ$*9(mGp뛤YIM:Z5NIgzNZ#u8ל8KRˤoZF g =busϚ ;Z:kQU.&A}|A ZD#fF USwU`))TZdVNj迓b{(>$ KJlc]\8w[ nԩJKKS||ӵ`*ϟ={z衸8C^" ҿ~|HBq*917IH{7HE&'om@p$3gرc5qD-_\נA[-ҰaÔ^k7߄9:XiD]≳(]Os>jU4 Uw B)Qn7yd9RFR5ehڴi?e]|Ś0au & /Ԕ)Syk.kG˃7fiRyHY9V!Mih il1X˖-w]xFF.\Xk-Zqp%DDe!;\'헴wHc Ct;^$9O-)ګ?m꼃ڛz!]v=$9 ={z\Frr*|M^^^Η$#'0ZPPP+m2Z.͵C uD?BU(&\dWYRӃ7ʫ?H;,0D]:U| T8-2cVqܱu Z4Sֵ߫d&D%O밫~TZvCOjGA_qPM`,M(MSZ[--ZvW۵kqU2[4a?@)))ubM7jӤϫuT{uԨU w ZIFػMU,z]cu$[IRB#GńX+''[q>}w9s*=_┘X1ⓤ?dK#>?ɟd1D&{K]%'igVG:E[ågΔ~:p4[T%iT޽էO ѣ%I#FPv픕%I;tyGՐ!Chܹꫯ6&5O[*mFxA3[՚u2+X#FH[lu4hI {jҤIڱczlu`*do߾z뭷tSΝ5sLs9V} I }jqXҶH']duD!*JIWbv>iMe9ku4h.0 .̓;r݋Rk(ÿHޗ<+h"w?',?6@qkg#S-N %[kd>4K:NIqE- o7mYjw9\yaHZa)AxcغD5R<@XlCʥF:sYl{Ma\3 L"tooݤKT.-9@돚~Y-3Wt YW~D!+iwҚUPy_gwڊ $!e?i kc:1Ҁ[ @'4@ґ}RRq^[*}6[S$248w5IK7i7$TC;~?O}p6/kr_O՟ogä}P~-w9Jy?H_=iֆ破Y9f zrI?-ܪaHsvu.}ǠTWϐȔ.{"pl٫һ~8-6|.m_a6$wmˤu|R;<߃S^G"UҀP-! 01mB釷VU{\i?oy,:V~)&A:)#qCZ_i<35LJlkuDkyK ""aRrc"UsX} |0V*!?+sX lWl%sߤ*p*7y4|&<hf P'y{rkbmln[$5lnM cJqI<ͦi% .nv&@]TiT9/WJ=Rt\wriUO|* a*,,T۶mpT **J۷: IRbb"7JTϧj|>UOlZ+̴H<`uۭ(:cTϧj|>UOl"aaHaHF *>>^mڴQffonuXa9rҔΝ;WqqաEzH}U ԤIñԩSxkV1ϟm۶r\z)bdee鬳RƍժU+ :Tk׮:1m4O>㏭˱HmdzvZ͞=[6lW_muXa͚5|zj*=䓚>}Cźkt-XfΜcjĉZ|A)77"¡Ctig:3o<3F/VNNJKKCYZDh߾y-]TK.\!ChժUVH?PCQLLD\ӦMƍ%b+;v8`u(9stgjڴicݻwСCeadrСC%"޽[ZҼytyYNDj֬q9P M۷OKW|5k0Al2edd; Z|I>S׫zKR>}ǑHm殻RÆ ռys>:a=3=zա ٳG^W'''+//ϢPǫ_~ٳD+WQFѣ{GVH$tR_|rhMLIDAT3Gn[#FGkHu饗kѨQ,du])))ZxmK|o߮O>zBjvW۵kqUA2~>C͟?_۷:.]HzoVO=y#sWНʟ fH6϶m4p@_VT uTJOOWNNJ 2Po]KYR3 ?" M,YDK,Q~ԴiSmܸQԹsgVjc0`RSSOh?kݺE\۷OzZb$K.jԨх㕙޽{+Ź~zM6iŊj֬RSS-zcƌo>@7W`qtֻ{4h PoK}'VLl~0h4k̈3:vh=_-"ˆ 0o/:4K30`^z //Z|RRR:^{,XB=cڵk%I~_Wփ>M4I+VPYYzK^z裏j={v㕕rjԩ*++$UTT(hΜ9(77iv͚5kW_vӒ.322:i=5.,'Nпnwq0q:^β{n[o<Fm6zw%=R[['JjikkS 2R[[m#ez;m|A3FK,ÕM6HsNqɓ'tjƍZp$RqqEb[C6MsA>ǏfSZZeX,;vlcG.=SW^^x eggo$|>vmZrRSS˗XSLZl6 P$7.N;}?:>s|tNVo \yYdZZZI3fг>zHv]K.U8Viiy3J aU_ }+==>1?:>s|k\ƯX%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^/Oӧo׆ :?UQQQ?K,PH˖-SIItRaÆШQd^{5{z5n8l6͛7O/:v<NJ+a=zO/R0`^1./kΝ7n ÐRFFFZzqHj3/VyyN65Ѩ֬Yp83fHl6l٢9shѢE~TTT(hΜ9 ``{߮p8׿V~~$T .uۿ,Y?\K [o߯^xA&Mŋ;7n uM7i˖-5kVp00֗:K}z9>s|ǽ˥#FH&N]v饗^˻];bѣ5k233֦@ eVTGJ. }?:>s|ϰ EFb=;y%I}^^_J]wrrr ok۶mf7ѢEcǎ闿5w\Imݦ+W*55UZ|5eʔK%) I_A^}VAx9G]]~|***s=ٳg+ i߾}z7ԤCjܹ,=v.]p8R=#&+laf-Hr:JOO}>9>stWeX%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^X%^-UMc٥!ᣝUd "1y=.}cMavYA;֠u;+iq 鍏 #/ 8TX4fhz͝2^5*Qɉ.͚ #.ixzi>U%I^pP6Ĵ1E:6SӮIkTV:69k6 s|@IҰ$q8${5C|00m pQД1Dcr:zw_>% E?"MͭjؼT3 0^/$%Gz&iXF~jάt6ė;Q3@7 GMꀒL*HR?l;]?Jf at{aU5Ż,@? xo[ӧOukÆ ]y'4w\M:Uw}9|(ҲeTRRb-]T|0 V98|/6lxzTRR{W$=3Zj}Q+x<{;cŊZ~|IZJպzK`Y龄c39DŽjf#c7oF|JNNΝ;ey{Z` cZk׮$~^Z>JJJ4i$XBeee*//7|gh-doԱf0|h4{OpX3fPeejjj4{k^N2-^XD"3gN5UYYNj[SbvS~t,%%^ܷ î)*U;Kݫo]pX |رC7~rL kec-Wf҃[zt^8aI>K:K}z]ТS-n<ިe;榀?ؤIBr$'+ap% WB^O39>sAx҈#$I'NԮ]K/%=R[['JjikkS 2R[[mBRR7>}x0 n6j?`tjn?uM_+5pZ*5|I>s'dejk4{ߕ;=O39>3rѨbFLmڴIEEE@ ;w;$M1?G굡˱$ym_S=뜑jMa mjZYtNyW'׮SƢo*eO>c>A>OGqe/կ~뮻N999 zm6?$u]zꩧ<=֍7(I|۴rJ*99Y˗/WqqLrID1E"|!k9>sī{֫Y{6h>@ؖIRTeh ų>@~+|R*}PH5o)[Z:O6gg:>s|gXEЏ'?N>-ϧ"=s+-YD---zԤ3fgՆi=v.]p8R=#f%>9ѩÇtg>yxtt|Rsjn9|>%)Hi nT#y5٭hSy5nPK o8݇4m|^5Lv{O[Z Ww-Hr:JOO}>O{ 7+vKJp 2BQ5/Ϙ:y$932T1oUV)ô`|7t|k\F^%5٭fZ^d$iF;lN?:wl_e'yd%M57_WVL;Eu\ <e]>ҴS|wR4*ۭ-Uɽ:6]7GTUO%DZ`x7u2}$)S0d1u *\=-E{<ׂҮ^~#Eb]gI G\`#HÇTH!ϧSyƎ.o 6[v#Yjrg /ּo_Z%Irш!%͛q %O24|< ?`ie46OIXT'r {F+J6b~bPbb1#.O) Iʾɉ{ɓ&+$Ict?"1|qVڮRͭ 6d5}{%I7mi/ZÇؾM͟*qh IǴk+թ5U4m[pTE#58O nYop/K߶U'I)|zl6rG=qB~em8eӨYlM˿Ѿ+mjZ7wKĴ1E @G$I֧{\P[TmQU 9$INט=,E[WJVny\Lc=/ jWIGu_$'44ͣᵇikt}79fW4S7A6WeMc9>; N_2Bͦ;A~f륵ժmS_Y&վw%'tLTUMPF 42@Cx|ؾM6%enA12ۧ{SXdR'ZS|%82&U \ p^PH/$9RӔq˭5-i;}otnԔYvb1z?dbP5Lyվ"5{ r$MhLߧU Bz{>\+/ILe [Ç԰nm=7@B+;7L|ӯ5iݎJ^h{lTc0}spLy}{Extc)пS6킏 EwG;O`U6~JϽGMHSwreudToy.+IzQ8T;Z#g7u;=@cЩiFOdVfyc4}PUјٿ9SR7ռB_| f$IF$3qs)e֜~fO"tH_HvWTڟd. Ij话Rfrl>ǣ7KZT2+\ rZu|u.NnrU}'u|9ߗS2b[En>?ۯ-R$ڻ. A=jZN֠$iY˭o"I WUɿusםkֳG[>G,p pA8PHˁ:z^`mjUϕi$U)lml5es8Eбcj]ƞ7ܬ؈LKjY:'C)×"I{n׃/00Nj$}W,l;e} &WnnIj=|kѐs0{R<J2U Gd־/Eͦ􅋺5,G@h$)ܬƏ?2@O/0Z[ոa$[Є!q3 Mo!<&WοAmjG9 I;nDֿx9~i:tU\\x@G?x.+--~B!\RJKK#(###n$Ɉ%I"%\Q<*2VsZufẅ́l8kkrp#/غuNO{E"s=j9Ծ ϼy'tyǻ<ϊ+~z=Zju};m*O" M ]ViImsJmkzq~fgj% _kٲe:p^|./^qƩP7tl٢Yzolٗ:K}{㨾]Y!I:>dj]s,ߪ8TsojtP%Çe.\g:Ns|ǽr>6lؠUV);;k1buQ͚5KjkkS 2R[[{OJţA'%ţ/k5#CyW)628픳,RR<-Iw ;&w}c :ϡϰ 9 s}zw%EO<eeeI&O,ө7j…J]KSS\aWJ>1?g:{$IRdDc:xNht䦘\u0adjr,59A.WM֦k|〯A㧣׸2s,[Lk֬o[y<>}Z5777-Z ;vL/s|>vmZrRSS˗XSLZј"59~ј& iXQIEɮ$C;ֶ$]'U:eLnf̢l5ڶ"1 L׎R,fH y͐&5nڨ쿻]JVAx9/,ͦ;S+W[nо}oICܹsX.zHv]K.I%Ì#G8=AC| $8u&庍'5pR/mZ8sOHTI.SʿybخohR0^_|=s|ۭ~X?po0 l(Ir,eIү}hPu}ˠ/\N\x R[i5|A")X}{vZy,ͦnm6e3BlvRmܽ[M' 0 6HӦw\U(0T\9sձrM'inQ-$5%'$tߙh4fVPIj]ɻpUvU>S$)eni} .vRS N(g&] *LTqRm8UJ޳]NR+x.#9Yo,,fXqBkWfS?2^6lmvKs$oW40"\/`aѨ@kD4TR1t0cC3KJIH|jr50^\NFdye3b*c2ҕe8rǴOohr50^9kFik$UN%ۙe}^$AO0</`q2ud?~G[Yzm/M2BxLs}IR +\ L͛%IT%M0tM!9-M& XHUI|3ffxzəĂ1&Wsy&-+@!EDAw$)Ҽ?쉉J2UߺEF,frE0^"eۥhTS3YhcZ5X/`3+Xs0b|JvGBx 45=G]2]nyI۷ɈDL c'jpG>{ktfhTq5\!z1/ *j`p]gVkͦ#2$ݰsR$3ŗu 2<#L]%MS4fy\mvI0 0P-*eHJ 5$=3HR$f9N!I ) \ jZ־:J~~mSZѸՆ+i_uv.q 2} r9ۿDi/5lOKMv+k}0qrKb1-N^5Lޖe%Idžiy]IZ0-OvVwlv3fJv* paҩ Il{7WfiZlҔ11kv8ߌk?hkS`gJ9%^sߩqW>꒟S~pQ 9$CZn%bS':vLfrl6f\#I *W/Omi&ߴqߋ(P^hnmӮ:P٨X̐VIgx9؄ҪG3#Cߺjڸ݇GIFڛumeiGc3Uw\Kі9<KbLnOwIsvXWG5"?+3. xMPm.:6/3)cV0jκ p/x~WӦMӜ9sHvOVZjw}f%\fGE#]u1l6yM7,2U$IXT95tEZ@&WAx9dz>[nEcƌQQQ~_Jw$^{,XB=cڵk%I~_Wփ>M4I+VPYY|{[}Ǝmstgn쐑yOKMM\.^y"M^6P&XVfBx˖-Ӂ_Z0PcCOa))&Wޔܛf(I$Pa0jɕuIy<ڰaVZYYYGW}ĉ%ʴ)t}6bue_/}[ⅎW$)DN^Ǖ9:1Ya*!%?|>=s /(//Ç+33S6mRQQ$)hΝ;$I'OƍpBIUUU⋮%%>}/v/'wZe=U)Ule.Wh<}U^αl2YFox:qIIIQBBl6.=SW^^x eggo$|>vmZrRSS˗XSLZZr#g_q8JI>F/^GHj_e,h8^u|\ire UjX\,>A㧣׸2s˲l;_rnIҒ%KҢ~XMMM1c}Yz!v-]TpXzG.h49OT)TY)INqEq%}NQpwBM9zs|gXN~>>tڕL}8o7^l635^Go!]Ls?)e yRc$tv l6[us.E[ZL&1>SdD" ,7 ,}$3vii&WxI=Z!6 ="I`c\0I$ۖ3elF[S" YMchҢ J8f\f8K] gz"o*K~qz/U)IjIR irm]'jpqkV$]u]NQpNB!L?@<Ѥ@H4]n/Q«eL iN&ǙWͦ9s4vxj{Ǝ#%E18V8kS7\3jWLECU]ߢ4Rf;mׯS3²= V@vlw:Sd)InKwkmU}]XcZP|\ B9|:="@Er`$7Abs8->ge\?maHӘ2:ZZq߾M>BGFWIE''Kg>+^ N"jٷWS3)o4IRlHϛ]`Ex8 |sʘUp6T\ /3Srkr5诒&ʞ$U\߯Ego|)ŒLFM AlI~\wc@}Riv0Pkm:VאD]75W#%}5ǝ3L<3˄$M${bboۢ . F^"z{uQHL'jʇ_Xu@@kl&W$Ҧw7oױ.:c3v0^>TPemcN+P^&o7)cJ4}-Wd]k}5QcpzA盪CmQOs ͖{x~nnlr,3e)%M,[B$ɿU@"@(&{&(g2[{v;jIR`6/+2S=QJp9$IM3' U~)c,sx|̙ގcMMjٿ/nu@jcKҤQCtYCRt$I,%7B70m>,;aH h愡Wɖ #V%Y.(l$)Tpw};2̝2L 詀ҼϏcSʿu;i^6;& ^|>,Uf4giM%t:ب]+@X!ʕ|55n{V5)R-[[Xm[44@67c Cߧ/koi6Km_bϪc9 `g2"VXe Љ&nhrhXIRA_~iFY/^Osff*ah"ݎU+h߾%%@Ax^miQs.I.Lå(*åeۙ:`"@/ jʘw:$%:uJ<$J":Q۬T6c1gFG3e n!A\JCirĢZ5Vc[9E5)I1%PAn?R'ˡf*+cvg/KXe }))ѩRl0}yZNoLf˞SUbbw6v[?JKKUTTkv9ӟTEEE],Y5PH˖-SIItRmhs)gd=!AS$پ)ՆwTU99ZZZ4a=#ͦyO>wfŊZ~|IZJպ_61e }w5$O8̦]Νo1$ c޼y7oy!˥~^Z?JJYb/^f4*,w;nq'q’ d!l`!pg;ɲϲKY6l(!ҋv{-wiFӟ?Ɩ#)%]*99hfя|_jXr|v`QVfpVar8HBS_AVDDz^dbΝXo~:th4ʊ+M2"kDdf@@ECdv\U:fa$# +\UVa&MDmm-/ϓO>l͆l{{wC,kbD^7qF#^>9dFG6*^ƍ>}:xܹe˖ X16.yxMv1=?z^5pɇegB g%֣hi*SR13PK 3FK;wj瑣6*^QII ;we˖C$} '6KOZ#A<2k;G!cykVu},G즩@f?}w<Öihc|;l m6_zbpg|;O jg+T\f`Μ9XVmƆ +zn7H,X,fӝja6۹kO]tu;VE]wX y֣hj甪i3/o5kyhjL!--o}[TWW3o޼+ʼnFF2#csώ  {!<ֶm=ڌvvTtys)3fg8v2VxØ IDAT{b>(wq?0'N#^Ed;%.]ʱc'vzzh((V܋ ?E% freG4oEKnJ\Y/XED*v&7(*9f0krZ|x.{J5sZ@ߎ*^Dd\Ӛm""W!E KvLV"d^_ȿw7HD""GŋU!c^ t>*^DDw8'c1sFO]; N#"2|T\pK gH_4"`2G/hp"ED=Dcqtzud½d*c"F0t,߻4""CH4Ϟ?N}{ y  PkFD.H<[N.6|v|ёDDz^DD. q޾(Nu\,\RثsVwLޗC~ =O$xfYl',f2\电Lk2x` &KgaIE.r/YJ@,o[dYkt$!jg sG4OH$(=_ԧO"hIE#-?e@jœZuLD'/""睨tb1 ϛ>yHJK<~h j_Tt Ik6iI) \@b!{zH45kaDDV\rX0=s o:snVsy69#UqO4tLD/"")wrژYɽ7U[?F+L1(O8=~H?X_M Vӈ qyEyHnLiv:I;x"kRڞz29tlv?A# x$.\ 1c23qVT}s;D=!"26xyڵ\ƌej3*^DDރmXv5[ds-Xf/""おw <{2Lfm`IM%7ID'vyvB,\#2V/[@ӈ\;/""»=9d^< GIiDLڜ].GLŋ;w26V܋߻x_D""Fŋ;]l2^0"W=Kxew= H18ȵ&""H$WhrVTb28Dv ;l7ݙ٘:𾹝ODZED2Μ! hȘXw-)c ɞ@D{ '"2T\f͆k"ӈ?@%z&H$1©DDK$Q|_\8'y:2y +EdlS""r|>E̺M&nX8 =_Bg 56ODi¾%. ܤ͞cp+"Ttb17-TYKi ûm+w}""WLŋzɺ,dۤ=r]Lu 8fu6w}{nFwbX J("ru,"6];ID"Xepn%}{t{0]S{H,,*J3("Tw؋'(+38J[&Qҫ<"~~]5*bmu?4a_DPc#}55@ d282Y9vKo7R""۶$Xʘ[V`(k;9H4NO lD,EŋnO26wt GI)Rpn!;=ňX""ÇtL7Dx"Ax@fܸU_ Dd҄}vkz#xerM* H4 ;8PA"`^A!f c37⭞̲Lr2FGyW""ѳ8ұDD6δ\"مFE\.ՐQuK /g8~kr*^DdB]I<#NV ™(02Ȑ,L4u]^Ik?aflW]u#YDx i\gjk%fC7.2ܶ).sH:F񾹍6~կ~K2{low^?\^b>/8Ld&%TWGc/Wv/_rQUU޽{8th+V3ecOD} Ka'CV)"2ʨxmm]srrގfr9"bpk+GYZEγ^7 N$"2V56bїt}ksL&֮jhkjq?#?`zˢE -_0o(@1DEDƝD"ѿgE%}(okeӾ'[KaNM LTȘ9UC$gZJQBuXw0?}(kdB_0"c ct> 75b/,2:L@*^Ddj ċ' [ܽv)/<"}uw,gvyn2w2qEƅ/qﶭFEdZTae6 ;M)prR#byvm!2(Ru3<*(%3,FGqNŋi'g1{r/Mx-bg.7߂xGpefj5oni18W*^DdK$V|> I& n,/GD)/"2c˯4M?ZTE 1xS<yl<׭6"mm'HŋzOZrB .Vq9hy˭`6C,Fǟ6:C*^Dd3Lܰp= 4px>Lbۘha,ֻu &<&"CK,EdL39 g^@̑ʒOIYYѱDߛIDD4k;HaV*/(0;"2F钥 =oRVgp"[v6nfjN6 RD*/"2%b1[x֮38[?`mHqTQDdS""^ϖ74sm6Ȼ\dt @rZw݀DdP""Z<?()tDȼ&in>$èX"2ƩxQt1iu11pd)k?lbJJDdԊ|t=,ΊJR58\ Ϫ5rh×"SXEDFg! s1L'+aZɾ䍮bHD<"" Ex~9vaYT:y\ 2"r5܋sp/Y2:QyC=i$O x@6pG?%7&a2Gt<{XED 7WRssydox"2DR+gZ^%T_</"bh2)(n hsr4g}r& ZsUQ""tIplF~^ j Lj]1M-<~ ]'HUOee6n8{+WRUU}GmmAiEF/k 0q|U;0UoY"cYM`׿" HD}JӧOg֭?!O<<q:|3!XdtLoGGD`2qt:84ree'kevvy~'KK%_%BvvDg| _`ݺu|;aŊ˃zhD$);vs#>fenf"2:s6GܟXٿ{QUeժU_ۿ zY|y.*kT\Q-FwbZara qd2L"_h򾈼o*^BUU>(?Oxᇩ{!@1dggnD\Q%N͹CXnprqÍ&acWa?Ϙ1*z{9.DjEEpоj^nm ntV$I鑡vwI`n" sgcu]pay~9N7qY9tS?z=P2n7ձtRttt0k֬+ztS;˵s"'cX__"-[.J鑡vViyUly6^k<N'K 9'&3|dt#S+z= m-FU'֭[Gaa!|fqͷ^~PVVFqq1=_~O,'pS;X,΋;Ɓ!{~ qma5=2\+7b9KG9I?Lo@g+T\onXhO>$Az!^/-?1v"l=؜H&l Ofl@DoЙcgڶw%`JQ:&kȘD*?{@+Oًa˧m'(:@M*a Nu(\v )1Ct KɚY_6$KwL.plVEdP""â[ YZ8Լ/CϢuU37PQ_CGFMc4%yN+"l! xSq"?#e'^MeSyUDFXqr?\&Bɍk̙ED/"2 N7 :^zf7(}Vp׽7Q/$"ɼ)d]R6%}u/H@yEdӰ1rlĕka8J˘4I_dr-ܷq&on 'g'籼e,N;='M̙Gbl;Ҝ6Χ@WL*^DdM-N'#Nach^Ĝcr8(UL`.JoGqtxzXoP IDAT/ &?_dWOqؓ=s 4lLDl 3V ,=W_F=^P`l@Uv.*] @):G H.PȅCxaqrφ|"LaQ˖:hsaq;R'y;Gŋ psM3S'hJ!_ >95 ~SbX̓7Ed|R"""Kÿ=F"dR18%"Pq'ZaguwEM L'h^1&ȐK4?470/?M)DqhuOϡOVz S'yY-/ZL|Nb[LΔt̗Iŋ >EdYKƛ58vNr3f!Buǻe3)T[o@B1f=mǴΓL3(0tUTDlŹo?B'xFG9/"rM8?;071dBF&"&ȵR/cv:!vcS""n!>%#jr򺻰k :B +0}4|1b*2xk`Ӥ_"ޜq9'5o>9w @{xDL*^DLu@r1}d"2^er+k8ND,fl(*^D%QzK"(E,dQeDd2L}S, w-8Dd"24VDX<=|%Q;ɻR""Cd6Sp_ؿɌɹL&RS*\DdH$vc6ظn|O1u,K­'kzz.GvR+N9F>x;yYiF{4&4WM[Bi=ۉ4qۏP/:"r""جf>uS%~ 9c/`E1l3GJx^;E/@8zW4`Y˴ L&~?=[6\D x'*KLDշ ?Zd"sM}˘)Ex9"2zxz8'S e;_e.wywS3 HK3u&]/@~0"2zi¾8 E9XӁ7frQ:SWM4? ՞rQ_m`Z+y~9u[Y\ΉDc<ܤ2bub%,yk4Jۓa>ilYY"5"ޫ c#YD.Cŋ8 %`Xwizh\G9:i02ȰmE9NvZ[a(? l+=Eulh]p>qͣJ6&2姚޳} '{-)._A}A%LmiLkn|w'1LLfM3D{Y!y[1DDœ#8/UǾS$ fg1ヷBj)OmfRU5h1W\|+O`S@l:̓w '2ީxGm[|]7h ΠiT0+z耉NfꪠLBezEuq YS;y@Ӏ۶hW_g `2ϧS_CKg/YgIV v 8^“6/JdLLURy 6zy^x>m>BtLM%Kd(_At7O${nAix KFOT"x0_WGKQD})! db 8-]A2]!&iq))ZSD佤rN6Τ=@04տGL$#hO#kmdr+'ߞ$B}}>r|X>;%47Mk-3'ğ'2nx1'Վb"EzI h3\; ,NtL~{b煣q>z]}(S=d/QDDNQ|/gױV"8yN>p]9fT:OKzU\'BcaJ;NSq^Lnf>Xs&)"xF?O~B{{;|_g޼yF"ڎ&Jy5a/{~wj%L%{fݟf{cM@+B,""#aױVj/+wf`qͯ'Ki,u+EZy_K>M\n.yqggY1oT g}G}Gyy~,U y}_u-~2V+$ۓ}xP୭8[Gfo'En"Tz|玤cQɛt<_%[S'gEDdGLo(ʩf__Tm4gҜY @vwՑ@J)llN`9$SxddM/3tXU>;F$gˁFa2yS=DFaӟ|#q|`ӦM7sƟ^vmaJQ:*qn|7gY=fˁ&Z;dtV>.y"hǟ;F?9% fZw_7!(|>:f2şU򛖒2u*֬l‘8OrIITMnVB,]p'""Ww/٨2ۓ}g`>s҈'`9 ׃< EVO m~8X =>I[OHr^ȩNu8p> Y0ʏphc}2v]A;H ∆wAgj6-LNˡ'5BZ Ku-To4c@32Yy5M&""Cjj6uW13T\3s;.n>ɄߙASV*'2fAZw+;.2z;"sF8#AuP75rRȷgs99NB¶jN5rӪdqS(N;S fgQSEbTfb2/dC>nYVޓmxaSQI_(St(w3()lpsz趻i5Idsz賥AC.WxܶıWfeqY5 yvi!1)'˰Y߻afY&;82Y,6:Z囓 ^\]غOqq/< A-it%D,64eP&f+Jl!np`} } qCL-ɤ;DW0[Z{ _odm,͖ { :}7!+6d3 R賥kK%`qIugK%p᳹趤bI8?%Op:.2gӗ4} Ot:ܵv*h2""2:_̪yDS^ؼp4j%جf^~H BhO>d"H#HjQ&3&sԐ>pg8Oz<3 ]v [,ٹmq0 .[Dd&nJodJf)3T&6/ 33B{{@,+:ӝ_DMb+1Jb'9D-6VAlM6̩vB6' TN瀫>.pxei&ky~GxrI̶&庸cT -]A6{NvcŅױ^Om=2#c,3NO"\=#>2+N^]?S0+i-v'aLFI!'h{7@==ڇ-  Ēc,(h["%d!a|=Rq2DZZ LĠ2>3o<Yv-' N'""""2e|ӟ͜9s;w.?8P;h"""""caqFWژ5k?яNjUҰ14sHDDDDD/"""""2&x1Aŋ *^DDDDDdLP""""""cQ"}uQUUō7ȿk\صk<V_~y9=+WZmh~|UVxlz? z!*++yG0~<,Z; 8L?6#5hӢQ^hEE%(DLAdFN+BDF "MRRFNDRٜE$?ۦ:~~\<|8̇?-X@65W9}Vn[;vΝSNٔ\5n8M2E.KO<:va5(/]Qiin._kѣ*..;ZZZ4rHnIhÇD6mӧգGeddv5֏"'˥szVXaW\c?zk}S-ZHÆ SII.^(˥p;+؜l٢rJOOW^^_nG\cy^-YD:~Z[[k؅#جهB|rkÆ V\iؔ(4[׮]k;XSN?vd3ƺtC—s*+>>ުKB`%&&Z555̙3B҅o9;;ZnMBӷ朒b>ze /g-_;/]u]J=z %&&,թQ'On;իƎJ&9(!%(''G:tqBR ͛7 M2E .G{O2xBe޽{ԩSf&IRDD$v嬿w 9O/^z͞=[NS?~Ԛ5kbwKIRdddюH ޽Ӯ]={'9rDaaaZdQBWss9l֭[RQQ&NhwĐkjƌr:r8ڼy&L`w4cmݺUǏװa$ oK_Gy"ty޽[ÇÇuVEGG+55x9e[7ޘ>|իWK6nhoS]]b={ݹeY6% M@@e˖ITYYSNQ^:U]]-ǣ*77W%UMMN<Zva5P^#˥9sHϟСC?(**JרQ>|l544L9sfǏct_~r:_=I2d***lJzU\\͘1C4b|>;v6mڤ[nD h;gv3te{κu?lРATyy$}iΪ*-Zt ӧOUTT})䤦jڴim[ j޼y6& -FǏ;UllMBeY|nue)//Oeee*..>ev`؇Ey"UPP :T>O'NЂ fvU3|>EDD(&&F˖-SAA{jJNN1y~4稨(ZJ>OGmPXX]~KNS}Mgv\Y)99}F'$F0(/@y` #P^F0(/@y` #P^F0(/@y` #P^F0(/@y` #P^F?s*FwIENDB`symfit-0.5.4/docs/_static/global_fitting.png000066400000000000000000001274271412237106600211220ustar00rootroot00000000000000PNG  IHDR &^sBIT|d pHYsaa?i IDATxyՁSU];֬ "h"hPGGL8f|d91.fr^F'qh-*(4{u-EWhρڞ_UO\x~`1;A7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C7~C+R'N矯[nE\.uGQaa򗿜g=P?Z뮓$}*,,Ԇ $IUUU;w***|x@KN'|ROS]x'/Taa&O,;vPo$K/˿ů }]-X@ÇםwީCS~y^Z/0}gswk̙'<%I;v.]IUV?O}bF7oyw}M>@СC}[zܬ-[hʔ)6n(I>}ﱙ3g>i3 HP@ 8p@ ,%\wX6]ZfMٴiM6l( p>c+%%唯ytWoѣG_JO> ~^~ZZx̙g}V?z7u]wx۷o@{ \.Ng0 ͜9S͓$-ZHw}|A3FcƌҥK5z^Vuwhɒ%߯JtmJ=.\>@˗/2ٳgOpt^׫k׮]+ǣbM>]wV}}$x655颋.s]s@c fhȐ!'<, Ðj=u-RTT^|EHkz}K/hkk++**Jկ~Jt竭M?, 6 .… <m۶ڔIƌAIbcce7.=o~R!Ch֬YZj K/StIĉuWuƍS^^^eX7nTSS.>sɻ DMMMJJJիUTT8M6Mahպa>K$iٲeZlI󔔔hرz P@ x'1k,s='2lذ3Z P@ $''+::ZGxvvJJJ$y,ZH;gWZZ<|}u˾ZZZ$y#СCѣ~|ddϽ%544hܸq''55շ?zW_}?h˖-0a׿O41B6mR~~*++,>}ovcӦMNޢgK/ʕ+n񛚿.11Q555'<^[[{ouu$k$y/++ukɒ%%Qz3*͓:7qB :T:::k۶mRbbbeeӧO׳>/BUUU}n Mp.9NTOO wuuL+0\./_ &(""ʕ+w劎ք c9͝;W>$ǩ-'[eX=uSL֭[mݺ>W.{+8aM;wj''*66VRRRN #G'Ђ t7ꦛnȑ#t:ejhhНwys=OukܹlzUQQ|k?CeffntRw}ոqdOG?|Mߦi)8_Ƌ/XÆ Sbbl٢u o]uwjx|^}UՂ d/9(0 -\P˗/ן'eddh…zꩧh"͛7OpB͚5K7nw.NT 5m4=SZv??vءsGUzz ,zF?q''|gzÆ ӈ#T__¹_k۶m']buEiʕ\~j*}'}.!!Aׯ?en%\,D8x7M:U{L{UffNߝP$?#<#GH?pپ}p:9s:g̘UVpr ~~ V[[Ǻ [o{?ѣG5|tM*))QJJϟoABOE/ʕ+D}~ꫯĤؿIO 裏]n:;Kk;$I>OfPsWYyEDDhĉzUPP{ԩS5gN$6lؠbw}}63Θ1pGm|1ch֭8rno| @pĭzsrr{}}/_{WwVFFF܅{@K?O\WW r8fD3 ޮ[eeezW)I HMUZbғ$Il(S[gɩC 2Q^n[Wjr*P@aMN$vT.7 Q@Pnz$)/#Nԁ3#b튏+-1JV!IvT!2X٬%z7o_or*Q@TWђv6u V (e~Y6 HˈStM F b鱲Z e&{gA5ir*( A,fՠ8ex8]:hr*( pk};ФIt5>?\^{&NPyy988+NqGx Q@N[?ϴ~ ǣ+##C%%%u=RtQ͟?_7tJJJӜ Q@9&0Pؿ0{n:롇RAAΝ'Dk;4l0=裪3rY} -Vgr6lPqq^{>o۶MƍSTTɓ'k˖-L{.**Jcƌ֭[i޼8e&Dq7,(ѭzkkk籔UWW|ZZMc6e)5!Ju]ںN7\\pN 3 )1.!IϥaU_/f@!**JMMM}s8yBp8JHHuX NC8ԠYs?K/tϘ1C3f8ױN)7=NY1*iӎC x<1bM$u`eɉgf{I( !(;-V޻3< 5 UTܥ6^ua}2, HˈSBL$iFp_r H2 CyaU7vD$M-̐$9]m[cr '+}G09$Eج3$YTС2ad7iD$˩]r桀 6KRyM*;LLpG qIq/InTi˰` H(&IjhѺ6w{ay$4vYg+).{(aiuɩ( a0 M]UةU7G ˰zn5vp%˰01fplVpW7tCHU{OEi.:Tbf$! H]ѣU29 $*zYM.JLLpC #i(IWެ:-y70(<6UuCP@LFRa,!ˣ{X04zHnIR[S/WSkɩ( ar4}\$S/+ P@”a #IUVm;jr*: HY-wXEۭ>8( a.7#^WLɗbjjc? 4et&O$59̛_rB: yI=^y8gfǫ0?IђϾ_719B ٬:odR#%I}O;՛ I=ª8E,<*]16y<ҳvx J$EGt:%49B> 1v'yx<7k$C.6Q@ǰNgKWMfF@hFGHVWT䱓ҿ]cf4 0 C޽ jy#nS#mf<9 N/-`L$G7 AD,ό$kWAvbl͌ GIޒ񨼦MfH6U+GhbT\t2Sb$I[50]piNGP@pJÎ͂t;\G().IeX N)=)Z1.ڶ0 a m,IrU( FyqDգhI,P@lVoK㇥JvjTkhB|A yUI q{<ڸeX Uͪ!YG[٩~eX N˰A Z I򝒾I -]f@Dm$y$]cb* NDY C16e;%a?( 8m1Q6eIғ$IUn032"/Ia(;5FwK 8mK\t*nSZw]x<&'@0F&JJ*;T^ff$ -1.RY)1,@Q@0 #d*#)Z~W ˰( (%F+'ݻ K*ZLN@GKTVJ/v ߌ2ҏ~#wyK /+//wܡI&kg}fbs+=)ZiIL9 kO\nɩ( 0|hٲeZp~飏>󕑑]{TYYivs0 MRNp֎)k29tA͛7OtkڵZnC@sĉURRbvs&;5Frd[eX85 H?%&&*//O%%%r::x6oެ1ch۶m7n|>[Nn =/8s6>H z'd5vXUWW.B55݄p8,:Vky}Gpz\.EGF+s07/wxa |( TYY!C=z}Yeffj߾}}^_WW~]#!!d/}Ghz̎u10wxa  H? ^zωG$D[ԩK ۄ *04kR$͡LN@BYw|e${7gmS] &'@ଳY-0$ۣ+My'dKj;ULN@@9swF(){TYnr"s&2¦[. &:0C㇦Jj e&'( 8,93 k$|zP &Y( 8r4cb CrK{9K~7*?YI[UѬ ,P@w1Q6 NDR7WTe}pQ@`IGX5iD,ǖb=,q"nհD%P \4saD%GJ=.\46F&b(IjnshfABH2%IZT]p.P@`*բdfAZ;z#&'@2cWZbҏ͂E:5b74EapBDA@LQVJRpC:{LN1 UdIRgK/39& F\txL$}Z:&'BA@(M޽ =.T8[( (6MQb\Sc$IlPS[p6P@prcQtk̂ a_ĸHJ>V.LQ@̞x^TQ 燚Y**I:\*ӥҪV6u Q@{NO=^uk+##C%%%u=X{$8I &<$8*ގw:9]OZt~a?^я~۷kڵ*//C=͝;W'NTIIYv,HY\ I𕎶U54vh$8jўF/ H?mڴIqqq:}͝;Wom67NQQQ&O-[kgx7V.M-& ut:\o=X駟딗+''G˖-UW]|;Zx<jkk)))>+NJgϘ(I^/_{0QyI]Z I潵X2'?.=:x9N{{JKKtR=K/Odn8;uX ҢKj:4*e$] ]MgC$Im=rxKGé(n~f3;gw_=s***ҍ7ިkFqqqg:6Mmmmzꩧ-I:z^yM>]MM}$tXUSI+:*3>_ ㇥jxni;tUT^ݦ׶`P)z/wxa |Bo7߬Rz裏.M7ݤ3Nzz"##}C *effj߾ŭSFFF}koq񑊲E٭G/un(+-Nl)J4-77B^x'd  QFLeeeڼyVX=*,,g[֐!C$ITnnduww+2M4eʔ~]SoUl;J-:XSܸAtH{k}_W+ȌՐxlֿ:I5 r=ha]2iFj(!![/wxaKxcBlڴIo}uww;}Y] .OS}FAAfΜx@>jkk__5uTeggk7oVX;v5\.S,52UͪkoPαRrD==.1-r+[ ͗޲F5tW`oo;0NO,b@;w}ݧիW멧-W_3֓O>xvmdXxbjΜ9zwh"eee5=q+92QFo12/IsfHjŮ*e${Y5f 'Dn#hΜ95j)_"ө?&o ep9ok_-$q?Ԥ<.R[Inr:q)nӬI9[f(99[/wxaKxcBf7IJHHq:,EBW"[13VP0 Cw^;Zi;Hé-k5/92$ܤ[VvwWY tFEhߏjkWαRUߡU&'] *p=黼ޟYwFH:aO#$I;֫ ( An6GYˉڝjj43VPd ͔$m[.K!ۣM{krltIMӥ9ma+ 5RRn:L8( A,'λ##Ż|HA'n7SdU.Go~HM*Uͭy* HE+5:Ea(!2AbPZjZZ#ilNB $eK#ˈZ}.YHvnTs[cǭy Hˊ͔Ű*pvr˥#4$+^ѦrEؼyT5t3 HؔxcH806ablx7+Odס6u Q@B@N\bm1޻adҒuc$I=ںNV!ǣ{k49!@pTEED)NXgf4;p$7wI.mS g,Eb{',61T'̯VVck73@Pl%و`rfXO׏ rkQiUW)` ( !"A&2aĸH=}Z-ojh29!@𡀄04*yo#C&' ٩m5({WڲNN[Ԩtl߉&$Da9v}ZmS#6BmQʊɐ$UuݚΚ͓4 UTԥwTh]vf 颀Iw&Dڭɜ*kg_V*i39@pqi}Q$fkGʩyN|YUێtbb3a$j)29Qnt;k$ҪmG|m=| $X r%I]M39Q٣eRӭ[*UHSaC$Iݭ:z0! ɍdr=pCXyXr%INS[p(Me6fG<*،/c莫G+>{;7ՏQBD Y۩g*ڪ؏&H͗PB|^+n $DE=jn21Qx1 CUb]bK^|/%5 H:~#zkO$,' U+)[BVm;?[n7%' H2 7 $UUv+ج(KQr|$/69Q@BX>y<{TYgriӌ9xBR%dݮj~N9]^( !@nC.w棢a!!Ʈgkڸ,%zƭUxi#% H;Dt$NäD--)ZS 35utғ%d헕olW$E(-ItvJ<W+fi°4MdlW.Gs@裀00|@k IhnXfa9I:&S9qlUs[- H]UZAYf9͌ ЄaIEE4(-VYQ*[LNpP@B\ޱ.W^ 3c=ahL&EkJaF&J[˛f@h ۈ^Q̘tIRYfłg7vvp$jPo Y4<'Au<ȡ Q@H%X?{w'YW>=>f%ْ x%aN Orr 9'qȽa1@ !0%Y];L{wU?Z{Y~>m=SoSFOoʶG5hLZ:}[\(7[ )?S5M-BQ߽sVs BQR$)c[67 YmEUT0 e,ɽ7, =@$D!%B9>g>`q+E,ݛ;_Jit0&e\J1mj0'C9@% B!DI2O=/'?Ijkk~=Ã>pi/췥a#Z)]f'kki6 IDATҦ m)α085BBQtR8_"W|mΝ~E}u%kLBb88h?y=_XR+g %~xLV+rBQ$/| {tttLvVZn|mƍ۷Ekސb89-@|3;Y +`h MJ:!B$ cOa(PSS3eJFGG终vD.ɮ}IJq#}t߹=6ڶ`V[Mst`PB!Dd2 =fCQmT :eJ6{e=m~kٻQPh^hb`&fm״Pw+T* ΫGFB!(7bJ}UVm60lbوD"SfSdMT-&;d,;w3=KݿŻ*匷٬ۗ'g9pz[76(ơ <۫PߐLKo>-JĻHˋy$~zrO?4윲83 qi ;Gwoݑ>k,pˢ빾mNKiJvM.^:8®#y1/ 2La!izf̈|JC]^$BLb23244f2 /}K|SbppO~l߾ϦMxh K'~BWw8ju\߼E)ě3T^Ǽ;No&(t|[ڹ}s \rYR3ޢ$E]^[̞$ sOEQ>},YO|pbP&ϱ|3Εܪg1 fwoihjЈ แM8U"TU[ rkx*(x7`5D5@݂e}ixxwy9o1{RJ JN+dV774]KV~^E_l`Ļ:(RiK_ŒYZ=6.cwd<K[,k$'E]^$EZ@NZ@JP>A1)*h2@Vb3[vTm-^(&yz|V<%8b=1YLl^^GPH$Cn\HSP3IO;PUIBJwyxi;GLH iZnr[-}hE)8|ZuTp'ė؏ŬrF*k׭P/|w/h.4/G^?xB!UB1cU? ¬^~b9l@wd`^'lRȝ+eC3l/XMjYwpׄt/B!ĜH"fe1b0 O}Sz ߶-`,oxl]]ZXs0ˇIeE,B!dYqZ,HyYV]#J({"K!fxw/]CQ}n^DMEaѥ`4 %Yd!B\$^0XhUnX #=]<1 mj߹ ED:dz{tVճK&({C\ !b$fV,' zSY59ﮑ=^>1;7i]TH4^?=ŬeE-6K;j.B!9iv7Pap*EVNnY ޱ4Jqi-uֺu^>4ᇇ1@ }LDE,B!a 2RMl ZgvƮFwCazcy[X OΗ۩g"Æ5E. EQXeacα:7lhuYLE.B!J+ JK_lI4PEQetEzOZl&yjW3YeA5UQ &kٜN k(h(I">MK\ys#./"- s'c@%0>-xjbʴgg =cR>qi{|w,cQ݀ӻ9N^hB!('KfovcS6iq7(f5qFχ~g3Vy0}2@]!KƬ'LD&mn@oXQ'.Z7seM nD׎!kB!$V {aZޓij [ܤvÚFom]{j ΣCD79B!fKJQVU@UT4]c_ z&[Gv;gtquiol06d, xa ²BQ$ad#q" B7`:pw'GƟ~`7-Itp@#A^94L"-S !FqY,R;Xr5+lt*o_}/Ť*58f<⹽05D!(BQTzf͏G00XWcj(u9ҙl9z 29#,/@8VĒ !rD\v&ը а k\:Q pV}0Trc,oc1Apg 2 u!*" UKPGa]#{Y,Qd+T[:( yfjyvcQW!JH"M:g 5Jb 'FY,QdoYƥsMjne=N{a֬P_dxV霌+B!J$ b(š6\(( F/wXF5;Ϝ`4[74UQ4=A (>m?;N<%-)B!D1I"de]*lf5*vE7"LnYIUd5:fܴJo!A&x`'2w$ƗO"^<0g e\BQyWb&wLpWK%oܴ8sre5 bV{8"HfšB›HקO~g=#yfB!x#C=P !JsWblUv?ljXJUkY{ڛ|t \㦱j=6Z4h6I?jV(@ +|( +͛YZApX2G8CDY:|Xͦ$E]^$lIRʡSjgǃdԼR`KQV.dY#A6`64ThtHI5$LT]+vE=>7mi79! x0nZ7\&./"./̝$ %\*0Qt 4CcEbklmu!uU=:51[ʹԺpۈijd:4?xd&ٺaTUǶU d:Os'D[vɿĻHˋĻH2w2Dյq[ ]l?6XR][pz0ʏ^>~P:vGF [\ej~߳O UN:#շw/NL_|B!fGZ@JP9=AQD.IWd>dQLRr5'fa#iN DhoRww@$ͣObw]Gu }qL_WSuح&:#hAp ST$ݲ$E]^$EZ@NTn^  QM_]^r8EQXGuwne=vy~_||/Xݶ;.$Ո%shh(DE㴜P 'M34$s"xem~l RxwyxI@NTn"BG's5=nm`p19lfj<:J67纕h/;dtV#O0X8;sXiMdi:` h(je7OID&մ7y9&qdǣ\ĻHˋĻH2wrj5T9x'y]#O^p7vZg=K.Xӧ +yPajNH]7.1EmcAL4%铉E a7MMDζY⤳;IdYZ4%E]^$EIv?9=a4X^X2D:ȞdSzL]r ̸v|r3:zfE lVD3I㲙,~h1ذZ7{Cdr:=#1vM+.+"./̝$ %H*G\D.n謨Zʽav0 23|6mޖby5s. ào,][p׶nSJ fdM3Heřq8"PbI<YR"!"./" )așRbB^bMxud/tXk</+lV]Yu㜕*t EͽxaNG*'D0 ^=:c??I2S PwM^mN`(I&MT㦶[PĕE"./g-fO$TY-ǎijqԬޱ|cܿlPbΘ\JS.5s0B qq;,t4hucR -Xo<ȝٺaE%D"./̝t*A҄;I5Qf81F^3k[$rIFnh.Mɤ*T,lb% cDy`8aqZp;,\068?5ASj]-./"]NZ@Ju̓WL+\,a0N9!pf( foq Żk(O#հZT}X\qY./" Mo{!JDφՠ({sCӵ9,r JQNnuM4ոQ0va4d&u5z\Ŭw$vBQ\+NH膁\EY4Q&rf6x1k4d~gٴ.fY߿w&U!Dr~!h$WVO3W( idHoK'ʅaamG5mjei0t"pgxM߽k OCW/!j!` Ecz|6/^dA_lX.5u]7%\=& xV29T&aDYzFbSmf,&nq9N:qskbHˋ̂5w mqQ" e eŒ%^)v.J.XWUQ𹬴{rbT!Ig5F)N4v3g{[MH9d:+kr'wyxI@N$v]r):# %F\U)^r٭f\,`Ie(3y&"icc,fc??Qb !s" ԻX[ 9=O^ϳ{d/"N[t4y}7kbQwrlNcE*oCB!fMqUfrzì c8D7"PSƚjn¦uU:1Ml^^^X䱟;{M{u!VmfyO{ddN&XWY qA&UEST&@ ӻtgvJXLV8hqS_zB4JVD>ɡ#lk܌ Nc a3/ q;91ӯ(8yMZBI@Ub|m<fµ YZEQ<G;v-ri;.`jzGh^jZ v|c 'f!3!ld$]౺ymt?-BO`:LN%L xKLX^.eEaJv%iobRIe5aĒYg4t {Gb|{~ڛ|Tzs*kHˋL;w2 [loafբ*n kYn- t$NxfW?/dh2; p|/phb~M!W!i!0G>bGᦛn&cccl۶6~aV+g{x< yr(`|zg-gUQszH p&hr/\(Ow׎Ŭr'D$%X^(8f*,jR[jQtBWT&O {8` cc<˓5t IDATIAUUrήc=6_].|UJX ^"./2wPWW<hjjJo|rqRSSƍyW l޼yڟ!إU}$)ƒ\ߴe*n&w\d.IV2¤.ۊr*/+M>c DSjHɢFM5.63nj aʝ[uUB:;1 n 󣗻SGAj\K{;9ˋĻH2wkjkk7Ieeka8p+Wnƍٷo_1*PƏlcSzVV/CULαK: 1};S.\ © qZYRkuc'8&G BI[Q̺#?~o8>?3kMDn>}l?]2a2Zd>@|jk\ҧĬ\x,&{~x\^`m.U)菏r5.>z *=vY M7ZL4V9 Sdr:}qvN|R7i>1ƿ?{y}a<-+3ds:XrrqR"wyxi;I@_"?<_Wؽ{7r뭷Nn?qv1LJˁ#hNw_E7tZ͘U6fw#:L]K3i4;/I""r]s {CY29Uλo$gZ>V,[GC*M>+]8mfLJA(!Id95A"#JV, 0{(뭎ƍw,gӲ:]QOhH`,4T9ir]S柜E]^${ltywсf#O'N5 L)fK*y;v濻6oڂL"bFeu2=?N4#Ogl?5jVT-k۠ܳqx=/d#A~ +ٰf> ݴ#w8gZ@6qz{'„Y^>8UR9NE0T|v}j*TxlMH4!)۪}vn\kpSOh#O͏^"?8[x-/8|HˋyC'|'>(/2o6WU<7bUǩn?#|]W͵-P+ rh8|XTʚ-'\5Jj\,/?O8wj >e'dܲޱ|-znOഛiCsM*~N*8mftwr}'Oj1mMnneբitu3C(V8/;Z*]C}!JY׾׿uynw'?IoߎVgӦM<>~4Ba8Of~c;Y^dy=p=hn$r),&3lh?#kkW^M&!.#Aݽfo#N&QxY]}<Ɵ|%Tyi a`DpPHLhqÚFXh"ן< P+;VyEݬۥ"wyx' >}||UUU},YO|D_̹Y T㢩Edžl|=TERSQͬE@?ydKVK߳(./" IR+t>ͯ_}/Ѳlw[\,mӂl76@L0U˄2ko{,#ޡX]S9jk/~8_ɛ~ZT6?444xv9mf*v^;>MdžS9c ]>zr6.v./" IR+ l_`|Fw= y:C]ߌedvB~o,@`b{, f!̮>*=6V.5yM'eF Sfsy5T*Vn>  ufg> g:./" IRre,=y4]c0j>T;*Uf$cTP$<%,D:=6Vpz0O#{lg~wyxI@N$ؕ/#Kq4xP/fyb`ALN#t$g]n5O&%}1U'P'_7/kHˋĻH2w 3t +߶f޵6iNӜx$Y-]Sa]ί .At >", Lz;(Ws 𺬸Y>"./̝$ %H*a!~/@ XWVw 5sNlcS +ҋnz5m#fNnP./0'sLbIdu0ydHa&iucR<.+>ˆe^{%E]^$;I@JT`WCc|d¬A,heucuX$ǃavʧf>=\S~^AnMfij|'rp>݂iqX %ynb)v3͌a]n5ᴙq ۜ63NhkKO" IR&d"Y`<51e TEe啋Y^6o3urYodZNjG%V ޽Z JT|趥5Hrή^jU8vTQsY A) >6?T\^#M&DYbyAHA^A[j]JN429T&O^%w,Ƶ{%wyd$)ARY1#XU w4;tmʘjYKGdKG$+>]||T9*g\S)C3zQM6o 75od-1W A)$|0+7޷vw?0Hg5b,dX2K(CZ*&Ym]U^>8tΘUQ{Tzg~ I~ |ْ"0գ,lwyY>k.<,FME&9~J@ ~{]+$ %H*0 V@j8llr[,ggmP<kTsNa=@O_m46= R!BÕ +EU 7өLo>u'f>~*VIӏMBuB6xI$9<X<[ 8l3 ?viDeǑQt k #PT]+io׋Oaܴ%=!7NM"`mG5WV9My~OMs8-3Iă옼`u  vٿG)+x ;XoomqfFrz0VX͆%kLxd$zṷ״p)]^O%=HNzM@͇dF*Ig5lNcǑ.+U^׮45Mtգi0cM{db_Ů4 qo-Ga0I5{8JP@uɌR>uhOT`LgV5jaM'.{k۫x<@n`m?4*FN~l2Fm`,=wp:}S;sCܶ|'&IRJ^)_ #IJqlT>}T3 ى?;ڇd㯷~Ϝ/oq],ÉQy6.NX~{7DsVZucq~wGF^<[ys,NGNO1`{Ïi6|MLJ>} ja%u1y0N%˓dr>m]VodKr܍C KsyB$IBp! L5U\eɲխ^Jkɖl*+[ і#Ϯ|gNwqT4MGu<㓣q#sCusTs'dl64TY)ihLNGo֓i}^UP`Ο˴h}ŕM=y6~fn8Q^]yl9eNE'gbVKZk5Zjl˞>f3Z9 6[ovҢ iQuSTڿ[y$ AC8CTK ivlAպQokdCF15&?t\炳q3nsȖPg'fޏ_7E4?vj:vɔ:.Ucj^JAic b~NӲ0N\˒ tdg{(婋8SN{NȢi\?/ӱuUX_VB!&ܟL&'A IDAT$UUMsױPp}[=UԱQ B} !BV.עIΆXt0( &c{010 MFrܥs{AӼV61(׷9b# Pў{ov=!3oEuׯ:rQb,nj I>jsZyn ګ? iMg+}CM5i2G_ CtPsC1,DdlC{;+(F_OP1Z1t4nE:PW^Kge0L85F0~-'u?|y948QPeId/r9B\J+;_ +|4wMǀoW5[+w>F_Au߇JӶgQunȸSa~\;nTyj]ƧE0/;.l叛Lٟ-lbwrX +.=v*j[1%"뉂µ.gny vڧҼ6bRO 9ičp ? m* RNgw7TUc}~%C&Ei"AQ0b.9q]Yp'>&eTwKp?n( Cm ζCżetOW!TTM嵽ﰵj'q1lҝ|wVRL0kF]l*X}$d0:\v g pʣB}S͋y:c4Zy&Ο^Au^|#Eetp0_YNy~!Z@g[rp"9r{ mLF\.݅KSqi.T}T ̌ Wx9R̫P2ɎcjM躆h (ȗfj7sTt]+L% KCUDUu\j,WKUå/ [W{&3&%ok8R?α6I!#.R SH3N96k<բ2BIX1t &/;%[mz1GxngPC>۷teHS ۮW<+.aa\Y*)g@Al^gGuf3hMpjڽ+V zp IJEQe60)F#&41*F|fChmvx Fb6_@A(y^>ZX3OAaTh* Ek tZx~khG$N7=yqӸ1Fiu Ia"{g7־U̎kO*wH IaY H4wj6Unkv_ ‹~W\2#M?smo^ I IqKu?t]HyK8bTK3SOeny\vIsNA>RK.^8 ]r4v%.MM#?CNBdKa_DAG \[8tDU9?*-QOQw)UݿQu F\˜d6:>:IKtn?n3:gj7.URt t&GMd\D;kYUoff(']:qh.Nٯ% _? 7(0b0;<;ݷ;nb@i8c6Jv{SY2hޯI1j|V*yA10-:E)򏤸o[EYKV%eQ|̧(-{:~?)2.{}ϻl܊A1 ؿoU[5n~$M3yةܔy]P9-_G{b0-|߭nw9xkekw;xVWvZ>!d$'ѝF{3-TVQZ~yzg*/[\OjH?7;xyqܻ8وCur_| mfS8ZYKl~Q}M躎KUըk/x35qy#IF|ih`M{4h3X]t<5:a!x j{٫~01 ;p}EIKtn`Zbdzrr2VU;VCCG29jQO"RZKy{:Q~1fN<:\@\~Ԝtĩ j/B^SiqRVMOHj7*֖@jH Ŕ4u7N J`lDa>(0~uu꠼ FO~a1 (p(3x:֕o&f} GDoqDU֕oqi#ב~\RC;ݲ:oUxێ!MzOt(|7&Ώ@?l|UW=j*On_4bTr$Y]5۵3klɇz+(̌e fOqO)s7cMWg|q==~CGXs[rO{ҙB_8Q!Og֤];KjzsA~0riZ~ooLdQtߔyl`Ksf1;~枇}+)#{Kx">tς p^zն:8U' ǩL澹7i ]u]Eۿ4 UWtUFװ5sS )(qcͨgtİ(;k=g2hsZ)l,K,?Б[N>Ʃ5PkVG夷7)F_7( 66K%=$UnZmVٖGfب~=|-6 b8gbw~7̎Vt,c :ضv76<w]c{Suu4 c)i.H&KOH-9J|L,_7Jߎs-U;P_Y1pOoi])od_]AU " aTpô\M_0 р]T¶;qK3cB}O6ƩjXL.C YQ$%]84'a>,JҳS e5-ͭjwϹ aΤ8BYA]5֖mTUSCIˬq.j_J[l#0+nIA lIA{n0(\>~\$dNl8æm#+"N,ٙKs|3IX%&GObrDF]Pw|=v9BݟNgkClʎF])k`o}gH`alD1=/Dv:[Oyt,{]MzT]e_SX 0EKnubs.ɟQDE5 Opu]5)k_$P`lœD,'9c2llډPE%@9.`PX2ĉ$ AíC:\ RV˃@5fN妬v{;US\O}huZH ^̨>_hBOwy@&(fT[=1̌¸L*Y˺͞%%̫{=FTV7ߖ+ ѫEM9-ߗm2FEM=C:1ZtFd\vڎRzʶT_V[k]/ƨx;RGok{BPGNϊdWGqw=ĨΏێX}+Efri.ؽ֖mdS{P!jc=9>uܝٜvܿg+>Fg\Y.; eh5::>F S'16< s>=tUZm|~Z+(yΡ:Sw=W5weYdvN)[am{~QfRNNN׾6Vno"|Øp^gC.QҀ)(.4b##{قi?ވчWu]gWa-vW7iTBs&a6Xl@\HFw*Z.s|G0y.>Ɠ2d&$ AíC:\ ޕ،A1N:>2y210.fLxFlc?=37qS9TfC952%z"3b.jYj= wȅ,HyhS cD9,J|3yޔNטּ?MUِ_<ʖ:%)dGr^w|:[="rb(; CpG9ZQu1Y2t]gw>>*ZMI`p,"&7`7XY/Q1!3ԝׅ_wݵQ3!#,S4bI<7uQB,A\;rr&`oo+~|K\Ȭi<<.+o]Y8jKi):-;;@OfFd2#f Rk= 1 &.JGRP#kI=9O;@tm}7o}`pӖ?GD9/5@N4\;p oJ CY<9 :p_΁B#'TFjHBS `!?a pkLVhDdgw_őo5}$k;_\{]vO#|Ù?$;NۥxpyyV s6h1:={fGW_R15]|rK\V_|퍳667>N(H^n?翉N%e;]\ F2 p"is,\0kv\2 )l(ζSw17"iq2(gc[v{Zk=7|sgL_NA}zoΫ{k9/nIohܿ{9xV9uV?Õ( s$Ƥް% cMܥMF_(щxk_mk{ iIo_^E5paPLNdD$L`CV4vyis92vՁI1 C↑Bх(\z1-V,YYA}!<-V~q RKU!٩d|K [; :PCn^g\j81>ʴK1Zy_g^bP >fG{bB!Nf(Ց?b^&.ͅQ1rV^1 \1+6rqQLHu=*'S.O)j,f[NF$v Kn`∱n8 !Hƈ2'`x=>T[ @ZH _˜jSkֵk#7ySs?Qc%'b}0;~[88dH WB1L%'2q8 t TYdTB(?nP.qX8Dy`h7z5 ! B!SzgQM9ppj"+%yvA9;#ra7Uڄ8 B!k(/EDޢvbhPpUDQg\;gtX&JO A2F|x9ËE"{x9 }wv :B!B$!B!!B!Ġ"B!4@B!FB!bHB!B  B!BA#D!B1h$!B!!B!Ġ"B!4@B!FB!bHB!B  nS2{lVX& !B1$݀s?Ξ={x(++#..EyiB!Bxgmmm\x,,Xwo !Bu@پ}p\dgg{k./J!BAH?&,, Hv;^lB!'YV,Km7$B!Đ! A㺟_h\8tgÃE"{xw@Ytt4h~VWWKppp#8wAEd/Ëo!zG"\?d2c϶m۶1~x/J!BAH?㪫bfVX !Bu뺷ql,_իWw%K,!B!NB!b,!B!Ġ"B!4@B!FB!bHB!B  Cn7 SNe٬XMe˖1}t.}Q%ҥKfp8x>}:f⩧v:tv999̟?W_}Mppeyf϶?l~n:/cv?Ξ={x(++#..Eyiβe 孷ޢxwqW{)b=裬_^z~_ 7pw|@AA/gnv{( dժUYg|'zg92rJxX`wy'o񆷛&@QQ<#1e-[G}?1ywy?~<3gnc׮]nuuuӟ$.B?|6l~pAzJJJl߸q#%%%.b,X>+e4ygXj'NK/ .kvD?Xx1~ۈ#l j0wV9 Cjbtq]&&x ʕ+1v;˗/w>>>1Çywy8z(nvD?Z?g̙,]C1sL.ro7O mg= C /ڎ~~~h$O<O? 9G=37sKu !0DKK ӟ>>]*ᤤPYYVRQQAJJJYYYob@˶Ԣ! ++ Ď;A랳G 77d~0sL/!D!B1hdB!bHB!B  B!BA#D!B1h$!B!!B!Ġ"B!4@B!FB!bHB!B  B!BA#D!B1h$!WW&33?ܳ{a477{eB! B!բE+yijj⣏>>'$((Be뺷!W\رcٴiz+w}%b"b@tq{($b!XB!D^^&"JKK!CB_yQU7|S΂!3 B!{e\s5<ٳ_|MB1HBѯ?S^^? @rr2˖-ga߾}^nBo!XB!BA#g@B!FB!bHB!B  B!BA#D!B1h$!B!!B!Ġ"B!4@B!FB!bHB!B  B!BAX#l$IENDB`symfit-0.5.4/docs/_static/interactive_guess_ODE.png000066400000000000000000000436561412237106600223510ustar00rootroot00000000000000PNG  IHDR"ӎ>sRGBgAMA a pHYsodGCIDATx^ |EqE r XEAq`CٽoݿI{vQ;vHbu_wă~PMI>Ms-Țu8\AeB.nպ %&E,Yٸg~K`oovoS{0_'^u7)?žދt`<U@!A66Wl \1ܭ[]v}[_|4=''gg 2﮶<{m>β09XW<$ _w0Da'2d'Ӓ\R.)cG(pţ3\u`v:X}@ @)s+j8] ޚ׬74`ʤ2־ o5o>>/JHzsѕ>W& rd (dW zG-]q9~OM5Ks>tP>E+ecc𔕃aa^U-"b%eO:Dz-5?%Oغ8U)j/ÎV?b"Gİ"bX:x ,]:i`F+?~w8TqؑjsitH5N<>W& r }PXkRt t&?}k;ԇYJvN1,$bGdɒ]vFِ4r$?P^{Ű>ʆT}=8@E @+F E 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20zg͚5o]5a] .hժՌ3t˰aêW~:A eGVb5jT?\rӧWPСC@ 'M޾srrWZD b(;;uꔒү_KKFFFlle]}/_t'+V,Wܯ1 7֭[wӦM .LHH3g4vܹGꀮ]_|\yrXxnP0PF^~x@6=Zv={|'`ժURr嫮Jƍ'+Çz\믿^u+ 2w޲1|{ĉ_|,w>}:8z^xږk;qCHsbbbÆ DLLwܡP,0PF9" G o3g<묳Fgeeɲr׮] )xPR%)wk^FE]"55UzijP,0`tɼBlٲEZJ$z۩S.]~mڴپ}gY[n=Sڷo]vV@À߿P5kCd+$_+V#x-o֘oVUG-\RU p2/J/-u5jTJTrW<~n^5k+#2"AU,\Eo.[x [-+''e˖Rӧ>@a'tԫWooHNEK SF6y:SڙĪʙ`bHz&NmI\|6UOCF6jD Ic:Lİ 3FÖ3F֯D I:LİuD Iw:LİrD I[.01l$=lW_3F֡D I:Lİ]rD ]ZZZF㓒-[[4iR&M*UԠA#G~a]9sN:533sРA [n̘1CBz̙[lYdIz$>?5k3ܧO]f͚ 6LW+z۶m+~a;l]옘ye 8]vgժUꫯd{ӦMO= t`&b8v!wuݲƌӤI]qxbcc+V(WwQ'eծ$S:1,*WX5zMJ:uL:5##C 65j琚*9Y01D(ݶm!CeM>SN<>VÕ+}3ɔN QrrE͛}eJwX3^~>رc=l/`p.`b8d3o<]kNW\{ݻwj׮ݢE1c}GqRon۔)v _u>`҉`ٱcܹ˗/u˒mҤiڴi||޻zٳg~O<琚*99}m#!H =ZBWW{ 6t'LPn]c5;v '%#)_N:e-ZHD~V}ö`睧[!+99o߾bY͛7/xsssU瞫WG1|Y`b8M%={tm۪T2`~߯]ѣ.}þ^4Y%%%-]T5o߾W^j[X/K||gLi'=lW1\n6׭FöiR 0 1l$=l?c!#X֬YEQ61,aw u"p#|eq-_ޓR*$7bHaRηo. 4hİ9a#yMWŰ14İ [\۶LB )߰v@ )߰5lhU=F7l͛1w a#K/cxIa#=۵`bH:յ`bH;4ɵ`bH=ڵ`bH-%Ŏa\{&!o{̎\{&!oƍc]{&!o&Oc\{&!o|ӎΝ]{&!oyǎ+p6Ra;/ȵ`bH>\Ia#5kWϵ`bHmF;Vu6Ra۹ӎ'\; ocXîcF7l:wr6Ulͺ 01tiii5OJJZlne27t=lk1C5gΜةSfff4(!!a֭z_~YYY׿+Jgm 01\}j֬0__ ӦM^{W^% .cu`b8cbb͛5pvȑ#of(a _~ A ю;]|[֘1c4i+y޽[ G8)۷o7lW_m 0p^ѣ6m+.oԨѢETNMMޜ<1˺ 01D(f9L)"?>"Oa=~]Bfrb8+ռysS9AW^ylH#|6T].iӦeff$$$deeI{Ϟ=}2]SyĎu`b8⒒.]۷o/JO-!Mt`bH>~};FW& cZcOt`bH>M;9B ǰ]Ӧ*İ| [v OF1l1c 01l$&,1<`L@ ǰ==z*İ| k1ܥL@ ǰ͝kp۶ 01l$7a#իn@W& c~Ɏ]6asca#u ޭ[a6aZՎua# ^JWa6a;|;?HWa6a ;yGWa6a;LU@#{zcua#cx]=bHc_?] rrtk,_ma#MW}p4Ud[ZDbHtM|yOK*İ|ۼy\~床0o. 4Ea#O?'U|Ud/(A ^"͚/w%a#͛)Sta06aۻWG+aSa F=l'NX\mnASgJ;XU9S*İ[tz"I:ϗu0 DbH~|"]ESF ]soF %K`bH~m`; U@x#wƏctވa#7ްcsg]E4l/Dİ…v '%*{Dİ۪UzF4u~ > 01l$öu=ٟw&&#ws}.oLF aRŞ}QWDhİ ϶g/UD:VÀшa#6l[۳yH6u6 6Rav+(Δv&r4KKKkԨQ|||RRҲetÔ)Sڶm[ݥSN_}_av46RvMK !I\ RxM6RC󫁠u36R׭e%;A a3ǞڶU@F*zV3U6Röc=Xٺ(o^hIoÀOİ'*U/3SeE)LbULi bH %3BA~N?(A )aӞFUl)Z@a#4lc1ܽ1lm<;t~a#4l+Wrsu F h؎*VxV3İ^XWYcCİ[ncx$]{E bHۈ|ӻJq"RD bH|s D{a6Röz=Ԭi% 1bHۡC;Lmn߽pF vEL*}J}5(Eİ1l}3*}߆NђR s5J1lb ۛoSE6 D%u3U qwR~7bH￷J$s[^Tjw"1(1bH\bjD+Iʓ3}J6Rsg{l:؏#u@RYòB }"]g#61l^=a BI,0Jp/nF*eg[ر@*d)&\K)},a#԰͘a:Wjڥ[H9ӫI4Na&tRÖk%%ٿz(%*>VRu]İNv>T}nRru%ȹR|q)!T v}^:ΝJ$3JpvXV[d1lRC-߻vm(UT+9Pj\Rr -O1l *U_t#Ryq ` O)w֦Mɿ%HA 4_C^ ͯ!!sɼ]P0'F'%%-[L7w͛ɿӭeeYmγfδѻ "d5gΜةSfff4(!!a֭z_+VČ;vÆ oŊW\Qv3iJٸ~K٣R#+99O>bY͚56lڵkΝuŲ;CWְk=xgR8úZkPkk|k*퓫O?()b8e|yڵӕ< 68qXly智pQ'eA6yfr]&MW)_>Ѻn]sVueWX;ZW]e믷nɺVya USnt.=zҳ.wݥK^}]G{P(˟=zcs/_5f̘&mΜ9SW,Kt!55Uzs*aۿZz_?KqLw$S( D^ AbxXѣG7mTWH ri|ƌPv"3޹ڸZ kbk?yǚ3޲M^}z%矷s2i.'e]}Vgu?.S O+dyi.dL6<|cX$''WW,y>OѺkuŲ:wSe.iӦeff$$$deeI{Ϟ=y|rY47nÆ ohްb8⒒.]۷o߫W/-y睦MJ`7kFN @d D @d D @d D @d D @d D @d D @d D @d D @d D @d D @d D @d D @d D @d D @d o>۷%32@Û6maD c=(cx޽2l۶mϦL7&pC!wEc /1 L!-ӯX! L!-ӯX! L!-ӯX>zhjj1 L!-ӯX L 1 @ 1 @D] 5j(>>>))iٲe5-]K.+WjY'NHMMJ*o~zG;v%\RJZjtM:r5jԨ\ 7ܰ=,?^:󫺴nzѢE݈+EB~ =VNnC@ݻw?O9VZ^Zr_~n EW ϙ3'66vԩ2%$$lݺU W2?;cxܸq ҾnݺH޿N7xC旵k^gyծ>}ԯ_>ױcGrrrԮ1 2bQsW믿'\p;M T-Zؙg׮]ݔ!1vwW[l'˔ w_ӥݸGAxNNN]f͚ 6LWž3Itݺu1Zj򊪆-y0˭l۷O"M];vPŋU5lviqW{LR1lM]W:th۶muG4nX0E13o<]kNWž3՗D3PU7x]wݥ+jƍr剿l'- KBmȑ~iٳ;㮼nȆ; Õ+We(hCyrvmjպ LM|,ZF1cȶqE1,OfnYԤI] {r1,BrTU<W_}%y| 73gΔHS_޽{J8HHHpXYpt<{hѢő#Gd݄E͝;WFAԩg.ÇĕnJz-i7QoA]g,bxŊnYGnڴ1/] KKLLtQ1|UW=Nd UV 6f͚6o۶vk׮UBb8lo%'L`C 66.z衇Zn-&>YB.]ԶBa(bCh 4ؼyVNAW^~go"˗?_\ߧOgy}ݧ+s8 0QUB{OUyQDQ }o6F5~xUga{r\˼?&u~o&0+W^]sKz!G_O+o]G0`L^mۦʽޫ~ajժթS'ˆ+_3M 3θ[;nC@,Xe˖r=5k>SZt,Y"~A] }a1 @ 1 @ 1 @ 1 @ 1 @ 1 @%}} p)Q&FD8H򫦾2$ӣ(Qb8bŰ<߶}lo %w\=g %e˖dbQOdIIIqqqZMΧX12I0qJuʕ;՜c2(Ibeze)y衇&OܳgOb89c>8Sz-U-I!)SJjj*11<{U{'b&O!GpZZZj>S1L B S§E #1ܠAدJ7G S¤Ô)pqH w3ӧω't0%L 1L B 1zQ~W^u0%L 1L B 1nݺ}")aRaJ ƍ׬Y6iD6Dvvg8b8w ڵk?êZ1L B S§)ernl٢.9b&O .9b&O!.9>S&ϔO gJG0b%oX 1\r|0x)Q&Fo8p0!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 20!C 2:2G 2bXK[߾}Vi j>)>qwK  a;7|S^zݳO ݥj8gTJݳyV.%w|ǰl kW' z BrE`!rrr;+IW]|^4Y6J+|'x,t:rȮ]~úտ{..wϲD @ .=g}MǏXbnݳl8,W$yr]wu[n;| Q}رc-Zs9W_-1!{qs{ "C}7嫯_O8~Iy`a`ɘU?CQ@=L2f͚+V֭,?=ˆS bؙJI,]r:nܸ3/ R^tiNd@zvo."ݵK!IY6nҕPɶB-7^{v .tmT0T|,wQ۵kw}~\HtҺu|U~Y6J7J5j,X@HW>/ R<7nL29inu5jB>3Xf֬Yd(g/'gp $%M&+`]wӧDY;c|glIWo\s#{GٿÇ2ܲ٫gp*q }/SN;Vȯ/ h K\I~/3<3dY\~=X13O=T +l{, A裏=\}1.>ݳl8r?䮐'(ǏWBRVu;-]QoZ=|rׁ}d49~ʔ)gN(qˤI233{ co 6[> *\cq;oR|^4$ްaL  5jT:u*U3AjKtoB. .ٶU^zdq=*𪫮8jVZbLbwϲTx VEwz,z]r.%%EX*x'O5nx۶mώ;9uܷgρ8vƍ׭[nK=ˆS1߮XEkז_*!c$Qf͛;_ZK_ҬY3u6t Eg}Nq255UbXR2ҥs=ײeK"sS+?/A~*p2 N,WOZzU={ BrE~vذaqqq7P)ofŊQݸyf*Y ?䓺gρLLLTW[ڏ>ޑݳl8|Ewiw-1,L gΜ>/I&&L[ uUĝ:u*U9݃l:$KF"y5˶sr@Շ

z꺯E ިzK_/J5QFc#3e@2^At,NŊ">rSO=%SLm۶4iR6m\\PFƎۣGX!st,d3tP /mg>8$]٣zp,Nʯ]O&rB7ѭ]tE+W ( *YÆ \:I >\)ZE>k+|uX6 > bŰcQU"޲eKӦM%BnfY_GGydUV\(=+w3 r%HjՒ%:_;묳`cm?E֭[[Np,NΝ;ov_|0cƌӧϜ9SN Ɋ,"UY.-ԧVIWo,:zu[В%KJ%Y^_ٳIg[ѹsgG,XPڵk7n(cr'+Xpao D9gΜ>W  /cƌzٳ$Ŗt_Y%'ҡ$on^,=ˆS"TK*٪QHW겼Δ'7(#MHW>o{D9ُDfƍ2pz_=IVrg-y=ˆS1ꫯՖqѣ޽{ao +?Pfv _4%B*|${8))i?ًz fp,NEư_8m4]waYҕ%~qRJڵ>|qFѡCY y2$^_o"޽[~DرBq,Nǰz y]*zj׮]b@),1,}رnSOڳgO$nժ{S =T~m9F+|fp,Nİןo+u^a"O̖(Bl"Y&Gn;w.ޞ[8wϲT~]vի"RIbS : $ ׬Y( Lں`Y6a'͛7wM]h˖-%Qd]4+骐%O'&&zEeiW~駟6m۶U?.dg͚:ɏH'^ 'b1,$$KXNj E{ ưdM7T~}u8 yz]:U7Jq]wuF 6l֬ٵ^SO_>;;_t%T. eé1,bRE$]IxɲTxw_ϲ>GbkQ?%$#GHJeéȻkĈv,EAadA)k, /']Qp N骸h„ pcz 7M{ İ$vE]ڟ_g^~{ۿ_d&$I$I$I7o $I$I$I$ `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$IKGıqCD+".g ]$Iƛ$M$IҘ9"v#bAeXDĞqKD\h>X 885"֛J$i\7cI `I$E|8>38 7ofyXf^g淁mHI$%cf"I=2,Iԓ==F3cݐa/"V6.I$5d**HRLKFruK-GO58x~5pEf^4t+"g/is%({b!Ix*-c"I=0,I5njK~zϏ(?,"ynD):i=GD03p)G}_0͖$I*5**όU$LKԃ=y}^*%MVV~+PBH`?#ەkbV{gՒ$IgҚJHLKJDg׍D<==&kXTtDR}}##kRt¬S)ŵ{^f9$I'*,b$ `IDc5:3SD< "V<'3i␨nd 2I$͘J7U *%$Z.[:*Ӎ>FX\+~`tFć)̼׍I<]gTt*=nvxZ<`^{>=[ޞ0qm]s%Icc:ccI;$i:zfgu_FOW|8xIf~M@iʿS2oq=Թ7gʩn̿eg֧̻e=EڤŔ~gw}v]C$I3gR0V0Vy$u$Ijv#{= 3/6~8z7gZYkuk;RoӉ̼="X ZlklDq8Wp$I4ΌUUUU$LK&j9zffn2J{6z:" b{}FOgm~fo8bZX+"SΧTvS[r Egy^"bM6I$3ccIUU$i6t$IfAuUFPY+ߑbŵC><5"xٽڡO3Y3>.hӟ6sz*{Rt l |XEN\?b^S'p7n$IҘ3V1ViX=cIAd Iwq*yfn҇ |#WDaW0`\2saD, ef_M`o2533s8 89xR6̣X/;n$IҸ3V1V3V1V~$9!vt+"֦:`ωQ_G/6N(LlIѩk'j{(k(~!Igb2J[*#,Ii#7~;(m3]wp{f8[m+ϳ2L`;8~6)I,X?U$I$IjAo6/bT}D,E닷V*F=P$I.*-􇱊$ $ISfuD'$I4 |.U$it$I(->4$Iϥ$ =$I3^ <X ##yZ=!Ias'$I$IID, Xf]f]>̒̌>O$IscVU$3$k6m`J&@5:03?2gԜ$I0TuqdX$u%"~΢o;3~v.6氹$I>{B4 $iwj .,nK3KGıqCD+".g I6$I \:`C\=1`CxOHX0,I:/v/7ovퟙGT>)Q-">J^Z}14$I \?*0VbX$u$"W):`ڨ9D"rqvD<* \큙yGfgm:`}1,$I \jR=a"I5%IRG"bor*`A""Vn*7eg*"N~:Ŵ`+uY_Kd3l VyL}':r_ Ꞑ$IKUZ{XEƚ `IԑKC\)3wBD<03杖-C1y`vخ%dk$Isϥ-==!Ic$IVB o2}hЈR|,`2>_j 3p :GqY)``ʹD m b$IX|.mX%cI/IhU5L+3OL@%+U4V ?8CʿNqfL=}p7m-/{]Vq{ڻ#) bH IX{XEƏ `ILb ?1:bʣ S*WX+DD, | Xح:pppjD\MkUlY/}[md LfPyԗ/i bH IxXXEƓ `IR"bu&H8j@XT9oEĊkɀERׁ'/Iӝף=_.7Tvۂb5#(FOZO->$t}!IP0V+>U=Q2VeX4olb$pm{Xm -  KRc|*3N2<-3ge-K*d-WN5UeU=A}!IҰ0V>bR=$;D*U -Ω-Ϙ[U{Mw\D^FḌkWuyr}Vmqk%RGJ__}J݀k3? ':'澐$iKU'U$I0 $ױh'yӣ;V#2gؽx:,(#b?t VV<?>; x p.pgDׁ3k)zN@𜙷F׀#8c(ۨM6#ge=}(;=f[3/efYɿ%Ij*Rc: cIk&%I]+zCAeN>IC)/ EC,3/)GME=ڈ6.͝˿;GgSGP~쑙WmY*ns)F?b_pmD`$i'KU'U$iEf Y)X33oPz'/,3<xHvFL |+}ZDg極v ״[r5ވx p\wI4h*ҹcVU$ix$u-"N^X)IfQ:"SMHQy퀚42"A#7졘ipEf^5X /3ﯔ=8(3wnq̮73s)xOW7ED)3_>'y%I0cϥdVU$i](Vγb[OPL%`*k^l;ec5g:/kCE4dF)VŴUO貎xT_0V0VQ+*4N Z=}pa/gՕ(ç;>".[8`.c:-ܶrf:e/ۯG3eّ'j#5:/}K3߿$iF&V["2`Jv]H>0<*U4P*jXE `Ij%(FUj`ZYNYefWx'߷D1vE|)*.G_^x2ے.3oցZNGjrjՀ7 -$0jϘ^3"?P'e+۷uqF1V0,I {zlD>_oE^ 7EUWM{'ϭ}]?zi8ةx\9<3й%͍w?4$ *?mt=XWXEҨ3Vp `IcQ8"Zm`ro,oǥ}hGזӥr ů^8:Zd柀jIkuD <6f$IEU~ڢi#bc(bHU*4X&%iLS('/̻Zafږ̼Zɿ kk+U}4*IC%3/̕2əAG~t X<>vÁ8%c"IВ4"b> qskQ>5$࿁m*e/#3qqSV#bo\u5>խn$IfbQ~LD,&4/(:[f,3_Տz$IX^bϧ:(" mm1V$IRWZHD* (M+gDǁOov̛t([OuK$I*?kQ4h=$IEF$Vh| J_N.$It1PLkL^ FyIE=ַ88/#f+(k}{gfsә[$Idb󁻁+e{3Y̼e2V$IRWLKREċgQfUТlb%g|5k׶oG[n1#bkI#%IWdq>}x/#k43O-a"*ƉS@KRDīܡr ;TmQVIDx9]"8gZ$%I$F4VO.Ws4|e>`>XE$I]1,I c쐙).nQL+݁/0yͬ;St,rTD߰XXV~/I$QUX<$ @D<حљyiKycI$u)%!"mf8Gf^dTϤrPL87*\W\7qo}$IюUZO4ZٙyJUg ypƽuqbXFTk 쓙ҩۢTT)^읙g9C?jeo9w?3K\D<3">gEĂ|9"~G#!{;gXrOkX x=pVUJW|}^cIҔLK(F?{2M!WW+ީ#b},`jk2S6}wz6xxԩEq2/Oe(:\4#%"V֩]44Xތr2/+pIUJw|}^cIR'}`׈uZ9ɉGFG)2 ML^ke5GmSSkn=ثE_(G(ȝG16r8!394Xތd2CD,`d, pPUJw|}^cIҴLKhڦER3'R7U[̬Rf0:\wXf.s/gfM˵?<,z,Y^2,Ok,IRF.Vπ(Sf{uc2\U:sK:В4Zu:)3|Vw#mT3n|׷yjDOo5!"֫iaD,9"wEĂnD<>ُ;^cIcbNEċmkGd浽9UFSKkKf=7*G1yԅV1? jCD< XOL+]([˗uWBӭ9Zd[>Z|=+"^ l\>HKR_bX8V|5o**Xx%Is$gyFDx|YBjz18VAE3n_&pf_9*k{kf!"^N1yu?w+ZՀiι p:ů^& N(~arlk,I&1V9XV̼{MlX1U|k,Ik&%I3uvDsdw`3A5F݋'~'6nySdQ<,) ž|;r: #b#4gN%"bM[*UT5$I-(VCk_-8c]K5%I3Ruzh+`5gZJBMjzwH]צNѡŨ 3I_f>}̼:3?wإ]Ze hnsgf杙yp$K;fk,I&IR&,U)"I2+UXs"^cIҜ3,I7wTKy^um> 1^D `lV]ذR4N**"Ή}#b.$^y_u/Ak,I_-^97Ҩ1Vyy%Is$Ij`Mʢe:,d6]7!ٕvXiAD:"RvMXP˖N8O+1xTu$I>OXL2̼3Vƒ9gX$ X>~e]TSRvkeEĺLz6>3 <xp~.>")RoߒDZTjgc`ys $IVf^Qy̕3̼bm2V>5$ `I4"byu_)/3벪NԪSRNmu(fpWeb`ZFmyW}ZyRȮ5$I\0VYK `I4t"b+oOP]*.<y|fAb 8%m"|hh+[KDdھ>}7j8"֫\߭U"~$Iczc cILKK_V)~[f~*{TZS[3P5u5S=aueIeݙ}'{ZԳ+q^?seqf^Xnn;Eu)ypt$IRC]X`"Iz `I44"b>p Kݙ[XRj}[S ?gNvu*j,&"O5!ߧFz|Xxkf8yhد/˿-;U"!{̓M$I3da|c0V$U$IC!"Nv 3݅jG͙͹֯]>sVvuų+N {*;YO6=3r o9ݰk*3NU;UZ+kny5K"48RV)7ͨ7Jqd#kL^kjQax =@^NM"bZ}m{uH$UF9z$4 $I^Sپ<"7e+Zw:UJWGVt`+`S8q&H+[(F??r3-2o]D _D\DܯY~vprTf^_\xyftYӮo{%ms+/ųg2s}$IJa}UJ*$$IKڔwکrKmc^TT>â]] 3/Ncu]xbm)۱si߄):U.^Lli5;ˎQLv.@D<>=tI$U C]cb"I)%IRCE|`JQN̼U~3&bꮫ/2u>bOwSPt=63_<<Z55I9}+EwNw,i5VD,Um$In**tU$i `I4pyX>`.y*ň?U)F?bkw]:36M]/7of}L3DGiU1ݶqx):UYŔmDm$IsbHҘ3,Id],Z'ubF-7:E>< xnq\AxbT5pH沣P$IcXXߌU$Z$!"":o-,gSE{WnkGl^cx`_)vfCs%I`2U$IU&%IqjDTN6ID<4".6-O1uW[:P^`̼ 3o>RvpDhW$iyXeHhEf t;$IҮA7BxE$ICxEc)%I$I$I$!LK$I$I$IRC4T7N7XQT[$i,]pyOg7pFC7mqf3sK,_:d6pUWcxEfQd"4"bS҉\t)tH$g~7urvm|O}pˤ]~elfբ2AGxEgQf"9$I40/ 7pBy8ᤓP$I4p*44Q/IArDb"IA2^$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$Ij$I$I$I&%I$I$I$!LK$I$I$IRC$I$I$I0,I$I$I$I aX$I$I$I$I$I$I$5 `I$I$I$IjnGDl , \"3d$I$7I$IRS֬݀w[88<3>m9aUgfןH$I4I$IR8fMD,'_}g #)s8I$IcxE$IT&5+"bp wuE |/"J$IW+$I&s h͖]keޛ:]v>SpjDlٰs_6+$I4W$I$IeX}d#'yHϊ 8ݿ@[A^Y[H ,C̘'?TU8cN_&$-)In୵ۓܾ>UFLF=9lye l6sw^NR3;xXJ$1.,/taZkVջ_\\+)|>0l0+Z5Ia[g'4}"3Zkzu&&I~kM-'3feZk־13zMJrC+Zk^Hߜ I~}3+<"'S pr/lwr7%ySU}A'8$G|0-{8n-%$%y\$(I>wܑ俷>LvWE)t `֦v[ۦ#IZkH޽/pqWdi+#`Ss1l0C PwQm`aͤp/F s/l.0'O0C gP0w;gVd .8AHȓ@2%l0нـ"tX< 2%l#0SwvqW0Dv2^ :$=' `APrW0=%? `e0FXiɑl#Y8!M%frW0Lݠ2M v!r$0yM`%Cn2$60 !Y8I 0aYCAɐ(VHg;b%CQ0,83KBx2#?}v`lcG`, ` u'o>X7URlM#;nX'03D %F`[),Ȏ` `N(:6J%) IDAT `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(:L=.OeIpM$k3lnWk|g?~M&k4H_Jr[k=g"feܪz}7'SI-k^L:"+0+QUg%ycg $$|$飯/_VU_oN+L̪0 hKZk־4C|mls$ɍUy:չ= R3zX7X Zk?vokILmo|IhK fVH^w `V;\0$ןnǒ<N̬EU$/YmI>F^k `FYZ~Ikiڻ̪ \;x7[[+0c2E3N9}`1J?^N\MG^{ `;~vUu3VU]{fVxs+tOؾdyw;U\1LgcIȊ_@q,pFSUOg' \ly)MU=(%տa?vNfV}k+ 0czxY>#4ݐ½?Ju Ιz%Qv><653vد8ŃN4JU}~Gz\8_ ȭ=[vwU}4cfV_N\2XЁKN$T,qooYdkƒƶǼG^vʼyȬC +)Y{KWȫ 묩;wǪzTKgVOf ӬKY`{=ɋfRUZks[kw0mIbr43If^`tGwU5tCdV `$w$yeINr>o1;)WUOX@ +#]P2%6G@3ڽI^;X}]Uՙ''jfէ8t)=v{F&8Ul `V$B{Rj;"U_W/;0`t d6"l.0;ﭪNꚜx ۥ3~<|P-!TxzfUO$hU$H$lswk[kG3&M= W$i'`3)Yzf7 uv˒|i DnYGq+6TޛyLk7&yF[ϰ]InHrEk `[=v\+0969S@ZkoJ$OrqI&`[Zk,qu%I^`3 -lǎ#l9m P6ے6`<  #`w~,LO 0CHay@϶i)G2*LG ~0 0^x/.0lugXs`S\xPv} l Mz)LG 0fٔm"fQlx$5widR| `9 dR"vR,] UC `%)  S@̢n dՓmyNo"$/n&t `5޷No(/SP|`)(F"3KYUS,IgQYFVJ A1)`ɉMVJ au`Ɉ,J 0 ̳)| r"("̳2"B Y=NȈl; ')##A'3 R;Agpj!^>ݠ'ܳ} !`W(]x䐠NߋCv|A ـ#+{x ]$nQ;gz~z#|L6ݥvO{8ِȆ@wM{X e0D6g {XM$gpӓ \K $U`;U e(Ff =l'1Ȅ+"3$2/`MN`Ȅ$ȩY ! J>~Ȅtщ |2![J1< ,8 Y& `8pp=mΔcɃ@;b'{`(#@ @!`ɂ9k `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(: `N(:L=.OeIpM$k3< <&ɣI~3/~ms' 0+WU$$4YUMݭ;V8Ǒ$I$Or|$kj6` hVέ'ysN2%Iے|‘>/?KdʞK|wV׮f,`zf%$oL>Wܚ䓃]U嫟~&`In?6HrSU=gs+ 30$_3XI.i]Z$MI>:͑$7VCV8HI|G@kڟn}Iks\{=YI^YU_ՓWUÒ^Z{[koNrel${F?ZwZSmZw3:X@w$`f=I?ƭ%y`Mkm skV?o2`fT{ꗴڙk+Ϭ ɵ#ޙ\2,v]䢙'yz|Tk`ç8y)jmWW#4#ԎN2p S3/,o[kOU\1LRUMUw&,O^{ `xSoI&B^{ `FSUJr`/x]~ꇒ<{fo'F$+Ιz$5|< cGh}T\0꼽IniENv1=x|<]slL$M]{zD+KW1 O~ܳ1$?䟯dʞoMr݊ ,G^2>17X򃖜e,|GUU H ;A̘W^sQ־V'|N?I1IUUNht`yx\;IޛzU}Ek#In_d`+˽e<1ݑdɡ$X:Ѱ _HL0 o1Wi)WQM䪙UJr-乃՛t.P '}ړtWI^5X~f6 ^PU7Tգ~}QNrxf%iC^k `Fw"߻wBNꚜx ۥ3~<M= s`}W$U$vlڑ]4U$3H>fUI^_'6\U5i}ޞ~ӳ?n^ x3$JrmW-xId$&vVU˓(tӍy7?*F 靼;s_[ISU?|,䝾}]k׿|C*L=}~ڷ$9/ɑVUw$;㓼GM~*?Wرcy~"7L8,FvxZU}SND S11p QUyk^cw?7s(7Lk3UIޖ+s~iU}#l{]EN4k:?Οz;E~V2.px֎W5I~:U9QAiVA%IMzjki 3&FGEVx-co`3)DkVՓs~d#I~ZoN©wB3[yIEd"2" $O9I~4`b6 Id ɶl:G<ʉ˚KkU$?!Igt5AbGlT"*k$0xZW'c{_wڭU$?$rH ]0| ,gߟ_}*#ײ+d[88ZkI%U5I~*ɹIe;N W=1O>C IDATK+ΚzZ938gtCex0%Y`|r-{rYIGv8@VX ]=h;ɵIDN<lAg(M#\KZʉJU=hq`kBH`S*uC2-[Oq);Z{` m"$Mdpq3L)g ̒g&2-4g6 <&i`xh6 Keȳ> `AȳLIR,Apfe`,,$4qZ "Ug`Z `( qr1yO Pّ-?@gtK';w%t߲i <}nFyhFr0j }ms-{\ [,3\ `Xw󹫾sa6xdSɏ0OEq$K_IKr~/eyƳ"!IIvO>5I{eYnX `< t 1]Ԯ($yY#Ye}QIʲ,y,wLr|&$.EQ?ɩeYq1^g_tXۭ7s_lشz9xX0yKȘ. `jSIޕ䉋[g%ylQǗeyvMc$GI,ӋxI7eYV=6'/ݸ$,66Ҭ۰Q@9+mrd3ezɘ `jQŲ$J聧6%$I-3&TQ)+iEL9ϒ\du;g-fL[gEkjB~8+mrduژ/d\?OL]'SNKrRY'tyt7%9`:|(ey}qӓ|&˲U:,b$$EQ|,˷8.f0^? 0!~E^#ח|$Ș\ `*W풼d`ʲ|eYnNoEQ|-'9hS%yN0/%yUw{,oJ(JI;铊@Y06֗0se}&@wɒÓ/6E\"GL^dYNr|+eyYQ'&.eY^SјnMr\Yg AYE$f˭֒dMm}{BҹJxn:=&@ɒ/'WGPSI{Ww,?WuINH*UILEQ䙳?<&Tj!o!Kpߞ@+MdL>0U;*#ދ|amߕm*IT4D_*̷"s`ʧkϿρm y:Yr.8xG`0U;v]5 <~PQ˲q-u(:a](o9= @5O/I! j<n}IںheÒ|o߁U}W- j`:L^[$_O3 Y&gٴ@:\pp 2 ` `*SŮ.q72($9~`Oc,]4>uv3L薾Ae9cΐ5`Ti$Ŭ\9>.xFt/M۬W'9sJc鴙 з>_'GB訾362dȗ;5`:Tiʲ,GǍsb8*s\冊_g$&;U9&ܷܫ%Mg^G^UdʹɘՑ/#ot)cE9['9>d_O^I^^~[W/f.O+2x8dLr<&4*21q]؊9ɿ%<,MBF MBwuqWeь͚7eȚL `4x1"UQ˒?QoJIJ,8ɱ07ep˗ q#3I_W_-t6YML<<~gnoMreeɚ_##ns$a,"q؁2k+tl Ñ3=TipcUQEYX>kSke:_,+\96EQ4AE+cWv$k@(rL_d$W}40(:_,~UYoS->`h 0Lɚn `*SMEQ\Yh* <>[DQO-u6SE8N^JdM0U;?O#lMQSgLN P y Yiٴ@;a7,Ihc*|{2y2 >#~-fՊ&+@-&ޯ} `vEQ̹6eY`L;(I>dg%yRYxMM)V P;'Dޓ/nMվ${m}|p%>mǫ6EQ<0IVZ$ǗeM'+ɘL `*l ,~bgE[IlYOO&us<,˛~=T{ C^@.W 0u8%[=0 [(}s`eY^ЋEQEֿ[/uݺn 44Bqyq h*WEQ&kf-~mQ$9,˓(eI$Z$orLEQ!ɧn>}گ-ʲlu XnEv[T tr&3a))IJrܬe䏋I1ɚnJrBY*!IX:h307|C.yX 0(rsQI7Igk_LRnyr'P50)$/I^yV1I^Y啓LBOHAI^@A'0I `jWI(ILoI%~/m|uC֬,/mh 0φMrpP)LLY?Lin@ٕ.DuW7N&GG aŮmv]RX),#0@G(:B  `Pt#0@G(:B  `Pt#vXu6Nߴ1ky7m̦EƶfՊ@kM3. 4!pbnYo[* `9Nʰ?cA`|mF˸Bc~EQ 0?]o gsh#[/0@a{ExIe-ta: $ȶMvS$ 4'g`>-M4RfQ'fa mi7E LWͼe.[a\ SlQP0Z2}}ݲnF-fJ_`:\ gLZ◾s/L[@$DD* `h I\m\ `q0:5}_`2&`z4[@"8W@3(VaO.{u΅tAP ۰Q Ch0 PVZʄTwxܥ C ;j(~ P W@#@k8*ə4kV9r hUfBt6,͚U+dL ` 98ŹC Ay@c̿@(Bycmbv@'n`@N`Lnp0)N`r7S[@' -t3Lpm@g 1t0Lq| t:V9L KW W{9me.I Cy@(hW70KM} 昝&3$9LS&'A3d.=hL `> `Y'& <`X[@O~dG` `{F!@ɏTEJ 0a˜9a3 r$P70@MvI5Ȓ$KM2!2pKqȑ@[)Z/hcy)/,L sf)8>Z[6P 0@G(:B  `Pt#0@G(:B  `Pt#0@G(:B  `PtNӱnJY;sic6:l$kVo0\<*eP? ]3ɽo `)bmQ3@u35hcK!@n;'L@7($2Yن}?0Y `! ,{H@fzP0"]2z&Ӻ a`.DfRa`˴L< Rl%/0)0MP 0@^I EdaXe4۰QG)@/ 3i#Y `w^(d{F `7YBd`Ffs/] s0Sŝ/~IV&.I\%΂W9vEQ<&˒1*닢xOWeyT$l_#3˲|$C=_F+@ɽݺ e`Esw%y"YI[eY]㘞u,eY[\nxc#)~&+se{,Y{ϻܙg ?4_X* Ar}>|~xaGʵ.6Mp,sqB1f `~<~p]$q;%x cS@+Wh ost%*I80UXUEQe9>V/)$WMQ59u%<7"zK^Bɍѷ|n/ GLNR&I+&!~IqhzB^'dr+ OLeʲ(K8kmB倁/y`@]?%0#VK8}_'TK?tA%o*`zB^S.,Y/qzdUa6,Ihc*' "@o+@xw|̳Ɇ}X*/gٴ@9!EQs <|Y+->J)s7h Ǣc&瘃yOؚU+n%p0UrIClnXŚU+zLt6wxyc^+;VLNe/TT,I3勝U_o%9z֢|pql/h.Ǥf8y3W@\LNI$m}$/LV.b$X|jYW/"EQ\F-4+#Hh(wZyG~L8]'LʕeyuQIY_[IN.$)bYG%95ֽ<[Q$y>xxlsyjvu'G0aW^Ӊ6˗8a ͠eYn.;f=<;>$ǗeG{(֧0?,prL^fѵe/yM{tWY7e$'9wUoL$N,"H 瘃>}T>S @9fO۳ӑwŚU+~<veY䌢($&Yd]'RY7bm.N2vL3Gӵ3#K%@sɁ+O5!Cs3)z]LLY?LiG7BtB;M^f'k||#hiz|LTKxF%ZG \]%|{fӁ&&V2 Ou IDATfnQd>Þ=JQ#a}&?NL\u9nu˙mȒ0} `vEW5"`U+{LO c=dI0^6ݚY)\->ӆ)?GP /BgJ`G#ҵ]XY&O @opw`.](}"?.R|4 SU^.'`2@ ZWMqM ^q8}.~#CF S%O` 0.'GG^ `ؗf'oO %GO\< P rFvJ~Yra$TO t^/JJhmɑR-Z `3-S@Ȑۓ%'-wj, o @{ȐۓgKѺ }N` \ ^f{~L2\ Q.7Wo {hgAȔ.4{9;sakCɚ0Zݭ4G3\0B@`_k_0}}͐Sɜ6`X[@׃{|;hfȖ~`h>w`<}̐ /iUAn 4ZoNӷϾ}fhǣ[#[q `v.u[A@!#cCo`p0P} P>eakpE0 `Qt` P>aȚ`WO `1څ~s+hF_2(M2k&U' `qf^aP @m!G!o2_< @W)ChӇ9 siV]~i;s>wYd>OxD`WM ``jڅp҇J]ϐ;t˲i觮w!axp!GuEWԦ/k Wj2fG}-QnO$9a-hLTYB`;rLLw{_5VL{P W05)~W@wPnO˴([\ D)~e=D$싮RP9WSf 'm4J&Qnϝ6-L+w /M_60R*~i./tJ(~4A Ut_eokVX"fS@($t6@82D Pg]0Bn; PSgmO w]) M֗|:h;00}9?wp|09:L NpuQ$ޏ;R׷gAA}}O5F~h#0013@;9@F LfD `N 6ic^/jhU_㎽ϝy9ٰ kVHcdBu6ʲ+1im݆~/4 ,ma&<4csZ:BD8 3>dRCwȲ4)8l2=`B A6] ק}xnV_vGl] 0Y&KFR0M `ƶr;<Ɨ{p}Yv-]3;8 s5=J`>IiξiRw?u,Nܜ{sYg/~moLht3ogfՊ 0 Yu-'=~Aw1F?->GevoNw488?,s^e6 ~o0[(n6&gd!"6~U$Q0Ɍ ;̏f6seQ(_q:;þwoɨ GaL87KnQCpFs7<_y[_䞧&+o~#c>|nfD m%0*YGi !)Vګ|?&?}$E'3?: ƙh1Bݜ|:`{Dfg: `QL4ۚ;}9۴ӊ|'%E$eϽN͏e`d>W6f&Ϯ@PQ&YL@?i@WML +pA dAGlޜ#^,ۼyΧ/x3rAwaUy@>O7fo^eIG\a-gM13bB&DZ (iF8<L~'>9nyʜG%xt8λ?0-91M:0yԤCy -3v,Mpkq,4 z真,ܧta^=/o_EFP$;98a!{_wŜko[<<wo,W^R,< .VqYu9ܙgS|U+;7ޚUW= _1(o}w89+;b/giy=TMd`XK=v3BW54'㎺58+?%8*O4&1P0t7~z]45҃9ߺʜvsb@|.?|iN{_/ FwS4Τ&MKu=Mηޜw+ݵטGU`r G ZSR->?V_WN;9y{^1w{دE~ 4dU u^䀟_\5?{ycO;cYaۆ1;I@(jB賟p_~g<[A85`1&aD h7w4Ays8-+6r"[Wus=?D^;a0@יMxvss][Ufn\zAG>u@- !`){w<{ɛ%ٰqucqú~]nsue- 眕y_[VޙleUe 8 L,ӷX(rCΟ;H|p~|y;^}~.ʛNyjN34@y#@(;'<ṙ[Aē+]LW2 tU;oxC3HE/?9zrS7;L^K~Q31EQ)de뒜eY_fm~sc>^`xф2M t[.6*o}G>2)jPOw[^0 OLm9ɻˊy5\fm09秿ce~>^ءߟoܷ_=wFlY>?_?=._7EGK1;|WL{80u `*W풼d`ʲӲ,/YP,-QI.~2Q䑳mL,O-rìXky˫gs&0:SS$}㳓2[ā:1S/yBUeDZ\` M',/7ϻO> [Oȶ*:wm tP}?r+|{Hfrҟ7r.ӽ53/7>=rs;oϣY%xTjwi=u`+ʲ,ڮ,%E'9#S;oLCl8(CLë{[ aU9i'+VNhdۻfn TF^S>qZV|}7gTy|?svE'yc'~s#d`>9*/Q(]I1IVͰ_.6*(>)c~cXy97縳Xtݟ9i'}z_/aGqw\/^KV/~1&{C>!@+aug>;ƫ|T8j{/@/">/(dfSScfg;AEQ.rH38OgqI^]Ӝx8ƫsp.r/wy=λ99oʓ(Eg|/˚7ͻړ/bN?\z Z&M$Q;youe,oC/Y\o}4Z[SSy ˲(u$%RUE-ɗ߫(b"Z׭X?;ؾ:3]oqy{u^tyg='9"|}E fb2A^Yh]_A9G=3xJν5 >^s`~ ˿|#9hW?q={UU{,uQS:o˶ -yB%ɁIVz|cY qY?)bì}NtWsrUͼ}Fpɗ/Xpï$_Iv}>>G=( +|yƑYh62Zwiuk~oY.;iي|أ;?,-aRؾɧ~^|:Ͽ閡7ԗސ Woo;kn_֯صƁm#uQS(vMrKG?3fCbBe>Q|76oʚ~;jlNrl^}裚Ћ"'|=~9g+ ?]\m}sD{6 Trla}&y(֬tkVtK|o徰{a.nY"WI sUn}q{ݏ[{;{쿥^~zܴ|en]Sn^",),_a`N_+*dV7&r}\6x%ht}\'Tے?:wy+̵yeWY'ScF!dah1f 6FQ_dhŖW@Ay 3aB $2TR?)rjps?{=ds)1nIuv嫴9%֝&_:]wU~>ޛ 98OnsMiu_?K>\_=s?+7nM[6ڐ7laޘ!l' nzt☑sj|eF+xAXi#{y7]wsGymsTbl||{>}ɯO˾s<;ژk6n56gdž{ݰ);▤ UV#NZW`Uxι\tYVU'9l0ڸ_ {o~Rq_8Pg۰13r+.\9y풶%[jYq:G `>x]󜓞{CG^}vf;s`W58Gw_I-~=ɋt.Xе6~qy \~Ǒ;ny sg\vެV՜+uy1Z3IUUv=[%7Kqڧ^Χ]f,]96?'K?|cfU79"_ym_G\vٹ% 6sLU:ˡ'}rզ?2?<滟/VtϦk0e;r>L_s<_M~7knVa(ygԋ-mfo596`j|rnU>srf'9(Ճ~gG䎗;^~~twr˿n09 ӕrLݬ{Zͱ66Eo IDATۗx IȕGͅ?O']MeqM* ΃ j Vslk6lM[M:2gzbκ٭Ńu,5Cه{Cgk9Krϝ./??^u':Ʃ_UUkm)9{<1Zk%h)T繭i#η.uBoؽ@Zjq;~\ѦrٖMo7;7ltkΏ6ur~gok~mW6]vfckٴkg6]0v 캡]Kuwq:yəuK%_YSXMZUZs?J[rզپqK޸57mM[sƭj䵫6mͮ0R?,Xu]ܽ&]->>;]guugٺkG_>ڏLrß5 a*ɚW`K2e\9I~sܲ^B<9rTl+v8~aG?Y@O]qKμmfCX+.8w-.Wf/<6mU;ZU5X)36;I>:K[P9F}2=f\ѱaդ%}%_a)Z%OGqz\P}.qq8/չXW1spUdISlr?Xb+Lr~QŐ̸}1ɃGJXUHrHuI:ZkNlZPIrNK|V̤>(Yٳ <+Z%'IL0$9E.>lڕ -Ξ *JEN]cAPF! ק}ܧNC7OQfJrI '9%GqS;3VI^2~\U=sE:0&I``*+ z~0ڮ$:/︪z`t]mc^$y"}~Gڟje7`z+ L'6k{[&y}խK曤ZE ~1WsLգN~#s=u(W5`nNUkU6lǎ +'|`ksߨFbۿ1I6. ?|eF*?`NyI?10`<I$J_K+Uu^˓qW'y|k $2?II }MR< lq$9}qM\,>DZsl'g WfH~8r!0ZUUK$OyicsaǶΜpl?S䤑Mr$.dlӴ7G /( B$(31k<>Py/C'ySo]4ΫJ$䈽 oۿc͚0S+`}Zo L0Z$PUJrr[&ْ$_KrpeƵ#˪I|Qߢ?k%~'#7 L0SZfo:g?,|o`fbW> $7S&f7v0+c!Gq`&(3e Lb59 I`Rքajw\ևX0`"B~!-zϽx`|EN09 o Qi֗}nƵM9FNb,s|%X"MEg7XM|`:$0; M߂X钏@> zb g`uZrks? 9F]u a|VJ^ =6WQx!8 ]rVbgY80w` }&$EXgB ،Y,a̖\V``U<$Y c׫<$y_ a>y0 f  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =i˲eq798Xeq[fFIfDUuZ3f =Y _`'zаUAI?u~3 gNȞ[=293+!̖knKFk]>`X]+a\vֽMXUoYPUݮsZk6X`rlY#0X+$a\5[@0@O(0@O(0@O(0@O(0@O(ĦY0~ Lf5z srZu -zB'zB'zB'zB'zB'zB'zbӬ說T#lIri'9v,cnIr$MrHIӭsgBUUc1\$Ϫ$Nr$7Iru2x`sgrX{+p QY$G%$wː+lKK5Uu &OIr^k1sX%9={$/i`5C<*I$7guIޓUp㒼83(Dt$KW2z&WtU'``jkJErqp7{'yn(G,0| *UuJgq_'yzkmJը^i{Qc!ih>>췈-˒IM!_ِ_ TUmHw$J$w^;,'!H,bUu$͍S.K$$on7}sr^PٙO,\2aU=.[s=wtfy3 |fCN)$Y[kǷI~.wFmUut„5qhu%w ik2xПw$W,J]IQNJrhS|sOem ?YKIZ;vQInßUU?:&W鐯tU[@3SUfg2I>Aۋ[k/XFTճ$Wd &%eoVU/M‘o%Ok킽$8uyZk.g~Xs,A֮^` ^~ZkoIđ&fgIr ^U`+0rK$xS{GU=0?t]ArD k[[|)ÂJU$?LkCSI>~#/mRժKyfpaSI>ڼϘ;dEoHr9f1'tJk//z#WlU/[@31|/wo$&nc֜9vS s1- jI~֢OZkYʂI^~"}J - &I-b.uI'_ِ_ +a#s3bUq, 7`ncֶ8N{%>ҾGUc M  :.ѱ)UbȞۢ%sNqs=[ $H$g-fZwl U`U8``Vi/Ca.H푮-In?C#o־촻`=}='{|vs,\ \`SfvWx|w||xfai\-=[/  L]UNKxJƏ: !W5K%WXYY$-_QDBwxn!ɡ+ְzhnm yX*6W`L* Usl_xu585~un;Z:cںs\& \?Y~(f琘tna jCIrHIWzv kkVH/ ,tc縶w㺅xytZ[3Vzvu3M]X9 @(e r ,V9nXu8jC=kkV@? ,\iw%ov V~!ɫ:ے` Yu87]{&.,\Y~ۯjsսƺ༆׸֥zx7%}{GNp׷|`OrX+ rRf$"7'9|e}ъ"ҽƎ\?dH{W ת$oϞ0[k;x^΋x=`e$Gt/[k4˽>{n w*6W`/*00]Iu_7I;uE7:`/6WU5N7lvH|[c |#~'TUyYIN_1&^%_9U'``8vhO{\ULN[k5EIoUu<0Fڗ'ycY?F/K򳭵sWg1aK7UM[%yUG`q#__Z[xT ${ڐ$Nrȸ ܡv4bծd9^:)+F?H{93M[tIՆ,$33:x~k͂ RUɍ$}v*ɗ7$j}tdܭQ񭵷/#6uE%_铫_ L LHK;3H.Y<w^:jΜxFTշӼv/?%٧eIztH=SUхy|B$nGzq$9<{u_3'@U`+0}rL 3jc%7^PaZPh}<Òg祃3fˍSޒ-x6ɓ2(2:,ݒ/"oL>:~$_ْ ̵֮i=1c|qۓ6C`rZkNr Aj_HړZkN%8X'ZkoM,X^7Џ'9<K#WI%W=[@y!''e-l$g֮elUվIv|~G%tk훳 ֋I$91ɁI`{3[kߛel}"WG%W `4@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0UUՆ?eĪveHr$Zڝ[kG'9(^d~Vn\$wjZGkv~FyM1 =YO [H'[kmMkYx9)sZkN7*` >%GKU*ɽ;oki +ɥI=J$UUu|'fpgnZ{lks#[kW>ZI+RU7ONK[knE{dpgIRtڮ$9ҵO_\̱UUw>{c苽mSX﴿w7Mݭ^ =P;OY]2"Hͪz]U;8zN>k=vbxjFGZk&c{ސw>woؑ[k`-|},%=T o2zr{ۄV$to^II C3$[<^1wLs)Wtڋx\y9O/3搖;yO:?@/N{3X鴻W} ޕ'0ϩ^e$wkݥ;kmo]ZAk[kKr\$/^ƼQ Շi?^[UOiw{{,u$|(PUs=S`_FaifQ0ZK;$yV?d/<,;ZW{8MVøyGNؑg;Sv vXotG/ xr7lvvk_=IU?O<;æܮ`9/uS+~ㄦYh tO4:3p0௵^ZoG ><]_OʞoB6zAwI>=~^Z,z{-{/pIF;eܓмm Uhʄ;g3 IZkHN}H_IrHUu?msO›\=~zT=2(Vڹ ` ?jdj}$Gڷcs`Uđ+盖Vjuwt-)`͖{ȣsߚ䩋9vsaZk%yH}I_ȟMrNŀ[k5^i].ûZ۱VUgQ&4.4/6M;fCn-+bV8I~bsOŸu`.IM/vHk?nZgf+i:l$y&1Ii?=ɔ Ӻy!/HrN3P%Uo,f|%yn+ I>1>(ɇs -U$v?VO%Hׯ$9pV{j'ɧFMWUyU%b#ݟhBsUG2>~pqVy\UVUX1IN;[k+{F(WwL&^UKUTU>k5UY ɣ>6gUz|U$דe}2{K'v$0~:;ySU[.yÛ(ɥI=1`օڷ2vx`D1uڶXEuܰ-aMI~},b骪;%鑮Oּ 0aUm,^vջ}aUjN539)佥[U7oPUTU7 :/ɅxUuj_^(0yH$TUN$HrYInʏLrPw)ƚ$Cmؼi{'$w3aki)lNb|"ɓz1ŵխ>)$>X!`-q$In$.b7Kf^]H'+g|6YLXUa1<$9坾%y_3 N{˜Vj:`꣙c|ΩӒ18^ydk흳`1 kPkYa0zʞcdnj֦-IivX `4@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@O(0@OluTU%HIv(`mڒ䨑Zk*PIΘu@<2;gbN6sc SUuh{'9!I'9'ə26`29qGv6rĬbXs.|Y0IIyyѾ%}_rb`U:!{>j \$/N̽#ΪzOi}y ^oj9$s$52[kVbxk'tk;yY1|r1`i۬cú]M.E8NU1׸ժ~#+2hcӒ{>ìZwΓ$Zݟ[BGK*y^H7Lt=߸:L$?I}ܸ}Rk_>\sVn\ouI;յNӒܤ\BsX'3OLrqݑj$e0?8ŒA9VUO\fw:Iob V?'yK.ɱUzrt ^I$yce/Inw$$$?yE=WxHF{U_lcr!I.`m]o>e뭅WdK$%9!&9NgT>PNJ7.m:.?N"vt`f՝3;M;Ew䀌._z @$UF;dְ}VFGa_2&9r$KcXu?ZSzkIv$I$'WճrH/JGe^̪]\ 5<<&ڟnm?HUݟN򬉮MI^U;SqA%7-OWIm| SUI܉tU$`f틧PÃ'/fRw"We{^ONN$ݒ#Mwp 8[-ݕ{$h#nII^-?,Iq'kr{N)I̮LB~wx*l}iF~gLt߿eIn<$Q-q/&$wߺݧuO\7LgkPײUÓe쌎~^=:0ڿWVA=]_Sv4OYId$ ` wnZ]XoźEQF^zZU]a)뮪K҉vqӪ& I}J4 UªGU]yJƠ%~ 5M򒉮u, s w[w}aF~5ѽoM-"ɕ$X lF,{t)P3U*>J9KݝWU$e~z@-uSUI;!03k#<0W24ɓ|>XU ݼ;6eOupk.qTzAzSbse~9J``fZoI_)L򥪺 ={e kX &]U]g IA% jYm LMzϪꅾ|jƵwua`v}bw=7(`I]U ml)j,/4^3Uu$JyK`d`Y`ixe*UvDi3[@I~+ 7'7[dtrٽGUտu׻N1IKUu>yUu`NtbFqA{x$4DINA1[IQ0G 9~оr_UGq׮掏~r?ubG'Iw*ɛ&-`VǒwaI?Zq+PUWe4j.uNUjuaw*q4\fq~zUy/Ir>{UT՝2Z`!f&UsA3<~$MJ򉪺VjTUG'qW+2l{/ DɣDkXCINJUռGWզޫLtгꘪꉯg.0IUu<~თ^?^:r47<-UIޗ}䠪)2:./ѽ]UH\vI>_UKINNHrd}4!Ik)'ݯo|w\{UuLwV QO;I>VU/ G'9tb%Y=; Iޙ1͎Ec\p3<{1p+/=hߠ23}b Yo?~$e^'>uVo'y;gtgނ9O XkG%DvnUwy㮝nMTՓh$_Ӓ8IgyW_I˪Qs2 &NIÇIްȱGs$'-r0͎fvva{A?N'S1$w3[ZUI܉_%"0On90kV:_ Orl^M5InVIՇܤskY$K$?]%yUCի]SS7%a _Oܣ?Mm?x IDATx[;!}i%9}VJNr$JrV]In%=/࿙-]I{v M.S=`Uu\/;uJ)? g$zw LSU(漱:z'Uu$ߘkWd>SJ`t YӮ`MU?$ft $}$7oN&CF;lw'9ϜB=WHrN/0wJ$ggt׻{1A10wCfv{qA S8i1=o{q4l0Us?t?ZؾaUR|NFUrweK`F a_UW֠ګ>ɓ/lJ`~I^U$'$NFI~3I`wyk`;&K$EW9tc`X{.qyI.3[stCWTՖNNr* :#Ym%9  y3|51IgNڙiq٧MRUp5Ó_2;*GJAlTڛUwO`꘬>p\wUu$vL9?"`uaӮgip40Sjl~<)I.R9iSkMsU Rf#f`Ff`Ff`Ff`Ff`Ff`Ff`Ff`Ff`Ff`Ff`Ff.`5U^I:%ES*>mJr}δX 00kMi̔#E,#fl7*In$'9/gM69eq>5j`sf}G%wHrJ`c:8_5wȪ\?ɭ_Ir$L ~*=&I^KCIτ5M6v{lӪ 矓>|'gL 6Q&92-N|Lf'=qwQl4`$w^U=K?NrZ7ѿs]aCo{<,yXU.ɋ$Ewݷ*ɱqGWխWX$@J\%cWK2:yܽ49nI>4ѽx>P R<;ɟw)?y٪$:+û{;zX3 ䷪U+ 6803nI$O>gy%CC$Nr\wjM jv =b~Ew }zU2]ɓa8ZUVU=qU$K2#Iޛ䴪z8GU(O3II>9E7Y1K8R/WU}k♗T5UZU[x&$Uk_qWզW03vO$/h|$O=8<1ɞ[OVV5aXL1}}w_ؑdwLDI]`3k*ۓu$_aynjTվ ;|~ߡVՑVOI.h?bXNW䦃g8փ%f<8ɽ߿- }aetGTo$yS2 ?d>o'~O澴vYfgI5u$w[cFVs>>,!K??\fYsOv&_QI0]Iޑ]&[w?2$'\I?†AKAWX ; ڧ,qpp=Y`f;<-ɥO<ڤ>?s݇/č?]-4w'NOU.ٷ%?u`f 5 ꞻxI^g| 7[tuۗWO|K,bΑI.?x5`pA%3K̬Zwwc1h/YhRw䤉[GTU-0gxkW$v%ο``f 5_\س/-sKs}גUu$7:v a<Lmоh9h_~Q0̚30U;4^5h?rc*cwz0̚wF߯<WU'g`v;hw/dyk,0]&y?7w@/qpXےb=1Ў`5>h_sz0q]a$Iw-]7ߜkT vU3;K?0c ,d,d^=h?r+'kָf0z0{-vRU_tW0{UgU $\E_o_`=Y`X['O|@U]yOԳwO$K23[תE$|׫ڋXU&D/A kKW]hBUYkV#Gfq`VuI>6~"}/ZyU}ݾUsFwbMZ ɛ&nN/uu{TվIlu` am}8ɩ[UUUuVUu$Iq׭{6^UJrDUn|jSFaU&]*ay,_Uc{$JrzUW]d1/L_SߪO'K'yJ`UnIle7 <$MNz䩔Uu$G'9tb%I0sƺ=U$kI k]2:[F1'"m5&y&wgI^4}X/ViI~3}<0 렻ѧa_Mr>{}*[w'`Mti60VoMxx6؁tIY>+IUUW⃒$Or|wuF#vI򰉮 yJ2:`r0juϦU RUWn9oTp40+$Q?__*0lꠌ._m |I5x>+ Ӄ;ɷ|/\I*IvK$'Bwg w$wH_+'eGwςf?WSO ǯԍ]4ɾ;~d&98_5჻:,ɧV}T$[rnU1ɳ%}$Inm[U''yUWw:<ӒvkϫPUoNG`\U2$|%9IE&t$ywU]nς`9uZ2$G ^9}Z?!I&1WHΪQwm9![|O|F=2S'otkoHY/38IIE 0{~6"TU2 ['=_8ݗ&yoU}.g8~IK5 uVy~U}0<~iCyd[[yӫI?~F<Kh1tmmpw[$7?V;xג}sv03nUUUU"VUw?T՟V՝yXÐ=w3$Mt1 ~ޟAb5~"$O+lm0,ZUVU=qU݌.tI^Iޛ䴪zqU6Ϛ{TՋ4ǓmF__d}ğ퍃.erU^U_NUUq-k`C]}&?Hr"n(h ;jwYOr*E3sj$I$ʰ+dtǪsNŌs+sIڪz٪t9I~?Ϫ/4L\v}<8qsB'ڇsvDזY|&02 :wZ/dt|%w((NU߹и3zlUjo I&wmUu:Of^0n:hv$'MtmJrUiY;gK|p-q>lY$$7ktIQΓQUM}j$Nwݷ\?K'vYfʎh'k%yW#<`$Kצ<FVꐪ{ 2h]ppJrAw1eaU5`̚}Bw?5bwݏJJ$wOrnu3'5- 70sGV 2I=7=Ƀk[%[IL$3?oU+k]ɧ,q-y:Nmwu|X0}c?3im>?s3{9{wlo\cݫ<GցIvYWջqJ\5Xs.Nr8u^jUUS ?:$606u]m a̚N  l Ld{SwT˓heؘ*$ǵpA Kqkf}i.0焌9{&bVU&}̿vWA] -c_ڗ_f-JIvJ*ʛ%yCU<3{Ys̚30UCRSaFl4k[I'[ OU|ӮpyGmۮ $Uu$~9?MrO&yȌ~PU]v}UolErWKA`+̚_Aw8C\+sT 1܈찴(s}'\qnI~Q{z'ĘGfMj?_aRw{i%6Utm3 kPU5v_`E2 c'w9ե&94[ޚs<ߔ-kjUd􉦡UֻVun˙j]51h"j$LrDE|RכtInmC?~vjl~u'rk^D L]:0k1UUpʠs3 -W/Jr2swj$oJrKw$w[Ϲ`wWH6y쳚DU'~uF0l.oU~0 9dj5I0IYb K|w'$A ];60O\mƟꗳlKrۉ7<6ɣ|wI~eBW;Y ڷ[Ī/Ɂ]'9q ~Yo\힃GR3K k+/rI\z䨉ܿ/_p$xUֳFٱRﲄ{6hj9I3~Jw"j$:;ɪDf&+.4a|֬%'ysFyLwϧ+< )[ja V w~b $gNJr"{b&UQI6~Aw`]+OMD-}ram}x~Ir5jP9ɾv%yD~Ii `UՃe/4X=䪺s;Nt";z^UqpknsN) pS''9VU;W]|&C?\*$wh0ocCh}xǭA]LT)~?pWwsz"$G7/kS˻O{`WߐыqIƮŌ}Fwn>}E%a{Q%Dߟ&:99m`I~wȖ|tS|$O]TU?#SA4(ʉIDATg·3^ 렻Ia_Mr>jʿ_(^&9f}pW,`v8m2 B 7׺]탗.$7˖Yw~Z׷ 6%^F~ɖ2}Dw_εZwdIg6L}fg.sa˘$C2oF;$^7V8$wZrX;3 Kr$_`{Hc8zweT|8gm{rߛQ~${ocyIޕE(Yw+ɷ]a)IRUhAOgFIN$ VVXr=ISUkyF]+]Ǐ~doGKh0K8{I7:Vb|_l`!PU5X3} :I^'Ik.3``ll\|iӪ`CNi:<%}jߩFuiQPukX5Uu$vL9?"3B 0# ̔+ɡ]$hJۧMI5>ϙV1K!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`ߞ WL`  0!&0`BL`  0!&0`BL`  04g mPIENDB`symfit-0.5.4/docs/_static/interactive_guess_vector_2D.png000066400000000000000000001007471412237106600235640ustar00rootroot00000000000000PNG  IHDR"ӎ>sRGBgAMA a pHYsod|IDATx^x#i@x)҂ H.QTDE)bC@ R 6, (pTpE@ *J H{ٳg9eΞv9?;;Q͝HHHHHHHAIL04 @Ѐ A2  04,ۻw /|_,E1,bJlkf7o׳xڵ%Kꫴ[Ib;tf岥}e>s9y왙\9B;@-4&=uȊgoqb` S9ӴM;Иص;K" ;s6md]o^q!<0퉓y)wx3sǻ{_?b|- mtlA͓ w=55k׮%!ktxgKOQh7YdCOSgy2 B&4O &_|w&ܰ;m[Wzq /u)apR0^(E2gkD/,ܺ:hѺŋ"vʁv[ae/wnQ{m< 2}9g2 ΁½ȯpοdx5-_~|UWS^d:.yTL˰۪a3]Ua"v{A V &p2/J>z] :yö-|ƒ}/pQ:B{Ľz=K#3d{AL7D-a>EKd\\zušU?x]t즧hXږac!aj6C,q<6pҸuc2d&՞Ki d*lS(+a d<2+F!_Cd⥋?uaqfFƪɃ=erT& FףGܘ$Ls\2 A|/9]%-|1Kxo+~vZ̒)+@ؑ $FZdIjjk׸6(KF.sAi6֌0 s~!mPV~I 8 2 da h@da h@da h@da h@da h@da h@da h@da h@da h@da h@d222ohѢvۚ5kD0:\xqҥK_tI 7%Kܺu ֭Kc2g}VR%Ɖ/8w 70yd|d۹sgD,mM#G}7"oիWkժ5j(˖-_E>7'3_ݥK7n1jjj}G_BZ~'nTci<@UXl۶mԩɳ}QF3d^vMs1csGyij瞣<օ 6⮏:Bu{饗4^i˖-bW>Hav0hMO{5Lxj.]|I}%N:% rO>2 ڵkGZKf"ķ~p‡~X#HaE:t 2.A{_ ֤Fw[čnUnDϛٶ aƍ~zrto\XsЭy}3aE{]: y&Bw[S>V͛7SoCde?x< w*oҥK|tm۶Z3Oa%_7AlMɳ&+7qL~?W@Ŀ@WiIÇ|^;czfV|B: Zޒ&<=h?(QB*=Ȱtڵk{=;57m㏏96OPgiӦg3ٷo /"R d6{oBj֬dadؿ|*T 4}ܸorǎeb!ܟƝ|bz |X Wb5#yk da h@d(YYY:wy\Է$GbpaA 9pBmQ6ћ@2 h,@ri, G_~6W y\H\2 th,@r࢐aCEc  ( (dEX8E!H<.$. @:qQ4 9pQ0!qBy\H\2 th,@r࢐aCEc  ( (dEX8E!H<.$. @:qQ4 9pQ0!9d8@d#>=H<.f栐H<.enP!pH$GEcrҤBΙ=Y@`vMYBk/mǪZC+V[a͊+TΝy7|4촔7 V?cG(d0r,PDd1U]xȑ#ϟOGݻpDCcNKٴIb{Ϛ {(pBL*meٳ"o4W촔˗xk.l4d8Ad~޽;Qp?L]st WVB s?, AcfK:Tr࢐a|̊b&QivQ:ʛ ڵkԩڵk 7ZJ&##4СCh,Oi)˗ŋ2z8g2Cg<2C;v$%%yԠCW_44I=ˆTַ@Cl@f k3fL||ao瞖tZJ^F %-MX !~6 3f % w w9-K[ʼy@eZaq {xQ5/ZrѴ-.I&Ɓ>|^4p1ydR={l߾|AcwR3鷔GҥY’\2 _87Ի7-,{&;8qb5 ,Xd;sѢEn  -Ʒl(鴔l}{VvJf0 /OA*Z׮EXo1z-eTV$' (dغ9(d8er&LڷOceo-,qB? G2Rr.uteQY3Vde cn࢐a$ y֪םedzE࢐a$V-0p45 ME:e͊g=࢐adI;a0'H/[&vNFR>+ԉ͔\2 LjAcu+_z+8!,^pB#(2 |VZ%n#x. Pγnރ `0-'*N&gV= 34'\9y\ q-ώNKSj`ez ]AmΜ9s[nm߾}*U.^ȋWrKn޼nذ5U ~d!;VTԚ~!CL6-::z^^}+D ͛AEcybgvt~Ya\42&55UUϝ;G":g^tȑ ㏴xb& /={6}>lذxn'۴iS" [YH*RaɅ<.!JŋEٲe’A{7L˗/3g"AF^{}hϟ7o>x`n',Xu!##~'ΡCYDAd! D0œvvSJ `ap`pvvvǎNꫯbbb6u< mӇC{~eժUkرHY~G|IIIdWd!" ,J:.]XYݺ˜?g}jժyX2ܪU}p6m=۵q㸑_'91a?(yVMD2 x?;z,Vl$,.48p7޸~Ei5,hѶ6z@Oˢ3;AxqV ATTiϞ=Oњ;w.=zT3E}yΜ9)Zuv_~u9A7:EX._[}VҲ%+k='8E /^ʕ+p9&( -[y{GR˖-N恥{.%%ex`  oHu nqQ4 E+g )w pA+0sL^NZTB uk}]΁ڷoOv*}222DkFTV wa|]N'zy\h܅$>ΝJw8E Ar:KznqQ4.:ߑrEiܘic 8E!ٮ4|ȍ<.x N:Ӳ$VV$࢐al£@.oyy\1w/ZaޓqBFVbҶ-;&nqQ4?-܅tISu*,~.  _⪯_U4u yu23܅4T>-,~.  \ƍ,H(y<.~ǭDE:wa~I~pQ0Ab$:tY( }5~BI.̙3Jʬgo8E!`qjDU!Ye݅o+UKypBYYJ,T[',*qQ4`|YbY m  ogpa%3SXTh,a5V=* ^}UX\2 @0#Zh,|Vfg+Y. }̎vӭVGlnqQ4%:3>*;vq ^l %4֙b \X\4ed_<JQ.5j/YlG6lX||(J߾}6m*2q@I:W}ӕofe= \4ef6a> 9v-P@9wNX<Y dyEY`ATTJ222Os:pq'9?ʗWNB =F9sĠ3JƜ9FDVۑ@kժ5vXQ5kGyIIIT&c0qh:7WX"-?,L_S+1j#7}3da;Q2vȫh8<1s*UXY> 9CE[˖ / _cxBG8`XkV? WW.\` ce>^E*ZN*,z؎,2,i&p9~\)Sh̍vV!Y/X,+Vݛobb"߇XreFbbbU;Ac\:7\YYJF0hE̓*Ta8(r͝+^'8 gG,ZI|+OhM Owy"bIE+S)Rh0l-Z0:UWgv"y"<5xZOѤ|J 3Lp^c.ս( gE;Dyj|,JT3;FTFE-˖1\(*yH boD;Dyj|8XvTg"pK;_ɱexAsqʘTqy"<5xZ%bvaǎ Z-w0ߚ>U>G   , ˋV/ʖsvaq ܫgOW :xG2scYC,rWˋV.X ,2 {^n_ [w>Y ?}w.(7Ċ] H\4e%1O/_yғ'*Od& xXQ'*KӇWbz {&[!> ϝvm@dB"OM@~vcŝte?H^ pQȰ\GD9 {T4F"OM@~0J/Z/ϊ^X. vM@ W+[Vr/評gudzcy"<5l塇X7+(%pQȰdf* 3cr 'zt9v@} *WVΜ. V'E1ƱL{S;'NYY 0:oGQtpQȰ={/İ8irR({&a1\vHa8>ʝxRP(dI" @٧7|`/L2Zj WV3gΤӪIOOe^WCoPOfXYb3`\2l 0r78p?khYseΜ9ӦMKII2dH\\܁DY$Ŋ;BxFMo16h%1~<++ZToaq.pQȰ!))̛" hQ aUlD&MOd%>>~"pE6j|˲eBquδ-[hV6s8(dؐl6Ez"a@aV#Kfffdd DMܢE ɁdvRJʕ۷oyfQ5e|rett~}Va2. ΋}O&S'V#ˑ#Gh5kֈ<ڵkL֭/nݺzΝ;*ThϞ=LEFFiΡC 4&O"^z+8!,N2zXپu͚" nhI~G"?F.kU$3N:"GVVVÆ  $*ljB:ƅ(HGWXm,΋ č{8 jꨧqOaV#ɋ~vډ enoL˖ݮCQj'%< f" ?t1"olD&M_dJu=hnܸO}zJJСCv=ҫW/?eਨ 6"oب ' 3I;A;#D. 60/TB6y/v]\X2eʔU$$$Z{ͷITB;-[M6{ްW` 3;\=_"OMiYH)WBѩS'VV~x࢐as.='Aw4,, 1r)lۅŧY䩉hevҥ +[W|Y (d:MO͇4qPgz ~7䧧6,1Z[5DE)6 . Θ1It( hP5Dyj XAxqVT8E!ٺ5Bpu(tu2uo˔a{y"<5qՉ(io ge)-[&Mتm(d:,TSXHX@c:,AOY%K*W'SPteʨӿ{8E!öߟ~D&ԑQ?zKl=DYIc|Dʝ;Y1^(dJVC⣵Uaܹ",$Ա<+qcV֦ `\2l hQB֯&~hr#aDyjP$VP$4. K׮ . L௝?'SEsCnНaɊg(d.<Nj,\vsw:y"<5 QH5IVKJڬ[7aqBr2IMegx}{fhB h@jx܁,$NуJӧx. ڱ2~GWoHdSRRh9׭[G]vG}Dμh4&6[<[E ¬={,#`5'DY䩉S)ؼ/`D2Rv3fc=FӧO'Feg̘Azݸ$y=,lZKLԥ bFde)+_a?Dyj"9Խs;gY2=]|<`5k({%Fc֪U97nmnݚvF]Vs2NCٌ[2(R%.tD}1Ø5k1EE)Id&2C꫹|LtyyVPr[8EG,Omۖ6Hk׮͍5kwSdgFtu։|IIIdWcg3~z<{Y .f Gـ Odq@7k-IP xũ8E%a^hui0y#;[V:orNI"<5k-;wNR.8E%a>Ekĉli֕J? ,@^￳  RDyj"!ޮE J{nR҄hpd8--m &MD\IDIz,X裏jXjР:rsΕ/_vCbŊ%U+~/v ߁$w4+l@!Od&b]tfGPȪ\482b b4&mѢ)+ߟ8}t= ͛7C_x(L٨-:X/`b.Hի@!Od&b|W=q×_obgE_jx~Ɠ- O^dw. )fy0u1HI׮"@,D*4IZ>(۶ pQȰWb-D %g?7ςL! $Dyj" W\Ϥ?"EX[o pQȰe4aھ(W QO\tʔ)ժUMHHXzf޼yu֍,X gcq t1wʚ77pBv~ø.-񬍀1Hm9sM2dȐ8􁚵kFFF7nΝoTT{vo䧱8%wL:V42 .  [nGd ~]:H(E\+[XX mҤI~DFQ.29<#ڵEi۶mnD #6ny?`τ(da*ede1M|`]jha̲eR+W%XuL/2| 6=;ڎpvx hE~˛8.ۋjl7'aܱD_.%\XpBMgpXVֲȂp^lE6p&M_dnݺS^qӎ)Zf0,ZIʨs [W}J#LHUOE ]._foEX ,M>=%%eСqqq7{5kРy„ ;wa-ZI+ ˻\2ljԾLz Κ"`p^dlwժl{SLZjLLLBBªU11177ԩS;>>~~_G˰ϭIQ:TvG^X?qBkȂ -<>l'SCi~.;="Aa Gi;W ldl^{MdA|WO{ﱝ5Y'S:;Qj]i/,@࢐aj%Ym3&eŕ*፤>h8;[iЀD'Sb9:6iZb/ R\uZ+d}', Xf_lY䩉!ct+(^\9xPX8E!>6uӿF},.EyqcdY䩉u5\lڤDEw;  M;\gP8 , hp"/jvH࢐a_⭝SŞ3Y+WD-P4`Io:옏?'SC]4&(˖ŋH\2K9 222e lAz6V?Q.\'SmqYOX|8E!þĸSuD֭E%ot;YI>!ǰh%ѫ+QCIK pQȰ/1;&K E,|ɞ;&n*,R"Od&As C;p汲8pQȰ/5.￟}p1cN."Od&񼅡NTDI{;QtiV픔֯_ϋ֭[G]v7QL=H9rD!≵K j~/ !Z駟n۶-m ׮]95k7'a2\TKL2o<.9cΝ;' b o}5wD)͋,1qY39 ;^xaʕ'APRhѿ]kO0wɏ>恥 sq-HkڽȆ+. ]gCsڹ%5!CDo,0nYt&6+c4.jd?]RzhǎΗ1qlll-H8}t=H ڐg5yLry3++P@ٻWXcǔNKKaEߚ!.SLҥof;}5u+)tİS8䜐#(?=Ȇ%.jO`[rQ L:uҥԋ={vjZj% r LB>0yr.$m_-3SiؐoCPo |N7du_5:Z9xPX C<+.v]HXBzꢛ7^@ͦM]UhXӚ4I6b++]]/NSN5d8Et宻XA~"NWtni>׸qHw4.zɝ^HOO@vvvtt9sD Al,P{q{&&bvtQK@ )n/GU/,ၷkYSѴ…XrQ{ $''n nc[ r'ZZR+KXSE4%ꐡIfnLO7U[o ;V]SW}=zƍעE5jt-4E4yɇL<B ]*@c9$rt,Q<9~\,XuQo yVhѢTR1115j'|EP F"OM6r#y"<51EIuȺtaeu*/ #pm AݘB ]zAu4(:V_˃!xP)^'"Oc d8hdfk vSpo]~;l#Od&ƓtRZdeݦ\*Xl9"Vaq$|@I486MY+P@IIG"CM-ZI+(T(|2[i,  k<&q9.qK=YoY^oHx;Ż=L\i,  a"-M;O>p@s sq`Մ~tˋV^ԮʺuXl>t衇XAθeB-Ԣ#AW5YAJc2,(P!fKa Y(ڒЪ?:ۣ@E@ |MNߔ.Aia<6a)6GiXfP!FaۈGtfMߠA8q+xҿFrR2+~Yaa<6a)0&22ZXA(ղY+:ݱhkbtʨQ\(, l2,|{-Nh㏬,22t|Hi.%&E\cY;Q[X^X@8!Oc dX-1]%D"<+hؐWjP0CZâސE}[;~,Amr#Oc d84e|/e2Yk`SUx7%Pu3g1,E}[<:8ge6a@<6!I8T H/\F?C=ρ33*TPKy\Է56OHV[@ni,  :ri溯"^~x4ظzkBE+Ex ޑ2y)JL\RmA.5\ax;rڵTΎpA^k=i, 6yN)G+defP:4%\|/VfzC][R$O?ʪV?,,"Oc d8(Mbw~%6m߁33Q'>-Ա#򸨽ptg"չL,i@X!Oc d8JL4v5-X3`yL)F1t#I4^`0Iܬ{?c"5!3L"…} C5y>rk֬yE;vlڵE&9tPlfCщq*qᑈ9s#\pL`ʌ$9m SsR{yEӧO6mDFb<;Z;ĝ?,B!2VӘ1cԩ#29$%%njBg3I~.PSkDL'G 6Ӧ]DqӖmQ׏эيh"H_{fdѣ"ϖxm۶"^c1hefWMm4% y6yQ: G=2Lc j=yN/EiufGŦKb˺`rNpiҤIEFQ֭Sᓑ1%;|sQ3S&Nȳ}8EEgvuT\aHd?4}CߢLl#c+rtp`زeѣ)BDZZ/SZф Hz)裏%<]r=Xm,&LRj՘UV <%Dh޽i5+r66_ı-Z 1vDsEɳϲʕ}kXuQ q [?oqKe,Az<.j&nwti,  򸨏krxA@2 t>I׮LaXl@:qQ_kדőo Fb0!&+%J0~uaXl@:qQ$;[iӆipʕ+/2 t㢾ɇ2 .X>Gb0!&v)\oz}awXl@:qUIZW ki,  h~k2z4%C"Oc dEU(&_}%,ym H<.j&/+Lt넁2 tk2dS? Oc dEmd2/<6aCSg<6aCS=׬^2 t㢖k7L P֮<6aCVGҥ !,ym H<.j&}1 nHF<6aCPO?el.,ym H<.j&)qqLyGX4ۄ ;w~C/s ˙34XxYg #Abpa 9e  dh, GʢߌzO7e_w@I$G.<5PmHg&e6_ҿ"8O#9p8( a 8/O#9p@FFFRR+8( a h@da h@2eʔjժ&$$^ZXªU:tPbňoVXĸq7n\Hevi׮]4-%@M1gΜiӦ 2$..,^xȑw mv̙۷oߺukTrEQ| KHx æhҤI~DFQ.2©2&55>&iD4ǀ y`WhBdE8޽{c&''<h,N-%0@ȑ#k֬yE;vlڵEY8>dggw;ySXZJ ,k׮yE3fL:uDY8^}٪U 巓 c@K u60poq"| 3@K $aS4iҤ"(uŬ";;{*Uڳg0ҠȰ)3ӧOOII:th\\-AZZY&MD{Ȅx+W<˗E)h,! ZJ eʔ)UVIHHp +VPLQӻwoQħR1sLQ| K"> da h@da h@da h@OVV֡CΝ;w )<@ 2lr5 *(<@ 2|+W&$$V^?VPr g:uji֎_K)0RxҐ޽{ׯ_?22SNªǘ1cnB /^\r(Qu[lv'.a .,!IF&Y;vlٲcǎwumݰaC:u>̳ѣG)RF;wߟB{I:Rv2bŊ_~g駥죏>+W6j(&&Da$M Z #IpժUi5N/-/uC;~YfK,y=[ o'p 0&-AI.J3a@4(h 2$M [2l)DG5I֔d>x7,2lo4PH 2  04 @Ѐ A2  04 @Ѐ  <7lp. "2yA&£ / ù$#oÚHAO5$IYSz߾}<@2e-ڥKǏE9sLϞ=gϊEٶm[- ,XRѣGggg2 K2|oXB#]a IDJL£xM7:u׿徸ݮ]uA:tvC˗֭[rrI~m^*@sy0$ 2$O-K,)P@y.]J)))~z^n:ڵ?ŋgddӘ8Đ\exԫ]0$ 2$O- .t ˗Ih{餵Ρ3hW^?77o~ ù2}8rvZ ù 7l Lށ #I H$2L,Y䦛nhXܳgτdv|N͚5ǏO$\9n: ù СCJ0$ 2$Oʏ sN<'B/_ޢ \#EݻwWXq0$ 2$Oʿ s/_Nb>Or_\~=eSJ(ɋ&L)Z{*Tހ #I H0 p׭[o߾/TR?(p=ԠA*%nKΝA>`bŊၥ-ʕ+ a$IdI~ISkժ;OѣGQYy汱4vzױ|GF$AI(>@F$AIa@5$IXSI5Ȱ}%oX2d>x0hH6da h@da h@da h@da h@da h@da h>F.#d_ӿkzf v5,aOhe?Ew;1_'_0/L>gΝ+r %20m:&s{ iC vڕ+WD;t*|]Si! Zg 5uŋof>}oaJOOOMM}N8qea3[}fP3H'+W&LzX:իW׮]p3gwg 5t4^8 S;v7m۶իY&ۦMm۶Qσ/LjlȰZ9JL2P0aB*UĹrs73[?}fP5y%J*Tp􋻉#ްa9sxv2TBd4xرDFFRVy`K,)U? 7Э[7c%v6XÇ5Ӿ}cǎrCN:SOQOBEzꕚ*Qapf޽{@+7ny>3m1htۻwo1JLLEڵ @0n/[,W4JLBؔa0Kٳg?N򖞞wЩ-stBcYGn̟wsLjɓ'o6ֻ@ ,_ڵkNS~( <o$jĈ|ȑ#n8"ttW ~GN>=i$~N>}T (Lja1/^<99yʔ)⯺x'Ye锥!w}WbEO%Si!UZl@ʕ+#3m*ä4;w~wipBh;N,XPH~ի>iii_~%$TUQ/r5ի׺uk?7nLG͚5;t@>3m1)۶m_u3fPvܸq= *hN &W^5kVժUy,ZQ{-pѣŮ.T_7 ꫯ(X<0sf7{Fĩ](̴ƪ S?JfpSy~/{ ʕ[hk_-?# ߍ4U3MM4iѢE;d4mv=hߣGgϪLj|+WҥKBBXa҃ &PجY~8}:EE~_AZ{glѩJTR-[v"EF>mFK.ѐn {Mmup'AZz˰3ӆ]Yti%ĹlAry~?_Ei.\oĉ j H#Lj,ɰI UǏOw9yf͚XIjƍ׳ghѣGՁR}f8/:[o=9V駟ZԩS 3ӆ]9~x׮]Ih>5"QFׯSy~(*^|EP +w39E+^ T1uǫA42 m$l|oka*W:uHBxvzBK/4xE.\:P!5}ꩧR!!LN[lY2iμ`ի0tZ[DӦM̴XW;֥K? f/+~ "믿֭S߂ZEP4l߾=ߓFdɒ%6X_|Ae|?-Ѯ];~xI޺u޽{K0y-pd009s*V(°!1113Ǝb>]%~[%3H۷ONH F̴XWtNLn$ToifJS熌1T7nS~ν|GժUiyHH2kԨwNyBj6ln&o(}fPcRm駟Rw w={={2 p^eXIO]>oK@NeI|I'$$lڴI<Ϭ_7!O &g 5y0qO>]]x{nF{VLP$Cu_.X`-^y &1⮻r/fIPg{;sdeeJchogHV۶mˏ"H©AMzz:I)}fP5b,~N [)рX܉(%% ځ.SYՕӧOw1^ҙ#&Lسg0y}fPcC Rb5RϋNe҅>Xbٲe FÞ_kҏ^iCM_}3T7_>׶mTBe8?Щ|+̴ƞ OVU/TPW\a7*ȇg>3muN Зa>g tŇ버B+|"SzfMp. %р .ėB\2 @a$$$$$$ 7w?ԥ#2IENDB`symfit-0.5.4/docs/_static/linear_model_fit.png000066400000000000000000001001241412237106600214130ustar00rootroot00000000000000PNG  IHDR/5ysBIT|d pHYsaa?i IDATxy|}l4#@ b161v6qsں99揜>8/9M4M w``!͌e!,K,ѭܿ%a9kkAx` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` @x` Tߵl2M2Eg]{4g;:= 3hƌ*++=N5uTM>]?ڧȒ*|ַիW??Dc-~̯~+Kzgj*\.=c BcymذA/^z%?^?xs?Pee~藿mۦ'|W`a$D ={^z%M6Ma\=H5{lXB-RSSf͚gZ`$R-ʕ+UZZcǎiZv&L IO~W7nT^^^3`VI5uMMMlIRuu|>f͚?&##Cڱc$i޽D"={v#G@;w$رCYYY"If͒jծ]}Q҆X,^SNѣ%I$Xp(##1>/~Lnnnvnw=cOt3ѣWFRμ<ڸq~)???^x/gG~|6*NG"544tpm*g}V~~ߪEEEzڲeKܬݻwkɒ'nkc*++USS2IRYYo߾1}bJKKW$ec<֭[㒕%)ŢG}T/T\\B %IЊ+v'*++ӤI$IFRyy|I= zxk^ibMhog߱٬r1ION{rbܓűǍIǏ/eVcŊZdI/UVQӦMSO=PH+Vкu T^^zJ'~LCC}Y_^VU .O0iWD1p>vg|}[/@qح*' z)^)^)^)^)^I9P5'fkl it|$iL=t(e W Ϋǃ$?ۤ77H\C&$iR?R]`8p/H*VEV?/H*.]%9]Gz?}^{(}jihL[C ($Ⱙt7m ae.֮R&^w/?v6p^~RWx-Hy+mr8l n )tQ[5[vK)kXY7^65ɿuկHF%ITܳH9w/LpFPHkk(lʾcr@̬68^0b15nTW_Q.gL. 'Ax.0 ۫5>ƌw#rF*VR}Z!V<ɲX, .9^}VMmln<,{N,6=N )Ң[odD"$ө܋+&C^b?Puo*QZ.Cv'A^XLM[?zFTyU(ev^tZW RGV^#rp%$S]J{k|yZ)SYA#@c>YT--Ozۺ!@@W_VO%Ð$23y`wbc0Jkm7oTs[XkhF} ն"p$[*'٫/IVսWF=YRR`r.s^?\. FbvV :߬߿XpTї|&7kn(Z,ʚS.Ke" 6m{Vvy5!p9z:_.\~z2\$)fzgI/Αf5 C۾ J]77`ruMAj uSuTn d$ftGh jP]z` b'k#WƗ\w?/&xp(Ծkk˾u׮QM[j(7ө@SSaʝ ֜ojy鵼y>Tie^L /_|~_k߾}s͟??Cꫝ\ b [ B*//SO=%zaYV-X@?񏕖o$`uiC5u ț+WV7VJmr\LQN_U'+YA'͡K֦UTTﲎbܹsӟ4^KIymܸQ/222s1?FpX?яO 4|pZyݯտ.8ң̴8RHЄ9*~m;6y[u##,/PdK3θ /sܹs/ar8fQIk׮~3͘1CRGYhvڥR;vL6mڵk5aIO<~M\Mӱxmڸ< xp_{2"5|hӅj(k,Y*GnT+X,mݺUgVVVfΜΖ$ݻWHDgώȑ#UPP;wT;vPVVVOynvǏTuIј$iWV Y~ڎQ횕j?v4^s&G~˄+|%"W,Z(cƌѸqtwk֭9sMvk|.7|ĸӆf?sF3' VZ?V1.xίY5#t$gMn˝wD0Bc;/W0tPɓ9s^ᰚ;;~y^IU :HD cUVVqO^}rb첁!)dH9~~yz*}})1eKOЇ+?Z:ױg믗MRqgZoUs[l7ה@DxgϪ>~ĉe۵yf-X@TYYIب}{TZZڣ76)zaZfUVqOB}rbɛTչ;lVX.~~yv}GR]dەs|mNNEcEBz#zj[򽃪on?DcZAy\r/ͶZo\RVUUU:uJPvvn_Us<N:Tqq̙#ITEEVX!ۭt'?QYY&M$I5jO駟V8s=ŋxh4HlɆqO^}rbe^Y^z_9(n82Ѩ6}"(gΘ%҇ɐtl"_K_VmL\K{D5z AR={GԱ؊+$IK.O?Ç^Scc 9s_G?VuڤI>/BVU .OWFnjR}ݲ:}En.rUmdAF =v# DuMTJcŪƒW.[& g/&jTN-ѭubbj׬QiĪxhF?.{+xWꚂӇjpnZۂ/ת=Uŝ%auծYj]|Vv%`I~*kK7?Ө!}o$9SWHLFDcyץ~N&c]]Rmk)C tdmy +esDyU_חm F*1=۳Hnk7 cs˯"/3 HuҜvMA97յjxvRo/0UEUu<(wSxr\+v['  hGrEOj=2oL\JveЩN2v0n }yҌ͗GjnxohsPeD"$3UܫWH*7t\nTY-7,'A/`2 Co^.v/f}}QUk$ iihl׎#/5D }FZ0zCzS֎*ws߃ tGgv輢1CiN9̔u X-]ꁦZ#d{KhlW0uxohXLMo /4eU(ep',1LsK%$%ʛg!)iWk0ҩjWӼrmW!t٬Ec#L7GkٿO+UM%9׵j ';j5%c# ї/RWVxM_;A:rK(/aui2ڴ5);#EOA7WN^Q㧟Wfdsʾc+.Iiz 8Y3xmhg9r218Sv^lJ,wRm}nj@V7/xsIIT2B!IP s"zz;x@6Eec7q^@RJM%:x^v rѲ;GN:9V͙\9ɈuS rQ$fTX>kD"߸A_SBxX5{<.#|Z?>t^gkA=~-z^@ҲYЋ7w'=ա;G)3pXEbݧPƖ_go5 ?Me:oBR0u 7 &Z-t3Ȃ&YpM6ێVj<9+o#J+ڛFs[XHR$}<@o #^KwM)Պ]fĐ,͸35]Z-;wkvGއ*9},LNS5|mhh^7 3i&sHѰʼn4kjcehִt.Oߘ/k=Y-U)8+Ŧ39!CNeg8}.ޮQݷe;ʾn.OJv-e{tdl6Jșbs!aF_{EƎŢ̙]or;^90Բs|kW+tRi%]R';!cmǎʷfڎR*aO57 ;+kԼ}[f͕w2eΜ++fCx}`U<\p4M?Hk% IDAT$Ex5AF%IVKW]eMIIl@^@p|"&ڵh&i|U,Tko(lʞw<= [FFb SvRp\ۇĬXL7ˊ뙷͔gCJɻ@!>%)3NbZo*jN뮱㔷/3K #јձe$_in+^@>$S5j.٩ (ڎWʕj;x ^K)(aZ*K?(;юj5Ӈ8'mi:R2o3NEy\wLS oLץݪųeKղB:<.6w?2&MNr|^ZÒ~|LuSpp4Ot,}Чew&z#1VZ].mnK+*Eʹ{Ng;?vZ-3x@wN.LHOTQ}nՄn㦋BzCͦ;)gge%CzɊejb_c ./̈^]H gN1}[."X;nCj \;.-TyiAJ>^Բo|kV*xT3Vފ9n\9jkI`۰L?{8)Z0}ٮD74[GO7(3-E%9rؓޭD"UY5['%=Q66Oj FJ4GN }>";ٲypsb%CsZ-p%: u(֛}$t*g=]x h/= հC|C Xrϕ%gg'A`#\#SZE|x=}r-UFxUz'ԑ#xDic%3 ^.#xZWux1(_އ*1u+}fmwV!GM.&\W'k/MVT*{U}-w*UkQF[d{pAUuEޓ I,))ʹ{rY$ĺپr*k5ȝ0^@3"^7^Wh(ry\*GNNb[BZ7 t P/[FzRy-(ݙLj!Y:ySr& HJɷzڏWk[Ɨ$35a4cyeJMlcp/ kj{yZv85=]op$bhdo^ɮmY؅`2~X*%K(ݷ/ f+{.Z,[Z^@?Bxw.ЪvT;t¯+c뇊66v-e͜-ϒx)AxwtåqM>EzE/&Lwr+NHn$kh;ktEwh蠌~XPm}]en=$i<x&sPy+Q7/HLve㯙1z$`D}琚’株6/E{ zjj jtG.ۭw1Լ#i#Z3C%Tt vݭajք|͛=fEx IǃEhL׽3zviU[05*IbG|s4k^"6nhbaS&ޮ1Kœ>;7@c^t"C8g487M%s$t_o~%jW$ĂAսo%#.c1Y|&d\o8TŃW;QGx1) IjL[Z/r/rm=~+1Q5~I^Q>^Ϝ1S%/}@*мBmUhӓG{5qdϗU;=E -N)cҫajٵSW+Ts)pƗ(+2aDyZhf4e,!׬/$<ګ3Ƽߨ/IvU`zJgr[hKR5+vPRX6VY,KOu{_Rfyܩsrg&5\'ats! E~nUNN:㞄ĸ_]y_Y/ky|HYo7 b{81̼~%$U# bT.Ow-ә$  GhlWNS): PHkk(lʾcr@̬6 i @m=pN}yZpTUQyiAۺZ#rgz|*(R3MwiR;ymwN-ma*tCNOO#1} U%hڲ‘r2Z4X}{TzBuט.D|c>*x߉׷xp (8PUm}U]R|ڸLq]sP+&)-qͯ^uB5z`2x+teWvKMUg4bܦw)luMAWӮqr.SR Gb:x}VMoln<,{N,+oj~_LBx&hӞ3 e[բɣ{g3ZrB;=E3^9:2-- ?@F$"I8ʽgr^(kj5|q׷u:lά /@vS6>vJg7)?7-}#zgxp>Zz*աSjJt{|,RG*MZ[:VsewR9Qm}Vɐrhɜr:kn(Z,rϕ%gw򘙜 't"ݪ%7P+\v6Ъ ;NU(+T7&v |nڼZB1$Ks}5 C_l5 ^>t˖YPzO$?8֎٤`$M{(e[w5-!jYfCKH'5>8^Dx>UW;@zNלG(a/I`gęxp /`$Gj5oJQ $# }l]jv8Pan/$XsZղkgXAl!}Mk0ң:CxH}O6Wsr1OV#Crˤ~͘l}\u@7,[j;6U$򤋎V7hǑZEc&+|x-6y[u##$Ye_{ɖftf՟76vn*n# 4!ڼb!c3ϱC /@o"kn-:oR6=X>Bi)Lx~Von9|Tj5on7"5|hSSGbQ֬YTܾNO{5t_}仦iA:_תl3Qm:=?=ç%I z`e{fUŝh,TckHtR/ߓadwM}g5{`9n/j])m[Cڹ̨;Sn \$鸾y5YJqјPz8SskXNw#GTfڏלÊxXLs@o"P8'ԏnPk{\Bv-6 NpTCrӴxpz^:"}W+Xo<&j;r8\Ja? b/jKKb Y1i@7L7H;:-<87Mcªm_y>TYϑE ~&ܬ[odD"$3U.R eu:a :YsumCe2{I^ ׿־}T[[?~c^x^ZMMM2e~iǟZbz-B!멧S__{N6ljՂ Xiii7'XbEMwsٳ`/ GbTuҊ[# cdc_H mmm*))SO=%I]կ~^zI>VZ%˥{LХymذA/^z%?^?x~J7/Km۶MO>d߿A)l։^.Si3KXЉ:yI1S\$QN]+$y;wΝsawoF7$Iٳg>ТEԤkg?f̘!#,ZHvRii;M6iڵ0a$'w]?T^^ySh޷WGORGVG=&a} QCKT]ck種mLEGz}@rK+ӬYⵌ jǎ{*hcFܹSceeeŃ$͚5KVUvI_CNmT8Kt+I{9G` 8E^2\$ЮuU9T3/WR[[+IzG~_p8?&77v]n;~ ‘~|LNK\)6=8g Ipg7&jܲYp)-3K]>W{574uKxMGe}iZZ;Ub@3_ms&\$-ڍ4߯hkoS%IVSܫʚJp8lY-:t^Ӯ苃ujcg}(>-"cV&U ڑHD ]fuf㪾drqwHub$͚0"/b>P_Sh(;5v{uI\=ExNһ"+é AxH^W[l%Iڽ{oJ&N(ݮ͛7kJԨLTVVF۷/~g}X, qO^}gTڻy3zƈ۴YU/^s4o)mh$)%Q ^ŷO༓~l6fL; e^¿w$UximmUUե O:(;;[C ѣ>_*..Vaa^xTEEVX!ۭt'?QYY&M$I5jO駟V8s=ŋxƶ~8ͪ,㞄Mѡ3ك]d8TWגMˁ:j?~<^K9RǍWPRߏy9ﮆv30=y]{D7u|zG%ur/]T?O%I/VZFM6MO=TM*CVXu]vʆ=Z~V.\'xB.WϾaZ`vrr$_c>mwNM!*piEJsRi֮RKK:ɻBSw_qON{81I^̆lɅ_hɋ7H}|O? fȐ}ǼnWԎ>#[Q>?'{"􎤺l mkSݻowe:D8ʹ{rY$[ZZ;pߢ߾sHpTy͞8Xw'э =dD"jظA7^Sh(90; SgZ%K1 C-;wȷvBgi%]RٹWvݨpAx~*?'Mk=zK۱Y#5СV< ?{y}WZm$$$6`Ʀ ȍxR<{sgnM$޹[LĦf ƀ`c UCp@HS>g!<C~~B6x]>xhcx!"Qc8UgFc9D-㑯7u+Ԧ7k!r_xudf(2 I0SDODo^bZ)ǫp*G8aݶ`ߴ!YոYS-KOVy?J |;njUY1C~n"xBDTJ&Gza۽ +dH5#T?nŊE-Mȹ  [B(#`1vXO<+qݣgo݀Z\l  ; } km뚒RV6W IDAT`{Gƈ1Q5w󇐗;: ѵa<.5eV658;4j0ȀguSȤYў0Q8{|pUY֨ѪX5g4ғ.7mcbM H1 !jBf&فDJLH% yDD^(l>I . xme%R}||A `߻[ALVC"/TR&Tszb"XBDDas>paA!1 | myn"!gq)a=E MJGUB!8yFV8 5Pg\[pb' Jt9. 9!""" )eUSZI^*RtaϟyZxnC=Uk5:,U3.~aڃ+y1QLȀL&+]C('>nF7GsbMH_op9z}@r^92QT0QXM.Koct;@ H9,,gᅈ(:^({ܰ]D~-xRЇQ) DD^(f~8>m}E)fðd)I97J.ҙ|a(^(P_i#fXOӊ(3G,-Gc 2RcjDD` /DDU=/kZxo45uѨĊD3@*0 "" oK3֯CϹZ0r' bDD^(6,[>FCg&Ad)Rg?|hjjq\;,^i12 !9 ""`Ol]|>DnB[F3d6=KB6keP Q`x!"!%m[t9 ĝar(t!N1bsyqC~~"" "" u87)+`\ 윈t^'"BDDas2֢^ `Z ea;B./f& zQg#"' /DD6V?^SbMa4b%&MDot8/o҃$04OcB]W 6iE =p/ &ML%Hyf. Ev_8=~l?ֈD%\&ן.Wlc&- )DDDBDD-?3H}v^,!1h˝ԙ^n1iû-DDqᅈq,[7#WH "aᅈ p>oo 0լ*7o{(KgǛx\&"" /DDP<е~-z뮊5Un5X^>X5gvmJ! #E ݗ7u+&a\ISmGQפAC!E""<"'`sz!Jv+ae8}/K5_XyB5K%TQ큈"ᅈ1؜^|n,]uX y});w@9؜yп2-g""eu+Ů !lBq{5)Sa\ 1E3,E "zDv_pb T>u ;*F*A>{ km2j@]P~iV2)fUfbVUVXMDD# #I%{UTD&`?*~}\;^^PG{z=6%΁f5*A" ]on#0ZEAFrXADD #JJPbtN 6;Ǐ6B_pm[/Z’:Mإa^<>.4 JP=Ƅ!ŒlDD cX>; VH/6b܈ 0j֞t:aپޞAL~ыH7RޥV >J98tW[Bs*2DD cШX1Ya{aS4I%` '{;#e2>3C4w?FqBXSȥ?8$';BD& /DDO MQ`FEB.ŜlkǏo$%\ǎ²clǘkLOHd5^zv lF̓ s' uZw~` D4 W /DDTڍmHITb\3!ɸ|R&TCzH5aҍHr4c`\O霟WEbHQ#C0| Q0E rmxeQ ):^QV2?u ͵td؛ź"3X5HOaþk=WG+"ᅈ(ڭ= đsmX<0,[̐lϵ\kEOƔ/6OнS4ۥ0=ɤ8"XBD,j ]}CgZpD3 1s\&䲇^7vúc{v#+e \̮*dJR*s!7!|yHJP`rY:uv^X%bbifWfElϡ`LD"aᅈ(x4̔ӭwӭ;X6 }O AD8z}RM+䣽WQ-~}//,A^z#kþ:\ow EOBߑFEDyI(MW(e>.GϷl^B!t;a^V .(ϐ%(RR HPYњ;] BBߚ&˝N;/DDj(^z)JL*1!)Q 70 Qqе~-7ŚhLMq1`F0c\Mkwy~?@pкBDff/u # KҠK!JQ]lBuSPN>ICGͼMڰ=Ί5Ez:+VA;a!efMB* $_$3?'_@D=BDFn%x}w.7qɎ3L,_CЪX>~-}p3,Ȓ`X)D?} > gY HP5ڈysdqV)j̪zsK$zf63:+'qEz""H"w }i˥9qnzfPf&Gا$vxAd&Auvw{ETB`!t A lV_ù~cLxqz}3;<}i _]vrو^G&uk QYagxTd{}O"3gd9>Np6~ ' з1%sZ)&W@!-nfbþkh1ŋ3 "rn""GBDf  <`u33ɓ0,^Y@PsߡU/T")b Qz=y'l H ,误ӏ> 8ef7-H ҽ4!#uuveDDDY /DB~?V}_  ld*6 եN9o/Ok>4 ";B(W_F]b=q|5MqmN/Bu[ڭ=jw B`C cP>  "+=.k:x5ua aL <}>2KLhNjyPHw]vX;~<_(^(.x[Z`޸3bMaJqe gg'PmzA$%(pr 314 _^Wʥxz|=y_p'vaZyFᅈb hpGAfo`޲ ݇7j0,^ԧ@"clƸLL.KGJR\l)(Lƕ&; TOmfuu"""Z /Duo oTUi(?gB {nd !L!dI%y^3qv2""`x!;|M .PE:zVo τar(5@Ƅ1@DDD1Qշu\5T5CRC,UPF/Looo ecaY u~A#""BDCs zE)sraY BDDD.կjEEEرcotb„ |s׋z ;vìY!bA-v?׉Ě\a $OT(1 įe2>?Gvv6~mرcJ%7ā_Z?O}~a/(bnXm}> H5."""2L6]A{;Ν wL>wƢEt:qF)SE̙3 ]܎Poo_Q&Cy026 Ѱ2F̚5 * Ǐ?? 330͘6mVEUUN:Eܹs>}MQQp)6PGcv4y WBiJbwDDD41ܥ o ىK/m۶ `4`0bf3 wk3A>[ u𵶈u͘V(prٳg?fTUUaΜ9عs'L!E&#ɭq\GڵtQz5U9c㸏Lc /455ﰘ~w_, Ǝ ﮌwb c ɚ0\śXގ4Gk /} is@rǤbqiqG&;axyۍF,[ 08z(JKK. ׿\.Ǒ#G`@}}=Z[[Q]]H  (fdR$'kbj.'[ºgj…TwFcOC>2qG[cOO.?1w\dffկP( /o/Ng}@ߝ[HIIAbb"~ӟK0B l#M,{}nXwlC+dH B `f/|OE?VW,=E}d=q=v;z=&MkB onL4 o5^__ Jk[( ǎ¼y#VXN(3ڡm{ ps홈F0ToWD.BKڸϟyZxĚzt1L@3j3p}]^cJdnw}8`E($`l$UDn{qn==y!zo4¼az.k jV!q{ Q_hUo|zu9?E 0dvxD :'1* Sm㷘a1ǎ7oʒaX )fC"|N]5*d{1 1@0$/o$7R)l&""^/w|.u=W6ita {>$*t ~urpP /<3 a ;>tLj^ V1 8y k| wR)Rf= Ò>y4*9^^Pkw/  .PH΍F)CZ(^7п ﺊS_ˉaZ ̬':>ԄT"㳰pCL(\hbx!fJrSD3f~zwhT bcwOE^ E!vgE.6!U™:3B0HG{DDDDh1jH'im늴4Wv3]oqkv']4IDAT27: FB=4QhADDD9>( ˖MH>r7g$ja\ b't< .4XawplX?x,*5U(~18݃ML*Ay/N/nG?m.%J%tB"4_4N/oWszf==&~^@߬a$X09B aݶAo'3ft9 6͝.@NRc d﷝L*ARoh1{DDDD([{r̘&my:zbe+WA3>SKw1j7 ˋ$x~Jt߱1NP`x!abrbbQ8*UkPZvm= 0wb Ōqp0hq*K12I/1 cPdCZziH@=VT5EAGa4b%&MDzMzzhr t7\NfaTV ZjXbw^( ^b{^F݃@0AN\7f '@zsN0ib" /.{j{wFe'cRiC\&B&?|\z߅R FQ5 &"""z /D1twj>~n &#/z!27ߨku݉+J ㋍8~_U鉈(.0ŀvkP/ƗP=}Z$$OòP CMv[{O< '"A-kQ̮k"""" 7k?di+`Y Un^}lNDzu3ȤR<3>ό_ ˝h M4$CzN"""^bX [jmkΔ4e$WttmÀYrҴCx}A~Eq狍6f_ ?r7 'pjն"?= '!w=koyN|ԛ5b2GŋB݊g | zˀiN/N_5cZEF"""1]icN2*aٶ` $ wJhf<4SedǏ)0F c3x{bDDDD15 *G >pOaݹy\9`Z lnAB.EE/b䘴b (tCDDD c{>HAt>M:n4e*VBa2A>L+[dԷv$+EDDD12 Jtׂ ]{kmaA1s^ (,ȤR\mڃT JtJ%n 8zNVxSNcD݃; jxajxZ+}dY85u,3X5gFebT rDDDDOFn,Ά%JdF=QDavU=>SՐIuv²iǿ¸l%>qP^7ൕO~qDDDD ({O67h#B|? j9r].t~ޞALE_JwCÃlStg!"""u /44;|xn|>|Oܜ&X&Cs_dqۤn_eR <1Ј 9Q ӋgZ1UY4{z_!Bðlބ*ֵqy ^_WexnFQfڽu"9A|?ыk-0_Q?ki10ZMѨ{rY:4*9N^?BYʹ<`x$O;0&'S3p,[|N\yb sXSfd¸rWC"yt \󄈈10Ј3*;z,%%ˑ2s$V Q,bx!B_u`]@`߳B TߚALtDDDD# 4-fđsm 4l5h6J2i,y}*I*B*`h#䚈^(nI$ݜ(z(`xa!v} @@*Õ*\ș\'`Om7[Di [=!kB ǁ}lۂWHPo*Q%۪8P&~mHV[ϗѣcxprN_BPP^Ա9}q{&q0\g1cRiZDz8zq^msS"pB1`m+l_ǡlk|kV#ql9k&?>;ބ2L"^\tFDDDD 4Ap|ߝ!R/~.`\IB"NIZer>Iu: / /ӂ!!g7&8Q!yO'SAbd>R2]QYɸ-r)fˈbWDDDDbV>Œ! AW*^6k5sqJ'[U4Q\cx#p:M:5GXi j@Hc͜1\;-Sf`Xm!""" ں)vA벊uMi +WC#?BQV2M{LDDDDb*.d[o*fO4T93'C"`t#"""cxuti\_fͭld4,h@a ?FDDDDBq .$]>r&JVoM'V(0PDmN|#27 !a+@\o)hb E a[4G6*^^<sڝu PZ.R0elzZ&"""BCJ; ki:pp:I}t-УÉ Y5NDDDD1ᅆLoC6EM1X;^7=.(PVxDDDD^(]]0oĚ,%ƥ-ۧW_P4/9&m4Z%"""8B݋^_H8r}%LRC-xR Kqrn2Q9 """B `zԵ:: +.Bͻ%!{`ݾ d2$|™ R)/U¼9""""o /@{N4l./ﻆ/@o3i'Nonh1E?;$Uiw6i<+ |^]_ŶKn{o/0ivb뙈yR*d 2c|dڛϻ58]0 -B@"3m:s @22DDDDDBݸlV@yjKL8|"*o|˸=D̚ke2PR!Aw iZįn7C0 `ݗ\ ;i6>(XErgYէM """ae HP} t}U\ " :rnDYaXTJkG0$@*&4_9)HMRu+r)&04- WJDDDDեN? SMɇ{KgCU1hgK_ UPee;֜ 9XNT5R}eR<3>όώ2Ե8GhUżb[0 Ns> &Ɣ J$'(9Q800jGܿ _kNu2Nǜ| IlhP /#̝i.% ;.AMZgwd<76i(uJDDDD0fqlTeLĸ".]mGYIFi)(n &|*fcvi62 Q꜈17?殾R MGu OWu7~ ӷD`\ 1K8X&8Ҁ\[nDV{YBY9VCV Kouݎ G`=&֔90լFBy$I[$""""r /1jޫ(MAZ`@뱣,z=V"i4 FDDDDK:|N7c͜bLBwn? 2,Eg!U*-cxe~n1o=D Wh Qn(r^bTA~ >7B7kI°|FDDDD /1j=kJJaZ(vDDDDD] /1;QS̜ĈhcxQع3DDDDDuc\d:m """"" /DDDDD^(.0Q\`x!""""BDDDDDqᅈ """"" /DDDDD^(.0 ?;w.*++zjF%""""2Dv؁z i&6Vk[#""""K /C=֬Y˗cԨQxסVaÆhFDDDD^Å 0m4&H0}t>}:/!` a4z=fs"""""o /DDDDDn`8tdX,L>Ll9oñ8#}☇P*(//ѣG1o<@(ѣG/?q5C"0ű8#ǝ0 o}['TTT`ܸqxwzrhFDDDD^ȢE`_]]];v,z>ڭ% B """""z9DDDDDDqᅈ """"" /DDDDD^(.0Ę?;w.*++zjF% 7Xr%&Lӧ._>`~3gDUU^y466F[JJKKos쇟?Ĕ)SPUUŋܹs/@s碪 ؎?ۿŬYPZZݻwAzcʔ)kK; /1dǎx뭷6mBii)ojF5 Ǐ_@*<;#>o֭[FW_}>/S8bڵ())D"p//T*;v??#%%E܆>׿;wC/nq<Ln|M۷/"zqEQSS#'?B¬Y7Q슆bJJJǏ 73f~߉8Naܸqۣ&,X 9rD˿K7c?\K/s77¿}{QpURR"޽[aƹ[(//>Sqk׮ %%%ӧ#|᝗pL6MI$L>Obg4N' 55 @բ NJ^o̙opw^^ӱ|r_^><͚5 GACCҥK8y$fϞ >R<8;w@ӧO)**BVVAfC0hWRW4B|ML8Gtuu)oߎ/bÆ >OMMM+;jkkӟ ˖-S/sA.# ?^|E>Roob6P(j B%:? 7JY[[~=JXq ~@ii)\>˖-~{a/~b\po&8t#t:d2ـqX,0LQꊆoCzzX5փ}/7_Ο?Պ+V8~8}TTTp쇩4-EEEhmm?կk-Zb,]ַ}xq6p\܆cxJ8zX B8z(Ə(Ao={wEvvvsrr`4}\. M6 | lق-[`ͨ%Kyf0U]]=߆>}x2_M*qf+** qqz:=ُG ꓘ~P(xqeg?Fv{:>0LAO߾ݲօ~7N8(( ÄI&('Ed lie  G v6OXܰV?(T*G\ ߏa3$T*X]]5/jrr2v XEݎ혟7/eFQv(VVVbqqܿ~Qף\.{tr9DZh6111Fo?JӉx||ZQEjJλZO砏"^__caa!fmllDV빽z1==[[[sZ~M^]]]z'[s?٣Gyd^+o,p8ltFk jhh<_488k۶mڲe|>֭[ڼy$Q[l/~ ںu^|EJy}+_o<^3`V 5r9'Iʒ$hݺuk222TWW}I>`0G?~IҾ}t:#E֭['ժQ†P(~ZV҂ $Iݒ ׺Ț#ák<O䚜 ߷r\k\שSW6gsFB<zDkQ.鉌*hppN~0pm*a=z*))Rߏ Zb$iҥھ}{F^T__9r$r͎; TWWw͵HBM{K/Yt:,ŢO?T^^=3*((w!IԽޫm۶r)==]O=|rIRUU#S ГO>[^NcE#?*3~O@}b.='Jbضm>D~g /h``@W֣>ƴm6KSCC}Q5^WOeFwX5Wv{Pngn/Jܻ"G{uV/K# K<ϷLKQբ/ܾ@g}>< .%}p{jX,_ԆeEvxFx@\ Có\ b<[IX6q)ǙOoTZeUiqEF`qky[˳3-Iɼ53zqa ;2LCj JeEJ5,G$zt__VYWDǂ}DnFƵ읍r#BNzuG# a#=qu?f*:k 9E{,Wrw.\$i<;4<4* ";zQބE󲴼mPEWw} - g@5IL[UhuMں K̴$Q$L" JAviFYP͝>/i*vȝnXMyY N ԥ K . 3ӫ*veta,` @x` : ~!b5Kۛ=$.)-+J.+^WO7$mHuhբ|KL9o$\j)$. p)LՎ1͝>{]}~dꖺbUlװ?8ڥs 211\ҿN';&I;*IrحZ@7-.0܄ p ^ rϯ-}ZZ>Tޡ1$ٔTeb NBa f!  N>xb}Wz āǻ6 ֖4LĩSmI]T1`rmM9?:7Nope泺&_ / }6Ew*uy0 uL6,+2"۬;3^_2LO2,\@x.SNW;2pܲ*wTjђ A|I{n}_UCCjjjkMmLߗ ~=ZvCg57UVi͚5w} 1Ur%T{?Vd+b+hź{bLbѦM}/Җ4qO?~[>222O~5ַӣ{N@@w#f&_+j?A-:#EeG1Hi&mڴia9C/֮]+|ٲe8:>}Z^|EJ~a}+_ѷmHYHM57#zewKCzsVX/aXk._^wu{1GaA_>6|k}tF$[NVUICz@[T q\P#/WР͛7TMMM{}_/~ YVy<9eddLxs~Bǣv\.Wgh4vH@Ry1~?7}ǻTWYB'.>1)Q= _^>&H@?ׯ;dM%~Y5nZ\:*:tHw;m۶Mt=Ӊ'_Jƍo|C#wdZCillL zG'ooOeZuw 0lK=ޥaP}~g1BY0Q}}C|@vD'&=1%Z<s&,?CãAyF''٭xlU⬹=XPIV֕%Z&o\VP0{RlVV.|Y-ғG`HK+ZP2dqL~{]ޡ1Uf֕r'iAKyry#Hu(-壵%y*˸³^0-g;O*ta%=jY**բ4cD\` eׇ]rQϯ-^c B"`Z!Khp+A#`ZKڬ sL%)(8V}r]\I1whL{uLĂ}LfoVWTؿӣ߼dW-͚c~>8$Y-ҺBݱza.Hܟ }p;oЯ f8 . KَwyIǂ1&SjGbajʳЖljAˠMkנfQ][i1}ߞ:bz7= Lu JIoK;>ZN}cUZT{Z=Ǻdd*-u@%c6UXVhp$9P? Nh֘dM#*M-uJOaCe%)3-2;XzwT"wp{u̞c{d%GJippUU,6+UL5Y(_9i:rW6U˫J5,$ YI^J2. icL/L/L/L8ӫ[t3$+E՗hqEey A _=Vϐ’F^{[;5<4+LwLLg̉p:{!y#Tsui.qMhHڰИibS:64W*-%j䤩kpBfU+ՠs.:UZ]7-.ЂRAjw-l\Q1vR 4ul9vnYQ[V] 0ms' FW"I=ԮO0P^~VOy,[ENL[[OZIjJ&9_Kqn,?1ǙVƪ4ׁ-?k5SϦ~xG%Iy}j8S b10mŹZZ3-5ɦ ˊ #~L$HRwTzQaRcۀi\'WGQIRsנGsz=hZ-Z6߭eF[Y'&au/1vDn96*0Wg߰2LK$ySҒ:pGTW֦F ^@\w[,0$ŢUe۬uene{c6L;߼V-pXuEF[ 0- ۏ5~1f ^c)I, ^me/;jV-7"3RҝڞVt *ǙMuŚati ң))ɦ[W0wVVF}są4]FjΣ$E:گ7TjieΔ9xڣhh`\VTWOHlL3n`xL7lt9fx4=ǻnx{g5$ҾS==cuf Q͝>[_gpe3;4IA|V羅9ܧ55Abc̨WD$/5KR6)<i)S$ Qmq L=/6\.%kU2RtS>,Ki mIu6fV+U]#v\IUtm|~>UFC\Yo39?:ܯdՕhdد%QrmҝV5ueE,rNqfԭ%囧 cB&ݷ!;?+uqI+EUTǕ8gZVn*%ٮ+:R۪o^@<5ѠVwTZ^PSR< CZ\EeF5^ӢK{zk9m]WaX]/08Џ$vPcyf̒ ֮A<کѠiqіNߤ}n vvt_ʲ9SZ8/k07<&+ׂ܈zpq}@MN4tNFxĽξ)۽Cc6d&KwF zuwvR0VfC[ו4vD+w4V]a TktLoT[p<}Zh!)+ȜЏQySx#v6,+҆eE1}^CxĽ' mlqY'N5Śh@\0oxLZJJiaivnYQz;V̅H[ 9iZ4/K[#mVOۯ`¼ ^l#gz4FF\IErROdK|#-(q)Ǚ{/vǪhNzК|x RSϯ;Vϋ}ic7xQ}~Iw=BjuK@8@<"Lӡ^ mt'f4FCGi _\>j=6G)rs'`wzٚyesbz-ʕђM\_΂}$ /wWU544FZ5<6nܨ:jjj}߯\k׮U}}z!L7MZJk֬w] k$wD~~҇f ]R+Ig+3!ILuS*Tg[GSE3沄 /###Zx}QIG?y=zx@ccQxog?.=[s?٣Gyd_ /վviNcіw\:W}6Vh|l3r`Ji&_㎨a'?׾5vmZhZ]]]ϧ_|QWkתVO?ۧHN>w}WO=/_UV/Y}xF. *}~>qVE$Y/C &*\Ikk<֭[iP]]'I:|֯_f*..%ITmmmujF7;䟢c7"׾zmO7E`n .Nhwݑ5-GCQx<599v\5k34UeڂB5Nڎ3<s=*w4Ҷ<[uUUL! 1aۍ:|إ߾@vOh[ֱ>lFV-busBpt}@E{s傼<τї-YDQ@ /===檷w'b`P^7jTjlGs}o.ǚtl.h9ՇM}Zhhe՗QsO}#*KW;\]Gϰ&0V'?> ￯I젢Щ{K8uܹSwg\|sI}Y ի裏<cccڶm^z%A>nw׫'xBoVN=JM` -QVeg 7ظ~?GhׯTrw2RxἬ #/A_3OEOvk<VmEPpGuitl\˲qyv۬nꮵekm,ˬ2 w[~lu Wz-85/LbǑΨ-P @bG4rC]vutn<-(up /14sFy4ȩ{6Ϳĥ=Ғ*M1m6 λ1m 7~N].>_s憞ե*N|3 Oqʈ?]Gs .:}*6GJK?yiIwת'ظ* JNz1ABᰬ,i#raWe[FX,U:Z+E2enӒl&gu6U[n.QPIK뮵, nl\VtL’Tdkg˳],Z\$@7lL|}r}$M~&9Ӓ& ;Fx bQ)kwwX?Cc\Wj8'?fM(Nc$H;o2" xjolP 2"8icC)VaN%:%9lY-M<3'B;֮A=q/|:6mݷL&5ٮUn;о&_V ^1ꞖHpjY[qm\$:#բZ$8 ຅ahe::WiIjv40.wT%TeY%؊K fY}pɔvO(ϕ meg$vsNg߰v~ة1U9npm I5:u]#$ z`>V_{"$ge=CKGGV'. L'44Я=X,)'W(5y?DuϦz`b;X`\˳,kk iNF*vjMMv}pGg:t_ AU^ 3:y+ZdZ[&` ϷUN]ocۀδgNg[?i{w|_bfx4S˥6)1YZ0oB[Z] uE1mHpTWczPfE%.bp>\ K]jlPFC F+*ɋsنE:6NߴXiޡ 09 $KtBl9SbXlzY C]ٗ$Ţy3vϙW>D'Z#L22@ -%|8uF[V(י2%r'TmV-͵S\#/RK)1lHuV'Zĩ\We DxHFh $0 F^|c:uΫMdy ^5WsP3;);30W}xW;?Ԉ?yYڴxNN+c:0`\/ .O^j`U/aǴ10Wz,—gI:ձ>96U~["H`bL&;3YHڸȘmzs[51K J\Qm)gP 8F^ĝN9.wD؊])W|RtoSJ]k'}:  mPXo|ЪK *ܶD=Cj9?M,n+́p/JW߰ TWg;|g*59~~٬V[ZuK .eZ| ;F &~8OժӧѠ*2??ĕǻ#5ScۀFo}B+UI?EE4e)&0-~{+C)]yKpXvEPWltY1dӭ%HnJ 0W^ĕRe.29ܧ?h`(?;"g\-P3tOIWq6C6ӧd-ȍi@\YVHlVn+Vqn]SZx9ԧwoL}|urg`㒼 ]k zS:ѿϮ]ym X-}zc6,+TwTŹL3')IuP( j~K{g;7NEΓ9گ!}K߄"I}~8ҡ;V3*l KT}ʅys{n\)UM7<[s&P>_lS[ϰr])jX^Y|\^/UsןmECj87]jWkאܮmXVx7ʲuwBbyn~0ȩׄ\IZ[[``U@x)ӒNzkyݰHRߠ?=-š1gH'>du s3' vڲ6,>9ӧNr2U_@7=LPcO$\4]svaEj)tbzr?Ҟcݺkm٤eXTW,_9i.Կj*`˩.?iN ĥk IRޱ X";\"*Qmo;}m~-\` EN]کlVp}-m.m+7l[uK]6,+2,nP8V{ϐG1M>?0P8,XuG,Pg; C@0gpe3`u7N$nRfZҔ&:K* jp+=IngoM}Sfr5bn .=w4My}g"I}~wC[֕+[uZg~6E;׎N}>{Ox ' 纇&}s݃ =k9Ƶp^Rs]UvT`<isج[k`U0 v)؞djݍs]Os_hOvشʭ`04ufvO?^oDimUrhj`^:e:TS/[qY 2U]sH[ÖPmU:%F01 ܀7V*#աgzeZT_[V\1|8Qc\ZSY/v Veg}Xs6-ȉ7"Q~O\ûi~Y.|fZg$_أCHpq^ wD`T{s$WD#dm;q,X׆`Vصvq6[꯼CL]~@ 05wBwnU}u 3o<nU8֒d]>_Ѥm^$ٵvIMy6 [/fJV<'Z4~aCJ-ʝP8vBQIҰ?7Sݪk ]yL`JnI j<4 ]r<3V#- 3D|#>&8>:`*;#zd2R> C|oieN3t,n[U"eo/65ͪ߶@IbQ][ u3].0negiBw;ek,URJO|MUzbQ+u*3icL/pAw;}SŚW44>8Hu-t]VL7N}@goRy7S 'nyt3jѳVC(ѳfC#`Etx00`J͝jjyK+fp:}t#b9Ӓbvc}QmAva嘪=֚;}$iǑΘ'%iqj REa_e'gMM~T[Y~ sbz0=SJM랍Jv"mڸhV_3aٱ kjeu(=Ů\}LޝGG]{̚}J$$DQL4.Upmիu{NmOKJi{ZUǭhV(b"@=?! L~39cyf -MOK!80M3򽯟=YjUNG /{'ҥKpBKm&図h"\R<.]F}݁x+tK U41A1a/)T\tXK䧨(MvۡS⢜) |*${IDAT5fSRRҐq0s;ٳ%I/UYY+VhΜ9ԫ~Xf͒t(̙3G6lPYY٘ R"uyS]F@X,]tz2;pKOXyFmmt%IuuujnnVEEonttʴn:IƍvUYY雓 /"̮ ~ʴxbM4Io~nAo񆚚$IɃOJJiinnPtts8>l竬Lwy Sl6B~z߽!%V+}h.zcQnn٫nf*-Ju=4w^[;wիWPեj}ߔ$nkժU袋$I555Wyy }^y<^![6U! ;?v޼E6vו އ2{:{|5#ٳF=r8K%I7|~iĉԒ%K .@ҡkV/V\\\'T_l&ɖFye{Wֶ g}SU(=Tw^Р}{jooWbbfΜ_|Q ~ַ^'?QGGfΜzJN?eZ5o<\.UUUizKqiǛѧh,LUv'H=ܕںLۭJH H:ԛe%G9՟_4+SslT {:{|519^hQOslj\$[Vy̚)YsWΛ?.g5-˚=wnhִ4]03{}Î0oVEV,u (9.QBV|R .ѧWV]WOg=lКM C6nUGK>ܠ.m~h @5 #o)I^Cлr{2 iz^Vjw@;*axُ~8Vӏ|CSe'ƄWkjZU(j:8.ʩ@:MskrF r콜7hlbZ#ƬZ/ڬjWC[$):¡UyZi;;\$`K+>ӹ;[|Ҩ S5猉F$U.a[0'^_;=gL7M` E9u%PNZ6vv5y|خX4O4jִ4%ŅJ_fXtL;#sԿĆ}`Y,(/#w))w ԩܮho Iml`pˋTZB*J&(.ʩyIi4ڼM-}-#w賝^tJ)|uƯ8+W"MaNN+LԬx&DhW}ǠKNؑ/pC&կҌyS~Dx9ժ (0h<5!Rߞ[ ۛ77=^x`TQG}C7<^O+I-T%{A":¡aN!KKPG IRmC6n\^S6P{W&NQUi#~? miU>e&GiYrj;|jdJ'':nzZQ9ܻ[^WfL#]=f52buYa0bxirи$ .5Ӷ5mb LmM$whsR{#Ĵas' ?.I=#xpU=c(i2>%+nˈU2y鱚1uI1,#c1xzrrcԨ#IHd8sNOѾn%EhRz,ˈgj?tbS2 e: Uw47Ŧ}ͪ{/SN;mᲰ#eD+3%:6tqկE9O/0O4j&y'%̒tEuۥEZIML֌)I] )?;~kJVV< 0 y/hu|_7SGKV*2ܮ3f'/s>x@]KfMTZbs5ެ٧d)"0@1jZڽ=tpmCma{zqsyFV/uۛuhG]6&O W|4B c~}ĝl9J}nוgM:1>l*=]6MnZ{T'Ekׁy<&a'w F̀۫-IbTu듭r{ %D3s]zknh+U7]x٭JK5iJpq꛻?Qvj춣*sMdYtj~N=*Jr\,AcПۮnIny;a۬:wFΝ9Ve q|^C7饿[wG:_p>^G<^o*@(b>YjZ}_ѬP[;}MIXy$wPp$Ї6|ƍ ?47:;0u tIR;4(M3h\G('-Fc/$F9uNY_0йN ; uQۆ #I[^wTehJVvTdC%洍8|Aғu\s&'kg79lV'Vya賚t);5Z9 Z-':QNxqi(5!BTr\(T PGx 1=}n=f5qؿ65(?+^uY,'`2tE.T]"04=/I9i1xK/];uhJV\*6hܥIU@Kϰt^0Y]5d1r]RÇ}NjT0^Pˣא>r4;!FS/ 7. Ba6%Ƅx\Si ˋfv)+5ZN1`,^LO}Z ]J ;t8]tZ^^SǬ.<-[VEUCx11׀GeivzI r+qWdHœϾ$;Rnzb#\6do0FeF+9M#`@x1>mq̱j%r4866flIx ee$Gim9Cx1];:{tjERi^ʧq㲱qj&e$dlSCD:T<)Q%Uܭ0%J/\Qxa*J&f{|bZrb$IavMΌ Tಱq`w^eƋU(]o18W~I鱚jaer`\ sgNOt ecTanJ'%j Iq뎫J-Rq)^)^)^)^)^)^FѲe4{lWuuuKL2J~m-^Xs^{5oWkkkKL2Jy}_UW]ɓ'k +S"˥M67fXTYY2/MGɃs#0{ F lCVYZZZrܯc-C~CC}M=ts өb^Z$jպ馛ubc#FDc=tDC}Ner-~M>]>u54/dΜ9jkk#<&]`J0@!@x` @x` @x` qfٲe={JKKu׫:%x ]s5:STYYKv2oɒ%:묳TVV[oU'Taa-Z4hw}5ktkƍvCi*++Ӆ^Cw[voJZbŐ9s,XYf\SKKX!#o/^{G u5ХO֮]nI/~invmy'tR-\P/"""tmrrSuu^|Eb}9xotꩧo_qqq9=>^TWWAhiݺupBwyz,`﫸XSee*˾{pҪU{nIҖ-[駟ϖDCyƍrݪSFF]ikkQrrD*&׫ESOՔ)S$IMMM4s1[oizWޛa*))ѽ+I*,,Զm?Ys=y=sZ|~aM:U6mҢEJq̿qt 'dنƥE))) e…s)--7~} ?WkkjXk׮?zRSS}+婾^?|;3gN+Rrx I=TO500`08t:U\\իWƼ^V^3f2aZp{==xVV}T]]***o_믿˗DW\q/_NTyyKwO߃alƬV7=4OKJJd۵j*ߜ׫|k6Oӟ%K(==]CK,֭[*""",X7|SK,QJJzzz#.."ǣ'xB'O<~?!t*11_RRx eee+A*##C=l6RRRw] U[[W_}UyyyZf~_/Wee%}"===ڱc/Taaar݊9fبe˖P?u]wK ƕe˖TTTG*-- tYBY,!׻.^xuЏ<^z%utth̙?&N8btM***gʕz衇T[[lr-͡G>}](55U]v.nLo͚5%iпW]u~K:v]./^zK.KUUU? ))^)^)^)^)^)^)^)^)^)^)^)^)^)^)^)^)^)?32t0IENDB`symfit-0.5.4/docs/_static/ode_dampened_harmonic_oscillator.png000066400000000000000000012505441412237106600246530ustar00rootroot00000000000000PNG  IHDR osBIT|d pHYs.#.#x?v IDATx{;g h;Sb;Y)-@I[uWݶFCu%muU1,݀mCRTj0"ukn# ֹ;'293$y<`>}4MNi89BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBȀRoKG$Ib\{MMG"MM@8[P(gKB9[P ΗB9[i„R989BBBBBBBBBBBBBBBBBBBȀRP^}ո馛۰aC cڵ&loرcώ3gƼybĉ>   >`~?9$4M#IAyXlY466{߾}o߾ؼysu]qWƷxg|'6lI Ztǚ5kbőiD VUUE]]]1"w_ڵ+/_.Bp (dž wcm`1|Xxq<䓱nݺXzulڴ)VZSN4M#IX~}~2#a,Y$8p@DD=:~>Aywww,]4+VUW]\iӦʕ+c+W_|qPf=VZIDD_?/֯_/RDD$I]w]L6뫪;H$4{2O~83ݫW4M:ʓ8qb̙3'44MWU9rdG ,c…$ɠ=6mID$| ƍקs7n1(aTWWC=ӧO/lmmm3fyoڞh3#9$3M?mΖ%r/(%rH :u[[[ 6{]goc'ףGC?\1f_ piPѣGniiÇ\NYmQy/ .WWt'˖-sE]^xa̚5+~ED- 'O.PUq8&LH$4׾rt~yg=5x>9@S/hnnw$x"""IHēBq :]O{4;wF/s=WSӑR#?>/*|*׾?}oV ddIDz;pHi s-(K l @18_P(gK l P~ݥq9FUUU :"DkkGe$)Ȣs'_D/eDDe˖wǎ֖;eʔS+kj-k6Fw[[6i\.F4L!Ȁ/q +-(K l @18_P(gK l PNZ5*/+.5o x_/]\suK;TQ(5jTޞ=_hЇ;\Ur1i1c=1E1¾hѶas?+>cƍ?~ۢ;+Sa.,44M}޽O{~ϣF5ffUgXL]8%j?7QU[{=o?:Ҕ8񬂞u $@X&,XG+]]]qtϳ>>l$IIW\UUU=jU>gRWwµ]Ů~t_WWWDDiC+y[n)x&,^{-̙sܵcƌii+W'x:7|s$Iik_,UrQoEq'\ױwO4`ytd'^>L?lƏgq1")4*wi1mڴXfM[.}x磩):::6&M\rI̟??jjj r/3:}]EnA fH̜o>ٗtm|+q_={vǂl\sualذa@[__۶m{G{$sܹs<3Ki\i FRY9#Mx'bKy2(oiF#G/FבRSV*s51=WJ@92$j?{ 4=:i"8=4?6|@|ptiזz8 B9+Կ縟wz4d'cޣ,{={=!BBȍ\I"R, J-ҮG[[zRJ9BhuUrZQ,(\kbHb%Lm&Ϟ?R00{2}>~w 0\)wmi.W𺦀XĬ3{-r)Llwv:Ƅ|cB  W{zZD0qe#X^12եk a$I3xgѷ{wĕd1ҷ:Ƅ7ҷE͖:) qRwItΛ /}|g6cDUSSL8UMM1kbe: q4]4v_DK~"'%I~b%E-\#MI:ӧG$BWճfŔ.?|g̽"+I11E`qŕ/8zzcYD@%R@㬪1_~k>PJ!ԋUSz1PiBdjjbʚ :GuEL@D2Uooor( "rޚ$)8?ЃEL@MM;v<_$0: Hǧwm|*-b"׷SEmmmDDi=HDooOEK˳%NCeK^OAt=>3"&LIL&1kpELP4sF]]ݐ3ǢƃB($Wp>p@t篊$I2׍vɒկo~[ird2Eα ,IǪz޽K!"|Ϝ8پ}V>I+W c[n XpX[0R IDAT/8V+wK(Of5:Bɔ:̙'/-8Նh/b"( |>>pA!@gE#G1PB djjb-8߷7.b") zHIJJv޼[c>TD@9S@%4eq?+^) j:Hjk >pL!P>>o ѷrJlʚ Gw_W @YS@%V`Adg(8T@B($IqnCEL#PW*_D@9Q@eq媂ÇirH,R( 2:-jOZPpޱ~]D!ƕ wF߮EL PfW1c"%ʉB(35ǟճfw{irL$1i=[D"&ʁB(C+WitlX_0@YP@e~LTp޹~]@!*߸kS)b"BrUYES.bBtiTWw_W4kݺf?RGD!Lmm4,_QpFELPْ$$IJ^3PWQp6pPY4-MRGcXé8j~HI8_Wb͚qg: L!3zq *BsRG`P@eaٲM͑ELiltc0( 2WlyY3zwl/b^m۶Fgggc0( 2װtو"%ظץ-u`d)Soa]i\ZTC?/EuuM̜93N>yY\vz9xcݺɓŋ3We==uW\};^7MV|;G__$I"";ug;&7Ԕ ee9|p֬瞻&2#E\.}W>y9ۏz?X>iL6=>oy˚\w"""MW{^wܜu3f̌oGD{rg}XxICm_b'"Ihoo壾_*RUQ;o^yW"F G[>\ܶ/c0˂o}E`D…oK1Ixsm,P!ꗭ(8~vs\# {?y&}t;o}$Io'N4Y( BT54D@9X̾ n$l3Fi&hk;R̘$I̝;w{,X8.> %Jx˖:0z ˖GﶭzT9RO>{'k=4Amߐ׳f7sak|<#>Lڵ3::Goo$?k۟{DwwWr9$,PA-{}YEOKK4,[^TzcWkw{—򺮮n{GΝ/ y$IQr\|_;A4 E!TK""wmjV@%?|k7vgMCkjjFkθ[""e٘k1H4q7 y}Rq|7#IH$3Fwww}]l6>/g5|ި jI4,[Qx80 I8mьc>*Λ:&)S yQݹsLjG}x>>Gun[Hd9f'+""$K/GrYʛB0uEw5?]4˝l1rɬV{iӦGccSDDi;w0--Ç"HߩuG=mر}Fcֈ8""V>gT6lxrLsP|*@TDݢݛ@)7!gDǂ 'O<I ;zɣrwf+]]:K>HdP~BjXg۶.byނh˾3?OD9y{ߢ༩qє>ܶRޣ+ehqekkQvŽU*B@#F>ݛ7/ Ӛj.YMգޓD[Ϝ`ID$iߍݻ ڵ3nk( |ں;w^DDi65ǎ mqE>7qei:8۽{š!wx~u1'?`#n8Ufʤ*P݂Ku771 09}W,SNUx/Pt[ziiIġC`]Đu}}}qƇ>hooϿpHQ]xŃgrOyl޼iȚݻ[[F}{q"y=0d׿hmm4M-˗giFnڸ[k4McÆu$ſں˘:u༹yclQ_I:O2޽K!"|Μ8پ}#~ӎ/~>1YD\-(K%ccn rAGwl̝͎y~?zoٳ{g)Sb㢯/Z[wEooo$I ,o,'o{8D[[ې5}1cf8p G~M7?Xp$I̚5;&?qꩧEDĦMA{{~!󪪪8*dS).m|"~F$1I1eWڿh֬RG8R^ n>}})r*`8q_(oƌ/}-n >?x`룽mpm[[[ $I]Xd287ǢEKb˖gڻwO۷7rgK./$~|y.صkg~m9b燼S 7|*;ߋYi:g-1gꦛn!ॺJ!"|864 -+l/:4&ƨ>7wKƂ%~ rX[L<'O+|W̝;/4z{"ں8⬳Ύ~Ci7D$`ߚ5Ē%'9s֬q퉞r0)?).7>_~ed2d2qb100ӦMիω/$_foꊪ>}Fv7~q s׮Zuf-kb`` JƊWN|-3{qq)Ç"Ng5;^IjK$+,=\q3g6yo_+Z_K%~O"zĴK.+r*^?wKƂ%~ rX[@y5*N&S[swoi)b`) VpQYBPB0EL'PF( R$xS@z46+ C!T$I~ᢂs0q( W7Rs"劘/ T@E ;/ ՝ "I {1 0^Bjh7_pޭ&0-\Tp֣&0ԏP@ؿ{w tt1 0PpaDD"%ƋBjp|d {@S@@D݂ ) nᢂ["狘k a0}k a[pyϖ"%ƃB MzsPRpaYwBd a[o .b`,) ~Hٺxa1&y'FlR4XR@HF λ/ 0SpQYOKKiZ4XQ@LݢEn߾"ƊB`. R$ػ6s ï3q$EergVB58v2t IH55۟mu>;|Iev4O$o/EL׋G=__3#@[ho$UWo/AP99In뺎E7 @[h%@q==Ra ?ݿ>M!h IDATaKIrӌO/AT99InׯNc^g||.BR{Ir[{7x aKֽN3=BROO?'zz aKz('~-@?Bb{''ɭN3>>!lr޼5e||Bbt0"zz aa" -ىerӌo!@[$zz 9a˕Irk.汪o -79IugL\1Gɽf|<!lA(_ A:`o2In!IrkΫXd|<!t@[p}s<!ʗzz PGb i7CBGIr!@QNN[j*#Nu,2K:|FDԯN3=B:bw?O!|C&VO{&@RNN[=zBr2In˷2C:LO <;%z:B^Ir^e|܇!tL9IB85Z,26%@Sq<`c1QK0"BGYrooM BVW *ɭ2>6%@T*Vu Bbn#@DDD1'2>n#@DDD բ3B ""qz\b1;|BuLO(v?Oy7 Gg&@|P GM-BEˌo? >(Fa,|g ֽ3=LDZɽ2HE1%f&@ߊ!b "@| HH[p],߾xOH[0"3=H~?7ovw88L| !b8JnLBŨ%@8g|'@|X---Xcqy -uoLOO)] nU GM n @nQ:@b^g|*:o~ nU {3gzD OEo0Hl @ܪpܛ!$@$Qr A[p&@9 I( yk:6B <@H*{3ezI;eO%f&@(-!Ъ!d#@jcZe|%@*Fz׿f|%@*ֽ3=n Z^ro泌oV^/Qrof!pb @!|k7obg|$@ܩ| t!p^/7o; &,.2n 6288Ln Ba#@_!Ar[\]zGHqxV˷o3 62xFD,./2=n 62x .$f=- @\Hq~2 . 2#+ N ɛ!tKtd&@] .,[ lh۶5 ۟Nヒv .,f=-'@\Xq~2 6#+ kM#@e0 < @Wd+|m!4y;8=>q l/B`-f6i l7B`-YG{S{\KXh<,/B!,e]!Hq l/B`m'ռ%ee:@xa!Uˆ{ZKX 6J]~%yo5Nde N .BY"@X Nd4ykym>@'Lړ8}5}N FD4uN@'{Rj>=sG4'8xR~k+h2 yogw–:>,,۟gٽ:/{oِ!l! YǿSܹW?o,/Ue8>zFn2Ͼ„{h|sηdE5!C/spn~m8kN:BOOʊ"}lX.~v! xpd7,qG"rTN5 IDATUg]#@u^71v{яw-50PGnމFeug]#@u<杦w~+U`@OݼuVy]"@uڥnyw~䭩h۶ v;deヒǏ; Bk+f~y7~_k啎ݪaDIUu=0`s]ϒEh7o. {"n,/7˸uxQh2Ճ佩ο @;Bxf}x}C"2G.f?i\?|1'ɿ}3?{0n(6)+x~歩"aKܺQĭEܯgw8:>Nqxf+lɊ2yky/`–V^-4mytr~<^`de>m4Ec`K[ zZCʫ1t)yoyk`;FeTUk`;+!!Y9MޚJK؈,G۶= 6"+³Oo{\'@l~FD4uFLb彩<crjT euK`ªq !1Y9Mޚz>B`cLޚ" a訿10pƬ FDSW=- 6fڏce{{\&@lh4I'M-@/Jب,/b6B`VE6B`bE %@l$O_E\KبHmc {S=-a 6jڏc4$B^!Qy""y[.=. 6n[{\%@l\ɛ!B`" \~u=a 6.47JظI7%0\M^=bx0{kET FrQ  { gm◿*,'_͟m{Z8l_{\#@ow/wnZ]FqdWf!@BD|e}ܭw{H`5BOOmY/z\#@λ?ƃœ_=hʊ"ykܫ:y系ݼ&y:@\,=;q !;ݼs;,+vGEw<杦wY+M]G?wN;lr b/NXEwڥnywh<իrQEww޿;.ˋ䭩 Ewmz%/OqѢ2@ B9\]2)"!"{7 2nHGgY\,mp?|sGo,ohvʊtm8}Mk`8&/{?i\?|1-$røu#&y,1= qFnoz4oNR6x"W^ʮDFm#Fϫ_`DZ!gޓ!B`3']h.{GB=B`SM?do)ƂKnڕǂKnR@M5e)%@l] . on z jV!Xh MT9@M< O$@!Bm?do(@!h|0Xp K\ǂK^~ޒ! 4Ci }5P'B`sm?o~Sn TJ\ ! -z k?a9Xp Inޓ!uh!{@C L!Uh|p!PﳷǂKN@77͛k>@~X -: UhXh Iy}[,@ CFB!Pﳷ9 pj4 .hB }6O1,@]j4?߇>-7*#@TV!y -hy''{!P ǂk.@U~(@ }6GB!Pc%PB*͐q,"@T‡p{[p CJ4BK.@U~XXh Epg) p9Ƃk@u8\  !IN[!Pf5@8!Pvu5PB:M׭ -zi~!G,..4 p|*]Tp ARu[!P볷4 p|*5Zp* @T1,Rp lORuےR~Up lORc%PBJn!P~v{b5=BZmeos j>{K!Ezp*'@T-Xp lOV[bXؖ!P벷%pؖ!PWi - jnb,'@Tk߇ӧ{8@՚4\ <łK`[@՚R xUk>{KTp lKz0eY Uk|pp}]p lz@!|mK9 ;nnb8}J؎!_*>7t_ÿO1@%M<,KUxOp>~=MӟbTg8}ݜ]doi_$t IDAT!P/~y!pgB!PG} Ls E}Qyԛ7g+vB?onO4Ű,L @?}Esxxm&A K~ssǯ:!PYs77a:!PToOpߥ)$8@Q͋E!|@Q/}~%@8Mo3 !Pi6OmA _~Qon.$@ 6?"7e:f@MNN?__wo{} _*]}2|u$WngiO~"|q>N/.J*|q^_}?O1;_,n_ɓ;?S@yh>{KTp '@a@yh.{'Bt}(O8e) Furw^. F4M@yh<@y=yv{k,B4] .t]&B}wArQ-3eF7qbD3tЗ!0·?:.Ĝv\C{ S޼ץtZ} C9mkY:-{]!0nb{-INLE9  B$@ '"@Ĵ ,IN̩y֎kB`81q]C=Nļ !ԲtZÉϧv.G.px~nCRLye Fp)@bCRLy! 歖%q <!0s󶾽ϟ C !ZNKB`H1 C:ay!s sjj) Ê)7ouY:. F0B`X1mB"@ +a `.bj///q <!0a!\Υx<B`X1o"@<av潖x,B`X!N.CZLyEyCYAZLEyCŴ ,KX׵xB`h1kq <!0潖x,B`h1"@xlk `b[]!0a `bN[]J%8b[-K%8b `~bjx B`x1!PKG^L.K%8!B'@ o߇xJ{]!07t\!@L!ܼeCf0>B` 1 07B` 1m%k5pbn.Si#@BeCB.M)B&@L#[]!0sV!c Fp `l4bN[-?B`1歞!05\: Sڼ_Υ?B` "@i=5Lp] SԼղt\%@L%t\%@L%ܼ]! Sjު! r .} S_S)oi ܗ!0xJZNK} a~mR: C< 0(B`:1mK%p?t[R:. !0x `>tb͛! өy[7\q ܇!0xʛZJ%p?tbJwBF$@LgkC^q ܇!0nbJ{-B`J1歞}S)5ot\!@L)ZJ%pb `.b[=k5p;B`JpO5p;B`Jq#@BNK>) 0B`J_as~ `0v]Լײt\ Vt\  < 0B`ZF0B`Z1mKv9q FVLy[k ן?;ӊ)mkY:- stZ r /ͻ!# SjjY:.S K%pB`j"@8m/ DZ< 0B`j1歞KX׎k6z ~cbʛz.mi^i F)R Ӌ)7o\:. SjjY:. w Kœ! Sn!0Rv˥xB`z[°w1N6!ZJ%~'B'@LoSڼײt\#@|񔚷ZJ%>!c >r&@!N,@?BC8ԼUB @|tj.//a\:,qkQ DF[yq *˲"3YwrFw CHyj-ߖ~c?  !ԹtZ!08{-s%C8vͻ!NFSq \NF̹yq \NFSV!MFrq \NFSVq \NF m[5pB`)q]c1p!B`B-s%p9B`1OZJ%p9B`/_yq \Fp)}-.B`(1O[-!0sVq \FJSq \FJ:eĜZm[5p>B`()O'@ e/@Bk;-CS޽Y$@ %B{-8!0)7k; É (B`8i:K| IDATpRۺp<5oĽ\:. I97o+<8!04MNK|pbVBn!0t:p84u;9}-B`Hi[-8!0歖8!0 K%pB`H)m-s%pB`Hq'@XAR۷k59B`H{°mag {s#@ )i^i G.|i5RKsy>'@ +ܼ"@m Լt\ snj;. JԼR¶m>B`X)a]c1 B`X){-s%9B`X1OZJ%9B`X/_yq  u8Bʹy_!CZSVK CK97o>B`h)OZJ%OZ:>B`h1歖9lq CKyj޶{5&@ m/@Bk;-}wu 6C>c^KǐNy_q Ë B`xi/@8KM^S.K%&@ /ܼ2w\m^p)@!0cj>&@ /Bi Ky?@.h ? "@ ;v*@  !Snj;. b .!@!"@  BBp}!xjk)a۶kW!4Mw\ !S޽׹tZ !Ip B!c%E !*@ J97o"@u  "@u ¹!tjR:._  !%@*e s53BWm o;R ֥tZ x{E xup潖!锛KgoE x#"@Oa Hy'@p=o"@w:Hii L.\!BCCJ!><4!W"@N<.\!;)k :މ;ZKBwi/@X:.I \BC m:<x%@N<{]J%!;)"@@!潖x!@pZJ5B)"@@;ZOi'@x!@xjR:.Hy'@XK!Ϗa P<!Px!@R:-k /B } 3Bt[]K!a;.B \K t&@АԼ!} 4ZJضF'@wSq ݽ۶QJcx !#[)Ĝjv 4̱ i*,.B!4?~u B<܋! Sjne%N`Ea͹%N`ŜKK!cs+!Sjn;^V[y]`d+c~04Ba-@8MSKK!9"@@+߿Oyn%02Bi ex # #J B;[ B;BLͭF&@pcsF&@pGH;^c;@X/y]`TwNTw  cqu9w ˲t q}:ssY t:M!^BOsjk/`T160*B)5sK!Bͭ%JsLͭ,!J^.zx # xv$@^st  x@iu/yt  xc:Ws!Nble 4j/`D 160"B)5䎗0"Bt {Y0B'[!BDI!Vsx # xRex # xcs9w<)ʲt?[KK82BBLͭL`9V؉!F!VL`9V/vu (Z_6Z NTrt G&@^L`Bs9w t>OBv @9VJ`!V.%!al'@)@{  v%!昚[ynvblnt# ^st G%@.@V;?~LK^:^ # 5pD; 15pD;cln5玗pD; k‹!$J `B16*@F; k¥%!NBlLx G#@9ս%!NB{ɹ%!N;*@;9tws w!rǿgnf{c{KЫP-` RAhgb%BUDĶ>J" WiIH[iL&sMsfw2gf6vw'KrATnuT( ̳%5 0B 5{l%5JgySB vY7q k!@ҳSvk\Z"@PytjZZ#@Pf{yԴFBi<ϲT16%l5%kۥgy֩q k!@Ҟ™Tٞ(=fYKXK*[g!Ibi{ԸDbv,ϲT, <ىb~5k'"'!@Pyg35-`- X fYMKXK*X.<˺nnd]=NV .tq+˺H1׼uEB`k_ύ+=˳Ap X\_ώ/=;٠s,n#JϺYg9\diu}a :!2mhuҳtBe~nd]Y :!2m<WL_='OFV1 !@q+OtB>c5u躞y'b B>"s#L S#Iw[_uӑ\>4W{qU_OcO5NtBm4[6V4@XjXv, +(mvX>Bl!'@¼#@ #@eQEkf+.n̿jkf+( ;YMKv+(m- :5-` t|<"IJϻLkf+(i4"o睬5 3BۥgyS! K[ҳn6S! k'J,q L`VYԸa&@VԸa&@šauj\0 Xai<@8ʩ(0VX.FDt;0VXsajZ0 Xaika&@VXcl,sBCv `i5H[< `i5H FnG  mL Ԡg#@jlYEQԸa$@PG0<_}1 %Bˆȳ% +B4۽]B @PƥF$Iy 7B$FVy Л!@MDYW%ԤjNKF5IۭҳѣGE7m7n,1??{ꪫ*߳cǎw+@PN"On-;SԢt<155DJEIĽW\qE% T*N45.`X\Udjj*:zϞ=o|cQ|0"bƍqw>P{|SQ&0] ^!&I7xcٳ'/q}~ضm[r-o5nGs P^ig_v{KٌGDĺu_r]:;w?կ<IDQ|%yT$ٹf c+ ̳; γ%l @<_0(ϖT%l <[kယ?³K9?7tStM}sw?>hDDdY?O[o=ڸqKp1<WnK_9&Ƣ12R&>-gKAy /gKF0.E:|򓟌׾.&#=d0Ln#'OueQEDD\ve[]vEZ p1%335-`h͋^OOOuoڴiMj4uօ׳177WXLv&\n˖-iGDѣGË^o۶m(^9s&VeUgKEdt4ӧyvral <[Pϗ ʳ%H> IDATU| <[0(ϖpaڸq|XDp@f3xꩧ(x _ONN_^ey#I("f/Sq:y;Eq!cVt_|gݙ̿3X3<[0(ϖT%l @<_0(ϖ ʳ%64V{Zk׮ qeݗeY<䓑$I$I]w[N8esԩ8r[-mKϺ%  =$[Wղ -w3>}#B@ovy0 +w#7FDDQ fgg{ޓyLMM-D '''n8 o~cvv6$$Iv|'$Y!~8L EQD$1==wyg3cjj*$$Io}k|];\(㳟l<3?wIDQj>ϟЏf]zd5.`X4W{Zr=?8x`E͛cƍK/ݳcǎ3.(z_>E!|0&''cv瞋N.,o֭[}K[< =`-iZ~7nh4$I$I/B<qF|C`Ǯ"xnH"3HeyH ysR6"HAP5vAx$N]Y8!ԩ|6߷^oy/!ܼy3\r%\v-o^}W^yq )ݻa{xp 'jlo|pׯ*0TI ~^!i'@Љp = D45\@:Jz+Y DLcVrn t"s©z$@ЉfSn tb5ZR7iBYۍ;)K-Gi-G9w.M7!Ʊz/97\@o:SVp  HLsp  HLc&@p t$T nYcS%F#q?Uoe BTnnͦz"@Б8rkh  HO2FK!@G8Z ٳaX45\@O:2 C)U%k!@gbK!@gbJ[ɹz"@B!@gbM.'YT.'©z"@Йm{p6k!@gbJ^7Z@O:8{/yjtf5&FK!@gaXk!@ga1{ɹz!@СR&@p: th>@85\@/:DZzp  j?Uoe.i&@ BqJ 8:S=8GM5@Cqge-)7h  ’s%BC0Ve-@pthS45\@:X.)Uo<5\@:5 ,97\@:Jc&@pt*T >iJ.)Uo۷޽kX!@bgentjz @Щantj Ù3{S5,MS02kX!@8Voe-@pt,m3M 4BR=@Xֹ&@б8!i"@б?Voe.`ic:7\:S=@};lk% t,s%,Mc1ZX-={FK/k7pwS™U ό{Ÿ/=N tlgo/ g΄w^rnx?_?,\y>]?exO ?' ,g)͛y+yjxQzl̓?U* Cng̋i697\<㣫7¿g?"N"BέRފ!tg__"N*Bp (~cZI'@йp u~A͍1-$ \Lz+yjxX?zw8:X.czXd \Lz޹6\<{cyr,pL0Ja7Z:*%pp{S q?RXO(b{vw3Csqg%+@ݻί~n_wΝ .nϤ/o;{ǰf΂3gU-Yw˖2ek Bd0a*ER3I2!!BF1 7cKF޵>_Kng.2ߑlu_wg?._B@ jCY׼75ty#_jwot&wc]ߍsύo}[dW^վ4~ ύL;``E/:#v?{e3 22@PF#& Bʩj8|w#:crxwK,ñJ:ED {9kڻP^J6ff3a??x=PDߚ5yi=G;ϣ7љ(~u^<j91pX.ο j ~Gs}quI'~c'1.pNW }yl۾?1y=Xņuñfxhڿ!@ Բ0k7 P{v󿿦Q7_nH/?-%y_,~{SOwأ"NXŒ/~CQTN{O֚!Cn IDATfJ6T<@ P?|6^Scc'uw1я _wVXKb/qߌCߺ#9gLMŲ}xFF2@PF0Lɨ&lأ}6:'-Z7}8J:m:\zY?֡'̡oz-}C^S1,뚷X3?վU[ Vʧb')*'}#~%OwNe]vLNΝvZ-_՛fԏT*-.&/}DK~݉X7*}}y;k>>iUc?||G8#֟})} Zf$Y0k̈́M㓱볿ÇX~+\D͊Ub1pwߙ8%Q˲¬ @NΝ]zV3뷣̳5;j__Gyv_y}3@P(V4lzS_$oEpV'???N?hbJ5 vL^ݑ7]o*==}.J굫 _wY]ڣD D- v'l޽?t[_NSĚF]nm&j|g$ 0[M]{1uH׻~o46^թ ŚfqF׻_jyD D=kf ܵ͂K>DO-bտu:O?Ç`^2@PFVu&&bjb"a'Gy:F﹫M+_Tkkb\כvQtD'CF׼='jdd/t Ī_t .^f|8p˗5`>2@PF5oHm_{T*W>g Z3kcG5`1@PaL"&^z17KS$j4}}W?w{|3@P՞ި0@P"QN'a+*%Ro4 Bfȭ_#G 1?Pc?GbdD H- v ޽1z=]o?򱮃sEcӵ]oveNBeŃ.ɘHb֯Dۅ7po[FuC_uFEJe]vL؋/Dy'R$l5ԳF 7of;M!@x0Ob+5_tODDmfƵ:}.:SSJ0 (  0wl#?VWbɻߗVVc>V+9Ý?`V%J¼7`.t:qfO;jCCCձzs/GJ!@ԲFaMˎuH76 L=kf sDծ7K?T:d-o89JeY;6`h.976\Q {^,xUaޙߖefdjFa֙m+:v]otsT*D掳>.F3$l@Y (Zuy3QG|p1ℍ3ό/;VvKFB u l5DM+:N]o~CMg~#Q br(#%S˺ǚ0W?W0=/aŊXηt:q/+@) (jOOT v3Oз5_w%j2Q-ǾpLz%a#!@ ղFa p&#>RW-)awXv9$BeYaΛ PvíNa }@Vsך>¼yh>eb PVSNOF^xuqwU;SxWDRηj%l@ (t: P&/#G]t{ze6 GvŕyСz0a#!@ ղFa9~<: P&[w쏕^GZ|f4t}g]mY1Je]v'j@lNE<՗?h}a>q6`3@PB5oDM(q˅Kc5'|gx{:k[Koޞ e`Y5oy&?zCDrwz{jUi^|%ѻxqs;6`63@PBz=y;o&l@YLMLOG{ċ?d}Uk^T*]zso$jlgjFaM({Eda}%1U[ OWy/rc/)Ǿܿ/a#f+%U 3{)̦*عbIj`Z^^xgAuGBZJf&Į]qKΉzk+·^Ձ¼h9B7Y;6 5yzuqy˦?? o0LNF 0 (PV̈́M:V4=֞ {9k:#;RwN'a#f%Uk4 X #ۺKS)ʥΉo:;LT$"ЕW{v3O'llS?x}jYV<:NT]V*_).m|"&Kذn8 +;-Rh}F&J6T<@9~<:Ǣ?q' =KŚC@Eڵ1Km~FGpaf]קht[y }Ft:O؆T*tU T-˺f3QfT47[׆lCFƫ_}Wt6`0@PR y&VGz"ZMF^O؈Q5 8c 0[ (J¼=f`y-oMԄjo뚏ug&&JeYL٦l# EߪU Z]_y8~`FJ5 v'llӼsD]7քmx=pcq=w+`jYVf&&N'F- Dvŕ z ]qET {NBg¬ 0{vG߳0Ϯ:}} zT{zco)y3ƶ~?a#N7%V˲!|`K|u'j©Zַu}g&J!RՊ+֞}gwŊ| g]6t2@PbQft:m ੘+Jjo뚏}g&nJeYՊc`6z8T"rc2L7]ŋ cdF.J5BeY׼75`681uXa]1aS#ſinٜ B uy AЃaW+ôY8ፅ'hFJRGupAaΛ p:c X3568ҕ0@Pr,+y ӑGFz0 ĂK.I&`s"&[bI/.`8,:]R5b֯jn6ȓOХoJ  \mQf&C.jOoFi=ˆc`݅ض>z$a#R1@PrQuZ:v,aRz0 e1xmf׷<[wxMٺ}|f¬jE B e]v'j@jűvCW\z=a^?ۺ}=/Ņy-љJ6wZ(ڹBhlUW'jO;AZl5y߱=a#fB9eBil¬g;묄mfd==wfƦk-5 sBHLBa>+R$l4zygzޙ-V)Ƕ~/O&lL2@0Ԇ[朱Gu͇6\I:K}N6=&MfSq'`& jYa{!\sd¬pQ}N6i\~𴼳a34Q-O 0 n(. IDATy3afZȑ8| Q2FWVkʥƪfxh,c<S ػ8LOW\jS˖iJ-Ԃt7 / HzgX-˒[qetwiΈ Ǔ{o`S;z7GFɼuR6yj{=d6ڌW v2`/u^If娟Z& ߈'W^'Wb3n.](do3` (uhj>v25`/wwV2o[BF_G/\P4<{.n\3LPJh棎B2oldk3.]ըV*s4o?iLF~7GZdjd6ގލj/!|]"+{#֎_>ջOVlbqKqJz5j^Z-&s~3u!@ T森Bn2Gڕd8u:F3cὕ z4ΝOUnވ .lv2u t?ѳgɼur6W_*Mvv{Z6̚B(Z¡B{ԼyR&g ;m6̚B#^ZzpG۰_U\nƨY2@PŴ®BlWɼuR6w_2M] d$v dkWK35 hdo2` D: lݫWJ,n6wեh]{cfl (NfγMOyT*q~t8EZz!@IL 13`VׯEL&ɼyr6s磲;m6̊B6@1v25`z׮$ՎpPTuR2oǰ,c#f!@Ia!A3Dc=7/\Jի=/x+qK'+%Q 8 ɼuR64Z'm6̂B./GVK#N֍dVY\ƙpTur2r'FnF,%QThBe2Dd8ե8Zt8G|exiJhM >~]2o_؆z&ɼ{J6,%R v:z7oLgjAV]Xڅd޻u32 Bߺz;W؆ur2loGv6 %b[[1I2o; ]ZDQ$+2 H2@P;1B~^d޽~5&QF(%R%a!Aѻu3U~t6Ad6vc~6(%Rld<՟2@X?y*P"*d޽z%c^BM $Fn2Ǐc7ɼy|6EgccɼwJL&xJ6@N'S^T֍yZ&MdIlE6%R~dk`ߺjo o eҺ ^ /!@TjOc`?lG;ɼy|T*(ŷߎş JRFj%sݘlo'm(֥lc铌m L>F]YtX8}:_JiaDDLMxJh٨,c~Q]glC-}x4j̻Wo6TJhM dlO~U2o= eUVyR2ܻ`3@P2E;=@84$onݜ7 d6Z/$7+ObdjSGiñ{PfSZ'+S (ba!~4̛GR؈2j\cedvc2dln ߋf2o[؆yкt9MwV6B)Zb+nLj4ΜWE \ƙɼwFL㌍ %S $ᅬ/Hs3a.^Nfn'60cv!@|} X7ϭej¼i]n\2@P2j̇N6dV۱0Oj&sB*d62@oL&o'ըTwk3X1(ZP23@@,&I6t25y6?,ƃA2o^"y'jG$ލbVkj>>YOJ4N $J͵ |ponfl (BTd>t2/l$?Ai p<JhBWgyj65NFeq1wo\1@PRE!~郘lo'B^1L26/ ()SF:,h< l ®BW̖~z6kkSލ뙚<JhM  xƛdX]#o;&sB*Q2o> <_sB2|i ;2O (ZP2 x%ɬǎel7m0&ߺ !@IU[d6ڌv6DD76Yɨ.,flW?v?Nd|~!GFNl}y,=ʪh:LM>{܉ïz=?<h9;52@ T_uFui)*!O&0>w|O}2<q;xqÇcym#%V!_>{w۽?y,kL4ϞMfb JhO  x?`DʳGSYTj=Gr'_"!@M velP>Y7*U[X|YT8d޿}3c" ڴQ @|M7}ʳaTT~T2ݾ J2@{ۅV}'N΢slױt^B+S^w϶v}QT䍓^aDDLM0@PjS;݌Me{8ʳGlT,΢wމk%m0@PbEP2{13(ZەJf#FgAnJ%g%혌v? 1@PbE=5뇖vuWzd>|iMÌmB+Z?2@djP.OHqq2fU LوJ%n` (m`/f+~~w+ϾJfJ5޿tn`OV,}x4o+0 X^(dnjysaATgNlz,s12_JRDn'a!p|r%Wƣ8d~{1e0&o+0 \mce_!|4\cQYZN۷3_JhJf^NRQHϲ7*`NVj2ݾ$c#T{[EFgڱ7bn\$lI/^fƆgEfoǏc3/Jna'c{oﭴb2ǃ}]ĩ`gM{o cW]5uk`/l?z^/Ofcbae%o`> (JfB0Լ~!S̹dNLÌmB+d6vc2gl0&ފgl8 lmmB+ڇd^/_90Lbpd^?q2cՈjI%WSa |>}dnh4bcɼg=ccjBYܻ;5< sl%Wm6#*d><w?IfñJ60{ӫp2/!@U(Zd>t2(ɬqdTCAQYZJmB9P! !v'Oel{REDwg#cb`-9 ݝOVcu5mC6!(SfeaьwNcԼg#Sb`Cl1@0+'ND5rXzZɼg=ca`v2u xs[_|'Nfl{RFd>`~ ad R6g'I6!N'&Sr؝ɬW2h#"w35@Nh _O.}x4*Z6z+js`aD:LMi2yرm Jӫɼg=&IFg`?N_~ Ϻ@il>4b2Ic3V#*d> @ (/ƣPO, S?ѷIwDH IDATJadVY\+ZXzd0@0+Jʽ'Yڙ>@qu(FV^j2ܿm!@}l+""ɛ] ~ٚY'DZd|x64V$Nl>HO=%=E¥:׏b[[\vw&5Wsd68啨&m.%2ip8cigOysu=c>Z-kɼwm.%RṄn&Wob\7@>HfÃ{\JnȮ癚\\dV7bB60>I:= !@;@&JTՆJTfg3<"H^L2v L2̛kDcu- Q;SThٳÎBI>,F^2oxE7>~Ϟd"]ǽXx{s$zgbGUR0@P2E!O+@~2W/~މ?m/>?[~>l8N\O \>JhBz{ɬvFW37o=ɼmB)“M.Gc|r+@_Kavw|1@P2N&K`?V*XZrxE7<{Ngl.VKecdjG/^x8%whdlg|ƨZ7$wL LѺ:1v\,dX^x|p*|B2hp:B){qI2o 9:9& pS v G87WLfkũӿZ-ɩPJ:7QD |W?Uj/,dly\Z?s~V4V#r@ (JĖ\ dV_\J qocT|4? vcttt*w\vJhٰb'j&u}θ}}.[1w''}{!@ I ;_Wɼ o?kE|NBM  xE`bXY9VۜnDDD^2@C (ba!YٌoflKo~˥W576vb<U?21@PBEj2v |ۤJTrV*_-~M|ݯWⷿZjJH~7 bT\f.@~ĔLxd^_^Gk7⣵i74uqt<ٙ"Xo%ooDT*kV4VVϪ>`n7ƣQTՌӧ1z"K[S+1{g!=|mގzu)4@Q^W`&476Yog+q6B8@n'SOf(f{/c& ;8I6BŽBAz \ /25 PqQ$1xMͼ^ܘO= (J# Cq%j6py476B Tn'ay&ө?1o,djˤ“/g2X n'c?Kf37oFqJ6py47LM.%UZl1@Of啌Mr㉃m)JjBFA >}+RT{[R BQ2o,dlOs#=@xx ad6v36>tXF}q)_ bv; ;c<gl0]ɬ~g!Sz#28 ԤImK0=@XY.JQDsm- BM 8t25.×/ϓycy%cɬm.%UmM  Jjļ\rc8^2B*Zk(a2jQ}'c+E{[\ J:3f3;r$; QKZGci9 .%V!PVlXɬ(cgĊBoqٓd^_Z.I~?dl0 Xj%a t<|1'r2Pyo{+S!@ly tâ;wu2(Z2@*%Vldl0Y+%LfL7%6qc(a2/-elL;3nJhBlFA=47 Yhn#"z;ۙL?%Vl8c5x0bJhE7ƣQ6&7ʭ11lgj0 \ma1|W Ycq)_ fnGqj2 !@|1@\^0&r2@T*ho$N6!@a @~GO87 Bvd6x F~6!@U"?mNK\Hq2JRFq̇N6y ̍(\Wjɼ t1@@m@9&R&Tgf ef(Zd6v36g<`aci9_dۍh 0@@ N&}Yy}q)cۚlO3}5 .ļdKcmmb`  v; 36ȧ0ծ]ڄflWc揓yog;ca(Zdvdl ՗^;;LDn'Q8c7ca2o,.elNs}#?"N׌mC pjl|r~?f3c#u|w3^>1D}эƳ8:lkWqoc>ok], E=1v:KeļhoEuJ^xmA2W=2n]_|x3~~{y | @+Q{Z6Th%Gqӿ?~䫗ߏA Bh&N'S<wRdl47 FGpoF}479  *ZTd>v38{G&ŌMIkl|r׏y7}oΚB""h_MfN'c5|"|aj4VV#"v^;{b(d6>l = Qףp7vx;䫗i8  t268[YV[2Ocu-nrNd(Zd6v368[GwRel|ZzW<{|p*>9!Qldlp?Dcu}b}磓{t|:&DDD6@\~㓓8zi27@gf~~+Wgkũ;;s:&DDD}5 .Ϟ$!*J4VגykWrp V+elp6L tj'A94  ""j|fjpv&ڍQ]kl|rʸu}}}.#!Q|a HT &+J2/>N=Y1@@DDa!pO l Um4{qos9xoΚB""ZGev6;38}'_n7xSdγrGmo~( IDATr1@7V+ ;MNуyb&hO}'WU+w^[&}\+_-GR9gvE'Ϟ6vaٌo61F;?Z݈GO񧭧3aq]o|z` ĔN&o0=@XJ%cMܼ+Wbkkfaeh]a&b&ۨT*\]Km·BQ&a!pq8d^_0@Acm=c|r @~Fj%MNѣy}!\յd6>>gov2v(b|eXYTyow'c ڄы128=goݎL6*͘}'v3!(ZWxn7_S l]xW͵d5@\nFnȮN&g<ѧy!\(O /|h?li䲫z^Et;uDGjFսUeɶR^tDO퓀 ϳi:l>{ nס>9󃃄koU:zh @z!!p/DK|D||ׄk ࿴uѻ!p5w~}Zv(FY5i  z '\\{O&PWU5ۍުŻK.!>^C9&\!zZ..vwr~pp pY_-HKO4K]y~L7lNN= W-$@'!jHۜzf nb4WGHCO \ 7C9=nA%U)Fj<u]'\pD]^-N ^5Uwn>'o^'\pDkg'ߏޫŻk^9=: W!::J B>z[\&\4zVy(7~%U+Fay"lN \-B> < nr:Za_Rq4~Pxn  3Y7 +g[gZYp pOBhxp  3;=Bf[O[p pE:j,%@gBnfYS  {(%@guYzp Ŝ}+~5@ ( =~5ujۡ(K&@@TEo6YOA%6(Fj|NSB`i=|n757߇o<Dex H9=~%6(6WDK.!QY!@ G9FoA%6}(d?Wq5GBǏߢ|?`[QV 7!QYSp)@llx-I1W'k.!Qe:/+cvK70z밞Dex0TU|n @D9Eo_B{w7`[O(K$@@Tk"lA%6ɺݰ/j2Nru.뷞ϣ`?`Q&@DDen]nޅjN ۦ8lk!Q, ヒ붞M!e0~lBy"K @@ۋު!p,zkݟ~N6!Z |;BeB^Y<@VCx y z_vJ 4j .U~%ʇm5'\h :g='\lbx~64ʺv:㏰1z ۪$K @@R> vwr2N(5YϦ[+p Z;;!$z8>JШ)@XaS |Ah=yTF[9'\mh !jHSY<@.]1O={5_OFYBHlB9G~?`jOv?G韛!p3ШVGBH &hZ('@^/z z6GndNp נONZV5MP͇͛k!gj V5Xf[>O)a}59J p^Cp)@Uo6arwkbᣐ=|k!gʚ B oC}rBˊ(z[.:1@ ZϧN_bt/B]U \!gj .,zkw!{ &)^z 8S֍7>4[[Vp p4C(#@^<@Be%!i .n/Ddp p{FpHpUnb8V㣄K.N3e=B`;߼ i nbtYج \!gj߻BE@*fp?n6<~n  pVn/z@len7&jEtc.HszBHc=Eoy?jy{9'\p1NCZ igagOɊ(z[M&\!5ۜ{?HɚY8?B%v7B ׯBl|p paXס<~l Ep.YAV- wU95;{DKBE8 \n/z.< ᇐݿp pwwC>؏W$@dr&.*g$\p&tC.@si &l>|H7yɇPy[Hsɺ B\$ZE 'oD!p1px/_owoW<Eo՟?H NskvB+/rp p_YAȺ́dȇa!q%^/z;]K ~?6о]z.@lB.$kVE%]7jB1F$&\Hcp)@\z W/N_9=BO[\ӿn @!z[\&\v .Q15WI!  zKn2 l߿Ov~12B.)Q-%\vy<@Vp p۵Z?EI1\HSpcOOn!e+F$up mWE%mVuc0 x26>oӍB.$=hWe%mvaZEB B(O B^^- .Ç!{zVh IDATk&@"vvŻk۪)@臄kj0z/k&@Zz{X$\Vy<@VsyCpy:F ˺Z..n譳p pQ>6O paYAV- Ϳ? gg ɰ^>$%\XEo]%mOk_2iqM]'\ ;Až&V5 -/Eg~1"@U .-F;3<쵙qߜ2u9ˆƉBv|)BZ>}}xO|~a\:თ}{l8Q કk{P`;Vc3^>pi^uH$ϼ6ōxBZ^˽Z-vZ!_,}۝e_بJDiZ/ ક"$]\,p Uzg{e_بaoy)E\T?BjTu^pqϾ.r!_\:კ'y_9! \ln}*7yO#baGs>ˀ~V:{_i(dؐrm$&@\RR`DDL}#۾x@8p } \Lz=m-DW=s:90O|x£FZ~$ITw]*@t3ktm-ՕA9y{ĭo 7t|*8iwlH^Ͻw[-vۍl21Q64u<[Zō!RZAg~.ͼWk X9qlPV˽w Z홙{eB6 4N7{lHip0JCCnU`k7{\%IթUB@lX..H55t|*8iglXn*p ӵgr.x㙷R/n ذrMؼ4MU FD4N@ +G2oBK؊R]r=;hglXV˼u[.vL2!@\[IDux}q-@ `zZ,p 9r97 PNpui7[lXnpU`'d+ccjNjō! ]N5Nn' \mhj*:(h ذrm$]\,h ;CQڻ78Qo az-m-ةzvt3Ȗ$I MϼN7-B6l^Ͻw ZTو4ͼW&& \:9y  @%աH2V5Nn+-X_uj*v|]X aID>y ֓ 8p JC{ \ohx}d!@ `Sz死P`'jd+.X#T2+ B`{ )Z-m \DfvpP$R MNfW%@[wq%Nzʄ!pNMe&@[;ۙʸ!pyK֢RzמV&& \9aw!@k{*h P̽L7 Z B6\޻4 ;Gyxo ɼ [LM+ uy vTMjlZ20={2͙{eBY&+ӍH{![\g[20`9tu5:gf \vlr='@(@ܯӜɼ T`g FD4-["7@ מmf*.##1p`}uDqc]O- \  (p ֩NMeVP[@DXkT,}ꓗN7"MHUp:X;w.ҵ_ICDQWI-Q :sg"z{e|5[:9y-/<z"@) $I )n 6H$S?sJD2V17eJ%rk ?(jW^/ڧNgKlkkؾdzͫcMa/\o2z=&@`kTfeuQgk. |D^$IѱbͷD#I-ߙJQ=v,ǎg=;zq󟍥O:Ttfg3/}/\Ԟ8Fȑ- P$BL&@\^9y/p NMeΝnni,|1GoU47}ƾg|}TF{m"}wǥ}6.Fv?X8oHJmZ 2y!!vy dv0"be{o5b̾ccV'b7<3OyjmXw$7=:F/^m\xvF[(Zh= `G `[Я^/:gf3`Rݾ}U,huvUo/$Ii q[Kw}{;b ^Wpo4~'#)q- `_?iy CR*Edfd(xٷ5_^S1G?fmTmKw~*ў9}EM;{˛^m^ 52ZviIT"zn6s` N }VOV;y)yW߿O$jBX쟾=Ο|hOW7E$ۼ`s2aitc>R О&աؿ5۫:y<93(7ȇӹJ%lr^0"!Ns&V$I \r'cG.M8wv~1mkTg?'F1?+z_ӧK^m^ 1k=ݣ\[?@vy`U&F20y_m4 \nt]q|0~O{IL|Dgh/gbOm8  `˔#Jj Ԟ K20eWO7]jũWr>+zrÍ1ʟHr~?)1sS]WiNlTZ]Oݎs2chr*:=]vşK=9&_SQ eHL@#Wv{Dwyi\9BT^ϼutfg#4^/p @1S譮d/Btάly߾e?G_EiR{U}W|ty `K[oڳ3XAKS= 4N7]}+[^^ɟ7R\ї4}wF$ɺwf1__uA- \ ~n63o45ŨxcD)V`7h}#13]ٽOxb Xs%Ro~eQ޻ C! IDAT ![*7@ ~ӞV' \PR!WoVD9q0Jjv?Q=vlg{q߫9?\![\ @4scc.(TmuZ+Ή]A|\} #)#U=^ʨ?>v:qZ\| lr=;@] ~ig(Vuxm}tEHJq_WвݩT}~ϿH4f qyjlZN@Z[^μXCSSn7ڧO7g>b_ÏylAv$I3)䥑 $&J-UgۍޥKōvs&^(h @ǎE$I}(p ;‡>yC_-'<1nxُDiϞuwż!P BTn0"VAKklfxp{+bptegQ7ű/Q޷gϿ]1?! !@* . @hf+G") \PɩjCE]7>8xxaV=v,&'r8v{~'@*jnKE^pp|%F5/@xHp=kϜSHr_VJC{K"8w GlrM]g~.ͼW& \pmk8|8hMk3V]8p@ ZFRG?ƞsq}w 7zO \ ~n6s\[CǦbq~B3^iƙ?Xr+7 ZƗb|sqna5kݨ SYys/gQգ'lr-'@ ~ndJ{TکNN'?~H4$)xڹw#.*K^3=1svaFD5x¥9{ |kc`߾ܾ@_)]>zvpMBgv6VFur*ֻt) \я߶s~c Xă4>`musK;8vS>%@ v3xKaDt%\VOκӿE|w~D|W T[=}^+>'@+r-Bf2>Qkk`(DWO r_tu5}_8+>=8w/:ssq5zX![./F.p PKѽx1>86Vk+I̼NOk%Mh3}nm{*%K}+IH-۲pXx`\r6\g^9s̜9;g.gLq0cW lo0AՒVKVZ}H hk5.V5۶g`e(MNG '/:ztU7r|Fxc38>JOQڐẓbnw7k`-kV\. ٕXJɓX;avoI=/o1P*eԊuKX}[#.0"—'?s%X safH 6LNSS5 k'b&;Qغ5bӍslΨ.xnFG{N`}1@@ JK2@k[sz:1+Nl˰ Pd`m괚Ѯ&>vb'˨Ij-FrWn7OR?G8E~]˖խ5SM *6XrQ57ɰ Y3g_7gԈ4|o)|;.6RvΞٳ=y7> /šBXn4Ӊyq!>&M XkN?P/)nrٔ"Ro)\._}g eǟ7NO}ºBX:ztBe[mV v'fj5:e~jUꙁ b~ lؘQ+.dߵ=禽?'78;~ϜyPw'־])mpf֪fu:5/V*5XYJacGc5fW`;6SOF4QRv{Ү~`}Xmf50XveVK^1Q\Tm֯ǞfSߊCOőj-X#Zzr.o{|3{jnY쓇Sl}Q/~*8>w"r'<%u־]i2@X7@kU:"7P`}(}G4;vԑ/n7{8䅇X{x&fq-cqsgcK=qO]w_}slӍh.8ͥwx^|׍׌L=] {M/}Y1';뿏В;"?2@X3@kUs+2llܝ8@ؘʸ r/^yF##3CC=|~I=""{j.V5XHCqu<.[vGD,ip?f#G'iUc#w2p 63g%ժN'fJMVdb|Xt3l~<ܒx ?;Gk:g∈ʯ+ #Kzۍ>>y_gCF]{\.zeuU}t(ѡ붫bWEr7+g3=>%֏])?<@Ѯc+2jdFZM̋mXyFFќ>2l>F6bߵsbr; {l=Z#v )⦽s|J{G?NxnƆkƫ? ` 2@@_`Y|r':tݨEVK~0[߸q|꒟y/KvĽ}BXcKc{8ܒ9xx6571gx+_ʨ /r\䇇v!5Ĭ0Q܀AJRNEͰ \l߳©ȇR)]yU˲we==KSsKgj߈x깙~ 3s7/ăız |o)=3,,$=eotcY=ܳb+h=a3`Z7^EȗS`i * \i禎Da|<{j.V5Q#Zzr.UkU`\#KnItZ+.ڕ:@Z-C2jS:*2lsW{[ 5,zpÞaXǡó1_kDՎb!#Rܴw3l\n zh%e+ 3?.󣚭a~`Eܝ3r1P( r+cwe8~:}t(^u}%n3֗.*q).>G_WxgM7C{"fdoڔkԚvaax!!+=f,KX_Z1ov:Nwgxb {=cql|V;|KqT׌L=^}@q_v,?n7ذ1rea Ц,9oޮ YʼnJFMV,[Μۗ|Ht x': ֑f(bLs-RFs@~hSl{{?ϴfcm|wer`m˗ˉYfւfu:1FZ%;"?rfnY޴w|xs>/|g&~"qcFV\.zeuU}t(ѡ붫bW@.Qe/wޞz׾Ge/wֶ|Q"6fYqacP|yF_.UӉuDxfO?uSV {l=Z#v )⦽~ {vƩGǞKQ#Įw-gfF!}5Xޜ i4b|b^l˰ Rڕ<@vbSuϾq-{U `];qѮNGFcgȰo3i42jd!}.'fi_ ԼPd`)M&FD\/Gۮ^\/K矋S_|b J5708?`Ź=ra-&@?4өyq!@Ц(Gkn-_|V;|Kq9>qc&N<'~yv;.3(╱q56`ʗ1ft(2lRk6d`a# IDAT)MN&6ĮaCF,||LF.ˮ;7!ǎ@տwBXÛSvQӉYRɰ Tܝ5NEͰ i4bJ=uoҎd.,78Q>a+ |kМ&fT5uΞŹ OX?7ooɰ\ W^[3d4ͨ3jC:+2l:&w禎d`}ZO}2oPF`iF0QI>n_Yt;J}e(#W*%Z-6@/ΤoyDD-퍣is\b=WgfTʯ+Lg>M! w!^j55/Ll˨ 4;1oLMe`}iVq_L=3K6ymXNȦW-˗7'fz=&@/5gRs(LgW`+LG h+_:F|cw=nM̻f|o2lB.?\Nk3lRsz:1B16Wi2ypI?7<o}|>;BpCC1+z̡Qьb,'k Ԛ&fʶ n&w捩n7#g QϨOWޜzfF#F@? `MjV JMVD%rRbޘ:acN6n;gzg7&ss1߁ f'fBXnӉy-6[n` J;w%捣SXۺv3#O\V+]wxf˗ˉYVϰ +Z-:g&慉JmVdbvnHMֶZ4' Ė׿!F[=5WDv;Gͮ3|ysbY8 КŊBذkwb֪Vs\m֦N'>3c?w 5tqg{^i!U!}/SzFM^i$Qˮ PL n7ǎfW`:OGҮ]Q~-6޹gsxrRO_~Wz!}N۵ZFM^iL'fѱ f`+dG@Gu#X{kc{;"k2X{jݯmh}?,rM._.z=&@ffBaaPSX{N6%C׿46e6yy`)]g惏'軁C|bޮ2lBz/(NLd`0;1kL XSԗ| =BCfq|~!1?27xIb+~&VO\.rbޮR[XMnf 2lv&'sǢacj%_: +'gr=v.0Z|=nB2/'.j6.U^ٳyab"6kG)e3l6ㅯ|)@>co A͟n\Lmhk|kMgFh/,GD~x81k jҜJ%&kKi׮Լqt*&k}_ۢ0>a#b=Q/mN̋?^!,vaR0:]5$?) c7禎d`k |51 3lW_Թv~0^3'h>2@@&iZMKլ&FG#78a495 ,ɉOhͷ& 6\ύ\ǷJ>ṅ?n~1@@&͉YfVLbVd`)MNG \fz 1 wfcߵ8WD.?qz d"?\Nڵ6.Us 4]YX˰ 5ODt:}]nͰΉ>:tO7j]j5O |y81kM"h`vJ ,V \ԑ^Jsb6}Osykv&N}Z d"?\N;=]`:gDga!1/LLd`ܲ%䟟G \ȉv-1x6طwϷKSϜX'>qoDo}.y5F\ʸ붫bPCrO\L=w٧eM/wևM\?3n,GsF"7LKUL l:O6XǎFl[ An37c8tx6khQ,c\Xo~Uy8 v\.?,C\>CC9sbq#`9ZiJM֮ Sѩ|2jz|3V~`Cl7?D>ʯeX?rb֩3l,Wkf&1+LLd`*LT"W*%捩 j5jHb>0)̛2lCad$Fn3}4:3@@f˛Z-&r5Ĭ8Qɰ ڕ]y /M̷Mʰ[('ssq ϰ!vtMhY8 Lir21kLMe`k͟=l[^ 2P(oK=3߁hj5  3r/9SM͋Rڕ<@جNGȰ v򳟎h-ڔa#X}_pļsl8 I pb֮3l,G+m0xveָ n7ǎfW`[<}:^W\[ bzWI |k њI c̰ V񒈁G2lrgl&Wܼ9Fzm~+1ήp^La!tj51+LLd`(m{bޘ2@^8|l}ӛ+ko`b~Cΰ |ysbm6hdXla%&Cir21k5@pK_ٳ[^ W-{Cُ|(NFe䇇SvtFMhVz+eXY:Fgr1gW֐;MyȳQ{ /fԼ]gXv3gDmև ntmVhkybe[`oߒzfcf3F  3 %Y4py5gfRbQk25oʨ igO=3rik_{?a#{ B1r BXZ0ISb|O2lkOnp03z=F  SrbX;a`)Z33h  X?J3ztbRό~gFm`mi_lvob9{6? pb֮2l,E:+6X_J&ѩv1Z3uƫd֮\.o3h8Q# !,va`)Z3 /iz-O̰ vgO=3r᪫W&8{3l  SZMhTbaedaDDTFM.sO=|{bOd`}#CD2lB2:@X3@+Q̙ya!@ PbnXON~ө7lrQT^|ۍ :gL7'f`ejΤ'Eidbޘ:a˧YD ߴ/FG٧̰_T/}2.Wd*ˆN pZsG26؟FKDžg^̧^k`š}͹ߢfjBLLz=SZY!( %b6yuۭϓyih(~F{143[+/glBLL杆B4gYun.c44ß)7OelW嗣SHCfl[: oGdUTynI#_d\Z ^|f‘X錍`1@@VR)*|`ȯRW ȡ6_0@ u><;T. o{4b ;u0HZR)c# aW ?}0~ށ#F>dީoądWLf`L_G b&yln\>V2-/}9c#/JKGL`0@@vd֩odl\MdV`Fu6ns!|Q*2d __5w/]?{2c# ]e|"uMi-B2k. Z_-ķ6c# O4:z6?  D/^^Nf99{^6;g٧#d>442r/nnO>dgvѩo$ṹm-Kf˗}n%cmbyyt4&/c#j( 26!^lDHi,չ#1P7359_F=O}oTFG36vM1ۓyՊ9c# ]e<=@nt/]WHj/_el&DDTN7Nelz^\SR) _!1?Ռ`2@@vEF=SH{l2LMEVRcɼ |h.G|VG]w'VO26!]unAke9 |B2k.-el|35>CQJ/g36!UGJ|! rz:w$c>V[Hn_3Zhx=pc7TС8xwn>l}`2@@vr0+aDb&k٧#d>T*el|3?( 'D36!}Q/ n['# <]n%hLqWF5tp*A$_!؃ di426r.KչmX\d\< 4|{<2p=f~foOgl{Bpi ra^5@/d\ZzRL@Buԃ^>+{Bpa|6ʁj ֙3m63>Sn#G26ǣ<6oOgl{Bpn̆ ?SmX:\?Ya>۩26OzO=!}Q/ l ~k-sn)TJͥŌm>Z4Ṅo1n4QK_36!}Q( ףel{핂9TbysT6sDt|{D`xl~x#?Ee<=@klfl^syu!@Ysi1cOn&hL IDATyWBz(%ERF /*ˆN mEt:|xH6\Iu|r7(x{<2*cc1›G~ xB¼:7 )cɬjE|r>SO@&Nz(H7^饌`w3@@_T&; /dVբ2y0c6?_o.-fj5?x?.|+}k1|HFNC oVI6 /jGFyn ET+d\4@ g)̧?S }?cyף^D24 @VssP6̚Km6cL9Tb7Lm`w3@@ @YP0@x*z^6צګѽ|9(U*L=`$Gsi)c#؝ 7q0hz^W dl@d֩ף~!cksga7_ hL?tfG2!}S8@0@YV+el@Ba |hn2m145Fy@2oZ4? c#} 7ºBra^5@00Gyt47Oelpu}0?ejKet4:>bG]!}cOhR|e(T*6̛K\_̫sGb_藩X2JΞvMa!C{%=@XRHAR<@ @_^]9azX{|`-Me<=@ؽ|9v6@DD{l2kQ[HFms$P~~($_까`0@@T&݋LMVVYuH&\|z0"uz)So$~^ -8xN',_!E 7WBN dVkQMJ2\Z֟}025Cǣ4<7^x>.vU @VF#.%BASh0x*c7^K7}>c#`P ML}'V=xF; oJ( %BȪ\ Hd\\mN'O}{Q*26 W >MTD24 @N핂R)WkV[H>x?z[[^?KZL~GF{KV+?DF0 WºBȩ|6 DZkU[8z[[:s&cu[c߹#*hFT*3OGB2aE(Duv.c>¼ ߽?S`UgNfɌ`   J2; Del,|sT64xd^1p,c#`M?r/=\ .Ue`nrj-MfٹMj ɬ G6^}9z[[=fl ?Û>ؼ?ksJ2@@_U&  nd^3@0Ȯ6@2^?K136U׋G^|7~»ףgy=yd<{}!}U8@X7@ ap,u/]s]wd>ߎhFz_M/$oGڗ:8qr%}T0p WF#zn6VYm~0o.-fj{dzܿ~w*|W3N8u'Ue"=@^t/^WdVH6|RCӅAh6Ɉx !}U4Ugj[{e9UdlQ*6̛K<2=ETDqfVko9}"JnYW;QBjhx0@9ósiYs!/OJLqW2:J2[!)O^GWoL?*UR)w Jz:g`7-Kf[ki42;o'oCjmY [ވb^؋ Wr9*ɼ0@;[!P_(̛K 7S`е:SGcu#7DZM曋2Vlb2[0Ȇ*R OnYz#WyAʸB|xn.SGRd\ZozɻRٯ&L֮zsgc}t:m.{o* +ɬ<62zYsi)c`YTwߓ 0٫J,=@n>x?_`x^2?xϽwr7r%p›ݮJ+  `&Bݤvt>TJŌmK/$Dzx?}_ݻs_h2oZ>p;@3@@U zvcܹd^3@k>rC2\: \>V;rcW!T7}#}'f0 waok+z͌m`ٺp>z[[ɼ:k`-,$b&~ fjFR)Λlxh,y _Z2xغpa'*w* تcxd4S_˅Bݧ6,|YR(˙[{Qws3ok76j7ݔ[qFyr%h;1\D-nlz(}䊯mm_27 w vG#VYih(2`;Yٌr pCF^U5]wglGgO>vs>*cc;YP>p/X^r2RFݦ6 h.-fju/JCC1w&'c{yX?,Iwj5ʣɼ0@;R0@87 ehr2*SS|sT6^^Yo1mQAD?dtۭ ? D2 Ni$Bjd~!536_,';S`?v2אָ/el ` T& @?WYuv6cShpT&^vca87_OO^ g`\݋yuv.cSmX2llm'}n%O~G*ddXϞƉ726 0* 鱈Ym~0o.-fjE/P_735T=XzLm / ʄBȭ}n0> ۭ:;呑d\4@|:X|u|.<;26/}9j767{7.@>u¡(glv*Q_HO/'n%oޝrDDY?~xR)faejy ` T @nauv.cvBm~>5 ܣ/_;r¿ى+˧v!_oc{ @&aws3v6?WVYl&±d^>͌mW~8~F:W??qr%~s;Y؇Jrej ` Tz=S?+ɬ:7 ;6{h^W^ٸy›w|9] &3*yעUFCW l ڊd^5@ 榈J%7Oelfqfb|vɛ835Ϭ^+cj5P׋ ?{2_!!rºBNs^2flN(W1|gb6nv하8ɛrz8"͓+] (&N*{BBVR; +˅\&중d\Z6?޼;?ܮJ26?Z2@@(JQH杺BNayt4Sj mmelV[͘?d:>]9vg;k?Q$ O?v+c#9†BN핕dV=<R)cvJm>=@ڊ3v#+p'=ܗ98ܑ;Q匍` ``T& `;VYun.cvRaDDsT&n~̺r,~᚞33QۮJe ?}"zn6s 00* J2 +*ɼ uxd~fj!kzm_ݮZvגy3q7vBFe!̫!B2\2@ˈn7;kzgpE3.LM` ``T_>mOgc=zV2elNY8̚2v_JfJ5>5=;ٮJI_J ]>Vl猍` ``T&&YJa>l`O/$ Ξ?)/\t*CW}78~vVRPx'2qoL*ɬsn7Je۹p+R|eqEͥŨ: _}0wKW}78?vUo][l^1oZWV:;SN/7+ьV'*13Yo~a6΍l; sC * R ;mhf&D+S12]׋_J#|d~㡱#qD=PLXz8ԓ1yWWp6ά^zX|+G{BFh0":B¹MȡT*Em~!.W737v?߽'n7OZv'm_[7Vqt/_b?cTYQ׋G_z/N}gV/ŏ_x7;ETvBFek$Bh`x*s`7x|;6;nhH8xqǮZ3qGu|/8ΛwUwXyl,_SaC{e9Ugg36 ±d3]ok+꯽kQ;:'7J2Ez'؉+?} 1@(Q90;upHٹmȥP735vMtd>q|:ɼW^u|=  D23@ׯR ؓo1JCCɼhWKvz `L??^ pN/7ڥzƙKqzM? 0P j[.̫35 RGys!ΥKqHc_JT36jGc_K3:c#zx{e[yP _'N!\rz21ьmȩ6?޽bx*s`P5Nd>q\ǥ&n^ʹhFшFt\lDŏ~vR݈n7zNDEiGj#QE6呑:x0chz:bhz:6<!21:F&ޝ}uό%|Ivĉs8s8I6mچPJ`Y~²,–---r~ؖ.Ph-!Iısiɖɲe?ZB˶H~>/瞗r28uɘܽ;J{?\,' ˖GUѴzM׬ki67LN朩9ԕ\8+%Ue55t qٕbҖK198kץ?S#3O'ۮ2rKe ͒ IDAT2Xַž?DCw|)V۟L1^eb"J1Rv튉]19'R9^V}{cr8WVtDӚ5Ѽn]4>gjijEhjsLQ@@]pb1$8M %SL@=LD:ԿK![&~Q̝U|&GFf=ήǎűı񧟊?׻鑃1=r0k]|h^>>}ɝpE{>v?ϐ(O 8PWڒ bTd2)&ţZ.ySB,+cj祁}\_bih7%3/VНwD` "*qg]8dLRDrcͫ15t [^Dvɒhxq\ri^|i4v\l=ٺrq6ԕ\!0;"2=2Q.'ΓQ@8r~p͵ipkXuq/DرGFtȵ Qy|<>hmG,ҙtFU""{e\rI^|i\|Idn-)֚}? PWrm5 #<6NPycWwJI8Sbl3&ZD&M9P_s^r}JIGyI,}З8Z*őo|-VRNbzt4c۷SODg:Rݚ:?GzWdˢꫣue/[.^9IP@@]NP@XVbY)rK3!ח8&bjh(Vz.mjnM7vvEySL0?q^}X"ؘr2Jc#|3=tDg,\k[d"o;L&8E%j& h(OECe*SX\y*:cŨNOgNN؎blCih΋//=7ꈩ:kشs@zPWrmm5b1$L %6vvE`&FDv) PkXvEqhr]{ό#Qz_,}kSNbRVcboFQ9vl^,h?M]]uW4vvEvA߭ Gsr/x]1}PL9#b{м-V裏x$ޕƞ}ʍ1|mTڙ7tׯLp&( d"dI⃷c T, N1 gJe[4G8/GkSNil9/l.$o[ޚX@q/E L6b*1wp_ 5[kF~F6?yn8?;|J{?;kWb7LN}Q=cc21qJ7L=#/EK1Ѹ$^/GZ;^nuGK\wشs^4ԝ\=8rX<g] ޾?،@i3ZD¦sz"NOW7Z.4Ɵx|8Ѷ銔Pvġ;:==gfb墍‹"Ʀ9;OkOO,65E/}/_V*1woL\{xᅘQVcq#q#Q\27FOWiF!u'W(ԁpUM9 ,j5$RLܷ6p_!m|&ʇ'۷\bt,mnBjV*qGwSO͡\,Y~l8\xQ,9o}dcޑ:c=46낿L6sΉ9׼.""cc1ԓqcczd2EDwFoDWE 7F~͚>BNPHQ9z4*ǎ%ΛSLK#GagRk lI) @zZ6^޾( \~왧cW4M9R*}ġ/SyhlSm*Z/НSP*R]qK_X=->pT&&N̩1鿈XcBaS@@ݩPXI`U@ذ|EdSLf#?74Пr"L)>p05פ eXc=r9w PvmQ=#-R1ׯl&3WOJSCnnisN$G᪫pQcoOFT'}^e|rΘ6KH[w %w~-7;lE{>v?a(r)ڷ\[c޻c޻kORCwhիc[M]PPwrBZddRL ЁYcWWIͽkHljJ 6`{dGbVDDa˖XỾ;bʶش^tl+v<;|\kn舎[+n%=3|Q|rΩNOǑ-|~ N_ ; #"bdWtę'}}}j5Jb Y!6_b3gٍo_JD:Нw׾>2lHKi` /c'OiHkW<}1y~T{HaOw[hO5-6N]& / /|_#'wP[e{ChXt~BiP@@ɵ~0UXF0k>t(q՝bA9DdʌRBXGM\zYZZSLp4uwGWgOG46]r21ѻXH]KK⎦ 1Ծ:"9 {3Re/WaӰtYtX񶛣8/ċ/!rqo6PWPwrXJI`q>x3RL@=66E51g@ʉ4mQ.'ۯ.4g7ߔX@q,"8tǗmIolSt|zv9tF /?3C'wشsR͝LCC_wC_wC{8tǗbm'U6Y/}]oeozsdyL #hʉ`a>Ps՝RI7pWi4ؚ8z- _.J^q~駢4޾t0=3?reK[#O?='F99T|ڈ*!ܼ} Œǒܷ7Fx[n/\x gWw; ^lv@m~@]ʵgb1$M %?,2dId[[SL@hK~8Q= 1S+6G6O1db[nНP>z4k']>rɥDs>ԙ{?>-7wXRsꎖxk΍[nXL&siX#&ߌeo|SdOjOo|$=<k8`&B!G8S@'VacgWd!8=޾Yuj*&sRLj5q^ز%4p51ٿCfn?:=ѰlY8j5C(GOjo7ѲM Mssش36CccPK19U\( ]vcΙƎ?+n8tq_j4/(l.:h\1ix KVɨY@ՕbIˆR.ܚ8˶Fŗ~dbo 8D+`Y?I-]|WdVcӊBϘ+=]mhD.w7Ǒ5ӳ_zoz[x#K'+gXI`>8S@pʵEÊyi?4@F% W]ԗ}}d燿ըLNSQT]_]Ifcw~7b_[|0"bss ٞi KF`oD^zour2FnB_wcJ' KB{t(4|^H]uMdn4\_8ܷ7Ɵx-m<׾ؼnwk3ם={]U''co>><&lpL Ye|H*.?oVz_ȋퟎ珛=8ŸޗRe\On 퉳X1$Pv#㯸R*F6D8 CJ~6MWG!q^mԜ6$ O-گU\,FRLp)>-v}1gםk?X#YղX+xgD.7=x_-;ӥwm,p@ rq4$P?;tܵ_/ǚwm`q44DӚs~L. lsK1 ³o9?ۣZQTbs{Q-Mj5/EӪUewȯ;w;tmǧȞfwţeL8ԭ\[bIVp*)_om4+$\* ݉F&I1Ӵjun98Ok~gyN?'|EqV{ZKc>WxOd+ӯ +%Φ+LNVp,1CgNO'5큚5צ`a[jy{JIcj 6_hyN'/Ŋ~h_==qqßeG_x,4 [B9ǟܛ\@r3]]_.0 ؒ6FSOo|ǣMklC_O87=? ѸbL|wƃ_SنYY6i IDAT>oyqcjDD941X@P 5 @[ў?ZđG W?/ߛ\QO) 0WJ{v`k#ɤ`d27\s˷`V1ATK'\+✟|"˥Nh)"V__cjVrr\7OC4MMH4IX(Pr""cŨV)W^Vmb4qX_k,n%ؽ2q^P@ Mj \Ršpȵ'߻*woL&*SO>#~aVkE/(Z/tܙ.%K+.*}wx뎿%vWDԭ\<,rl<0PzbuG˿o.ESy2qX_gtDOWۼ{{g~Z, l\2})&X7&Ϋqkw`.(>pַ__ VSCfdUq#-guFX\~}>|`Q@@ʵ.@*)%aƕ/4ZsX_r-}kg~dr@Lߗ8/\}md2,^46&LM`ܷ7~WcgO8X#&MM؊uŗxo`}򉹊kS@' ]5 ǛZmx7tׯM#uׂRZ, lZ:I1ƷԜQVSJߎ_P\1d0zbuGK5?\{źYY.c~;V*sD!u+E5qdť˒r>;Z❯97na]d3"PrKFО8/ 8U{bjyDπ,fM\zY40Ǟy:D CZ/|.OE'\v_~);:RHoƕ'\3vkG8Ş]8(  tq4$.yc-Ħ)e2%K `!(n{p5)%XGb>0R"VT Z'"4 =7txa&Ϯw\]lVg?xı;͔, k Y\g.9/zRLBQ@8%WszRL8\ri4^8_x|O# T&caO8?#L[Y|n~J#p[g| C#1~=y{TӉ  -Ե\8S@31u`⼱sv4K. >t()NVip0&$ W]L&DS5"ݜ8DĆ""b/ƭ% Y<>{>;1.Y=?sկM!L&na]5ꎖkWww~C\ ?~'"o> 1W{?Q)(5LZj*LDT*.̬omy?.44۾kRJvKHuM!K=;T<ژjGDCq Ҋ pFM9{~w4µ]?ȯYgbdhjŊ|l+z+:̻M;c=4;vHShjŊB>=]xy?;k~?DSw|Q8CPjSL P͹B4vLSST''gU!ԭL5vuE/4# g#rܪK͸<*9犗m9kWŦi8#&=}2"L4|cSc_ꎖزqe\~Xz^Q4XKӪKgQܳ;?X?n:ͤԛ 퉳BЁY)rKSLBf#ӛ8/ 8Ei` qv5dRL8m}r˿~vQ$b࣑V^qMȬ[/"z~^.Tq=/~qx|{_J:'١eX#?G+?o5?XTPrBlztԃ`٥p}kg~PVs^ꔒ,^ޑ*9oJ8xgұ矋ݿQ=^k~#})?;|R}Pv߮ U&e}}/GcW׉7Tqs{Q9vl ԵB{\P$A<Z/q6ooTJU`⬡#M1 w{f5\8qvL~`Lߞgy_hdr=I;Om/,T}k?W\9c]8688HBZP9/)%cjX!VaTQڳ;0LRyk"ɤ`q=}Ctc^X\>D߉jik;}s'VOs?,DX?xgrp0ÏؒegźĽGb}i<%_ƾO} +VD5}+OYӪ/9j5şǁQT?L!ufhIM ( Mf/ۛ8+J1 d\K6\bׯM,!|f5 `^z0~',l?V:{buGD5-vZgbkms~?2rg ~2qlpPrYyboJ%'ΛSLB[8+ GqٝWE&V |f2q ❯9KωC-{>hLsflQ\׸re9sQlٸ]&XS.2 '\Gb7-FFRHQ^PHEmӇF"ycש=S/qV.|pinlޝGug.V(.HQ\$Q,ɖ,qNYIL4m'9󞦍8,I4I7u4IU$ vV,9"~?>6hhǛLֶ>{>p&ulhMCXJlqcOYykz%Qs\nŖſTiʼnqy%v9 3E!`söb"P|lrfxx-f.q0IuߐY(y"5}sy];轷ժ{Z޹F.ØIEC~/ܴy]3_SO: 3G ޴ɄT_P|,f9F2\tf0 [>Qv2!X:nr0 귽]ceiOλ) N9 |~힧V͟uuak[D["%h"\(׭ڰ_ۣjfr U> ?[My~45(?W}W,xJ =IԴ |ܦ0u0 `P@|Itd9sUT(iixVT+|-ܷ06dodֲUdl<55jsME3<РOq{DQzHr\̊弿gX{䘾:w0 ]g/ X-|lXt4@JRG[+n@Tv>/J0|`" UsWUWP2N I~Niw'P~u ;;Γqe+ϟh,(lVĤp|%rN!Kv*`tjve;QWRW|lvT'aĊό[ /  ֡SquK6+ .|:>gXhq{Hġ$xo\m9炲8k僖7UQ`"t*(otyQ5V`Cʼn J8@2BѡKJ<.νCIKr'U)p0 `& KzTSO8f̙Ӷ{ǣƏ~\%C!iwKHB7hY bYBYr,%Yn_f`E].\x[ X?5*e&˝Y*ԑ.>UnL0 Ӫ7[&}Ɓd uD)뭰[JRWuKI>n58[ BA|ŢvY΃7 90SV孖R*}{L?J{ @" r)V%ð_6M ٿ4Mg,SegA]>-Y\h4-{?Kma4`RojgzzJ>٭b2a9Q Z;vٖu=RD`{/݋YU;@"A;vD;COq)W(^y[a6K叓/&[μ W ږf{)  CPh{saW:?tAS`"M_ɴ{u~jy,&jg  ]Tʗ] q_yݯȧ.{?Ki*cs j9w0 ˓iJvuZ+ֵS]`"ըnc:,GcGu{ߝvT;@"Q`ϔME |*e$X^( ,HU>LS@g'/{?KIijJLFLX-loLt0 OYF-硎\o]c9O;ls9.{ޤz p(Щ}sBl~&>&uzt I, >z*l+oo$ν]a)Va$`D98]w=Cir6^|P\4޳zhY $I*=}VN칳 9XV.xsV) l )|,$]SWPRoigzzJc/Yљ` IDAT-E;`afWT[S!1`"UnRG?.P2Syzwkec䇆lo\DXv( ,Xl7`[Qe6Xz7^Uj. fjCayBJn`@!ycixTwXBA<\ dl2*MJ`19t*^|UOnmnXʼnqyOfDeBm}DKgl + hjk[dβ1˙70Sab9w0 K%y&fˊ;&뵜?BD`)JzKߔ^CFs*C ,/vr;uq]`٢s[¬Mak ;ڣ9Ba_@H!/`vm {{L<`97`l6yqbB_`"ZqNֳdVϟ?D<DpR-־Y[ov575kL|Q@X\;轷;ZU{o_;e`~FW:[  *&`ybE2](,J5=Lt( XJˣv=eS}Rjewv\t~-BYc;i~gU*񧞸Eak[D_o]v4[-`*Y*02b9FV۹]9:ɮNycCIͿzXγ=>`"Ovs[_lѡَ`^4l֞M=~G~vUX( ,P,=`BrDLX| 2|>y缃iX =5Y`lu|GJ}kr{dVHJ}l ?_VOo]iwS2MP@X4嬘Hp"lfrɿrq0 K_!1nyhv`"ly,ɃņLb"/S{}kupim$q_c+*Wvwi'Ts, OU,TdLPǭn<55΅,yYBfԡiZCw80 K5z45Y(ho*?/RG!`p¶CIX8[['ŏ}o. ]T)s0 K[r V*4Rr+-=b:`"@{S|nS@ܰc?ɷijR>?+p '{o~q\y{Ϫo89`Qwf0J`[@*KTROk9%(ur6k[ؿ=Rv^d49d`11M>N3Guk` hZҪGRW}ƺ!IRSW칳Iv7?>7T 'y2ɬr|jU|{CzPٟ-%u2UuAݲ^G!!,NLCo.&%`3opfudLoiXY(XC7v80תNe'Y*9,S_V~bSݭʺ+:vm/ݲ7wBjgh8RG_V1uRsT\y1uPB#K%~{䘾:eb'LS=|p$Ωd%3)+XRj89p"_>y#Q/_}rγ0+;-g׫op0 `ykqz<iAS@ . hJ\wBM{;3hz~mE.h~R>m>٭/}A?)yUW|~`!2 S)2Jٜ\V\Nf>'3/4KR$󕏣g%BRi2R0dyVaģN(&7w䪨" r}y_c5s ;. L&N**Y) Kua\+PԡC-r&8λ- %i_R@$IdRJm:ݰ劏XTS4t,A*IWTBsZk~[ɗ~e{lyWZ{孩yh`<dVBQ>[U~uli?SI''UTarBʼnWn''TL&/ R*TJdrWu+3vD3 AA*+W]U%+TUUY)08S+.|UgwL7.G!`QTU)?t쬐FG$Ӵ{( s//;,d\`HV)5e9| 6UkSt ezΫ|XBAz@ذ@MyUesU=2 ޹F!?:n\?mQhՇ>!&>gP>՟WUe: o{~(q 6U*(?:*06*PU2M^)Dij㑧VZyjj䭭77^8:tMQA!`Qq–䤃Ixvr XlV!V9%i=4 Unѹ0GKyr>jt0XhJ?f[ըw_=XֶbIuu4*/u6pT(WEƟxv7 W'qULLS?wN'_[Te6ԘB rɜ Y((V>6l^BJ+FW){>%OF;8R_,yE?crC!`Qqm W~4|**i^UL&/铟VŚ%bc)s=5uSjpL07`nJ ݟ4^Y`+#DF%o:: ;Te9q˙7BsUSٞ`H<`;U#?$3+;xI=ry}'%?:E۽}t}["Q_,FYEnՆUS44`jrHiWJMi~RL09SJ>̹ʞ?R:}َ1OP^E>>5]$CZ hV)N.ω@9w8l9+&&e |6<߱fdx<˥lQdL%E,ɐ)Ya2̋vEd]轐d:5Ԉ HDժXFk)n|Wp;.; rf *e2r&`q˙7q0 `7XΊ*S]`"Аr}Sͮ;- %iGUeLVsgm(P*{U*߯o= P3Y+jUzIflyNW'UJ;VY#wUU+䩪J@@.Oʭ z:?KyyKtzR)SSܦ.N%UL$.krRĤmJ%z$I U]umhkS`m<Q@XT!7 B)V1i}Qo$`roig{{(  %v) װJ7lԯ_.;)}6; 8i5o7R>nC7&33s9 |ZSh&\1K%e{{:vT/f2KyGP"-䭭N:yjk婮J@Pa9kW^@h*n\n_yhEsDkgpTR)RarBʼn GGU{c磣*o3{JRǎ*u|Ю@{孭 'y2ɬr|jUЬEpRZҡ4̟|,n;F) o$"WEŽ=aéXܦvY#:zXGTL\a,ɹ}JV(YB@&+T1RX QuLlok KPW[icï|.r;Ls_rxiI'Q`C6*p&y+.ou. Y((}[S_9z۫`k>4UlyV.Xojڿj[ZUY)N襽USUzqyJYRN!W"W}$O]mVp&,OucK;?ОNZy}\JRSeĤÉvPX #loiXܦuIi9mp0 `!0 C5o3cZ`*0JɄ^=&R Wӧ>T JJ%]e Z83RL&/:ÿ/E*MT뜱BcQ"mԆs-ОWff\^vΛauvΦPfRS#N/N(LM}N{$V5*p&Un^뮓"piw僯s5C!`q6``ǭ/lDLX--Jef֒]3WR ,[oSjY@4w>'s4M o9oWyVսb5s}4M },T}rJvԡJ:)Jsz>W um?֬UEk<5NG]Gg!ٶ>E~3ֶ{M=JM9 C %+ԧu<ŜVL6riYrx Vm*A}U]w: (q%%loϜ˷Qmm [ZUg榕! jp$uj )W E~32 ޹F!?:dϿɷv|ۥR6l_2^8sZ Z,*}B^o@]=j! 8B WYΊI0[(RXv=O!30uBr^4m/kU.jXgNk'?1jV: c?9}>FGmwc#jXLTR`nonV`u nN ͼMzh٫>-jE~.[7/mmm/TWwLr|^j~mo+ж^+&J=ʜ932Y[Okm[kc*. W5nkjL48R_,9oe ; 0?LT>{Q++ߪFz3=='`q:e93^Un4S]nVbe%(|'Xj}PWߠtk'$)ϫnJ|䆆xq/÷IMܸ _oz(")|pX7*FIY*)׫'T$YRD&k_{p$\/[, ]1Zν Ψhi, R@tbQ_7ow0`!ue$=(,Rf c{ mP*X>9}>准lwG_,!|ᅮQ3WWxa2gᰂ(zUn"OuͬZ%J;ڣP\kRa\hiUEKjyLT~hHJV1FrSNjK_2ހjZWVUt{%I xlU3 A>{#;seg> J)Iy,Tk(]eӧ>sFuNCԱ#;oPǡDxxk|+)70GH IDAT`;d 2%;hJ?&0XA7lUp7pf2 ޹F!?:n\m&w0 Cw\,$VQS1|Z놏kq .T7v [f`w8l9+&&e& ,ixzhq3-3PP M&`q*\V.BIQ>LUSvSWU9-_S]{?|Q^ݱ_BdF7ܮDNBQ>[U~ulieȡ Y*)}[{(qW2Y;BoPhۍa!ֶbIuu4*/u6|!_}|2K%:vTSG(}\f.7+rE;ce~VRxǛ!ϊrx= ]Y(8gm =552<pپ\0C!LT`b]"\!O$B<^v8+EF'W#74 뻶;ǣƏɼ1b WSU_R9ާ:uK!I:?PɸVu˦zm[q 5”4^MkZ+婭UU޸] uMEˈr,sj[|Nnԑyd*}Ǐi?T}7ZFX=+B" W΋%.H$-7; !78|lrvi rw*?)P,j'y9 \R6J_Tf3`sBjg(sn2U.JHJ9U2PP`&yFcGfhT&U]+ﯰ>Un^[WTR>=OOqz-$J8efE΋IiJ||ɓ$y# U4*iQ@q8 .y%,Uo]2gR=N~\NoՊp(, `PMUOpTRI/nxLu٬;&Iڽs\EXrC4ܳĬ3Hu*ߠͷ\h{?eC[nҞr W99\O2 wD!B!`qBz; U>y#Q [Z:Pvi2 T,|Ite~y&ORM+ X>V~45T}NfjbsܷvߺF+%U?U]7|\Y ,[BZֶ\pY((y@>=K35 'A4^ CVu˦zm[UXnTU`MkZu`[U=sZ=zNuk?i*}BɏЮ[w*tMr+ T)5Uv^LL:瘥#s N7XJ)FG"bVab\g,[uԈNpQNeqƟx\2ͲՊr~`㪬TG>*P*Xz\~Vz^skcr߸ %Q@ؘ&yJ>䵿ߢL:+Vb`#)=zV,bzNx(PTGZޤ@vJcz')-#Uu·T [z4 ;) L8Ƥ*<.~pźP==:C-$v?sQNkVT5uodZ: pR7;o?),i;eܗI/TS44qiJ+y@*xǣm7xzMWjk){nkK_#-:ݰEb^ }VPMI9͔Y((y@r t·ަ 2\`Y(a.( ,axY 橩RO(T,lg'˖g=W5~. xUͮ, %iG) `1MSC?׹ndL%G{6|j=k2rZ%ˊtcEjrqx5kbVs{JWуSk}H["ל K.kZCIY\B7]["2 S_)ut3UJ4ӚxiyjTum z<:;4>,[e+;v M6'CCtn/r)P~===S-mi! 4 BIHBNlgɛ챤Y4fFd+ь%Ym9s|y,|>BdUlc%*@hWU$0^Ӫ'lO.qE,nO\k2^E9Tvr^ԅSR=YEb虧H>[T"X$)o3uhϛOhMsr=[OD7ݫmh<5̕}L_*QŞ[Z2vAubVmmu :V4R:aҎvrnWTYF|C^0l_QGZ*߮[T٫%V4R:n¥j'lX ,IW-J+lɰMR|r^S0P +^|`ŚEgQNaw}|B 0f>'WܵٲU!2쓧B FGi‰Imb{ְUeʷlF8ڡ|"qMw+u[gfU6ywsTDGNvETѮM ھBD!`ʄCu,Xk eK$dzY8 @(!OsE`QN$nޥR.=8`F[V6[ͦWT0Xvsז ݭaӺpg}+,`.&(5q |߬TnSm/د+|M3hgkXQ&Xax-K#'^ȉe++onKO8{GtY%tV cX$Wx5Y&).si-ڞ \M%ū͠tljQt^uU.l2 ռ}% HeY?+)PwiDUQSRwp}aӑd6Qtơx | VeCUqAazu.j*{MټEwwvO1R~f`)2F88!+5Q =s =s5xjt]&+\uߎαWX$+ؖdY (aEFEJ ,gc CV&3e{y9wVh5r$w+eeSIUssk'4|hmU{KT,;7ut ˰ƽ6m(:FCGMr>+lJ+t%ɞMkCjyBYL{nUջ4}ܬw8X ][|6S)%u( 9zܪv}^7{A[uazȲ mmu~_*s%X*L`g lwY0 ӔsURNW-o΍~{K\G7T8, ${E|v+sS%U]Xْ*oZV2lU+KKWM0҆;Zqgt񯿢ULOT~ii:zVg5(ӹtab)>$Xb ,I" ,;:*k\ѮvETѮM ھŊ+O]moF][ĜE9S0P|7"0JX+WntTu)W$84?0XvmjЁ: C/]au='y| ̑d\.U[ Vcb C7P]w5,gJU[VO#/(~2,ˌjct"ziܬڝ}zG'X* ,Ih{.KT _8BB[:j(^1: [>ͮ8xVC ݷU6B0+DWD`uE9\WM}sgN!%{xhΛTu׾Uow$ !]QS;ee,sQ[_~DMd`^.ԵdUnhӮyWsVIQv~^M[ݾFձVj ztB?$;nVwʽdRrTd::jmr0Klc%Ȅ /c:&n(SkԔ}#cKS,@t3L!`YK?Z-mh, (v;4OlϏjAUWXbG+vExnS="2l}hL(xaD*ۣH񗎨βfΰL&F%ʪ$I|.sZl{֪')ط֣[67̳:z)$g:]Sy2u7.ƶiTA9Wz]zݲ{#Kڑk9v +K)i7US΍~3ņBeUlg71*72pv,h,5oG\F"hƉf5k˲Y0]U{igOa'Tu^6[+`yJB cVV`=2BK~HXE|ח:d:Xm1Y]U;4*Զ}aVmmu :V4R:aҎvZRUSm fj ZO˴zŀ"?$PӯG5k#KV PotQ X":r2 Iu+Zvmj bA!`2}mL$\BBHgs,/>z82ea:7H'۷[E9Q['ΛxSgBA r\w2+U7^V CM/*JW`T'v@~h_Z>99KSXr񸢏=xPW zyRNڛk=:b߻hjV]t٩ C V+3u7`~1d@XI2uNIG2#SNunql3Kcm6oA, w`$ <3?:3EԼ\DCw0m ~[~@TElj.a`'xq <EB H.ټM۔v ۵Z mP::EezeXum{]kȖ}cP2+*TyN9jjg50fVnxXaGV~tr`2|rt,(D<,$ u^0.VV&##s|##L2^<਼KiCW ?ؿgw IDAT %Ulc,KUnp`F /a*dsJX cj*\N֩lԭ{ I.\kW1Ѣ}گS"|voSР.|j?kժV|&_<#?V.iӥ7VmWg6e׶ٖ `ow$]5Pd Wwިjxᐆ{VޞYw.S+#*~}<6˰f5+W~dD١!bC ^ގŔR.RɅ.yɲYY٬#c\^R2eJ9cvg;z,[tg@(ٜvS5.WKw`^@XL`*L,++aE̟LBd cFvM(Ssª;s^A\KX `%0 CwߣoC>OL*\"ǢT6W2R]T?엋#?"Ce#SSL҉mzyU1x+ahjmzG m GUwߣ. %^zQV6;",KǎjQ9TuUV^>|Y5,*W6Uvp!a>dɝ;3J |L:%vˬRەU2/=++e+/as^S991Pw0]5zkSo`..`y!dًJR.'ld,L# 0ZjL;-{ɓ;=3ZsG-~,OǏm@ީrSǏ_% B07h~{rTW*@TEi_t\" _Z>Y[ז@PReia/fVվ{T}{U mmuV@8aE)399j|.hOyϞa*خ};~==# *Un׭֏p|<@ف2F/oDJ%KQHI}3+ dF9kǶdײs*xαyOA!`p_H./Q%/˲E!`!mP$܄R!G.3ewx`!KU.hEGn,a5;lT}{es8J\KǕ rFu*V[^˒0JT%Լ;nRW~oeeGF/kէPemJX!/EW r|;*oC{USv0/a~oL|ᐩaӡ!Yn߳tJλF5U௾O}ZeKX!Kx\гOKEPJ?|Y}Tf*)?2'˳u6n*Y~#:p·owl1XQх< ȗ͚reܲ9]ݔae \dJaӲ@{t^ #NwWj]A%U G&.nc`a CUkWO5P$?گo[D]8^x=[Y6l,a5j%iɟbϻd0+osTD]I GT(~= 9PXʷޠU|bV*׾US\kd4'}Gʏx_{McY;X,R.6toG}cفD? WHNn.nNeM2C75i:\n.lnl.lnkN [K:sߩ3ݯM3ee2TZV:|s*|2Ȉr##ʏ*97:ri{D#Cqlg.-pDÑehSRg֪Ez9es8J^o PGWXXJlNN vn[zK % MF!`(@hrTהVmmu:gQN3<W@iY٬F^w48a2`~WɳeF^mԅ} 0‘AIRmxhߞ5zyۤ@+g!T~V*[JX! ˲xYR& ~#G6-g6%g6)W&)wfTLR̨\+/VnlMMŔ=rA#W455r7(gcu9 q튨֣]}=)`I3}oc% tf!4ninO]8"kemSтo,a5{ J?#ꍎșIΧdtƻ$cr [,`yߤO_+Yd~V:7B0s~|s:W_SeŎ&`\6UzghLfc9O-xRa_oЅ6m>$,yV6t0TR J_ (l4Х͘\ղO V^Q9)pVVc@(__6=kSʍk ΍~(k:5uM̲dg.&TCC *78TnhHV6;d.YK'OLn3M9r458hI|Fo,=z:"W?|(v9'[RXEsx +`d"'9%q5a(>G!`YK?V0Tu[h-7ԬtoϔǏ'?yYA+i7US΍~X:’e殟˓.ƽJ:=S @+Dv|3*?:Zɨo_5'~c +WPVgVuAvaCu׭W#:gV&XxoiqPTI( 0V@(ԬJ6(۳$B ]ZkjV9jjdsg=~GWxN\괭NpBG;ÊSJgrr:L\_{T)8%Ø-\ը۶jYƆRvpR8ᐲDF,?myvpW*ᮔN޾UZkje|2 c?뫋:m߻NݵA.^t E Ð7ݼ`?+ &FFʌFcsb1b|s!GCPs9"3?VkWڸR [u7Kl#\d#m:n,NւnWWp`{KX R-{Oپ>tRެ58xVC ݷUy\@ >γ3ѱu{IgrsY` p[_Rc0IR.;5}㝥+Ly^8VnppFv[5S|6ʷlUD5zYҚZZu5ݠ:[=tN]IRYjXFY:r֐*aJTemv%ܕ+(Uhe&mw%-\t>PGgxlYarQS2mO>Vv`P^:|Rb&c*OTZ撕(tknI4АFC5cϞʪ3/uDB,*K5 o)=sVKZ˜,˒%唉lw%si-ؖQ>**qXv/\݊>Ȕ\F'檩G/0*E#hˏȞ3Lmot:̹.״jg>W\HF.|N7RFORQ܌}}a8jjUfoP}/UjyYe_P(~޽WƦ9tS-̌.TM_QG!`qrY[Ѳ \PYۆ@  t`;s{^Qg6YmΰZV7eʇBE]GCW rj_*ϫ r Rء3׵v?*>cs84Uu"Wi<ǞMk'4lޢTm ׽Tm"xPjAEa$gcMMr55Ԥͫ=q^y5.u>igfڿgZ:|"}j=esÂCRUSgR9ӡF1O͔83IɗR}>VgJPHDTe%_rى2(&<>Ae0jRȌ| ;K+ڞ$e"o$XZZ$`n|7e'HhTW70VGMBMש)۽VUOp Xֆ_MO<^Vu50J '`qqZ՟.|K hY?Ty+f5ʏh_{u>!vḆoߥ-xRz_75k֪νvW{0β,eB!%ϞVe:^ׇ2Da$gjV,gSuST[4Hjk=j{۹ѯ Sigg[[)NhgXxJLNNK;%(fצ8x/I;w4(IuZs^2ҡХ2ENhAUju/%ӔAUrjEΖ6se-պ%ȉ5| yrQg?K/> \馛w^~.NckZڽ{߯իWk06 4I% VٜN9=]ï"YVvKX B ۢ z Q XT I0W}Fe:q4`X8sW+_Rv Ze)7eΒr%V4_<-$i8T}Ͻ\T)lo I;teXuj}UMf5v_ݼKU{ɽv\|23=եӧaf77zjֳwInP::3wgRY^.sz׭[\"t_أwb;K 'U.tO==J򥉗 [U., %t/`+u99j*\ڹѯ|Y\VdesrcrWl,YyKuy.5mv˲yI$K,l,W49ZaD0?2‹8j&`1r L' ?VVV%:Š5O}W5񠢾  / %˲?+74T}J9=s!,NΆF| |/R.JSS9To<ɸv{^k"g6abϭWUOIf,æu]'6u7̥gV6ء;׫};es8c%$*yk,pTR  f8AaZdM@\R N¹ow$|gb0^mب 'neJBcao '̏N{`J9NMgp:uηu+Zvmj swfYLFTRV:-+V>\NJ}Ϥ5M굋gesS IDAT2sYVVf.'3M<۬lV^ϫqCx`>9Hpm1?{>S2 vôKvsb{ٜncvzIJd5veǿ.'k}joYq9tE߯:b1*&䎎}cӷmY˺t10 v577( j``@fo}K@@_!vl#o@rX+`ne"'?%sY+z~ʶŀ +klla 4p$UcBI1=lj6/+NjyyLk\89ZG_ˢHRͩf, @qyңΩ+"[>i˅egg4Nuߔ{\˪Ա6ҺKN+ẏPvE \8xJ/H-6[5rҳQ6[pT0l{֪'T-{İjnye)H8_AѨ# ]Φ`BrҺIW"4`n]yjzGt3 "wCJ+LJ%O&xN~m[֥u1~+e\D.=Žwfd892N\n.\n.lm%%6[6KNgc<*[1ŌUNH:'> ]xqؽޫO~#o~G/DeY*++ӧ?i?~/W^az'?kJK\ s+)|n-2?yTwj]*~X򩔒ghS<}JduM~E~ xo]68 gK18 k[[)NhgXxJLNNK;j& 0 +dM.ê=Ǟ˚KV6ԅ J]+Wy[& M˽K>4`YX##z i]6-G6%g.%G6-g6u+i9r)K A4RIRs|0&B Mwl2e)t(cw)c:;Ǟ߲5aLؿg9 9kbooOWWW}sZj2 Cۿ[}󟿦l"|p)ݾ}|A}ձc$I>>믿:L_2!e"'9%s^-T`Tw7ec ,ݔ$YMo6oγO&Ky}'o]}}bf08AB<@R"MuQZI%lٲ?l6;~O6'*JI6w],ER/@ \`Ϫ7T5螙穧ߐ~i)V#Q ^vI_F9~ZZ c_}{mx߃n\^w4\D\el g> ٧mPDDf$ S[alӀ7^Z"COuoASFcC+?՜N!u*Reմl37p@ k[stS S8jx586_o{K ߏ+L8 ( Mq.]D2;ЈmCh^z !""{d'LavQaZT- ~ݎގoB8A8DV<+c8hREou"Os= UMP“.EJ89 AÐ`^s}-~C؉讲mڽZ7yׁa׾I @__l}/_ĉ'wLjFUW^$I$ X2|0KUU|+_s= - |_"͠,iƪxN؆kazQA:;vFck"""(3vC?""<- ~ O: hQr/ևQw':jiş"}۴HD0Ϡ#v]̿ WTU1~ &=D}MO= -UVҎdA\Fe#À]Y[p-lHX`&ڙCtM-Dxdo'^xF2Ka6ځhGr4ИEcb6Je-Bx#I hH/`̵|(H8n|\s;98BfN_񋥯5Op4[Cxdo'Gu"آ&+̆ b\ ,]OYFy,7+9q80;K;j =| ;!""""r=j^;˫m MDDDD@^u/A)/˹t:'Pu?Bh^q>pǺxMa n>tO>ZDDT&Bxslm,ldU!31nBxoLJ0H$16g{T0#*>"=2^A1è;k`WܳM~u8[;/"""ZUwƎ4R }lfuJ M9NEa*^t79 #\(rLUL 3.v<6X[ߺ M7ǠŽ7|Vh?;7bO1; U80Lh@&B ?WRy#ӊa!t}s"Z;!`'v0 5]cj/]P0|2C @QE=|^;!""{WJw1> odx ϯl4պږjDDDDDW_Ig}vU}>+T@h~mHn#ȑ#hiiYվL/:6m(DDe" ""~~u:}t²V9Q=~ר""z;"n nuKn?0~{-k[ZC̋ veFD_ݏGDDԆɿ#n=]1؅M(h#efg17B{pv>!ߧrjp>Zf[&z x=JI]ԥP[ZShГP|J-L й0Γc&g1^$UvwB`Nv섯|sj^"*B SBawFpz!\>6 cl -\?`""""zgoЉiBEo+|g%Q@ᆋ 0Z\[е[hGsAKڜoKDTtv: @jC1r$%ΎÐiY6ox ;z^wB/lTz^ݫu{-պKpDDDD]@N< I @OOVi8tN88uT5;w:?X}QyQ-,=o*V\14ǧ CԤ%E`׮\d ﵩ!RdImCg.85:qhK jS ӄ11TB7#}:L˵6C Vhek`hDDDS@a۰b1 `\Xp`._P6`hI 烤i|>,t!RTXKVa) ,˪;(%$;,CH2":@RHr:YH,AJ $W?輧k$!Pa{cm;z(L:/"""W &й01&d;`ZW7n`pn:;Wu_{m*{mh%v6Mqjp 1FƂSぁ5#*==- cc0F> BO92a,yZRUh[zs>g\"ahDDDށ B82s0`ÜsB材9X$A!ȁd~ȁ,>w׹|NȠ J>N7oT]g> f~xPn$> le=5 ! 'P{x|oa@d F:uzvZ|:[&u$ 1 U/nPa ),,T"J7h 2!Lrrp UIk>73,L6D&d Ln^d2N_vݒN˜Zaۂl[Pl pea;mR2 xh-שJ):r9 NTZp^ِ\0R(ZrM&x@a ,IqX' lwޙywg9 lk+>Bo@KNWyUm fyCDDw;wzʽk4::Zlճ䦻w><<\UhCӃ˗/CU73m(N&ְ"""*㟯m/֎tZzd =^㪈Oe -:!'0i^t͏ܶ)9QlyJ&p?ۨ-hkTC _)Nw 3hY 6N!1}$""=+_^~mzUujK$"":;B?,bǏaC,6K}ڵ@ CEDDD l."T{:Qȅ wS{mV@;!Um9hRJ(} ܗ[&l Qg9}l H5p G46[LuuCRKwш*SI$l$߻ׇ.\\nz#@FP5dT? COgT w5gT4tw7 ;I63Rjdr֑M>պVF!I%"~=>2Ў黭/X (Le4t v*n!Kg`Y:|V>Ӏ"5tϱmXXj K  % }eg~(RRcմF'@~#" /^, m3t^ulj&_3BhN6 |]0@vwQ-C@5J0YEpbQs /] %7ForBe&:y[V`0m d] /J)>hֽ{A^j {w{hzMRofff=ji*UUfQeכ4 j_[?C՞yM^""8~ur P1f% ? 0= 'հB""3߂9;[~IB!PvE`~o|vƯv?U2TȰs&""6!~ xJj~? X~-QUW~?x zW1{]-77v{Nf6\>Me6""" q&/"yR/NQC`.Cp>v섬ivڔ(5:y'"OdvyHn,FДqi4'f7ӛR0<* -`¾BihdhDDT7Ёkal[qBFA=@Нq$.+tH%B GD#gY8 9 AL" t]h{ d\1 #6W6$I|>Ysk8wqٶ (Y:|-a DV* ;LN& ZնᮿP,ih4#eW[A|y |""C,jqS&l]zn6|(6Ԇ0 Ùd 3 ZfQrUn<HD,:r_ې -YwBCyI b.h^+M2Y+L_E:98ȦݗJ% )\s8ZjӫuSy⵺js@F%IBd?n/< UձtJùi۶]!Ћ^(+|^Y@{G!""Z?MDDڏ{\ljBXΔ]`mC;phgF'b~v ָB""{b]6>}{jTѝ@$St- #P)Y&цo䷾ Į'1n IDAT5hx"">Iܽݻ>?7^/^0_wv&\>{QKw<>TסQ%2H^8sH^+)څ} 6>߆n^8N Na6X| Z~<0s;ݕ#1݁HAB I)1t.!5Yص/Ҳ`E kV̇0nBK=vjg s0gg97 snK#x.@ I-L HAV8ĕ_'kڪ9c?:7ëW.kGv/@h t9\8~b/{m;6Hh%TV™ϭ+/nf'IdJ?nV :HqxadI`aA@kZ:>vv;{YOx'"L^ jEѹS(22P-jc+a©Zr.,P腡:tazDhDD! &` D^t6C>V6 QR`*L;A–K7m֩G/Ura'b~/vWڥXW+^۫m /qBA[`iN¢-gн,lH†,lȶ包t *.Lo+ xF^t%Z}DDDuG{:1U o}Wʻ[ZCu"m. -TՐCC0qtBN NDDDc:RW.#q<¸uk_P4L5lAw;ԇIQVqGx'{JIBZ 歹Ųm!5 o$)@Z-!!j| $s SZXY;D#"ج\mÊp3868;ktx~ֳ%ivւBA('@k{;"n -z+axKyB~b,C V fVLN$`D cj|LßIosӪrT+kjjHH$ /] @ag]f ,._?9i^})/Q}ZfX! t"ӰT~p"lj2\ QY7-C""(4!L@ =ud+JA &,.ke\¶Kɇ&%fy[V޼\xպ6ˋu{պy!~B> sẏ µwn/lې Sl9aVn~aH­H>$pIx`5dCTfۘ~ Cݱn|&x]qbYKjU*YyYݏJK*^|=s?}[{>XSBDDޒ8w:>0&~WNVڏۜ興>I#F1+X|mèX>yL4l-1ֲ ]>ѺBCY$ϟGeCQ|l؂L4b> !B` }m""S%!`>܆p|c>:}l[77R s蟾[~̇Z1·[1iC4""Fujz>LpffI0"iRj1  }AP.pPwE`ïGv7oTYaxxni5 jSs՟^,mjPv[2T=+Ha = SO# }LY  -c03h҆@d($Mb텼L@DɋXȶgP FǹMh%CŒXҏ|kڛ9۲tzM3^he$^y3_Y+tX8h%%,o>nfWW۝^ۋk xź7Pv>,+W*d-r>7Olx]]p3 a 3 Ckց̋A6j8:sFڱOSSe0p- \:ȱ4p:F8SRe8>"*Dcl!""Z:Fl[9|MMPa͕\@zWEDDv3ˮ _Cc !ۖMC3b-ea՟7>_ˆ@ܙeiGE[m ""Dl_ֿ([ø=UcxK]a0 "d.."y<"q }=KR0Ѕ^L4`6ґ ͒h>N"Mrt_^xzy`'»޷?2cH !140bׇ`ͮCŕz%̟o{}}H)u7:SW+j*  ,'Xpfl.hЊ7z#P46Bmj՝Vr`?~k[J?Z5ow޿qIw-/ +E*~i؟0T#rqxnJ$ܱ;n`jP8z91v,beThJ $T"awˑ0Hr$[.V>/T{;g { gћ70:'۶KNqjp 4 OFK4NC&Ƿ{=4a*%r>+2re(&|_ʉ(5kY$+)`؀$Ȱ% B4$ؒ\2{?Xc46'I 3vd@$wmsC>o+&!p&璀?lq !.@~@{c]5`lB',`Zض}epN:tÄ$lHB@p†;!8%?SR![6|V 8Z8A!P-Y-0ݱ ֛ۏdG )*$uɠ(%-Y9TіjӫuvZW+;]$W< ˯w6`w< IXA}(E~v>/0,xM +ü Ucʛ(ۆ}fc: \@ P4zE/~VqJe_c=̥Z[6{ݭ榲qjf?[JSeRjh=mI];1wݒ[ln݂qʝ>m-i5}ɏ>_`PƕU/u_/Mt |7 )ʲݭؾ?9ڏotɝqǀ /wڲu.ć] KQB``k߀YA'?}zuήУ,aF"ۖtn?O㝿n^ӈ_aXr3dh|΍cz>ݰ5q@7u7 v2NO#qհ!aMlLC7l%I_x퓈h<t\[уx2kڇ'>[ŐAL8<lf]HߛA>B=!5;lEsc z'"9e4cA!8zwo@kiZs3whvJ} ~~1ڞ~Sڝv^|Wn+yQqLf,"X Ř;^: D<ͪ} 97snnH 5j4 _4 +0H'ocKRPȨ~wuVe;/_3%s Ba  lN\_;'oN'p z;x0+rem"YwT=!%|HZanPDe炫ؒ쎳2u-a<`?$UT`Z]2-@RT'dP!ɲSgkC[/A?'CBp}w0;‡\]<^m8g0rs|mA3v)El(D}k7^ę^|E˲5{^q=$:# ï~i=0d[n ~-w|6k۹-ϫuk|L#[sm;@ sv묂uekA{n:94!}~,H v]p7ug\]E%Ek4" |E'TDI\UEXl( "c$"o-,Y(DDDT寨:+Dv(@~#w}+f˜ײzFcZrLbWk\Queʟu6r A""+[ ¾3l-I_?aoTwO:FKĵ?`k0vg-[X'>}ӄOt)#'nDD],XLoatǒKwt#Ϡ-GDDYkc8~nddzBocyga n쁩V޵Po|x /Eh9=ۉL H1T\4z* &샿}M՞V"l ]t$܀°A,? i-;҈ݽZ[omLBkmkon@oGtWyB뤻KуVZW ;l CˤϤ4LʙfN4Gfn6C~2ȨdT F1& 5 Cnع⎏ΞMwj}epUd ~Z^Ywx=.,+G@ o[Aw[a*,KV`)>Au4B4(~ Ys>_~ Xi j^*X/|E˗qflB|Vj-I$؊ `qog;6_MAKN0>[f"f2 LE&0Yf8* LÀL5 `6wXKoYNRz= i!I i9\!g|Q© Y-K2 IcZN\].$w3}vtPۋ$䧁^AԎq͠C8rEns@8 s(8c+KJ! gMC7NPgk-Q`vkmwlQp,g>w B:" s~q1iQԻdɹu8Qi9?؇f`utLOOWץ5== I "k,a/PJև>9Qv]7wǡON1d3/ٽNY4@X_WFYӠCP70% B iw3_eۛc@ڶr?S)Y^mۈ,X;8v A+O%9ta.yPP~?5 g'`)*,9(h LKVAQsӢĽ3 Gj1{zּVj^GIXpvFs+~=na;AM(i m5Ƀ] L;.wE&ShStyy^.9qu8N%#v:B; Uhݜ%u2HGkMWC׬vh۫k(mݺh~ll {YվB$I"kX&pq~vGDDٹj *yO`Ų[zpPM[o\OMcwk\QeWaovmz>]*SW'Y.mAw;H#""ZpWJet/@ +zꘐ..ي}ڋ y}SW ;\wcwv#Ϣ駠5GW,DDt7YMdzm96ιat͎ذ|MMh0?Co-ʋ-Qihػ {+ ۆ>9^x1>4"lv:k_V<LAz{y6PaOA7,5m:!Bx`A7p03??.I>mM @A-2Gk!`mF< o-Tu`(-:nfWkp C 'amDxCg9C W̭ệ,C `nT E3S`>mY{d ㋛oąΞMv>!Nɓ @2P TtǙ;V aE8zQX [lHN~+>X S9v7< T;9aJ IQ*gٰ*vFk~*mͨv[٫{n[a)2,ŇJ^sa!nH 3,!SӋxu(V>88xvd ?$"" $ؒ[!$97G0܇K[K 77{HUUڵ B"zϝ;nmmEggu }@3sgYվu=aLO7$ hm-ClܽDZ>X""Tf6"`ے(O J4 +Vfɳ vqUDDDY<~ ²ʮWFDmKSϖ {ADDTl˷"Q֗03aeDDKƶT'0XGFck̿@*&""*f%b_To+~ض)Q`qV Z(Z6381윸]׺6 IDAT2OLb@оdy='"";}uoP:t͍k~ oX"{޾ܽ$1Ė?#C;ZqrrhǶ0$"3a)n5ھT#cy)2ИESr4%єfm^ !IZ!!&ĂX 5c1 XowmPDeN؅ .qeo]aY7# -橛H& "*$G a6;B%""0!Lj[yۖD[<0mgO_v 0pkDض lێ \#y"B{ָ2""ՙwaZv/~"{ۗ^FO[ç1^L{gc/ܽ{c$""* @oZX}+ӟA3(BDTضC;[1<+yQZ |߃{=3700~ unBw]ki]_@DD^u\beйp]sACj~C_[ނ?}]}CwGDDm_Q4L7taCz #LΠ) /yzױ7 'ۆ0PJwkGuN'XN"ZɝgH_A |e ^VϗɅLv4 %dY-oUFf,x"[% I k$YAn`V.am#hTX [V`IӉ{%+%cwzv#jt:XwkUIͬ[X爌Mvt.t4!2&FN`w,EXm V~y"\8i@X ס$C K." ~| @\ܹJ[ZCi {r۬@;lݔ^^v[٫{zoKպ: > o#daCMwrߧEP,-!is s`LpDwnKDD%( L!9a|>tp$߂H$PWnxLgeS * \\syOfx/c:xgo~3wGG+K/DNIsU],xꩧpY{}_|tCC=ZuDDEDˮ !5zoը""V6PDDTe!qLWp&Qj~ce`g/2REgn=UDDDqZ>I &eS)_?g?Pꈈ^䷾ę:p_|Tģw^$$cm;1ֶ };d^/3=~'>|?Q!"^Djd{G=7q(ްדCaCxXs,Ixm询E""j#ݘ>6MXTF`qE-IA,؈Xn7bCǖ۠65A2Gxcq2EiAS4qdw;z;"VѽfჅ׽lT:/L@-Bza#G  qfknf ]OBkj^. 9^gjd| ц; val*SS024 ";y\$T5DTU?LM`:xV<qXD<,7 0UwW'QU᳝N䳝rqEpCYB,e,!AHߒ B$@ s% chQX0Sة*` !L ¶!Kڅ IHθp6 Ymg_=_qgXfN_gW@{1r.&ː h0e2!cS9Lgr5'|_ek+sJ۔rd=fjY^n+{vնWuޝqΆHII)%j,Y,KlgY4iV=7i4Mi I }7:駟$I0 7/|6Ç'~DʗYH6Ed \+ Z_!""Z@?1"a15%h/Wm- O;vWa_ĕg&~?UDDDjÁ'Qp N?|_ܺD&^zCT_<\Rl/j^v`}W+/!y,'#~8d;vλ`oZ911OD D88o ǚ։AǪՋVdȺ: Xu&l.pKEj/RW榽0zna&>ĺԅqdA CZ}l.Z8 C8tz`Ga߅ غ-f>8A4mHaƠd##ȌdA>0g:J){'M];uY=PaDtӲ5 h}ǖe~m nI vh~1iu X f"L$3<5鍬DpŶq ,~fc!Q $Av8 ;>1/;ٿSfO״).DRjXU뾞ϕZڹ8+mp#IPRi{xdnN-uU4Aںzn>,Lv:.מN7XouD \, " ifML\ @L ,'dyR@2%0OI}20I!~˹uB}C#IVo8V6`",9; Ϫ!~@w΅?̧>)|$IŸٟ򗿌7IO}SX^bicvmxWO=yBx'B].}Ѣ3Q9QN##L_EUz{a18xD:toҽ="IUmzh  UNᄦxfgQag}qwQ<TcDKЩ༶?x*8-iˆ ^2w;CYX@V3(s{Qy_w-I P9 DDekps={}m r|f>_`6=MOæ&ڤy1u97 O""2!AW4dtEkuEw"P35@ndM.4+XbX`ݺfJYn^bK I|*+ ؞MD'. Ȯ7 #'cmg0~D)@HPD ; ib^LϮwitfئ&l9uڼ"{~b\/(0Md ])ħQ1>3`b;M$e$ N&][&/1MXʘ\L;lx "7$_ㄦd Ĺ'g$ nMʵA Irrm"#GI蹀> B!$i"ozIF׉wl,@MsSNj+A~Er`NUnv \ ~w^:tO# s֮];ѯwgyfMd۶mЇ>Tpž9"L0 |Sg><#¹L/~ _zzz $IxQWDdM{HD_f(DDd]Z]әwTw[o)qUDDD;v4oc^o !""Z]1?~9uzWFDDtR>o{;\7 ""l~W!@3D}o)]DDTcg0̏ ^OklDG>4""(Ⱦ {W!KH^4:/c2]o݁7 7ܛ ""@S';qsg _㛧!hkh6h+Q9X$IZ] 0BcHN '9/빺NNA uGvYsD眓g9>"ʧg  d F\=>6}tzh Ȥ裣aسuIOL5TF <};cC,*Qmdyo؈?[8w^\0cq8x f<cbHd$DP3QQ$Z-s&/l h0dujJ]hV0+MfպX:Paax9Rcм~]| >U C80]QUCkwNf*7;.}k'?'N@^z /QWWQM٦_׋ab>'o6$2 /_*/_Á>m$I§?i|&"*Bv`ƧdD@HDD֑cV(a%DDD K$[V!qɮDDD4KAmb.UfCͽc?N.tc/>wĕMec>͂}l˗"""Qi3>qf,;ʼnh."')~#Ew; kGDDd!ݎ;Fw#ucs4"#r`?@=o]0h(gN!vN@Yc #z׬D_m3= h^^1n.M$5PkjQiz!Q"ׇt#4`_BבGvꯃ@g {2v\ n.z8|~#ҺcwrY *lzT 'BQPxxV=7 ~?4v=o5t'-9@DDf=h{[cv̌!f29Oo0D2M@3RЌ d CrU]֠+tEͫZ lXٖ]z;$ [Ul !IDDY:oհ0mP9 Xbպ=z`Y͊ '%G? L\T$ kzg_Lݻw'ğܹs]5q:jjj~DDeJxN ^%""*wpxADD4[於WCDD4;G ;@HDD03̴c/߃l 1DDD i߿ }O>Y(""%\MG_g+`!r*$""+7`/?uma_|&""6f4|z_B9/38?¹~B86R ^ "K:H^Aq[D` )9<IM &#G~q ፏ›'* 3Dޞuvӊ q{{{&֙2eO漳C ZG/  Fp]ހkXp*;#lz t L";M8+a݋JQN a>TA~o\Rk)%IdAـꢷ045@(HC13Ќ T#гSs|9h_)L%aRt f2 JL`#%H$M(i66 IӲl6Zv'GuR]Ά  崙7*!bU;Xg6MGGgkV۪kZ8+Vms y$kbs~O~PXlvڅz6l(jҤ|Rn݊{Ë/cǎahhL~k׮wߍwxэz;rx6ϮXo""" }_sݺVDDsKq9Tm\ŠcR߁dmhK[xnIK)ߏy M cQ(:ayW6  IFx"/o.!͗վw!;%Usm$_އggO[Q@Y2hr阙 bG W?u2w\oCgzU7ţs"H=Q>?p i݀MUڱ}]M33 $ΞA O@&8us_}./j`h ;c{6-jmDDDea(G&ObQxcp'C$BM4.2RSjuM6p|&歆l8 7>(R֭ǿDDD H4xvώۑF/~qLo _6$mUy9rnaQE9zaNO?RW0#燰l]{.aHv^F ODEXFM[ :o΍ \- 36/IH\H\^1ojAOAf @07@f`df?2Iv;T :UUS l] NqŅ!j""""+b0ûZ`V-ʱatADDDDkZ7b(^o6#B""P/U,""hn@b!!bǏmwn/aEDDDX5kU$/]Dy8׵2""8VJTQySn4}׿f˿@g%~s?4#S>@  Ydpo":/^A{Ô!|G ֮BW`j[`(*c)&|Phq G aCOotm᜶_ M#% "38)G # ElQ4U;2iՆbCFa +8..'d e|l6_V (FGp=т ӄHaR0)d27MdecrX BϫʦxPkjBAZS Yf_Y dIžݫSA ?Xwᎍ eЪYwY5NDDDDDDDDKDDT1'o@IJXe߼%?U{Ke`cP\U%^#y۫:""Ғ$ o{}y< V0h$oIU߄(%Vg79t`_#•KX; %~=Q~Eԇބ+n\+|U&@BͽBx/9tF$<}hǷɶ}& CӆJ„/2 p?dECv:ڰMڴ@}qqY""RK nfЇGC &*W4#NmF;(n'fdC!ۮ;5 ATH i|YU1ӡ_ ,ÐCVaJ LIdI$ :Lw B a٩aLLiؑN!€bTPL}bYڮP TCGEH50H&E: N/sԚ@;Z]׊BDDDDuȹADRHg 4>ehk{X5NDDDDDDDDKwxQPE&JBr8KXe ֕Ѽ`{ )cG Wma!U6[ N?pCQz{a_ĕ$rbGS|y*"""IUcP^칂}D_k4֧~k*$"r $@{l~ 0vʏCèrg+@HDDT-p4 G;uD됍篧< 7~ϭ;m'7@h:JW8ZKWõq36mcytpY""C9˚ Foh[?>dTtTUhv9]$-ÀH~~[氍 $9R _)SdR 7gsaA?^{,fdH 6*$UHتe ;΅س{ՒZ !DDDDxnK] /`Kjq""""""""Z $"xB""*sU?LDDl$UiS] $"R(Pkl$˨}3ܳho*""dFG1-ǹ 5oy[*"""IxP^  G>\hI:7jEocH N4߆+nHdC:hN$Uc+[w >gQ:B+/Cgܾ5$iY@b$BZ7`Svl_@S=`$]QAX<Oi36okF(āgnTF}6wL"H %LgR`,D4# y<1*l7" | À>2] ""#*lM+=[ ۍ 2}z(_0~*D)2(6#ƪ]ظ &<æ) [$- ËͷGX=pCaSڿ><џ=џ=[ýVovvS<طo8.{ w@fM̤xӧ?s˗=޲ Pi3k!kڢ\m][(ag}ʼiˆ0pz$ #·C0"צ09?/dpVCqg_ႊ۝[t-hyGk:Z3ősi |;GDDDDDDDDDDDDDDd1 $"!*df"1m""" ^3*yU޶tT ^?Mv8\VjVͽcg i^~ @*qeDDT2Cw qoD+QEDDD}64}3/70"4>P'"0#Wf]o^k {g}ƃ!I6bۈkN NP_D!\popm<߆>8sݳ{՜ID3 T>T!0B)OByLf3YFq=p'\zz<""""+yxW *p{[`bbI ju͌}0cl`a$ )D`D8*BcI$lTOQTHFѐViՎbXNj4'RSՁ'߱,DDTQ7aDDT J Rr8Z (RW\tMC&ǏmwmImDDtPnT}~iE*/;J\U"a0ɼ}d ,2""\+zGG +FXۿ 5s|'""#*zkw㊿ -0AVԵBX1r-l0uL3G/>KH6tVTm *jG/ :|pshipnNQq^¡A oh Fp]ހkQ^OBi$Ξ{OBeYڕ;kjܾaQKDDDdE$aUhip?[wᎍ ~&I ۲m(76=u]0a MOC3R44# UOHa&Ԣ>'npBv: ;P)UUjf{~ChqDT""(ۃL08m"" !^DDTQl+Vƴ.ђKv^mZjC[ߎOQַAl%*M83_>UDDDT+V` zed M=+E?\扈8_v77!9fs ]~=_AeD^=Ӏ:H}5D_ P7½V;BͻݡS/X $Z$xfgQ!}qwQ< A@6p0GY$ΜFiŽWXq56”)}|@DDDduuhC`G b$B:c)y(Mn,n=i>#2]3sJdB)+0d SS)\PndevuvdRn9.  ]۹aB""(ǓM/0P8Q9Їly;UY|9RWL۞*qEDDD7;ZjsG*!""*w"rF8ͥ-*J:w>U[:UDDDT4͏$/_*7}*+~hnX؃! ==xu]Y60zfьC!$/㈟8Tm@UV8Vo3g0ZDA>7س{ m4CϞEYBZPF\`Қ`mmE4e.{8TNR(n7|C8~.g_lPLC&$aB0!  ז%!&ݾ́* ` 3%E9{./+TmM%Y Gq+ ! oo i%Ja:tD'ifc{[/^o6#B""*ouA9U{ݝ-h-PkjJX Q0gQ}͆0MD:B>1uP+"""Aկ  5BcW_O6mܥH9]6BpvY섡?2%""A5-- #!D~u%݅TwF1͛QձoD/8G }04ADK깂ĹH=s0/pZ#X4!쬝1drO*C$Iiغ !}εo^iָ ]}{[jYĪf6~|MDDDDDDDDDDDDDDDDDTQ'o"" =DDTy--/1m[U4dV⪈Uul-a5DDDžr%\ y[o+qeDDT ^9SC-QEDDD7nNJ=? DJᅡ~ lUDD' ?)QPV4c~%"""h~?|<!G~˗dF4ǠVWcUa 7п~7Pw˜ ]$"ԕn :/=س0f߃Ȍ #z0_GY@y_@}>u1Wk[Wۂ` 読3ƼkYVu3yW0#燰l]t{98PS.܏p 4Sh7@EsM+j55 Хt*eυ*C\;t֡=Q97H c[[M;_jDDDDDDDDDDDDDDDTy@HDDEz䔈Rftj]hW6g!b!-رyQDDtsy4Ou^FYo(qeDDdU4|t>ۃ| ""Zd,#f X}v?^]f2 3DDs`fbgPGZ4|/_1e=%""|~VV0boAaOwPTOb]I0GJ0m{^jf*GKC3c]-{ U>ԅ:>T'Il \]-0m툈2YkǪuQ`!U&2TYŠAXr""<[2N۞,mADDD9B?u2okH\Šʏ$I=y< hc/ BzKTV>d ]3pUmxi-97ȁf)qMdF;P|/w#@QDcԂC@]$H]V1]`Mk0VU!e?<9lY-TN^6NA2 Ɔ.܇H?\Xjjߘ \]9,Iس{Z8xjpq.ܱfakVfDDTQOB#@HDDe(34M٠p U""P於ɮWCDD8f2ݽek !""*_mBo@f 8m{q$hn)qeDDd5`?~};ngm%y s 3-ط!ԋ:gu„O DC?>B/0\;A2""")N'wL?ur"Ј8c,Hvi֝@ġ~V qЬ_'DK{G| K}/q'Sj[׆pnh{uhC`G b$B:c)y`x -QEQgg2" P_j+MDDDhiAЁi=W t˘DDTZcG72\7""2&2j}F}ۧKX(H8nȸ߅+λÿQ*h2gZ4OտEfp`_ob o=bC.+12n co`;߆>:R6ۍ|;vDDDd [}6DG;~ z,BE4_Do^e\εm-[IY!TáSymTp_+F,dl`a,Ѧ54ٶ p_0ADDDDDDDDDDDDDDDDDDDDDd9*l|Q9 忩\q""\於mBב}VDDDD t]UUjʛw0~yUĕQ9zaN7(\P=,f`kh W|`_}ǟXW5e!$"Xz$ Cg;zyNEDDDGe8׬sڻ;~G?yf2GF9 εk\XͶ`+P9*?=QgX@p=9虙 RW y K|`j pm PkjK^0*NĴF4R⊈mx8UBd!Tz`'Um(a5DDDOl}1ߟ}Y4|%ʇ)߉nhs'ưpx{m#""YP^4}qF#*D'W HR>}!K%"BD?3-z;և([oYꈈZ];BwA:#v(bǎ"߷3㱉Up[7Jz\rd .~,b&ٰ.Bj/` RG*?e3mfB""8כ?0B""*O7 k~QR\.h mOuuoUlb@<:"""먾^.|ԚWFDD _ /@5*?X͖lcCF܌;šu”߾-eU0'j7߇ӹH->IU׆v RA?_q$CW ^[3@E$/]sakj5+ 3ߞ_P9pjaq?z$<2BJ2 reÞ z1Y!o# xlϦBDDD?{w}BD)QI[-Yl-[Ŗ;iL{Ͻm'3i;ii'];Ix-Y-"%I?9eA|H9<|"T  H8s9GGm(ki- Lt;M/ tׇmZ`W)#Sέ|^#O?>p2||PҺjH?yrCs RwШ㴇ַ V =߬ySO|U,j'{~Yt<_s`~u}^7{:Z>vRD\B@rJI}-j;OU睧i\.:m\ʡdsae|=t+sGYHxvR^{E+4Z,yښ)T  mgI(O>/y'L!UjWe*8Pd4;2 D,ݠM9r̚3&CS>K펬ѱrm``V^ y"-]Ih4=maLo>t4Uͪ)<޹ }uD'׾CLfnjR᱓j9p9{~+Wy1瓯y|--Z*_Kr)ν mN=UTM:eq&GdZEà%XFaB+\ _P*Z-j_\P@8  I  O.6TrZ@eάLF~yW49T>$+ljw0 {2>~L}Iktϳ )ˉqSg,K;:v]+뫯Rf8Cj0)85?Sy&fuÁu7D|I6ގ!eyݦC>mYUs?sY{T Z۴˿*ߪ`5G%SꝷzRof2t*}Sc=vRN55V֨^iJ>)S}ȧղTIV2,Wh2.wqK)`HU矯Η<=赮߻e]T;/8<*;Q@Xr1ۙ ؗP JJRB#&췝w0 O7k칟HgV 79 0/v MjH꫔TKηEkW?Qpb^WP'>a*Θ^y{@}gw$#avoBL˾*|ۧUw2LJi>̽lB;.She,K!9tg&;; .԰jSÒ:q!ȜX`rT_.;F#k>N[W_Wyio~EC.iH^ Eң?p\/?ꁷgW="oCX2 ÐߝFdg&;;P[*8S)M+<|:2U_duU4^LX)˲x_YI)5sSwdpTexrr9Iҥsg0mZylU}6,}uD#:14}OftɺWB@)Y@H;.U@87ob`{NX'N(?LU-4㪽n^.a͖.m=z}U_'̡ܹ|>]=2+/)ߧ-y$WVe%֩G_PUnk*^pjܺM$ILFcG5١Nt8Yf%S԰Rv髒Ay"Qya#yyaͺͤo8^|<|/\/%IVlSG:9vLҿߞKV&eOwHWMa{Iv]&˲de'*$*$'*$ʏĕ+?W>>xQ2^VҪSM+x;P@8}$Iy) , = KvVT.6$oDfܦ$I+FLV˭Qw\.r0i!6w\r}OdG֜yoN y P!(Lc5|N-s*DʡۙaufBjnn(r׼JUB*Lw]N};||xRPfbBn5-3 S ddUSs곿F.G;cz θf@R:bZkFm^3h)a.g)ʛϨ*RUnRܤ)U1w59!+scB\%_ܟ D!DB@sYFmLw9SWH4,~k֪z:Mvr7dNڲ6}1ɲcwɂZz%(`^F-U+sBAew+z]ry<΄Yrbp\}3KY$>9{T?rngU(KYS3pL__Ʉ2==t+ӭtOrecҩY2e)=ӫUUr2Uqn%jry<2Ld2LSφgϖU YŢT,z났\N\NG>٬I'O}%lV_U]R6l   J ɤCI^~8^r s*`ip\L).]p*R1֛%ٴ4T[n?3yGTs%S0  <}%׾*ecr :;ǴX( GFyihe.h0%   Ǔ^n8f;3|Ur8km- Lwwɲ,:X[m psK 2 Cu7TS@*UAMkҞj)g[Eվ:2W K>-_* YLQ羠>!//H鄶}A 55(j|!O}<:oS{q˲TSOپSR.6$pO 64(O>7]['ER )?,H8{}'4Rf;+&F Xǎ8>n;i`*Kpۥ~AO&;;TfN&;\꫔TuQuyܺMPŅ]Q=_ʙp|.isKTiyWhz0s ar][+OYrr *;8Р TL)5~9>,w}X:JB@E2AY!I!`RD"&`JJRB8fcCI<˥ovMчT'd%DVxdiuنF>̷Y-U}ozi'_}Y~KH3:|~DWtqf;!햷Aކ)IcbŇǕQ~Y٬é+o IDAT+; ]_~'\/w]XzP@Hf0d;+$&pv s*`iq l]ܶTJWv˖9c~GS'P[U-'Xw+yq;1UuQ5G& GU#O>>lIjsʐO&Qg?Lj}Aj՝;KEg/ *Y]-y|ͫ[dJQc'TH$TH&:H81Ą+2T++>W5hrB2! 7G2^HfDĖ\DaKa))n*]ndD=M2nnYC]5GVW~T4Mv.fկ|Ymq(X K}^V }cI*'oQp?yc{yWSe+л+7`˥*3Ma?aH+WN*ULXlD\ZBNBV|VB?żż? >rYE*ʔ% b2\..G䪪Ҥ<b\9ӫۧ9kW9ӧQe>o^^Xx="AY>t0 ٬ b\w$`6tw,˒aTB}CIlˮZGV!9CLo|MM'̶@b? nԡD`1L&_*748'dgV>@B@[J=2}Á|5 Dmxb\2j#zm˃'/eYR(PU(HU_\MgꙇRpsS XLf{3ƓE0\,VrUΊ Lo;3!J|_sy ,Kq.`NXŢslvλL ߪWU֧;;jy`6ӓ{gT>5_*Y>e]Ty W}%>x:a0M^2}|QS#Z.O}V僒czS XL( T$3 *R΅F~x0 銞]Tb.ԡl56AVL^+v|eeLmdk[`ȡD`15jퟑ cq?;RdYc})ɲ]G~Q+62;mY[g#.pN/͞Kufj󚈶sS Xl85PP ɄCIe @r,qy똃il2yMfP\UժzŢFx̹@YW\ض]m:,V˥+(kw9N`ɜ<'ۿVat}˕ߩ۔ڿpEدO_yv]&Wū\knhENh -gR X.QV!IdRFnؾ80 Z۔zLwÉjk$I)΅`;okůE+H L='>w]_[09J8k孒4-[H(T*  mIR*._BIJwwQ@8'o(9ٴ١$,=eZvs+O*: 0SVoKV6k p*RZZigOPޫ/ߡe\[v)؉qRӖQ57PVTؘbޯċ/l˥nP媪p4a.Ш  mgd$NP'KNfоT>uLmN$MM`驻>Pr>I`!}i;;Jin!JmlԪ5kli[x렖WKtgL=uB@R:bZkFm^zJ`3[z|VQ:`ah_QN k!œesy=>].(u*A1v%8%7<\rpP`2 C6ޜ*p"@%)fJf&`"O8W( S΋F}Z>p2@=@5mvCRxx=P{&ا?_5\YѲϺ#6uSzc[wz>3R>rQ:`i(`P`vVHP@_ᡒswBVf_@eYCwdrMLUN%~dYSGyJu7(_p2tbQlv λLKkiU2]Ӯ/S~:E~${~v]6qRӖQ57WiMS^]"=0( T,3T0t0 g.sk8UΊ c1yQ*ā n%ppiXq/489gQxm'Lg({ڿn`6x򻿯؃h3YMe3.PYP1Sg̺ӊ_;7j,LNjG5r=EEVU~`.P@Xf( ̷\BO( tw2AY 䪪r0 K[x.%_yY)#O?oY]p2lb_rM`ۥ nP"nmT{r:G٬<~*ZYWYE})=1 kr$*4s3~;VP`V]X(\mgqYi8]nDapՕ,Ows0 Rdl`]ѤKmԄ<`"@)VoSV.g p]8fEjk|qy,Km_ Phbo8Cg-Kݲ,KPÌ GOݮ[Q>B@2E,90r1 0 C6y۹01q`yͦv_SON;P3O)}䚆;;Xs 7[ _K]֞!}{eX?C3`z;c3.{_Kپ lݦW?u\^Y=?  K ɄCI8Snp$Hlg.Y\@E8vDY4@|+W*u8>=LJO(&Rmw( P'W+oVAw߯ډœ `̸v~V|2:f׻I+VW~SHt 8B@2B2PNWLO8>n;) @ZlgԄ Xڊ&}v^]a8BO< `XŢ-Y3TÝw9 4fR-Je7<>Wm~Ub=\y⩲׻ Ym~E]?frU]PW <,0O⪮v秜 pJn$OB$WP]D΄,zw߶]$llw0 0ߪ\EN9/${nn4J,{JPǣg>M7~UԦ?תze'6\{v ΰ:ow\6Zv i-;rwa2Ay!t0 d8 ?Òk/Upv\ڵj״kS ~&]WFDOdJ/,'vtq&u5eh;s rVH&N) ]\U`2 C6ޜ0*p"beYV .!̧6մoāS ƞNup2XbQ} w; 츪x>F#KOӪ*շuѹ ,i|vNksKjLޕ͊Tss,* ɤI@nؾ8;0%˲dé ՉqR ʗ(`J`l %)ģZvryk'>YrM]|mBS]m5/S򥟕'IC'Z{ϿRIRSدh`.b'emT \x-V{jwze>v kB@E3C!Y>p0 (UzP@UΊ c1yQ1Sg̺J|C[J지߸I7N9/jUNKOW>PrM nP"cZo~]_T/k_ԹbvlhX'#avo5lԇ|HJj myUmʘun"7ߢnjB@EsBB ( s+Q@(I. ` +ZYv%IM#ݶ3OA l %)#ZvUry*omטToMWQ_T3?֚Cerv]8u$^T{OBSzc[2fZmY;ovkÉ׵-VqF-I.Ԇ_K5sg\2C!Y!t0 ņlga:A]LXh9]*:k;>۱9^F {93J=RrM_ :`|*ڽ6K~TuW=?kQr?l!=R%L1ywX^';8f=qu]j|=KJ* ɄI8P1) 4aڦԛg9m=YpZ Xxw}J߲DˮZ.T4dz5%/L-[J0\]W@c@hS=Z{@F9-KcDɟmV5as{S{xHuaIşxL{j%봯 յJk74IN`PPvVT1q0`{QxTdY #@Ce[1b_Vkx<يfȶ06Qݍ79 *U,ߒۮ1C!5|.S8}uD#:zo( {b*ƞN _7qZ8{v^>4@FzB#>#+ o@olױ e.IҖuQ}Q@hf Tr^H&媯w( R.+9%`jmSbD)ZJV.wvBI?]}\>,49̸Ukf+UkFzsy!OvN)۫~XrMpe \աD0M][i;kjA*fb{Ob:Q@()72'?|˭wV^wV^+~]Q#X( T43*9/$%pvsSq lKwu)RSS<)k]Sd]ls$|ۧm %iT{rUU9 *U,j;ߔۮ1C!5qO}oi|?*.kϟ*Y{yoݥe^'3i1ʽ'1us*7Sǔx?{1n~\-4w!Wc>%뢔;`ɡP\> OVfC; ɤÉK].f_@P@T PU[&r:p"| e[1b_@UH`TZJ9σdRO4 Շ|ڲ6y -/ ` lX +#y Ԋ "/rqZ̦rIL{\g)R,}*%O}ΟD!6D4.?\0L!v|mfd KmNƝ*賝յ:`~En]=6|U ]uĴ"׎ڼ߷@92K LK:`r|K }J طXL[<"%  IDAT709LR=ILvV4˥W]D*;R>6uS!t8 `),K9I8( γ''q>S@5imuj f;eU/fr˧&INgp\;/o2e 7ekPH w`*mxM }{7uL'OTo.ä ܓ(:KU,j~T#gwP+T6yf7 PA( T<3 b*8ťk0)E9$qIߓ(fJ3>Ԍ 9Or)2ww T  lgIK]nAT0q]WȔL1ie̗憀VNMbvf-DngL/x?OcS3jv{1>*{R?zdC'V 7)r ?ƞT,?u-M UYViI){٭8>~v1MoUކ T0  mgD$.74Tr Jkm- Lww90voC/N]B,WM8W,zЀ$`vBI:ܴI9_>4@!|U(omט˖;LC!5yꮻAgM<ͪVU];GI1x"l T}ȧ-kjnX}II4JT(5 [+R'w9,P@?{w[}މ9wQ\$JBjvb;v$Mq4mdڙiL.0ܿ/p^`ЙL'NK,nڴYqK,ɲ-&%Hj~,cZȗ"<8LyYT&gs uh8̢Q"ͦ r`W<7O}s '>w튈`xT>?;╾uEw[qk?CWǦbQXp=h򔍿/#N+2)6K;c{+z~1}h۽'z?Ѿw_d2zv5gϟ3o8[gǦq`~Oko=k5ZSO?͘;~wG|%LE!k^Qa)$4xra_2˭uh8qV3g{Dě.t>p~]|-k.`i<}x۝7z6n_3Gk|N;nO)Զ{Ol[L=dQ>w=v4^(n>o32좻/"VW% 'fGG乩x푽2D#wsUyb<.};qߋjiuvE_ysX XrGXL1ͪ<Ѡo/]QM&/W.Tct&޶/bG_ZtP--<)/lJ/z[.6|⟥ `d2;_rTΟ3_9?Qx䋱GE5"VofEo=|2$<&dS_{㖍|OXV#ⷿׯB@{6,!yddRJ@3+'_"x+G%Ῡs#};SN`ؿ?^c_ndݎRL\JM;:R$$>}8^ᶘ-vhFb䉆;_FW&Gϻ3&ݘE+:|l_BW_ZuE~oxXlع~3G|i>h)䢷-kDL>hL(]Yŭb\noP@n8&eVWb,X-ZTRtp2Lghݵ;Lkɿr1^\\d<=??{ј64l=} {W\η?|%h/6;8E{p lcݛ#2>c%8r/ |ÖU[4jrL䙸bsDǁbo{ \y]W6=y wC1ԓOzb fUcСyͷDPH1p-zqlM?>~,ڢ<|b߉7>X֜׻G N5N%.iCQ9?q笟;_n>8ٿ+^%ƻ#2gVccKr3G^XL>(=x,n{ˑ틞=W; !pr]])_K) nFG4Ç;* dˆ':s[^|Cc/ƑwD-J 6{X\W{׻l@=QqrUpZ%v{!v{!.o 7BuO/99YT?ǖ[Fϯ/:L.$gWF!M!gUJЌ㉳ܺZRLW# 7&ont4Sp>t0qֲ((U7#8>7n:e"b߉ņm\jq9^O?b*d{bg%F^zf#Ɓm1:'NG5?BvB4甗朵Z*3OG'c#kgu+ѲqBB;qVL1 ͪ<\@XO1 ~ۇ OFuz:f_:8ط?4R9?Nivdb#Q}vK-7 4U5OE-D\L6-:n# zL=TDvgr~4r{l sKsNaiYk*.OGߗŴuyotyWd[ZL) )亻gթRIhV7_B˯J EG/:d )`ye `zo*!/ŋWL|v/Ė.k>깩xXJ5Z.bo6|D7+dmh۽;qߌKnfB;ν;νٕ=7F&~8q߳U\4_^ӧb1Obz}IvvF;u-[L`i) )NNfUO( 'XxhJ-ׇCgh۵;4Rf2C}cg<~lnݧE23}ύiXv'_pl)?Z곟g7>|!XRǣģb셫><m/=_OG#:-oٖ%Lswg%rݾg` ҬNB̾륃Fy|ldֽwEmG&̟)人gBW^xYypEZmd"Es/+ h2Z-'|r` ؿ?WǦ♣cq4 8s;b&>7/DۿdRL jz|x-J[xh-%/d21{^bOX?%=&OŮ3GbKWtL>|ii[E~(-Y-:cS_Eڗks_{l\L׻z NGbb#Q_XXQwsIVh R) GX6թRË } Jd[ۢeӦX8ujhXq󯜌dcm@/ wȿ&cGc!_01;νpmΥ@lMq:sC!q ;tv}a!y*y*""Z6m[Fǭ{mύ-o<:r߲>jPiy^#n]t(n= ) )亓 ZL; @S)5ЯTWa% {X b1zz8wƾ~d)&X:lyۍwlއ?XXPhMt6vy.Nj?SpT\ǯG&]ƛmhݱ ꏓcW=zUV^bc1wxǯȶFow7{$) )人Ϋ%,D`3ֈPlעٖtCbl+aݻըXt+Qzޔ,'}˝h+&/r?Ba -db{0&}hlp25z/</<(nmDۮѶkW9ݳ="J3VʥK1D̍Ǐ/Emnn>_-:n=vgzkd ^CB!M!p^-MF fSO. BpVWNF]`Tb{X)B!~#q?3w,zn*NimXl?pGq붥 jTW89;Zsmxl;&O/]Z-GGb~t$.~kmۣm{nmCL&DzL<|Pl9'ecٿtSPף21s'Oј?y2NꥋyGMD& ZOrLD29r"IyAa -[#وZm܉QMbEDt?b`%uθ©S+qߎ| dco.E pgsCLl-2+r 1^Wg~jT/^c?ŭۢe`l-ѲqS;ձxX/B-\v=esI-Zsb̩X8}Μ3g>?Zƍѱ@t-\. 4L6ΨJΫ%,xpUbl!^}eHʉX)Ӈ&βmmѶcgiSKܙ/GE=d|^}'J6y ːgb>Kbٟ3OG^,Kt̾|̾/}pO) =-7l"E ָC&Ί۶G'47>_wc?-SNp=לnֶlKKt{EDD'bј=v4f \]2hݾ=Zwzppu%NUKL 3(֘l-[c# Ud̍$ZE@s*ް%/&c7 [6tƦ8=1' 9~[|s_{l\֜4l%vݻ#|(Zϝ'b䉟65Q ή(nŭۢu(n fW:) i人gdIh&m\֡?0z=qޱ@iQG~=JO0ʢ'sFp7oGlի bhׯDd2?i ehQ9>Ok™ӱpLϜ '^u=ѲiS no~=b9Rt@uu'ΪSSQ"ͦfPnP@XO1 MCqYtV>{&3ӑkH7>t0qmo;RL\ }׾3-){vsSQO?f?{bNKL&(EozT/]i!(E(?rBD{r IDATɓe"|_q@ F}#MD!M#ߝ\@zTU(O$] תuh|ĉhzNwܺ72\Uݨ.:}9|(:O9{ol3 wN{~; Cl_hpY2L{z"7yVʥKQ9?K6=թNOEuz*jQl娕^.J'f#[llb12?{Xl[[:"uvEwwGc\4\Wrj%WO. ,֦M#S(D\^t>7B5hnMM%*^/~>qg2oU1p]Qd̴tē;Ħx-c2dQX> _Z-jd2 W&+iG!M#p^-MF )kL4( s+\L>mcFGRN@j8ػ/$jDžocT/^\tpT\z{_I9; wF~8-)%G&L61?Y @ȶGrdihKQT~Zg ֦CgC_.4.[,FG>pg/Emn6DWE6yC;d"Օ8 >VΟʥ)`U.]X-yܰ%q^-M\ qONR"`W:)ՋOTK֕'f2_>0h8nO) m d'Lܹƺw' })&HVg(j3K\ ~"Ғ^0`U{T<}l,OB-\veCJ.BJ+qV-MfPO. F&d 6D-jF%Ӈ&βoYL 4[F{ r Uw[3?p#ZmO)={|4Rd:48I1 ˩^ "":ndDD&8/;1^ ;m>R"`?GY|G}l4jzJ +VJ;RL1 ͠<18S@Kبp娻`My9j3Ӊ}SLFD}7H`E+8G)JN5JFce[G+?~oxX|BJ;qV-RLZWբ2|RO!,֡ G+oȯ}?J%D?7sŸhG=X͞8rBJ䵙)`-&^N]K1 }#n]|n*.RLv?$gĥ}'@Q33zNn]OlL&dj깩8}~8=1M-Q"`( 4* J)%`+7SJ!Dp|nt$4,Ç;H) do6#QN)@_YT&& ާ"ٙR"`5{ؒѥ9X: h*ji2$uLD܉ѨW)`M:8uuEPza5wFqymj*&ͬc?hֽ)%VKsNiiBJ\!K2\@_>2|i9 ' pTiXJj5f;8ouod^L&ӆ;͘?ZJfUx!7iٴ9?tkB4甗`鸉-#S,&Ϋ'&g@hT@17:RKǣ6;8w 4Z~-ѱZ}/^ h*Z-|6=Ud[Z z-ҜSXsNUJ)&`-+O'}})&܈Bja&M/ |g;M1L.|k1s乆;hnJ'fv朮9X: h:YuR!K2\@XO1 4ܨBjprauvXkZ7>pgsr9D@;1_|Ý{b>R"`-cs9X: h:u3,zBBX.CBiX bWXzuu%o~#DZWӟjNdr[6tƦk:cs_{l\DRq3MšB]TBrQ^!,b¨VXp}9|c߁kY#> wݗrbJ_YϞi, )%֢oxmrmC!M'՝8N* ڕ `ٴnds#`ILJ. uuGq۶kٺm絹RLU'^Ý{K)Vw칺b; #4\wR)$UL{{ M&[,Fs#/kUTbs}"r42l | w&}Iyb"p'~G&I)=t+.!c@Épf\c͠}ύu] w{M*Z-LR&>#ޑ^0`Mf2CcS_{M}{"[iw'+FH|޾@sj~3Q\[[ʩӇ%3huozaS$ ^:=wߓr2`;ǎ6{#Ѷ{wJfgM3G|i>h)䢷-+ h:jiR!פ2\@XO1 4֡a'F U>t0qֺsW::RL4B__~1Gwsyȶ*.KѺkw>pJfeS rٕiuu7W'K)%`*O4( S@˭xÖ󹑗SL*^Mw۟b٬MW/]i\$tFj;ٶ?L.b2`5R@@uuEd2('rQܶ-q>7:bC -c>p7 "^רLL4;" }^GޚBN&\ggZL1 kMmj* s@:Zv$FӇ&rz5l`)thscBO^ Xu.}1ԓ w/zGJN!M)՝8J)&`)O7+ t'*'rRiRJ%fIwL&D@3d2ᓿM~{c)VW^; ,DZN. LN<޸0כRhnCs#)%j;yǾ)Yqy:3R"`a+\.6ֶBR+qV-) ' sz"[hI1 4ȶ%_ܮ6}`0[nM/ ~#'q^-M#_J1 O5͏GpJB!M)՝8N* URL-FqP|npŦ'.DÝf̝2b SH3j~4NSo o\Q+ n:GVeŮ<8aB\rb4]>1<|(6؞݉YgD96G~4v\zsӟdFV~ jRQ.~MѰbEvcUL MOGmj*6,vBGG䛚2lթj쑇co:|y~["W(d g[!<2@ݝa|ݩ<|06|Ouj*&ۛm\.F&Gko'ReW hCZQk~ףlYvf+#֖G 0{.prh޴)1 ̝~y4YzsTF ԑé77<'?5ԵBgHMXʃyCBXk1}xmj1gwbgF)Fs'JD.xS? [u_s_ԛ5ku\?!uёUF3lbVH2jiް158|(&DDN@b޶}Gm^˦Mqŋ^zsQ#`6&'7XַM. ԵBggbV6@onɰ  !Y۽;5o!uˣqZ-N}cQʮ|LQ+Szh\*V?!uؑ<@Xɰ Yy0yaEWM4oܔM6@zV,}#rěRe 8j'@TΝM/8VOgVH >aRawwM4oڜM8ɉ ԯdLvb޶}{RFzM$?}1S~~̯Z?'1y`]3WlF!uЙ2@8:j56,V.4oؔj1yHf]c{V.'mۯ˰ GZG2>])}_N)vuŚ_}k ZBZ1 IDATe0jfWE4<@X6@ IӕWFXL'ʰ @۳;154D3aoh+~)j\Myp0N٧3lDDvgRorZ$3@@]+t FDed8&,Vɉ% ]`!74DӕW%_V[qu3l0vH|rD]C#_} F`F̀`}J%7DU3j!uّW 48 y!;_KmߞaSس8rEs>Qʢ"Եt|{2rC^%V33@@]˷F yy@j^ʨ 0[͛gD̙ ԟ=Skwd`~ع\xaTsɿe:>'?~WԪuR?x_OK[~eypmG#*ļZwh rt1\>\W5yPw_8ulQfW\UFF2lbTL Bռisb6ypԪ ԏ={\cc<;n3y7'_~n.jA+ QS\#q;kG}qt̚B^#1 gŨ<05t yĬ65}'2lPjZٝ>74f`~ Oh϶8ڽ5!Ny0T6{voa=ovC!uљUfA[uj**#yCWWm ђ2@1q@FMcQ>3mߑa3]8oly^5]Q:sA}UqcǏ >?"C#k{iQ)( Ů +{C3lPIۮ5@, ݗm?HVqC ""o7m_Bì9]"p ;kSQʰ Ii }!,d͛6%f؞]Yӓa邟9|]Q&mۯ˰ ~9&o}_iX~eW o|=NgWGhb~1KrЙ<@X6@ӕbWwr elQʰ 2{wbkj-[3l8囚b5rwObZ7Gjw˟XŸ̨3@QL DZͰ Ayp 1kʰ p7'FGd`)U1ȞļgEX̰մfmJ%N(=M)&wQDzCώWbr?¨V:>]@awwMKtUBb>y`m'ʹsyX:oxN\TΝ@ZAN'jSSw͛67Z~/wXR#<<]`174FUc擇 \=Skwd`~OGo&}2l\:vz4vᩘ.WXMq֞Xo]cQMk\&־ojʨ@v @D;+#&2,xtTΝK ӼisġQ"e `qK l\.VȰ Ґ+b%@ݹ/K4˞wkvd ă{OEӲF5n7Rߋ( Wtzg{X,ֈ|3@QL`ۆ_X7mN*hL:mߑak56ޝ̧bۏeԊKQ> {8Z-_e|ި~炵~}=XT 7eS 22@UL ˰ ]y0|"e \.7mN' ZZN[yV ,]?hyթ7qG4Q+.ăv/dR)Nxloa.WgeS 23@UL ɰ ])u;&\jI zhTK ,^GDed81o۾#6K[P5(vmz8GV˨qh _;X54N?#<c/_\$_|Mt S`Q*^P e4<@8Ӈ< Wsa\'O);JۮݞQP舵oG<ߏD׿ +WF_a;?'yh_iw]]ocGOCbuWkxuo\e*c[xʟ? X/wX(Yed$&,t†.X5o%擇f`۳;1kj}_aдfm~oD wC_| ?pFܼgdnsZs|qGZ%ժ8ɏ71㏿⶟/  *tv&f UK; iׁ?3+ou y1upb޶cGmK5ϊ_zw'1 1renS\{8;_Jv{zYZSx|sŋ_]?  ]iՉ3lBUBXTZs78o{8ՔԳ={Re>-{ޭqK^~Tĉ;4 s󞆹yυu`g}U*qc?ٟ\.wQ?>bW`(v$FDTG"ՕQ`6t v֓}j$bm7oȠ!2gWbVh捛2lP_Q?FMub"wh]al;=;TL+X,ĊΦ~kO[~ŊΦ8zj4m.̃ر;S==˞c_4>5]eye\4 {\cc::2l\]~`|0"b#}tkT;P> ļk#g>X7ű=?7Ҳq"=QȨ#sgGn:zK{b O]; R؞3޶_ys Kq$!Ĭ!,;'f#W~ڗz6@زek3l@DDӚm\ccCq?jQ~}={o빨g;6gr咞/VG姏xqsbb~L!H<4z2:O%mwd'kٺ5ּ7" Իɻ>ʥ].>z꒞%>)^v !~[O(]c14Mxsgo $ŠBW+|Lb^ʰ p)c QKy{Ks`ٝ.&O۵;b˥ލ8ɏEZͨ8vz4/}qt]|.ݼ!^qX՚z5^qƸ 颞k==1ry ੊,$tf(w"n}o2_/|Q~ " BR\U Խ`a! )hgoK@=loļmu`&+^Sq _4>sb\=ƫ{guvp#/yAY0@ORH*#QV3lBSL̊+| .,"V|su]#'cMWkij]YX֫ɰ 3rWGsov߈|p6 s󞆹yO=nKw\'f㩽]3;v?{-`ع,9բ2:]@bݝa`.xuob6Ҽ,͉yp_xMV؞–g^ 0\.z_{{ 3ގ8;ι9?w/iGk~x|_E^+^Sf!GDr1б*رٿPb޶}GmB!VWx;?R)f @N:⶛7+nZ#"P)sqߜ\CCyb-wU%!m?e0*:r-s IDATt,|cw%fVGc p1rBzÛx;xޝƫ{/k.z7(}eČeѺ4Xz r(tv&sUPD7tugk;6wǛo{VeW[cêhjCrg&Ȱ!S_ض p)r|x;}xݿ}3e-qzm=c_o=Ow2޸]MW^A3x BS\0by!@* >u=| 1uyo'xQVGmߑa.U.׾>r|Ro˃b-dpf/i}DD?g0Z-ED6}M7~+X,4ŎĬ2l^sb:eJ˦͉QŇ,Ucw%fhٺ-6̅\>+u:6b[elv\vx-cuWkx-㶛7D>˨Qg5>qÍ2>0,4iR<8WtE.o-["*Qƞjj?pbk#Wm(;#|SSz[w?z_s{,{ޭ5َݱcswTL*PMm=r\NwQN~&~30'|+;? ٔu=_SDԦf7F7e ~ (v.K*Vi0yݕa K+xQ>sġyN14<J4 )V؞]ϷmߞQS?(,['?~WDz{>=(ttdԐ 7-s3cЙasQ"e m;&@r\4oy2n0veGOCbuWk<;#l 37>'qGur2v|q^GӕWeԐVguƵb~3{@}JlX[eY\DmXjJϜI ֲiKb6u쉨Nu"Ukq};>dgOGļmus]ˬkbo_QX) 2h|7۞}}\1>0 S;;SpFMX(gFT*y+6@֚$FgW`ع`VM۶ XZWDCokq#QV3h\LLɏ}#cc3?EϾ*ּmQhiu!ݓQrhj}| ̽]f=>&e6A+][]Fyn\ؿ/ߎ/yirynBxBGGDʇ" ԝra˳+d.W,FM{>zjַj%V}"1ovG::wXv~|#qn>sqjr ?xwf7R޼ek/Zy<{|,}>o8y;zA{vFh 4j55+T2lq'%k7v腗noc 龜\ >C6`/9OEZ\+㱟cē_jolW ՈVkS$Obhp/.Q~d$5k 4|jV4@ի5譅?v'}41SSb᷿v{;W8AdPMZK|1pFG8Hj^f؛6ߍrnj>t/*F VW?oo9f7~Ƿ۷6wS>x3_a n/fBnGc+H}(B`+7};t͇ 8rbq&&+Y_=kW q3?J3hzpu}kq~5ګ+bɓ2@P(fbMiňV+5/T*vRD,7m|p#Zˑθ9Rwn.mc ;vWأ$я2JS/֛Պ;XߊG_s_Z}kW}XO!vN(_.fZ-&fuk^˨ J'7fw^uF<ԼHW>OGҋ{bs?o?F oi|0?z(Wue ;]vHa^vc#rž Ss(:]`G=~<謯3_z5d d8 ƍj޽N;5>jqIc?Wc/hnrTpxD"_*msqNT7":ߘ$1O:WÇ#3l$ҩөy G>zk-u `(HfQvZ: tz"5llڻFԩ O~DD=GTcpc%ޡ xyv,cيB>8goFlݸ+ QyD#5'֯ Xwߊh|s/LǷr}}=B1>@X3@鉈$IHX:kؓrI/~8qt8͸Q]؝;mnNxu>޹`|1=R<}!{֦4/b7~=ȧcG>CQr9ʥtF1A ͼI!!ȗG N' ԼXd R?wϼ~u.s蝳鱸~{9ގx{̋;v,c6 N'^{_zxrGODncv'b7~=)_ršQ:uzUjԾRԾR4o?rTO'>I?؋@87N'M-VLJpo^7tf˳{cOn$C~1o~#Z?oQ?y2}Gc Rݬh?_ڟHg?G>(%Y2@) #ԬU[̰ ;Qv c *MN=R-nFG3n=V^{Yū[goljq>?/Gӟß~7co~+n7_3` 1zj,_^$1OF/T]IvBH/w \ʰ ;9IG+y}n!o\J(Ȱ Y7o~s8_֛1__!Vr)V^7#z(>P,4rb,_˯G|B!8ٟG}N  S¡ÑxH*hVWbȸ@uZXy|/gZ uƍj\LJc1_kW#(=3QT#I:vl|Au-|3׮zrvn` F?q/~6 LvO.ZM)QBa`)MLRlmGhCOg,R- ]W+/ICϟͮ X朥ޜ=?w_E_ÿFΝX}??RѣQ:;*Ibc㟈s/ʓ E<5ojgT̵+cv$ӓ{s7譕K#xß3߃6r1|v&D{c#V^}%7cIO?C31<=CwBH+ FR(Dټgެ2n@j׼8fd;oG{}=r .B}ϰ Yyar*?:Cgcx\ >s @$I"?R杅{bƍRc GdحJSakogʮ@VW>7$I2l@V?2UFu8V=lu;FicQlDΝ ccQ:y*NTD$N`3@]]2n@2lFO#Z{Y+?""g2l@>Ko=arxFӉ|^ߊƭqhV6IpP=v,N}gpp(KH fba`'c{>n|BjDɸ*6=1Zgoomh;IDq|FS艈-05Eb_{<=U+O.]F*(MLfz=6 <^YI͇dKxcO?q2 v`|'ŏ=$ɨ!\.Yak!~tYO͋Jml`bk^:OrjZZʰ YYy+5[>@oNOfׯGku56[||jG̙ !Gfm1&d_Iҟ{nTW^vdat:lQՊW.C<b_HceUe,\xk+#:^U'>I_8W}n.6[hC3pyÁ+ \XJ1PඈA IDATSSlmf0Ibٕ+p¦B}'_5FՋ:RL޼F#6ep(fn }%Rj^foS3Yٌʰ lܺ7O͇d1@I lFȰ Ps95[OgE`)ȧ^ɰ \5>Q6!G<5o--e,Zj20sMcĉԼn؅V.T p?> yV˨ Y(,MV77@x2LJ{U CJSYZ讵]Q#I p?># \2@4?^Ԭ{aV_{50Lm p bmNh//+Gi#"^zY CJSIgdKR?Jgʰ a6!?>8ժ-eԨV+#]'?z=&?4}OW DݎW.C<b_،N _.G{fbm.||eU/}|,^T׼>{!V.< Y.{Afm1&lF*c6(}45^ɰ qVl~j>tv&6lB؄Bj Ԭh؂Tj:;N'6oⅮB!lBRt ]:F֭ |n#G`+ &ʣa-ѪRBa`+u ]ɨ k:3hhz&$ɰ[a6!_.wͻ V74]XFMX‘z/+Vj><=a!lB<5o3jviT]b!5Ԭ>;a Y3Oe2@XL[Zm|jDnh(6~P:5oGca!6v;V^>l}6` &$Ir95o3lvhVYql,$ɰ  v f;֮]rj><=aBؤBy45kj6`;4Sba`(}4#c+Xxk>tFMxP`{_>@Xe/$Tj^3@dk҅l(fa6؛   4y&5xh.qVl~j>4=aBؤ| iF#ZwJ%6~R2jt+/v͇  `ԬYe^k.,t͋M'"W*+S‘#w &Ԭv;6R:5/ LEir*57@d]~ $I2l2@/vV+@O5doJgRލjmhKVj><3aBؤB5oj5 X$Ia`)MMN]]@Z>>5 Da M طJ%&~4pHR+3l4f3V_>| 0 &J yazYfXM()tz"5^ɰ pЬ^|x\mxX`$|kԬ8fxx3oE{a Yrzsg+C3@[/fbMN;wRBa`*y*=l~u.2tb…Լ4u&6a -(˩YV˰ Ҽs'I͋ Swމ杅|x\m.M{R:57@BXyB/57@ -(GSVm1&Jc>}0)lSOfkz=6A||jw(g^0@[2@4@'5X$9o4}.$ɰbP.w[ZFMNͅԼ86a <>@Nf+[+\hSs 䆆"ԼY[̰ ynQbe<6A0HR S@ -H$ rjު2ljTJ%&A QJW/a`?j7+sg#W,f^2@[1@_4 Q8t(21xlwa`_|xf&6Bآ|y45k3lj̧T"y^at:Qͮ ,_8r1tve9kEr95kj6a5J%&A2pHR+3l'N'V.D~h(FBآ|¦B=Yfű IR(Dir25_~#6~Ѽs'5y!6lEhj֪-f՘O͊w.&lErj֪բdiPM ۩tӉlve}cԬ' ʣYٌjmxPňV+5/HWa`?ظy36}=5>aBآ|ˆVm1&<|׼Xd8|>JSy ?ጚ yV˨ e0¡ٕSOf {SBO|(6lE\ЖB=1>@X8|8|>6A4x橮알{]sny-5>B$Ia#Bآ$H95o3ljVY2aЉȕJy ":|܇3lv2@PN lj6A5Yq!\.JSG41aBx.bMxPB!N6}=KFZL$|{yӌ3Ӄ-N#$|&R?:lRNJU1F'24=o|`uʹGU}V2@ޚMfÞq(wy!_)Wo)S`Z~Zp̧ϝfCJfvao!~?7mll&nflFK/'d4OfCᠷ ?7W;LMR/'72vQ]KSDFl5d6(mn:Tqp2|d2Y3v7X_Kgl@1ZdV1ZK;ۯߝKf'E^'_<ҕKVӯ+@1[ &F @ijFm69sQ,_̛/!b8ɖ^.ͧϿ  T+ 26͕LMlpkwfldK/%g cfٰMfj jr"RI+7glT_BxLV+ ع%N'coMOs'72v˗Ks{BxLl226ϕ |[dmldlDKW.%C1y|e!2wKF @Zd~./glåXd>}|Tj1UjMM'B6|Ҽ~ @ZaE|ecꕈ(ϼ~!cB؄Z=;͠ ʹ:9 xQ=p ܼ S,]̪WNdlv1@PMf^/c~a R/'72vjHN9F#c#B؄Z <a\dgllkF1$/dlv2@Pk&Ao!cAN&'O+7odjK/&DLz5cB؄z̆^&|\2kt ;ij?ZgΕ ~1Z_%g:9!lB!2\YJ2T*|d2_y#.ߏbc#O !lB5̊gl@A[7G25~'N$Çq^6vYt1U:Z6l7 V4z]ݹҼngj4O*Wn\.`׮&SQ;x0c#B؄Zk425໔ Vz@6߭Dȓ|a&2`'0@P)͇^&|\zdl5OJf+܌Q ⥋Z`G0@Pף:5v|75MWYk0c b8yW6=;Bؤzk6 { P?7N&_q [1ZZJ/dlNa6j%BM ;Tٌ?y1 =ktXc M&a )--%F!sMz5n |=bd~şF'26`0@Tk vKF!s5O.Wn Gd>sB6$`V2z/v&?܁^T2_~=c KK_!lRmv6 26 e&jf3cRFĩd|("c#`+Q,]|'O{!NF$`%Z3QssɬnglxN [iG1x0g. ;BؤZ+=@1-dj@JMfv'csፌm؉ tt IDAT &UjMO'a h{qc^MAS6VXNi>}gSշl  z u\i0@_z9*z#O'bc0z-&KGә(Xt1O>,&|2c#v"0맏̆ S-ۙlNur2rܼȼvV=|w?_+v3BdƠ6 ,flו /|r(ǽJ|ziՀTE,^JN>|L-dj׍>|]GK<ϕ[U ؤ;wb& odlNgƠ6;[zLM|z#"ngj0Xhԑg~8kt鏥B!Amj:gu ;LMcc0{Ki>Ơ|`p?k/EI=O=;BJV2 dVL67QEDgO<}ፌm ggٰg`;v'*J6w5s_'=n}f& x<)g  ZBmџK۝MKG""bTOM{݈H{7)"/'{.&z:c#v0&V+  lAv&qx݌:ox*"" FD<_w~?]\Y&&`L%B&DDA y*s+L1w$&<Iu ^]CVB&DDvT*mFVɼfQFwYf oflncƤlF^OY JN&[kd6Z^;el|wNJL|eu T*fp!@Nv;cU6@|LM2cd~8t(c#v0FV+ zMwY`k[||keVd>ƛ 1&B&[1`~>7:ed1x c eaӯ_W]!Qg ÇQ2o6@-g^+͗߻ 2Z_WyɨZ 1|I!@6\i^ c/Dmf& ޻Q'7!Q5 r̕ jQM_Fj5^=Wn\b0G|;j1}|2Z`j%V(Fm~7=@8܎JG!`:Z2[_7\]k&g֜Q b2Xa @>͓"J߻ uW/G1$7!Ql0"LMAv&Ԧ?M?*1ٌm g[`!@dhw26kkl{qmR,߸̧Μ 1LD2zOhn27@eSϔ+]#d>73`3@cTT>; Oqz!wMhNK Bn|;Uӧ3`3@cVk  l\ih JSg$?݌zF}_cO$sG1Bz!ϗ VQ|eגYs_}=~?wT"yW2`/0@cVM l\z~pTjmkr"*z2_zJ6"~;މ{ݕ8vVz}2~FǨd!YJfÞB6N&ۣz@|d(F`?˷L2 Kgi|~死* 1&A vY`L=̆^(cp*=_=|~\szL!Y --E1dlңv'c3|LM`x(XL̵Ƶ?|`Ƭj^/S(wy!O4c/$e0V~Wz^\X`tRClGn'e06WyI%~|x>a2%jd626_ݹdV?t(*z6۫R|1\\zJ+"iԒD -QM26_n2k; gϥâk+{dDDq{'yt>3cb@JfBҟKfv;cKǣl&W2KG""sw/ϹG !lzk6 {MA7=@t26*zL>̗FF7}r:i7/n% +b2݌G{B`a!c( 6@Og'bc#Vn7;O=$ ѯO_򩭨c@JfÞB0\Zbc#;Z-/_ ]}(OOf3/w3@[6;F1* JF R;x0HK^b4Jfɸw#Ǐ?#'Jao!Sklk>O:/ƨZƵgq/DR.{Q*?IH}r:S+ߝKf'j 2u\o|836[g'1Ѩ8wH=oBp` ԿcsA+ܞgxSO;MF{,5cc̗^ϙ[PE,!=@X?܎/1*jVU@Q``2?{ݕoįq"G==?gslo/?̛ co` r@2?_ֵ˷o޺~S%Wdj{KߖLM \\=f́G^|~\VUسA7Yo3ؙ&{>ӿ-] X||cGG36`3@c#"bmd_7Z^Z2o;LJ%ϞM櫷opq1c#^j2ofl~`/kJ|zi|4oۙlSgϧâw+{@KZ̼|e ]}k%aDĕ[Ks?7W۝LMvz`2_x1cԫjel~`hFzJDQﳸAaՊD6;W^1\Yv[o} Bǫha?}0ma`yt8W](d^3el~as'GOn%!!p@ 4t 9&3[le[5HBi  Hm]d#|^}*}m4z9>@B=R4u!_P80͗.a؝6{7l|;B\N!lCx}b*f|܁h_5Ѭ\%l00{-|W`}-a#}:gNw&j~c x<=;Ս9kf#Mv[ٰ ˯ .n7t9'l8/&l~bU W֦C! ŰQ\6Zݑ~{Qu5 V㣮Y!4՛Cab"/?\6rDʉP($l~bٱsOc_S?5y!_)8fzs4_y0Hv/B? Bf\_B!g&7sf#7/j.FfXy필m`w-uʫ_)d6`1@;ׄ amb:zfjs/xx! ggCqr2ac[B!ˢ%l3gCyɄm؏ ( _W_{edw5•8P,RU3zaVO`w)MM雾͗_~9 y:ńm؏  `=o \faWZ/u.l^?Ldpr ,ް?;?|n='yB +UyelF IDATd(NO'l7ds0寄^|Bm- o730W{.j+k؍W[߾_Փwms(u\B4 F#ez(| rX4vaa 'O~xO\ ?Z:;\ [vN"7e !$P v: ܼ\_H`o*'|7C3\:-3n<;t(L-3@ d9v;ahZ=Qkҳ$j~r=,]=* ySPgF;@^A6_ ,NNL6{n J%/={6a^{^='B@0XYIW`6Y B6{SX s>l Ld|ilWoZ}` @|n5zan` !g&j~qr`{{ZOaFu7Ŀ.őOm-t2gzQ !$P r9;mvn3gn` 0whð; ]pemzKw|6ZKgτF4P(s ;@B!dhk X lcwgIԄ؍Wl_zR)TN޵> @"j5 xf#7/ l*L|h06bzX.Q"Gjt7ؽy5;emp@4oy:aؚ_ ƹh^9v<6cɪܼn'juF897}'lჄ47}O&pi @"Ja! 67CӉY!Ny279 \nV^}%O^{] DJ%7w \Hv C;<^FpO>pF2p @", ٹhk n5;P,ʉ;y +\fhh^ ݖ\PZfB  ,NML6Sa!tΜN.3 h^=y* 1@ e9B 6z=a\_S_1/b//'lg8B!T']!  RacBaV3@Jxγgӕo>37&"a#t 3@j%l;r!@*Go Ѽsm|r߼/Q| :v&S3@X3@J0wm|Om~QXyh>ԗnL.BHTF^a6`c#:Ѽ\7@Rܼst&pa~B.V( 1@ e9tevng5)Mp8y/ðK>]m-tN?͋a 3@ zv&Oq.7/ T(Bh_ꄕ^M>]`}=uw(NL$l!$Tac lDT(M$l@B!O"ak y@w#]!"Pqj:,Zmvn#>@X/$l)jaK7FW^6ˡ{h>wP>t(a#PPh]aV'lW9q2ԓV&= @bYm1F|\3@0*GCqz:z2 M40u  JaJ`w5sFx@3[K '>~g%j!$UYN`llR4/ [O< | KgD|;zkF= @bYlBQaoW_-l~QFw'^/so(dYF= @bJ|2@i¬VKߓ|X^/<Ѱ?P߼/Q~ {V&G3@X_H<ջBO< ƹh>s-JBH,DMvnͲz=a a+_ko6AF'0|=s;KvBHe8;{QшfBqR=y5aYaGS7NBRfV+a7X_h^ '37C//?w B63 dh6f37/ Bwagb_Xoĕ 3|-a#`JJ4 {<5bUOB[O<aFuin~ ?y]Zf}hDL(MO'l׎D߿6}'a#? /<ͳC 1@#UY!_ Mw#7o=h&u?`a4}e 1@#PVYÜ7o͜šBq5Cgφ^{Q OGt 3@#U^/ VVoFa!* o~h=d}؛Z< {h>7Bqr*a#Y`Jܼi'j0af`U< Ѽa{Im-4,` Ȫܼ6@B0X^BqV Փ`e%t<{I`m-W@-,$lV,~?yB:pr~60`DJh6X[ mOm- VVyVOP=uW(_< {4 ̑0y5 @z`Da!DMSuMiz&TO>s:a#vZ8Cgj|?Q0"YۭDMSqZ-Q{ P歟=AF h>sd1L^}MF0`DJsB`6PJR^XGyܹ 1k枩=Dm` PTyezaVOva8&jøk=hD#ak61@#U tv^7~^Xy儍Wps~60z`ho6?fa!nw͟( DmW~( VV쑣ak62@#TVYJ`t¬n`]<&h;aW6bڭ#9'j!PVfv;a_] Օh^6@ 505b40؈GɫIF!P67@J`ܼ\[H4w۱PoKX 1.6OԓB?+c!PVðK`|tܼ\%jN*vg?Q1.! ѼrD8'l!P)g0zv&ۈfBqr2avܱ;Bya!VX 1jko_|>,?%l!P6_{V&یfz&B@ 6ù3߼/5bF( Tq.kF'Bsڛ }#a#FeoEL86bFe4;{v6hDr ),gaT~h䞩430bj5ڭMG3@X3@U<Co~z+a#Rk~*l럢yV=&l!X6? Qu% VWyV7@,i!Fh枩r9Q#O`IJj5MCDMHrT櫯~+a#Rr_9ܱ;6dFTfV+am4rr p;=sSB z0LP(x^,omain.HԪJj4_a6b?z4Ma榯$l; Uխn*uW-YE,HL`l%a6ɜL2I501cf0vd`d0eRWKջkus׿\?^eVXRMUhOMEɱ kZMX žeԯdt[~;o!Pڋ__!q`%jzF{j*2=9>@X X*zV$#yhW+IJq#μtScYrj!R5c0"ZV}<5+ X徨?dL|hԈ/SB__^@3@+, az=5+ .yj+5smXJFL|ә7#Ͽ"#95!b__7nLۍltSd!zQ(c%/˼i|yx"F,>ݿf_$ɩY96XYj IDAT  Xoj/~IDO8z=96tt8g =I1sjBI&96XYzai(^п̨<n?v5X_j^ї\c#X] @H*ԬmXGZ-&+^1;o˱59W_y3|Qɩ>*Ԭ՘̱ jUR>7fSnS#NE>Լ<6[cz@RfFtDaf`=DRI/1Fo]yWD\ΩN$h33_vY 1@7l0~Uќȩ'=7?̛_S#X @H*̼5ȩ Eg~>5/ w?#oOͻ q8n8O|,ZG慾}ٕ96!R&'sjr<1@J}2ofoFܼ'Fuy3G}8!7nB__jj4rl2HNMe_COzrG695"M{n.~7;ϊvҚ7#Ͽ"w̩-G*Ԭ5وnc|ufgshjn4#Ͽ"ʵԼ?!Aшx_MYb#jawq1s `kN3B[_ 7s_rN֧nhOMJq/: I_1Xc @H*̼=ȩ @!BlzS3o?X_N֟|:~;fD95!R[ ՚H ,mž( V+YXȱ0s-q˼?kW ?y95!PD&'slf=}\ͱ UR_y1h}hNLă{OM/xk$9!BIGrlDa-&f̛hmZqlϿ*S+X @I*Ԭ=9c|5ɈBNLP3^+lْyw͉Z]8dTҧ>CJd#&vњH , pJCCqƯ6PHŃ}wt-_h|3owW"F> T'slltM#89=//{~qkrj,:?̛ WS}9!RIڍFMӬ3r!'o g?&fohmhogލүDXN`0@=$TS֤B`mjNgHNMXK Rl{뢸qcQhֳ}ŸmofUĦ'>9V RVR|tslf*(\UM{j*_D{n6RTӉ826أc/ͩ?$jfޚl ?YZMX6=̼Y|8ηGʩvc=_ϼ+ ƶ_}}$f rvc2&iSrm4&U[|E8#f;?n7VG /}!PmyqXf6o(Rd#6hYfW{}2瑩jZ|Scǽ_'\C#X @)!LaXkn4'GslZֿ_(2&*n1Vmn _vyTY94 @I*Ԭ=9cמbj^5@}ˏ{w}{shԻx2VK#?ZJ.Tb!%k5vXnz=3Oj=B,їbxs>g4vlZ߹&EK_HZq_e?mۗG/߷7_Dg~>n/R ǔ*Ԭ5iX[P[Mws987Ƕ伱1K?J=+9Qoܜzә3Ćv-y^5{m;̻;c= T\%Jjj ֖xaeؘ v?\\}q׾nwI{_]ʼkOO7iߵoIo{q87'J7 cʖԬ=9cלfZ-&گ}/Kx,{ǵ7ܳ]qoV$Ùwoo޾z_zGDyW3w#| @IԬ=3V+6˫U ͱ 햻'=>{ǭ9g$j;6duぷ%KޡfbfyfNezLR[S95X~xjV5@tz|>M~= Iym_,=VBӉ^qow/i @I7@0@ N'Z-6,fzǁ?>Dn(evq#_²S{z:^}Ҧͱ og?&f@cJ%3oOɩ j9nhmXN{/{n޻49ǟ;~ QܸHxﻣ=?l}qϟ&ñ;ʡp<$JCRVc26˧9= X3O-,{=i6sNxÛ)?0} qQ;i)u;8}oGyl,v3rhЃJjj֚ X[vlm#q`b߱}d v-aS8?t+1r'2Dؚß&_rD}ϕb^ڵ`zPRfZQHgX:hg kKԟ99b1+sL->@x;1r1xEQ_nbv[LM1{7xRo1 <,@*U#"ZQɩ kMLd֞ S{Ğ'6v;7>8IM7syg$6cc .SUZ1{7c_nٛDѓ~G\W26?Q(N?ЃqG Z^˵ќ.qR#}^Pڸ1u1xqo?݅Z1c>山<;( EiӦ( Eqp(J،x4}Xn,.8cҧdžω]N̓q K,]eKEiOBBA$ҦMў:fޚ4@nz=5+jQ(rl@.8]3q8<vK1?.>w4v(u֭|#&>{u,{JW:ƍ1ˢ?Уv!5Yyt4&Cbh0KP. /ot,wJR)w1rQڴiK!Z{9fjɹ jYV˱ ,BC /gL$'=9F^ݺ"azTRfF#&KYOʵ*Ow_ɯSZK8)EGi``y" @*U kS|tffRs"|rlzⓣs߾3fo5fo%ZKK 1bOMy/г @J[R3jgZ-&b ]pQ ]pQt_ލmўtDd˖(nrm4ʣ7vFlFRo4Уj55Fgq1}}96Xxf^W(ѿ̈}Cw.Lgm0 QVbge FD&756K'k4)6zCP†Qܰ1ʣFc+t؎7@n4rjzjj96!ҦT T> @*У b$Jjjɱ iY6cX] @+UYȱ v VÒB`mDwhj^VÒJB`i3s!>@n Vf WVò[էYO͒j5rm`u1@=,nI:9z46/k\ͱ >%jfޚl`iL?`jj96!ᖻkvUϱ.Jҕ"JvBuݸkߋ=n .L>7\ww{h&.tW @+t ]XRM[#98yF(u;36GDĞq Vjaȱ ɹCCG2g7?{ǭߩg\cz\ak!лn~ 1?n`3@=.saMāslpa:~(\Y~ Tf^g#>::z?ۿ߼!RGZxg3=@xx!>@]\|mNbφg7l:{|W%-yȩ KJiƅ r队zt [RfF981Û㞃<0{pxSwL7bՎÛc9cJ(Q(l3o5v37?Tc^|u]΃q`b=}627> lB!J55o5981;Ŷ~:>@*&qoCNVۍk;csquw5_^tݜBXJUsyc`M#1`e\Ş}zf{ U  w4.8ldrW`;,2@@a#&'KwsGcpa*fvw;]Aj~ IDATk 鎃< U lIړ9689B!.ڣb=z3oFibWEȴL8XY=K`}3@@aDDkS׬gZ-&p[yOsio`(Ud歆Bw5Ԭ80KJKҼX *Pܰ! yq$6'5>k9679'MK`}3@@PZM[FmNN^OʵZM>giF/>6p *T S>kFXm;Ŷz1:D!IuKj6@ZM\r={ *TYkHMN\{v6:yRͱ SwgBX%2f}<3/ `]'=BѸ]X.Rdt;(m ) qƆ;Ɓm#Ǎg fGSCZUêP k8`^,@N8 h8虪"0l>EQ9dU??}[ݫ`&t0lay- ]<|֧:|ah>oճp3liq ? 7goz&P0M/@uMފɸLhnN|&ooxTg\'@3{7}+@a-@Bf$>,!vax< l2 9)'>>}77{ M%/7t=u% 0#7Bm3tmZ:7@f$e6> .C1?Bɓ26t]VM% 0#a>ai1/1tm&@Nf$}u[Q2'c/@m'Ed\ @3.N,y7aBwBxu03 CMދδCff2@ SUÌk``fbU&ocɸ m->j*XV۸3.H uq ,!̌!0C%oEd\!@32y۽xvC5еɛ!|7M^M,ل˴톛l3çkp3ni< O~؄/<%`fba/@QǓn2-7م_W_= Nُ ?h8`fNUa?>}Eiɻ!/§^_>&w? W W~WZIY&Fx66Y߽e\ܧ[ZBXV oе[Z2^9>G~ކY M75 ,:8nW_ !.y+&8^_\>_^}Zo|E@C e&6tm&@y|Wy8 BXUd]5}":y/:8^WOγü!P,°ۅӧ&͸=;aCf(V¸3-KCNދɴi<9;aCf(}7c*@xrv?͸;yaCfh]Y>n7c-u%p>asw~aCf*>,qɸOMފZry,|;=z'ߜhp0S3.뒷3.~w_?~'@3ˉFo߇ki2~:|wGMCqxLJxl? 佨 _W~Q~ni:|7@~CMދδj>|Yo _>&N>wwx[ LŪLv/^uX߹qpƮMW?y6\=76'ӏ~sg/!@3j>}8}LkB&º(23A|7zIpgZEd\$@3urv?bL^kh'!.BjN2y7@^F^!X,m|m'`&B *.y/jBx]0c CNދF^!X,m7LVÌk``bU%oMvlh䭨Z2e e:@v}4਍TθKfl2@BM%i'E%\0c,'ϴ8vC7 l2. [{ oE=!!\,mo{\dZ&@3a/@ܾ%ºδMf.!p2y;y ɸKf.V5tmV4 r"@ oh'uq ,!\mE]_g\6u%l0s&cgZnƮKދ"X6BX$@ nϸل8&Ed\&@3wrv?bL^=CNp`VU8)}gh'''!U1pSqɸ86CuVk?9P@`bY&oϸ86C{uq ,!,@,m3mV4 j"@ nu[4 r"@Ivbw}O6{Q * m>{o p4h.2- @ ˉaa7dh{Q7q e9y7}%1  '3q ,!,4K^8Kɸ!,D,m7 l`!b5 ډa-@&@ ˉF8~2y/k8BJgY$Ed\A"^8!!,DmE]_g\,еj|cHBIJ}i p 6 Uq (_eZKɸ!,,bLǾϸX `!VUy>WK7 l6ĪJƾϸX8q"j*@C$VT, U 佨/2!@ *y>`ɆMi BXx~U% es7ϸ!,HQUٳn2jhhZ2!@ dZ,M!@ j>}% m:@xZ m 9 }2jm Y!e>^K佨`rՙ y6WK4\48.03.M"8>0:O޶O8f\,ж[Q7 U>a|$`q2y+..2.#@ _eZ,~x7hkIJ a/~k%w!{\d\Gf^L*`IMi 'BXXUK2prv?8>@S±ʸXmijʸ!,PΓ3.䦽Lފ"8N@ʸXmi2.$@ TT۸لnq > e^4q °ۅɓ|cE>{ɻ!>BXx^M*`)r.@O(>,CXwBUMPGJhc8y y>` MފGac5p`buU% eV4M%p`bU%ocg\,ж[\d\K 覽Lފɸ!,TΓʸMn6{\d\K*VU6}vs6thLK B<}nٳ|cY./'Esi 7BXaaLK6y[߻Ny'8^P*'ci 0weV4q Z~.@|[C&oMq 7BXXUs6\^&oEsq 7BX_e\~ C&Ed\M,Vs5n6a?{\d\M,VU&@|CN BXX'oc}5 *)pX`y:@k9t0>j' j>Ws5\[\d\r:@8/1t[4޽,mYMԥ"Xr&(%>(VXZ='@>BxĖO}8 ^\j GZm8/Û7=n6ǂ&=6M\/ IDATKыu݆!ݶ%@Ua_ڤ!ݼxO GZ[:aǂ$/B GZmp~55ICvMS BxzrB&/B GZדp ]\!)x !+x O0*svOGB`:@MK Tu݆+x pR; lG  tWͮ%`FD00w0{n ^OfZocŦ)x #@3S՛}8..I:[ % 簨잺5%1CW!\Bbu݇c_<:߼Q.!Pd Im;ǭ!\B[ Ir1@!PUCc *x #@3T՛6tnS`b]g7-x v"@ &!0B@!PH 8&<-b <Ϟe$@qj=nRLU: }WGJB0SU= ^H=daIk)0Sdt H}v[5MK : !ũm[n ^|!T vaǂ?J:L]K Mv!<~]G8qBj]OSQ2q!cv_5pI`z=+t pI`qn~9}_GH!-?7?Tc`ƪz݆cWGH6fWSUuRRn% |p8 cw:-6Bfl c6ax2mSSu݆c ^4]!F.!XUap_SNfWS Ufr%@igœ'ŋBf aHǮ5@Ii&,Bfl\0c%mvͮ% uv]K~)x `fR(eOfuJHmb#@JfnXdA|0 l ^|B M>@QaL)]k!@js݆^OQ.!fR%vj춈1T>!: 0><Rnم.& cc4 l ^|.B T=u]K-mv[ E BE77}^^9C>@_ ^|.B ,uvO]W{x:eU\@!jnC/@Aj{ ]| B BNDpQU]\@!jsR%6Ŧ \2 BlaǂCj-6_B!Px:_N|p%@O!7a! ]W{1Cc@!'Խ,t = a@MX,= ҩoʼn @W-% ު6w-u/ ^|k>]K/%@Mv;G8gkoeǐ|0 U ފ|0R^ cR[GC'@'nWko<|,eko%MonBUOG @c*D%rq g\q!\fWk& k6EB;b}B:8S!\ Bqn1Pk=*c#@BxG5 ccv'4.!8 !+t -͏77!<p9wEXTUv᪤v6,nn ^| B2T:eku% >Pm6-]K6ݮ% >|pZN}v!\B& Lp5^ _fUZ. }q,x N~r_% @| n6mL)<[k/~5-*T:pyTtlB_[6ak8|=up N_[ ^| 1=u/ ^|g!\B_M݆+x %96 /B_U]vK!\8 }76tpNBߊ!I.^OػF EݣHl91A|1 !4@Sņ A6aM%XM9,&SD$yn[Ux/6 >jp \Of>fhz^66 >*;vDMha6'llB''y\$jD;2a`[ ,<}We6ulV.h b Y\$l\GX^G,!G՗˄MhfUg Cd Y4@U1@xy%llB *;9fr plr Ml8fo߆u$l|Wͬf!.6!)v ifU4FMm3@t hvyv>$l\͢Y>.6!);vH4gq6 :]9@0@E;uL@hZ U ٨HW:a|] ], A'llBJ0E&@fVE|\&lBJىBx,eq&.  h. }lB;y>.v!pl8f}6\l4NC],5b٬3e& :}1?wg kfU<]_z~0]!axYp,e"m]8looe6 IDAT !;o &E= U,6v!-? ?ݓOwMg߻\͢Y6.6v!_zBxw"zYu|.[:a>'lB?ٻ'D!>;oæiy>.v!By~>feՇ0]_绨thgUgljd!_՝O:!~y;5<O_%lB —:wO^tGg_s}lͲbz>`X"B!4i4wwkfU42a` !A3O^D_f,6v!B'WywI4{w^\=*:2a` !~ŕg. ^=2BBY'/Yqj@:$jB?qgh]'ߏThvptGFA`_ 4 ?aͻC/ۮ\h˄M]3@|_#um]T:r ko9_ɳoeAx? ]ԻM5fyY&l ӏ~0 ?0|Q_|^/> BXvHwY47@!(|V}p]- BJ̳ͬ$Q .<,ٰfBH:B>+BZa4klǭ,7$lBZf ̪h ) ep2"QY9IH!p-q^4_- B :a^ ) 7WѼ] Z.f!e'h. ̦y^N5R1@\[6,Y0@)4UC˄m 6BHk0+'%l`cp\z 0@lM6EMۆ%l_$a>0@lMVy3'jߺmBBx| [3xuB:>|[;D4@B`kz!y0@U3v!<6ʆE4kMakիpp4a>0@lUVY6fy:a0@lUVѬ 9@8$lʊQ4<= 닋maڬסhn'VEљ:QxVeشm4' VeW *M5r p ixz y p .fE <0M5$Q1@l]>.;f>K7Ѭ7lX$l'gϣy;3@1tͲ8m!h͚Gv>Kz f؉ldkCX-]#E3ߵ_}GMNLͳŒ֑?j6@1@lHT ֢BQ6(ҩ!)$<[Z* l Y^  &( l--_bEZ. Џ fh!WZW !ivˑ[hZ&@2@l0ϣ\^sY^ #%B`Ӥmli&FaV. Я 22¼XboY^ZM~5$I OD 7ͳAWV"[\(w}mnR;q&]hF@]43;7.#M l--SRja^ծ^XW/Ƴ'cp};)}0@l!٨өZy|OƱ˷|+ٯ.c?p(*Iҭ@Uz]6ˑShf{Ν|c:_8b>aTCaGkycjezaVM׿֙sہB`Smli&пFaNŎDex0o-FΖFkxј?w?{%\@ v377ȑ#qԩ8w\ Ɓw#wE]}z>hGxϺ^$I OD 7ͳAiY|e0v+ή< ]jOOj]z$eY\|9N:_x|#z$ILTfXr/zm8'" fzadYl]kkۑfWV";S( Lkӑt}ҡ<J lu_#I<_7>cǎ'?CCC$I9gh|8h-/WLQ/̪Z]՝cCO>dDDy},4-ODy?~<򕯔Uz"lgK %5Fka0O7u=y;;pܹH$$ٟ4M#Ig>U*##Q.[KK%:s&<1ug=jcflgGzayX]]<#"GGuo}kyy3<kkkYz*I(̳@mT-""⟟x}8vu~uk\ǏZ/Gi֒BLz]1kWDD|{Ġ'6DqZ^(.w|m-¼Zo8sg.ų:.6#kF:4wU33c;76CnӅ "k~;wnlϟ?ߵn_җ" IDATIH$cbb"vgϞOj$I_W#HQWz*;wFex8֮\i\Sdz<. Ul·-jO}SCu_T*z+++T*1<</bߏGy$~73">}{ L$14>͓/4Kn7 bp[ L$$<ϻ \8Hx+78HQSSSqر 裏Ư?kSO=9jž7l VM8[)gKN9[ ΗtN9[1@_񬈈;v\5 =o|^YxW"8zhdYiODΞ8п+wţql왾7mni0ƻ<lcΖtnpSΖt%rSΖVoswJ%</n߷o_׺mH|'}vLMfWZAo䫫\0) UTz]`J$8yٳg7t3g{=66ֵnuw_z ]Zqͥ39l#WZwΖ* vСCyDDm_|1"=rPV{ң&lw;&ϗzӧ m7q8uT~K/]z߾}=jK=}$Iā;ٳ6# yDex8֮\i|dJn;w ;|k >L@g-蔳%| @-K:l @-?E\azԧ>uѣG=@xk7 tRFoD$ϧَt"Ow}%44>͓/4=٨fT-"|>-蔳%| @-K:l @-[;رcG$ɫ/뾓'O?ID$?8rHy$I??QtxPb|mfZJ v'H$""~w7._\x????] { {h4ݻwǻk/r|ն?Ƨ?kGFF~n=@'ɩl套b+%.Gey6]b`+1@ءCůʯDy|;߉qȑkϟߊoۯW$q8qD?~<'>??ɷVVcp<[X( Y+Z-$) vKK'~8yd;w.vcccģ>|;7Fh3F333W'NƋ/.\ػwoŃ>|;G:NNʹ7Z %rem3%6]6::?x<]yޗۺMozSovW:@Sqs7Ͳ@yG6%J wtr0"_[+ '[\|e0OgfKll5 MNfy/ʓtIM!Ptrmϗl3@8wo Yb`1@npHҴ0Jli7@X)  JT*NL歅@y93@D:Y<@-,ʱlFkLa n!CSBQ¼:ch!i•sgc, lf}8LHj$@Omlq&Pf^ OD%MKllEH' dssYuf&Veʎ1o_a- d{i6tM!3CY0_b\/yuf6Vetj0JlYkWgKjleI'y^b</ U z&,X=rm`dmөZ$[tLˆl~&s[Bgdh0Jl#hy:cX@$J O|m`s]y0N !Sda-,6Gsnm !STa2@65Yex$-  z*h3@xv9ZYmssYuf&$)  z*,67@.@ؽ'*#y6( ܞvCc10<\b`;1@$I:=]g ,-  J:U+̚oyGtك%@_Ik|#Ƭr^XWggKll7Nf;[bؘSmۑBy( l\6w0uW n }epިuWa ]=u0F$%@_I$ta5 ҿsmggKllGN Q/ Z¼:s6vd;Zaшz}7.nGɹB?|w˓ͷ Vw=>@X| ;xjWN0@,a䇄mtaGoVNwB`au7*Y&,}^$l|/w`X`7nVsOsӗg09Er'@ZY ݯnGəB^d:{?\y/w`XpF4={O$lCr?}rt}.쿗!к,Qpw:cлy˓;  mtB?ֻ/ߟ0{$l]!ZvXv4/NqF7_}3j_?''gou@B`elr~ u#faV՛_U?.*h69;M؄ej0u5߅G/޾~%? ڽE6bx  {{=͞< gBՄXڽ~{#< Ãi&]m/e៾z7߫r8f`4!a!g?OԆeӻyrh̞?D?xZ? /Cht P+|`x  h^NQ4kV~_ޖB^/Q&ByM|:`+] T5@8ӟٳmXfQeig׫I&,r8f7 +$l4BVZѼaãh gVѼ$lò_^8}@:z!yO&<l+4B^/MN2aQqxPA&zl>O؆eTF7Cڵm3@V׫ӓDMXVA4˷60@XڧgѼa\^LB9X;  j.mF8x8, R3@ZEBAqxWVB+ 56Ń'l2)YV,a@U By2JԄeS5@o&lgZ[vv4/ҕai̞= m!Pk+![߈p ˢ8:DM^2@^6DbxK80 dBQ20{$; d, h^+CBc1@,?fp uW۟~:_\O%Rpyq uV5@oV Kl&gɺP_,ãho+w K! Պp u59; $ >&RX[Ѽ86@ȫy>JS' Fߊf(Y8޺W&l K# Yyri6 >@o$lc|: P;e&h >.ʼ)B-ãho >.h_$2a6Mqxmn+ K% Yql0eM K%a_^&lCG,N J6؊f$LOWژ>yF|k'afX*yPGiP+Aeo >>Ri_:_\p uQ5@_} O3@,l0f(]jj0?ϒ tV4+O2]|>a4Ϸw3@,?fqx}6, /G|{;a8B(DM+š={&ϣ^6 K) |+)a~q$lj li>~ ؿ[+ K+lU4EXH{,B{m-aW3@,}Z4/ l|wyw'acXZV+;||`.>~kdXjU(̧ӄmX! Kmbp>rt bڟ:_\O ZBaQF;UX KѼgif/i4_ۻ 3@,|g'88a>k{ >[ٍfOcߍfn7dm^B`!0> j0 N'agXza%ϣyq`)Y(ޝmތB`VVB͋{A:K9: ^6o!n4+2a>|! oD8L'lRT v7BOx3Fwv+^&|L{lmN&o!O? 7oFs0}8{ 9@c;l|߭$jv Q5@89; 8aR+*> _~ 3@4F y(u!~4BJ #CӉmHi68I l!!PFwvYqp/+FꨯDVaJEahh6{$L=L؆T^| \ۻZN6o!(ne>> ^<0_/x{F|q=?,h_$l jB͋mHaEV| ;0@4N4+"a>~V 3@4Ni(OC=^_$ln omjE^6|Ha>ke6!8+y^/ !h @#ۻl| fkwV 1@4R >|.DŽmfϞrtͯ ޽=AieՖOs0e$!H&لd*5;mNnNeO"$„!2`, 6֩{/`RL}j}R[ߕOK7}p +Ri{z0"6v f+Q;u2cn2@u\.ceO#IFgVk:;8 3@':ʽѳm{2؆"yy`o6!U &>˗3jG&BWWv` ަyݑLMF=Q` -[X$Kiܹ?>3!U}p WD^؈94ܛ @>laj2f>(cz,=@عqct޹)c< $mO _Wd^B!c#< $Z66ߗ̫F2Nl2l> Ю 4l̮XV3!":r4vtD?_ 4;0z|ewJf;S Md^=v4cf>4>, /M;;ܿ'OF6upӼ35!5& bg۬lSGY睛{3!5#"Gfj]3N}CQ(26kRX g>qqb&fkUG뎡czkӦܸ1ǯOĺe+jԑyeh&K!p[yxsqBϲy`W} IDATMƑ x|Ϧxxny`o\]5l;n7lcٺ-cK]F<+:>ug/T篜g_ u;75fg66vC}1 SSQMGE-p{{>q>3<6nɼzl~67f#z2 `i '?wN]ŞRvL#Gos} 'b, @{sׯlOb[gfzl$>HV`i ڙϧ-qB5ΌO]}c(OSo Gca:tF# 'D ,=@{x1kM}vhxh7Уɬ>5ћ×f?$>?+2X xGvmx~^eh(PH擇Ի|ijpӼ2?S!p[( 'Owo(7{C9~[dDUkFid>up4z{k41d޹UK]~G<83>GL-DWgG_vm+-YFU˗ʩQڹ%?k%rt}Y2MG3miJˆS*CcG2<|M|զo< R\sٺ-O FQkH]n`0@p *C&b۴D}j*x" BFˇB[Pٟ <|(S믥B!V?~ _e!-d>uP4tLV2/y0:׭`y1@p*Mfs왏3i_dwɌm2 <|(S6ګɬ,?nQ=D$a2{\\9u2z4,?Z2Ә=i6gך}O< eVi>yLMOш&֭'-еxg2xh+'Ox2_(}mB!*MsGml4c1qզy߁'35X H_~)SQ7H?ޛeE/lMSߌ錍ߎzx2c!@ yd֘7flM|5W`3@B{< ]]/elONd;7Vel hr9V?X2 -cF,,$Ofl ho=4K,_z=.|2/J#,ZsWtn+Oq033-?sɼcQ`3@bB!*C36hIXn]2xhgl.wjUF!")ttDߓJ 1}Vc~>.B2/tuŚ@1@|i~gj&C,\<JF!"xg<̧s_|h4 mz{?= Xd}z:6qY"WNN'`tsoF!" Eܛ/B,T364_djо ,bgW}@2jqw/dl܅15|8wn+zflО dߏ(uԯ\(K{!H(}-K/tuW~d^K1c<33q'b}O|+c#e ?ziůLmo^&5O;==/t߻9*'󅉉K-Bhs2Z7Ϳ/1?ܳQՒyehtn#c#f -[o{lzߌFk XbEy`͹Pfjܙϧů4q`螟Iޏ+l/jZ̚KP(ĦoݝYt)U?#qץ3Mr P?26~kR ,;~7Mo.Ũfjvqb&""O?|[{L˻ ,kٶ͹_>7/Īxb7QlԓwsUqdwm!2Q(cܹ/uGOL4;*Mә ,#n?|e|qFnhlmzƝq7Wwm!2DOPW+˦7SݫoG Mj=L36m:7ə?_Wc~>Iԣ޽Y]2 eQ[J^-ۘ7ͳNFV޻2evsȐrEm( RI8feb{{ظ1_㶺޸3z2}( RuzgȤnu\;:ʞc,ڿfs8nCr1H]P 8t_צ߳qMשOPs=W̒[;1/<;Yٜ/ҶqSz薁{3 6 j3Jy?53κρн6,^%lz㍭w܏$IF4U4ЅԀB眛 Ƭ[EYrӍi{X'~8=MXKY3㡬}rNiV2+RQ(c}lVǷnnZ~t0!<;1}rN|8_ziyx)mg`) ! )nؐ~ y㑙yW[CCzI]g-Mo]Op| 5Э[^,36iӦljjʦq]]\ri_VWl=2;]rB?|Ԩ}}kHݧoߌSwa]v{ B836kf=]_~Ag~0+˚8o3ScSloߡz4=O<)}oS(o#4sN̪ޝ>E}}W<4kt uu]mǠʫq{ia:iݺtkMGkkuR?lXS% zJ!V:e,y9O_M־)=w~71#[xTB*UyYZ_+fU{P! $TPGg寵Ey~+vABE]ζ|=(J!@<`vW wl.I!@<>oNN( %6g֝ZckYLv *+˲˳O!@nPu֖gB hkTu6gB ٽ[yQu~ *``Gy) q˲agB W=N1lPh[DT;򞃇Cvn>E!@pиCsИ#9L!@~]B8Ɯ~]jսvguB<wHfފ,-=O:dHF P-T#gY9s_\k7m-a4fDcJT} 7U:u ( jB ( jB ( jB  Zh IDAT( jB ( jB ( jB ( jBbXt`*:P@ P@5@!P@ P@5@!P@ P@5@!P@ P@5@!P@ P@5@!P@ P@5@!P@ P@5@!P@ ^PΝ|0OV>y'tҼѣG?GqD> 2\ +,Z(Ї~.(<Ȋ+ruGtP($Iq;]6o޼L:5to-+K-iiiYge˖%ټT@߲v| _~N B+|n 8pWFutt;Nnw^|А*W\qO~p imm-=, 1cvܜ/| Q~9jԨ|_رcAr-/^o~[v=\pAs?Sνttt3<$]vmKfҤIYdI566fijjK_N,Kuuu2dH˗4X,fȑW*6U='OάY:޲gϞ1bDz,Yfb&Lo|֭[%^:SN̙3~Gljje]g}z Ȑ!Cښe˖t޽{2~}0vr-|\uUiii3dȐW^ƍ;sG;BT|׾:G!P) !I[[[&N }ɓ; Z*;3b1b1Æ ˽ޛ}V":U}.N'N̕W^}ݷ4%[o5֭+k3iҤ ~#ϕ1|vm:2UT>8u\!&MwܑݻP(X,f*jf͚|/2v ~yK.䳟ldӦM˻<y+jbŊ|{+}uСO~SN9S`+g}v~o߾I6~a}?s>8qyw*<餓2mڴNIү_\y啹[l>0%7p]+_}KߟKsmu*L={q/g?яbŊPR]ܒL2T>Z( d֖;Sc#F3\x9#K?ڵkwQb͜9s'N8!^x3&_z`Ԕ{˳PZ*Ircp"ѫ~) ) sg>}dʔ)> &~ᴷ+2`-g}2qb7nܘGydW`jkk]wU* ?~|ޜ|;? [-|ޗ>t?^zi2ktrɒ%yGJk5*ԧ8r嗗qgƌ;][n-ܒ $Is9]v/ޣ>իW'\q%[n[w饗^_>eW*=xJ?r)IR3ϔ?5oy$:ujP?#ׯOXL^rWouGwy衇o}+s.H @ZhQ~6{sܛߋ}˚ ѭ[|ϴi2dȐZk޼yylSs9c=:hiNs=;]{_ץ"B;Y@o?{-y'3mڴ$]w]Tա{@͜93IR,S(JK}}}ڒ$3f ^>'+W̪UramӼÇwzzꮈ@ [bE/~O|"x;* _2z0`6ٳgWwz___Mzƍ˖ 1||;vlY˳N>mwfYlYbf̘O~e@*C'>\2+WQG9jGo5^{myE]#<2K,l+z  >-ZdA+I@'3{$I>}r 7T8gM_ǎ[8ԨN8!IJfڦys). ux( e?^(AG=zl<([7yjî[Κ5+w}w{S__ ٭-^tJ >|1?ea޿P/o9B!B!^{v?`2^zeСI?W\SN9%cƌsg_bΝ[TQFbb̞={sfϞ{7C]8㌿%u^Ky?g+eߛgy.xӚ5k2u?W\qEF]X{@%ZnAmq=x+=XfϞBbc9&_cP:::O:֭KPÕ@[pa僳gΔ)SJq- I 6)~L0!7pC Pu_% ,ȢEޞy睗O?=Ғ ]wݕM6P(dԨQ:uj?5\g?l f饳owq@ǃ>8'Op"_{u]9&\ҘiNK1,1f-#&K]tDr-yN37R@@AT@㱱9wyS@~mڵIR(Ҿ}]oi5kvo@I+Wرc4IkkksWV8-mݖ>IҡC\{N@5Xzu}ڵ?\ve};vLϞ=e˖,]47o.y睗ڵk*SN9sf̞=;1cFf̘Eڜ}O~jj:=[lɨQu^ѣGW:-Ľޛ|0IRWWI&V2+ږ-[]nz>`{6lE]K&i4yOSz4g穧WՌ;6+WLQM>}rQGO>iݺuȒ%K2z|ə={vP%]qX`Wl޼9Ç?(RE qU:-Ę1cvڔJ{_HjkؾpG& ,óhѢ$僧rJ '[ꫯ&IvW6Ue9<Ȍ5#gu뮻.Æ +N>=v @w+LCCCH׮]sNKmٲ%=/9O/n@ŭ ]tA-c͚5ͮ۵kKuuuͮׯ_3o߿-[$͛7M6;5ZjTo1~{.][o1Sh@R|O}}}wy+_7GJJ;/_< .ܩk楗^JQ)"ܓ1&L(&ɱ?PMwG}N8!555us=ӳ ,hvݣGݚ о}f׫Wޥ }KnrqgM̞=;W\qN>眝:ua'B3555)J)J7o.ͿKͮ{;}$IJRf̘͛7L}}}MVހإK @o/o@?S*:+I kK,̙3w8=ofyn{4#-W}.R)It<;5x<.{C=te`лw 4(R)R)w_VXùS&ih}$/]xg>STUV?~|jkk$7n̰a2mڴlذ|֭[s=dСYbE{^WguV9昔J$믿}k{qfn۶-=X?gM2%]v]GM<9'ONrN@Kr}eihh(mݺuzꕚٴiS(r)[nIV*ww,X9CӾ}lڴ)˖-e=rwwޕ4bĈX[oNQI#<2uuuxGu\ve׿l-ڹst=֭ҥKАq ڳgL>=z rѹ{s}A?>X{Κ5kV$.gϞ>Jwܑ'f֬Y午˗/˓P("gyftСbh,Y셶W_}c}NQٺu83cƌɢE4ع`$&Ŷi&]tQ^,]twߝn)[p}__dȐ!7n\:vXYҥK?w-ϟ?(_*?3}l۶-Irʬ\<۴=餓2~r!`k%K4[pa;(_~ػsKjާ}2lذ<y'lٲY&;vL=/})C ɑGY M/U34#' _r}ϰ*(5U/-VM;B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B  ( *B-#5#IENDB`symfit-0.5.4/docs/_static/ode_double_eq_integrated.png000066400000000000000000001063461412237106600231270ustar00rootroot00000000000000PNG  IHDR &^sBIT|d pHYsaa?i IDATxy|U2 $C01"bEŹzzlQzZǞ8V+ c<9{G$m**dk_ǣ6+{xy]kY\.KVnC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6nC6?sj/~.IRii~effjʕڽ{i\%˥~Xg}V۶mӯ~+I7 i͚5Zjo… ntOSTTGjϞ=$=z駵xb7P``|A}Zfo0+ W)..NʇԻ*ҢGjʔ) ܬYta#CJaaaZpaNS,X{|tt0% =3׷m߿NP|dyzg4vX|ltuuے2 =Sz3hٲex{\mmǶe}|]mxf̂` o~-_ /SCiΜ9WJ4mNC?p ͪ 57p1/fcnǼ.GJz]3g⑕D?C=m۶)//OO?UǩJ4wIP 9NY1b6l̍*mٲENS?-ZleggkŲZzUSSիWwo~%$$\돛/I-WWgp}!,.et]iq7%IIWTAvBoL1/fcnǼ. ɌLVRj$R8 N  Z0Jڭ+ N  ظPIὥr:YzwbQs?Z:T_<Ĥ'*<?, 7jV]MJ N OWpHs?,18 0xm"I,k҅FC1)3`$*09ΞG}Uv['>\sS?wvAsRphѱ:zܣgp]C TB*wLIҒ%j7uM>q[lVvbI҂种:4_L-Xҍ ,38 ٲe5O4cLiƿo:u˴hIRPPfƍ Ɍ  q1:wNyʜ@NFxMK9NmۖGyL$I%˥Ǐ*#cdKչur ͜?HvI0:g*)9gG̙']vmu-֭9ZbK}ѷ6lXG"HU4؁KٌpY99Un=⋿q=JKKo#1 8͜{.H{[Um/_[СCyF;N&LcZ[/j׮ 7`Rl|j.ȾM(. W[.^}ק>?=}v:0Ա` ~j@oS N_SSZZZzzzTWWn\!{]C IR Sy'8dJzN̓bׅCYeM Pۦu=.Xòej by V@<)  f[5=k$YJ N|: <#Qݕ4woiOGp~vM,I*-WM]mj/1+YvQ*̋5iz$0FM m',;{Gw|w=Gֽϭ(Wv=ԏ??^;;{nx>ݻ1]@so}AR/1#kV\.R='yc99mٲY)ڹs?Sw6w6Wвe+> 5k{BL%B5~J$t^Z[: N|YxxBCC>nhסC_ӱcGtBeYW-???m߾c튊VTTu}_RBB>`$)**Zܞ( ^dƼK:.=Pfpۺ5GaaZFƎ*Hqq 4kl͛@6 bÃP@HTL uvtז-`"Y,-\$'g>V .֑#t\ڹsJxCnt SU|V]*׬A{O*[k~ !#ds*wLIҒ%j7uM>q[lVvbI҂种cǎhٲtwwp讻Ոqf/Q*?ߨc5-klFC]? |$dS ?ef++k$iƌ ƍ ȩS'T^^E_Pfƍ &JzzztL~_*44L<~p 97[G/(cvё٤N-_Զm9zMo}q\r\:~22KmFJABɣ" -:T3dqj'l*)9gG̙']vmu-֭9ZbK}ѷ6lXW@.rW,2jNbsΞČc!dRZD1>SN&EDDhժeGϴt~:ĪFwq=yF=cn}:"՛o뮻ޭ_*2:H:Dsy:v[oW>.;sW ;v&LxǬ]FvbQ^qZBdZ+nWaZ04( ^*ȶPצsgk6~ѱyտ~VKWOO;}/?8d` N b+$,@a wnԤo}}wu0\= ٬{w -(i48?==w9Ɵznblr'ОshQ%Jet$v:`{oݺǰƕa۔1ZeTSP :+YvQ~x4e)Iӵhp"* 5R6{fƠЀUVA` ɜ*ex}Hph&H*̯Q}mcCLvIܹ_>%ξF?\EEV-_~~|{y-r\zw=>Zmݩ$;+K* ɜG.ԡld#/=3g8#c*3?ok==o˖JNNΝ"Iyy٩N>&IRwwΜSOX Zt|=ոl1!<=QTpZ'(<<\>VllL/~~mǞYW-???m߾岯IӦPf,mehkѢ%Zln,I SXX| ʜ*"I:sA~Xqq 4kl͛@6cNmQfL-X۷b Iv h+bV `\ب7n~٤$…uH*++=&7pb-XHRrIqqn}_˗{p1/U'TpО8Uym矐([pjIrCz-[6+;{$iEƍt_{LN&;N [ʧLІ iŊ^kٲq8RFteep~  ФI-ٓ՚pb"֦?*g[_r%&O*Ijhh[o^z/ҩS'T^^EH4{v6n\W@c6q}x5zTUU/gI[*++V#z (?ćeG+伴 9VA1"N)M8I7ݴT۶TNNUo=rrȘ}MK_r߱K%I#G*((D=e)--}\|  ˭PjZ0ZQ1_Zah5“`]֭9ZbK}ѷ6lX٬QSpgƍ+ {/wEƢy:yB*s +(}1XSSj%ImmmzWt:h9q=[XFm޼Qo;z]}N|?Q4faJY( >WAΞVTŌ'\_``&N_9%$$W_ر4aď=[ڵk;p8b{Lv5ƍieUVH,BCÔ5O?SjP@GԱJ9z:V>HuS??ϥΝ%IK.cv~{}@M,I*>SOԤsLܬfc, $^&Iڿ4=?4:e=;-X$ks^%EP֤Ĕcd-P>>ӳRI(r0T( y#%IM*?`p"x ɘ`?IҾŬ`HQ@Џ]S%I-:_Pgp"x >fJfB%Iw C6Z0ZTWݪckp^\i :D-MڿXbeWgΞ#I;w~>$""Rᇿ ʋzJ{w̜p~elV-IjoWJc='yyg zЩS'Zt7J7|q3"Dtsrxp8**Z2eu@EGnF'Z-{M$b,38FGA8*5=ZI(i%<#QAFuvmWlllԛoE7pPDSY,Ϳ6]k^UwC{JFuvܫ]_xhހKȣ>,&ɥEDD}hC#P@5f(/\dt,Ai䩒z衯襗(( "YTtFNKwM6:>+ w5“`IRrr&NnZ[w ULx Hdt&H҉:{ZԈ0cO 7:Ơ\N\Nc͸ 셣dٷ4455Vuu*--ѳ>-өE n XphgԡUZܠs J͞M{};00P'N/ L#P@pUfdԉhZ/c)Tv0:L-X*v^0JTSyQgOTQSSZZZD0 $ED^wbuw; Nǿ'=nFvUoK ShhhUOQ|VId셣 4vBl̋٘1/f2x!v|\{^k e$Szmthʿej|MgMy1b6|( CW9_|X~~ӯup88.N׮BYv9)FGb6UA^;O|̋٘17c^f K?Q_>zWuzz/ə:uj/jBc]o`>l̋٘7bЯk]V7|?gӍf VUW$uu:gKn R~~~.]K593I'WT&NKȴhcMX[`?IMg08܅ ӂc%I͍:뜱6b8n:vT NwEo/?\.i\fP@`@[{VWjp" 7  5efR%IwSC]0( 0b57Nf graBbB4{hIReYN06 0cHŌ$^`8P@` 6U4A ي( 0p͘;RTvA' P@`*s)*6Xg2 IDATk NDV-y,۩m P@`:#4s(IREi,78 4k(ƅJ(Rc}0( 0%ͪn(բ/Xӊ E%IU:@4 L-sH%I*VmE`0( 05w+nRλ'0:Ӌ Hj"`( S24jL$)/\ N#X,]sI.SjQ@1Cuʉnm_x <ʨ11:3YtN'WW3tEK>ZVJQ@q~6]Izm. )( HawM$UW5mvF4#TpD,x,ŢkWNThx$ijjh38> -0On,Erhړr8O@KH%V]ԇ NOBW1wFEI*Wr( E'kqF$C'բȘ`IҞ-*)78o'u6=Fz`}VZjXÊ<*JKV$]lԆ708M R%I5-ڲN0( 4'I*>S N( $ŢkWNPBr$2ry^F>b۴bTGJ>x P@쯛)](m4:נ$*&X7ޑ!*GSS]5 2#)XQ55 Q@O0jlq$S>uw Q@O1qZ.I$V]ԻUw77*( 2jIReY_{RT|Ţ4eF$|a\ŢUPjz$dvn:#p5( m %DHN=[ )!W\?2"!Lt`:gl(B]7=M#B$I% `)"*Hw{r N`~`C{+4<@kYڎE , 0.WBq^ .!? DMTLn^};r N` 0"u2{K;|fၺ"I:~\ןI ApV7Cqay܋I`>wt%FJ kaMz N>ZygRDKJkGep2nfiS5~J$Ba57 `Q@lV]wD͘;RЮrXU-'^ E׍$viퟎ\0IZ-r轿Sjc `&;3ooTa!:$FEk}3/IڷX7^!P@ۿ4*mp2AL&,"P}!S#Ӣ$I^]7ݙəIƺ6y9WeM' `RVULoG[~W `( Y,MO*åmkwNNNNx#tf*,<@t`ֽ~L휜< Z},%FJ=9}KTWsdWx`|4M,Ijn[/t 06UiɍeZԦ]r:s:07 &OO-PP$Zcjo28',1%Bw,%IJ拇TUlp2ˣ.40͝Za?T&-Y\( եoY;VZZ_Zrvm`BK\ˤ8]sJ]=FCN};QAA,$rfZJ7uW;eb%I\׶ d t]w{T?O|P3fК5k J _2Sc'IڴC:y-Yp8pϟ_Gjԩ ;6k,>|IZl6zzڱ:;{:0{1߱hUUU#ЏbQƬ%$GwN]5h&+!92 -ClDB&f$HZ>J֡=q!p;V@P``%|[UtXP l~iv-[5YDkrhbUoвUSo>ol̋٘1/f2x!g;V[[+~.m_u!dtA=F&%WsUQڨzu ML6:ޠy|1/fcnވ2M^xA C4gΜz sR~+'^6UAjnn4:ΠXlҭ_;Dz\-57NP`70b6|l0p!4w\%&&?z!m۶Myyyz駯5,{{ cZ TOwC0wqFԡ'UQҨknh 70b6|6VyhzwF Wnq\ͭᕔ<[~^{oӮg08Fw&3&=KVK$ݪ54zojhOHDT]9Q)\/S1b6|l0pLZR2J*S+7_qM Za}YVC- $I]m9Xfpp]rzO;~\5x IДޓ7(U[GK,&f$螯q1ھ:~@1[$uhc'hSlK:zAax* IKXι p?ŢthjtiÛyzjipBi( &vkvsA6c ײ[&k $k߯r9\L\ 'k+* 0\ڸX9<#QЮgWrUSbp: ( &wkvZi) OKVLm_T̈W_hњiuur:dK #$I=0,Ϳv~V\ұeWa~) X.9gtjՌ#uW.ҥkOhPjpB`6yS%I;TB?\"u }B$IejwN:;EenrEF.+m|jf-%"ӥc lOR$IԨD۔8M|-Ki{֭NkK,{_F +Vp]ƶ~ S ҊۧsLWTl$E{r9#P@a6ݾx$MVrA^>^we{;;z{K*̯fk!^NWj\$ij,Q1quV7Cqam^{R{.6  Z,󺱒nD$F/LRXD$YktDqL5-'C⡦VKn9T 8 eX4nrZ_;F'iT7^^:NvUKm9yx4ݪsG_Y F&I*:]7p@[֝Rsc)`Q@Pjp"`hh uײ4~J$pDn_}i555 OAY4>%Bso>QAZIks4~J,t zz/GXftL( ^beeH]NsH MҽfibFV\.(իW9TW( ^"5>LKgHёkWNԽfiJf~Dllr\zyS>9O +s^<]sEFIZ[:gk^yCRp3``[[W\Ƌ] Ә!L{X 7Ojш0M04wSKU;T Whx,g %17c^l<0X'X,JMʻ4}N{OT߮=[ o?uTY `P/kX 7O`FnϊV[kZ[:rTWݪc:wN$\v̋٘1/` [|?oŚ<:J1!FLgӄMHPmUNЙUvvn:=[ >a&NKPRj$),.NCCzzC%U-zꥃr8]~q6~k{%vBm6wϧGgNT մ\xd&~TXB=gǼ14 [Lh[#BoĹz5]Rө)}KV%krfFjQSC:;zT^ҨcTU,͢ Y*Ÿb6|̋-X,v(*,@O~9K\VSnnfOscWJ]l3È qSdPB1lwܘyk؂eBý`W|TWˡv͞oe?Kf)yT2f(1%BNKMr9{hU4rTXdlFf ٘1/` [|Ԭ #xzvkhlt,XLȴhuuLΞRٹIRUE*{KFEkxe]:^KTUߦ?VJ3:욘 ҩ:{JttN djX8B#ӣeSFޅs@Lȝ=K//TwSqQAQp 5:sJgOT}|?Fјqeēg혍1uB!_4{=tT'C`n0˥ M*̯VvMj̤9:Z67[1sc>E<~ -3ڝWksLf4:S,R#׏ӅFרt:ںЙU:sU9:Jvx dXtE嵭zckғ5&)hOZ-JQQZl*JzWFNת2W3yUY52-Zic5jL6؂eBF-VԶ꩗ۡ92sKf+q:(iTQG{O[,RRj*m\B Jw2Ol̍[bBFedq)zLm|8|N*˚U|VgjO'K҈0q1 1._`6|̋2x2/7hIҵ  bC`n>˥Zմ~1aJQcbF|}6fl̍d8sǒ1*v\ZM ӲX,Sl|d\(k$4wDnNVn*yTRj|+ &dv\lO_:v٬fjlo̍|.|aJcGhԘhQBr֡vl̋٘1/V@bBf˦~!uv;ǿ4KAF22SeM:_X:5Ե}1vȴ}Irw-]x ! 0DQ"HF;՞؋ؕ"6vvyw{GI R  < {TϪ4"{0=Ӧ{z2*{)a&s{KMI@AwlG-LElR?&*pN{n ǙuȺfVNb/u\R7&\@NH槯> uOFƒ/`r1.™1 QUZ iL=MpIOpITj޾/ЙQq>w )XϮvc+෎I%u=z Xd&$ijB+"HQ>wfNj97Ư饽) !HC2¶u]2@2q<cÜ>6 @$fbuMXfL!{.X|~c}#yvna- i 6emzϧpfHNK\Ħm4EMi! yOpI@AJzt*Ok}X NMꧾrSOqTq !VNҵ*IתF"HH7&\@NHl𭷱J!SMja-("661 IDAT`ɦ\<́$5ZV5G'yOpI; >l|o?Lgsއ` ؤ~KUO\83Fٴ5JGwC-tt7`rBMI@KͣZLS"TZElR?5SdƋ_Qkt(7cZ;tlsH !5O}lR?%d$R5G5 ؤ~k6uckCqg 3n;/Jr}lR?%dS[ɇnZA&_G/pw&^~!D0Ba5ZXv-$R_LcmgN s?@s[-)!DI H-NXD'ʙ` +0p1@ro/3巎u75oM'd/DQ77Pv\"g~oB̍($$l @0ԟ4ir%ˡ=gjwN1D!B~y( ޷ xa/'/~w!S !^{UVMk-$2eq1_QΟJ@B$@ļQ?uy~_H !Boo~K;3ԗa߿ dkY 2NrD(MZ㴴iMTUZJbHJU[q\W pOXBE ͭ1[c[26R %Y? fq*$-q>;D7TbSI[LFBk$bީ—ڊk8~!;O"&c !HUUZⴴٲ/sH>dx0`r. fLyƦH'1b ]!DXoU~s3}i_=YBMSinTyGf8HP4X̐淺-/MhL>J 4y|l\ogWeϱ'SWoJS]Lz2\3=_YҤD,(UQ}1 vEԔ%B4Fin#%J8"UE0xCݭI+WϞ4X~$( zBƏsT>ww3QB9 @d36j8WX2ΌMyl8j2,R,ʸ% T+2/UOZμW*땲D(PY1Q((}MC5 4SN̕t γ'l_\磺2i 6*a? W>ӫi SCIuF*\YXg"eVnקo+Z%\˂ڔ\krpj݄NpWt_Vn_*0(t}NH-/Oq=4UKom>$_&\R7WȗHHW~W|S!"9B)BCSd}n&ZEV(LWo~RPYʭҔPEQL4Q͐?d!~42,'˦EUg/ s']Ģm[K_y0l]]CBDM"QSma|&V *Do)iАА ז 5JyxS(K8y 8=X.j8l2Ϗh@wk@ +_&\R7S 't`{n,qlΞ}O$# D" 3DG~b(FZ$Fh25phWwa~{'$̝?ll域> G ;ش*Y#AI<7fpYee-. `)q F/z>C"h61#VtqDcP"H0hՐ\@.X\uKTZbWbtb h$7BuhܿoZ }~OI zOt * jE/` ֍8~E(knpͯFc0JWinfRPR(Y\_f3sm?otCh̜1D&ሱAe@5 "TE_ǚa?s)V2Rb84QQQհ1:=oH6'$Z+jX9#'o.Q,'A%gKs%NI޻eKU"1?DbID "1fH 1:!&3|Ŋɸɟ|l;[K 6q9##bt{lv*sϫ%%"%<ĥ}㷦e?dKrl\%7\t-*U>c8*eщ c]T2w@H>l.l[+!&編kFe2MWF a S# G Q5%RYN.#dO\ʣmb&#- ΏGo(YBqEC+K7 ?D4WƤ`a47'p!既(ͭó+Sȗ*_/]%ȤYaj '3#:*Eԇ_y0'/ }}{Ǽ_(g M'8<( R(ٞp?ݠZZ1ZZ1Z6&1{} ㇓pb˸x@!WXow13 z% Uᙶ tC Ha"- s'$`l'9^=WkeK;31o#_&<O!cpoٷb0ZZѫfTcb*yǶb~"e bL!_ƙU5e"DPP.fX'\f4B!3/I%̝K%MT~ٹx0C"oDO?|p+;nh! dJ)S400 $ف҆چށ^iŐ!uxB#13%~@l6b`c6Vײ_5UMHoY1O3֫`"FZ@HF]eNZهvw@$4-g My=2BiR_4)Rz5E[;f{0*-M4$nH7VGkem3n }&J7 mS10p1eJ0;0:Гk:[(u\R7v=׏㸵֗Ɂezxqr9?o}NZ@ɓ'wBs5ߺۏ񍧏ʖxΌ{7r۶)K.!=.TF/+ 411&FGFG)J !KTJx8K4HJ㯗*dSdY+/921$,SNqFZZziSW'ySlW8̫}hOF}B\kYX=ۅ XcY|b~&Խ{-5JiJD7D}> Q |[mk;_QREȹx5|`X@y=6:2.@ypZYu52ahk!s0Z~o]չk@ytMs=m}*TUFg dO${c;Ny|k(7[ opg&!d_5 {x W^yeV!g;`o=sCgFkek;|MlEDb)\RSȟ:EI^ Ä׬%n]~]].gqK&ؤ~KZ@Nl֏_ˏ_gJ,ؾvu{c+϶>D7ɾ6n6;ebDH[܍ߦk\R7%ulR?%]NZ@DpU?9¯7e ߹ͷq'j8\#B!DH"`4UeϦ6ljcxo>2|vv-yc7on#ZyO"kd|}pj4F|MLtvT\B!!]H[M^#޶;odyWĉq<``4_=_=OcdVvoa&¦|l!/ !uɽ{s$mF[;19ٻ]M:W❓ü}|Cgǰl^tMaӪ$;oha:rB!@2wu]SٺkHK=7᳣:3H@H<ϾՃW'ټzB!X$@EƞW_Zyhj>[.H75ٻ[;g%q?c;DOb. f0zXc$W%ٰQ!B! BԑyfgOR8zV:; u/UUx58,Χ8~-p9" !6T.߰UqtB!D}I<%mF~$3r5!{Hszccpqhʺu5mq]!?#iӃ~dSWYl^q&B,2aɤC3_ByCz{kZ"A}Ѣ8e &c5}]s,*$J0ɞqNLMUjLVueD!b$1<#[G.ʍv?xE)q6nu6<^ld9?aFF5:oaSgCe*N8g8š,rq\Ep7ͩ狄4:ctD+7-݅B,{@r:MsˊitGhzA@:* MQڛܴVn;.y?TB,CbmFLSڛ"@5FgsHH>B,&Oc?}'_[Ui]|ɦ]SYge[訕K6}#yGW8b37h9J{l2:#!K!.<`aO\pVZw0;;xtb :X0v\Nj 7˩7-1-qBjs'+᤽N:" 'B!G3(=wMZYö|IJk*~w&{G:WFG +0*ZMRlc3DdmaZ+#5iB!!&)1~ZJa}"cnP\/Eȵun~22^<)9<^N2ckpb*a#%D!Ăo!ײ{x%":5>̈ CS7^Njj2^`(Uv&tv;uFZaZ47i,a BqC,Y+ɔd9Er,vcQt,,ۢ䔰=u=uq<۱>=<$N1zeat]CqTECW54ECWr}C505S31U_W+}s>RS4 ;M';&eR%NX-Ig e2g33V$M $- IC$!t]B1H2n)1:"e)eɕxS~.]q˰\+až8M-8UQ k!z"!]„*zea-LXaQ* MMW&mt$B-T[O 3<4s @SZ+o5FF7t *oLS`(6gWZS&ZUlqmlvmʮM)c%lמK. W)(!E&Lj3Pb52sXetѴHhXYNܷьuTƸI29"Y 'M!SFBB~QE/95~s p>C< U)9D2H aĈqF#naBLy7`oc:M}>3JSS5]z.%L-Ktze锱j72֍hY9H.5~=hBU3ՏU&I͸<2#iNƲMp"|ycʟkGBz%`ⷤ$c& qd,DCĐ._Bx@Ă*'S9>vSdz/mJW4ڣmt錶k5LsSW"o}#]=(f{5?l'4[vidʪTwr<\9O<~ijBD*!,R -B #$ꤑn;48ƲLiҺ_ZSF(X6˾UCoqXhҺIc)21 v帞G&_J`I]Tϖ\&WrPTe"BMQDԠ A!1/FۜI=OcSr=ֳ2ލ.lw<<ݭ|D}V#EQ04C3Hs 3fbUZX] 0Si)T5)*t.fcsb6~ Qg0R,j`h:jj 9IUZXә~VaMUyyߪVDMV]bvˤ%|iZY:_<7q'N$ WJ5\r?V)KD!# !& bƊ)>+^{3ɍf7sκ[o|cuׇhE-Hgzn-D]-=[vIiUr<up<8?/9]qu) RUUT{(S [  _( RU&5Ui J(UP*QwX<=we4%B?CLj%i/\vK^kA* ᐁe<(ܔA>&]4*'o]O)feEY=xe/s\fwɽvWm͛x*ե @ey~v [:vs2]+<׾Jߟ<@ua؄Xh. sq<:(G,a2b˕3yڤnuJY582vū,'뎿Nڗupݩ.>FL..Lq^}~MW{ڎK~@gj/"/bv֧ Z$CjuxP9zMxy3`opQ-y^%xm{ɺK*3?nJYXUv(6%ۥl;8G*S]ʎSY3ޤID *lW5MUT*렪~ZYWotAWm5tJggw߻tgدl՝44GjZ.J-MSEBzWlos鿉Z7u_{$)ΌGG_nVmAڢ-u<2_QUCm K.2B,EQPX.5xC+&W,/B+CVo3|B՛>XT'Mٷn3|b麌Z7W@SOЬw;?Ʀu>:pE~=ƟV۹}G&ˏihFSڮieSl 3%L /s}Y*_GUv*\aN !C#CI2TBNT'k.kJA Dpf<_;-0A׽wg&#WP1?(u! ]SkC_+G+X?Xt(T^sZ#_ @CUBih+4'WZqu?J$lUCFHWkeI/':py/VfsBk*J<2Noai_Z%}g\eL Q $Z 2Հ/oߪSm2t>q߬%:)eڡost!ӛ~{;x {o [n !X:E\1?{\׫KÉUrʶ\&OoR)M^fڙ8DYH `Tˆ/'4Цl33[S%,7@C}V|ub%G[|dk=ΔVĭ{i'w`B!$ DBS h<}kʟ|3B| DG'B,?WGË֝erhfr;clǥxS:Ǧ%Fu<? >< 3Ο74u}J=^~NFB!jQy4>~j!N&W[j;#$,sc?hHs]s]ƞ#?!mݺ?Fs'<B!2g.d"¹?`e?%vk* տxK1 Z?{$?|*oj!BL2z.>ZX߸?E"zny/Ǿg<֮K]B!G2y;8/ݮnh\˟CzndR |=h-FѴB!X\@'N? _`Mê̛3r]tџ^n$B!C2Rk KЫ}oA};Z-k |ky/PU|?#!B1;q)?='7~l_3>k8YKBB!"d ^)**!\s2d^V>Z?)Ph^W!B,_@7qYuW/xG77p2~L|ض B! dȔ|tvG XIDATFZxp} :xo~[jew}O}-]B! KďN\9[~s<̫0or--t|C!B KHu+6G7ۻ< P\V"4=[mװ6m64jbl`VL?I -1mqVzMmR-S9"?730~fy>b}|3ߡͷvG4{&9 Qk:r^%fbB'2ccfg(2M/+ltsy$?-V򴤰\wW tr:X38p;F&Ft_%IsfdeSU]9zDW}k (-O|}v Q_!|J猛?~"Ȉ$9}foܬp:ògv Q;UR<0slR1ڙuaݾ+4kӯJN ~`@Կ|Ӥqrt7թ4ro-a%'i GwVccUVVg}6Ӓ%P{Gu\3gjJZT#Bݫ544ŋܹsURR>)Ӻu1Fg5o 3_*5rN6 $̆uQ{Vvv{9P{fbsر;XJo<7|cs1… Ԕ,>̍}dee~ڧ~jVZPJΝ;Аom۶mf,:;;511Zaa⋀ڶ6-^oPw|(p{ﵹŘܬe˖СCcn 7̍}SzzoZfǾPؕzKӧO1F---jnnҥKj-Xn}֬Yx<PZZ_}w~zz7қ%%%B}̙۷([ O>k׮3;C(;;[.Ƥ̙39sFVkBMEE:;;5o޼;OsȨաCoeo^{{fΜTZfgr %&&jٳgԤzmٲEq2^IIguuuiϞ=գ>ɧJo'UmmZ[[SOE),&z17ݭ\rݾ/ىPؕEJ:uۧ^xA422b*++M~~)**2|XVV}?nrss͛͹s"Jo>lMNNٰainnĖcs=~7̍] Zp1ىPgmf/^lV\i8;Lc"ނk !k !k !k !k !k !k !k !X=IENDB`symfit-0.5.4/docs/_static/ode_first_order_fit.png000066400000000000000000000562271412237106600221500ustar00rootroot00000000000000PNG  IHDR &^sBIT|d pHYsaa?i IDATxi|kd–HMD buѢV֪)6)=O[=ʦ&(e„LBB&3$sy?i/5Nsw/&b@P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ @P@ $n[Gֆ s͎;tuשO>kg0!( rt}/d54i  o߾UWW@x4_| &߻nҥJHHЃ>]jԩJJJCofu_u|ڱ~i˖-VDnYw~ڱL} 8쀴zlӎl6n҂v\.in J H Uyyi˕[:6[<̩~_7o/~fɤZ#bTXJMM`n`fancfancfan;93rRSSe5b?믿^W}}Fz}xs 3 s 3 s 3 sC(q Y*,,Բe$Izꩧi&]s5ھ}̙5 7 Pii>pPF"; B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( B d( a1:j( a}[D HڹBK5: ( awdyS.8@Gv*1VA C]|y,FGZ8Sft űn,>08( a %IU~ _4@ˢ#$jjTyiE 3iR\v$jj-fLf҇\*I+)w- H4Ukh90}$I?X+cp"eP@T$oujn56pxaJym8]ig&IR Y@a$Lf$J uUZn Q@XS9Gv:뀓ӱ( a̚Ù$I]vt Q@˜f %I PORu vh@P( aNH$S /B"$%%$I+h|tEiC$Iy2:"$ĵRb$}dt/ 2Q@"DZᥒ$ө۷$B$#Kj$jjDժOF?5TT$~S@k Al99J,)IZF~D@`( &m7WSg $$'KJѫp1:" $˜V,Id<'N>D>֬24  Hm6^^z桀D˯$yTii桀D^+.;[T CP&Y].Ir~ep"i:P&MT;F`$^|$9~DDaE$-y( ޡ-$Uz[ Q@@[VVffߍz5+K䖼c(`2>t$ݪ߿D4?h;ww}5|pW7xvipK^= H3̚5K;vмyT\\ٳgu[nմit]w7PAA&MVhINVEK?PVO P&j:u TTTnM/k7nܨ=zhرӯ~+kϞ=!z-y׮{4TG}=u_~ڶmk{={h|ZpRRRyJC7zC@sY222dkTYYYr\r:8u|4inFY,L&͙3G)))!˛~|<ٺY)҄:پni׭[gyF<^}U7N=***B7O_Y33%q1:; MgN;j%IӧOȑ#k?yb9^h5+s:ƝSrr^g5̂̂̂Ǭ r:|2㕚z#GhĈ^L&{:|p@<*X>['֬R<)vn!( M(((jՖ-[ԿIҦMԫW3kݧ/uǏZHk5J#Y7ŬԄ[`fancfancfan;93҄7NŚ1c4w\͜9SRnHjjvnfu]իW_}UGѸqzO'>R/\kvتUʼ~%kYp[Yp[YpBؚaʔ)ٳ&Nӧk**j|Faa-[&I~]T]}|x&7:NthBToܠOU{ܷYpdbs,8-p,8-p,8-p'gŒ8%/$V]z$t\.*7Oqw]bp: H*u%S48b$d R2$WwV1lm)O_IR#2G$Tfa( 1"wޖ18b$8i( 1$wŵm+I(Y&op" H 1޸ >tPn78b $Ƥ\|,iiK NXC182.BTT_}ip" H J2B,IIJr$fF58b$Fe fsr FP@bT\6Jp$[]mp" H ;`BۭʕyJy$;\'@ĸ+$jjt|@ĸs d$Ir\~Dfg2qe j6o48J?@֬,IRe'@@&EW$~]'@@6xIIwA@$l+%In Q@pJ"$IK28 XSSVx$>m( 8MƕWdJ~-]btD N!]vD&!Q"x] צ.)$_NnQ ZP@2fc-oԋ/$5; Nh@w5Z2%Wx.ɖԋI_c NHG5Z2wA CU$I׮QCE( hRWK&˗&ڶSʀ5䩬48"͒9z̩] vA$ ޾$U^)O fIAηfwSrʕsi( H%I~]wRRʕ[]mp"D fXIsEiI( X|J:$w䭩18"AfW_.$t^K+J䩪28"Ak3IwĪx P@NJ"IRUjp8 NpGYi3vdH^x8s[n$UP NpFYk3jl6WafMPWHN|UuwyU2'&J*op"# Z%)I#GIv۷>HtIұc걒$ׁxn( hQi*.'Wtlk{<'@8EVe$P5'@8%_0@|Iұ%s NpAA3ʺZIJ08"g/%t!IX55'@8UL&e]s$WWK Np@AI8$UB N`4 ZUk%IU,YltVeA$U]#wH6Wj|>/\`tV$I56]'Q( 6cʜ$IrI~D0!aIJR$}:Lڥ־$FAȘ,eOA䭪Rҷ NP 륤^Ko/Wap"\.~a 0@ܹssoQ{֘1cG0idȞcbW4ìYc͛7OŚ={JJJXWSSPnݴd >\w}*** Hl+on˻cn C( MՂ 4uTHv^x3.ZHIIIzG{G:uҧ~j@f4E&o߾O۶m;cG}aÆd2:` 2$$Y#%9Ymr[^X@iPFFVcYYYr\r:=x2224m4 ŋu7=-i}Um%˕yeee=N+vYp[Yp[Yp[٣4!77WNS>OfsP||RSSO[]vs&4('ݪ-%A^S Z3 s 3 s 3 sC(Q@PPP ժ-[M6W^gӧ6lpڱ={ꫯ=CwJPU UbrŬԄ؛Y`fancfancfan;93҄7NŚ1c4w\͜9SRnHjjv~ٳ5f-ZH x>y"9-Xbfancfancfan%Nbk)SgϞ8qOɓ'HTXXe˖Iڷo{NƌիW駟VNN#BmyI\ N`~CtN牘+Ѿir9,KZ<>S&j5+##)f f8f8f3CAh- $oU*.18Za%J<er9lp"$ NΏoj^0O%=( ;m1r$t778Z a)Qn{[{Dh %sM97"I?-t^/%_p$j{p( k9?Y ~ʞ>Q( k ҫVgp" VmG>!gR$ɱ5y* N`Q@۫ʬ]*$}/7:DAXruymOIR:3,P@1>|$IG_|^ P%͢ykƫ순˗ g5zPggl& ?Cn=$Io)ѣ@ FKbUuԸ+"I֛'jߣKϫӯ72&žf9U>$޾2_)It7~lT4fղf5>ȋ/S[gp"4l+熛%IJO9( Xɽ(o?I%KUn)Qs)S'h$oѾRjs*Ge !j 믿.ɤ={*!!8 psr>c) Fwr}iҥڳgQFiȐ!kxK/U9K)XQs ȤIh"-\PݺuhРA2e}|>#L&uN%_G>'_Cѱ-ԥKu]z7K/)77WӧO%\b}1όFlL$!Umݻ{UII~_jɒ%8qѱJ2U¹o~>k@͖-[TRR9 8P#G4:ZlVO~}n>?%~3ۼy/_~[C Нwީ"eff̖k/u*6c ߈Zbo]#FtĠ.WƏUs[rK6b[^xAVU Rvvmۦm۶}ZDLfrWj~"J6`I Nr*k5r2_믝 ú 6EM;w\l0r/[#㉙u)X^|Ś|M-ZHTIInt}(..Y'P@b%)Imoq|'t}dfS5EMٳg7!C,e)+)eit,5&m޼Y a.k܏d$I*_@ EM;vw>㣦F/ƌnIR@ƎW IDATʈC&U&!&y:2i\.cD) f:u 5}tkԩFBmo$-ǂ NIՒ%K4|رC&I~_źe206dNlߦ[j{J:|%ct,5; *..VaayL&7ʕ+eXtP>L&r3Y$IeN*SD7nt;SÇW~~ёa)jtZeN' ЂfO>ꫯpB=ڴiё+$I'oSw N]fdꫯkizծ];]yFCɺ:)*eNfDt_VSO=^z矗}ݧW_}U.n&lnxt[{XQ! IECՓO>5khʔ)2L6m bt#gp*cP@P[[ hԩ*((PQQn6 5ojkkC.cJx$-:hAiBii<{X~m۶o]t:?A=X""L&Nt$U,] ( Mp8Ȑj=u,++K.KN3gխ[PD0ɲfdJ]'>TEiB]]l6iNvݧ_nl٢+dYp}3_g~>c%nQ4NNHH8u^<򈊋e%b"enNʛKOIlYY!i3 -p,8-p,8-pC6oެ[nE۷o~zqںuu6lĉO+%uuu?~Caαz>?K:v\dS.v@PPP ժ-[M6W^ݻVXqkРAuzMksY,f&DܬS΄uUw?6C~@s_kԙ< H4n8kƌ*++ܹs5sLIn+//Ufff@!H[W(WwUk>MC&shw#qf္!8L={jĉ>}&O"IRaa-[fpBD*ɤnRRt[x뀓ӱ@Dad2.mOIRs>C˜fQLm5n?1F j/~IRá3Ď N"@LfrA97OftTUFG҇S_x^OG/S~h BP@$yʛ2MqYْ$}'_}@$}{Mn?$غE~* ; XSR*墋%I@89.Nmo6cKU:0 Ud \Q@ɤ6cƪݤ;eZwu|^0C"R.>,kF$rqpBbvUoн$nWO/Vݗ{Nqr VY X|+r(YPu7j)_8%I=24zPg%1@,a@3Y,ʾtL6*9O)&y=ZnQ@Q@ /RdΑ$SSE^Wb}$i'cc( Zcfev$9[_QncCP@9Fܤm/)Sե1@,K7x%.]}'孩1: Hj°nb:INlݢ}=ݟ@Hm%wh)Uڐ$OEzB\,gl@Ъ( aە;jw/dNH~[q.wY@K+I=SJy$ɵo=;_d%P@kZ:>e]3AXwȳO[{x,P@%٬̑W)7˚%I^:٧k ]QGSK$IS= NE,j?W9uz4~t #ouz?IRá5SW68h bMIU;R۟.sbrX{2:h@1LJx:=%IrZgLWx N @ĊP_ާ?/|xsϘ.ֹ^^Fa2>d zgU.ӾJ)RRՒy=( qxo=$W%o)yH>sSK5:*1 jfe N>s(IJ.҅ߓ^89 Q@D[NrOe$SS6EV_pxNZ=r MQkzN{m8HλZtIR۴r\~/cJ \.}QXBv]z~}UVO߯<{6lXx& 5co.n,n*w߹q 4ìYc͛7OŚ={JJJXWZZ{G]wx ]Q' Q@P[[ hԩ*((PQQn6 g]d.b|M7ݤjٲe$];)i0Uf*{ӲhEՄRy<Ա~驧:cx8~ĉV p&YEÕr9^}E}(_ }UYoVbF á YjYYYr\r:=sԣǿ~aٽ{֯_.(dyƚv?]yyX|I?75TTBiB]]l6iNvu{ԯ_?jFg/ݕ?X97O9)IT#6EK`pB`5nQ4NNHH֯)//?u'|2X腁89/|컘UTʱ59WRz|2.Zp[Yp[3y$ڼynm߾]fsuwh֭g/++ĉe65o̚'iEܫ*/U%ݪ\FUW#qU|)hZp[YpBS^*))QjjnVM8Qtj̙7nF{tz'~N >`̂cAﯕwݒ6c*mP]8f3C( a8fۉXj?vU֏Sr2L& r`ާ~PN2lmpBBbߩoOBf>CyR N@xde8K&Y/Vr|]U|:eNl٬ .T1k/bt\ Eb)sH]ReorݪٸA7h_vw \W\_|b`@ $'+Y:ByM$uv|K'܎Fh%ބ7/E%,j6:EeX1)VVgOҦshgM:l>֬V+mȥ9Zq퓏I(H- q]sM/""S={ً'E~-aBtIǮ qdQ|@DD[jۤ.?OWēqE- LYKDDʤ""2X/oݭUs򙧰ef2&g`v:=\ ""2DY\.Royhݴ{@W}/HHqߘ-5LDDD!l<{.I7΢m'~`Q"ּ5F=&o6),۩xzID aXNFSWrl/11Ypk=d>@DD0& -=a"~Oo!ml,xYDD$FDD fK17k]xm$i~Zy $͞sxWDDDDDJ[(Ma1rJ98#5p#^a}:$͚K1\@DDp p&3Q4.'m@U*^ I7 i\u@DDpn݆wt>٣ɸӺy>ΎY]_5ksd'aMNED2Sd20j$E;Ҿ}+m۷:^ #?x_c F,-Þ`!IDATወe"""1|8wvNmVڷm%Xs J*i|eÆ0OY9}3 uG_nǡERgg6Fh۾#N:Yw$za m'9P07p;'MDJOk;{V, }Iwйo/F8Lw[+Ӻq=SGS:}Ǣq܂R)$ϚMD:c秴Isw2Q| 霬IKMx dє,LDDD,IHtFw75 o:7@JG#)-A˅{8&0~v@DD`XH[L`Oof|dbti߾p_B„\c0YgOD$I,""W8Pc2!L4 +3:w`M |ヘ]Xx,d2HDDN ""rEq;=6%xJJ1 ::v]1:vcN,II;wXlilm+"r@DD_0Lسgg2o>`tCkjni-}[vvH8܅EXbzQm+"'| %[[ܿν{ܷpS]Nr-~&#p][B~_gg_}0Ep ""rUzxZ tsd0? םJ}piH@MtJc\jL&왙33I5#!Xs tcI@DDD5CWSM1)N@@QG -r7/)a2a<đ55MgKD$.@DDD{]CGgFuˈD誯#XSCXv/hà]uh߶5&GpG><G,II罶D[Pǽlƞ= {0'_kno $5|=`]z9!G998`6aX@՛r&rE\}R| Y<cq0:B'=N B'ND6 "}.`vAgpui 9qRp&j` """W Պ#'GNLGAB'O=ADO0=Ntv:L09@NO{B+)p -+{V6=@"(\μN]>cz.Bu誫#Twuag D3zJ3-uKJ""""r܂nkMNƚ Et]uzBu1XHG#Uޡ [jtl`=f :m%,2@DDD\?M V {?gD"}>5U 'gFO"_G.bMK4liiұbMMlk+a8oĕl i 0 HGGOh׆>gOp8vv KC*.J t=t{`MN4/HDDDD,Ʉy# [g+-5jj"|^ [[I>K KRkJ פcqǥ_r5S+l;IO%%%p8{0 "t55>}F !\v\ض=Dws3pu(=.W4$'cINݷ&Eo$ޤ^AESD@DDDkd2aIH^qXC+P'cSLO ; +>_4ᖖ^xD~B~?MšDm3los;SX5K """rY8= z&d&\LFG|tDۻ|o'Uw @ջD~%xxbIF'y51Éd:C;|@DDDsd6cM`T"]]tnipkK45sgW'P'# #vFj}V;~Ykb"O"DO'՜l> vx3$L`2(.Wlal|308CZ1Cwkk4|mikF8"v^L&n7O"n++^6O:B0"w-9/ """"Sv9c3{Eank-v zs=l-#xϙ:h%pl%\r) = n,n7de0 ")]=Pk` esҐ4쒏M|) yd2aq\22gcT0 3E):!BDDDD*sf*͘L&G`9V׋|@DDDD2guG ;.(r9=+A0f̘1+Ww޽)u]ٳgG*"""rv N?Zy޽{y`ŊYON,YɓYjeee_%~?z@ɫ#?2==`0733W[jj*uuu2VNd~ޫP(ԫ=\Gq7 t:/gZ,ʅLT]©fGupQ.ju@#`6Gp 8N^oL]8nN58ۅS.&InX^ ɷo΄ 8q";v=6 ;vPZZ:(c@rXh|g]+W@lșiW7|3mmmȔ)S9s&?8PPS]]OYYsWU9,Yz(Xu;^Ts B'?aʔ)L>˗ǞSZjUYQQŀjv.GsǞS.{ /PQQ+XfM5ACa2kw<ɓ'<3 |`0K/y'TD",Y4|M***/~ի^;Oo6ׯ=Vw!Ν˦Mb{L5?͛yx'xWx}͙}G5*vQeլHMM7G'dڵT]GGQRRb|'zwQ =n~Faaa^7o6JKK xb5!СCFaak[zq7[lQ΢;#zg̜9Ӹ뮻Ks~`,_Ojֿfcܸq֭[cm<n駟6ϟoB!B2O~]"O8,V^^ή]8g֭L6-\gܹt:cm&MO?!9W"555fmmmܹqƩn_qٺu+SL{Yoߎ뮋-Y{Lu;gJv˖-^'á `ܸq3Y`AM vZN>Ͳe˰7:~_0uTh #p:x8ʐECCC>Ӌf?O-[M7nijjbڵƌCWWY?y֮]KYYeee^wL8#??`0Hzzj֏ G,|q)} `Æ LkײrrnSLaذaSO=Œ%K(//!vU())aܸq<>|uoo_jvÇ'77\F&!!\խ\.y9ºuXlwUsg,]a~_r=nd̘1T͙3Ïc=|3<ýޫ]8n,_4JKK3g?|4}: awc„ … ͛7qtC3(//g{og̜9 ZZZz'_vtүDD$>@DD䬒l8RRRڧ={0}X[8?1̜9"ƌŋctuuqѳf8 8v0EDdi \M61qDnwQF\.rrrbP3fL]]]_ɘED$tDDD.ںu5kVvd׵l}ڴi׃\0ظqc"""Q~%$$pqmذg?gչ %""C."""""Fg@DDDDDd(ȠQA""""""FDDDDDD 4 """""2h@DDDDDd(ȠQA""""""FdIENDB`symfit-0.5.4/docs/_static/ode_kinetics_fit.png000066400000000000000000000651071412237106600214340ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 3.0.0, http://matplotlib.org/ IDATxwxBM!$j!AE4 *E~Q8c= J󨠢""*HZHҳ@ٝd\^Lfwo#73}_>Yf9 иqc}7$1\ڴiû[SRRݻ7]ve͚5K&664BCC]WDDD*GNN1114^[3gN}?۷׷P@jƛo G%))$%))T8{7hРʮ]>|86mbԩL2#F_DDD]< ʕ+֭[/<A8ΝSO={Gyw[ݞ]DDDĝʔ)SNyNXXTEDD6l3f 77HIxI~9+.hBɇ~{G-h֬_|{o%P(vZs)<:οZqu0`L,"""U”)Sׯ_=wu˗/g&'<:(""A}m$δ>_YYY̚5b&LPn3uT^}ʈ(ǨxrAa2}t4hp~ ,`ܸq3ha0MLmvʜ 6dȑ|x&<PDDDLj*֮]˭zBBBѣGs@1UBBN?={xS?`֯__ :`:("""n7}tF͈#rI6o("""n}^ '<i[kSB+krqfod3뽏;^v^dC}Y *|piyEsvw/6/(l8Q~ŐwDԶ>^ʗ+w3(+R;}b٬Pdb5ATzkQ d[ߧP#Bߋo 9 Q5X zGƯINϡ؟9pgQaԧ s+)a/gXP۱rf/Ah}c_11A{0؝ߘΤ[n6Tu>TjƜ} Ag; `%UÏPV.SS c/.GJ\1O*xR掗8 ,>'>evG_0GiFTT{f̘1]ʥX ')rc~Qs*REDv;G{d5r3!C|*bVgϮFYC\KyCyŧNlR}DDjp8GH7.VرK63.|N*u'<7?~<'OfҥnݺB;v,cǎ%/Ϥ= `%bkj`d/1Ff=c l83 Ԩk\ ?F8Xa.Ss>}:VyC;^z. T+AG5EDÐ {7dc -$BNal5UDA=}^ϫ]6kk X "*,ʻ{Dj`)0eGwh}8(}ٻ0a(6FFehq1I6bشS>~1XK1 ߋޡ]PpTb)GŽ?h㒬JXIzG1a@;^}9#5ً2cégF=j5+z5h;93J3>"0y9Kw事#߿TӸV8n;pڙ q ޱW]3OQ|}}#004UQGR `%Y-P9DCx7OStbXz/W恮\NIT(zFskd9=@MldkQΦMLM#]QQ+w8ck56u)D]!$­qEܩPh/=#6i/3WV0;^}ϩWXE䂩m 6i/&[tEDِ'-žϭR$@t{n@+)T]s `ov?lH ̎$"${.ec%|~[NmJ\q@Y-!7oaڲT@qØ_/{jAl'`V`ǁ7npe1`+vbSz-B͎$"4Kv.!`c,6cpFlI4DDCqqsg0}.^ّD:r:v.Eףd;^ۃ9yERt2w}߬3Z_Tر(}9|{"rVj!nҹI׭ZID"H[ϭ@+QWhpjI97*nbXc,|S1V+vm_`+-h|%tF%4yٔ$4"^GөKըM6J_nnJL\㉋# -Ztz-7oN`` 111ӗrH{>5iFsy L6TD%< 3x'?~<_~9|z"99S>}:< SNslٲ{7|e9tջn}:/ЊZ5tGr:96`K[A #uЬq?FꊈYNU};v]vL0t_˖-馛7n)?lڴ {YbYCXXلVln? ]ƽ9<߻%\ѸBj0WK6Ь4ek0"|~{8XTTĪUxgѣK,)9]taڴiX:cΝ]b0SCz=ӗ.qX "Rm9Ɗ¦p$lٽ=3}aZ HD(nQvv222}Νwҥ N vJB K)g ۦ>c΃y];K`c;<3 _>@]*# q'Ot:O;ʯʘ1c?~<;vd۶m<DEE/q1jԨ Y߇[E]L[KP:a7F:0h~=o^N Xn]l6)g233O9+x܋/2d[&77|k9< >bbb+N x.~ޔIFvaUq:aX%lk FktVj#  ̟?ot}N^^)%ft:9ݸ+o"BШ6+vf61M5{O4 aL̬3}"<׏2zhӉgܹ4lhZ /b^`Ϟ=ԫW>}0feI~dLzGEs Nb+i"Dsy};`\"".QxH[~bp .t/x@ ⊦m~>]s[Is8|?6_QcM{@W9EDL Щ!mϗ+޽" w7_1ޭK"T[S?,!j( 9>#>@cږag8""D Y-!oaڲT@s#yWL_"hs'4-HUXE,leծC$͡U}$RaNc;-:}DDD H_?Rm̎$R5K߃3^h CVCsT*UȐ.q|.oEc4oc@y|'"RͨV!mckаvexGs#^?ކ>ZGe!"rTıj!-W]D/T kw|H|ؘEDDΛ `ӣU jP>_MIc*Cƾ:AcDo:*Uʽ8.j%.p9{a[#()0j6w_DD< `tG5 ۖtknv$ ߄U[\V!" *UPH/wvaҢ&/ޡ('{QV"c_l"\9_"".XE ܈m51xr_gBODM4M~ՠV,N19 ? \ N2_aw.4RODčT!].fCfNiDCa$k/]`wFY?SVaԤ}Z۝|tqD*(~{ ޾oF]! ED `7kӖ"nrʩ@cTDD ⺷$v iY|z7:ibhN4ی}55o)"RUjˍSp8MN$r]K`JwbQ@Wѕ6?*HgnoCH;psqD 7çw‡`W< ED4T` EFLV a`A½М""U `51s#lV KwdÞl7r8`to,}%Ь'< !f' R& w(Pĭ=ۇ!7\g3^3Ӊ9RFb v/41CLڸ/ÖBfX\Sբ;͎#^އw'\I TM_J^Qi#> ?lL(ӉH%Pf vrn'? d 7  N'""HY-nԮ dRdCfƙfTgXj=z(G,Y̞=Dy"""gرQXXHNNNGuv}(ZErnvmFS*# DDDAFFر3gbۙ;w./:cƌ97GLLLsjkGKv]`rj*y6Bh} AN%""rZQf9+(~~~$$$02ϟOΝ}_ζmp8N,eKp:adɓ'3uT6mSO=Ejj*C`РA<쳥6Oe{Ǝ#xdj)Y۸8UۊI0q ""Rx~qAFMzz:̝; o111OSc4#[XBJBՂW61;Rղ X04\ʟH5iC׭T ql@ȻlcBUN'TDDZS2ni% 8ZX?m6;NX0~=5/U#UDDZS2V 3L㽅[Y v)`,~ؾn,tnnJ1@8ZHb3& >er:7Z:,~yDDD*J?nHgش;.`ش!ݤdn|q*""ATdԜdʻ{|ߨ9ɞ98S+GB#""RTԊ,ҳ Ns'],rocwzzNjTC*R*Wlfo,v1C}EDJJ=Zbfʟx,@)!6Qa C\mer=~PM'URR6s I'%ô[4 w|>~fq)SLMMq.N"'ń5'̀5(?nȠWkv&?7T""".gq:nf;qy[vb`xbv)Yd) <$qy{VY蚁g=!;"aK""R.O(S>- 4{O>T"lV M7|j7{<׻I.PavQj7ߨW1ݻw3l0f̘_'|mڴ)%χt1S7D硤7AH N%""VHFɦM9s&cǎtԉI&p8"b[Dлu$vY: tœ' w 1fq;Gw҅)SuV:t(6;K}.&߇L_Q=UoOb?KN$""b %K2d͚5ѣ{ԬYXr _ceOqCk#""b"S `zz:*-Zo&44%Kb jz/؈Kq}8g7)hyDDDLf40~~~ԯ_ӷo_|}}=K%:oFaO6}] ׁ+3;ҩ270o[&!"ռL*?w|chadBl ~z |mStȑ |-dAl" }~4`JJo+.0G3~ؐNjVOXڴ2+DRVw!G;W"S?cL98qDKddߺRo7d0tڪrv߄W%0?lv{[DD<}~4>QFtؑcDzqF3bH%e8>Q:Qsc@~i?s?R.\Hzz:=IIItܙ&M0|p~WM]%wҳ X0ß 2 b.s{T3 U /`{0p@իǠA9s&fE <./p/ ucU_׾H5U%gϞ?4͛GF׿oav<9 q%+h;:=Lr.O;Otp_.?L 2,#v͔0E0;dnp\*rLjlV /i{rMs:GW#}'""rUJ3> V2o͵S,y6~Vc L-g|[E"%߶do;+S35oec׫05#""atP*j!IՒ:3ג[XRotW니x0={9?{ ׷-HgMupQ.| CtXLZoXDDr(`N:Z,nJƍ]i-~'-:pEzN'̼׸F8<so.gddp8* 2#TM28!#ZGv~`Axm;#{Q ҳ wBаs6U~"L+n,n`)N'LܞkZF dī?9/6pffYYhoT!]x-a x]ODDR9sf?kqs*q{4I?RK7V?CP]u"""^Aq]wUѢEnݚypB3 xKY-^dc ,ڵ!EDD)pĈZm۶ |'ӧIII\~fD4&îl 6ph> 5;?MuCBg N/ bܹtլ8L7}]_G{aoؽ}?ל""QmRpż|___^~eRSS͊$nc;c0o>M{A ?n'""RL)IIIk׎?y駟aiݺ5'O6#x >wo}߁ZPDDăR;vHbb"֭[nԯ_s3bzmF4qaW5MLMr Jx5p4~pB=pf8Yf1qDOِ!CXndTy6+-!>N=[oLoe!"""ΔثW3<66))Nnm }_@m*V|@8up8*|ƍ)))qa"1 g9}nc ɩDDD< `۶m9x`OLL`O`oCKTt/T㋊aX~N4B2Ѓvv0؄"""R^ 6o\ Խ`kT4>ٯyWvN(""q^WwTUk_mn]:JX&L"^^BI4:=ba-C<:L\PDD#9 {V}'(߻cÆ >]A@"""IPoFX8o>Kcj='2rܙPDDģ{ًᛡ(f]=tH\ռ%t yERDD2{X`PnoCx?2n *""T}_3B"R,bn3!EDD< GI!|3 vhuZv.O^ fm )Rx@qca&Q㜟ѽUEvC?YE*zi’wނu%V oц&jS#WST"""AP\(ϘKAB|8=!>|\AEDD / $ zz/פ^0o⋕"""r<?8HHH`ѢEzbᦛnrqB/s1,`l1K%UO]{lP7"""r<Θ1'|5kеkWzEjꙗ۵k#Fk׮nJ% ¬'MW˟<(dJ}}O17gȐ!lْz&LpvϨQhܸz/]=TkPXTTĪUѣG=z`ɒ%}ѣWާ2)G"X9ؾ=u}PȊY"""RAQ8n'"ddd?)S0iҤ ϸq +}\PnTR=el' tiPȹxueN)9€4iu֭?dgg>T6Npp+k_v[2kk5(DDD|P֭f;l_ff)goΝ;ӧO>øLJ͛7ӤIS% 1{n{Ǯ {>YG/'<4m/""Rx@????~ϧsΧߢE ֯_ORRRo߾t֍$]=Nq^M>~+ɃBOr KܚADD3ÇgoߞD&NHjj*C`РADGG3n8/5U' Z%>pPΥwW e=q%lؓ㟭Ⴡ <9"""c>[oѣKߙ;w. 6 55tSzC09cPۼ)u֩LFItGDD*8xrrr #;;PLuR-yV6D!aWt[;H"H!t3""b.}~{%`1I2yJ?Q<߻%|1s7y֥]PzLTX/iE(\؋Ov41?~è9BDD C;Wc qNgծCa`'],1 ;%Ш+Dyq"""@P],6l~pÛWQ![ ljx@973vPy΢C\m8]E`W5/a _^P ty4geZxO+rKxO+(""^EP*nX1ؾMM{G1a@;"ϛS5EDĻh"hإ_'_ivs3>"KWς28gZG ?DBwP5- vi΋j!I!V5"Ą"""Krv0%c'!,v'Cbَfq9@9%Bv6ΏR٬^ ׶ fq)@9tX}|mV޽!++#X""".(g`4Ai\&ƤAiL^Aʁ\c ޞUSc׿-B9ppٱDDD* 5 q _>`|L^#fT*R _Arcڗkivϴ;]3r8e9̎%""RiTTSZ<&_3C:R/ğ2pdgPS-/i_E5>#uHNᮉt9XDD< ilw1Y>06;/X"""DPy1KLG\\%R?,sRjMPNؽ }nlӾFuk0D d=]Yqc,';X""" w nmN㱮$@H , ^ݔRdwX'], ̀A@MiGM+tl摂B IDAT$""RA*3<a2.UC\DDD [dBp$tx4rq ;}`W}DDz\5c_y6r;f="""NГ-r3V#h74ۻ$M ]NiZ(7g: 8JpqA(,R(PԖҖ6]69ҽI{ۛ+''7ܴߜ{9uj :A碑 *""3Nm6=X *uDY|C`型8J`tw<3+ $"YJZH?,ɟW8P# u|)NwᴛX>#HDD)`9(fP1o\4*X_hhJ=؇#ilPR`4!M An֗cʪXx5@1U[xPzxbsCp K_%BЈb(7' !u4dGNX1ޚJgく!%@Јa('f) <10 PoWdO+<%LDDPNN}_\RGC-(*X0c{$Ncq]_&Kyؔ`23Y$"rdmj!tswe=!:pK K'Ec|@kmYx*-9 iH mDD֦K=rqn%s䚿DP`plyau(,k)(,,]sZم嘻(fI9IɁ-w? t6ju]ܱX&^΃҄ں<ё\kP. z P:ZĂ1=ݜqEVa9K&F*,+X0l߳l+m<$0/l}a߶\}rj8BDD_ :j#ܜnmha#;S2 &Y/= 2OhQ)XP$!2'9 &Y>S0ic6ilT 'HQd\j}ꬻzj 6 ^^^¨QpVFY'P66*7OŠӃ1j^)(,{0ɋlcXh;aÆaܸqHOO={0m4޽@hh(ƌV= *m,橔 u~{ apd95a"DD@6K 8 @||W^8q"-[M&c̙zMɖqd M{kIObN1`T/ubo\$ep)8F$''c̘1njF{D`^vA&dpolya( j;\bK0VZ,\L&W+Gvvv/#88Fβeˠ鬷n}p;$KN*8;0 B;P[b슽{"PTBQ6.lܸZm+(,,222l~Y +8SL>Zw{`hkpF f~qOuyRGDDv"*Fo_NNN^;Xt)v؁>}[nX²l69 Rɱ!xjhTJN_ǨMć;Ρh:D"",@F$$$T+OHH|{gضmbcc[:L( x=:9O=maHW+hE&"j眤^,X3f 66qqqXj1g̙3l^ڵkn=twwd Xi8PF7yj f-gYPgE\g,y7zxX#~eQ#pʔ)o,DEEa֭ sϕ+Wh4Gŋcɒ%z\_u@'B@K4\8?FVay:-?Q\-s] D^?3Q5CLpL;ƄX@&67 L$$r0nCXw87K+J0&dž^~Px #c&v;Y nXy [NdajskȀN~Ԃ1dh@J`y4P+jdID| GFZ)w`|t ܜ$Z@&6tv qXpproDdUa2c|{$ݰ.'Qa|@LA>Pr1Wj|n?Q S;cz຾^÷d -o\C „~A Bd=6De[˽p24*!,#HĖYЗ6twwL U(Ȟ&6@;^>hI &>{R2+ „~/7Milu1dhJAO,9e$f),lPI@\<1qҍLdMD`&6@'6> xN*^IVecLpW[˕ `Pgalz[j9;VO$HBLĦ{2]-OYd6NfZ 0o'^BmN @#y:H"LڤS@`@^<x'=/ŶllM±&=!O EFDai`Q5KLڱoW<=3a[j6˹% >7:DD- b6Cpbe& f[U\vnz&KyؔZ5{[c1GhFN@nr%|x 1a^ޭ#w`]V!c"A!tp$0-`s-$QjM RmD1x1[G ;S[9˜x @48 |>pZun H,(Þs9{.PYm=<0{G ^PήjG5@&6i7@ǁI- I)+TH(7 ,j%y gDck[Oi>zk7 r[4Z]޸+/#_̵&9EF%GQØc8o7 A 9X{(HJk%*Mf870&?Af 2r Z/H" Y{ 4* Bl7 Bp4#0@4 $@tpvo HVa;y5 Dy"6ܒƄysc;&<:|a;dlT d8GKMrV +PoW ~!ЧK65$x#x:HhF}(> 07 ݺA,5:̂[ a>ĹEh_0&9` {[,_p ?"0. e8q)8QCWPb05z D{m7!!@)`]o !b.PzCT:Db=-+%S9Q:": Nw!.Zӹ^^Zդ88 u0lI)k-=.^BDTREbP㪾{{\vNf"5SB.BaY\Áy٥zzg%1 DGZ{ ]Xkr2 B4nOl&H"&hNҌ׋p:K3֛%)==]:a':*x M=No x/M; BD${\'@ܚ lWrK`G!=0)SuT&DԮ4uqm u.Թ`dOkyфK7qz_/ƅE8SFw(,3sGwى_ `K(\Hl{BX:vKr2 [Nf }p{WwpiZe &-z|I Q7{G!;;aT/?\-%(6Tj^)nTT51 qEB]˥ɣP&yahoB)_[=.m,DDDcF!Xk%".(%H/ŕ\+ռR*H+EZ^)3B\Nޖ/C z5B=Z8^D  8i?wBDDVe3-Pn%iy%H˵lg䗢X'.ii(,F(p@zmy 3 1wI Q;Ԓ`B,@z%̸y> 7Ky nTsA՜Ѿ2I;P^2R *Mfdˑy e X}P*Yz=t:-=穅.I@tSLy }jI<;H 5JN^j- nTņJ\ח,p]ou3~Zړ?r=dVa9_ɷ<ؘ#"#\Cu0 %d#Ye)2຾9zu}9(0k_G= _w |5qs{o7M#r=bh/_K?[9H63ByCSJ~Zyh7M)*Ǯ39x?C_^Yz =Nuw^x o75t.әZYqMx#t56v ρK8`6C#"";kS&wv;3>#nWlD^y%FrA,w{GE /W :U o7wg'(/I5!~;?""Yёv4ј7&FNܗJ$qĈ ,5le}$5P3vO5!-9tQS*#f1`[zꅉ'bٲe5O2z?lر7|Ө״"d4f$%%_Vvcu``\XXw~>"2kY$0LVZݤl25C0dCH9 މ!ZW^ ? ,, uz ##ánl#(,,Dhh(E2H}}}RjФ gg:Ρ>8U<==nv;۱8j<{,Zhj ,yPYY/ :w7xfZGw ju Z-V^-N>-͛'իWͮn*-Z$6l o6l'OSLBK~_T"Ə/BCCEqqΜ9sDppHHHG{۷0rl޼YlٲE;wN;wNBVT!<|ÇpѧO1o3hsQQ֭HHHs5k/^,[crl/\P :͛7OtEfB1~x1{je<>}qpF$''c̘1nj$j}W\Avvv߃3Y `xrr2***;((QQQidºuPRR8h>cԨQ . ((:u*._ @ޟ͛7#66=п^^h4b͚5={6 CǏp>9 rssa2_EZիRdwB,XCETTK5 ՕIšٶ֭[d9rcr} ݻwob8uꔬ?ۗ/_F||<,XW_} /ggg̜9Sm?O>$pBgϞPT0Lx뭷0m4>LIPTYQsĉZZѣRRRPPP 6`֬YHLLڜyaǎj~^{oqшC.]  ϶lFll,.] ߿?N:x̜9ZOmcܸq ڼ~zYk׮E޽#((fͲ֓]n"___Tߌrrrj|r=<ؼy3vލN:Y`4qjnF]"66˖-C߾}b Y999999>NNNmoh\pA֟@DFFV+իիWsN? /2Nh̘1/"-[@wC6FALL '$$`E"""P`4خB&40`u9ٽ{P6k,!eŋE@@pvvÇ'O6h^/)++=...mg϶;vw5Bm˝ ^5יZAAAGN>.vQDEE gggѳgOjժj˵۷oĹsj<&c\!z7o ZVtY,ZH kߍBIH0$"""r0L @""""0$"""r0L @""""#FIʈ#Я_?,_N9s4VѬI=D󑔔zfGD@"'|Xb  ꬿeسg oߎ#GDNN~' 6mJKKpxx8.]ٳgXjUK6ɘlXqqqx駑,Y͘0aB%K?FRR2220yd,_k׮Ŗ-[o 66ǎ3ݻw}XΝ;W+ɩw߷CP &&Dj; jںP(\Uf6wsCDԚh4Lz!?n&D$+8tҐ[k[rr2JJJ0|p "$"@"?PTDǎ^ΦM0~x899I!9>}/ &O,u(DD` 9шw7nԡI=DDDD=DDDD aHDDD`9&DDDD aHDDD`9&DDDD\ܑ݆IENDB`symfit-0.5.4/docs/_static/ode_model_fit.png000066400000000000000000000710751412237106600207240ustar00rootroot00000000000000PNG  IHDR &^sBIT|d pHYsaa?i IDATxy|,%d_7Y$TQDqQjZ{Tl{lӖVzXO([.ZP:g\ Ġ5d^#\>u\0 @X]A7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qC7qCtR͘1Cg֪U>myy-ZiӦ;Ԯ]/@:XBz|rІ kkkŋ5c +*))C=$'Iھ}o+"á$bڵZl5|=Zzu֭֒%K4zh-[LiiiZ~$7nV} _ШQ@555jhh={( $vT;vv>}zc*++$mڴI_}챡CꭷRVVIDRMMecz-((r,''GUUUjiiQccB~͞=[?>"pt9y? v96 U//_Z`P_d NgyvwkuݲlE[oɓOB۷o7DRaa^"HXMM\.<O]*???6kѱDz'OW-` W\\,ݮ-[hN:U+W7 C[n?,ͦK/T{э7(IՐ!CΫŢ&ȹl6<7}l|_ǸpEY-\P˗/דO>*ZJO=hԂ 3']wݥ^zI~?8կ5vX'?QqqLrB0}l.|_ї1ã>I&{?GyDϗ$͙3G$)==]>l٢;C;ws='%IZ`.]+V;aկ~%E}ʿZn*;;>6 k>\csѿc\8F@ @@ @@ @@ @@Я͙3CswHQsЫ^sʶijT'~˟r77hȐZO]/ZE%I%n5"~(===vז-_}P;vlɓ\.rrrQ&DqoT^^~Q$=okt5Tׅ[+fb+W`/V Ej-ySvBNwK_[4w5jm:uZob z]C3g~N4mZ3~ # HDtsc"~M}[ߕ2q L. Lt#ڷBwUZzY|RۺX!77(33Sw5jtkȑk솕h 7믿Qv{> ީ-[66S0`^S/;jp6kllTssy$~D?jfKeXL g,kˇ=j={3#  n  n ]Я͙3C4kzwҜ9WGQn[/_Z򗒤YL{Ɍ~'~˺ܟOfݭǯ4q⥒$׫W^yY_~IٽY*z)XH$bH@f/А!C5dP]zd-]HF@4r3O/>k+;ݙj:G"1 YFA! mwkTD#$_P$s\ H)JYɟr~%.K&LOLEEX$bQNKUmk$0P/&|#u%0466~֚\> IsSX~%W5k^(brU8`!0 ~0t=ܧ{ϜbpN tnlJp5ɇk  H*]:# @b*,D gJ;N~[Sk $CtR͘1Cg֪U>myy-ZiӦ;Ԯ]<~ei„ b|>zc5`{8$.iŊ*//?'N{ ti֦ŋ[oՏcz衇ovJ---z7vc;6(7-vѯyi)G@ڴvZ-[LŚ?x^[uvkɒ%=z-[4_^tkС͍}o)Iݮi`t >ٳGPH%%%cڱcG۷oӻ+--UYY$i9r@'HMMe@ ۭmAAAc999{4{l-^X6=g)6egDRNXpt9y? v9ڶ]t!555֯~+\.w}jmm5BdE0_Itv ?xt*tktF5??c~i͝;WonfK\hξ-Ic mn{Kg6}l.|_ѷ/Haa^"F.K[.jkkcӲv{\CCUuuujx5lÊ棏E>p$v}j b|}ad%d଒>n,|˖-Yצ4WJx >6kH8Bmjm-䓯MчPμ}F2dZR^zJ EH"I>/_{WG<ϟ/I3gz)-\Pzg|r˚0a{9\?ffZZZtczOu_.j|dHfKt$ A!&okOp.n._.Kv\v6RSrlbnUvvV,aq4Em"Z8gnrTKcsѿ>žZ[;nvfD8)JMIUݭrO]]YmRau1.# HJEYnUֶ/#v[l=ghio/0a;DG8KH;#dS;e'piAtu.F0 5[ޢ`Z-j *h ٫@XsE-&RR{&6@Ab##`j 6GCGh .v-bA‘t{{=U鎮l._}I344 +;o j DCEc)0θhVcYHڕ(͞4+ÑtG2RҕHW#N0 0$bHmkP. ,_ F5h* bUFJZGp3"%]hHs"i @Oo[@S j41(oG  F >GZJ< e:ish!cpZ)5%S8 HzY96ad P^pQnl< ᐎ4Ե11\C3+RQYJAu*T=e"f ֘̑5J3G(˙ɂHtn*:tD{;ᦣn8Fjg\vW*$+ j('UkZtxE6 ߧMG:aMјQ=Fchx٬T @08/@ }UsE5o~m;Y`K6v]=#4.{e_\IЧ%CaVtʉ\WduvUy^k춎"Fzi|XQ#䰥$b΍HzZ4}"tm~ffKCik~v֖kWBnmrY* 4!&R. I/ 6DAߖ$U7ޭ[ޚ&Ñ 1굳\;k˵`v]dRq8M)(ġЦP"s:ƍ﫢ɒH"PFn*>E7n:%b1 CGkG.-Wenm7QNи1rآAnr@@F:ZimQzN$ѣt;TVCu~o6krDMΟaC7o7n:2r[kҸqh޼ʼn.a:tLe;TVS6MDCG^r\ ! 9G3c+qRFWp?caxK>US[`&NPI]WL8C^KNM`XkZ40%߯On֎r"._5J3 KUR0sp VECtDW&!^>8>8Q䅩YTӔIP&nIt)Hp$v*+CF챴T,,̢R `\钤&!1I6^zozj 6wylBX]1xOR==OV}Ecf%ċa:xXOk>VĈtxtts&J? CO|u2LO'ZNƎ[dѤ=s3^6-U0p@OHu(T]S@jX>P5މMj Ŏ.]>hrS@}j֛GS[2±EizQX*ݙ  Y +H׎u:VӢaGޡ#z蟵fWݬ&M5Cgk\v  Y(̐$aU{}*ra]_7@qŦYEv\$B8EۇO5@:t4vesiΐaW*˙ H^,2]Ju}nb+H׵î!䶻X! gaX4(CxuO@ߧWn<~f6G.Ztf̘ٳgkժUڶ\-Ҵitwj׮]gmki„ f u9rYa5hq9|sHܢ]}~>+VPyyy8qB4x`-XK6-^Xz~_C=7xCnMMMz'U^"z[ F򳘺ӗǃQնرt]?2bڵX|^{:G@(ohhS:NCk]5<㘂%iϞ= B*))+--Վ;ݾ}OXiib7oެ?P_טӏI@p$GV)leXuՐ+Q>TSSl#//O@@^Wٔ} IDAT]ڎ7srr~IR0c=f)kFHڊ?T[u􂩺e%2SI>OG`~m;ԤItWhӦM&VxQ  Y_:Um=0-{FeO`eB@$9nA ;nmn***fzl6fƙoϷG H[v5+7efy^OӴڡpm"!IǑޤ .%yFzqvcsѿo/DRaa^"自F.K[.jkk_]?$)HJJJ[n9z<;|xڄB咤ƀ.kfY|wUWhGV6U7;'ޤT?=a.|_eIŲ*++-0߲e&OܭԩSr}0uV=Ú7onc۶mw]PNNyS8wOcYϻV6amqiMxU;6>SQZZ[{~bF>6k>ƅ#(:j…Z||IUUUiժUzꩧ$EGC<N,XgyFO<.KryJIҰazTO8Q(/ 3GyW7< ЖmZjiTq5T~? ~O5}l.}:<裚4i^=zGbS̙^{Mg}V[lwܡ;w瞓u,ZFN;\լp_潩9آ>~A_YEجj֠0Y .TxIvzej$- ?'c緱ї&ܡ 9cQju!a?|_u1.Ss=Mަ+Ǝ]5r6&.$@ 琙Pǥ&V6!.[_h$e93uw";3@Ah9ٔRp$uІ#oPt笢sJMa' 8{jm/ɏ{j׋:xXfO՗K[HCNXC'4q_%mXw/$N]Y $ 8# 3dZ:XI9p${UEdE7V7V6-D"btjJց|&}J$e:@i oА<*I#O[OMpe/b8ONg$I5$ hc#ϝ'/A% U:GA*5&lv֖KFgԣYC\ @tkɟjXs~/tR4D,Vii@;43vxrNêпmou2J 聢TeH%45#8 ŦLnu,lI rzH_Y|AHHk>嫚YT@?Czh\45j'x=keP=U% |C.bQq(@Y]p%*Ñ@C.PgTOp5磪mzi$e;%2LA.P@Q]u{/ɐ4ô \,rIRH c}T=O+~+#IpU @q:g$)gHl)풤[q,roz2::S3Nw]%UdB.RTIf(oxmti;K^h'}Y6-UdC.¼yW(?M)Ւq^ռyW$.1"rٜzhWNtY @p8T>:?ƏWG3p8\]T0ܮ;[-L \HVDw\3S֬c 酗AY.+h IͣӔI $3F@^0m=GXIWNn'?$M͛F^@# 3͡\Mԩ*IR+GwAV ? k%S/ɕ$=ڠV{Bk `]6M_e>ahǁĎCJ_rFx%N /LW)I*IXe;MCLX-DzbѴQ ǽ@^[IR3Sw/JO!dl$) ko\mVY6Iҽw)5%55 Ee)NپڸSy^IҼas4>璸|@^dY5c7mj1F{UTV[Ge@[:TiE #FDwm{/*Ŗby.eFnh}t$Q5,cPvM#IpO au!tM;@o &Q\ Io IӰ K{,j3\`%yJG66gGU۴n$i9\ .]SDwhouު IsQ")hҥ1cfϞUV}jr-ZHӦMӝwީ]v z5{l[͘m^}?XVI6G>Y VXr=Z|~_hÆ ڵiŚ1c^y衇$\R֭ӿۿi͚5jllԒ%KvGL'GJO:t\Un$(,ႃ_I֦kjٲe*..hڮ[Nn[K,ѣl2i# K.e]1c֖-[G86M'IhO"1"zw2dish%7]kS={( $vT;vv>}zc*++$}%IuuuZff͚ebfvދ~M'PQIMS3_ >(;;[v=v,//O@@^[ۂ.rrrTUU~3]y*++c V2&Og.}]5?R t]@%}|r8. gmv .ڵkuWKK ?H[5sb$iKE֟[xhm\Ks7؜Ngyvwku\] >\RtqUW]7xC~yd%}.4MgƳLwN(Ѷ=ep_F@TZ0Y2{E"7G5}{>*j~jjjrxr66-ĉUXoáaÆG5y|G5}l.}$=裚4i^=zGbΙ3G$)==]>l٢;C;ws=[/Y~jѢEZ#?˭qò$I|Jq~)٩CMG$I7Tjq !nV$vUi wW^#IZvtGz|Ӫ)וRQKd& \csѿc\8F@81@Δֹ.>ӮY;m]6_$ oJ uYᴸ`6Gs $C}ӯɏTJnu~T_5@,tIҟ8bHH%I>H/kf"qdXtu(VlMj4Jnu=`@/ fM,-Fgۉ.GBzFI71Yt1z$jt#5%E~X,`W ]'%EƑw$E~LʝLC`dGcx$Iom9HPY%I׏0 @aF6W*Is$r"K0 HqJ~M5Yk?,-, 4 AlVN$IJ WM//cmV0Ldy i8" [F$bݵRTQq6n|?>@NE3\ @^9IIµC4R4nܫ7D` { b C Uȩdm?6S#:F@ z&IKdR;ߠkAdj In?OeIzBp"K0 H0$ICil|IRS[d"0 H j$]=t,.钤*$DS@x2#e$IE7_>RTצD`gmX4hc0'U3 g;FtCfuyj+GJ>T0M9R Y\yi 0 ` qT=_$iϝj٣$IuM~u;bљϧOv5 #?`$u'Y-GASVFS[cguҴy5(CRt2>x qVIHpe'Y,-IR]v:F57aDɕ$15M ^ @l:EdTZ8GϽKdHpD{Ќ,bDѩ2IҥyJOIi:#{Fx!&[_hhكWgm(9Sl2$>ы0Y4{&NLwGH*5h^  !|,I^8U)V 3 -Izy~l `m5=.IY4^+nӗ揕$5/<#&R]Hϰ~uٖd]E&@<@iO>I􂩲X,_?VvUሡy_!&^K#"I*-k[g $ +$[;_iH^}.,$}cۂf!&h jwޝ~əbӽ7Dm륷`{gLj9&ёvUi:SЛ :w*J-РBs׵ȓ$=~}e-VUxHJ {ՙ\)q~ 7@^n EƝ>>_%IЮCB@^f$)ϕiEb㕑"I͟k7`8 IҔIN:SfCu=Ezў}jDGMKkΔXwWoNWi)9"c@ W> %#kwK.-j{ n]~~,~BH4@@K.Ռ34{lZSۖkѢE6mNڵ+WԵ^ӧӁl 6QK{$ij~|_i,}Mz݃  VXr=Z|~_hÆ ڵiŚ1c^y衇/jժUzVCՃ>(ﷄ8ѱUծ 9Z˭W҄YjڄЉhXv-[b͟?_]֬YP(-``UwsFCU[Q#IkΟqWDuL`0?kO%+V<^}Iy}tUQjr &* 6?ݯP9<[?găM>n[@ ЭrL>*}Q=6,}ۛ}  7@HEeM/Y9+5|a^ӌEW_G^<By^E"YUMM\.<O]w2-kӦM׾9s?ixnқ}nӔk}1ݒ,P EaһJq!C_U| >6k>\/2bvjlٲE'OvԩZreaںu~aIREEkܹO 4=SǙfqj6IÚz[{u/֬Y?kϞK%I-*{mf.$Ͻe^&~ ѿ{wu}{=If&+% P*-HK=z+zBՋX^bQjr>V)nVqAV% !dɬc&#!a$x;]~=j[7jGQxռyd}ݪʕ+l2IѐlYV͝;W˗/ҥKu饗ꩧչ+I;4dzjll]?`Hԗ5^qcL!l藿;ux*$GZ-Ӗ}~}Yס߯ުORq=j[7qlQ_gLbXxƏ+Rwun&͙3G4sL+L=Cڰa.b}zGdt:yf}73gFKYwIpoz5kd IDAT?X~pO,IdwdF{ _NsZĈbÑg5gK~yʷkC o٢{L_:'}]_Dc5-{]5c86$L$KY7\4A%o H8Pt1 ͱKK&)'#|_n,CSNb%[S zo/-p#ǩk$ჳtRY jom{NR8} %JLpoNNT$/Ooq?pr!! jWnIw&_g$өڤFB[8|پOޠO~u+U玕$vuϓ }fYLpo٥C B\~FU;;+p ǡka[l M;t~xX$;u_7i_=!8p:ݶW4$~uCtyd !{j-0@c}$鴓8HYu yriJtFѮ/$>%yb6nG;-0@@cyk8 ,bMpocBʚ@_DomNtDAiwKxǨA#ۙ8?"Wk22 W?ՋBa$k`!ǠVޠW4jЩ MZŗW(7;9zDoa{ # }$iDӯbeԐ-R5S0`9E:mZ}~~8jW:Cw=z}}ʙzr-$e:\*p?ٲ8/Efz|[33 y]VI6~o}.?AZRVZf{sr={N?}mIۇ͕M  ^޶Zv`"Qվ_4,$=9;1c~-cLkԩ:Bw];MGIZ:z}m{kp؆8`(jW$iX֐&9̚5eY'I={atޟ|T[v5ةfֵkS5\ƨ@F]@d:# 񓖖|盽+/Զ/&}"]tH Ƿ@C2C"'Ι3+~Gfy_[ zAkzVZrҲnh̑U1TkBooޯ`ЛΖ)p]dXVL&S{CHoOGm%Џ@^uw:%kh{QH׵矡5}g>a3e$$5ЋzwA$ 4Ew[kTvZp.ItWHJU.@$ D]A#{޶Z6lЦ :}h4*Of7]$Ò\۠'bpn~/4oHGUzss<>תϞ۪!]5-%GzQaIf1 O,.5Z9BomRk_\kjkƤb͞ӳ>u3GvZpX=pY~sx^~_<^xg 5HUI,O?sssk.IRvv׿J FfHXi)iʵ9p6TKJtө;юzY!opiMKkݣtM[)c URg @"<Һxwkz{yz`M dS5DKkgN? 4=ZTvZGadI VGz|s}>_sm6[%LWmUW$$X Ցꛌrsl3uL6_>uj:UiV0O>[-4:OhD5أƱE}c8~ݮyiɒ%UWW+Wjٲe£!ٲZ;w/_KK/SO=%׫s="`0@Xj? opY&#rouڼAw6Y ^Zol:$Gg#suJQ̬7S\ñGc?#`ZdJegg릛nҜ9s$I3gԲe4oVm O+,dNC $@0PR7?6v|٬{[}oۅ;[r蝭f[uA:mhN:H%2 iTUg%7ͶibMP,0ljf۬{ ۨͧ*Ae$nhtIN$l$Nb@Rmd$ N'`3L*dW ."0TΪVעZUF}Qb6锢,,!BT &b(Hpog2THW#]3&K\~ngU@e]!C/jEM1 EHN-VVzZ`"j"(@f9#;#MI6ע/iwMݝ$7Oh'_4E_0ȦCSF ҰL٭8<=@˚jCWGCvo޺vuF /nFג$kxQ,RL{j" 酌NId2`]vFxmT Rӥ[;2JR^KbHkrɭ*/ $`rJQr2XSImQg( I*b8,K9&giVE C{emֵk[]ۼjljgdS5 CC 250SC 2U!koɎW$$'dZtA:}ؠh3}Nֵko$T;;CIS;lю/[1I*pápR8}J$BAҫw@ y pHKM% iKUPCI I7{Y*Аte8/CCU+̦0@ f&7˒b)jsU]ߡ*KWߡ .#PxR&妫8/]C2T>87)@?FA뚂iq#rm!gGU Fj Ӹ!C\}5bb?ȦH"]EeP$)Xsdt(7]S~9D ţFj]Fju%rumnf10F7fGt x B5z7V+LOpo%]"}Ka%ȈKuMnEn(ssvkruJQrҔcS~Nx4FN@x<ukz{^:HǑX͑Y9֬ 9Y|j]v{s}>_sm6a_JII\-Z, B ɧ9kIDATj[7qlQأ'.HQQ d6/()ͦ644tkkhhPAA$O:l 9yR4&51{8oQآϒ>7NE6m.0߰a&NRO>6 C7n_/I*++G}yIjjjTSScS[G kg8tj#))feg۩o QآGc^Wq>v͛7OK,w߭:\R˖- Ζjܹs|r-]T^zz)y^{.LW\q4a-]TfRII1) )C#N4JT.jC\ñGc5-Il/^+]wݥnIs̑$͜9S$)33S=6lؠ/Xy2yzuep׿uߘ 0 tocb1Ƞ1B}c=j[7j!!!!!!!!!!!!!!!!!!!!!!!!!!7~'5epK;/>0!In[m8v; 7oUW]գd2g?p88xϱZjiiM:)) LJWmqlPأƱE}c=j{&L6M;vX}}>544hȐ!uT+I***Ν;544tu"{-j[7qlQأƱE}џ^1d}GѶ 6hȐ!ѵ *--Uee|>__`J#7=ܓwkVMMMZhRRRt%hw]-ZHfYt/õb h[UU,Xrwyzw=gݺu:UVVJUUUPWWnIӦMge˖K}a޽kT^^Yf}o…Zxq15>qƎoD}םwީiӦiǨq!$H(ү~+[}B/^,˥gyF?_B[n$n-\PSN?r]wuxlcѿq7N}FKK1~xcѶ~ظۨqc$*++URRիW+33۱-[XC UTThӦM'O;v(f9)8V>\aav!x֭----Hӡj&#]^k8w}ڱc{9\+Vtjɒ%X?{t|>,Y;CVF>'NzeZNӯ~+y^>rw^=3{T__;Cv1H l޼YW]uUvɤ+Vs9sVkGly=qtl6ZZZhmUqj*==zV+5CǏg?.yPcb M0!:gF4XVv;}GѩcǎU(ҿ뢋.EZ|%ߓO>:>DiӦiǎܢ"9nm a"5448z***RssBC)C(**Ν;544D{Nƍ>$wuz)w}ַ%FmڴIs̉5J*((|{^~e544DuvvJ^}UG?Ү]OFG)ϧ|>PPP  4b9XϔitlذAeeeRmܸ1z0 mܸ1z=7NB 6hĉ cҤI62w5aÆ1ǣ۷sbŊzV/m/--'J7xcO>DyyycҤI?l|g7l\xᅉ~k.cܸq~;t/j|qW_}k.74:,cժU7F~G'7a?5vm̙3G}뮻θK۷oqg_q#$Xoя~dL4ɘ3gK/u;e㢋.2&Md|{3o.Hng}'Kځ0ˍ'nݺn[ܹsR?o߾xw{ᇍ1c;vaԸ/-2&Ol̘1x衇Ǩo߻[0q_عs`ܘ1cFQ߾nr-Fyy1}tj#&8`<!ր!!!!!!!!!!!!!!!!4H~W IENDB`symfit-0.5.4/docs/_static/piecewise_continuous.png000066400000000000000000000454301412237106600223720ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 2.2.3, http://matplotlib.org/#D IDATx{|m TDTATEsp ;{"α{!ӹ8V7aNJRTZHB6JiӦ4͕z=<4ɕ+Hz{4~_  C ,`1@!X bC ,`1@!X bC ,`1@!X bC ,`1@!X bC ,`1@!X bC ,`1@!X bC ,`1@!XLD3ϧ+))Ia]WMM233eY-WVVePZZC])=$ lr5 n[YYYq+"@Korr2(c[0 ,`1@+//O2 C˖-kw̶mtt*))I^xgBNcǎբE:|~׮]4iF{O7o}'J~mahҥ9sfco'}^-):D 1ϧ^{M~j .谛xGn X2VVVVo5m4曚5k:^:t:[o]UZ^X|#-#Kv߿_C ٳ϶wת_~z:ь%&6H`G6Q"!ΦռBfG"-BZ!Oj@b/(2aDғp*5O*kꕖЄm%(@"6zrM~B thA^fX!tGA%M W.)ʢr3QE>Òkdby}#Fu%ժp{>T׺HeM}HD"iIE&d(P^ 5β@!FЂk rXtefhpMw:xx]b!(4-7CSrw+w]8;yt6C_q6wk~%(AbIxMI$-Tfr5 ZܬC$IlޯFQj`DU5h/.D 򔙙)0lٲr-2 C>h+ 8M3eJ'f`]]ƎEuzܲe'(333LެsWm; F.f>}O1eeeK\sM* 1:cp럖kaf"X̶v馛n~33rz0 /|ar5 Y6>Cwk<nw[1.S!sHG.D0K7~iFt޲zp#Jb2%5kTYYaÆ)..Nqqqڻw~ӟjĈ_wrJKKWtZ&\~H@:sM7骫jW_nI?.11Q]I_,+Ҟ#TzX㇝bvI lΝ;[P)))6lRSStq.5d%iZnn*_@b xÆ :st9Hϟs9G/Mwt\ Ml ڋɓ'wkܞ={z0JKJTeGnc. Dm*sXFA-|VGLD` :3#Ygf$׫[.D`n`0F)!;=.D`JKvhҨAZm*ck8Њæ~ v> {ux]UZ^Xu14 q~V THiʢr-\QrW}cNhZnFaB `yY׶ʢr]R&IR^she1hDj_5x͡kbut10 p k+nwؚX$GP 8]2䥍e:w)p:d8Pl )ьhWi@xUѮZ#IB`y9EDgq; M9l͛th@ c!h ἡz=ZU|@4-7CSrҵZ5JKjF1NLn-/,/ΖfhTKaDŴLDZ5-fƸL%m*.wev9vWi*y}~ 蛠)cKV@11jeQ(n[ӡy9yYzmKn*=Go7RnƠE嚻~z]RFe8rm[T%0 0x}~-\Q,ϵ<׶i9_ c֕Tk;_R^Ғ$I>@$ Ƙʚx64!;E~rX 0Ƥ%9>q-k uXD1St(fngONJWVѺp LD1vy9._#P߄8}LI cд -3^ζNi}?-W)usl|Le˖>ب[guL}M8f胻s?P8= W 4~):uP?mͱ@`14vX-ZsGQAA>_֎;tךPi MhTmG:.[`aZtfΜk„ ڻw ynN\.CUnhbj4&ivIX~D̶va0`@c<nw[,?QSǤK_G+ (^snN t޲XeqBsMe:db`cco{W.VZ;eOMԾ4fz`ccoDVr@bbbfhVg3Л,[zKfdx*@{1kkkUXXBIRII o>555׆ ]^WPCCɕg`DMi Ĭ]_}{}]M<9i<%%铟_ qf@Hb>ydumc4XdUGr}ؙ l0Nf3XFD;7db0FDf31]4A3?n`b ]P mwXʙ @ "AIz A5@te2҂2m\  :ud[] :db]b2.?Oh /86dSj= @4#"(LATi҂/.A1 Cߝ8BڽNAnKkge2pZ#^׍*IG{M, 妉%IoWh&WNr$]xj|~ Zm;6uib` 6%gғkV]& -nw- =x}~UeZJHcZt1Ox@]w]~}Q?u]\=3:r䈞}Y\r3xx;v;Z.L}n_xxZ^5LДt+VeMҒ"vF Ծ[uD ka&V :-$ <n}#r:^3m&LՌqC4qdj'I6..Iz=YHe0چ{r\R?>u*1Φ*j5% @dLOOv}Z674[YTkgI҃oӤʢr+'dVzzVZXCCV^.ʢS]@I ڹsg*%%EÆ ӝwީ75jF7۷nF>bbMIo7n#f t嗷ޟ?${~iu]:zn6:tH\p|M%%%UrTj`[וTk8yNg_AqYr B'=mDx\8p@xMNc x}Gݝ.SR1gϮvt:rXZwb٧zn]lDȐt胻;~h/ΖOjT }@Q4:=Y* :gHPDZT ADlu2Plb@"mY> 00!",~OldӡsƳ aB G]7_\r$ ne#%I//Z`=@đ:{S&h`9@a]W&+Z01Oz~>R0fKO$eM|&WuaY(-)Qz-+,3,$I>z|>` @ )_iնf%a$Gnp$i{ @o#t?8[ q6'%f@#tuùC%5EDDKO͐VRf@L#"" O!yF0=@D[mWuj] MMM/~lGz~_cG qQKOmv9,zH?-Zm۶~;٥YͭP 6Y6]V3f5\#FԩSaKjP= &Y6N4Iov!Iڼy>}k_32k3 k܂Aqf`[.KGnՃ>ٳg|Ur%ajNNO>ޫ&fvI˶ Zd}Yg#f7$_stlFYYTK ڄ?9I\^* >(VgѮ J۵uߡ"""Tt$БF ҒA{iEDD CF IɉrWp@L!""mHRrkc[=.54޵FDĘs+ٶ;895-7C' ӠD> hdtd8~'$&dn]/kv׶i)}NV1qn34qdf#Sۄ?I58tTK7T%ы'TIҟݩ&/c Ҝ +_Vn""*M-Z}sZߝ8Bif?';%IZNih2"Qk*M+-)Qkԇ{. gv@w,**wշy|;;5r72-*+5wIA'IG&T@t!"jx}~-\Qζ*?|4l5J;l;тWQ'IoV#\ ы8ϯ~kG/W@""jLNQ!#~ ʴ@Mj 56C r$]l\M._z_Zт2-7CWmwpӡskZne37I ٪RrrX׺jU+-ɡ )۾j7ŧv" ovA M_9J *}.>m` W߹`$m |5'ޮͥf b%%I#$IL^!i\6R:=nIk*-/,]URD/K2͙3G۷ƍ7]B_Bf˔$=r5t+5w4ɏ 5ɏ5w<' IDAT _x*..{ 0"->#I4u;,*%*w݇UK du}ZfIu"WKp ~آR5>&=NP}E5뷅|WtWnի5dvmя~5GG&ϯ+?IZXSrҵB W yN'!`$rW֕Tw&!Ʋ]wŋ5j(_ҭު;C_&??_NƊu%՝79-zggݻoW~5Ʋ] :G>vwhZvm0++Mȑhya~|a9qr7u!~k5z}fI!p rxpR@7 JJԂI\;+kM#4sM>c|-f@n2 C:Kڰ| p {7oCGvWi*yiD8 w.W6=t6֣ C r4-7*h@$l~g3uM W.)ʢr*c@FSx{ϵt/\QLw0 ϗa;.Qd]IjRe*kCz`X[[|;z'u)]LZ#5\R&d(yCͳ'd,:ee`ymܸQ6lx#站nJC -%2ď-ˑ("~m,--Տc\\~~Ng-++D4s+{dH18˖-ӬYdalx6?ѣuw p(c82UtވMS?KM>e`RRrss<֯_?{讬17oХ)wd1@o!Yj7hd{fby6=]_)m ZEK#7$u^Y@z%拳%I?Uzx]`G, B0pQݦURKXd3$I Wk&W* F?FhW 0^_kwUiyaG1 tX#УF?_~XW[ŭ,**wշ>thA^ B@ ҒZt9ez+5wIA'Iz]RE. .<5U?z$i+[UTꨳ居+0-&}]]ZWR"1;>)ן>*>?#sTI @FU;TyҒU""h o|9 5 )T%*@/ frCey9ی  ֕Tw9ѵc35:=I`oҝ-3u!@;iuQ}g]j=M3XiIݾB@;i#-ɡS?[r%4qd*R@MNQӡ@пMʖ$**sPe^fZ#IB`LKO^d !E`Zntg@;6-Jn~zih gf~6=In[NS.Kf(Z5AM(>PUu 3X9W6@p+đ1nHP;R8,>Y*2 :3ϫwL@1nr$KvW?jf׭K6jfR@ J]6@3tHe"x< G%t뒍jh] (3()Q?1N=/ojN(tFzqe5yO%k*-/,]U&-`=qfVkb\B~al]E9Oq[e8Zӭ z-:󕔔4͜9S۷o7,S+5wIA $zuIV<zyezj͛7OVZ&M:UuuuftkbuAwK[<--\QLW` ;XW\SO=4mܸQ^zIU+nZs"&ϫpWK*wk]I&L @$;X\.$)%%JUtZ\~ 5|M4IxxZpHҒAK-cjTH'?[;)K<߮-[ xL~~Ng-+++҄e8 ߐ48)Aqnܺ}JO<Ħ`K|"Sw&!J+zw5tЀ{r\0V Hvy9r wThagA^I1,߁ӝ;+5w4ɏ 5ɏ5w I~_~^~e;D%''6-7CWm\ӡsk~AgnwɬV;yg;;Yyg˕ IT>}LlZnw8|Αđkdƶu.I9O}XPs+ᔜt[-0:?SnN\.^_zGf8+?"Jض-RZ4ɏCr~teMp`:bƸL" S';u.6K@ V86;5%Z 3J:ԝ,;?xDPbk@ u4kEz ͪoMG]*ıJNIK1?FbfV#NOG-ߪOn|Iͬ0u(vj΂]R CjY-؎u!OӢwwUSߤ>#(mVPr^Z Of2 [K)f!y.$i΅ô\Ӵ:n:qp8I(ˌYΡokp&_ ĂWXV$_A,B Ԃ  $xMtiibqy XB7t~wө]bP0-R]@ vH[/kg~3a)}ݠ_iƟ>Ԣ]bH-imjPrYY'+:$ICZ~$;7O_Vg{topY\5Zgt~H/nBM+j\%.ֽ)9QْY tŒGFb]_Fg$׊/`s%%]zG]:tQq6f7gzvݷ~Fϭ+mwrWn]R %ͭT⏽whVowБ7^' جz,z}cկOۥZOֽ>6MIW#**sk}.n(+;{_&54y%ÐPֽ䶴Ɵ@[i'CZbߌgx=gzg33f'J7W N_u;Ngѷv5nz郓tSOG]U?I: 4qdjPk.\Q wۿlw96ٖQm9%6d[s|msZr{kou KpgS~rokuMz >Sv084r_>ڣ53tɨZviY#w EY_:*Ð~tɩ?t9{6;I_qHҽi;[/+ݝ{@sw.mWwCO+jBnq [Jk]'ҖʉV@u/=9QWUv/ʋSi]ܢ?-_S7fn)91T T3|ù0u(`L\5nhB xGoo7\7ocO>:NfhJw KNG_GjƸ!82մPZ.ܮF8P#@RY&TAphС`ZVԹ^qvU=5I{ 4v/TENG@0]y3@ ƀ1;{ț;Npnɞ ]MN E؈-z`~3a,RL ˿=$'N?%tC[8/-ҧe.={/[(;1a#Zx뭺C5Y&~L"Pwڏi:ڤ {"k TZJϯۧӇ̥$Gn8ohP爴nPj=HO%F븵p?> ;H+Nx@r?wTTՏߤr^=֭,*ſ}[X,> _0ɺ`uKJ'ftgL$ g:Y& CZ=zaCכ[h#o5-5M>~!m[)9Q-*fL̈qk;Z'2 3H>Иo\0\ت{7wMo|ks M>soYQǵL heČhuL,c/`/AXE–+Uv+o]' 'e+s@J[54RZf]i+J5wl+b6GH3M@iږr=z>$ ];.Sc25%CK-Hs--~z숓k%ZKiq h|OI$p&_ g~VR^[kwW<ѯ_p0y,l+wвMekޱcPR^6R#Z9ZZΪ-,RL~{/}Z6;O|&_3$/^u]/>mP]_DzH͞0Lx{uEFH3$aٯgOj2 &I17|P9 ]9zn8o.;}_ˬ$Z'fDBH-gk¢]nxD+Rg7j]I>yPn)ח5d&sjƸ!J"]=~ߩ\cƌѣ>K.$҄ 벳{vv%#@6xq!}>UO\mv:H闠2uùYCbʬ2l\- /;c=/X5}tkذafČ`vkTJkt|ۼɧi? 5~)?HR7=:PS/~Os۴mGđ5A4%ĝ\mtŞK3:}[/?^/n}3̙3gya~|a|=I/QY͐owttfF2} ngb׿>>yPUoWiQު#W}D0f8h@]42UGuh*~.IN뷅[qFs=m:u>_x|5fvwx7%']Sh&m.=U6;$w}6;M:}7.an7Wϯ/MlGCXWAk vi'jm)ݭJ Hf(IM7ݤ;O'NO<}[o54 fs@A*A-p!pIݭu%J Hfo}KUUUկ~r_. @uD?ԁz1Y X8Һ[jyDojpmiϞ=x<ڸ[ ]IDATq.RKC-A֫ uΣ CS 5 GjA^Nc'#I UvJ-,G*Jb ո5Ikt5-cҝmCNӡs몜꬛X~(piUPfV*}, ;n~ AlLr`9ݭJb@Lθ=>И`BҬqCmqSneV*bJwǭb}W!'!/A"n`f BOӝwM=ݭJb @Ĕ.Y GjƸ!82v;pBw+P1~1n[NS.Kf8rwk@X#|A*k7cİH5X bC ,`1@!X ;@.znJ@ZV 555,+USS#iv0V=~%%%0BVVVJKKcrj>_~|'F٬9l:thGrrrLn~|/?#X寅5c/,~ovnɓ=|럑b3p`1tX bC`x<7n Paa̵^aÆp(##C7tovY!g?Tvv飑#Gjjhh0yuEo߾0`c=l9{Zf%￯L>OuVq٥LCCn͝;Rz^Нwީڴi.M>]3رch"KWּykժUjjjԩSUWWgvi!3tPՆ a]q1cnjvi!~z=:.%6^uѣ[nKoڴw?!M_se ;)+蔒BbEQ(X,:D NI"a.j INlgHfw`}x}7}׶>ۑ2&elGɘ[nj1. 1VeԘ˗/[J9L(#~j$aQ2K M{{:Qr0ct)~َ1]]]Z~6lؠh4p8l;RƼ^SgKRuvvQ;w'&''UTT~B!رvؘn޼i;JNZru\~Z㇮\b;,u~]tIo޼nN<)Y%):zΞ=k)Ҭd~rlcCp&&&Qn׫Wty|>}vUz{{vZqr$+ct1=<'HvzaR>== dO˝c,SSSӣ}ӣ/ *,,Գg֖tr tQҮCz򥪫mɸy^ݻwvUW[[nwr,Hr)//O)a帺%eee*++snܸ܎bڿ>}LF\o1?ϧ3R-g>}RSSjkk Iۇ|*H$Ç[L2ƨCPHCCC(KܬɔӧOF]]]4fʔ"I͛mDJQQხ]&כ-G,Ӿ}TYY۷o۷o6md1YDQ(*H$Suso٩'NN hά޽{ܞ<7v=~XpXɵ%%%ZntqUv_Z0-mΜ9|~nܸ477۱2dk&" (Cp P0@8 a(Cp P0@8 a(Cp P0@8 a(Cp P0@2vIENDB`symfit-0.5.4/docs/_static/piecewise_differentiable.png000066400000000000000000000553231412237106600231310ustar00rootroot00000000000000PNG  IHDR5sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 2.2.3, http://matplotlib.org/#D IDATxy|սLBH @@EqCꂠ(nh]Z[ږmOWT"Pˢ,!@H`&LIf{=|;e9{1v3.c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>c>#y4tP(""B'NԁjSRRzJ;vTppnv;vvv^z%Ĩm۶o߾|+.gvWѐqi:tV{UFF%I3gԪUxbSAAv%??Z}W?Q/V޽?A7oց`]6M'NPHH S3hv]}ag@yyyvIM6vٳ6mؗ-[V}FѾvZ۰l(UV\\l7Lzˡ:rrr8888888bcc/MxT|ꩧgtRzTTJKKu̙]w5.!ٳgbT999y;.n>S}W!CM6Z~}kJOOȑ#km3..NQQQ5)--զM&00za_<">Zd.]ff]pAd24c =s׿={hԩ0`nv˗K}g?I˗/WzzOv{uh 1 dn-%I5i$]pAGŋkxĒ_J.\O<3ghذaZnO #vWVU&I`<2 ! ! !X׻_g¦?|^@7dP[c:{/CtC M >^&]EQj+ކ& 6~*/BtSa)Ixyn!{j Q:*24PgΗ_O%nϨ;WNx1W=2nNe2h w'tOFIqaNEZr#z~(@=ȝC`R ttAMm5W'I?4TMd1UX4ÌmZӮ.x  j b$U X@T5ܺ}'u|DŽoTJ+l.x2 ս &&vV?ҏ[pJNNVLL VXQl^~+N.K8#`QQ… e0twnk\wޖ(LI*IZ.V)]]#ƏK{V\oQ=z]Ȟ k =qI^Z3fhL(..N'OÇ=DVJFAV.00p{O!!!;=oذaz_ߖlȑ#_5͓d>bc]v.3pup…TyǏםwީoի%UȺ̞=[q}[DhF$}j'e8p@?p րY9 q WqA@*2dkKJJ~EGG@e-ޝc er7ܹsJKKSZZ$)++Kiiiij?o5yi&eeeiǎ뮻dZi~F& |q@<"*11QYf)11Q/b9˖-nה)SjmСC:}tǎӔ)SԧOq խ[}3-dձ2Y:|ꜫn`.SYVL&Y,x;yz}k?W[roW@8fuL6WЋ7B!/*.)3jҏ-dP;a-wD2a4WGI?R#zA>N= &^~ P^a>7CBFuuIҲ@M@/5yh0N .=dK渺F^j2G;sTac?Pn+ڵ K6dPłAlg2Dr &I<3 zիvi)K}{3G%.hS J6r}zIo;jPͮm2w~hP3ڟkuBSԴ6=WsWe(R\Z)Hs5.!FQZ7WK>DYKvdkZ۬^9 bq5-nfUm굹25p_!?A#zk 3Oiձ3Ba dBaƒu]NۥRWxDܼy#+Ve0jÇoO>D T||/_Rom0SqY#`QQyθq㔛[}Y6mۦ{G[4ivM}#c RAQuu9!`5qצOg^3X{GVU_|EkƍS:ԆjdbQhho*S^^O^rhQ舍7*""B{#w8IGURΒ0x+ΝSZZ$IYYYJKKSvvΝ;^۶mӑ#GqF%''cǎ^ƴi4{꟟y[N{+ڰa}V5wUjzm&hSN+Xo055UJLL$͚5Kz秽{j„ ݻx[۶mSHHHu)Ԍ9R˖-ӢEtUWi?aÆsTJVe=Kʵ+%I3j C/9RCuhR;+@T ry A, 7#z0ETB5UN k}qI/\ϐ3L@g4hNr$]~/?ck:IIqa*j S+Ts7S7yK=c/*.cIW.%DkL|R WXa]lL|bLA:a)!]6p-z=Ѡ=5aPg'I~F?$i7Y, !N*ߨ}'uNBD:牕 .bIZfmY -8=TakvѠ, W!A7T+2}vf"AKt$-dI<2{s>xf !vm4XI[\\ h 5q2?sMDú-$Il9j@S(!IZvBy.42[ zJ+lz+ ChG~\.04*lvm;iǵP*l 7b2sV8 MUʵ,_)Hs5.!=tM¯t_RW(8=>jmzf.]#IRKvkmznO:VA:] Od>fUm굹2׽*{(@UpYr-J*#hPʑ}sUBAyQ >0F6 C1>("$iU- EYΜoV]u}PR\MAkޮAl+>&T\ ]92@ ! ?As%Xx9KU/ಝ99JR>j\BL(Sa(SL\:w'hs%GJKNf oZYVL&Y,&ٕUbET:we){c Ҧ_ݨ6~mpO\|K8?A#zk 3IO&&vV:a)֚/" \#͛ Xweeez4`+&&FӦMӉ'm^`qDEE[iUM緩%U. C2#.**;k~̙3zgu+55v 6T"BSmGtӂM~ܪo^[>y<"?^Ǐw&Iׯo$eggk׮uu~O^Woc'y8*,8@vG`A n#b`0+L(..N'OfQRR"Zp79'Ag{8ܔb׿ֽ[̞aÆח_~~[fY#GT~~~̛7O&m,緩thUn`h eeeRvxE ڰaFII߯薙!Zos̼pʵ\h{x9)--M4egg\wuRSSBfYfYm=ZϯצM;v讻j<ϙoS ATVa;[Z^q<"*11QYf)11Q/;>LǎӠA]}lݺC?;vLSLQ>}tw( @۷oWnZ93m'nRaJ6p6h- m*ݮ^ZVz^vo߭#z4緩 Cf|iyԎuhu o3˴,Ѣ=v]e/;[-)$P',.GD jItHڇ8Vq߰ sZ*Bhڈ5xH>j&NF}sV|c@34Vƃ.EDzdT ڒyZuu9$ ZUlX;MYW.DD{ƞ2 ~r9@1p_t }'?r)@ĕ!m@$iW52OK&=W?,tq5 \OT'DnޠVCKU~ ;j @T|LGn߿5rO\v\Y\\ ޏKl @ -l0ޒe)9SJVe=Kʵ+%#\zUzp&7oVrrbbbd0bŊz饗mnо}l7T\\4dmٲނG+;5znlI=t0\_Hkϟ;w***JcƌQaaamг>ڳgF+;;ކljql5֠+tsHҫ~h{ veh!A˗/ĉ%Ug / I*))Qdd^y=c3l0 YYY2;vlkuZ)--ծ]j\#Icǎ_g4hNrʰw$ǷX~ѡU1W 3x|4͒GFFVROVEEE*{VkۍKւ/[/S:z^2 ;Lo5 A 5{ve5yiܹM/CK֘(d(X!AJ fo٩E:AK6e$UFGwY_;޾ٳgk֬Y?[V6|g4m IDAThD:һ<=VOkۡzk!ุ8EEEiկjӦM9rdhȐ!5yTlahhh[HlX;MUuasp+ϝ;4Il =ӟ˗+==]ӧOWvtV1z̚5K.\뗿㏷d[/nRF:zF8U1oax@/֯~+]pAO<Μ9aÆiݺu СC:}ts^JHHК5kԭ[{c^14w624HFt[utCN > .qV3<Ƀ4aPfO)(*ըWRQi7X,dwCz[3&hƵqz4TC7gᾒm l*U3EN(2cT+3>xӋG,Λ(bjF]S *5n|n!(#c?RsZ:>g2ͱaڼºC⥂Wj mC&@>8@8Mn!uֈՓ33Q1M]TaْzU@8G'$Ņ5>|H KW(>D9MZŤ  U44Qu/e4$IKSuԹf y&4Lj7B>O޺*;Ѫ&8 vYѐn~C #CtXIҼ5ԍ1ƨԣg.㤫m5LAzW-^{(,8@O;"UB雮$mCJ]\snNϕ-]]noԯn+I͇WXp/@x[Di`:_Z6E#+ f|e/ಝ9lEZz~5o~W «z|_ ڰ?O8rp @x+#kڈ?`qhDxft/(3>~r@x=S6znloIҫ2uZ@CoT,\].EO3br$iluqE>cdώ%] vWKS~sk?Petu9>%6CUR^h}@7Tdh kG\]Sakۡ|L;mUaszza\>Tة"v]'ŋz~q1A֦WҔeiv]WZLYbPQiNmwpΝͭ>֯_/I&445 jQ鹚dr-5RKv;5qY:os:mܝWN:)**ճgO]u^c0j\ՊR6PmU]Įt?ea>+JKKd=C2 uw9uM]t~3ٳĥR .]RX)YN ]vggߞpj+ +Vٳg5}:۷/^>L~t5(33޶KJJdZkpBǞt|Xiii1c㏷vٸȸh-:XQüQ -:X>_i9gl')aÆ C=tﲳe4uϞ=G}TfY&IڼyZdb\BG)%@yŊmEi֘ze?R}hm;5jdba8KWtok՝/$EC3ǟ'`>}L;绺$\"kMUR儐r+H6ʴv(ߩ[I (<8@y, y$鹚*qѦ IwL+h UҡSGYKv_oRKvkmzuJ Ӆ ]vp5 ۧNkW!crKhg/药:m\I8Q)~{aϵ:}Z#).LѦ յ)Aœ~tKH?Syk<h4V<'9 ~yBBE|ڀEGS+Ts7S;mD?V@_]:b%vsP!l&:6R WXaߖn״)ڒyZCw?!c+<|g4hDpMY#zJ*]v9%;}p& >v>W^Μwqu4{ë9cBue`޽ \/{wRDs% N^op2ʐxU!qW =\X\ŕhI@x[X2ސ#!]o4h>ml: N^abEu.2IA3o)Izqe.hơqӕ^ϕ3vZ^').LѦz[Xpzk.mCi@?rU2O׺}榔@  \go] Ѧ +{xpztTIOTaI -[CV-#dPxqstA M7jt, no`r 7GnV7*i4%ky&6  .j5ORN\| ?A#zk 3Of\=tB(M6'T s`4I=gC. j@t~?$eۜ.J@MY] ](MJ+\]@% qb"CuT}%^`qDEE{ͦM4dGz뭷ZZ+w$6sqE_Pc޽u[oUFҞ={FO?>VdTN>$? J][goׯ[o]o$_~JMM՟gy-Y&|دՖS:tHtL.!L(..N'OÇ͓d>bcc>:4kloIҋ+{3DZW;Ԁt7kՒ{k.z!ٳgbT99I%6=n$x` 0@>**JfkyyyWxxx*444hЫ*24PNw+ŖDWttt1b֯__uꫯV6mZDz}r=c. C">ڴicu]Zz$UN6\GլY~-\PyWa=>W뀹|Wcǎiʔ)ӧh֭$)77Wi͚5ڸq _~Y:KͼF2\[Ky v>j2*$hJtk[WX;w_& tuI@ulקT>c-=5kL[N< 'ٵPV׶C5'ny'>y@@񚽀WZ2k)~-9PrD2 zA-:wN/ܧ?h鹚dw'IfKf.٭ձ}^q}:v< @3T욻*C V6wUFG /o|?WkO E@3d\w1\KR 7^*鱿Ik@3:=h_^WXǗRIyESJ2@"BziW+4_{w+źg ͐hS uޠIqaMj{`Ϳwcz& @ ~F$Ke!93v]Nmhq Z0uL5yLAZ0uQ=4qP*lv=.ovePQ4.Va+%@yŊmNߥ*t[[~ܪ~ѡd `-wh,hЈ0F wj6~VڟkկBMBۧuN=Hh% =ۗWXwXC=y:~>LG+kf.g"Bj ^tRqM3KՑEM}@ sپ!:(Tc=` lRAQ-Leo!-gv=9tYyNr\%8_ Uװv.8ٮJ^<7.!Z V0o)H st G)C4:STڨ6އI @ k}5&>J)Y+,VDH尯=ҡ>2\w7j-yxLm4=haIqa~hЈ0F orҽc>xx‚E.Jѹf \@ N}zEhɌʞg5cN](e`E^͛C*$$D8q8P5/`(.f$5U|L(I!ڑUG2B xpӦMz'5tP뷿Ǝ y]hheA1(ȱ絀r}M50 -zp-Lіz! x ݮ֒psv{zSN)""B6mu]W9/ֳ>g6>VU&IEMnpO;URnӭDnסSEJ*Ў|dl-Vڋc>io/b$տ`sԭ[7UTThРAz嗕X%%%*)i5꜂yeGCZ׬@z$dP)YڑU_LѠBGx]nׄ tmٲo߮jZz״f}իW׼K;we_|_3vf׭=x *޽YkFoaqaJAm=^|I^Z_tu6Mu]_sjtpJ+lw'5uxVR\VY{v=<!+ް0 bj?@ ^6SO>͛$h4jС̬@6L)*lvL(ejF3FiY)M{7ENb>Y%}luۓsVڶa#LCL(W@ݮzJ˗/ƍפ64`p鹚*Ѧ IoԒ2czv?#IG7sg rLT?E嶚VSR\zGn+x -]T+WT>}_7Lj۶$iڴiܹ͛'I;w^zj_w}7JJJrt!֦jݺU:`]\G`-1L1Wmjpc,7+#o7[uiqWa=5,.Lqe0_Kz,X Ijh"M>]-ngGldRbb6op\fU6*И( uNA:|HwMKu |R\KJV2]vN\`%uKv.MБN̍7W_իB-#%Pݥr-J*BiC{#I6{^omg$_oY3 ÜnWNߑ?y#kX\x,݈PiT^_WV3Z͑zw̖b\[]ݡvQFU6-z0ICuh\pK~=nV.|+] 1U!8A|tf,ީԣg4zcJntf'G?KS׳U]fU/rA6/P]ngU]|CuPH+x+ Am R\k3HF~12LQiD~RzF_[8GQ=n8ƑRg,M}v>ZMxH,\Ƣ=”2.=y,`Wa\I5Hۧ;%I?LT }\1 eiPTVQ[᳻-/7a%Dk22i9>ʡvGΥ_mI }zLSQoo#[C%WKv_69jhsmzn}&gd{D_F`3\%^}~zϿ驥{t\]iueDHO7ЧOԌv*~V{ޝfRM 8TÕ6j[t6UnvBY2۷/T5wTQN|wdVPK|OvӜ`96lFei֦l-~QK Ъ]6\tyx:e;s> `3 Pn78*IDAT+k. AOITgj5HsE~)9u-G*E3mv6fԴnY} KejF{ưh4ԘҚ٪?>.&ɓ\ߨ]\PsMl>@wcI#z|_]o 55ԓHhi@ȽFTqK{;\rQ왹%|Pr,IRBP͟2X;2=R $ K3tuu۪9]>{6y& ܬх=mI5g[go|?<\<-KӚ4t/I]%uSR\*&B;n\;;Ye2Fj3̇iJ9Rgiۡ|IϞ^޽1Q_;&>/X.Ұ'*D{Gq}u=evw{>gi‰"hQѦZ0LZ3GɉC/ަ޽go^;g/6=W/ܧ’Oà? G뙢R=Ա4B]ZQђ)$P?<7_P%NTIҢo8Ԗ#vUdPeoQ}=i Zv\C/]ggLmΥٵPV׶Cn{/;*JyϿ^^]g`Z:#%Gځcb Dӝ/{xtaۉB yT45ొN[:fRҫkt DԯnYRHՅOj|}]W3&bބ\2{7صvLbo$[f$'otMQghK{Σ˞j!YQ!H g_!'tonC}-hRWe*8@gHD}Ͽo~~I4k ΢~ L+Lv)Y)nx~hw$M cG'^mBѽSU7޽鲏c 4m03 >w[6 ?:m)epjvU D+17 ϡ]bo~)͙>+\-~ۀhx_ZMi͚kki-/7vK@x z5*UpiK)2zM\ub$^A+H҄{aJ8o,OJp@Yށ7Q\y_T4+pJ<15k:wZ\0G:"l괥U{`Ěنz%刅57C񏥩*ZtǗ= #yLɩ}rLKvwF |ŕ8WccƻHQ!HctX}Ď6\uuwJ#@"p(oKRđ H%Joa?+clƪNEkZR67Wوf"}T dmbCfְӖ }vz%X}n~O䦏œ)K?Y.z41 ?"{*1(4-y޳ʨ0K؛ H_SkG@m 8mꪛe J baߛp6d>ggh_e{\AaUL&FpQE9b,Ӓ ^qe#tYƆkumxKZ#8mꪛe I B__F`˓I|[l &ˮ`uWș3KgڝQ72)R}e:5tYȡ7%.lzށ5Ӗ 4*M]_boR4#[mw;ƧeWX̛!n>ƚQ#%1u PctOV5;H D`Lv5]vU7fa$Iyo-*Z/1r/Nins(._IMb0-ˎ3__XhjjСC.7}Nq{==-&E!31 ]S7>pك5K{l4Zt8${~rqׄo7 7oƍq5L06m̙3ݻwcڵ(//NÆ )"=-m1xs%Ik 𗣕Tby<:'DbaB$ƍиd4zE意j.OvM&k1nDM+)g I nwڅ,l޼ӧO֭[… Ǐ̙3k!33Xn=dQ78摫F}=q뎸E EApR>*p90! 'Spm%W5&s5]'lhm0t".tEI#o7 HLLĖ-[,7-B^^^_x1 kyl Ν;E='@D/$KLަ@o;z|U ]㻩 Nѡ9#0jam=Mm8YյYk{|`{t#%[;&шZ8vX_sqWؼyi&ITbwDӝvi<5'DH4Eх:|U \jf\-K $ߛnJ9 R]3NVĩF܈;=NoΝ V\#mgfHn`2p5V}>WCgki7vo흸& ?'ct.\G=<0ɣMm3wM}{Y?Ŝq}RHϚ_l6pŸq^}U\v 'N'|X^w-"??/2֮] N]vHDsG\>w2*C<3z|:YDŽbOiM_+h7%Ñ(rM@9K-|7 Vtߓ7Ğ`7Xs̝ɵ#Ț78 iZC;~6넠Tz۔DDvy-O&c0"Чg3$&m3oJFpSKr)`""@|@ j)gN?_mv#[pSb$"bM/]V]h~(;GqS{b$"bm/567JhD4c썛[1O[sB1y$Rt![ӏ_o(h[_FDm]DD/[%/޿m7ss 5) \HDnM5p 0)  0)  0)  0)  $O3 2WBDDDbJ> h+!"""k577#00P2dmىh4Tn0j<y}ݯ7x Pj8QQQv}C5\_#op:gKDDD` DDDD ^~zީj̚5 CL=5\_#@SDDDD HDDD0 DDDD HDDD0 .'OJBiiHGALL |||0bdeeF$SUUg}Zt (wiٰaRSSaÆ]6o V $%%ȑ#r$Çc…Jž={.IRyyy:u*4 EL$e$$$X$`޽reyyyPTX|ܥ@b DFF]eeeؽ{7裏]d.^NlݺϟǛo?XfܥIh4ҥK.fvK/Ә9s&,X^/wihmmŤI?ARСCXlN8"ttt ==r&(8uN:ٳg###ϟ4IZ[["w9Ylz!̝;WRO?41e˱+W!!!(,,$)//olK444d2!<<ᨭ*,A3f`ĉr#g" FAAƏ/wYGII .ŭ1dPT9u~m ^Z"^|E>}ZSO=c=_2U.`]Tn 1r~?<Μ9;w]ƎR8qK.Œ%Kp˲Yuu5rrrc][Qp2ihh@CCCqdZO<.uP^_o?W\Att4;S^cMM Ґ{`^{˗/ǭ[]]F?DffСC2V'=J,ZHR$ /`Ϟ=8|0ZܹsuVKɞ={ Zmyd2AR+$44~[o7kjj0o<ڵ ,&b7Iۥ,Ir\իW$l߾`k誼n2VFb ^x8x"uosٳg=3 >>+Wdq@@@!**J$U\\b̘1AAAuz5555kbbbׯ_]DDIGףz&ҧrіEVVLl۶ zmlo|\YYR-[ | h,k7+suXf ,Xh477#??ħ~*wi6h4=kׇ:Nɶҭ9sFHKKooo!..N\"wiپ}?bɒ%^߁.mPy!66Vݪȁz},Y"wigmr&_gXX0ga߾}re7lc\HDDD0ο$HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD HDDD0 DDDD ;oxN IENDB`symfit-0.5.4/docs/_static/polynomial_surface_fit.png000066400000000000000000001156121412237106600226640ustar00rootroot00000000000000PNG  IHDR<#sBIT|d pHYsaa?i9tEXtSoftwarematplotlib version 2.2.3, http://matplotlib.org/#D IDATx{|4DFB~WUԊ|)6ZlRB5[mWSUb+݂Rm j%Tx=XG7x7>X]=%7DO^>K}#]yzk>{o{}"yVTc?%Hqu^y:gqgm;tw׳Oxz<$)fu~X1$yx>x=[cʹS:ifwo:wusGmR4xFm%yݾ'o_zc]vqnvIHԮxݽXW~HDH'`񞨂eH )W".u9?z{{5bĈ#F+[MMM B [|<sCNH6LM O2?.]{w@ۇАmnP9,DZrwunPШa,ÇOw}' l%+_Eٯ.G{ܯsşUVVɓ'kݺu:s׭[>2Vƙ]7^oo$~T<xc,E=TP-F ֫ ?"OCZ|; ]VGg}i??{QEVږ1H=SzoRls>SҾ$)jl$HrmYSeY.Nz\ɞ?xl /(?ərw]#J.#Kӻ.Lﲒ$;J.+Kӻ.Nﲒ$;J:o%wIvz%]Vc9ST-W+UgLﲒ$;Jǒ$.+Ǚij|Bf*}ı ^\Okhhܹsu'Oֽޫ;w/sZ ?N"b-X ?.b-\-b-X ?.f-µb~>,d$v+mmϓ|vŶ}WlnqŶ[Rv+m7m7PiTb-tqŶL_ u9şvh޽]viĉ/c(HfX)pW\q⊁O|҉6@.#@u APÒ/r;Vl/ {PX [*@.#Sj*PyyNoYǏOn6#F86G ovi7x8E%OUf3G;#v}K |ZjĜFv6k1byqD|;Ͼ\[GpW53Wl{ѫó#St"XsHwl{*ssKE*1JGmowŶwbۻb=+utb]NtWl{O+=#cnbۭmb>7b=GmƋ9cEG5=Vt+{nɟ'>:5O<q͟a͟?_>֭[X,Zut82i!7P .S1S֮]eTUUfM>}fHBm,G]FcpOUVVD"|\hLR~_`A;Xm^OT2k׾ РiӦiĉ n>lg @a8[W"3-Ђ K/iӦM566!-ϩ@a3.|w(ٯ.{WO u9şW__5khƍ=z`P`bRU_tٯ.w/?e0^O(vaGl©wDXwy?;t|v:cŰ[Ů^#6q>DOɎzMwunq=ocBGޟ?@l;"]JoldGXĶv_,kʕzGUQQIR(RybX ?!cPg- GPX)PK.$͘1#}ٲe7o^'8 u9ş>QGr~[RWk-??>ut~@{`@9~#ȱx_ۿHR1AR&Bۭzg %L] k_a,f޽['OVIIJJJaG?RIIz{\nllT8N]]7ef@I6Sb@z[N\Ygwy'>;wԙgkZpzzz 7@UTThĉ mְahwmo{h1]J>ccmo/.VeǩF/-VtۑVb KȈSpD1RhKAcr}xX׷ȿ9֪iŹKRbŹZۑ#==g4f|75kZnEK鍀uÛ1>;&ʻl /(={Nlm?XbӭzJl{zc]A2>uyÆ ?LX,kVzW4x`IҕW^_z5l0-ZHgVss۫38CGq6mڤ{ꢋ.yZdI?dk&l2UUUYӧOW8}ݧx/%rŊ3f|I͚5KO<^y۪$?Լyt7jȐ!\<g=_+[ Y$ r8$UVVJzqhT}ĉyf͚5K<&Nط#IfR$QssfΜ* dHc]D"D^  >穡AӦM:pKK4tЄ1B---}1bDC>\ jjjR(J5559,X@/z衃yu5>z7v<7@f.766!͵맾^k֬ƍ5z#GG vޭSKUhA_ [@Hc{0Ԑ!Cn~?iZjz)7.ɓUZZuڵK/r'_~YvO<`0ɓ'ӓlΟ~_KG}ᄂ^:_mW~sNHb;ڱ);̎~]"?Hbm{b;6݈z:vc[߽wDG,uloDbcͱEF }>.IƱθŶsՋTXǬvɎnv.l<|\R>*** T^^P(K.D-ҰaTYYŋkҤI}_0aΝ[oU/^K/Ojmm)3gWoF$zZHX ?rKfk@f,|`- ,]T4cƌe˖i޼y;C%%%3gtꩧj*ˇzuWSNQyy.Bvm|(`,oYcƌѲeڎ<ȁ@|eP @]vzРAd-YļOMM{tN 9k5kOԗeUUUNO~򓁞yܐU,7|SK.QG_׺˵pB?5D"%"|* @Z$[}rzS>覛n '.L^ziwJ455) %~sgg lm?|  HӾLc@5J&LHh;cӱبp8p[XSSlmˋm& }!qu)h۶m m۷oرc>`P`0C!WK8sPX)PW]uNnIso[{コ{g7A/$y^Uo~W OE痤1_a c㔤.>ّ1vI$O;=9č9>SG>#*]87\V>xq9SdK/\15BFZ-6tNwQ.>=nDvFшQϻ{1jOQ%x}ٯ{PqloQ5fGިU]}FWs1qeHzyu~BMf 2r?jʔ)Zz}Oƍӝwީ}k=5xYPa,ٳgk= fX [qAHu9e0V|@.# Sbz=jiiѨQ4o9V4$:QFy#܊Ftď7]=)E&j+cJkO)-r}bFԼdǩYXzR[:c]l//(m']"]Jolmw'Ou=ױ-[_Bo  u9}gyFg}8 y:U[[-[ ISLQEEt9h۶m D"5x`uYzwsNyu)V}+zj=ڴi5{l~u^qЦM?-Z֧'$[a˼Vco[pXԧT\\^xWjijj 7ܐжf=g j]6e˖J͚>}>=:$I+VИ1cOj֬Yz'+Vuu$?͛oQC fx\@b9?g?VX+W^߯nM٧Qp8V?&wH$rH}$Rܬh4ھTWWkĉڼy4qľI5k"Nٯ.ku?ꫯ5\|+I&魷RSS.">`P`03d0@RH$CSCCM'JZZZTVVC&wĈjiiψ#:Teee}~u9B]  TggŊG^.Y`^z%mڴHDm,1r?3ԍ7ި{~o]_|qROY~XI8࿒[l yEvkĿO]4ߧ]SzGd㈯}$ƒH<^A?$EcwtFbus\YS|~xq\5'd3"S__5khƍ=zt_ȑ#ӣք?wԩSs%ڪh4z tνn5'_3#QG-=?w+u5;RdqQ>㽉Z5;ꪋx1y"x'BU힛gKu,jRI'yg]4)U>g lg@7@-YD s1Zx.2}>4"iZjz)7.ɓUZZuڵK/r'_~YvO<`0ɓ''$Q ˑΟUQQ;Swy@O`q|_ϟ+WGUEEE5zB t%hѢE6l*++xbM4/V&Lܹsu뭷ŋu饗@m,7uHVҥK%I3fHh_l͛'I;TRR9s樫Kz/_S\\\W\qN9 /mݖɇ@zc9P0h -YDK,1SSS{,S@XNy IDAT [qKu9e0ƍu뭷Yvիu9=Ƽ;xIm%IEeꊺ8YgXT$9bk=ktGc%f,y>Lpзvb֧Pb۝1ꩫNl{b۩!=Outt]w{[n߮K?F/| ڿg 0'WzGcOS]]1twky'I5b\R]vY& )Wa)@;vPKKjkkڂ>kfH$pRepX)@---#F$1&Bۿ֯sBŽܐR~u'o\P.S1X)`\xZccp!G4pq]Lm˗?&&5 ȑ#%})Qw}'- *LL) I ФRPSƍ#Gjݺu:$I===ڰan洜qٯ?V#cO[ΊL.#eZfDlw>qb;qcEJvl{o<3D]ѨɎ%I]sfԬk{>7+f5Qxpͅ% Fjs[%"=zJTR̬YW-O?3fdlj382r?yjƌ)ldi y 9F@cيd2r;V^<=7.'Y7nܨ3;wD"5x`uYzw>SO:]ve ;38C wq뮻|6m>OizuuiРA}+zj=ڴi5{lfa`< ׿u}K_2#SuuuJY̱_xJ}_5g_zc=X5.bi>1@٧}1KqxuqQS95%)қzS%a=XZc"Dx1y"kAb)}:AcYu1}cPK߷>C)ͪ>Vsd8/nRu)\WW:^/~[>O9f͚G`Oӿ˿3jzUZj@mܖ裏֬YTUUN:)awissѨjkkڪ5qDm޼9msAvc@q͝;WW_}=؁OK 9 Y.]u9)-Dݻޮ:Osy睧 6HZZZTVVC&1bZZZ Snfh…ʵdk_]V.MMM B ljegJ͞=[sy  F^a577__|@O? ; F*ٯ.j?uMw466*'URR &$s1}i_#GTOOZ[[{n1"9şFwVMMJJJTRRzK-ґGi{b[: F*ٯ.UPߤ1= jȐ! `04em۶-};v$i*--պuڵK/N Ҿ ܹsYfiܹ׿n  8RI6R2v}?رC[nUeejjjtW .5sL]V/~zIR(%\Eiذaŋ5iҤj?y`/Æ KiiF'w'ʝFd+Պ9M3.G{q=6O٫Fk;nKvl{Q~DD[\Q#ފyxtW̪:5l |*D۞^V<_+a㥟vbzbۓmwhme͜9熆IE]˗s=ܣ&-\PǏsM6wܡ͙3G]]]:S|rA`'O< r@3ft/_l4h,Y%K{z,Cyko2  ?d+v=a,F]F# lru9şexbVxPWqoiӦS֯__|Q]w +مl]uU:u5hٺ{~MMM B ߆_Դ[f'9ş4|ph„ svikllT8N}64?^Zn~f|JLL R2 >2M2E۶mKh߾}Ǝk   m%~#@l / f2du9ş<ޮ_;vh֭TMMj]p>}fΜk֯_y:meѺKQGi0t{|]/mnb9 5`=VyTO4>ьV]tfRG-ƳƒBOUISΚһl٢3g I袋|r{{Ԥ jiӦ Ԕ!ru9ş<5cƌ~zq/Ќ \;V|@.#@d2r?d)-@.#6nܨ[oUڵkV^s9x{{=#ڻw_@4`ȑ>=}dŊ3f|I׋ Y* ; P4U4UG"O!NOҴ0H$i_^UUU:u饗j}ǚFU[[V]]'j)3Q /0vv}?رC[nUeejjj^W_}5vXmذA?Ou'u}q<Hz^ǾȘqۋ}E{1v b%)O<$OqqWkcqz}yrgJE1뵭k[xSSnᆄ~N_5vXرC]wN jiiQYYoĈjii8#e6w"UK]}F}{n}Rgs'V]j$SU3]ZcQ5)IGn_:1jv U>k!l٢3g I袋|r=jll׾5;vnF]~5e@?illׅ\'Oرc3y,  Ԍ3x9R˖-IK'~׆IQFiرz$}P_zzzښgݚ:uj!WP DZ9kr!Qd޽z5j(IɓUZZug׮]z ~HB.cYj KSYY믿^_җ4j(w >\{$) K.ѢE4l0UVVjŚ4i& @.`u]K~ӟj߾}5jfΜgswDsQWWN=T-_\Đ R K_c 4HK,ђ%K95;XSMMMZj?\SN7߬'hz衇>5zC>ۓ{(U)( +b4jĩbV8u;ךu~ys3㌚M!֊t=@Ƕp–JN36HYcE%>hXknE)= WX$Su+wbۉmmU:Oq .#q֭[X,Zutt+իkӦMjooٳۛ RKe)9gϞ=҆ 4}taqztH{=3F/5k֬C5_m8vXҹd>EL;;Z_7e3r鯺|n͙ak,???Im߁r?`z饗C*ntO^<=7dGtOf Ҿ\}}֬Y7&9R===jmmMqݚ:uX`P`08P?#U$P wQXS穾^W5nܸ'OVii֭[9sHvڥ_~Yr!3^ll*/,IQb=Fld_0' Npђ}aXRuld,΅ U6_9G.hJ4ҀKQ3x3o>h.ޜ1VL.ih;XSʕ+裏zPH BKh" 6LZx&MN;mgO>|A.#.]*I1cFBe4os+T>~v̞?c(}R5kn%HW’#Su+^PScs'KOܨ OanV-u樘)_jv58 \}X>XcTk\RyFٳgaM>>WGGJJm]ͩqc!'zzߔJ,XI7R/݋?tysRmZH8HӍ?\}\,d;lKEDmFJ} ro0@j x7Qx IDATU,zYFO?F}O'> ^ZX 1G/e@6nܨ3P(͛7S XSfƌ)}G]5`0`05\ /PC $VUUDEd)K ||˞F|Ex\w}w1yW@ʕtF544$}]?hTsю;SO#GO}٣#F|NȠt|C.kz5lذ'|~곟$S8ԩS2d?RĴ=._yǎںu*++U]]?_/{1]ǧReee:ctK/ՏcI7M͞==4eUTTJsm{_TWW@ Gy$3X1κXuYKSU3]uYǓJͶrqf'ǪsX]q\ji mu=|qDC7_YY+VsZ1,䩵k&l2UUUYӧOkuרQ2=MI] \OÒ>XPggկꮻȑ#jj_e=e2 yjhhдi4qľJSNg}HD"E"wn X) ,K/M6YFO=~8MMMچV#M _\9?g]71i+Qh(k͚5z5zzJo?pu/}K1cX aq=/Ge>Lm@`O2r?yjZr}QUTTE T^^#G^L7.Vt+mwEf*6bcSMv3$#$[Smw?X;5[񜑶qnjO?2X:7@\]I!vb۳]ƶ۞vsPQ@:.]*Il_l͛ Ƶuruşe2Rl/ {P@cOjjjҪU?A:unf?>~<󌮽Z=s*--_W*//?ƦX?^/hXɎsuEfslc''mO%6Vz#⊍3RތOollI%U$ t]&lORZ'q.lt.#'OmذA׳>u)V}ygt駫Vok*2  iaQH镮Lm@x={J6l%I/|ASro;;ccOj;^ص<ߌ .\,k֬}aÅ"dPf; `l6IUFT[؂[GiU3TZz.@瘹]:g`\h*((⋸;3gD}}=f͚cǎ!++ %%%֭[GKDDdnDDDN1 5g̙3'ȣ!"@pYy0.E?ef!""P\^NDD9)qg"""""""""";DDDq'" ""8cR_}!&&999xǑJ<())Amm-2220w\|ϩƥ@jtYJ@R0l`Ltg~%i^UcH@SWeu(ˢHis@|Qi?m=m76mCQG^ŚԖ-[0uT1bN< &`شi***0vXvmصkWGNDDgy,<(Qq+L_|:t@yy9m۶/SO=>ȗMq(1.S4ʟжm[oW\رcx<(,,Fnnnn555>@Iх=(/.s;R"`1*.36S8pB`ٸ+ЧOo؈$hɓ'ݺum'??>c: """Sf\>f\&""sqW_)?>?͛7cΝ={6nTTT趓jVw<=(3.iDDbT\flpM6 6m?Ot[W_aժUx0tPdeea…8p yݶ4MCV|dy " X yѣ ł7B`ѢEHMMELL rss'1aj &la˼#" ⲿ٤6mPZZ4uuuRJl6x<>J+fU)`m|Ǥua>JۮJo?-SUTrLՏ*mNIJYUX$պg )me$X:fqIdeeᮻM7t'x˖-K/=z`>|8ߏw܁oֻ~;L0oFH%*.4Ii 9Ӷ̴I~$HҶG=~$12mCѿ":u*֭[oHHH@ee% 11111ٳ'1ydeA49Z%99٧<99NΩۡCoq‰+"bC/%x~~~B2=5~;DDDƨa80""1hߨ<̞=ۧU?Ꞌ.[^UU] GS?gQ41*.W^J@&ҐoYCClق@vv6zgWEpd9qKݻѶm[t3gҥKѽ{wtK.Ell,^koΤzZl/""2fh*((@AA<ݻ7,XMvW_ũS0tPOBǎG,*mlyNG@Pa 'Y{VE?@ձYH*IϮ:9Mu>MQ'lc|~4G8عs'*g.6i$Kx衇p){8~8 wy : O vcժU=i-W_I $fRQrxq>:OY{XY?_i?H #EY{P'WE3,o͆t5k?vB޽q=7K/$8va]?$[][䏹&4Sz,T?u7 i5 \2''8ɟ'* Qq`l=zK,AAAбcG}`֭vn7jjj|\8FD< Ee"dT\'5'L4 SLAQQzJ8NiQYY)m/??>?4Z,ŘEe"fT\flp䏉edd`(++=܃I&O?B鼼A*o`OνEخWd׾Z׾6׾ꃯI¨ 06Sq!q򇈈(By zQ͍?>K.>(,VZ> O<?`ʕy'l2Z ;v@JJ 2'LhժZjlcǎaڴi@ll,:wӧ:̣&"XyP1.E??y۶m3f F]oƈ#sNgV,_رcѧOYuuuXn] FeR;vc=t5k0fڵ B9rO>$zCaʔ)8r6lW?n͛ǵԑmx16HE16HJQm)ҬMy dAddh,`vI6&IXiٳ# %;;Kr,M뉀&7onr6oVnެ)#sp>eA| <ѣGٳ|/_8p*++1bv[b? 1ѣGd w}7^unݺaɒ%?~<amAD _y0.E?#r~~>y bѢE>e?0ѳgOl6455aɒ%dz8t萁#hǫ N>EEEի9?y-M хa\&"Ĝ9s_зo_:t4iRRRYtEyUUUZ6nlbؽ{7p=`ҤIO}~FB^pBe{HLLy45Z4f2e"l_uuul+Mx744`˖-$ɴ8cbN8p 󑕕+Vxk}t8g&VWW "پ̅q(#ѣd8x l2x|k̙Xt)w^y睈wgՂ!kjjpWC4lڴ .&ri9Q`|V\½ދ*bX`wz!:u {/?AwABBBGN?&5w\9:uBmm- QZZbbĈڵkQSS3۷oMd=0FmW@VFNUՖ,k i,5j Ӝ:_zv]kӿJ*Kլ#=OEUYzvkc9x,[,c%'$<U]iۙg=N߈=#.'$$`z, -ZtN0I=z&Lw}DdffÇGii)oHOOwt5 #&"?̓q(1.S4I=c!""0.Q8q򇈈(B:HDDDŸLь?DDD{Eef!""PHDD9)6m WTAA pA@޽`9kQ\\"p ~rFmWL U*SUjVUT6g4֮I޻*WU=-}$GRZ|puÐv06j>b/f:o?!Ҷl_grLδ'BӶӶ]ُBpǤ:v{ 5k`̘1صkzm۶kA^^V\ Ӊ={( "\`nDDяq'Lj>?/Y(++C޽1k,L>sNC=L""Rr`\&"~x;hjjBaa!NPvQSSDDD`\&""P䏉UTT >>aʔ)(**B^_-Z(..e]C/DԆtZ! DDͨLݻQVV{&M§~ ̂ɓ'㮻BSO!##/?st%L#"rsc\&".E31sbȑԩjkkQXXRb… ~a͚5طo6l%zgUc?+:V8,٭^TϵӢ?;scSK2f5+k.菭M:q+18~?Rɰn]uO7*bˁJlEO_Y{:cX.kO]Gͪ#9U?ج#;G#qDSc:OYyJ^koD֞oOU%9ǣХ>F.Z2.S4I=z&Lw}DdffÇ̜95k;,[na9%xg42E3`NPjϕ?\GQodc +$u(18$w^ӡI=ʟnW\׸.olH;s0VP"9.sW\Õ?]3hf =!""P\^NDD9)q򇈈(Bqy9Q`\hl_DDDDDDDD&ƕ?DDDˉ"2E3NTAA pA@޽`9PYY|%%%EFFΝo6n3ɶ3rHu *Jd v|sBٞʍddclB (6Vm,pRVyGQGIrSEc1XҶd6 #[y.9󉕥.b%N& 8-9֧:H'}d0 3ee6Hܼܼ͛͛.)k_&ձcG?͛7cΝ={6nTTTHCuubM8 (20.E72c3Ep}p1l0t =ӱw^xzz:} nذ1})ٲ}ɲK됼dd!;TkWm]n06+Z㲲}1Wuپ}]~c0*.zLނ!vQWWBfcPc#/u"GVOiyJǦxެ ,M>!#S%iO\&yd<g\edy*Kq/\,IEa 6]N_WL+8OM1ܢ5.q"9@˙/&N'r4]lf\hƯ}ܹs DEE͛R7={Dzz:&O?_}?7`l6EEbd:8t!c%""TLMь+"G~~>}}97taÆ7p]g<"̌DDDĨ̬aiaX0sLoƴiЮ];Ƿ~QQ塺瑗wAuзo_|}ή:@LDDDį}܎;s!33ӧ|̙x7PXX$P^^|dyew3l?Gi U16Y{6ٹZ)>eݪz,Qv>N#"vH`WMƩ#w&VGe9$m9%gN_x![YTVɋΈMJ5{Fivzrn.`0jcIW.g}+iiiHIIAII hhh-[4Z fl7.>UuTJ֞*S?⹺!lSG#q@`q^TIUu$YvY["⬪KrLُGKhͦLh8ᒷg4nLь+Lĉ7nV^6mx˫# cڵIKDD% <lق`QSSI&yW,]EEEػw/N;ҳ`.DD˨̔1SbԨQ6lOyyy9N>퓭%55}a"oߎ ;Neeeҥ ࡇ̙3qbwy ayt`l&""p׾Lعs9*++t:}8g|M>'B*[,,Z- ̀L̸LDZܨWÇ1c +p\\|zb}|␔-q#)) m۶<}X)PDrXU@R(ӬJƠJoVet(ʞVi٨yM]c?b~ei]̣'H>~Uɜezn V]j6QqH Ri鱘&gHjuSN94]Gl ,ˑiW'RӶLnlvg{LbqYo6MӠi?Mxbo\}Ն'Zz vz+N:C⥗^ͦ'7lY"q9??p򇈈(B1SQ02.S?MpVSS}Z ? ~J+<𙈈(By yQJC-WEP&!sQb/""q Bk_-@~~>, fΜ 8vM ƢsΘ>}:$zCaʔ)8r6lW /Yf7Z%uT[%۬~dcP}nџ5W#ב@~>.E?8e{&{UX?mY{qc'Ѩ[T+9/IzuZއ"&V;~JA_x~{m:ny5:Ghq5? <\Y8,Gsa?^OPG: d ~$u:cŵSrLh8)bKږ)h.DA$:W.1.9$:<.:\9cc/k,W]N؉'0n8^/nݺaɒ%?~<amAD f|2E3~ĦNQF]F_hժ/."}c3Q G/"j¤ Q^^;wwɓvv}ʄʯeQxqh.Ffe"b\hƕ?&ta̘1 \.wkjj0j( .Tn~~>}MM=3`f \1rTUUaزe ~iv45d\s QTTCAp^^}6[PQ$0AجPQcT\flp׾LhС)뮻гgO<ðlW_ MӰiӦޅMӠiOG0b32Qh1.S4 %$$O>>eqqqHJJB>}P[[#Fk׮EMM jj|}}.<]]UVFTu!+KJ .K =~ziY^ؚ)5Ixe\bc>SG*լ]O[~QY:YmoRuM@9PMS$H^RUxIXI9%wZ[NKᤕAZ!NZAVnrh-I)ÁXiA5&TY"8mjlSgvmB]Ӷ/_i_#O T^^۷}8p]v è0DDq'ZR{DDQLD]9Mь>WE(n,IDD9)q򇈈(Bqy9Q`\h"";DDDq'Z|̝;3f} !pעEEEj[6U,5; O5J.#KAj!I^U16YJwU*`֑WuZxdc$HQ[=YxHzWDYk5AF:H 2ަI^!y8M I)}ny]~ xHV[no-?_ǚcMn+C`fYevm?_m?mҶL.Ons;_ر=233u/_\:BD#!PL)kND-l⇢2E3N8qahӦ9كe˖^興|cA;"""31:6zDDD8cBƌ3+9sɓq]wxꩧ^xAn^^}qZ۠QK'0Aج㵤QKfT\flp?&T^^* 0[ԄVzw饗oi4wU&.""j/M#q()qDŽ ={?K. ͳ>s92C%""j(8cB ӧOY\\> .\,k֬}alrŢ[nrVIU^G6ڤu16^[sH&{߶lM孬n~K6P/GQI c4+u$ckבiWAF~Fr*cJ+)KOkQmcqꖫӺ$ yY2S5JVܒhJ<Cr'浵E޿Ur/v'uN> )ni*$#OkwS5't˭m$dXoL^Wi`r Vl喔]]iӶLcӶ_g%V~h8cr;vs=L &6mBvn:vmعs'O1916E'efڗ8qƍիѦMc۶môi_\r ϟ֭[㣏> h̏?&6uT5 Æ ;W\رcx<(,,Fnn=ۍP,%"Badlf\&" -2c3eR(//Ν;u_v`"tMf~~>y1)h{c'"3U\͌DDŸLь+LØ1c^y\͟?Ǐ͛sN̞=r ***塺&&9XADDd͌DDDtDŽQUUx˚cժUؿ?VZ{w,_3UI*kOSz׬w\ ?풱4IR&C5Wll423G>h+ɦ iI:wHӶtp-(R3KO薿'Cgzu留<8}Z)EJVnmR僫--SsLq\iO-g Gb˦imMSҶL.Onk?ALь?&>}!)) }ӧɓ''DRR6n܈ӨG6Ct!y06 Lьk[ Áz ۷ѣ?Xf pal&""`ʟݻ_`poscl&".͸򇈈(B ! yQӟ˅_Wΐ̌?DDDDDDDj9s&͛]v+ȑ#7߄{hE8CDD򇈈(rkϲepw7 .R,_:uBAAAΔ̈{E(NE#)4 544s)1bnj… E}}=a?`3csa?Ǭ… 'y .< }ʗ,Y"zђXzpj$&&Zb?Cff1S?f:~O]ʟ#G/֭[-_d ^~e۷/$ǯ}Dvfҧ 7|&""""""@N @IIOyII rrr4*F\CDDDDDDfϞ &`s=oSL (-Z(܃ slͅ9E~1k?DF3߈1ӹc~"E>}K'ĩS/#+++C( L{'L?DDDDDDDD&""""""""EQNDD9"}Q@[`֭bArr2rrr0et)h};Nٳ^zi #͛yWСCaX!C/K.AΏ̸9LD?>#GDN0b$''C*x_Y̞=[|Ŋ?~<˖-kV?rJܹF­ފ_~x<;v,}Qfc&quס}yݻYYYشi.fi&cbŊ\ك>HKK'|gy7x#fqzKǃo;w6`{(r"62.gr!.| ̙3gΜ)~,ׯyX,_B披<"!!AtM"%%E ͡B>63.m?رɁ1S\"r}Igr5K4v'4. !ؽ{lbڵ_Ezz!}9rDVUl61qDM#.0[o%bƍj믿^<oCC!ݻ?#~1j(qQr]vXxBW_}UnZׯ_>}ѣxիE.]ĨQByX,fsUWy|4MdffnM_6|EPPe!BCMlgr`8C~KKK/ / ?=znA%&&F:t{|AkH_'N_~رc()) ǎBs 8 bРA9rJѿf!ڵ+ڵk~bٲes>FϛC|_}ov?;w??? #FCu/B1x`1k,կD#T9qYPe!B3e!> "9Nߞyt:ԩSƍŶmDYYظq:u4M_mm8q۷㏅04~[!.V_=o]Wjjؾ}z1fѯ_??r(#C?/4:u?xDΝӛYwzɓ' HII;wBq1aX|.?C~bccY]SS#Ő!C_mEf\\τB$''s/lD#9qYPe!B3e!> "9NP@ ŠAn~nAW_}U$'' jhP7oh߾o~#D^^ܹ(((>ԩ9wg'>sӧOnAdff?_3gBZXիEݛՇBn1et:j.K\.aZswɴQɓE݅f3}0~x1h vZ1zhq5׈/\|gb߾}bonv?77l :wܬ>dQ:6+. ,Dhb3r``l&"9NP444#G#Glb,7n'N0FxbquyaꫢSN"))Iy睆׷o_aÆs^hvܹ~HJJ'NE||?~Xd8q4M㧪{'֭['֭['{=Q]]mXz3g=樬Æ bȑZw} w]ӦM^ԈAruV(.\(V\)ڵk'ϟ/^y`Ѻuk7!μ}]{w {`Q P`e!BCmlf\Dt.NC=$F{7do/R׿ ޻C䈢fߒ|WB>}ڐ;o֊RCںuɾq˗҇!>Ӡ~~QU\(ˁal&"B;O2̸e8CA> ^>>s1| , J[G"55MMMQb3񨫫CQQ&N,!e0BlNN222 ZB֭}ʷlق"..^T Y"w_q>DB3Cr`f"QKr7飹wBчB$$$nw};?С}^ѹsgW_vUV//+.~n"99qf3̉'_{wHDR(VbV: VA[;}@6VVZdbi#V.bY1F~3wss+ϩ\]]zooI*WLH)˽l>vjv~Č(X[[F漾sh4^])sfgg2fffގZ2맑w_Tq||;;;\s5gqq1vTb\L!?PVRDL1#߱ONNƗj3ncns{qqQ]__(7㤞j _L1G.&\/ Zt:wDQQ?EՊ[[[l6&4͘վ뻻GGGky||vKKK1111J*3eߓl(2/)2Ș c)2Ș c)2Ș c)2Ș c)2Ș c)2wgmTYIENDB`symfit-0.5.4/docs/api_structure.rst000066400000000000000000000125331412237106600174140ustar00rootroot00000000000000Internal API Structure ====================== Here we describe how the code is organized internally. This is only really relevant for advanced users and developers. Fitting 101 ----------- Fitting a model to data is, at it's most basic, a parameter optimisation, and depending on whether you do a least-squares fit or a loglikelihood fit your objective function changes. This means we can split the process of fitting in three distinct, isolated parts: the :class:`~symfit.core.models.Model`, the Objective and the Minimizer. In practice, :class:`~symfit.core.fit.Fit` will choose an appropriate objective and minimizer on the basis of the model and the data, but you can also give it specific instances and classes; just in case you know better. For both the minimizers and objectives there are abstract base classes, which describe the minimal API required. If a minimizer is more specific, e.g. it supports constraints, then there are corresponding abstract classes for that, e.g. :class:`~symfit.core.minimizers.ConstrainedMinimizer`. Models ------ Models house the mathematical definition of the model we want to use to fit. For the typical usecase in :mod:`symfit` these are fully symbolical, and therefore a lot of their properties can be inspected automatically. As a basic quality, all models are callable, i.e. they have implemented ``__call__``. This is used to numerically evaluate the model given the parameters and independent variables. In order to make sure you get all the basic functionality, always inherit from :class:`~symfit.core.models.BaseModel`. Next level up, if they inherit from :class:`~symfit.core.models.GradientModel` then they will have ``eval_jacobian``, which will numerically evaluate the jacobian of the model. Lastly, if they inherit from :class:`~symfit.core.models.HessianModel`, they will also have ``eval_hessian`` to evaluate the hessian of the model. The standard :class:`~symfit.core.models.Model` is all of the above. Odd ones out from the current library are :class:`~symfit.core.models.CallableNumericalModel` and :class:`~symfit.core.models.ODEModel`. They only inherit from :class:`~symfit.core.models.BaseModel` and are therefore callable, but their other behaviors are custom build. Since :mod:`symfit` ``0.5.0``, the core of the model has been improved significantly. At the center of these improvements is :attr:`~symfit.core.models.BaseModel.connectivity_mapping`. This mapping represent the connectivity matrix of the variables and parameters, and therefore encodes which variable depends on which. This is used in ``__call__`` to evaluate the components in order. To help with this, models have :attr:`~symfit.core.models.BaseModel.ordered_symbols`. This property is the topologically sorted ``connectivity_mapping``, and dictates the order in which variables have to be evaluated. Objectives ---------- Objectives wrap both the Model and the data supplied, and expose only the free parameters of the model to the outside world. When called they must return a scalar. This scalar will be *minimized*, so when you need something maximized, be sure to add a negation in the right place(s). They can be called by using the parameter names as keyword arguments, or with a list of parameter values in the same order as :attr:`~symfit.core.models.BaseModel.free_params` (alphabetical). The latter is there because this is how ``scipy`` likes it. Be sure to inherit from the abstract base class(es) so you're sure you define all the methods that are expected of an objective. Similar to the models, they come in three types: :class:`~symfit.core.objectives.BaseObjective`, :class:`~symfit.core.objectives.GradientObjective` and :class:`~symfit.core.objectives.HessianObjective`. These must implement ``__call__``, ``eval_jacobian`` and ``eval_hessian`` respectively. When defining a new objective, it is best to inherit from :class:`~symfit.core.objectives.HessianObjective` and to define all three if possible. When feeding a model that does not implement ``eval_hessian`` to a :class:`~symfit.core.objectives.HessianObjective` no puppies die, :class:`~symfit.core.fit.Fit` is clever enough to prevent this. Minimizers ---------- Last in the chain are the minimizers. They are provided with a function to minimize (the objective) and the :class:`~symfit.core.argument.Parameter` s as a function of which the objective should be minimized. Note that once again there are different base classes for minimizers that take e.g. bounds or support gradients. Their :meth:`~symfit.core.minimizers.BaseMinimizer.execute` method takes the metaparameters for the minimization. Again, be sure to inherit from the appropriate base class(es) if you're implementing your own minimizer to make sure all the expected methods are there. :class:`~symfit.core.fit.Fit` depends on this to make its decisions. And if you're wrapping Scipy style minimizers, have a look at :class:`~symfit.core.minimizers.ScipyMinimize` to avoid a duplication of efforts. Minimizers must always implement a method ``execute``, which will return an instance of :class:`~symfit.core.fit_results.FitResults`. Any ``*args`` and ``**kwargs`` given to execute must be passed to the underlying minimizer. Fit --- :class:`~symfit.core.fit.Fit` is responsible for stringing all of the above together intelligently. When not coached into the right direction, it will decide which minimizer and objective to use on the basis of the model and data.symfit-0.5.4/docs/conf.py000066400000000000000000000225201412237106600152650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # symfit documentation build configuration file, created by # sphinx-quickstart on Thu Nov 27 13:46:41 2014. # # 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 symfit # 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.join(os.path.abspath('.'), os.pardir)) # -- 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.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'nbsphinx', ] # Contains the locations and names of other projects that should be # linked to in this documentation. intersphinx_mapping = { 'sympy': ('https://docs.sympy.org/latest', None), 'python': ('https://docs.python.org/3', None), 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 'matplotlib': ('https://matplotlib.org', None) } # List of targets that should be ignored when generating warnings. nitpick_ignore = [ ('py:mod', 'symfit'), ('py:mod', 'symfit.contrib.interactive_guess'), ('py:mod', 'scipy'), ('py:mod', 'matplotlib'), ('py:mod', 'sympy'), ('py:class', 'sympy.core.relational.Relational'), ('py:func', 'symfit.core.leastsqbound.leastsqbound') ] # Always re-execute notebooks when building the docs nbsphinx_execute = 'always' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. 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 = u'symfit' copyright = u'2014, tBuLi' # 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 = symfit.__version__ # The full version, including alpha/beta/rc tags. release = symfit.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #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 = ['docs_build'] # 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 = True # 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 # -- 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 = 'default' # 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 = [] # The name for this set of Sphinx documents. If None, it defaults to # " 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, the reST sources are included in the HTML build. The default is True. html_copy_source = 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 # Output file base name for HTML help builder. htmlhelp_basename = 'symfitdoc' # The path to the MathJax JavaScript file to include in the HTML files. #mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML' # The path to the RequireJS JavaScript file to include in the HTML files. #nbsphinx_requirejs_path = '/usr/share/javascript/requirejs/require.min.js' # -- 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': '', } # 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 = [ ('index', 'symfit.tex', u'symfit Documentation', u'tBuLi', '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 = [ ('index', 'symfit', u'symfit Documentation', [u'tBuLi'], 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 = [ ('index', 'symfit', u'symfit Documentation', u'tBuLi', 'symfit', '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 symfit-0.5.4/docs/dependencies.rst000066400000000000000000000022611412237106600171460ustar00rootroot00000000000000Dependencies and Credits ======================== Always pay credit where credit's due. ``symfit`` uses the following projects to make it's sexy interface possible: - `leastsqbound-scipy `_ is used to bound parameters to a given domain. - `seaborn `_ was used to make the beautifully styled plots in the example code. All you have to do to sexify your matplotlib plot's is import seaborn, even if you don't use it's special plotting facilities, so I highly recommend it. - `numpy and scipy `_ are of course used to do efficient data processing. - `sympy `_ is used for the manipulation of the symbolic expressions that give this project it's high readability. .. the seaborn images in this documentation were made with the settings that can be found in the gaussian example:: import matplotlib.pyplot as plt import seaborn as sns palette = sns.color_palette() sns.regplot(xdata, ydata, label='data', fit_reg=False) plt.plot(xdata, model(xdata, **fit_results), label='fit', color=palette[2]) plt.legend() plt.show() symfit-0.5.4/docs/examples/000077500000000000000000000000001412237106600156035ustar00rootroot00000000000000symfit-0.5.4/docs/examples/ex_CallableNumericalModel.rst000066400000000000000000000014171412237106600233540ustar00rootroot00000000000000Example: Piecewise model using CallableNumericalModel ===================================================== Below is an example of how to use the :class:`symfit.core.models.CallableNumericalModel`. This class allows you to provide custom callables as your model, while still allowing clean interfacing with the :mod:`symfit` API. These models also accept a mixture of symbolic and callable components, as will be demonstrated below. This allows the power-user great flexibility, since it is still easy to interface with :mod:`symfit`'s constraints, minimizers, etc. .. literalinclude:: ../../examples/callable_numerical_model.py :language: python This is the resulting fit: .. figure:: ../_static/callable_numerical_model.png :width: 500px :alt: Custom Callable Model symfit-0.5.4/docs/examples/ex_CallableNumericalModel_ode.ipynb000066400000000000000000000055171412237106600245210ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Example: ODEModels as subproblems using CallableNumericalModel\n", "\n", "Imagine that you want to use ODE fitting, but your data aren't\n", "directly linked to an ODE variable. In this case, you can use the\n", "class :class:`symfit.core.models.CallableNumericalModel`.\n", "\n", "Given are the variables ``x``, ``y`` and ``z``. ``x`` and ``y`` and the parameter ``a`` form an ODE model, where the derivative contains (due to the e-function) ``y``. ``z`` is the result of a function with ``y`` and the additional parameter ``b``. \n", "\n", "``a`` and ``b`` must be optimized, as shown below. The class uses the given model dictionary and the information about the variable and the parameters to fit the data. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "from symfit import variables, Parameter, Fit, D, ODEModel, CallableNumericalModel\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "# Create data\n", "x_data = np.linspace(0.0, 10.0, 1000)\n", "a_expected = 0.6\n", "b_expected = 10.0\n", "z_data = 2 * np.exp(a_expected * x_data) + b_expected\n", "\n", "# Initialise variables and parameters\n", "x, y, z = variables('x, y, z')\n", "a = Parameter('a', 0.0)\n", "b = Parameter('b', 0.0)\n", "\n", "# Define model\n", "ode_model = ODEModel({D(y, x): a * y}, initial={x: 0.0, y: 1.0})\n", "model_dict = {\n", " z: 2 * y + b,\n", " y: lambda x, a: ode_model(x=x, a=a).y,\n", "}\n", "model = CallableNumericalModel(model_dict, connectivity_mapping={z: {y, b}, y: {x, a}})\n", "\n", "# Apply model\n", "fit = Fit(model, x=x_data, z=z_data)\n", "fit_result = fit.execute()" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a = 0.5999999793326405\n" ] } ], "source": [ "print('a =', fit_result.value(a))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "b = 10.000011616374367\n" ] } ], "source": [ "print('b =', fit_result.value(b))" ] } ], "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.3" } }, "nbformat": 4, "nbformat_minor": 2 } symfit-0.5.4/docs/examples/ex_ODEModel.rst000066400000000000000000000011621412237106600204210ustar00rootroot00000000000000Example: ODEModel for Reaction Kinetics ======================================= Below is an example of how to use the :class:`symfit.core.models.ODEModel`. In this example we will fit reaction kinetics data, taken from libretexts_. The data is from a first-order reaction :math:`\text{A} \rightarrow \text{B}`. .. literalinclude:: ../../examples/ode_reaction_kinetics_simple.py :language: python This is the resulting fit: .. figure:: ../_static/ode_kinetics_fit.png :width: 500px :alt: ODE Fit to kinetics data .. _libretexts: http://chem.libretexts.org/Core/Physical_Chemistry/Kinetics/Rate_Laws/The_Rate_Lawsymfit-0.5.4/docs/examples/ex_bivariate_likelihood.ipynb000066400000000000000000000672771412237106600235360ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "source": "# Example: Likelihood fitting a Bivariate Gaussian\n\nIn this example, we shall perform likelihood fitting to a [bivariate normal\ndistribution](http://mathworld.wolfram.com/BivariateNormalDistribution.html),\nto demonstrate how ``symfit``\u0027s API can easily be used to perform likelihood\nfitting on multivariate problems.\n\nIn this example, we sample from a bivariate normal distribution with a\nsignificant correlation of $\\rho \u003d 0.6$ between $x$ and $y$.\nWe see that this is extracted from the data relatively straightforwardly.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 13, "outputs": [], "source": "import numpy as np\nfrom symfit import Variable, Parameter, Fit\nfrom symfit.core.objectives import LogLikelihood\nfrom symfit.distributions import BivariateGaussian\nimport matplotlib.pyplot as plt\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "Build a model corresponding to a bivariate normal distribution.", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 14, "outputs": [], "source": "x \u003d Variable(\u0027x\u0027)\ny \u003d Variable(\u0027y\u0027)\nx0 \u003d Parameter(\u0027x0\u0027, value\u003d0.6, min\u003d0.5, max\u003d0.7)\nsig_x \u003d Parameter(\u0027sig_x\u0027, value\u003d0.1, max\u003d1.0)\ny0 \u003d Parameter(\u0027y0\u0027, value\u003d0.7, min\u003d0.6, max\u003d0.9)\nsig_y \u003d Parameter(\u0027sig_y\u0027, value\u003d0.05, max\u003d1.0)\nrho \u003d Parameter(\u0027rho\u0027, value\u003d0.001, min\u003d-1, max\u003d1)\n\npdf \u003d BivariateGaussian(x\u003dx, mu_x\u003dx0, sig_x\u003dsig_x, y\u003dy, mu_y\u003dy0,\n sig_y\u003dsig_y, rho\u003drho)\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "Generate mock data\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 15, "outputs": [], "source": "# Draw 100000 samples from a bivariate distribution\nmean \u003d [0.59, 0.8]\ncorr \u003d 0.6\ncov \u003d np.array([[0.11 ** 2, 0.11 * 0.23 * corr],\n [0.11 * 0.23 * corr, 0.23 ** 2]])\nnp.random.seed(42)\nxdata, ydata \u003d np.random.multivariate_normal(mean, cov, 100000).T\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "code", "execution_count": 16, "outputs": [ { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\u003d\u003d\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "hist \u003d plt.hist2d(xdata, ydata, bins\u003d(100, 100))\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "Finally, we perform the fit to this mock data using the ``LogLikelihood`` objective.", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 17, "outputs": [ { "name": "stdout", "text": [ "\nParameter Value Standard Deviation\nrho 6.026420e-01 2.013810e-03\nsig_x 1.100898e-01 2.461684e-04\nsig_y 2.303400e-01 5.150556e-04\nx0 5.901317e-01 3.481346e-04\ny0 8.014040e-01 7.283990e-04\nStatus message b\u0027CONVERGENCE: REL_REDUCTION_OF_F_\u003c\u003d_FACTR*EPSMCH\u0027\nNumber of iterations 22\nObjective \u003csymfit.core.objectives.LogLikelihood object at 0x000001DA8EB8BF28\u003e\nMinimizer \u003csymfit.core.minimizers.LBFGSB object at 0x000001DA8EB6E7B8\u003e\n\nGoodness of fit qualifiers:\nlikelihood inf\nlog_likelihood 106241.24669486462\nobjective_value -106241.24669486462\n" ], "output_type": "stream" } ], "source": "fit \u003d Fit(pdf, x\u003dxdata, y\u003dydata, objective\u003dLogLikelihood)\nfit_result \u003d fit.execute()\nprint(fit_result)", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "We see that this result is in agreement with our data.", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } } ], "metadata": { "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" }, "kernelspec": { "name": "python3", "language": "python", "display_name": "Python 3" }, "stem_cell": { "cell_type": "raw", "source": "", "metadata": { "pycharm": { "metadata": false } } } }, "nbformat": 4, "nbformat_minor": 0 }symfit-0.5.4/docs/examples/ex_fourier_series.rst000066400000000000000000000024711412237106600220620ustar00rootroot00000000000000Example: Fourier Series ======================= Suppose we want to fit a Fourier series to a dataset. As an example, let's take a step function: .. math:: f(x) = \begin{cases} 0 & \text{if}\quad - \pi < x \leq 0 \\ 1 & \text{if}\quad 0 < x < \pi \end{cases} In the example below, we will attempt to fit this with a Fourier Series of order :math:`n=3`. .. math:: y(x) = a_0 + \sum_{i=1}^n a_i cos(i \omega x) + \sum_{i=1}^n b_i sin(i \omega x) .. literalinclude:: ../../examples/fourier_series.py :language: python This code prints:: {y: a0 + a1*cos(w*x) + a2*cos(2*w*x) + a3*cos(3*w*x) + b1*sin(w*x) + b2*sin(2*w*x) + b3*sin(3*w*x)} Parameter Value Standard Deviation a0 5.000000e-01 2.075395e-02 a1 -4.903805e-12 3.277426e-02 a2 5.325068e-12 3.197889e-02 a3 -4.857033e-12 3.080979e-02 b1 6.267589e-01 2.546980e-02 b2 1.986491e-02 2.637273e-02 b3 1.846406e-01 2.725019e-02 w 8.671471e-01 3.132108e-02 Fitting status message: Optimization terminated successfully. Number of iterations: 44 Regression Coefficient: 0.9401712713086535 .. figure:: ../_static/fourier_series.png :width: 500px :alt: Fourier series fit to a step functionsymfit-0.5.4/docs/examples/ex_interactive_guesses_ODE.rst000066400000000000000000000012021412237106600235660ustar00rootroot00000000000000Example: Interactive Guesses ODE ================================ Below is an example in which the initial guesses module is used to help solve an ODE problem. .. literalinclude:: ../../symfit/contrib/interactive_guess/examples/ODE_example.py :language: python This is a screenshot of the interactive guess window: .. figure:: ../_static/interactive_guess_ODE.png :width: 500px :alt: Fourier series fit to a step function By using the sliders, you can interactively play with the initial guesses until it is close enough. Then after closing the window, this initial value is set for the parameter, and the fit can be performed.symfit-0.5.4/docs/examples/ex_interactive_guesses_nD.rst000066400000000000000000000023441412237106600235300ustar00rootroot00000000000000Example: Interactive Guesses in N dimensions ============================================ Below is an example in which the initial guesses module is used to help fit a function that depends on more than one independent variable: .. literalinclude:: ../../symfit/contrib/interactive_guess/examples/simple_3D_example.py :language: python This is a screenshot of the interactive guess window: .. figure:: ../_static/interactive_guess_simple_3D.png :width: 500px In the window you can see the range the provided data spans as a contourplot on the background. The evaluated models is shown as red lines. By default your proposed model is evaluated at :math:`50^n` points for :math:`n` independent variables, with 50 points per dimension. So in the example this is at 50 values of ``x`` and 50 values of ``y``. The error bars on the points plotted are taken from the spread in ``z`` that comes from the spread in data in the other dimensions (``y`` and ``x`` respectively). The error bars correspond (by default) to the 90% percentile. By using the sliders, you can interactively play with the initial guesses until it is close enough. Then after closing the window, this initial values are set for the parameters, and the fit can be performed. symfit-0.5.4/docs/examples/ex_interactive_guesses_vector_2D.rst000066400000000000000000000012701412237106600250130ustar00rootroot00000000000000Example: Interactive Guesses Vector Model ========================================= Below is an example in which the initial guesses module is used to help solve two-component vector valued function: .. literalinclude:: ../../symfit/contrib/interactive_guess/examples/vector_valued_2D.py :language: python This is a screenshot of the interactive guess window: .. figure:: ../_static/interactive_guess_vector_2D.png :width: 500px :alt: Fourier series fit to a step function By using the sliders, you can interactively play with the initial guesses until it is close enough. Then after closing the window, this initial values are set for the parameters, and the fit can be performed.symfit-0.5.4/docs/examples/ex_mexican_hat.ipynb000066400000000000000000000577261412237106600216430ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "source": "# Global minimization: Skewed Mexican hat\n\nIn this example we will demonstrate the ease of performing global minimization using ``symfit``. In order to do this we will have a look at a simple skewed mexican hat potential, which has a local minimum and a global minimum. We will then use ``DifferentialEvolution`` to find the global minimum. ", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 1, "metadata": { "pycharm": { "is_executing": false } }, "outputs": [], "source": "from symfit import Parameter, Variable, Model, Fit, solve, diff, N, re\nfrom symfit.core.minimizers import DifferentialEvolution, BFGS\nimport numpy as np\nimport matplotlib.pyplot as plt\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } }, "source": "First we define a model for the skewed mexican hat.\n" }, { "cell_type": "code", "execution_count": 2, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "[y(; x) \u003d x**4 - 10*x**2 + 5*x]\n" ], "output_type": "stream" } ], "source": "x \u003d Parameter(\u0027x\u0027)\nx.min, x.max \u003d -100, 100\ny \u003d Variable(\u0027y\u0027)\nmodel \u003d Model({y: x**4 - 10 * x**2 + 5 * x}) # Skewed Mexican hat\nprint(model)\n" }, { "cell_type": "markdown", "source": "Let us visualize what this potential looks like.\n", "metadata": { "pycharm": { "metadata": false } } }, { "cell_type": "code", "execution_count": 16, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "\u003cmatplotlib.legend.Legend at 0x225cf47ab00\u003e" }, "metadata": {}, "output_type": "execute_result", "execution_count": 16 }, { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "xdata \u003d np.linspace(-4, 4, 201)\nydata \u003d model(x\u003dxdata).y\n\nplt.axhline(0, color\u003d\u0027black\u0027)\nplt.axvline(0, color\u003d\u0027black\u0027)\nplt.plot(xdata, ydata, label\u003dr\u0027$f(x)$\u0027)\nplt.xlabel(\u0027x\u0027)\nplt.ylabel(\u0027f(x)\u0027)\nplt.ylim(1.1 * ydata.min(), 1.1 * ydata.max())\nplt.legend()\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "metadata": false } }, "source": "Using ``sympy``, it is easy to solve the solution analytically, by finding the places where the gradient is zero. \n" }, { "cell_type": "code", "execution_count": 17, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "[0.253248404857807, 2.09866205647752, -2.35191046133532]" }, "metadata": {}, "output_type": "execute_result", "execution_count": 17 } ], "source": "sol \u003d solve(diff(model[y], x), x)\n# Give numerical value\nsol \u003d [re(N(s)) for s in sol]\nsol" }, { "cell_type": "markdown", "source": "Without providing any initial guesses, ``symfit`` finds the local minimum. This is because the initial guess is set to ``1`` by default.", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 18, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "exact value 2.09866205647752\nnum value 2.09866205722533\n" ], "output_type": "stream" } ], "source": "fit \u003d Fit(model)\nfit_result \u003d fit.execute()\nprint(\u0027exact value\u0027, sol[1])\nprint(\u0027num value \u0027, fit_result.value(x))\n " }, { "cell_type": "markdown", "source": "Let\u0027s use ``DifferentialEvolution`` instead.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 19, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "exact value -2.35191046133532\nnum value -2.3500650028722148\n" ], "output_type": "stream" } ], "source": "fit \u003d Fit(model, minimizer\u003dDifferentialEvolution)\nfit_result \u003d fit.execute()\nprint(\u0027exact value\u0027, sol[2])\nprint(\u0027num value \u0027, fit_result.value(x))\n" }, { "cell_type": "markdown", "source": "Using ``DifferentialEvolution``, we find the correct global minimum. However, it is not exactly the same as the analytical solution. This is because ``DifferentialEvolution`` is expensive to perform, and therefore does not solve to high precision by default. We could demand a higher precission from ``DifferentialEvolution``, but this isn\u0027t worth the high computational cost. Instead, we will just tell ``symfit`` to perform ``DifferentialEvolution``, followed by ``BFGS``.\n", "metadata": { "pycharm": { "metadata": false } } }, { "cell_type": "code", "execution_count": 20, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "exact value -2.35191046133532\nnum value -2.351910461335324\n" ], "output_type": "stream" } ], "source": "fit \u003d Fit(model, minimizer\u003d[DifferentialEvolution, BFGS])\nfit_result \u003d fit.execute()\nprint(\u0027exact value\u0027, sol[2])\nprint(\u0027num value \u0027, fit_result.value(x))" }, { "cell_type": "markdown", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } }, "source": "We see that now the proper solution has been found to much higher precision.\n" } ], "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.1" }, "stem_cell": { "cell_type": "raw", "source": "", "metadata": { "pycharm": { "metadata": false } } } }, "nbformat": 4, "nbformat_minor": 1 }symfit-0.5.4/docs/examples/ex_multidataset_likelihood.rst000066400000000000000000000007771412237106600237470ustar00rootroot00000000000000Example: Global Likelihood fitting ================================== In this example, we shall perform likelihood fitting to two data sets at the same time to show how likelihood fitting can be used to fit multiple data sets at the same time. First we will fit using a different parameter for each data set, and then with the same for both data sets. This shows the ease with which we can experiment with different models. .. literalinclude:: ../../examples/multidataset_likelihood.py :language: python symfit-0.5.4/docs/examples/ex_ode_system.ipynb000066400000000000000000001366061412237106600215310ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "source": "# Example: Multiple species Reaction Kinetics using ODEModel\nIn this example we shall fit to a complex system of ODEs, based on that published by [Polgar et al](https://doi.org/10.1016/j.eurpolymj.2017.03.020). However, we shall be generating some mock data instead of using the real deal. ", "metadata": { "pycharm": { "metadata": false } } }, { "cell_type": "code", "execution_count": 1, "metadata": { "pycharm": { "is_executing": false } }, "outputs": [], "source": "from symfit import (\n\tvariables, parameters, ODEModel, D, Fit\n) \nfrom symfit.core.support import key2str\nimport numpy as np\nimport matplotlib.pyplot as plt\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "First we build a model representing the system of equations.\n" }, { "cell_type": "code", "execution_count": 2, "outputs": [ { "name": "stdout", "text": [ "Derivative(F, t; k1_f, k1_r, k2_f, k2_r) \u003d -k1_f*F*MM + k1_r*FMM - k2_f*F*FMM + k2_r*FMMF\nDerivative(FMM, t; k1_f, k1_r, k2_f, k2_r) \u003d k1_f*F*MM - k1_r*FMM + k2_f*FMMF - k2_r*F*FMM\nDerivative(FMMF, t; k1_f, k1_r, k2_f, k2_r) \u003d k2_f*F*FMM - k2_r*FMMF\nDerivative(MM, t; k1_f, k1_r, k2_f, k2_r) \u003d -k1_f*F*MM + k1_r*FMM\n" ], "output_type": "stream" } ], "source": "t, F, MM, FMM, FMMF \u003d variables(\u0027t, F, MM, FMM, FMMF\u0027)\nk1_f, k1_r, k2_f, k2_r \u003d parameters(\u0027k1_f, k1_r, k2_f, k2_r\u0027)\n\nMM_0 \u003d 10 # Some made up initial amount of [FF]\n\nmodel_dict \u003d {\n D(F, t): - k1_f * MM * F + k1_r * FMM - k2_f * FMM * F + k2_r * FMMF,\n\tD(FMM, t): k1_f * MM * F - k1_r * FMM - k2_r * FMM * F + k2_f * FMMF,\n D(FMMF, t): k2_f * FMM * F - k2_r * FMMF,\n D(MM, t): - k1_f * MM * F + k1_r * FMM,\n}\nmodel \u003d ODEModel(\n model_dict, \n initial\u003d{t: 0.0, MM: MM_0, FMM: 0.0, FMMF: 0.0, F: 2 * MM_0}\n)\nprint(model)", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "Generate mock data.\n" }, { "cell_type": "code", "execution_count": 3, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "tdata \u003d np.linspace(0, 3, 20)\n", "data \u003d model(t\u003dtdata, k1_f\u003d0.1, k1_r\u003d0.2, k2_f\u003d0.3, k2_r\u003d0.3)._asdict()\n", "sigma_data \u003d 0.3\n", "np.random.seed(42)\n", "for var in data:\n", "\tdata[var] +\u003d np.random.normal(0, sigma_data, size\u003dlen(tdata))\n", "\n", "plt.scatter(tdata, data[MM], label\u003d\u0027[MM]\u0027)\n", "plt.scatter(tdata, data[FMM], label\u003d\u0027[FMM]\u0027)\n", "plt.scatter(tdata, data[FMMF], label\u003d\u0027[FMMF]\u0027)\n", "plt.scatter(tdata, data[F], label\u003d\u0027[F]\u0027)\n", "plt.xlabel(r\u0027$t$\u0027)\n", "plt.ylabel(r\u0027$C$\u0027)\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "Perform the fit. Let\u0027s pretend that for experimental reasons, we can only measure the concentration for ``MM`` and ``F``, but not for the intermediate `FMM` nor the product `FMMF`. This is no problem, as we can tell ``symfit`` to ignore those components by setting the data for them to `None`." }, { "cell_type": "code", "execution_count": 4, "outputs": [ { "name": "stdout", "text": [ "\nParameter Value Standard Deviation\nk1_f 9.540426e-02 4.440700e-03\nk1_r 1.065130e-01 7.165679e-02\nk2_f 2.706117e-01 5.305035e-02\nk2_r 2.633613e-01 5.647220e-02\nStatus message b\u0027CONVERGENCE: REL_REDUCTION_OF_F_\u003c\u003d_FACTR*EPSMCH\u0027\nNumber of iterations 30\nObjective \u003csymfit.core.objectives.LeastSquares object at 0x00000262DBB69A20\u003e\nMinimizer \u003csymfit.core.minimizers.LBFGSB object at 0x00000262F8938CC0\u003e\n\nGoodness of fit qualifiers:\nchi_squared 33.98549455298767\nobjective_value 16.992747276493834\nr_squared 0.9936568366360846\n" ], "output_type": "stream" } ], "source": "k1_f.min, k1_f.max \u003d 0, 1\nk1_r.min, k1_r.max \u003d 0, 1\nk2_f.min, k2_f.max \u003d 0, 1\nk2_r.min, k2_r.max \u003d 0, 1\n\nfit \u003d Fit(model, t\u003dtdata, MM\u003ddata[MM], F\u003ddata[F],\n FMMF\u003dNone, FMM\u003dNone,\n sigma_F\u003dsigma_data, sigma_MM\u003dsigma_data)\nfit_result \u003d fit.execute()\nprint(fit_result)", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "code", "execution_count": 5, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\u003d\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "taxis \u003d np.linspace(tdata.min(), tdata.max(), 1000)\nmodel_fit \u003d model(t\u003dtaxis, **fit_result.params)._asdict()\nfor var in data:\n\tplt.scatter(tdata, data[var], label\u003d\u0027[{}]\u0027.format(var.name))\n\tplt.plot(taxis, model_fit[var], label\u003d\u0027[{}]\u0027.format(var.name))\nplt.legend()\nplt.show()" }, { "cell_type": "markdown", "source": "We see that the lack of data for some components is not a problem, they are predicted quite nicely.\n\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } } ], "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.1" }, "stem_cell": { "cell_type": "raw", "source": "", "metadata": { "pycharm": { "metadata": false } } } }, "nbformat": 4, "nbformat_minor": 1 }symfit-0.5.4/docs/examples/ex_piecewise.rst000066400000000000000000000035461412237106600210160ustar00rootroot00000000000000Example: Piecewise continuous function ====================================== Piecewise continuus functions can be tricky to fit. However, using the :mod:`symfit` interface this process is made a lot easier. Suppose we want to fit to the following model: .. math:: f(x) = \begin{cases} x^2 - a x & \text{if}\quad x \leq x_0 \\ a x + b & \text{otherwise} \end{cases} The script below gives an example of how to fit such a model: .. literalinclude:: ../../examples/piecewise2.py :language: python This code prints:: Parameter Value Standard Deviation a -4.780338e-02 None b 1.205443e+00 None x0 1.051163e+00 None Fitting status message: Optimization terminated successfully. Number of iterations: 18 Regression Coefficient: 0.9849188499599985 .. figure:: ../_static/piecewise_continuous.png :width: 500px :alt: Continuous piecewise fit Judging from this graph, another possible solution would have been to also demand a continuous derivative at the point `x0`. This can be achieved by setting the following constraints instead:: constraints = [ Eq(y1.diff(x).subs({x: x0}), y2.diff(x).subs({x: x0})), Eq(y1.subs({x: x0}), y2.subs({x: x0})) ] This gives the following plot: .. figure:: ../_static/piecewise_differentiable.png :width: 500px :alt: Differentiable fit to a piecewise function and the following fit report:: Parameter Value Standard Deviation a 8.000000e-01 None b -6.400000e-01 None x0 8.000000e-01 None Fitting status message: Optimization terminated successfully. Number of iterations: 3 Regression Coefficient: 0.8558226069368662 The first fit is therefore the prevered one, but it does show you how easy it is to set these constraints using :mod:`symfit`.symfit-0.5.4/docs/examples/ex_poly_surface_fit.rst000066400000000000000000000013761412237106600223750ustar00rootroot00000000000000Example: Polynomial Surface Fit =============================== In this example, we want to fit a polynomial to a 2D surface. Suppose the surface is described by .. math:: f(x) = x^2 + y^2 + 2 x y A fit to such data can be performed as follows: .. literalinclude:: ../../examples/poly_surface_fit.py :language: python This code prints:: z(x, y; c1, c2) = c1*x**2 + c1*y**2 + c2*x*y Parameter Value Standard Deviation c1 9.973489e-01 1.203071e-03 c2 1.996901e+00 3.736484e-03 Fitting status message: Optimization terminated successfully. Number of iterations: 6 Regression Coefficient: 0.9952824293713467 .. figure:: ../_static/polynomial_surface_fit.png :width: 500px :alt: Polynomial surface fitsymfit-0.5.4/docs/examples/ex_tikhonov.ipynb000066400000000000000000001431111412237106600212040ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "source": "# Example: Matrix Equations using Tikhonov Regularization\nThis is an example of the use of matrix expressions in ``symfit`` models. This is illustrated by performing an inverse Laplace transform using Tikhonov regularization, but this could be adapted to other problems involving matrix quantities.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 1, "metadata": { "pycharm": { "is_executing": false } }, "outputs": [], "source": "from symfit import (\n\tvariables, parameters, Model, Fit, exp, laplace_transform, symbols, \n\tMatrixSymbol, sqrt, Inverse, CallableModel\n) \nimport numpy as np\nimport matplotlib.pyplot as plt\n" }, { "cell_type": "markdown", "source": "Say $f(t) \u003d t * exp(- t)$, and $F(s)$ is the Laplace transform of $f(t)$. Let us first evaluate this transform using ``sympy``. \n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 2, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "[F(s; ) \u003d (s + 1)**(-2)]\n" ], "output_type": "stream" } ], "source": "t, f, s, F \u003d variables(\u0027t, f, s, F\u0027)\nmodel \u003d Model({f: t * exp(- t)})\nlaplace_model \u003d Model(\n\t{F: laplace_transform(model[f], t, s, noconds\u003dTrue)}\n)\nprint(laplace_model)\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "Suppose we are confronted with a dataset $F(s)$, but we need to know $f(t)$. This means an inverse Laplace transform has to be performed. However, numerically this operation is ill-defined. In order to solve this, Tikhonov regularization can be performed.\n\nTo demonstrate this, we first generate mock data corresponding to $F(s)$ and will then try to find (our secretly known) $f(t)$." }, { "cell_type": "code", "execution_count": 6, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "\u003cmatplotlib.legend.Legend at 0x2b51a9f8e80\u003e" }, "metadata": {}, "output_type": "execute_result", "execution_count": 6 }, { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYkAAAEMCAYAAAAxoErWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAFTxJREFUeJzt3X2QXuV53/HvDyFFgEkgaB1cFpAMCkWmVLJ3HFMGg4PMAG0RdVODYzsyxcgzDk7aqu2QpuN4iBmHxCUtU+xITSk4xrwYM60mocWF4IIxeLTYioRENGhkbDbQImQDqpGsl179Y1ea1eo50kraPc++fD8zGvac597zXM8i+O11Xu47VYUkSZ0c0+0CJEkTlyEhSWpkSEiSGhkSkqRGhoQkqZEhIUlqZEhIkhoZEpKkRoaEJKmRISFJanRstws4WnPmzKm5c+d2uwxJmjSeffbZ16qqZzRjJ31IzJ07l/7+/m6XIUmTRpIfjnasp5skSY0MCUlSI0NCktRo0l+TkDQ17Nq1i4GBAXbs2NHtUqaM2bNn09vby8yZM4/4GIaEpAlhYGCAE088kblz55Kk2+VMelXF1q1bGRgYYN68eUd8nNZONyW5M8mrSZ5reD1Jbk+yKcnaJO9uqzZJ3bdjxw5OOeUUA2KMJOGUU0456s6szWsSdwGXH+T1K4D5Q3+WAV9uoSZJE4gBMbbG4ufZWkhU1RPAjw8yZAnwlRr0DHBSkncc6ribt/x0rEqUJI0wke5uOg14adj2wNA+SeromhVPc82Kp7tdxpQ2kUKiU19UHQcmy5L0J+nftWvXOJclabp78cUXOe6441i4cOF++5cvX86CBQu44YYbWLhwIbNmzeK11147ovdYsWIF73jHO1i4cCELFy7k4x//OADbt2/n4osvZs+ePR2/b+fOnbz//e9n9+7dR/S+hzKR7m4aAE4ftt0LvNxpYFWtBFYC/OKZ53YMEkk6Gvfffz+PPfYYZ599Nh/+8Ic566yzWLNmzb7XN2/ezFNPPcWGDRv27TuaeeTWrl3L5z//ea6//vr99t9555186EMfYsaMGR2/b9asWVx66aXcf//9fPSjHz3i928ykTqJVcBvDN3l9D7gjap65VDf9M6eE8a/MknTyu23387GjRu57rrreOihhw54fePGjVx88cX88Ic/ZNGiRfz0p0d/bXTdunUHdCoA99xzD0uWLNm3fffdd/Oe97yH888/n4suugiAq6++mnvuueeoa+iktU4iyb3AJcCcJAPA7wEzAarqT4CHgSuBTcBbwHVt1SZJe7311lvcfPPNvPDCC+zevZuvfOUrB4w555xzWLp0KXPnzuWTn/zkQY930UUXsW3btgP2f/GLX2Tx4sX7ttevX891113HMcccw5w5c3j00UfZuXMnmzdv3tehbNu2jVtvvZU1a9Ywa9YsXn/9dQDOO+88Vq9efRSfullrIVFVHznE6wX8ZkvlSFJHjz/+OGeeeSYnn3wyAD09Pbz44osHjFu3bt1+v+E3efLJJw855qWXXuLUU09l7dq1++1/7bXXOOmkk/Ztz5gxg+3bt7N8+XKWLl1KX1/fvv2zZs1i27ZtnHjiiYd8v8MxkU43SVLXPfHEE1x22WWHHLd+/Xre9a53HXLcRRddtO9i9PA/jz766L4xa9eu7Xis4447br+H4Y4//niee+45LrzwQpYtW8aXvvSlfa/97Gc/Y/bs2Yes53BNpAvXktR13/nOd1i+fDlbtmyhp6eHN998k7feemu/Mdu2bWPmzJkcf/zxhzzeaDqJdevWdQyJk08+mT179rBjxw5mz57NCy+8wPz587n22mvZsGHDvgDZunUrPT09RzVHUxM7CUka8q1vfYuZM2dyyy23cNZZZ3HFFVfw1a9+9YDf0J977jnOO++8MXvfdevWsWDBgo6vXXbZZXz7298G4JZbbuGcc87h3e9+Nz/4wQ/49Kc/DQyeIrvyyivHrJ7h7CQkacgll1zCJZdccsD+kdckLrjgAr7+9a+P2fse7M6kG2+8kdtuu43Fixdz1113dRzzta99jS984QtjVs9whoSkSev+T13QyvvMmDGDN954g4ULF+73rMRe27dv54ILLmDXrl0cc8zYnqBZtGgRH/jAB9izZ0/HZyV27tzJ1VdfzTnnnDOm77tXBm8qmrz6+vrKNa6lye/555/n3HPP7XYZU06nn2uSZ6uqbzTf7zUJSVIjQ0KS1MiQkDRhTPbT3xPNWPw8DQlJE8Ls2bPZunWrQTFG9i5ferQP2Hl3k6QJobe3l4GBAbZs2dLtUqaM2bNn09vbe1THMCQkTQgzZ85k3rx53S5DI3i6SZLUyJCQJDUyJCRJjQwJSVIjQ0KS1MiQkCQ1MiQkSY0MCUlSI0NCktTIkJAkNTIkJEmNDAlJUiNDQpLUyJCQJDUyJCRJjaZMSFyz4mmuWfF0t8uQpCllyoSEJGnsGRKSpEathkSSy5NsTLIpyU0dXj8jyeNJvp9kbZIr26xPkrS/1ta4TjIDuAP4IDAArE6yqqo2DBv2b4EHqurLSRYADwNzR3P8+z91wRhXLElqs5N4L7CpqjZX1U7gPmDJiDEF/PzQ178AvNxifZKkEVrrJIDTgJeGbQ8AvzJizOeAbyb5DHACsLid0iRJnbTZSaTDvhqx/RHgrqrqBa4E/izJATUmWZakP0n/li1bxqFUSRK0GxIDwOnDtns58HTS9cADAFX1NDAbmDPyQFW1sqr6qqqvp6dnnMqVJLUZEquB+UnmJZkFXAusGjHmR8ClAEnOZTAkbBUkqUtaC4mq2g3cCDwCPM/gXUzrk9yc5KqhYcuBG5L8FXAv8ImqGnlKSpLUkjYvXFNVDzN4W+vwfZ8d9vUG4MI2a5IkNfOJa0lSI0NCktTIkJAkNTIkJEmNDAlJUiNDQpLUyJCQJDUyJCRJjQwJSVIjQ0KS1MiQkCQ1MiQkSY0MCUlSI0NCktTIkJAkNTIkJEmNDAlJUiNDQpLUaEqGxDUrnuaaFU93uwxJmvSmZEhIksbGsd0uYDzc/6kLul2CJE0JdhKSpEaGhCSpkSEhSWpkSEiSGhkSkqRGhoQkqZEhIUlqZEhIkhoZEpKkRoaEJKlRayGR5PIkG5NsSnJTw5gPJ9mQZH2Sr7VVmySps1bmbkoyA7gD+CAwAKxOsqqqNgwbMx/4HeDCqvpJkre3UZskqVlbncR7gU1VtbmqdgL3AUtGjLkBuKOqfgJQVa+OxRs7bbgkHbm2QuI04KVh2wND+4b7ZeCXkzyV5Jkkl7dUmySpQVtThafDvhqxfSwwH7gE6AWeTHJeVb1+wMGSZcAygDPOOGNsK5Uk7dNWSAwApw/b7gVe7jDmmaraBfwgyUYGQ2P1yINV1UpgJUBfX9/IsNmPa0tI0pFr63TTamB+knlJZgHXAqtGjPmvwAcAksxh8PTT5pbqkyR10EpIVNVu4EbgEeB54IGqWp/k5iRXDQ17BNiaZAPwOPCvqmprG/VJkjpL1UHP1kx4fX191d/f3+0yJGnSSPJsVfWNZqxPXEuSGhkSkqRGhoQkqZEhIUlqdNghkeSEobmYJElT3CFDIskxSX49yV8keRX4a+CVoZla/2hoYj5J0hQ0mk7iceAsBmdoPbWqTq+qtwMXAc8Af5DkY+NYoySpS0YzLcfioaky9lNVPwa+AXwjycwxr0yS1HWH7CQ6BcSRjJEkTT6HPcFfkpuBGcAaYE1VvTDmVUmSJoTDDomq+mySXwIWAf84yVlVdcPYlyZJ6rZRh0SS/wX8w6p6E/hHwGzgtqGV5iRJU9DhPCdxUlW9meQ9DC41ejLwn8anLEnSRHA4p5t2JTkW+A3g1qp6IMmkmn5171rXLkQkSaNzOCFxO/BXDJ5mumlo39vGvCJJ0oQx6pCoqq8keQjYU1Xbk5wNPD1+pY09OwhJOjyHDIkkqaGViarq/+7dX1WbgOtGjpEkTR2jmpYjyWeSnDF8Z5JZSX41yd3A0vEpT5LUTaM53XQ58E+Be5O8E/gJg9clZgDfBP64qtaMX4mSpG4ZTUisqKqlwJeG5miaA2yvqtfHtzRJUreN5nTT+cO+/ouqesWAkKTpYTQhMfyCdM94FSJJmnhGc7rp1CSfYPAZiYxvOZKkiWQ0IfE5oI/B2117k6wD1g/92VBV3xi/8iRJ3XTIkKiqlcO3k/QyeJ3i7wBXM7jwkCRpCjqSqcIHgAHg4bEvR5I0kRzOLLBTzjUrnt436Z8k6UCH3UlMJc7lJEkHN607CUnSwRkSkqRGhoQkqVGrIZHk8iQbk2xKctNBxv1akkrS12Z9kqT9tRYSSWYAdwBXAAuAjyRZ0GHcicBvAd9tqzZJUmdtdhLvBTZV1eaq2gncByzpMO73gT8EdrRYmySpgzZD4jTgpWHbA0P79kmyCDi9qv78YAdKsixJf5L+LVu2jH2lkiSg3ZDoNDngvhlmkxwD/DGw/FAHqqqVVdVXVX09PWMzMa0P1knSgdoMiQHg9GHbvcDLw7ZPBM4DvpXkReB9wCovXktS97T5xPVqYH6SecDfANcCv773xap6g8FV7wBI8i3gX1ZVfxvF+fS1JB2otU6iqnYDNwKPAM8DD1TV+iQ3J7mqrTokSaPX6txNVfUwI2aPrarPNoy9pI2aJEnNfOJaktTIkJAkNTIkJEmNDAlJUiNDQpLUyJDowKevJWmQISFJajSt17hu4tPXkjTITkKS1MiQkCQ1MiQkSY0MCUlSI0NCktTIkDgEn5mQNJ15C+wheDuspOnMTkKS1MiQkCQ1MiQkSY0MCUlSI0PiMHink6TpxrubDoN3OkmabuwkJEmNDAlJUiND4ih4jULSVOc1iaPgNQpJU52dhCSpkSEhSWpkSIwRr09Imoq8JjFGvD4haSqyk5AkNWotJJJcnmRjkk1Jburw+r9IsiHJ2iSPJTmzrdokSZ21EhJJZgB3AFcAC4CPJFkwYtj3gb6qOh94EPjDNmqTJDVrq5N4L7CpqjZX1U7gPmDJ8AFV9XhVvTW0+QzQ21JtY86L2JKmirYuXJ8GvDRsewD4lYOMvx747+Na0TjyIrakqaKtkEiHfdVxYPIxoA+4uPFgyTJgGcAZZ5wxFvVJkjpo63TTAHD6sO1e4OWRg5IsBn4XuKqqftZ0sKpaWVV9VdXX09Mz5sVKkga1FRKrgflJ5iWZBVwLrBo+IMkiYAWDAfFqS3WNO69PSJrMWjndVFW7k9wIPALMAO6sqvVJbgb6q2oV8EfA24CvJwH4UVVd1UZ948nrE5Ims9aeuK6qh4GHR+z77LCvF7dViyRpdHziWpLUyJCQJDUyJCRJjQyJLvLOJ0kTnVOFd5F3Pkma6OwkJgi7CkkTkZ3EBGFXIWkispOYgOwqJE0UdhITkF2FpInCTmKCs6uQ1E12EhOcXYWkbrKTkCQ1MiQkSY0MCUlSI0NCktTIkJiCvCNK0ljx7qYpyDuiJI0VO4lpxA5D0uGyk5hGhncYe8PCrkPSwRgS09TIcDA0JHViSAiwy5DUmSGhAxgOkvbywrWOiBfBpenBTkJHxG5Dmh7sJNQauw9p8rGTUGvsPqTJx05Ck4adiNQ+OwlNGqPtRIbfwuvtvNLRSVV1u4aj0tfXV/39/d0uQ5ImjSTPVlXfaMZ6ukmS1MiQkCQ1ajUkklyeZGOSTUlu6vD6zyW5f+j17yaZ22Z9kpp548D01NqF6yQzgDuADwIDwOokq6pqw7Bh1wM/qaqzk1wL3Apc01aNkppN9ov/3sRwZNq8u+m9wKaq2gyQ5D5gCTA8JJYAnxv6+kHgPyZJTfar65K6znA4Mm2ebjoNeGnY9sDQvo5jqmo38AZwSivVSZIO0GZIpMO+kR3CaMaQZFmS/iT9W7ZsGZPiJEkHajMkBoDTh233Ai83jUlyLPALwI9HHqiqVlZVX1X19fT0jFO5kqQ2Q2I1MD/JvCSzgGuBVSPGrAKWDn39a8Bfej1CkrqntQvXVbU7yY3AI8AM4M6qWp/kZqC/qlYB/xn4sySbGOwgrm2rPknSgVqdu6mqHgYeHrHvs8O+3gH8kzZrkiQ184lrSVIjQ0KS1MiQkCQ1mvRThSfZBmzsdh1dMgd4rdtFdJGf38/v5z8yZ1bVqJ4fmAqLDm0c7bzoU02S/un62cHP7+f387fx+T3dJElqZEhIkhpNhZBY2e0Cumg6f3bw8/v5p7dWPv+kv3AtSRo/U6GTkCSNk0kbEodaCnUqS3J6kseTPJ9kfZLf7nZN3ZBkRpLvJ/nzbtfStiQnJXkwyV8P/T2YNivqJPnnQ3/vn0tyb5LZ3a5pPCW5M8mrSZ4btu8Xk/zPJC8M/fPk8Xr/SRkSw5ZCvQJYAHwkyYLuVtWq3cDyqjoXeB/wm9Ps8+/128Dz3S6iS/4D8D+q6m8Df5dp8nNIchrwW0BfVZ3H4GShU30i0LuAy0fsuwl4rKrmA48NbY+LSRkSDFsKtap2AnuXQp0WquqVqvre0NfbGPwfxMhV/qa0JL3A3wf+tNu1tC3JzwPvZ3DWZKpqZ1W93t2qWnUscNzQmjPHc+C6NFNKVT3BgevqLAHuHvr6buDq8Xr/yRoSo1kKdVpIMhdYBHy3u5W07t8D/xr4f90upAveCWwB/svQ6bY/TXJCt4tqQ1X9DfBF4EfAK8AbVfXN7lbVFb9UVa/A4C+NwNvH640ma0iMapnTqS7J24BvAP+sqt7sdj1tSfIPgFer6tlu19IlxwLvBr5cVYuAnzKOpxsmkqFz70uAecDfAk5I8rHuVjW1TdaQGM1SqFNakpkMBsQ9VfVQt+tp2YXAVUleZPBU468m+Wp3S2rVADBQVXu7xwcZDI3pYDHwg6raUlW7gIeAv9flmrrh/yR5B8DQP18drzearCExmqVQp6wkYfB89PNVdVu362lbVf1OVfVW1VwG/93/ZVVNm98mq+p/Ay8lOWdo16XAhi6W1KYfAe9LcvzQfweXMk0u2o8wfKnnpcB/G683mpQT/DUthdrlstp0IfBxYF2SNUP7/s3Qyn+aHj4D3DP0S9Jm4Lou19OKqvpukgeB7zF4l9/3meJPXie5F7gEmJNkAPg94A+AB5Jcz2BwjtuKnj5xLUlqNFlPN0mSWmBISJIaGRKSpEaGhCSpkSEhSWpkSEiSGhkSkqRGhoQ0xpIsTfJskrVJnux2PdLR8GE6aQwlOZHBGXkXVtXOJCdNs2m8NcXYSUhjaw9wHPDvkvTtDYgkv9/dsqQjY0hIY6iq3gLOA54CVib5dJJTmaTzpEn+xZXGUJL5VfUCcN/QkrKzGVwUas3Bv1OamOwkpLH1u0k2JvkegwvjfAlYiCGhScpOQhpDVfWJkfuSnA280H410tHz7iZJUiNPN0mSGhkSkqRGhoQkqZEhIUlqZEhIkhoZEpKkRoaEJKmRISFJamRISJIa/X/OI+Fb3I2mmgAAAABJRU5ErkJggg\u003d\u003d\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "epsilon \u003d 0.01 # 1 percent noise\ns_data \u003d np.linspace(0, 10, 101)\nF_data \u003d laplace_model(s\u003ds_data).F\nF_sigma \u003d epsilon * F_data\nnp.random.seed(2)\nF_data \u003d np.random.normal(F_data, F_sigma)\n\nplt.errorbar(s_data, F_data, yerr\u003dF_sigma, fmt\u003d\u0027none\u0027, label\u003dr\u0027$\\mathcal{L}[f] \u003d F(s)$\u0027)\nplt.xlabel(r\u0027$s_i$\u0027)\nplt.ylabel(r\u0027$F(s_i)$\u0027)\nplt.xlim(0, None)\nplt.legend()\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "We will now invert this data, using the procedure outlined in \\cite{}.\n" }, { "cell_type": "code", "execution_count": 7, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "[W(I, M; a) \u003d (I + a**(-2)*M)**(-1),\n c(Fs, W; ) \u003d -W*Fs,\n d(c; ) \u003d (c.T*c)**(1/2)]\n" ], "output_type": "stream" } ], "source": "N_s \u003d symbols(\u0027N_s\u0027, integer\u003dTrue) # Number of s_i points\n\nM \u003d MatrixSymbol(\u0027M\u0027, N_s, N_s)\nW \u003d MatrixSymbol(\u0027W\u0027, N_s, N_s)\nFs \u003d MatrixSymbol(\u0027Fs\u0027, N_s, 1)\nc \u003d MatrixSymbol(\u0027c\u0027, N_s, 1)\nd \u003d MatrixSymbol(\u0027d\u0027, 1, 1)\nI \u003d MatrixSymbol(\u0027I\u0027, N_s, N_s)\na, \u003d parameters(\u0027a\u0027)\n\nmodel_dict \u003d {\n W: Inverse(I + M / a**2),\n c: - W * Fs,\n d: sqrt(c.T * c),\n}\ntikhonov_model \u003d CallableModel(model_dict)\nprint(tikhonov_model)" }, { "cell_type": "markdown", "source": "A ``CallableModel`` is needed because derivatives of matrix expressions sometimes cause problems.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "markdown", "source": "Build required matrices, ignore s\u003d0 because it causes a singularity.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 8, "outputs": [ { "name": "stdout", "text": [ "d [[0.01965714]]\n" ], "output_type": "stream" } ], "source": "I_mat \u003d np.eye(len(s_data[1:]))\ns_i, s_j \u003d np.meshgrid(s_data[1:], s_data[1:])\nM_mat \u003d 1 / (s_i + s_j)\ndelta \u003d np.atleast_2d(np.linalg.norm(F_sigma))\nprint(\u0027d\u0027, delta)\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "Perform the fit\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 10, "outputs": [], "source": "model_data \u003d {\n\tI.name: I_mat,\n\tM.name: M_mat,\n\tFs.name: F_data[1:],\n} \nall_data \u003d dict(**model_data, **{d.name: delta})\n", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "code", "execution_count": 22, "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%%\n" } }, "outputs": [ { "name": "stdout", "text": [ "\nParameter Value Standard Deviation\na 5.449374e-02 None\nStatus message Optimization terminated successfully.\nNumber of iterations 14\nObjective \u003csymfit.core.objectives.LeastSquares object at 0x000002B51BEDA390\u003e\nMinimizer \u003csymfit.core.minimizers.BFGS object at 0x000002B51AB3A518\u003e\n\nGoodness of fit qualifiers:\nchi_squared 3.3130494560395173e-19\nobjective_value 1.6565247280197586e-19\nr_squared -inf\n" ], "output_type": "stream" } ], "source": "fit \u003d Fit(tikhonov_model, **all_data)\nfit_result \u003d fit.execute()\nprint(fit_result)\n" }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "Check the quality of the reconstruction\n" }, { "cell_type": "code", "execution_count": 23, "outputs": [ { "name": "stdout", "text": [ "(100,) (100,)\n" ], "output_type": "stream" }, { "data": { "text/plain": "\u003cmatplotlib.legend.Legend at 0x2b51bf2a6d8\u003e" }, "metadata": {}, "output_type": "execute_result", "execution_count": 23 }, { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\u003d\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "ans \u003d tikhonov_model(**model_data, **fit_result.params)\nF_re \u003d - M_mat.dot(ans.c) / fit_result.value(a)**2\nprint(ans.c.shape, F_re.shape)\n\nplt.errorbar(s_data, F_data, yerr\u003dF_sigma, label\u003dr\u0027$F(s)$\u0027, fmt\u003d\u0027none\u0027)\nplt.plot(s_data[1:], F_re, label\u003dr\u0027$F_{re}(s)$\u0027)\nplt.xlabel(r\u0027$x$\u0027)\nplt.xlabel(r\u0027$F(s)$\u0027)\nplt.xlim(0, None)\nplt.legend()", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "metadata": { "pycharm": { "is_executing": false, "metadata": false, "name": "#%% md\n" } }, "source": "Reconstruct $f(t)$ and compare with the known original.\n" }, { "cell_type": "code", "execution_count": 25, "outputs": [ { "data": { "text/plain": "\u003cmatplotlib.legend.Legend at 0x2b51c00fda0\u003e" }, "metadata": {}, "output_type": "execute_result", "execution_count": 25 }, { "data": { "text/plain": "\u003cFigure size 432x288 with 1 Axes\u003e", "image/png": "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\u003d\u003d\n" }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": "t_data \u003d np.linspace(0, 10, 101)\nf_data \u003d model(t\u003dt_data).f\nf_re_func \u003d lambda x: - np.exp(- x * s_data[1:]).dot(ans.c) / fit_result.value(a)**2\nf_re \u003d [f_re_func(t_i) for t_i in t_data]\n\nplt.axhline(0, color\u003d\u0027black\u0027)\nplt.axvline(0, color\u003d\u0027black\u0027)\nplt.plot(t_data, f_data, label\u003dr\u0027$f(t)$\u0027)\nplt.plot(t_data, f_re, label\u003dr\u0027$f_{re}(t)$\u0027)\nplt.xlabel(r\u0027$t$\u0027)\nplt.xlabel(r\u0027$f(t)$\u0027)\nplt.legend()", "metadata": { "pycharm": { "metadata": false, "name": "#%%\n", "is_executing": false } } }, { "cell_type": "markdown", "source": "Not bad, for an ill-defined problem.\n", "metadata": { "pycharm": { "metadata": false, "name": "#%% md\n" } } } ], "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.1" }, "stem_cell": { "cell_type": "raw", "source": "", "metadata": { "pycharm": { "metadata": false } } } }, "nbformat": 4, "nbformat_minor": 1 }symfit-0.5.4/docs/examples/index.rst000066400000000000000000000025741412237106600174540ustar00rootroot00000000000000======== Examples ======== Model Examples -------------- These are examples of the flexibility of :mod:`symfit` Models. This is because essentially any valid :mod:`sympy` code can be provided as a model. This makes it very intuitive to define your mathematical models almost as you would on paper. .. toctree:: :maxdepth: 1 ex_fourier_series ex_piecewise ex_poly_surface_fit ex_ODEModel ex_ode_system.ipynb ex_CallableNumericalModel ex_CallableNumericalModel_ode.ipynb ex_tikhonov.ipynb Objective Examples ------------------ These are examples on how to use a different objective function from the standard least-squares objective, such as likelihood fitting. .. toctree:: :maxdepth: 1 ex_bivariate_likelihood.ipynb ex_multidataset_likelihood Minimizer Examples ------------------ These examples demonstrate the different minimizers available in ``symfit``. .. toctree:: :maxdepth: 1 ex_mexican_hat.ipynb Interactive Guess Module ------------------------ The :mod:`symfit.contrib.interactive_guess` contrib module was designed to make the process of finding initial guesses easier, by presenting the user with an interactive :mod:`matplotlib` window in which they can play around with the initial values. .. toctree:: :maxdepth: 1 ex_interactive_guesses_ODE ex_interactive_guesses_vector_2D ex_interactive_guesses_nD symfit-0.5.4/docs/fitting_types.rst000066400000000000000000000465741412237106600174270ustar00rootroot00000000000000Fitting Types ============= Fit (Least Squares) ------------------- The default fitting object does least-squares fitting:: from symfit import parameters, variables, Fit import numpy as np # Define a model to fit to. a, b = parameters('a, b') x = variables('x') model = a * x + b # Generate some data xdata = np.linspace(0, 100, 100) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=(100,)) b_vec = np.random.normal(100.0, scale=2.0, size=(100,)) # Point scattered around the line 5 * x + 105 ydata = a_vec * xdata + b_vec fit = Fit(model, xdata, ydata) fit_result = fit.execute() .. figure:: _static/linear_model_fit.png :width: 300px :alt: Linear Model Fit Data The :class:`~symfit.core.fit.Fit` object also supports standard deviations. In order to provide these, it's nicer to use a named model:: a, b = parameters('a, b') x, y = variables('x, y') model = {y: a * x + b} fit = Fit(model, x=xdata, y=ydata, sigma_y=sigma) .. warning:: :mod:`symfit` assumes these sigma to be from measurement errors by default, and not just as a relative weight. This means the standard deviations on parameters are calculated assuming the absolute size of sigma is significant. This is the case for measurement errors and therefore for most use cases :mod:`symfit` was designed for. If you only want to use the sigma for relative weights, then you can use ``absolute_sigma=False`` as a keyword argument. Please note that this is the opposite of the convention used by scipy's :func:`~scipy.optimize.curve_fit`. Looking through their mailing list this seems to have been implemented the opposite way for historical reasons, and was understandably never changed so as not to lose backwards compatibility. Since this is a new project, we don't have that problem. .. _constrained-leastsq: Constrained Least Squares Fit ----------------------------- The :class:`~symfit.core.fit.Fit` takes a ``constraints`` keyword; a list of relationships between the parameters that has to be respected. As an example of fitting with constraints, we could imagine fitting the angles of a triangle:: a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} data = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit = Fit( model=model, a_i=data[0], b_i=data[1], c_i=data[2], constraints=[Equality(a + b + c, 180)] ) fit_result = fit.execute() The line ``constraints=[Equality(a + b + c, 180)]`` ensures the our basic knowledge of geometry is respected despite my sloppy measurements. .. note:: Under the hood, a different `Minimizer` is used to perform a constrained fit. :class:`~symfit.core.fit.Fit` tries to select the right `Minimizer` based on the problem you present it with. See :class:`~symfit.core.fit.Fit` for more. Likelihood ---------- Given a dataset and a model, what values should the model's parameters have to make the observed data most likely? This is the principle of maximum likelihood and the question the Likelihood object can answer for you. Example:: from symfit import Parameter, Variable, exp from symfit.core.objectives import LogLikelihood import numpy as np # Define the model for an exponential distribution (numpy style) beta = Parameter('beta') x = Variable('x') model = (1 / beta) * exp(-x / beta) # Draw 100 samples from an exponential distribution with beta=5.5 data = np.random.exponential(5.5, 100) # Do the fitting! fit = Fit(model, data, objective=LogLikelihood) fit_result = fit.execute() ``fit_result`` is a normal :class:`~symfit.core.fit_results.FitResults` object. As always, bounds on parameters and even constraints are supported. Multiple data sets can be likelihood fitted simultaneously by merging this example with that of global fitting, see `Example: Global Likelihood fitting` in the example section. .. _minimize_maximize: Minimize/Maximize ----------------- Minimize or Maximize a model subject to bounds and/or constraints. As an example, I present an example from the `scipy docs `_. Suppose we want to maximize the following function: .. math:: f(x,y) = 2xy + 2x - x^2 - 2y^2 Subject to the following constraints: .. math:: x^3 - y = 0 .. math:: y - 1 >= 0 In SciPy code the following lines are needed:: def func(x, sign=1.0): """ Objective function """ return sign*(2*x[0]*x[1] + 2*x[0] - x[0]**2 - 2*x[1]**2) def func_deriv(x, sign=1.0): """ Derivative of objective function """ dfdx0 = sign*(-2*x[0] + 2*x[1] + 2) dfdx1 = sign*(2*x[0] - 4*x[1]) return np.array([ dfdx0, dfdx1 ]) cons = ({'type': 'eq', 'fun' : lambda x: np.array([x[0]**3 - x[1]]), 'jac' : lambda x: np.array([3.0*(x[0]**2.0), -1.0])}, {'type': 'ineq', 'fun' : lambda x: np.array([x[1] - 1]), 'jac' : lambda x: np.array([0.0, 1.0])}) res = minimize(func, [-1.0,1.0], args=(-1.0,), jac=func_deriv, constraints=cons, method='SLSQP', options={'disp': True}) Takes a couple of read-throughs to make sense, doesn't it? Let's do the same problem in :mod:`symfit`:: from symfit import parameters, Maximize, Eq, Ge x, y = parameters('x, y') model = 2*x*y + 2*x - x**2 -2*y**2 constraints = [ Eq(x**3 - y, 0), Ge(y - 1, 0), ] fit = Fit(- model, constraints=constraints) fit_result = fit.execute() Done! :mod:`symfit` will determine all derivatives automatically, no need for you to think about it. Notice the minus sign in the call to `Fit`. This is because `Fit` will always minimize, so in order to achieve maximization we should minimize `- model`. .. warning:: You might have noticed that ``x`` and ``y`` are :class:`~symfit.core.argument.Parameter`'s in the above problem, which may strike you as weird. However, it makes perfect sense because in this problem they are parameters to be optimised, not independent variables. Furthermore, this way of defining it is consistent with the treatment of :class:`~symfit.core.argument.Variable`'s and :class:`~symfit.core.argument.Parameter`'s in :mod:`symfit`. Be aware of this when minimizing such problems, as the whole process won't work otherwise. ODE Fitting ----------- Fitting to a system of ordinary differential equations (ODEs) is also remarkedly simple with :mod:`symfit`. Let's do a simple example from reaction kinetics. Suppose we have a reaction A + A -> B with rate constant :math:`k`. We then need the following system of rate equations: .. math:: \frac{dA}{dt} = -k A^2 \frac{dB}{dt} = k A^2 In :mod:`symfit`, this becomes:: model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } We see that the :mod:`symfit` code is already very readable. Let's do a fit to this:: tdata = np.array([10, 26, 44, 70, 120]) adata = 10e-4 * np.array([44, 34, 27, 20, 14]) a, b, t = variables('a, b, t') k = Parameter('k', 0.1) a0 = 54 * 10e-4 model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: a0, b: 0.0}) fit = Fit(ode_model, t=tdata, a=adata, b=None) fit_result = fit.execute() That's it! An :class:`~symfit.core.models.ODEModel` behaves just like any other model object, so :class:`~symfit.core.fit.Fit` knows how to deal with it! Note that since we don't know the concentration of B, we explicitly set ``b=None`` when calling :class:`~symfit.core.fit.Fit` so it will be ignored. .. warning:: Fitting to ODEs is extremely difficult from an algorithmic point of view since these systems are usually very sensitive to the parameters. Using (very) good initial guesses for the parameters and initial values is critical. Upon every iteration of performing the fit, the ODEModel is integrated again from the initial point using the new guesses for the parameters. We can plot it just like always:: # Generate some data tvec = np.linspace(0, 500, 1000) A, B = ode_model(t=tvec, **fit_result.params) plt.plot(tvec, A, label='[A]') plt.plot(tvec, B, label='[B]') plt.scatter(tdata, adata) plt.legend() plt.show() .. figure:: _static/ode_model_fit.png :width: 300px :alt: Linear Model Fit Data As an example of the power of :mod:`symfit`'s ODE syntax, let's have a look at a system with 2 equilibria: compound AA + B <-> AAB and AAB + B <-> BAAB. In :mod:`symfit` these can be implemented as:: AA, B, AAB, BAAB, t = variables('AA, B, AAB, BAAB, t') k, p, l, m = parameters('k, p, l, m') AA_0 = 10 # Some made up initial amound of [AA] B = AA_0 - BAAB + AA # [B] is not independent. model_dict = { D(BAAB, t): l * AAB * B - m * BAAB, D(AAB, t): k * A * B - p * AAB - l * AAB * B + m * BAAB, D(A, t): - k * A * B + p * AAB, } The result is as readable as one can reasonably expect from a multicomponent system (and while using chemical notation). Let's plot the model for some kinetics constants:: model = ODEModel(model_dict, initial={t: 0.0, AA: AA_0, AAB: 0.0, BAAB: 0.0}) # Generate some data tdata = np.linspace(0, 3, 1000) # Eval the normal way. AA, AAB, BAAB = model(t=tdata, k=0.1, l=0.2, m=0.3, p=0.3) plt.plot(tdata, AA, color='red', label='[AA]') plt.plot(tdata, AAB, color='blue', label='[AAB]') plt.plot(tdata, BAAB, color='green', label='[BAAB]') plt.plot(tdata, B(BAAB=BAAB, AA=AA), color='pink', label='[B]') # plt.plot(tdata, AA + AAB + BAAB, color='black', label='total') plt.legend() plt.show() .. figure:: _static/ode_double_eq_integrated.png :width: 300px :alt: ODE integration More common examples, such as dampened harmonic oscillators also work as expected:: # Oscillator strength k = Parameter('k') # Mass, just there for the physics m = 1 # Dampening factor gamma = Parameter('gamma') x, v, t = symfit.variables('x, v, t') # Define the force based on Hooke's law, and dampening a = (-k * x - gamma * v)/m model_dict = { D(x, t): v, D(v, t): a, } ode_model = ODEModel(model_dict, initial={t: 0, v: 0, x: 1}) # Let's create some data... times = np.linspace(0, 15, 150) data = ode_model(times, k=11, gamma=0.9, m=m.value).x # ... and add some noise to it. noise = np.random.normal(1, 0.1, data.shape) # 10% error data *= noise fit = Fit(ode_model, t=times, x=data) fit_result = fit.execute() .. note:: Evaluating the model above will produce a named tuple with values for both ``x`` and ``v``. Since we are only interested in the values for ``x``, we immediately select it with ``.x``. .. figure:: _static/ode_dampened_harmonic_oscillator.png :width: 300px :alt: Dampened harmonic oscillator .. _global-fitting: Fitting multiple datasets ------------------------- A common fitting problem is to fit to multiple datasets. This is sometimes referred to as global fitting. In such fits parameters might be shared between the fits to the different datasets. The same syntax used for ODE fitting makes this problem very easy to solve in :mod:`symfit`. As a simple example, suppose we have two datasets measuring exponential decay, with the same background, but the different amplitude and decay rate. .. math:: f(x) = y_0 + a * e^{- b * x} In order to fit to this, we define the following model:: x_1, x_2, y_1, y_2 = variables('x_1, x_2, y_1, y_2') y0, a_1, a_2, b_1, b_2 = parameters('y0, a_1, a_2, b_1, b_2') model = Model({ y_1: y0 + a_1 * exp(- b_1 * x_1), y_2: y0 + a_2 * exp(- b_2 * x_2), }) Note that ``y0`` is shared between the components. Fitting is then done in the normal way:: fit = Fit(model, x_1=xdata1, x_2=xdata2, y_1=ydata1, y_2=ydata2) fit_result = fit.execute() .. figure:: _static/global_fitting.png :width: 500px :alt: ODE integration Any ``Model`` that comes to mind is fair game. Behind the scenes :mod:`symfit` will build a least squares function where the residues of all the components are added squared, ready to be minimized. Unlike in the above example, the `x`-axis does not even have to be shared between the components. .. warning:: The regression coefficient is not properly defined for vector-valued models, but it is still listed! Until this is fixed, please recalculate it on your own for every component using the bestfit parameters. Do not cite the overall :math:`R^2` given by :mod:`symfit`. Fitting multidimensional datasets ---------------------------------- So far we have only considered problems with a single independent variable, but in the real world it is quite common to have problems with multiple independent variables. For example, a specific property over a grid, like the temperature of a surface. In that case, you have a three-dimensional dataset consisting of (x-, y-coordinates, temperature), and our model is a function :math:`T(x, y; \vec{p})`, where :math:`\vec{p}` indicates the collection of parameters to be determined during the fit. Let's work this out with the following mathematical model. We have a polynomial function with two coefficients, representing two terms of mixed order in ``x`` and ``y``: :math:`T(x, y) = z = c_2 x^4 y^5 + c_1 x y^2` Secondly, we have to implement our model:: x, y, z = variables('x, y, z') c1, c2 = parameters('c1, c2') model_dict = {z: Poly( {(1, 2): c1, (4, 5): c2}, x ,y)} model = Model(model_dict) # prints z(x, y; c1, c2) = Poly(c2*x**4*y**5 + c1*x*y**2, x, y, domain='ZZ[c1,c2]') Now we can fit this polynomial model to some mock data. We have to be careful that ``xdata``, ``ydata`` and ``zdata`` are two-dimensional:: x = np.linspace(0, 100, 100) y = np.linspace(0, 100, 100) xdata, ydata = np.meshgrid(x, y) zdata = 42 * xdata**4 * ydata**5 + 3.14 * xdata * ydata**2 fit = Fit(model, x=xdata, y=ydata, z=zdata) fit_result = fit.execute() In conclusion, we made a mathematical model for a multidimensional function with two fit parameters, implemented this model, and fed it data to get a result. Global Minimization ------------------- Very often, there are multiple solutions to a fitting (or minimisation) problem. These are local minima of the objective function. The best solution, of course, is the global minimum, but most minimization algorithms will only find a local minimum, and thus the answer you get will depend on the initial values of your parameters. This can be incredibly annoying if you have no further knowledge about your system. Luckily, global minimizers exist which are not influenced by the initial guesses for your parameters. In symfit, two such algorithms from :mod:`scipy` have been wrapped for this purpose. Firstly, the :func:`~scipy.optimize.differential_evolution` algorithm from :mod:`scipy` is wrapped as :class:`~symfit.core.minimizers.DifferentialEvolution`. Secondly, the :func:`~scipy.optimize.basinhopping` algorithm is available as :class:`~symfit.core.minimizers.BasinHopping`. To use these minimizers, just tell :class:`~symfit.core.fit.Fit`:: from symfit import Parameter, Variable, Model, Fit from symfit.core.minimizers import DifferentialEvolution x = Parameter('x') x.min, x.max = -100, 100 x.value = -2.5 y = Variable('y') model = Model({y: x**4 - 10 * x**2 - x}) # Skewed Mexican hat fit = Fit(model, minimizer=DifferentialEvolution) fit_result = fit.execute() However, due to how this algorithm works, it's not great at finding the exact minimum (but it will find it if given enough time). You can work around this by "chaining" minimizers: first, run a global minimization to (hopefully) get close to your answer, and then polish it off using a local minimizer:: fit = Fit(model, minimizer=[DifferentialEvolution, BFGS]) .. note:: Global minimizers such as differential evolution and basin-hopping are rather sensitive to their hyperparameters. You might need to play with those to get appropriate results, e.g.:: fit.execute(DifferentialEvolution={'popsize': 20, 'recombination': 0.9}) .. note:: There is no way to guarantee that the minimum found is actually the global minimum. Unfortunately, there is no way around this. Therefore, you should always critically inspect the results. Constrained Basin-Hopping ------------------------- Worthy of special mention is the ease with which constraints or bounds can be added to :class:`symfit.core.minimizers.BasinHopping` when used through the :class:`symfit.core.fit.Fit` interface. As a very simple example, we shall compare to an example from the :mod:`scipy` docs:: import numpy as np from scipy.optimize import basinhopping def func2d(x): f = np.cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0] df = np.zeros(2) df[0] = -14.5 * np.sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2 df[1] = 2. * x[1] + 0.2 return f, df minimizer_kwargs = {"method":"L-BFGS-B", "jac":True} x0 = [1.0, 1.0] ret = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs, niter=200) Let's compare to the same functionality in :mod:`symfit`:: import numpy as np from symfit.core.minimizers import BasinHopping from symfit import parameters, Fit, cos x0 = [1.0, 1.0] x1, x2 = parameters('x1, x2', value=x0) model = cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 fit = Fit(model, minimizer=BasinHopping) fit_result = fit.execute(niter=200) No `minimizer_kwargs` have to be provided, as :mod:`symfit` will automatically compute and provide the jacobian and select a minimizer. In this case, `symfit` will choose `BFGS`. When bounds are provided, `symfit` will switch to using `L-BFGS-B` instead. Setting bounds is as simple as:: x1.min = 0.0 x1.max = 100.0 However, the real strength of the `symfit` syntax lies in providing constraints:: constraints = [Eq(x1, x2)] fit = Fit(model, minimizer=BasinHopping, constraints=constraints) This artificial example will make sure `x1 == x2` after fitting. If you have read the :ref:`minimize_maximize` section, you will know how much work this would be in pure :mod:`scipy`. Advanced usage -------------- In general, the separate components of the model can be whatever you need them to be. You can mix and match which variables and parameters should be coupled and decoupled ad lib. Some examples are given below. Same parameters and same function, different (in)dependent variables:: datasets = [data_1, data_2, data_3, data_4, data_5, data_6] xs = variables('x_1, x_2, x_3, x_4, x_5, x_6') ys = variables('y_1, y_2, y_3, y_4, y_5, y_6') zs = variables(', '.join('z_{}'.format(i) for i in range(1, 7))) a, b = parameters('a, b') model_dict = { z: a/(y * b) * exp(- a * x) for x, y, z in zip(xs, ys, zs) } What if the model is unnamed? ----------------------------- Then you'll have to use the ordering. Variables throughout :mod:`symfit`'s objects are internally ordered in the following way: first independent variables, then dependent variables, then sigma variables, and lastly parameters when applicable. Within each group, alphabetical ordering applies. It is therefore always possible to assign data to variables in an unambiguous way using this ordering. For example:: fit = Fit(model, x_data, y_data, sigma_y_data) symfit-0.5.4/docs/index.rst000066400000000000000000000010731412237106600156270ustar00rootroot00000000000000.. symfit documentation master file, created by sphinx-quickstart on Thu Nov 27 13:46:41 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to symfit's documentation! ================================== Contents: .. toctree:: :maxdepth: 2 intro installation tutorial fitting_types style_guide technical_notes dependencies examples/index module_docs Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` symfit-0.5.4/docs/installation.rst000066400000000000000000000010501412237106600172140ustar00rootroot00000000000000Installation ============ If you are using pip, you can simply run :: pip install symfit from your terminal. If you prefer to use `conda`, run :: conda install -c conda-forge symfit instead. Lastly, if you prefer to install manually you can download the source from https://github.com/tBuLi/symfit. Contrib module -------------- To also install the dependencies of 3rd party contrib modules such as interactive guesses, install `symfit` using:: pip install symfit[contrib] Dependencies ------------ See `requirements.txt` for a full list. symfit-0.5.4/docs/intro.rst000066400000000000000000000041111412237106600156470ustar00rootroot00000000000000Introduction ============ Existing fitting modules are not very pythonic in their API and can be difficult for humans to use. This project aims to marry the power of :mod:`scipy.optimize` with the readability of :mod:`sympy` to create a highly readable and easy to use fitting package which works for projects of any scale. :mod:`symfit` makes it extremely easy to provide guesses for your parameters and to bound them to a certain range:: a = Parameter('a', 1.0, min=0.0, max=5.0) To define models to fit to:: x = Variable('x') A = Parameter('A') sig = Parameter('sig', 1.0, min=0.0, max=5.0) x0 = Parameter('x0', 1.0, min=0.0) # Gaussian distrubution model = A * exp(-(x - x0)**2/(2 * sig**2)) And finally, to execute the fit:: fit = Fit(model, xdata, ydata) fit_result = fit.execute() And to evaluate the model using the best fit parameters:: y = model(x=xdata, **fit_result.params) .. figure:: _static/gaussian_intro.png :width: 500px :alt: Gaussian Data As your models become more complicated, :mod:`symfit` really comes into it's own. For example, vector valued functions are both easy to define and beautiful to look at:: model = { y_1: x**2, y_2: 2*x } And constrained maximization has never been this easy:: x, y = parameters('x, y') model = 2*x*y + 2*x - x**2 -2*y**2 constraints = [ Eq(x**3 - y, 0), # Eq: == Ge(y - 1, 0), # Ge: >= ] fit = Fit(- model, constraints=constraints) fit_result = fit.execute() Technical Reasons ----------------- On a more technical note, this symbolic approach turns out to have great technical advantages over using scipy directly. In order to fit, the algorithm needs the Jacobian: a matrix containing the derivatives of your model in it's parameters. Because of the symbolic nature of :mod:`symfit`, this is determined for you on the fly, saving you the trouble of having to determine the derivatives yourself. Furthermore, having this Jacobian allows good estimation of the errors in your parameters, something :mod:`scipy ` does not always succeed in. symfit-0.5.4/docs/likelihood.rst000066400000000000000000000023411412237106600166420ustar00rootroot00000000000000On Likelihood Fitting ===================== The :class:`~symfit.core.objectives.LogLikelihood` objective function should be used to perform log-likelihood maximization. The :meth:`~symfit.core.objectives.LogLikelihood.__call__` and :meth:`~symfit.core.objectives.LogLikelihood.eval_jacobian` definitions have been changed to facilitate what one would expect from Likelihood fitting: `__call__` gives the value of log-likelihood at the given values of :math:`\vec{p}` and :math:`\vec{x}_i`, where :math:`\vec{p}` is a shorthand notation for all parameter, and :math:`\vec{x}_i` the same shorthand for all independent variables. .. math:: \log{L(\vec{p}|\vec{x}_i)} = \sum_{i=1}^{N} \log{f(\vec{p}|\vec{x}_i)} :meth:`~symfit.core.objectives.LogLikelihood.eval_jacobian` gives the derivative with respect to every parameter of the log-likelihood: .. math:: \nabla_{\vec{p}} \log{L(\vec{p}|\vec{x}_i)} = \sum_{i=1}^{N} \frac{1}{f(\vec{p}|\vec{x}_i)} \nabla_{\vec{p}} f(\vec{p}|\vec{x}_i) Where :math:`\nabla_{\vec{p}}` is the derivative with respect to all parameters :math:`\vec{p}`. The function therefore returns a vector of length :code:`len(p)` containing the Jacobian evaluated at the given values of :math:`\vec{p}` and :math:`\vec{x}`. symfit-0.5.4/docs/make.bat000066400000000000000000000144731412237106600154030ustar00rootroot00000000000000@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 goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %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 ) 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\symfit.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\symfit.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 %BUILDDIR%/.. 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 %BUILDDIR%/.. 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" == "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 symfit-0.5.4/docs/module_docs.rst000066400000000000000000000036341412237106600170220ustar00rootroot00000000000000.. _apidocs: Module Documentation ==================== This page contains documentation to everything ``symfit`` has to offer. Fit --- .. automodule:: symfit.core.fit :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Models ------ .. automodule:: symfit.core.models :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Argument -------- .. automodule:: symfit.core.argument :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Operators --------- .. automodule:: symfit.core.operators :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Fit Results ----------- .. automodule:: symfit.core.fit_results :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Minimizers ---------- .. automodule:: symfit.core.minimizers :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Objectives ---------- .. automodule:: symfit.core.objectives :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Support ------- .. automodule:: symfit.core.support :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Printing -------- .. automodule:: symfit.core.printing :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Distributions ------------- .. automodule:: symfit.distributions :members: :special-members: :exclude-members: __weakref__ :show-inheritance: Contrib ------- Contrib modules are modules and extensions to symfit provided by other people. This usually means the code is of slightly less quality, and may not survive future versions. .. automodule:: symfit.contrib.interactive_guess.interactive_guess :members: :special-members: :exclude-members: __weakref__ :show-inheritance: symfit-0.5.4/docs/sigma.rst000066400000000000000000000102341412237106600156170ustar00rootroot00000000000000On Standard Deviations ====================== This essay is meant as a reflection on the implementation of Standard Deviations and/or measurement errors in :mod:`symfit`. Although reading this essay in it's entirely will only be interesting to a select few, I urge anyone who uses :mod:`symfit` to read the following summarizing bullet points, as :mod:`symfit` is **not** backward-compatible with :mod:`scipy`. * standard deviations are assumed to be measurement errors by default, not relative weights. This is the opposite of the :mod:`scipy` definition. Set ``absolute_sigma=False`` when calling :class:`~symfit.core.fit.Fit` to get the :mod:`scipy` behavior. Analytical Example ------------------ The implementation of standard deviations should be in agreement with cases to which the analytical solution is known. :mod:`symfit` was build such that this is true. Let's follow the example outlined by [taldcroft]_. We'll be sampling from a normal distribution with :math:`\mu = 0.0` and varying :math:`\sigma`. It can be shown that given a sample from such a distribution: .. math:: \mu = 0.0 .. math:: \sigma_{\mu} = \frac{\sigma}{\sqrt{N}} where N is the size of the sample. We see that the error in the sample mean scales with the :math:`\sigma` of the distribution. In order to reproduce this with :mod:`symfit`, we recognize that determining the avarage of a set of numbers is the same as fitting to a constant. Therefore we will fit to samples generated from distributions with :math:`\sigma = 1.0` and :math:`\sigma = 10.0` and check if this matches the analytical values. Let's set :math:`N = 10000`. :: N = 10000 sigma = 10.0 np.random.seed(10) yn = np.random.normal(size=N, scale=sigma) a = Parameter('a') y = Variable('y') model = {y: a} fit = Fit(model, y=yn, sigma_y=sigma) fit_result = fit.execute() fit_no_sigma = Fit(model, y=yn) fit_result_no_sigma = fit_no_sigma.execute() This gives the following results: * a = 5.102056e-02 |+-| 1.000000e-01 when ``sigma_y`` is provided. This matches the analytical prediction. * a = 5.102056e-02 |+-| 9.897135e-02 without ``sigma_y`` provided. This is incorrect. If we run the above code example with ``sigma = 1.0``, we get the following results: * a = 5.102056e-03 |+-| 9.897135e-03 when ``sigma_y`` is provided. This matches the analytical prediction. * a = 5.102056e-03 |+-| 9.897135e-03 without ``sigma_y`` provided. This is also correct, since providing no weights is the same as setting the weights to 1. To conclude, if :mod:`symfit` is provided with the standard deviations, it will give the expected result by default. As shown in [taldcroft]_ and :mod:`symfit`'s tests, :func:`scipy.optimize.curve_fit` has to be provided with the ``absolute_sigma=True`` setting to do the same. .. important:: We see that even if the weight provided to every data point is the same, the *scale* of the weight still effects the result. :mod:`scipy` was build such that the opposite is true: if all datapoints have the same weight, the error in the parameters does not depend on the scale of the weight. This difference is due to the fact that :mod:`symfit` is build for areas of science where one is dealing with measurement errors. And with measurement errors, the size of the errors obviously matters for the certainty of the fit parameters, even if the errors are the same for every measurement. If you want the :mod:`scipy` behavior, initiate :class:`~symfit.core.fit.Fit` with ``absolute_sigma=False``. Comparison to Mathematica ========================= In Mathematica, the default setting is also to use relative weights, which we just argued is not correct when dealing with measurement errors. In [Mathematica]_ this problem is discussed very nicely, and it is shown how to solve this in Mathematica. Since :mod:`symfit` is a fitting tool for the practical man, measurement errors are assumed by default. .. [taldcroft] http://nbviewer.jupyter.org/urls/gist.github.com/taldcroft/5014170/raw/31e29e235407e4913dc0ec403af7ed524372b612/curve_fit.ipynb .. [Mathematica] http://reference.wolfram.com/language/howto/FitModelsWithMeasurementErrors.html .. |+-| unicode:: U+00B1 .. Plusminus sign symfit-0.5.4/docs/style_guide.rst000066400000000000000000000024241412237106600170360ustar00rootroot00000000000000Style Guide & Best Practices ============================ Style Guide ----------- Anything Raymond Hettinger says wins the argument until I have time to write a proper style guide. Best Practices -------------- * It is recommended to always use named models. So not:: model = a * x**2 fit = Fit(model, xdata, ydata) but:: model = {y: a * x**2} fit = Fit(model, x=xdata, y=ydata) In this simple example the two are equivalent but for multidimentional data using ordered arguments can become ambiguous and difficult to read. To increase readability, it is therefore recommended to always use named models. * Evaluating a (vector valued) model returns a :func:`~collections.namedtuple`. You can access the elements by either tuple unpacking, or by using the variable names. Note that if you use tuple unpacking, the results will be ordered alphabetically. The following:: model = Model({y_1: x**2, y_2: x**3}) sol_1, sol_2 = model(x=xdata) is therefore equivalent to:: model = Model({y_1: x**2, y_2: x**3}) solutions = model(x=xdata) sol_1 = solutions.y_1 sol_2 = solutions.y_2 Using numerical indexing (or something similar) is not recommended as it is less readable than the options given above:: sol_1 = model(x=xdata)[0] symfit-0.5.4/docs/technical_notes.rst000066400000000000000000000002371412237106600176630ustar00rootroot00000000000000Technical Notes =============== Essays on mathematical and implementation details. .. toctree:: :maxdepth: 2 likelihood sigma api_structure symfit-0.5.4/docs/tutorial.rst000066400000000000000000000133061412237106600163650ustar00rootroot00000000000000Tutorial ======== Simple Example -------------- The example below shows how easy it is to define a model that we could fit to. :: from symfit import Parameter, Variable a = Parameter('a') b = Parameter('b') x = Variable('x') model = a * x + b Lets fit this model to some generated data. :: from symfit import Fit import numpy as np xdata = np.linspace(0, 100, 100) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=(100,)) b_vec = np.random.normal(100.0, scale=2.0, size=(100,)) ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 fit = Fit(model, xdata, ydata) fit_result = fit.execute() .. figure:: _static/linear_model_fit_data.png :width: 300px :alt: Linear Model Fit Data Printing ``fit_result`` will give a full report on the values for every parameter, including the uncertainty, and quality of the fit. Initial Guess ------------- For fitting to work as desired you should always give a good initial guess for a parameter. The :class:`~symfit.core.argument.Parameter` object can therefore be initiated with the following keywords: * ``value`` the initial guess value. Defaults to ``1``. * ``min`` Minimal value for the parameter. * ``max`` Maximal value for the parameter. * ``fixed`` Whether the parameter's ``value`` can vary during fitting. In the example above, we might change our :class:`~symfit.core.argument.Parameter`'s to the following after looking at a plot of the data:: k = Parameter('k', value=4, min=3, max=6) a, b = parameters('a, b') a.value = 60 a.fixed = True Accessing the Results --------------------- A call to :meth:`Fit.execute ` returns a :class:`~symfit.core.fit_results.FitResults` instance. This object holds all information about the fit. The fitting process does not modify the :class:`~symfit.core.argument.Parameter` objects. In the above example, ``a.value`` will still be ``60`` and not the value we obtain after fitting. To get the value of fit parameters we can do:: >>> print(fit_result.value(a)) >>> 14.66946... >>> print(fit_result.stdev(a)) >>> 0.3367571... >>> print(fit_result.value(b)) >>> 104.6558... >>> print(fit_result.stdev(b)) >>> 19.49172... >>> print(fit_result.r_squared) >>> 0.950890866472 For more :class:`~symfit.core.fit_results.FitResults`, see the :ref:`apidocs`. Evaluating the Model -------------------- With these parameters, we could now evaluate the model with these parameters so we can make a plot of it. In order to do this, we simply call the model with these values:: import matplotlib.pyplot as plt y = model(x=xdata, a=fit_result.value(a), b=fit_result.value(b)) plt.plot(xdata, y) plt.show() .. figure:: _static/linear_model_fit.png :width: 300px :alt: Linear Model Fit The model *has* to be called by keyword arguments to prevent any ambiguity. So the following does not work:: y = model(xdata, fit_result.value(a), fit_result.value(b)) To make life easier, there is a nice shorthand notation to immediately use a fit result:: y = model(x=xdata, **fit_result.params) This immediately unpacks an :class:`~collections.OrderedDict` containing the optimized fit parameters. Named Models ------------ More complicated models are also relatively easy to deal with by using named models. Let's try our luck with a bivariate normal distribution:: from symfit import parameters, variables, exp, pi, sqrt x, y, p = variables('x, y, p') mu_x, mu_y, sig_x, sig_y, rho = parameters('mu_x, mu_y, sig_x, sig_y, rho') z = ( (x - mu_x)**2/sig_x**2 + (y - mu_y)**2/sig_y**2 - 2 * rho * (x - mu_x) * (y - mu_y)/(sig_x * sig_y) ) model = { p: exp( - z / (2 * (1 - rho**2))) / (2 * pi * sig_x * sig_y * sqrt(1 - rho**2) ) } fit = Fit(model, x=xdata, y=ydata, p=pdata) By using the magic of named models, the flow of information is still relatively clear, even with such a complicated function. This syntax also supports vector valued functions:: model = {y_1: a * x**2, y_2: 2 * x * b} One thing to note about such models is that now ``model(x=xdata)`` obviously no longer works as ``type(model) == dict``. There is a preferred way to resolve this. If any kind of fitting object has been initiated, it will have a `.model` atribute containing an instance of :class:`~symfit.core.models.Model`. This can again be called:: a, b = parameters('a, b') y_1, y_2, x = variables('y_1, y_2, x') model = {y_1: a * x**2, y_2: 2 * x * b} fit = Fit(model, x=xdata, y_1=y_data1, y_2=y_data2) fit_result = fit.execute() y_1_result, y_2_result = fit.model(x=xdata, **fit_result.params) This returns a :func:`~collections.namedtuple`, with the components evaluated. So through the magic of tuple unpacking, ``y_1`` and ``y_2`` contain the evaluated fit. The dependent variables will be ordered alphabetically in the returned :func:`~collections.namedtuple`. Alternatively, the unpacking can be performed explicitly. If for some reason no :class:`~symfit.core.fit.Fit` is initiated you can make a :class:`~symfit.core.models.Model` object yourself:: model = Model(model_dict) y_1_result, y_2_result = model(x=xdata, a=2.4, b=0.1) or equivalently:: outcome = model(x=xdata, a=2.4, b=0.1) y_1_result = outcome.y_1 y_2_result = outcome.y_2 symfit exposes sympy.api ------------------------ :mod:`symfit` exposes the `sympy `_ api as well, so mathematical expressions such as :class:`~sympy.functions.elementary.exponential.exp`, :class:`~sympy.functions.elementary.trigonometric.sin` and :class:`~sympy.core.numbers.Pi` are importable from :mod:`symfit` as well. For more, read the `sympy docs `_. symfit-0.5.4/examples/000077500000000000000000000000001412237106600146535ustar00rootroot00000000000000symfit-0.5.4/examples/callable_numerical_model.py000066400000000000000000000024511412237106600222050ustar00rootroot00000000000000from symfit import variables, parameters, Fit, D, ODEModel, CallableNumericalModel import numpy as np import matplotlib.pyplot as plt def nonanalytical_func(x, a, b): """ This can be any pythonic function which should be fitted, typically one which is not easily written or supported as an analytical expression. """ # Do your non-trivial magic here. In this case a Piecewise, although this # could also be done symbolically. y = np.zeros_like(x) y[x > b] = (a * (x - b) + b)[x > b] y[x <= b] = b return y x, y1, y2 = variables('x, y1, y2') a, b = parameters('a, b') mixed_model = CallableNumericalModel( {y1: nonanalytical_func, y2: x ** a}, connectivity_mapping={y1: {x, a, b}} ) # Generate data xdata = np.linspace(0, 10) y1data, y2data = mixed_model(x=xdata, a=1.3, b=4) y1data = np.random.normal(y1data, 0.1 * y1data) y2data = np.random.normal(y2data, 0.1 * y2data) # Perform the fit b.value = 3.5 fit = Fit(mixed_model, x=xdata, y1=y1data, y2=y2data) fit_result = fit.execute() print(fit_result) # Plotting, irrelevant to the symfit part. y1_fit, y2_fit, = mixed_model(x=xdata, **fit_result.params) plt.scatter(xdata, y1data) plt.plot(xdata, y1_fit, label=r'$y_1$') plt.scatter(xdata, y2data) plt.plot(xdata, y2_fit, label=r'$y_2$') plt.legend(loc=0) plt.show()symfit-0.5.4/examples/fourier_series.py000066400000000000000000000023231412237106600202520ustar00rootroot00000000000000from symfit import parameters, variables, sin, cos, Fit import numpy as np import matplotlib.pyplot as plt def fourier_series(x, f, n=0): """ Returns a symbolic fourier series of order `n`. :param n: Order of the fourier series. :param x: Independent variable :param f: Frequency of the fourier series """ # Make the parameter objects for all the terms a0, *cos_a = parameters(','.join(['a{}'.format(i) for i in range(0, n + 1)])) sin_b = parameters(','.join(['b{}'.format(i) for i in range(1, n + 1)])) # Construct the series series = a0 + sum(ai * cos(i * f * x) + bi * sin(i * f * x) for i, (ai, bi) in enumerate(zip(cos_a, sin_b), start=1)) return series x, y = variables('x, y') w, = parameters('w') model_dict = {y: fourier_series(x, f=w, n=3)} print(model_dict) # Make step function data xdata = np.linspace(-np.pi, np.pi) ydata = np.zeros_like(xdata) ydata[xdata > 0] = 1 # Define a Fit object for this model and data fit = Fit(model_dict, x=xdata, y=ydata) fit_result = fit.execute() print(fit_result) # Plot the result plt.plot(xdata, ydata) plt.plot(xdata, fit.model(x=xdata, **fit_result.params).y, ls=':') plt.xlabel('x') plt.ylabel('y') plt.show()symfit-0.5.4/examples/gaussian.py000066400000000000000000000015671412237106600170500ustar00rootroot00000000000000import matplotlib.pyplot as plt import numpy as np import seaborn as sns from symfit import Parameter, Variable, Fit, GradientModel from symfit.distributions import Gaussian palette = sns.color_palette() x = Variable('x') y = Variable('y') A = Parameter('A') sig = Parameter(name='sig', value=1.4, min=1.0, max=2.0) x0 = Parameter(name='x0', value=15.0, min=0.0) # Gaussian distribution model = GradientModel({y: A*Gaussian(x, x0, sig)}) # Sample 10000 points from a N(15.0, 1.5) distrubution np.random.seed(seed=123456789) sample = np.random.normal(loc=15.0, scale=1.5, size=(10000,)) ydata, bin_edges = np.histogram(sample, 100) xdata = (bin_edges[1:] + bin_edges[:-1])/2 fit = Fit(model, xdata, ydata) fit_result = fit.execute() y, = model(x=xdata, **fit_result.params) sns.regplot(xdata, ydata, fit_reg=False) plt.plot(xdata, y, color=palette[2]) plt.ylim(0, 400) plt.show() symfit-0.5.4/examples/global_fitting.py000066400000000000000000000035321412237106600202140ustar00rootroot00000000000000""" A minimal example of global fitting in symfit. Two datasets are first generated from the same function. .. math:: f(x) = a * x^2 + b * x + y_0 All dataset will share the parameter :math:`y_0`, which measures the background, but :math:`a` and :math:`b` will be unique for each. Additionally, dataset 2 will contain less datapoints than 1 to demonstrate that this will still work. """ import numpy as np from symfit import * from symfit.core.support import * import matplotlib.pyplot as plt import seaborn as sns palette = sns.color_palette() x_1, x_2, y_1, y_2 = variables('x_1, x_2, y_1, y_2') y0, a_1, a_2, b_1, b_2 = parameters('y0, a_1, a_2, b_1, b_2') # The following vector valued function links all the equations together # as stated in the intro. model = Model({ y_1: a_1 * x_1**2 + b_1 * x_1 + y0, y_2: a_2 * x_2**2 + b_2 * x_2 + y0, }) # Generate data from this model xdata1 = np.linspace(0, 10) xdata2 = xdata1[::2] # Only every other point. ydata1, ydata2 = model(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) # Add some noise to make it appear like real data np.random.seed(1) ydata1 += np.random.normal(0, 2, size=ydata1.shape) ydata2 += np.random.normal(0, 2, size=ydata2.shape) xdata = [xdata1, xdata2] ydata = [ydata1, ydata2] # Guesses a_1.value = 100 a_2.value = 50 b_1.value = 1 b_2.value = 1 y0.value = 10 sigma_y = np.concatenate((np.ones(20), [2., 4., 5, 7, 3])) fit = Fit( model, x_1=xdata[0], x_2=xdata[1], y_1=ydata[0], y_2=ydata[1], sigma_y_2=sigma_y ) fit_result = fit.execute() print(fit_result) fit_curves = model(x_1=xdata[0], x_2=xdata[1], **fit_result.params) for xd, yd, curve, color in zip(xdata, ydata, fit_curves, palette): plt.plot(xd, curve, color=color, alpha=0.5) plt.scatter(xd, yd, color=color) plt.xlabel('x') plt.ylabel('y') plt.title('Global Fitting, MWE') plt.show()symfit-0.5.4/examples/linear_model_fit.py000066400000000000000000000015741412237106600205300ustar00rootroot00000000000000from __future__ import division, print_function from symfit import Parameter, Variable, Fit import numpy as np import matplotlib.pyplot as plt import seaborn as sns # sns.color_palette("Blues") # sns.set_palette(sns.color_palette("Paired")) palette = sns.color_palette() sns.set_palette(palette) # print sns.color_palette("husl", 8) a = Parameter() b = Parameter() x = Variable() model = a * x + b xdata = np.linspace(0, 100, 100) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=(100,)) b_vec = np.random.normal(100, scale=2.0, size=(100,)) ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 fit = Fit(model, xdata, ydata) fit_result = fit.execute() print(fit_result) y = model(x=xdata, **fit_result.params) sns.regplot(xdata, ydata, fit_reg=False) # plt.plot(xdata, y, color=palette[2]) plt.xlim(0, 100) # plt.ylim(0, 2000) plt.show()symfit-0.5.4/examples/multidataset_likelihood.py000066400000000000000000000015561412237106600221370ustar00rootroot00000000000000import numpy as np from symfit import variables, parameters, Fit, exp, Model from symfit.core.objectives import LogLikelihood # Draw samples from a bivariate distribution np.random.seed(42) data1 = np.random.exponential(5.5, 1000) data2 = np.random.exponential(6, 2000) # Define the model for an exponential distribution (numpy style) a, b = parameters('a, b') x1, y1, x2, y2 = variables('x1, y1, x2, y2') model = Model({ y1: (1 / a) * exp(-x1 / a), y2: (1 / b) * exp(-x2 / b) }) print(model) fit = Fit(model, x1=data1, x2=data2, objective=LogLikelihood) fit_result = fit.execute() print(fit_result) # Instead, we could also fit with only one parameter to see which works best model = Model({ y1: (1 / a) * exp(-x1 / a), y2: (1 / a) * exp(-x2 / a) }) fit = Fit(model, x1=data1, x2=data2, objective=LogLikelihood) fit_result = fit.execute() print(fit_result)symfit-0.5.4/examples/ode_reaction_kinetics_simple.py000066400000000000000000000023421412237106600231230ustar00rootroot00000000000000from symfit import variables, Parameter, Fit, D, ODEModel import numpy as np import matplotlib.pyplot as plt # First order reaction kinetics. Data taken from # http://chem.libretexts.org/Core/Physical_Chemistry/Kinetics/Rate_Laws/The_Rate_Law tdata = np.array([0, 0.9184, 9.0875, 11.2485, 17.5255, 23.9993, 27.7949, 31.9783, 35.2118, 42.973, 46.6555, 50.3922, 55.4747, 61.827, 65.6603, 70.0939]) concentration = np.array([0.906, 0.8739, 0.5622, 0.5156, 0.3718, 0.2702, 0.2238, 0.1761, 0.1495, 0.1029, 0.086, 0.0697, 0.0546, 0.0393, 0.0324, 0.026]) # Define our ODE model A, B, t = variables('A, B, t') k = Parameter('k') model = ODEModel( {D(A, t): - k * A, D(B, t): k * A}, initial={t: tdata[0], A: concentration[0], B: 0.0} ) fit = Fit(model, A=concentration, t=tdata) fit_result = fit.execute() print(fit_result) # Plotting, irrelevant to the symfit part. t_axis = np.linspace(0, 80) A_fit, B_fit, = model(t=t_axis, **fit_result.params) plt.scatter(tdata, concentration) plt.plot(t_axis, A_fit, label='[A]') plt.plot(t_axis, B_fit, label='[B]') plt.xlabel('t /min') plt.ylabel('[X] /M') plt.ylim(0, 1) plt.xlim(0, 80) plt.legend(loc=1) plt.show()symfit-0.5.4/examples/ode_reaction_multicomponent.py000066400000000000000000000017141412237106600230200ustar00rootroot00000000000000from symfit import variables, parameters, Fit, D, ODEModel import numpy as np import matplotlib.pyplot as plt import seaborn as sns # Example of the easy of use of the symfit ODE integration syntax. a, b, c, d, t = variables('a, b, c, d, t') k, p, l, m = parameters('k, p, l, m') a0 = 10 b = a0 - d + a # [B] is not independent. model_dict = { D(d, t): l * c * b - m * d, D(c, t): k * a * b - p * c - l * c * b + m * d, D(a, t): - k * a * b + p * c, } model = ODEModel(model_dict, initial={t: 0.0, a: a0, c: 0.0, d: 0.0}) # Generate some data tdata = np.linspace(0, 3, 1000) # Eval the normal way. AA, AAB, BAAB = model(t=tdata, k=0.1, l=0.2, m=.3, p=0.3) plt.plot(tdata, AA, color='red', label='[AA]') plt.plot(tdata, AAB, color='blue', label='[AAB]') plt.plot(tdata, BAAB, color='green', label='[BAAB]') plt.plot(tdata, b(d=BAAB, a=AA), color='pink', label='[B]') # plt.plot(tdata, AA + AAB + BAAB, color='black', label='total') plt.legend() plt.show()symfit-0.5.4/examples/piecewise.py000066400000000000000000000020251412237106600172010ustar00rootroot00000000000000from symfit import parameters, variables, Fit, Piecewise, exp, Eq, Model import numpy as np import matplotlib.pyplot as plt t, y = variables('t, y') a, b, d, k, t0 = parameters('a, b, d, k, t0') # Make a piecewise model y1 = a * t + b y2 = d * exp(- k * t) model = Model({y: Piecewise((y1, t <= t0), (y2, t > t0))}) # As a constraint, we demand equality between the two models at the point t0 # to do this, we substitute t -> t0 and demand equality using `Eq` constraints = [Eq(y1.diff(t).subs({t: t0}), y2.diff(t).subs({t: t0}))] # # Generate example data tdata = np.linspace(0, 4., 200) ydata = model(t=tdata, a=63, b=300, d=2205, k=3, t0=0.65).y ydata = np.random.normal(ydata, 0.05 * ydata) # add 5% noise # Help the fit by bounding the switchpoint between the models and giving initial # guesses t0.min = 0.5 t0.max = 0.8 b.value = 320 fit = Fit(model, t=tdata, y=ydata, constraints=constraints) fit_result = fit.execute() print(fit_result) plt.scatter(tdata, ydata) plt.plot(tdata, model(t=tdata, **fit_result.params).y) plt.show()symfit-0.5.4/examples/piecewise2.py000066400000000000000000000017061412237106600172700ustar00rootroot00000000000000from symfit import parameters, variables, Fit, Piecewise, exp, Eq, Model import numpy as np import matplotlib.pyplot as plt x, y = variables('x, y') a, b, x0 = parameters('a, b, x0') # Make a piecewise model y1 = x**2 - a * x y2 = a * x + b model = Model({y: Piecewise((y1, x <= x0), (y2, x > x0))}) # As a constraint, we demand equality between the two models at the point x0 # to do this, we substitute x -> x0 and demand equality using `Eq` constraints = [ Eq(y1.subs({x: x0}), y2.subs({x: x0})) ] # Generate example data xdata = np.linspace(-4, 4., 50) ydata = model(x=xdata, a=0.0, b=1.0, x0=1.0).y np.random.seed(2) ydata = np.random.normal(ydata, 0.5) # add noise # Help the fit by bounding the switchpoint between the models x0.min = 0.8 x0.max = 1.2 fit = Fit(model, x=xdata, y=ydata, constraints=constraints) fit_result = fit.execute() print(fit_result) plt.scatter(xdata, ydata) plt.plot(xdata, model(x=xdata, **fit_result.params).y) plt.show()symfit-0.5.4/examples/poly_surface_fit.py000066400000000000000000000015621412237106600205660ustar00rootroot00000000000000from symfit import Poly, variables, parameters, Model, Fit import numpy as np import matplotlib.pyplot as plt import seaborn as sns x, y, z = variables('x, y, z') c1, c2 = parameters('c1, c2') # Make a polynomial. Note the `as_expr` to make it symfit friendly. model_dict = { z: Poly( {(2, 0): c1, (0, 2): c1, (1, 1): c2}, x ,y).as_expr() } model = Model(model_dict) print(model) # Generate example data x_vec = np.linspace(-5, 5) y_vec = np.linspace(-10, 10) xdata, ydata = np.meshgrid(x_vec, y_vec) zdata = model(x=xdata, y=ydata, c1=1.0, c2=2.0).z zdata = np.random.normal(zdata, 0.05 * zdata) # add 5% noise # Perform the fit fit = Fit(model, x=xdata, y=ydata, z=zdata) fit_result = fit.execute() zfit = model(x=xdata, y=ydata, **fit_result.params).z print(fit_result) fig, (ax1, ax2) = plt.subplots(1, 2) sns.heatmap(zdata, ax=ax1) sns.heatmap(zfit, ax=ax2) plt.show()symfit-0.5.4/pytest.ini000066400000000000000000000001171412237106600150650ustar00rootroot00000000000000 [pytest] filterwarnings = ignore::RuntimeWarning ignore::UserWarning symfit-0.5.4/requirements.txt000066400000000000000000000000741412237106600163220ustar00rootroot00000000000000numpy >= 1.12 scipy >= 1.0 sympy >= 1.2 toposort setuptools symfit-0.5.4/requirements_docs.txt000066400000000000000000000000651412237106600173320ustar00rootroot00000000000000-r requirements.txt sphinx nbsphinx IPython ipykernelsymfit-0.5.4/setup.cfg000066400000000000000000000015041412237106600146560ustar00rootroot00000000000000[metadata] name = symfit summary = Symbolic Fitting; fitting as it should be. description-file = README.rst author = Martin Roelfs author_email = martin.roelfs@kuleuven.be home-page = https://github.com/tBuLi/symfit license = MIT classifier = Development Status :: 4 - Beta Intended Audience :: Science/Research Topic :: Scientific/Engineering License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 keywords = fit fitting symbolic [bdist_wheel] universal=1 [extras] contrib = matplotlib >= 2.0 # all should be a complete list of all dependencies of all other extras. How to # automate this? all = matplotlib >= 2.0 symfit-0.5.4/setup.py000066400000000000000000000002761412237106600145540ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from setuptools import setup setup( setup_requires=['pbr>=1.9', 'setuptools>=17.1'], pbr=True, ) symfit-0.5.4/symfit/000077500000000000000000000000001412237106600143505ustar00rootroot00000000000000symfit-0.5.4/symfit/__init__.py000066400000000000000000000004131412237106600164570ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from symfit.api import * import pkg_resources try: __version__ = pkg_resources.get_distribution('symfit').version except pkg_resources.DistributionNotFound: __version__ = '' symfit-0.5.4/symfit/api.py000066400000000000000000000010731412237106600154740ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # Overwrite behavior of sympy objects to make more sense for this project. import symfit.core.operators # Expose useful objects. from symfit.core.fit import Fit from symfit.core.models import ( Model, ODEModel, ModelError, CallableModel, CallableNumericalModel, GradientModel ) from symfit.core.fit_results import FitResults from symfit.core.argument import Variable, Parameter from symfit.core.support import variables, parameters, D # Expose the sympy API from sympy import *symfit-0.5.4/symfit/contrib/000077500000000000000000000000001412237106600160105ustar00rootroot00000000000000symfit-0.5.4/symfit/contrib/__init__.py000066400000000000000000000015271412237106600201260ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT import importlib import pkgutil import sys def _import_submodules(package_name): """ Import all submodules of a module, recursively Adapted from: http://stackoverflow.com/a/25083161 :param package_name: Package name :type package_name: str :rtype: dict[types.ModuleType] """ package = sys.modules[package_name] out = {} for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): try: #module = importlib.import_module('{}.{}'.format(package_name, name)) module = importlib.import_module('.{}'.format(name), package=package_name) out[name] = module except: continue return out _submodules = _import_submodules(__name__) __all__ = list(_submodules.keys()) symfit-0.5.4/symfit/contrib/interactive_guess/000077500000000000000000000000001412237106600215335ustar00rootroot00000000000000symfit-0.5.4/symfit/contrib/interactive_guess/__init__.py000066400000000000000000000002301412237106600236370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from .interactive_guess import InteractiveGuess, InteractiveGuess2D symfit-0.5.4/symfit/contrib/interactive_guess/examples/000077500000000000000000000000001412237106600233515ustar00rootroot00000000000000symfit-0.5.4/symfit/contrib/interactive_guess/examples/ODE_2D_example.py000066400000000000000000000024401412237106600263720ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT #!/usr/bin/env python3 # -*- coding: utf-8 -*- from symfit import variables, Parameter, Fit, D, ODEModel import numpy as np from symfit.contrib.interactive_guess import InteractiveGuess # First order reaction kinetics. Data taken from http://chem.libretexts.org/Core/Physical_Chemistry/Kinetics/Rate_Laws/The_Rate_Law tdata = np.array([0, 0.9184, 9.0875, 11.2485, 17.5255, 23.9993, 27.7949, 31.9783, 35.2118, 42.973, 46.6555, 50.3922, 55.4747, 61.827, 65.6603, 70.0939]) concentration_A = np.array([0.906, 0.8739, 0.5622, 0.5156, 0.3718, 0.2702, 0.2238, 0.1761, 0.1495, 0.1029, 0.086, 0.0697, 0.0546, 0.0393, 0.0324, 0.026]) concentration_B = np.max(concentration_A) - concentration_A # Define our ODE model A, B, t = variables('A, B, t') k = Parameter('k') model_dict = { D(A, t): - k * A**2, D(B, t): k * A**2 } model = ODEModel(model_dict, initial={t: tdata[0], A: concentration_A[0], B: 0}) guess = InteractiveGuess(model, A=concentration_A, B=concentration_B, t=tdata, n_points=250) guess.execute() print(guess) fit = Fit(model, A=concentration_A, B=concentration_B, t=tdata) fit_result = fit.execute() print(fit_result) symfit-0.5.4/symfit/contrib/interactive_guess/examples/ODE_example.py000066400000000000000000000021631412237106600260470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT #!/usr/bin/env python3 # -*- coding: utf-8 -*- from symfit import variables, Parameter, Fit, D, ODEModel import numpy as np from symfit.contrib.interactive_guess import InteractiveGuess # First order reaction kinetics. Data taken from # http://chem.libretexts.org/Core/Physical_Chemistry/Kinetics/Rate_Laws/The_Rate_Law tdata = np.array([0, 0.9184, 9.0875, 11.2485, 17.5255, 23.9993, 27.7949, 31.9783, 35.2118, 42.973, 46.6555, 50.3922, 55.4747, 61.827, 65.6603, 70.0939]) concentration = np.array([0.906, 0.8739, 0.5622, 0.5156, 0.3718, 0.2702, 0.2238, 0.1761, 0.1495, 0.1029, 0.086, 0.0697, 0.0546, 0.0393, 0.0324, 0.026]) # Define our ODE model A, t = variables('A, t') k = Parameter('k') model = ODEModel({D(A, t): - k * A}, initial={t: tdata[0], A: concentration[0]}) guess = InteractiveGuess(model, A=concentration, t=tdata, n_points=250) guess.execute() print(guess) fit = Fit(model, A=concentration, t=tdata) fit_result = fit.execute() print(fit_result) symfit-0.5.4/symfit/contrib/interactive_guess/examples/simple_2D_example.py000066400000000000000000000014031412237106600272520ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- """ Created on Mon Dec 7 11:28:58 2015 @author: peterkroon """ from symfit import Variable, Parameter, exp, Fit, Model from symfit.contrib.interactive_guess import InteractiveGuess import numpy as np def distr(x, k, x0): kbT = 4.11 return exp(-k*(x-x0)**2/kbT) x = Variable('x') y = Variable('y') k = Parameter('k', 900) x0 = Parameter('x0', 1.5) model = Model({y: distr(x, k, x0)}) x_data = np.linspace(0, 2.5, 50) y_data = model(x=x_data, k=1000, x0=1).y guess = InteractiveGuess(model, x=x_data, y=y_data, n_points=150) guess.execute() print(guess) fit = Fit(model, x=x_data, y=y_data) fit_result = fit.execute(maxiter=1000) print(fit_result) symfit-0.5.4/symfit/contrib/interactive_guess/examples/simple_3D_example.py000066400000000000000000000016471412237106600272650ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- from symfit import variables, Parameter, exp, Fit, Model from symfit.distributions import Gaussian from symfit.contrib.interactive_guess import InteractiveGuess import numpy as np x, y, z = variables('x, y, z') mu_x = Parameter('mu_x', 10) mu_y = Parameter('mu_y', 10) sig_x = Parameter('sig_x', 1) sig_y = Parameter('sig_y', 1) model = Model({z: Gaussian(x, mu_x, sig_x) * Gaussian(y, mu_y, sig_y)}) x_data = np.linspace(0, 25, 50) y_data = np.linspace(0, 25, 50) x_data, y_data = np.meshgrid(x_data, y_data) x_data = x_data.flatten() y_data = y_data.flatten() z_data = model(x=x_data, y=y_data, mu_x=5, sig_x=0.3, mu_y=10, sig_y=1).z guess = InteractiveGuess(model, x=x_data, y=y_data, z=z_data) guess.execute() print(guess) fit = Fit(model, x=x_data, y=y_data, z=z_data) fit_result = fit.execute() print(fit_result) symfit-0.5.4/symfit/contrib/interactive_guess/examples/vector_valued_2D.py000066400000000000000000000013701412237106600271130ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- from symfit import Variable, Parameter, Fit, Model from symfit.contrib.interactive_guess import InteractiveGuess import numpy as np x = Variable('x') y1 = Variable('y1') y2 = Variable('y2') k = Parameter('k', 900) x0 = Parameter('x0', 1.5) model = { y1: k * (x-x0)**2, y2: x - x0 } model = Model(model) # Generate example data x_data = np.linspace(0, 2.5, 50) data = model(x=x_data, k=1000, x0=1) y1_data = data.y1 y2_data = data.y2 guess = InteractiveGuess(model, x=x_data, y1=y1_data, y2=y2_data, n_points=250) guess.execute() print(guess) fit = Fit(model, x=x_data, y1=y1_data, y2=y2_data) fit_result = fit.execute() print(fit_result) symfit-0.5.4/symfit/contrib/interactive_guess/interactive_guess.py000066400000000000000000000335531412237106600256410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- import matplotlib as mpl mpl.use('Agg') from ... import ODEModel, Derivative, latex from ...core.fit import TakesData from ...core.support import keywordonly, key2str, deprecated import itertools import matplotlib.pyplot as plt import numpy as np from scipy.stats import gaussian_kde plt.ioff() class InteractiveGuess(TakesData): """A class that provides an graphical, interactive way of guessing initial fitting parameters.""" @keywordonly(n_points=50, log_contour=True, percentile=(5, 95)) def __init__(self, *args, **kwargs): """Create a matplotlib window with sliders for all parameters in this model, so that you may graphically guess initial fitting parameters. n_points is the number of points drawn for the plot. Data points are plotted as a blue contour plot, the proposed model as a red line. The errorbars on the proposed model represent the percentile of data within the thresholds. Slider extremes are taken from the parameters where possible. If these are not provided, the minimum is 0; and the maximum is value*2. If no initial value is provided, it defaults to 1. This will modify the values of the parameters present in model. :param n_points: The number of points used for drawing the fitted function. Defaults to 50. :type n_points: int :param log_contour: Whether to plot the contour plot of the log of the density, rather than the density itself. If True, any density less than 1e-7 will be considered 0. Defaults to True. :type log_contour: bool :param percentile: Controls the errorbars on the proposed model, such that the lower errorbar will cover percentile[0]% of the data, and the upper will cover percentile[1]%. Defaults to [5, 95], with corresponds to a 90% percentile. Should be a list of 2 numbers. :type percentile: list """ self.log_contour = kwargs.pop('log_contour') n_points = kwargs.pop('n_points') self.percentile = kwargs.pop('percentile') super(InteractiveGuess, self).__init__(*args, **kwargs) if len(self.independent_data) > 1: self._dimension_strategy = StrategynD(self) else: self._dimension_strategy = Strategy2D(self) # TODO: Some of the code here is specific to the n-D case and should # be moved. self._projections = list(itertools.product(self.model.independent_vars, self.model.dependent_vars)) x_mins = {v: np.min(data) for v, data in self.independent_data.items()} x_maxs = {v: np.max(data) for v, data in self.independent_data.items()} # Stretch the plot 10-20% in the X direction, since that is visually # more appealing. We can't evaluate the model for x < x_initial, so # don't. for x in self.model.independent_vars: plotrange_x = x_maxs[x] - x_mins[x] if not hasattr(self.model, 'initial'): x_mins[x] -= 0.1 * plotrange_x x_maxs[x] += 0.1 * plotrange_x # Generate the points at which to evaluate the model with the proposed # parameters for plotting self._x_points = {v: np.linspace(x_mins[v], x_maxs[v], n_points) for v in self.independent_data} meshgrid = np.meshgrid(*(self._x_points[v] for v in self.independent_data)) self._x_grid = {v: meshgrid[idx].flatten() for idx, v in enumerate(self.independent_data)} # Stretch the plot 20% in the Y direction, since that is visually more # appealing y_mins = {v: np.min(data) for v, data in self.dependent_data.items()} y_maxs = {v: np.max(data) for v, data in self.dependent_data.items()} for y in self.dependent_data: plotrange_y = y_maxs[y] - y_mins[y] y_mins[y] -= 0.1 * plotrange_y y_maxs[y] += 0.1 * plotrange_y self._y_points = {v: np.linspace(y_mins[v], y_maxs[v], n_points) for v in self.dependent_data} self._set_up_figure(x_mins, x_maxs, y_mins, y_maxs) self._set_up_sliders() self._update_plot(None) @keywordonly(show=True, block=True) def execute(self, **kwargs): """ Execute the interactive guessing procedure. :param show: Whether or not to show the figure. Useful for testing. :type show: bool :param block: Blocking call to matplotlib :type show: bool Any additional keyword arguments are passed to matplotlib.pyplot.show(). """ show = kwargs.pop('show') if show: # self.fig.show() # Apparently this does something else, # see https://github.com/matplotlib/matplotlib/issues/6138 plt.show(**kwargs) def _set_up_figure(self, x_mins, x_maxs, y_mins, y_maxs): """ Prepare the matplotlib figure: make all the subplots; adjust their x and y range; plot the data; and plot an putative function. """ self.fig = plt.figure() # Make room for the sliders: bot = 0.1 + 0.05*len(self.model.params) self.fig.subplots_adjust(bottom=bot) # If these are not ints, matplotlib will crash and burn with an utterly # vague error. nrows = int(np.ceil(len(self._projections)**0.5)) ncols = int(np.ceil(len(self._projections)/nrows)) # Make all the subplots: set the x and y limits, scatter the data, and # plot the putative function. self._plots = {} for plotnr, proj in enumerate(self._projections, 1): x, y = proj if Derivative(y, x) in self.model: title_format = '$\\frac{{\\partial {dependant}}}{{\\partial {independant}}} = {expression}$' else: title_format = '${dependant}({independant}) = {expression}$' plotlabel = title_format.format( dependant=latex(y, mode='plain'), independant=latex(x, mode='plain'), expression=latex(self.model[y], mode='plain')) ax = self.fig.add_subplot(ncols, nrows, plotnr, label=plotlabel) ax.set_title(ax.get_label()) ax.set_ylim(y_mins[y], y_maxs[y]) ax.set_xlim(x_mins[x], x_maxs[x]) ax.set_xlabel('${}$'.format(x)) ax.set_ylabel('${}$'.format(y)) self._plot_data(proj, ax) plot = self._plot_model(proj, ax) self._plots[proj] = plot def _set_up_sliders(self): """ Creates an slider for every parameter. """ i = 0.05 self._sliders = {} for param in self.model.params: if not param.fixed: axbg = 'lightgoldenrodyellow' else: axbg = 'red' # start-x, start-y, width, height ax = self.fig.add_axes((0.162, i, 0.68, 0.03), facecolor=axbg, label=param) val = param.value if not hasattr(param, 'min') or param.min is None: minimum = 0 else: minimum = param.min if not hasattr(param, 'max') or param.max is None: maximum = 2 * val else: maximum = param.max slid = plt.Slider(ax, param, minimum, maximum, valinit=val, valfmt='% 5.4g') self._sliders[param] = slid slid.on_changed(self._update_plot) i += 0.05 def _plot_data(self, proj, ax): """Defers plotting the data to self._dimension_strategy""" return self._dimension_strategy.plot_data(proj, ax) def _plot_model(self, proj, ax): """Defers plotting the proposed model to self._dimension_strategy""" return self._dimension_strategy.plot_model(proj, ax) def _update_specific_plot(self, indep_var, dep_var): """Defers updating the proposed model to self._dimension_strategy""" return self._dimension_strategy.update_plot(indep_var, dep_var) def _update_plot(self, _): """Callback to redraw the plot to reflect the new parameter values.""" # Since all sliders call this same callback without saying who they are # I need to update the values for all parameters. This can be # circumvented by creating a seperate callback function for each # parameter. for param in self.model.params: param.value = self._sliders[param].val for indep_var, dep_var in self._projections: self._update_specific_plot(indep_var, dep_var) def _eval_model(self): """ Convenience method for evaluating the model with the current parameters :return: named tuple with results """ arguments = self._x_grid.copy() arguments.update({param: param.value for param in self.model.params}) return self.model(**key2str(arguments)) def __str__(self): """ Represent the guesses in a human readable way. :return: string with the guessed values. """ msg = 'Guessed values:\n' for param in self.model.params: msg += '{}: {}\n'.format(param.name, param.value) return msg class Strategy2D: """ A strategy that describes how to plot a model that depends on a single independent variable, and how to update that plot. """ def __init__(self, interactive_guess): self.ig = interactive_guess def plot_data(self, proj, ax): """ Creates and plots a scatter plot of the original data. """ x, y = proj ax.scatter(self.ig.independent_data[x], self.ig.dependent_data[y], c='b') def plot_model(self, proj, ax): """ Plots the model proposed for the projection proj on ax. """ x, y = proj y_vals = getattr(self.ig._eval_model(), y.name) x_vals = self.ig._x_points[x] plot, = ax.plot(x_vals, y_vals, c='red') return plot def update_plot(self, indep_var, dep_var): """ Updates the plot of the proposed model. """ evaluated_model = self.ig._eval_model() plot = self.ig._plots[(indep_var, dep_var)] y_vals = getattr(evaluated_model, dep_var.name) x_vals = self.ig._x_points[indep_var] plot.set_data(x_vals, y_vals) class StrategynD: """ A strategy that describes how to plot a model that depends on a multiple independent variables, and how to update that plot. """ def __init__(self, interactive_guess): self.ig = interactive_guess def plot_data(self, proj, ax): """ Creates and plots the contourplot of the original data. This is done by evaluating the density of projected datapoints on a grid. """ x, y = proj x_data = self.ig.independent_data[x] y_data = self.ig.dependent_data[y] projected_data = np.column_stack((x_data, y_data)).T kde = gaussian_kde(projected_data) xx, yy = np.meshgrid(self.ig._x_points[x], self.ig._y_points[y]) x_grid = xx.flatten() y_grid = yy.flatten() contour_grid = kde.pdf(np.column_stack((x_grid, y_grid)).T) # This is an fugly kludge, but it seems nescessary to make low density # areas show up. if self.ig.log_contour: contour_grid = np.log(contour_grid) vmin = -7 else: vmin = None ax.contourf(xx, yy, contour_grid.reshape(xx.shape), 50, vmin=vmin, cmap='Blues') def plot_model(self, proj, ax): """ Plots the model proposed for the projection proj on ax. """ x, y = proj evaluated_model = self.ig._eval_model() y_vals = getattr(evaluated_model, y.name) x_vals = self.ig._x_grid[x] plot = ax.errorbar(x_vals, y_vals, xerr=0, yerr=0, c='red') return plot def update_plot(self, indep_var, dep_var): """ Updates the plot of the proposed model. """ evaluated_model = self.ig._eval_model() y_vals = getattr(evaluated_model, dep_var.name) x_vals = self.ig._x_grid[indep_var] x_plot_data = [] y_plot_data = [] y_plot_error = [] # TODO: Numpy magic # We need the error interval for every plotted point, so find all # the points plotted at x=x_i, and do some statistics on those. # Since all the points are on a grid made by meshgrid, the error # in x will alwys be 0. for x_val in self.ig._x_points[indep_var]: # We get away with this instead of digitize because x_vals is # on a grid made with meshgrid idx_mask = x_vals == x_val xs = x_vals[idx_mask] x_plot_data.append(xs[0]) ys = y_vals[idx_mask] y_plot_data.append(np.mean(ys)) y_error = np.percentile(ys, self.ig.percentile) y_plot_error.append(y_error) x_plot_data = np.array(x_plot_data) y_plot_data = np.array(y_plot_data) y_plot_error = np.array(y_plot_error) xs = np.column_stack((x_plot_data, x_plot_data)) yerr = y_plot_error + y_plot_data[:, np.newaxis] y_segments = np.dstack((xs, yerr)) plot_line, caps, error_lines = self.ig._plots[(indep_var, dep_var)] plot_line.set_data(x_plot_data, y_plot_data) error_lines[1].set_segments(y_segments) class InteractiveGuess2D(InteractiveGuess): @deprecated(InteractiveGuess) def __init__(self, *args, **kwargs): # Deprecated as of 01/06/2017 super(InteractiveGuess2D, self).__init__(*args, **kwargs) symfit-0.5.4/symfit/contrib/interactive_guess/tests/000077500000000000000000000000001412237106600226755ustar00rootroot00000000000000symfit-0.5.4/symfit/contrib/interactive_guess/tests/test_interactive_fit.py000066400000000000000000000212651412237106600274730ustar00rootroot00000000000000# -*- coding: utf-8 -*- from symfit.contrib import interactive_guess from symfit import Variable, Parameter, exp, latex, variables, ODEModel, D from symfit.distributions import Gaussian import numpy as np import unittest import matplotlib.colors import matplotlib.pyplot as plt plt.ioff() def distr(x, k, x0): kbT = 4.11 return exp(-k*(x-x0)**2/kbT) # Because sympy has issues with large-ish numpy arrays and broadcasting def np_distr(x, k, x0): kbT = 4.11 return np.exp(-k*(x-x0)**2/kbT) class Gaussian2DInteractiveGuessTest(unittest.TestCase): @classmethod def setUpClass(cls): np.random.seed(0) x = Variable('x') y = Variable('y') k = Parameter('k', 900) x0 = Parameter('x0', 1.5) cls.k = k cls.x0 = x0 model = {y: distr(x, k, x0)} x_data = np.linspace(0, 2.5, 50) y_data = model[y](x=x_data, k=1000, x0=1) cls.guess = interactive_guess.InteractiveGuess(model, x=x_data, y=y_data) def test_number_of_sliders(self): self.assertEqual(len(self.guess._sliders), 2) def test_slider_labels(self): for parameter in self.guess.model.params: self.assertEqual(self.guess._sliders[parameter].ax.get_label(), parameter.name) def test_slider_callback_parameter_values(self): new_val = np.random.random() other = self.guess.model.params[1].value self.guess._sliders[self.guess.model.params[0]].set_val(new_val) try: self.assertEqual(self.guess.model.params[0].value, new_val) self.assertEqual(self.guess.model.params[1].value, other) finally: self.guess._sliders[self.guess.model.params[0]].reset() def test_slider_callback_data(self): x = self.guess.model.independent_vars[0] x_points = self.guess._x_points[x] hi = np.max(x_points) lo = np.min(x_points) new_x = (hi - lo) * np.random.random() + lo new_k = 2000 * np.random.random() self.guess._sliders[self.k].set_val(new_k) self.guess._sliders[self.x0].set_val(new_x) try: kbT = 4.11 true_data = np_distr(x_points, new_k, new_x) actual_data = self.guess._plots[self.guess._projections[0]].get_ydata() self.assertTrue(np.allclose(true_data, actual_data)) finally: self.guess._sliders[self.k].reset() self.guess._sliders[self.x0].reset() def test_get_data(self): y = self.guess.model.dependent_vars[0] x = self.guess.model.independent_vars[0] x_points = self.guess._x_points[x] k = self.k.value x0 = self.x0.value kbT = 4.11 true_y = np_distr(x_points, k, x0) data = self.guess._eval_model() actual_y = data.y actual_x = self.guess._x_points[x] self.assertTrue(np.allclose(x_points, actual_x) and np.allclose(true_y, actual_y)) def test_number_of_projections(self): self.assertEqual(len(self.guess._projections), 1) def test_number_of_plots(self): self.assertEqual(len(self.guess._plots), 1) def test_plot_titles(self): for proj in self.guess._projections: x, y = proj plot = self.guess._plots[proj] plotlabel = '${}({}) = {}$'.format( latex(y, mode='plain'), latex(x, mode='plain'), latex(self.guess.model[y], mode='plain')) self.assertEqual(plot.axes.get_title(), plotlabel) def test_plot_colors(self): for plot in self.guess._plots.values(): color = matplotlib.colors.ColorConverter().to_rgb(plot.get_color()) self.assertEqual(color, (1, 0, 0)) class VectorValuedTest(unittest.TestCase): @classmethod def setUpClass(cls): x = Variable('x') y1 = Variable('y1') y2 = Variable('y2') k = Parameter('k', 900) x0 = Parameter('x0', 1.5) model = {y1: k * (x-x0)**2, y2: x - x0} x_data = np.linspace(0, 2.5, 50) y1_data = model[y1](x=x_data, k=1000, x0=1) y2_data = model[y2](x=x_data, k=1000, x0=1) cls.guess = interactive_guess.InteractiveGuess(model, x=x_data, y1=y1_data, y2=y2_data) # plt.close(cls.fit.fig) def test_number_of_projections(self): self.assertEqual(len(self.guess._projections), 2) def test_number_of_plots(self): self.assertEqual(len(self.guess._plots), 2) def test_plot_titles(self): for proj in self.guess._projections: x, y = proj plot = self.guess._plots[proj] plotlabel = '${}({}) = {}$'.format( latex(y, mode='plain'), latex(x, mode='plain'), latex(self.guess.model[y], mode='plain')) self.assertEqual(plot.axes.get_title(), plotlabel) class Gaussian3DInteractiveFitTest(unittest.TestCase): @classmethod def setUpClass(cls): mean = (0.6,0.4) # x, y mean 0.6, 0.4 cov = [[0.2**2,0],[0,0.1**2]] data = np.random.multivariate_normal(mean, cov, 1000000) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:,0], data[:,1], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False) # xdata = np.dstack((xx, yy)).T # T because np fucks up conventions. x0 = Parameter('x0', value=0.6) sig_x = Parameter('sig_x', value=0.2, min=0.0) x = Variable('x') y0 = Parameter('y0', value=0.4) sig_y = Parameter('sig_y', value=0.1, min=0.0) A = Parameter('A') y = Variable('y') z = Variable('z') g = {z: A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y)} cls.g = g # cls.xdata = xdata # cls.ydata = ydata cls.guess = interactive_guess.InteractiveGuess(g, x=xx.flatten(), y=yy.flatten(), z=ydata.flatten()) # plt.close(cls.fit.fig) def test_number_of_projections(self): self.assertEqual(len(self.guess._projections), 2) def test_number_of_plots(self): self.assertEqual(len(self.guess._plots), 2) def test_plot_titles(self): for proj in self.guess._projections: x, y = proj plot = self.guess._plots[proj][0] plotlabel = '${}({}) = {}$'.format(latex(y, mode='plain'), latex(x, mode='plain'), latex(self.guess.model[y], mode='plain')) self.assertEqual(plot.axes.get_title(), plotlabel) class ODE2DTest(unittest.TestCase): @classmethod def setUpClass(cls): # First order reaction kinetics. Data taken from http://chem.libretexts.org/Core/Physical_Chemistry/Kinetics/Rate_Laws/The_Rate_Law tdata = np.array([0, 0.9184, 9.0875, 11.2485, 17.5255, 23.9993, 27.7949, 31.9783, 35.2118, 42.973, 46.6555, 50.3922, 55.4747, 61.827, 65.6603, 70.0939]) concentration_A = np.array([0.906, 0.8739, 0.5622, 0.5156, 0.3718, 0.2702, 0.2238, 0.1761, 0.1495, 0.1029, 0.086, 0.0697, 0.0546, 0.0393, 0.0324, 0.026]) concentration_B = np.max(concentration_A) - concentration_A # Define our ODE model A, B, t = variables('A, B, t') k = Parameter('k') model_dict = { D(A, t): - k * A ** 2, D(B, t): k * A ** 2 } model = ODEModel(model_dict, initial={t: tdata[0], A: concentration_A[0], B: 0}) cls.guess = interactive_guess.InteractiveGuess(model, A=concentration_A, B=concentration_B, t=tdata, n_points=250) # plt.close(cls.fit.fig) def test_number_of_projections(self): self.assertEqual(len(self.guess._projections), 2) def test_number_of_plots(self): self.assertEqual(len(self.guess._plots), 2) def test_plot_titles(self): for proj in self.guess._projections: x, y = proj plot = self.guess._plots[proj] title_format = '$\\frac{{\\partial {dependant}}}{{\\partial {independant}}} = {expression}$' plotlabel = title_format.format(dependant=latex(y, mode='plain'), independant=latex(x, mode='plain'), expression=latex(self.guess.model[y], mode='plain')) self.assertEqual(plot.axes.get_title(), plotlabel) if __name__ == '__main__': unittest.main() symfit-0.5.4/symfit/core/000077500000000000000000000000001412237106600153005ustar00rootroot00000000000000symfit-0.5.4/symfit/core/__init__.py000066400000000000000000000001511412237106600174060ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT __author__ = 'tBuLi' symfit-0.5.4/symfit/core/_repeatable_partial.py000066400000000000000000000030041412237106600216260ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from functools import partial class repeatable_partial(partial): """ In python < 3.5, stacked partials on the same function do not add more args and kwargs to the function being partialed, but rather partial the partial. This is unlogical behavior, which has been corrected in py35. This objects rectifies this behavior in earlier python versions as well. """ def __new__(*args, **keywords): """ This is essentially just a copy-paste of python 3.5's __new__ method, but made python 2.7 friendly. :param args: :param keywords: :return: """ if not args: raise TypeError("descriptor '__new__' of partial needs an argument") if len(args) < 2: raise TypeError("type 'partial' takes at least one argument") cls = args[0] func = args[1] args = args[2:] if not callable(func): raise TypeError("the first argument must be callable") args = tuple(args) # I would prefer isinstance(func, partial), but the standard lib does # this so best copy that for now. if hasattr(func, "func"): args = func.args + args tmpkw = func.keywords.copy() tmpkw.update(keywords) keywords = tmpkw del tmpkw func = func.func return super(repeatable_partial, cls).__new__(cls, func, *args, **keywords)symfit-0.5.4/symfit/core/argument.py000066400000000000000000000117231412237106600175000ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from collections import defaultdict import numbers import warnings from sympy.core.symbol import Symbol class Argument(Symbol): """ Base class for :mod:`symfit` symbols. This helps make :mod:`symfit` symbols distinguishable from :mod:`sympy` symbols. If no name is explicitly provided a name will be generated. For example:: y = Variable() print(y.name) >> 'x_0' y = Variable('y') print(y.name) >> 'y' """ __slots__ = ['_argument_index', '_argument_name'] # TODO: Make sure this also survives a pickle/unpickle to a fresh(!) # interpreter. _argument_indices = defaultdict(int) def __new__(cls, name=None, **assumptions): """ Create a new ``Argument``. See :class:`~sympy.core.symbol.Symbol` for more information. """ assumptions['real'] = True # Generate a dummy name if not name: # Throw a warning that is is better to explicitly give names. warnings.warn( 'It is recommended to provide names to {} explicitly' ' as automatic generation of names will be dropped in ' 'future `symfit` versions.'.format(cls.__name__), DeprecationWarning, stacklevel=2 ) name = '{}_{}'.format(cls._argument_name, cls._argument_indices[cls]) instance = super(Argument, cls).__new__(cls, name, **assumptions) else: instance = super(Argument, cls).__new__(cls, name, **assumptions) instance._argument_index = cls._argument_indices[cls] cls._argument_indices[cls] += 1 return instance def __init__(self, name=None, *args, **assumptions): # TODO: A more careful look at Symbol.__init__ is needed! However, it # seems we don't have to pass anything on to it. if name is not None: self.name = name super(Argument, self).__init__() def __getstate__(self): state = super(Argument, self).__getstate__() state.update({slot: getattr(self, slot) for slot in self.__slots__ if hasattr(self, slot)}) return state def _sympystr(self, printer, *args, **kwargs): return printer.doprint(self.name) _lambdacode = _sympystr _numpycode = _sympystr _pythoncode = _sympystr class Parameter(Argument): """ Parameter objects are used to facilitate bounds on function parameters. Important change from `symfit>0.4.1`: the name needs to be the first keyword, followed by the guess value. If no name is provided, the initial value can be passed as a keyword argument, e.g.: `value=0.1`. A generic name will then be generated. """ # Parameter index to be assigned to generated nameless parameters __slots__ = ['min', 'max', 'fixed', 'value'] _argument_name = 'par' def __new__(cls, name=None, value=1.0, min=None, max=None, fixed=False, **kwargs): try: return super(Parameter, cls).__new__(cls, name, **kwargs) except TypeError as err: if isinstance(name, numbers.Number): raise TypeError('In symfit >0.4.1 the value needs to be assigned ' 'as the second argument or by keyword argument.') else: raise err def __init__(self, name=None, value=1.0, min=None, max=None, fixed=False, **assumptions): """ :param name: Name of the Parameter. :param value: Initial guess value. :param min: Lower bound on the parameter value. :param max: Upper bound on the parameter value. :param fixed: Fix the parameter to ``value`` during fitting. :type fixed: bool :param assumptions: assumptions to pass to ``sympy``. """ super(Parameter, self).__init__(name, **assumptions) self.value = value self.fixed = fixed if min is not None and max is not None and min > max: if not self.fixed: raise ValueError('The value of `min` should be less than or' ' equal to the value of `max`.') else: self.min = min self.max = max def __eq__(self, other): """ Parameters are considered equal when their name, assumptions, and bounds are considered the same. """ equal = super(Parameter, self).__eq__(other) if equal is NotImplemented: return equal if not equal: return False else: return (self.min == other.min and self.max == other.max and self.fixed == other.fixed and self.value == other.value) __hash__ = Argument.__hash__ class Variable(Argument): """ Variable type.""" # Variable index to be assigned to generated nameless variables _argument_name = 'var' __slots__ = ()symfit-0.5.4/symfit/core/fit.py000066400000000000000000000622051412237106600164410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from collections import OrderedDict from collections.abc import Sequence import sys import sympy import numpy as np from symfit.core.argument import Variable from .support import keywordonly, key2str from .minimizers import ( BFGS, SLSQP, LBFGSB, BaseMinimizer, GradientMinimizer, HessianMinimizer, ConstrainedMinimizer, MINPACK, ChainedMinimizer, BasinHopping ) from .objectives import ( LeastSquares, BaseObjective, MinimizeModel, VectorLeastSquares, LogLikelihood, HessianObjectiveJacApprox ) from .models import BaseModel, Model, BaseNumericalModel, CallableModel if sys.version_info >= (3,0): import inspect as inspect_sig else: import funcsigs as inspect_sig class TakesData(object): """ An base class for everything that takes data. Most importantly, it takes care of linking the provided data to variables. The allowed variables are extracted from the model. """ @keywordonly(absolute_sigma=None) def __init__(self, model, *ordered_data, **named_data): """ :param model: (dict of) sympy expression or ``Model`` object. :param bool absolute_sigma: True by default. If the sigma is only used for relative weights in your problem, you could consider setting it to False, but if your sigma are measurement errors, keep it at True. Note that curve_fit has this set to False by default, which is wrong in experimental science. :param ordered_data: data for dependent, independent and sigma variables. Assigned in the following order: independent vars are assigned first, then dependent vars, then sigma's in dependent vars. Within each group they are assigned in alphabetical order. :param named_data: assign dependent, independent and sigma variables data by name. Standard deviation can be provided to any variable. They have to be prefixed with sigma\_. For example, let x be a Variable. Then sigma_x will give the stdev in x. """ absolute_sigma = named_data.pop('absolute_sigma') if isinstance(model, BaseModel): self.model = model else: self.model = Model(model) # Handle ordered_data and named_data according to the allowed names. signature = self._make_signature() try: bound_arguments = signature.bind(*ordered_data, **named_data) except TypeError as err: for var in self.model.vars: if var.name.startswith(Variable._argument_name): raise type(err)(str(err) + '. Some of your Variable\'s are unnamed. That might be the cause of this Error: make sure you use e.g. x = Variable(\'x\')') elif isinstance(var, sympy.Derivative): # Include a very strong warning with this error. raise RuntimeWarning( 'The model contains derivatives in its definition. ' 'Are you sure you don\'t mean to use `symfit.ODEModel`?' ) else: raise err # Include default values in bound_argument object for param in signature.parameters.values(): if param.name not in bound_arguments.arguments: bound_arguments.arguments[param.name] = param.default original_data = bound_arguments.arguments # ordereddict of the data self.data = original_data.copy() for var in self.model.vars: # Identify data by their Variable, not their variable names. # But anything that is not a part of model should not be thrown away if var.name in self.data: self.data[var] = self.data.pop(var.name) # Change the type to array if no array operations are supported. # We don't want to break duck-typing, hence the try-except. for var, dataset in self.data.items(): try: dataset**2 except TypeError: if dataset is not None: self.data[var] = np.array(dataset) self.sigmas_provided = any(value is not None for value in self.sigma_data.values()) # Replace sigmas that are constant by an array of that constant for var, sigma in zip(self.dependent_data, self.sigma_data): try: iter(self.data[sigma]) except TypeError: # not iterable if self.data[var] is not None and self.data[sigma] is None: self.data[sigma] = np.ones(self.data[var].shape) elif self.data[var] is not None: self.data[sigma] *= np.ones(self.data[var].shape) # If user gives a preference, use that. Otherwise, use True if at least one sigma is # given, False if no sigma is given. if absolute_sigma is not None: self.absolute_sigma = absolute_sigma else: for sigma in self.sigma_data: # Check if the user provided sigmas in the original data. # If so, interpret sigmas as measurement errors if original_data[sigma.name] is not None: self.absolute_sigma = True break else: self.absolute_sigma = False def _make_signature(self): """ Make a :class:`inspect.Signature` object corresponding to ``self.model``. :return: :class:`inspect.Signature` object corresponding to ``self.model``. """ parameters = self._make_parameters(self.model) parameters = sorted(parameters, key=lambda p: p.default is None) return inspect_sig.Signature(parameters=parameters) @staticmethod def _make_parameters(model, none_allowed=None): """ Based on a model, return the inspect.Parameter objects needed to satisfy all the variables of this model. :param model: instance of model :param none_allowed: If provided, this has to be a sequence of :class:`symfit.core.argument.Variable` whose values are set to ``None`` by default. If not provided, this will be set to sigma variables only. :return: list of :class:`inspect.Parameter` corresponding to all the external variables of the model. """ if none_allowed is None: none_allowed = model.sigmas.values() parameters = [ inspect_sig.Parameter( var.name, kind=inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, default=None if var in none_allowed else inspect_sig.Parameter.empty ) for var in model.vars ] return parameters @property def dependent_data(self): """ Read-only Property :return: Data belonging to each dependent variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ return OrderedDict((var, self.data[var]) for var in self.model.dependent_vars if var in self.data) @property def independent_data(self): """ Read-only Property :return: Data belonging to each independent variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ return OrderedDict((var, self.data[var]) for var in self.model.independent_vars) @property def sigma_data(self): """ Read-only Property :return: Data belonging to each sigma variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ sigmas = self.model.sigmas return OrderedDict((sigmas[var], self.data[sigmas[var]]) for var in self.model.dependent_vars if sigmas[var] in self.data) @property def data_shapes(self): """ Returns the shape of the data. In most cases this will be the same for all variables of the same type, if not this raises an Exception. Ignores variables which are set to None by design so we know that those None variables can be assumed to have the same shape as the other in calculations where this is needed, such as the covariance matrix. :return: Tuple of all independent var shapes, dependent var shapes. """ independent_shapes = [] for var, data in self.independent_data.items(): if data is not None: independent_shapes.append(data.shape) dependent_shapes = [] for var, data in self.dependent_data.items(): if data is not None: dependent_shapes.append(data.shape) return list(set(independent_shapes)), list(set(dependent_shapes)) @property def initial_guesses(self): """ :return: Initial guesses for every parameter. """ return np.array([param.value for param in self.model.params]) class HasCovarianceMatrix(TakesData): """ Mixin class for calculating the covariance matrix for any model that has a well-defined Jacobian :math:`J`. The covariance is then approximated as :math:`J^T W J`, where W contains the weights of each data point. Supports vector valued models, but is unable to estimate covariances for those, just variances. Therefore, take the result with a grain of salt for vector models. """ def _covariance_matrix(self, best_fit_params, objective): # Helper function for self.covariance_matrix. try: hess = objective.eval_hessian(**key2str(best_fit_params)) except AttributeError: # Some models do not have an eval_hessian, in which case we give up return None else: if hess is None: return hess try: # The squeezing to a matrix is required for MinimizeModel objectives hess_inv = np.linalg.inv(np.atleast_2d(np.squeeze(hess))) except np.linalg.LinAlgError: return None if isinstance(objective, LeastSquares): # Calculate the covariance for a least squares method. # https://www8.cs.umu.se/kurser/5DA001/HT07/lectures/lsq-handouts.pdf # Residual sum of squares rss = 2 * objective(**key2str(best_fit_params)) # Degrees of freedom raw_dof = np.sum([np.product(shape) for shape in self.data_shapes[1]]) dof = raw_dof - len(self.model.params) if self.absolute_sigma: # When interpreting as measurement error, we do not rescale. s2 = 1 else: s2 = rss / dof cov_mat = s2 * hess_inv return cov_mat else: # The inverse hessian is the covariance matrix for Loglikelihood and # also for objectives in general. return hess_inv def covariance_matrix(self, best_fit_params): """ Given best fit parameters, this function finds the covariance matrix. This matrix gives the (co)variance in the parameters. :param best_fit_params: ``dict`` of best fit parameters as given by .best_fit_params() :return: covariance matrix. """ cov_matrix = self._covariance_matrix(best_fit_params, objective=self.objective) if cov_matrix is None: # If the covariance matrix could not be computed we try again by # approximating the hessian with the jacobian. # VectorLeastSquares should be turned into a LeastSquares for # cov matrix calculation if self.objective.__class__ is VectorLeastSquares: base = LeastSquares else: base = self.objective.__class__ class HessApproximation(base, HessianObjectiveJacApprox): """ Class which impersonates ``base``, but which returns zeros for the models Hessian. This will effectively result in the calculation of the approximate Hessian by calculating outer(J.T, J) when calling ``base.eval_hessian``. """ objective = HessApproximation(self.objective.model, self.objective.data) cov_matrix = self._covariance_matrix(best_fit_params, objective=objective) return cov_matrix class Fit(HasCovarianceMatrix): """ Your one stop fitting solution! Based on the nature of the input, this object will attempt to select the right fitting type for your problem. If you need very specific control over how the problem is solved, you can pass it the minimizer or objective function you would like to use. Example usage:: a, b = parameters('a, b') x, y = variables('x, y') model = {y: a * x + b} # Fit will use its default settings fit = Fit(model, x=xdata, y=ydata) fit_result = fit.execute() # Use Nelder-Mead instead fit = Fit(model, x=xdata, y=ydata, minimizer=NelderMead) fit_result = fit.execute() # Use Nelder-Mead to get close, and BFGS to polish it off fit = Fit(model, x=xdata, y=ydata, minimizer=[NelderMead, BFGS]) fit_result = fit.execute(minimizer_kwargs=[dict(xatol=0.1), {}]) """ @keywordonly(objective=None, minimizer=None, constraints=None, absolute_sigma=None) def __init__(self, model, *ordered_data, **named_data): """ :param model: (dict of) sympy expression(s) or ``Model`` object. :param constraints: iterable of ``Relation`` objects to be used as constraints. :param bool absolute_sigma: True by default. If the sigma is only used for relative weights in your problem, you could consider setting it to False, but if your sigma are measurement errors, keep it at True. Note that curve_fit has this set to False by default, which is wrong in experimental science. :param objective: Have Fit use your specified objective. Can be one of the predefined `symfit` objectives or any callable which accepts fit parameters and returns a scalar. :param minimizer: Have Fit use your specified :class:`symfit.core.minimizers.BaseMinimizer`. Can be a :class:`~collections.abc.Sequence` of :class:`symfit.core.minimizers.BaseMinimizer`. :param ordered_data: data for dependent, independent and sigma variables. Assigned in the following order: independent vars are assigned first, then dependent vars, then sigma's in dependent vars. Within each group they are assigned in alphabetical order. :param named_data: assign dependent, independent and sigma variables data by name. """ objective = named_data.pop('objective') minimizer = named_data.pop('minimizer') constraints = named_data.pop('constraints') absolute_sigma = named_data.pop('absolute_sigma') # Should be a list of Constraint objects constraints = [] if constraints is None else constraints # Initiate self.model as an instance of BaseModel if it isn't already if isinstance(model, BaseModel): self.model = model else: self.model = Model(model) self.constraints = self._init_constraints(constraints=constraints, model=self.model) # Bind as much as possible the provided arguments. signature = self._make_signature() bound_arguments = signature.bind_partial(*ordered_data, **named_data) # Select objective function to use. Has to be done before calling # super.__init__ self.objective = self._determine_objective( self.model, objective=objective, minimizer=minimizer, bound_arguments=bound_arguments ) super(Fit, self).__init__(self.model, absolute_sigma=absolute_sigma, **bound_arguments.arguments) # Update the data belonging to the constraints. We do this by checking # for the presence of data with the same name as one of the independent # variables of the constraint. If present, we start addressing them by # their Variable instead. for constraint in self.constraints: for var in constraint.vars: if var.name in self.data: self.data[var] = self.data.pop(var.name) # Initialise the objective with data if it's not initialised already if not isinstance(self.objective, BaseObjective): self.objective = self.objective(self.model, self.data) # Select the minimizer on the basis of the provided information. if minimizer is None: minimizer = self._determine_minimizer() # Initialise the minimizer if isinstance(minimizer, Sequence): minimizers = [self._init_minimizer(mini) for mini in minimizer] self.minimizer = self._init_minimizer(ChainedMinimizer, minimizers=minimizers) else: self.minimizer = self._init_minimizer(minimizer) def _make_signature(self): parameters = self._make_parameters(self.model) # Extend the signature with the variables to the constraint. Since # constraints will be turned into MinimizeModel objectives, they only # need independent variables to be provided. for constraint in self.constraints: none_allowed = constraint.dependent_vars + list(constraint.sigmas.values()) parameters.extend( self._make_parameters( constraint, none_allowed=none_allowed ) ) # Make unique while preserving order, and sort by default value so # sigma variables end last unique_parameters = [] for par in parameters: if par not in unique_parameters: unique_parameters.append(par) parameters = sorted(unique_parameters, key=lambda p: p.default is None) return inspect_sig.Signature(parameters=parameters) def _determine_minimizer(self): """ Determine the most suitable minimizer by the presence of bounds or constraints. :return: a subclass of `BaseMinimizer`. """ if self.constraints: return SLSQP elif any([bound is not None for pair in self.model.bounds for bound in pair]): # If any bound is set return LBFGSB else: return BFGS @staticmethod def _determine_objective(model, objective, minimizer, bound_arguments): """ Determine the most suitable objective on the basis of the problem at hand. This could modify ``bound_arguments`` in place accordingly if required! :param model: :class:`symfit.core.models.BaseModel` under consideration. :param objective: objective provided to :class:`symfit.core.fit.Fit` by the user, or ``None``. :param minimizer: :class:`~symfit.core.minimizers.BaseMinimizer` provided by the user, or ``None`` :param bound_arguments: Instance of :class:`inspect.BoundArguments`. :return: a subclass of `BaseObjective`. """ if objective is None: if minimizer is MINPACK: # MINPACK is considered a special snowflake, as its API has to # be considered separately and has its own non standard # objective function. objective = VectorLeastSquares elif (len(model) == 1 and len(model.independent_vars) == 0 and model.dependent_vars[0].name not in bound_arguments.arguments): objective = MinimizeModel else: objective = LeastSquares # Check if the data is compatible with the objective if (objective is LogLikelihood or objective is MinimizeModel or isinstance(objective, (MinimizeModel, LogLikelihood))): # Set dependent vars and corresponding sigmas to None. for var in model.dependent_vars + list(model.sigmas.values()): if var.name not in bound_arguments.arguments: bound_arguments.arguments[var.name] = None else: raise TypeError( 'A value was provided for `{}`, however for {} ' 'fitting the dependent variable cannot have a value ' 'assigned to it.'.format(var.name, objective) ) return objective def _init_minimizer(self, minimizer, **minimizer_options): """ Takes a :class:`~symfit.core.minimizers.BaseMinimizer` and instantiates it, passing the jacobian and constraints as appropriate for the minimizer. :param minimizer: :class:`~symfit.core.minimizers.BaseMinimizer` to instantiate. :param **minimizer_options: Further options to be passed to the minimizer on instantiation. :returns: instance of :class:`~symfit.core.minimizers.BaseMinimizer`. """ if isinstance(minimizer, BaseMinimizer): return minimizer if issubclass(minimizer, BasinHopping): minimizer_options['local_minimizer'] = self._init_minimizer( self._determine_minimizer() ) if issubclass(minimizer, GradientMinimizer): # If an analytical version of the Jacobian exists we should use # that, otherwise we let the minimizer estimate it itself. # Hence the check of jacobian_model, as this is the # py function version of the analytical jacobian. if hasattr(self.model, 'eval_jacobian') and hasattr(self.objective, 'eval_jacobian'): minimizer_options['jacobian'] = self.objective.eval_jacobian if issubclass(minimizer, HessianMinimizer): # If an analytical version of the Hessian exists we should use # that, otherwise we let the minimizer estimate it itself. # Hence the check of hessian_model, as this is the # py function version of the analytical hessian. if hasattr(self.model, 'eval_hessian') and hasattr(self.objective, 'eval_hessian'): minimizer_options['hessian'] = self.objective.eval_hessian if issubclass(minimizer, ConstrainedMinimizer): # set the constraints as MinimizeModel. The dependent vars of the # constraint are set to None since their value is irrelevant. constraint_objectives = [] for constraint in self.constraints: data = self.data # No copy, share state constraint_objectives.append(MinimizeModel(constraint, data)) minimizer_options['constraints'] = constraint_objectives return minimizer(self.objective, self.model.params, **minimizer_options) def _init_constraints(self, constraints, model): """ Takes the user provided constraints and converts them to a list of ``type(model)`` objects, which are extended to also have the parameters of ``model``. :param constraints: iterable of :class:`~sympy.core.relational.Relation` objects. :return: list of :class:`~symfit.core.models.BaseModel` objects. The exact type will depend on the type of ``model``. """ con_models = [] for constraint in constraints: if hasattr(constraint, 'constraint_type'): con_models.append(constraint) else: if isinstance(model, BaseNumericalModel): # Numerical models need to be provided with a connectivity # mapping, so we cannot use the type of model. Instead, # use the bare minimum for an analytical model for the # constraint. ToDo: once GradientNumericalModel etc are # introduced, pick the corresponding analytical model for # the constraint. con_models.append( CallableModel.as_constraint(constraint, model) ) else: con_models.append( model.__class__.as_constraint(constraint, model) ) return con_models def execute(self, **minimize_options): """ Execute the fit. :param minimize_options: keyword arguments to be passed to the specified minimizer. :return: FitResults instance """ minimizer_ans = self.minimizer.execute(**minimize_options) minimizer_ans.covariance_matrix = self.covariance_matrix( dict(zip(self.model.params, minimizer_ans._popt)) ) # Overwrite the DummyModel with the current model minimizer_ans.model = self.model minimizer_ans.minimizer = self.minimizer return minimizer_ans symfit-0.5.4/symfit/core/fit_results.py000066400000000000000000000264471412237106600202320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from collections import OrderedDict import numpy as np from symfit.core.objectives import ( LeastSquares, VectorLeastSquares, LogLikelihood ) from symfit.core.support import keywordonly class FitResults(object): """ Class to display the results of a fit in a nice and unambiguous way. All things related to the fit are available on this class, e.g. - parameter values + stdev - R squared (Regression coefficient.) or other fit quality qualifiers. - fitting status message - covariance matrix - objective and minimizer used. Contains the attribute `params`, which is an :class:`~collections.OrderedDict` containing all the parameter names and their optimized values. Can be `**` unpacked when evaluating :class:`~symfit.core.models.Model`'s. """ @keywordonly(constraints=None) def __init__(self, model, popt, covariance_matrix, minimizer, objective, message, **minimizer_output): """ :param model: :class:`~symfit.core.models.Model` that was fit to. :param popt: best fit parameters, same ordering as in model.params. :param covariance_matrix: covariance matrix. :param minimizer: Minimizer instance used. :param objective: Objective function which was optimized. :param message: Status message returned by the minimizer. :param \**minimizer_output: Raw output as given by the minimizer. """ constraints = minimizer_output.pop('constraints') self.constraints = constraints if constraints is not None else [] self.minimizer_output = minimizer_output self.model = model self.minimizer = minimizer self.objective = objective # LBFGSB returns with python<=3.6 a bit-string as message self.status_message = message.decode() if isinstance(message, bytes) else message self._popt = popt self.params = OrderedDict( [(p.name, value) for p, value in zip(self.model.params, popt)] ) self.covariance_matrix = covariance_matrix self.gof_qualifiers = self._gof_qualifiers() @property def iterations(self): if 'nit' in self.minimizer_output: return self.minimizer_output['nit'] else: return None def __str__(self): """ Pretty print the results as a table. """ res = '\nParameter Value Standard Deviation\n' for p in self.model.params: value = self.value(p) value_str = '{:e}'.format(value) if value is not None else 'None' stdev = self.stdev(p) stdev_str = '{:e}'.format(stdev) if stdev is not None else 'None' res += '{:10}{} {}\n'.format(p.name, value_str, stdev_str, width=20) res += '{:<22} {}\n'.format('Status message', self.status_message) res += '{:<22} {}\n'.format('Number of iterations', self.iterations) res += '{:<22} {}\n'.format('Objective', self.objective) res += '{:<22} {}\n'.format('Minimizer', self.minimizer) res += '\nGoodness of fit qualifiers:\n' res += '\n'.join('{:<22} {}'.format(gof, value) for gof, value in sorted(self.gof_qualifiers.items())) if self.constraints: res += '\n\nConstraints:\n' res += 20 * '-' + '\n' # res += '{:<22} {}\n'.format('Constraint', 'Value') for constraint in self.constraints: # Print the component and the value of the constraint res += 'Question: {} {} 0?\n'.format( constraint.model[constraint.model.dependent_vars[0]], constraint.model.constraint_type.rel_op ) res += 'Answer: {}\n\n'.format(constraint(**self.params)[0]) return res def __getattr__(self, item): """ Return the requested `item` if it can be found in the gof_qualifiers dict. :param item: Name of Goodness of Fit qualifier. :return: Goodness of Fit qualifier if present. """ if 'gof_qualifiers' in vars(self): if item in self.gof_qualifiers: return self.gof_qualifiers[item] raise AttributeError def stdev(self, param): """ Return the standard deviation in a given parameter as found by the fit. :param param: ``Parameter`` Instance. :return: Standard deviation of ``param``. """ try: return np.sqrt(self.variance(param)) except (AttributeError, TypeError): # This happens when variance returns None. return None def value(self, param): """ Return the value in a given parameter as found by the fit. :param param: ``Parameter`` Instance. :return: Value of ``param``. """ return self.params[param.name] def variance(self, param): """ Return the variance in a given parameter as found by the fit. :param param: ``Parameter`` Instance. :return: Variance of ``param``. """ param_number = self.model.params.index(param) try: return self.covariance_matrix[param_number, param_number] except TypeError: # covariance_matrix can be None return None def covariance(self, param_1, param_2): """ Return the covariance between param_1 and param_2. :param param_1: ``Parameter`` Instance. :param param_2: ``Parameter`` Instance. :return: Covariance of the two params. """ param_1_number = self.model.params.index(param_1) param_2_number = self.model.params.index(param_2) return self.covariance_matrix[param_1_number, param_2_number] @staticmethod def _array_safe_dict_eq(one_dict, other_dict): """ Dicts containing arrays are hard to compare. This function uses numpy.allclose to compare arrays, and does normal comparison for all other types. This pretty mich defines FitResult equality, but because there are still some questions on how and if that should be defined, __eq__ has not been implemented. :param one_dict: __dict__ of a FitResults object :param other_dict: __dict__ of a FitResults object :return: bool """ for key in one_dict: try: if key == 'minimizer': assert one_dict[key].__class__ == other_dict[key].__class__ elif key == 'minimizer_output': # Ignore this, because it can contain unexpected terms and # if all the derived attributes are correct I see no reason # why this term shouldn't be at least close enough. pass else: assert one_dict[key] == other_dict[key] except ValueError as err: # When dealing with arrays, we need to use numpy for comparison if isinstance(one_dict[key], dict): assert FitResults._array_safe_dict_eq(one_dict[key], other_dict[key]) else: assert np.allclose(one_dict[key], other_dict[key]) except AssertionError: return False else: return True def __getstate__(self): state = self.__dict__.copy() if hasattr(state['minimizer'], 'minimizers'): # ChainedMinimizer # ToDo: when py27 support is droppend at least this can be replaced # with just pickling the instance, perhaps also for other # minimizers. minimizer_cls = [type(state['minimizer'])] minimizer_cls.extend( [type(minimizer) for minimizer in state['minimizer'].minimizers] ) else: minimizer_cls = type(state['minimizer']) state['minimizer'] = (minimizer_cls, state['minimizer'].objective, state['minimizer'].parameters) return state def __setstate__(self, state): min_class, objective, parameters = state['minimizer'] try: # If min_class is iterable, initiate a ChainedMinimizer. minimizers = [cls(objective, parameters) for cls in min_class[1:]] except TypeError: state['minimizer'] = min_class(objective, parameters) else: state['minimizer'] = min_class[0](objective, parameters, minimizers=minimizers) self.__dict__.update(state) def _gof_qualifiers(self): """ Based on the objective used, we can infer certain goodness of fit (g.o.f.) qualifiers. The ``objective_value`` itself always exists, and then depending on the objective we also get the following: - The coefficient of determination :math:`R^2` and :math:`\\chi^2` for :class:`~symfit.core.objectives.LeastSquares` and :class:`~symfit.core.objectives.VectorLeastSquares`. - Likelihood and log-likelihood for :class:`~symfit.core.objectives.LogLikelihood`. :return: ``dict`` containing goodness of fit qualifiers. """ gof_qualifiers = {} gof_qualifiers['objective_value'] = self.minimizer_output['fun'] if isinstance(self.objective, (LeastSquares, VectorLeastSquares)): R2 = r_squared(self.objective.model, fit_result=self, data=self.objective.data) gof_qualifiers['r_squared'] = R2 if isinstance(self.objective, VectorLeastSquares): # In this case the objective value is the residuals chi_squared = np.sum(gof_qualifiers['objective_value'] ** 2) gof_qualifiers['chi_squared'] = chi_squared elif isinstance(self.objective, LeastSquares): # Undo the normalization to get back chi^2. gof_qualifiers['chi_squared'] = 2 * gof_qualifiers['objective_value'] elif isinstance(self.objective, LogLikelihood): # We undo the minus sign we have included to maximize likelihood gof_qualifiers['log_likelihood'] = - gof_qualifiers['objective_value'] gof_qualifiers['likelihood'] = np.exp(gof_qualifiers['log_likelihood']) return gof_qualifiers def r_squared(model, fit_result, data): """ Calculates the coefficient of determination, R^2, for the fit. (Is not defined properly for vector valued functions.) :param model: Model instance :param fit_result: FitResults instance :param data: data with which the fit was performed. """ # First filter out the dependent vars y_is = [data[var] for var in model.dependent_vars if var in data] x_is = [data[var] for var in model.independent_vars if var in data] y_bars = [np.mean(y_i) if y_i is not None else None for y_i in y_is] f_is = model(*x_is, **fit_result.params)._asdict() # f_is also contains the evaluated interdependent_vars, skip those. f_is = [f_is[var] for var in model.dependent_vars] SS_res = np.sum([np.sum((y_i - f_i)**2) for y_i, f_i in zip(y_is, f_is) if y_i is not None]) SS_tot = np.sum([np.sum((y_i - y_bar)**2) for y_i, y_bar in zip(y_is, y_bars) if y_i is not None]) return 1 - SS_res/SS_totsymfit-0.5.4/symfit/core/keywordonly.py000066400000000000000000000110751412237106600202440ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ This module contains support functions and convenience methods used throughout symfit. Some are used predominantly internally, others are designed for users. """ from __future__ import print_function from collections import OrderedDict import sys if sys.version_info >= (3,0): import inspect as inspect_sig from functools import wraps else: import funcsigs as inspect_sig from functools32 import wraps class RequiredKeyword(object): """ Flag variable to indicate that this is a required keyword. """ class RequiredKeywordError(Exception): """ Error raised in case a keyword-only argument is not treated as such. """ class keywordonly(object): """ Decorator class which wraps a python 2 function into one with keyword-only arguments. Example:: @keywordonly(floor=True) def f(x, **kwargs): floor = kwargs.pop('floor') return np.floor(x**2) if floor else x**2 This decorator is not much more than:: floor = kwargs.pop('floor') if 'floor' in kwargs else True However, I prefer it's usage because: - it's clear from reading the function declaration there is an option to provide this argument. The information on possible keywords is where you'd expect it to be. - you're guaranteed that the pop works. - It is fully inspect compatible such that sphynx is able to index these properly as keyword only arguments just like it would for native py3 keyword only arguments. Please note that this decorator needs a ** argument on the wrapped function in order to work. """ def __init__(self, **kwonly_arguments): self.kwonly_arguments = kwonly_arguments # Mark which are required self.required_keywords = { kw for kw, value in kwonly_arguments.items() if value is RequiredKeyword } # Transform all into keywordonly inspect.Parameter objects. self.keywordonly_parameters = OrderedDict( (kw, inspect_sig.Parameter(kw, kind=inspect_sig.Parameter.KEYWORD_ONLY, default=value) ) for kw, value in kwonly_arguments.items() ) def __call__(self, func): """ Returns a decorated version of `func`, who's signature now includes the keyword-only arguments. :param func: the function to be decorated :return: the decorated function """ sig = inspect_sig.signature(func) params = [] # A var keyword has to be found for this function to be decorated for name, param in sig.parameters.items(): if param.kind == param.VAR_KEYWORD: # Keyword only's go before the **kwargs parameter. params.extend(self.keywordonly_parameters.values()) params.append(param) break params.append(param) else: raise RequiredKeywordError( 'The keywordonly decorator requires the function to ' 'accept a **kwargs argument.' ) # Update signature sig = sig.replace(parameters=params) func.__signature__ = sig @wraps(func) def wrapped_func(*args, **kwargs): """ :param args: args used to call the function :param kwargs: kwargs used to call the function :return: Wrapped function which behaves like it has keyword-only arguments. :raises: ``RequiredKeywordError`` if not all required keywords were specified. """ bound_args = func.__signature__.bind(*args, **kwargs) # Apply defaults for param in sig.parameters.values(): if param.name not in bound_args.arguments: if param.default is RequiredKeyword: raise RequiredKeywordError( 'Keyword `{}` is a required keyword. ' 'Please provide a value.'.format(param.name) ) elif param.kind == inspect_sig.Parameter.VAR_KEYWORD: bound_args.arguments[param.name] = {} elif param.kind == inspect_sig.Parameter.VAR_POSITIONAL: bound_args.arguments[param.name] = tuple() else: bound_args.arguments[param.name] = param.default return func(*bound_args.args, **bound_args.kwargs) return wrapped_funcsymfit-0.5.4/symfit/core/minimizers.py000066400000000000000000000772461412237106600200600ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT import abc import sys from collections import namedtuple, Counter, OrderedDict from scipy.optimize import ( minimize, differential_evolution, basinhopping, NonlinearConstraint, least_squares, ) from scipy.optimize import BFGS as soBFGS import sympy import numpy as np from .support import keywordonly from .fit_results import FitResults from .objectives import BaseObjective, MinimizeModel from .models import CallableNumericalModel, BaseModel if sys.version_info >= (3,0): import inspect as inspect_sig from functools import wraps else: import funcsigs as inspect_sig from functools32 import wraps DummyModel = namedtuple('DummyModel', 'params') class BaseMinimizer(object): """ ABC for all Minimizers. """ def __init__(self, objective, parameters): """ :param objective: Objective function to be used. :param parameters: List of :class:`~symfit.core.argument.Parameter` instances """ self.parameters = parameters self._fixed_params = [p for p in parameters if p.fixed] self.objective = self._baseobjective_from_callable(objective) # Mapping which we use to track the original, to be used upon pickling self._pickle_kwargs = {'parameters': parameters, 'objective': objective} self.params = [p for p in parameters if not p.fixed] def _baseobjective_from_callable(self, func, objective_type=MinimizeModel): """ symfit works with BaseObjective subclasses internally. If a custom objective is provided, we wrap it into a BaseObjective, MinimizeModel by default. :param func: Callable. If already an instance of BaseObjective, it is returned immediately. If not, it is turned into a BaseObjective of type ``objective_type``. :param objective_type: :return: """ if isinstance(func, BaseObjective) or (hasattr(func, '__self__') and isinstance(func.__self__, BaseObjective)): # The latter condition is added to make sure .eval_jacobian methods # are still considered correct, and not doubly wrapped. return func else: if isinstance(func, BaseModel): model = func else: # Minimize the provided custom objective instead. We therefore # wrap it into a CallableNumericalModel, thats what they are for y = sympy.Dummy() model = CallableNumericalModel( {y: func}, connectivity_mapping={y: set(self.parameters)} ) return objective_type(model, data={}) @abc.abstractmethod def execute(self, **options): """ The execute method should implement the actual minimization procedure, and should return a :class:`~symfit.core.fit_results.FitResults` instance. :param options: options to be used by the minimization procedure. :return: an instance of :class:`~symfit.core.fit_results.FitResults`. """ pass @property def initial_guesses(self): try: return self._initial_guesses except AttributeError: return [p.value for p in self.params] @initial_guesses.setter def initial_guesses(self, vals): self._initial_guesses = vals def __getstate__(self): return {key: value for key, value in self.__dict__.items() if not key.startswith('wrapped_')} def __setstate__(self, state): self.__dict__.update(state) self.__init__(**self._pickle_kwargs) class BoundedMinimizer(BaseMinimizer): """ ABC for Minimizers that support bounds. """ @property def bounds(self): return [(p.min, p.max) for p in self.params] class ConstrainedMinimizer(BaseMinimizer): """ ABC for Minimizers that support constraints """ @keywordonly(constraints=None) def __init__(self, *args, **kwargs): constraints = kwargs.pop('constraints') super(ConstrainedMinimizer, self).__init__(*args, **kwargs) # Remember the vanilla constraints for pickling self._pickle_kwargs['constraints'] = constraints if constraints is None: constraints = [] self.constraints = constraints class GradientMinimizer(BaseMinimizer): """ ABC for Minizers that support the use of a jacobian """ @keywordonly(jacobian=None) def __init__(self, *args, **kwargs): self.jacobian = kwargs.pop('jacobian') super(GradientMinimizer, self).__init__(*args, **kwargs) self._pickle_kwargs['jacobian'] = self.jacobian if self.jacobian is not None: self.jacobian = self._baseobjective_from_callable(self.jacobian) self.wrapped_jacobian = self.resize_jac(self.jacobian) else: self.wrapped_jacobian = None def resize_jac(self, func): """ Removes values with identical indices to fixed parameters from the output of func. func has to return the jacobian of a scalar function. :param func: Jacobian function to be wrapped. Is assumed to be the jacobian of a scalar function. :return: Jacobian corresponding to non-fixed parameters only. """ if func is None: return None @wraps(func) def resized(*args, **kwargs): out = func(*args, **kwargs) # Make one dimensional, corresponding to a scalar function. out = np.atleast_1d(np.squeeze(out)) mask = [p not in self._fixed_params for p in self.parameters] return out[mask] return resized class HessianMinimizer(GradientMinimizer): """ ABC for Minimizers that support the use of a Hessian. """ @keywordonly(hessian=None) def __init__(self, *args, **kwargs): self.hessian = kwargs.pop('hessian') super(HessianMinimizer, self).__init__(*args, **kwargs) self._pickle_kwargs['hessian'] = self.hessian if self.hessian is not None: self.hessian = self._baseobjective_from_callable(self.hessian) self.wrapped_hessian = self.resize_hess(self.hessian) else: self.wrapped_hessian = None def resize_hess(self, func): """ Removes values with identical indices to fixed parameters from the output of func. func has to return the Hessian of a scalar function. :param func: Hessian function to be wrapped. Is assumed to be the Hessian of a scalar function. :return: Hessian corresponding to free parameters only. """ if func is None: return None @wraps(func) def resized(*args, **kwargs): out = func(*args, **kwargs) # Make two dimensional, corresponding to a scalar function. out = np.atleast_2d(np.squeeze(out)) mask = [p not in self._fixed_params for p in self.parameters] return np.atleast_2d(out[mask, mask]) return resized class GlobalMinimizer(BaseMinimizer): """ A minimizer that looks for a global minimum, instead of a local one. """ def __init__(self, *args, **kwargs): super(GlobalMinimizer, self).__init__(*args, **kwargs) class ChainedMinimizer(BaseMinimizer): """ A minimizer that consists of multiple other minimizers, each executed in order. This is valuable if you have minimizers that are not good at finding the exact minimum such as :class:`~symfit.core.minimizers.NelderMead` or :class:`~symfit.core.minimizers.DifferentialEvolution`. """ @keywordonly(minimizers=None) def __init__(self, *args, **kwargs): ''' :param minimizers: a :class:`~collections.abc.Sequence` of :class:`~symfit.core.minimizers.BaseMinimizer` objects, which need to be run in order. :param \*args: passed to :func:`symfit.core.minimizers.BaseMinimizer.__init__`. :param \*\*kwargs: passed to :func:`symfit.core.minimizers.BaseMinimizer.__init__`. ''' minimizers = kwargs.pop('minimizers') super(ChainedMinimizer, self).__init__(*args, **kwargs) self.minimizers = minimizers self._pickle_kwargs['minimizers'] = self.minimizers self.__signature__ = self._make_signature() def execute(self, **minimizer_kwargs): """ Execute the chained-minimization. In order to pass options to the seperate minimizers, they can be passed by using the names of the minimizers as keywords. For example:: fit = Fit(self.model, self.xx, self.yy, self.ydata, minimizer=[DifferentialEvolution, BFGS]) fit_result = fit.execute( DifferentialEvolution={'seed': 0, 'tol': 1e-4, 'maxiter': 10}, BFGS={'tol': 1e-4} ) In case of multiple identical minimizers an index is added to each keyword argument to make them identifiable. For example, if:: minimizer=[BFGS, DifferentialEvolution, BFGS]) then the keyword arguments will be 'BFGS', 'DifferentialEvolution', and 'BFGS_2'. :param minimizer_kwargs: Minimizer options to be passed to the minimzers by name :return: an instance of :class:`~symfit.core.fit_results.FitResults`. """ bound_arguments = self.__signature__.bind(**minimizer_kwargs) # Include default values in bound_argument object. # Start from a new OrderedDict to guarantee ordering. arguments = OrderedDict() for param in self.__signature__.parameters.values(): if param.name in bound_arguments.arguments: arguments[param.name] = bound_arguments.arguments[param.name] else: arguments[param.name] = param.default bound_arguments.arguments = arguments answers = [] next_guess = self.initial_guesses for minimizer, kwargs in zip(self.minimizers, bound_arguments.arguments.values()): minimizer.initial_guesses = next_guess ans = minimizer.execute(**kwargs) next_guess = list(ans.params.values()) answers.append(ans) final = answers[-1] # TODO: Compile all previous results in one, instead of just the # number of function evaluations. But there's some code down the # line that expects scalars. final.minimizer_output['nit'] = sum(ans.iterations for ans in answers) return final def _make_signature(self): """ Create a signature for `execute` based on the minimizers this `ChainedMinimizer` was initiated with. For the format, see the docstring of :meth:`ChainedMinimizer.execute`. :return: :class:`inspect.Signature` instance. """ # Create KEYWORD_ONLY arguments with the names of the minimizers. name = lambda x: x.__class__.__name__ count = Counter( [name(minimizer) for minimizer in self.minimizers] ) # Count the number of each minimizer, they don't have to be unique # Note that these are inspect_sig.Parameter's, not symfit parameters! parameters = [] for minimizer in reversed(self.minimizers): if count[name(minimizer)] == 1: # No ambiguity, so use the name directly. param_name = name(minimizer) else: # Ambiguity, so append the number of remaining minimizers param_name = '{}_{}'.format(name(minimizer), count[name(minimizer)]) count[name(minimizer)] -= 1 parameters.append( inspect_sig.Parameter( param_name, kind=inspect_sig.Parameter.KEYWORD_ONLY, default={} ) ) return inspect_sig.Signature(parameters=reversed(parameters)) def __getstate__(self): state = super(ChainedMinimizer, self).__getstate__() del state['__signature__'] return state def __str__(self): return self.__class__.__name__ + '(minimizers={})'.format(self.minimizers) class ScipyMinimize(object): """ Mix-in class that handles the execute calls to :func:`scipy.optimize.minimize`. """ def __init__(self, *args, **kwargs): self.constraints = [] self.jacobian = None self.wrapped_jacobian = None super(ScipyMinimize, self).__init__(*args, **kwargs) @keywordonly(tol=1e-9) def execute(self, bounds=None, jacobian=None, hessian=None, constraints=None, **minimize_options): """ Calls the wrapped algorithm. :param bounds: The bounds for the parameters. Usually filled by :class:`~symfit.core.minimizers.BoundedMinimizer`. :param jacobian: The Jacobian. Usually filled by :class:`~symfit.core.minimizers.ScipyGradientMinimize`. :param \*\*minimize_options: Further keywords to pass to :func:`scipy.optimize.minimize`. Note that your `method` will usually be filled by a specific subclass. """ ans = minimize( self.objective, self.initial_guesses, method=self.method_name(), bounds=bounds, constraints=constraints, jac=jacobian, hess=hessian, **minimize_options ) return self._pack_output(ans) def _pack_output(self, ans): """ Packs the output of a minimization in a :class:`~symfit.core.fit_results.FitResults`. :param ans: The output of a minimization as produced by :func:`scipy.optimize.minimize` :returns: :class:`~symfit.core.fit_results.FitResults` """ best_vals = [] found = iter(np.atleast_1d(ans.x)) for param in self.parameters: if param.fixed: best_vals.append(param.value) else: best_vals.append(next(found)) fit_results = dict( model=DummyModel(params=self.parameters), popt=best_vals, covariance_matrix=None, objective=self.objective, minimizer=self, **ans ) return FitResults(**fit_results) @classmethod def method_name(cls): """ Returns the name of the minimize method this object represents. This is needed because the name of the object is not always exactly what needs to be passed on to scipy as a string. :return: """ return cls.__name__ class ScipyGradientMinimize(ScipyMinimize, GradientMinimizer): """ Base class for :func:`scipy.optimize.minimize`'s gradient-minimizers. """ @keywordonly(jacobian=None) def execute(self, **minimize_options): # This method takes the jacobian as an argument because the user may # need to override it in some cases (especially with the trust-constr # method) jacobian = minimize_options.pop('jacobian') if jacobian is None: jacobian = self.wrapped_jacobian return super(ScipyGradientMinimize, self).execute(jacobian=jacobian, **minimize_options) def scipy_constraints(self, constraints): cons = super(ScipyGradientMinimize, self).scipy_constraints(constraints) for con in cons: # Only if the model has a jacobian, does it make sense to pass one # to the minimizer if hasattr(con['fun'].model, 'eval_jacobian'): con['jac'] = self.resize_jac(con['fun'].eval_jacobian) else: con['jac'] = None return cons class ScipyBoundedMinimizer(ScipyMinimize, BoundedMinimizer): """ Base class for :func:`scipy.optimize.minimize`'s bounded-minimizers. """ def execute(self, **minimize_options): return super(ScipyBoundedMinimizer, self).execute(bounds=self.bounds, **minimize_options) class ScipyHessianMinimize(ScipyGradientMinimize, HessianMinimizer): """ Base class for :func:`scipy.optimize.minimize`'s hessian-minimizers. """ @keywordonly(hessian=None) def execute(self, **minimize_options): # This method takes the hessian as an argument because the user may # need to override it in some cases (especially with the trust-constr # method) hessian = minimize_options.pop('hessian') if hessian is None: hessian = self.wrapped_hessian return super(ScipyHessianMinimize, self).execute(hessian=hessian, **minimize_options) def scipy_constraints(self, constraints): cons = super(ScipyHessianMinimize, self).scipy_constraints(constraints) for con in cons: # Only if the model has a hessian, does it make sense to pass one # to the minimizer if hasattr(con['fun'].model, 'eval_hessian'): con['hess'] = self.resize_hess(con['fun'].eval_hessian) else: con['hess'] = None return cons class ScipyConstrainedMinimize(ScipyMinimize, ConstrainedMinimizer): """ Base class for :func:`scipy.optimize.minimize`'s constrained-minimizers. """ def __init__(self, *args, **kwargs): super(ScipyConstrainedMinimize, self).__init__(*args, **kwargs) self.wrapped_constraints = self.scipy_constraints(self.constraints) def execute(self, **minimize_options): return super(ScipyConstrainedMinimize, self).execute(constraints=self.wrapped_constraints, **minimize_options) def scipy_constraints(self, constraints): """ Returns all constraints in a scipy compatible format. :param constraints: List of either MinimizeModel instances (this is what is provided by :class:`~symfit.core.fit.Fit`), :class:`~symfit.core.models.BaseModel`, or :class:`sympy.core.relational.Relational`. :return: dict of scipy compatible statements. """ cons = [] types = { # scipy only distinguishes two types of constraint. sympy.Eq: 'eq', sympy.Ge: 'ineq', } for constraint in constraints: if isinstance(constraint, MinimizeModel): # Typically the case when called by `Fit constraint_type = constraint.model.constraint_type elif hasattr(constraint, 'constraint_type'): # Model object, not provided by `Fit`. Do the best we can. if self.parameters != constraint.params: raise AssertionError('The constraint should accept the same' ' parameters as used for the fit.') constraint_type = constraint.constraint_type constraint = MinimizeModel(constraint, data=self.objective.data) elif isinstance(constraint, sympy.Rel): constraint_type = constraint.__class__ constraint = self.objective.model.__class__.as_constraint( constraint, self.objective.model ) constraint = MinimizeModel(constraint, data=self.objective.data) else: raise TypeError('Unknown type for a constraint.') con = { 'type': types[constraint_type], 'fun': constraint, } cons.append(con) cons = tuple(cons) return cons def _pack_output(self, ans): fit_result = super(ScipyConstrainedMinimize, self)._pack_output(ans) fit_result.constraints = self.constraints return fit_result class BFGS(ScipyGradientMinimize): """ Wrapper around :func:`scipy.optimize.minimize`'s BFGS algorithm. """ class SLSQP(ScipyGradientMinimize, ScipyConstrainedMinimize, ScipyBoundedMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s SLSQP algorithm. """ class COBYLA(ScipyConstrainedMinimize, BaseMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s COBYLA algorithm. """ def execute(self, **minimize_options): ans = super(COBYLA, self).execute(**minimize_options) # Nearest indication of nit. ans.minimizer_output['nit'] = ans.minimizer_output.pop('nfev') return ans class LBFGSB(ScipyGradientMinimize, ScipyBoundedMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s LBFGSB algorithm. """ @classmethod def method_name(cls): return "L-BFGS-B" class NelderMead(ScipyMinimize, BaseMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s NelderMead algorithm. """ @classmethod def method_name(cls): return 'Nelder-Mead' class Powell(ScipyMinimize, BaseMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s Powell algorithm. """ class TrustConstr(ScipyHessianMinimize, ScipyConstrainedMinimize, ScipyBoundedMinimizer): """ Wrapper around :func:`scipy.optimize.minimize`'s Trust-Constr algorithm. """ @classmethod def method_name(cls): return 'trust-constr' def _get_jacobian_hessian_strategy(self): """ Figure out how to calculate the jacobian and hessian. Will return a tuple describing how best to calculate the jacobian and hessian, repectively. If None, it should be calculated using the available analytical method. :return: tuple of jacobian_method, hessian_method """ if self.jacobian is not None and self.hessian is None: jacobian = None hessian = 'cs' elif self.jacobian is None and self.hessian is None: jacobian = 'cs' hessian = soBFGS(exception_strategy='damp_update') else: jacobian = None hessian = None return jacobian, hessian def scipy_constraints(self, constraints): cons = super(TrustConstr, self).scipy_constraints(constraints) out = [] for con in cons: if con['type'] == 'eq': ub = 0 else: ub = np.inf nonlinearconstr_kwargs = { 'fun': con['fun'], 'lb': 0, 'ub': ub, } # If None is given to NonlinearConstraint it'll throw a hissy fit. if con['hess'] is not None: nonlinearconstr_kwargs['hess'] = lambda x, v: con['hess'](x) * v if con['jac'] is not None: nonlinearconstr_kwargs['jac'] = con['jac'] tc_con = NonlinearConstraint(**nonlinearconstr_kwargs) out.append(tc_con) return out @keywordonly(jacobian=None, hessian=None, options=None) def execute(self, **minimize_options): options = minimize_options.pop('options') if options is None: options = {} # Our Jacobians are dense, and apparently we need to explicitely # tell this. options['sparse_jacobian'] = False hessian = minimize_options.pop('hessian') jacobian = minimize_options.pop('jacobian') auto_jacobian, auto_hessian = self._get_jacobian_hessian_strategy() # For models that are not differentiable, users need the ability to # change the jacobian to e.g. 'cs' or '3-point'. In that case, hess # should either be scipy.optimize.BFGS or SR1. # In addition, users may want to change the way the Hessian is # calculated, especially if they manage to make a model whose Jacobian # can't handle complex numbers. if jacobian is None: jacobian = auto_jacobian if hessian is None: hessian = auto_hessian if jacobian is None: jacobian = self.wrapped_jacobian if hessian is None: hessian = self.wrapped_hessian ans = super(TrustConstr, self).execute(options=options, jacobian=jacobian, hessian=hessian, **minimize_options) # Rename the number of iterations kwarg to be consistent. ans.minimizer_output['nit'] = ans.minimizer_output.pop('niter') return ans class DifferentialEvolution(ScipyBoundedMinimizer, GlobalMinimizer): """ A wrapper around :func:`scipy.optimize.differential_evolution`. """ @keywordonly(strategy='rand1bin', popsize=40, mutation=(0.423, 1.053), recombination=0.95, polish=False, init='latinhypercube') def execute(self, **de_options): ans = differential_evolution(self.objective, self.bounds, **de_options) return self._pack_output(ans) class BasinHopping(ScipyMinimize, GlobalMinimizer): """ Wrapper around :func:`scipy.optimize.basinhopping`'s basin-hopping algorithm. As always, the best way to use this algorithm is through :class:`~symfit.core.fit.Fit`, as this will automatically select a local minimizer for you depending on whether you provided bounds, constraints, etc. However, BasinHopping can also be used directly. Example (with jacobian):: import numpy as np from symfit.core.minimizers import BFGS, BasinHopping from symfit import parameters def func2d(x1, x2): f = np.cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 return f def jac2d(x1, x2): df = np.zeros(2) df[0] = -14.5 * np.sin(14.5 * x1 - 0.3) + 2. * x1 + 0.2 df[1] = 2. * x2 + 0.2 return df x0 = [1.0, 1.0] np.random.seed(555) x1, x2 = parameters('x1, x2', value=x0) fit = BasinHopping(func2d, [x1, x2], local_minimizer=BFGS) minimizer_kwargs = {'jac': fit.list2kwargs(jac2d)} fit_result = fit.execute(niter=200, minimizer_kwargs=minimizer_kwargs) See :func:`scipy.optimize.basinhopping` for more options. """ @keywordonly(local_minimizer=BFGS) def __init__(self, *args, **kwargs): """ :param local_minimizer: minimizer to be used for local minimization steps. Can be any subclass of :class:`symfit.core.minimizers.ScipyMinimize`. :param args: positional arguments to be passed on to `super`. :param kwargs: keyword arguments to be passed on to `super`. """ self.local_minimizer = kwargs.pop('local_minimizer') super(BasinHopping, self).__init__(*args, **kwargs) self._pickle_kwargs['local_minimizer'] = self.local_minimizer type_error_msg = 'Currently only subclasses of ScipyMinimize are ' \ 'supported, since `scipy.optimize.basinhopping` uses ' \ '`scipy.optimize.minimize`.' # self.local_minimizer has to be a subclass or instance of ScipyMinimize # Since no one function exists to test this, we try/except instead. try: # Test if subclass. If this line doesn't fail, we are dealing with # some class. If it fails, we assume that it is an instance. issubclass(self.local_minimizer, ScipyMinimize) except TypeError: # It is not a class at all, so test if it's an instance instead if not isinstance(self.local_minimizer, ScipyMinimize): # Only ScipyMinimize subclasses supported raise TypeError(type_error_msg) else: if not issubclass(self.local_minimizer, ScipyMinimize): # Only ScipyMinimize subclasses supported raise TypeError(type_error_msg) self.local_minimizer = self.local_minimizer(self.objective, self.parameters) def execute(self, **minimize_options): """ Execute the basin-hopping minimization. :param minimize_options: options to be passed on to :func:`scipy.optimize.basinhopping`. :return: :class:`symfit.core.fit_results.FitResults` """ if 'minimizer_kwargs' not in minimize_options: minimize_options['minimizer_kwargs'] = {} if 'method' not in minimize_options['minimizer_kwargs']: # If no minimizer was set by the user upon execute, use local_minimizer minimize_options['minimizer_kwargs']['method'] = self.local_minimizer.method_name() if 'jac' not in minimize_options['minimizer_kwargs'] and isinstance(self.local_minimizer, GradientMinimizer): # Assign the jacobian minimize_options['minimizer_kwargs']['jac'] = self.local_minimizer.wrapped_jacobian if 'constraints' not in minimize_options['minimizer_kwargs'] and isinstance(self.local_minimizer, ConstrainedMinimizer): # Assign constraints minimize_options['minimizer_kwargs']['constraints'] = self.local_minimizer.wrapped_constraints if 'bounds' not in minimize_options['minimizer_kwargs'] and isinstance(self.local_minimizer, BoundedMinimizer): # Assign bounds minimize_options['minimizer_kwargs']['bounds'] = self.local_minimizer.bounds ans = basinhopping( self.objective, self.initial_guesses, **minimize_options ) if isinstance(ans.message, list): # For some reason this is currently a length one list containing # the message. We check just in case this gets fixed upstream in # future releases. ans.message = ans.message[0] if 'constraints' in minimize_options['minimizer_kwargs']: # Add the constraints to the FitResults ans['constraints'] = self.local_minimizer.constraints return self._pack_output(ans) class MINPACK(ScipyBoundedMinimizer, GradientMinimizer): """ Wrapper to scipy's implementation of least_squares, since it is the industry standard. """ def resize_jac(self, func): """ Removes values with identical indices to fixed parameters from the output of func. func has to return the jacobian of the residuals. This method is different from the one in GradientMinimizer, since least_squares expects the jacobian to return an MxN (M=len(data), N=len(params)) matrix, rather than a vector. :param func: Jacobian function to be wrapped. Is assumed to be the jacobian of the residuals. :return: Jacobian corresponding to non-fixed parameters only. """ if func is None: return None @wraps(func) def resized(*args, **kwargs): out = func(*args, **kwargs) out = np.atleast_2d(out) mask = [p not in self._fixed_params for p in self.parameters] return out[:, mask] return resized @property def bounds(self): lbounds = [] ubounds = [] for low, high in super().bounds: if low is None: low = -np.inf if high is None: high = np.inf lbounds.append(low) ubounds.append(high) return lbounds, ubounds def execute(self, jacobian=None, method='trf', **minpack_options): """ :param \*\*minpack_options: Any named arguments to be passed to :func:`scipy.optimize.least_squares` """ if jacobian is None: jacobian = self.wrapped_jacobian jacobian = jacobian or 'cs' if not self.bounds: method = method or 'lm' else: method = method or 'trf' full_output = least_squares( self.objective, x0=self.initial_guesses, jac=jacobian, bounds=self.bounds, method=method, **minpack_options ) return self._pack_output(full_output) symfit-0.5.4/symfit/core/models.py000066400000000000000000001476051412237106600171520ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from collections import OrderedDict from collections.abc import Mapping import operator import warnings import sys import sympy from sympy.core.relational import Relational import numpy as np from toposort import toposort from scipy.integrate import odeint from .argument import Parameter, Variable from .support import ( seperate_symbols, keywordonly, sympy_to_py, partial, cached_property, D ) if sys.version_info >= (3,0): import inspect as inspect_sig else: import funcsigs as inspect_sig class ModelOutput(tuple): """ Object to hold the output of a model call. It mimics a :func:`collections.namedtuple`, but is initiated with :class:`~symfit.core.argument.Variable` objects instead of strings. Its information can be accessed using indexing or as attributes:: >>> x, y = variables('x, y') >>> a, b = parameters('a, b') >>> model = Model({y: a * x + b}) >>> ans = model(x=2, a=1, b=3) >>> print(ans) ModelOutput(variables=[y], output=[5]) >>> ans[0] 5 >>> ans.y 5 """ def __new__(self, variables, output): """ ``variables`` and ``output`` need to be in the same order! :param variables: The variables corresponding to ``output``. :param output: The output of a call which should be mapped to ``variables``. """ return tuple.__new__(ModelOutput, output) def __init__(self, variables, output): """ ``variables`` and ``output`` need to be in the same order! :param variables: The variables corresponding to ``output``. :param output: The output of a call which should be mapped to ``variables``. """ self.variables = list(variables) self.output = output self.output_dict = OrderedDict(zip(variables, output)) self.variable_names = {var.name: var for var in variables} def __getnewargs__(self): return self.variables, self.output def __getstate__(self): return self.variables, self.output def __setstate__(self, state): self.__init__(variables=state[0], output=state[1]) def __getattr__(self, name): try: var = self.variable_names[name] except KeyError as err: raise AttributeError(err) return self.output_dict[var] def __getitem__(self, key): return self.output[key] def __repr__(self): return self.__class__.__name__ + '(variables={}, output={})'.format(self.variables, self.output) def _asdict(self): """ :return: Returns a new OrderedDict representing this object. """ return self.output_dict.copy() def __len__(self): return len(self.output_dict) class ModelError(Exception): """ Raised when a problem occurs with a model. """ pass class BaseModel(Mapping): """ ABC for ``Model``'s. Makes sure models are iterable. Models can be initiated from Mappings or Iterables of Expressions, or from an expression directly. Expressions are not enforced for ducktyping purposes. """ def __init__(self, model): """ Initiate a Model from a dict:: a = Model({y: x**2}) Preferred way of initiating ``Model``, since now you know what the dependent variable is called. :param model: dict of ``Expr``, where dependent variables are the keys. """ if not isinstance(model, Mapping): try: iter(model) except TypeError: # Model is still a scalar model model = [model] # TODO: this will break upon deprecating the auto-generation of # names for Variables. At this time, a DummyVariable object # should be introduced to fulfill the same role. # # Temporarily introduced what should be a unique name derived from # the object's ID (preappended with an underscore for it to be a # valid identifier) to surpress the DepricationWarnings raised when # instantiating a Variable without a name. model = {Variable("_" + str(id(expr))): expr for expr in model} self._init_from_dict(model) @classmethod def as_constraint(cls, constraint, model, constraint_type=None, **init_kwargs): """ Initiate a Model which should serve as a constraint. Such a constraint-model should be initiated with knowledge of another ``BaseModel``, from which it will take its parameters:: model = Model({y: a * x + b}) constraint = Model.as_constraint(Eq(a, 1), model) ``constraint.params`` will be ``[a, b]`` instead of ``[a]``. :param constraint: An ``Expr``, a mapping or iterable of ``Expr``, or a ``Relational``. :param model: An instance of (a subclass of) :class:`~symfit.core.models.BaseModel`. :param constraint_type: When ``constraint`` is not a :class:`~sympy.core.relational.Relational`, a :class:`~sympy.core.relational.Relational` has to be provided explicitly. :param kwargs: Any additional keyword arguments which will be passed on to the init method. """ allowed_types = [sympy.Eq, sympy.Ge, sympy.Le] if isinstance(constraint, Relational): constraint_type = constraint.__class__ constraint = constraint.lhs - constraint.rhs # Initiate the constraint model, in such a way that we take care # of any dependencies instance = cls.with_dependencies(constraint, dependency_model=model, **init_kwargs) # Check if the constraint_type is allowed, and flip the sign if needed if constraint_type not in allowed_types: raise ModelError( 'Only constraints of the type {} are allowed. A constraint' ' of type {} was provided.'.format(allowed_types, constraint_type) ) elif constraint_type is sympy.Le: # We change this to a Ge and flip the sign instance = - instance constraint_type = sympy.Ge instance.constraint_type = constraint_type if len(instance.dependent_vars) != 1: raise ModelError('Only scalar models can be used as constraints.') # self.params has to be a subset of model.params if set(instance.params) <= set(model.params): instance.params = model.params else: raise ModelError('The parameters of ``constraint`` have to be a ' 'subset of those of ``model``.') return instance @classmethod def with_dependencies(cls, model_expr, dependency_model, **init_kwargs): """ Initiate a model whose components depend on another model. For example:: >>> x, y, z = variables('x, y, z') >>> dependency_model = Model({y: x**2}) >>> model_dict = {z: y**2} >>> model = Model.with_dependencies(model_dict, dependency_model) >>> print(model) [y(x; ) = x**2, z(y; ) = y**2] :param model_expr: The ``Expr`` or mapping/iterable of ``Expr`` to be turned into a model. :param dependency_model: An instance of (a subclass of) :class:`~symfit.core.models.BaseModel`, which contains components on which the argument ``model_expr`` depends. :param init_kwargs: Any kwargs to be passed on to the standard init method of this class. :return: A stand-alone :class:`~symfit.core.models.BaseModel` subclass. """ model = cls(model_expr, **init_kwargs) # Initiate model instance. if any(var in dependency_model for var in model.independent_vars): # This model depends on the output of the dependency_model, # so we need to work those components into the model_dict. model_dict = model.model_dict.copy() # This is the case for BaseNumericalModel's connectivity_mapping = init_kwargs.get('connectivity_mapping', model.connectivity_mapping) for var in model.independent_vars: if var in dependency_model: # Add this var and all its dependencies. # Walk over all possible dependencies of this # variable until we no longer have dependencies. for symbol in dependency_model.ordered_symbols: # Not everything in ordered_symbols is a key of # model, think e.g. parameters if symbol in dependency_model: if symbol not in model_dict: model_dict[symbol] = dependency_model[symbol] connectivity_mapping[symbol] = dependency_model.connectivity_mapping[symbol] if symbol == var: break # connectivity_mapping in init_kwargs has been updated if it was # present, since python is pass by reference. If it wasn't present, # we are dealing with a type of model that will build its own # connectivity_mapping upon init. model = cls(model_dict, **init_kwargs) return model def __len__(self): """ :return: the number of dependent variables for this model. """ return len(self.model_dict) def __getitem__(self, var): """ Returns the expression belonging to a given dependent variable. :param var: Instance of ``Variable`` :type var: ``Variable`` :return: The expression belonging to ``var`` """ return self.model_dict[var] def __iter__(self): """ :return: iterable over self.model_dict """ return iter(self.model_dict) def __eq__(self, other): """ ``Model``'s are considered equal when they have the same dependent variables, and the same expressions for those dependent variables. The same is defined here as passing sympy == for the vars themselves, and as expr1 - expr2 == 0 for the expressions. For more info check the `sympy docs `_. :param other: Instance of ``Model``. :return: bool """ if len(self) is not len(other): return False else: for var_1, var_2 in zip(self, other): if var_1 != var_2: return False else: if not self[var_1].expand() == other[var_2].expand(): return False else: return True def __neg__(self): """ :return: new model with opposite sign. Does not change the model in-place, but returns a new copy. """ new_model_dict = self.model_dict.copy() for var in self.dependent_vars: new_model_dict[var] *= -1 return self.__class__(new_model_dict) def _init_from_dict(self, model_dict): """ Initiate self from a model_dict to make sure attributes such as vars, params are available. Creates lists of alphabetically sorted independent vars, dependent vars, sigma vars, and parameters. Finally it creates a signature for this model so it can be called nicely. This signature only contains independent vars and params, as one would expect. :param model_dict: dict of (dependent_var, expression) pairs. """ sort_func = lambda symbol: symbol.name self.model_dict = OrderedDict(sorted(model_dict.items(), key=lambda i: sort_func(i[0]))) # Everything at the bottom of the toposort is independent, at the top # dependent, and the rest interdependent. ordered = list(toposort(self.connectivity_mapping)) independent = sorted(ordered.pop(0), key=sort_func) self.dependent_vars = sorted(ordered.pop(-1), key=sort_func) self.interdependent_vars = sorted( [item for items in ordered for item in items], key=sort_func ) # `independent` contains both params and vars, needs to be separated self.independent_vars = [s for s in independent if not isinstance(s, Parameter) and not s in self] self.params = [s for s in independent if isinstance(s, Parameter)] try: assert not any(isinstance(var, Parameter) for var in self.dependent_vars) assert not any(isinstance(var, Parameter) for var in self.interdependent_vars) except AssertionError: raise ModelError('`Parameter`\'s can not feature in the role ' 'of `Variable`') # Make Variable object corresponding to each depedent var. self.sigmas = {var: Variable(name='sigma_{}'.format(var.name)) for var in self.dependent_vars} @cached_property def vars_as_functions(self): """ :return: Turn the keys of this model into :class:`~sympy.core.function.Function` objects. This is done recursively so the chain rule can be applied correctly. This is done on the basis of `connectivity_mapping`. Example: for ``{y: a * x, z: y**2 + a}`` this returns ``{y: y(x, a), z: z(y(x, a), a)}``. """ vars2functions = {} key = lambda arg: [isinstance(arg, Parameter), str(arg)] # Iterate over all symbols in this model in topological order, turning # each one into a function object recursively. for symbol in self.ordered_symbols: if symbol in self.connectivity_mapping: dependencies = self.connectivity_mapping[symbol] # Replace the dependency by it's function if possible dependencies = [vars2functions.get(dependency, dependency) for dependency in dependencies] # sort by vars first, then params, and alphabetically within # each group dependencies = sorted(dependencies, key=key) vars2functions[symbol] = sympy.Function(symbol.name)(*dependencies) return vars2functions @cached_property def function_dict(self): """ Equivalent to ``self.model_dict``, but with all variables replaced by functions if applicable. Sorted by the evaluation order according to ``self.ordered_symbols``, not alphabetical like ``self.model_dict``! """ func_dict = OrderedDict() for var, func in self.vars_as_functions.items(): expr = self.model_dict[var].xreplace(self.vars_as_functions) func_dict[func] = expr return func_dict @cached_property def connectivity_mapping(self): """ :return: This property returns a mapping of the interdepencies between variables. This is essentially the dict representation of a connectivity graph, because working with this dict results in cleaner code. Treats variables and parameters on the same footing. """ connectivity = {} for var, expr in self.items(): vars, params = seperate_symbols(expr) connectivity[var] = set(vars + params) return connectivity @property def ordered_symbols(self): """ :return: list of all symbols in this model, topologically sorted so they can be evaluated in the correct order. Within each group of equal priority symbols, we sort by the order of the derivative. """ key_func = lambda s: [isinstance(s, sympy.Derivative), isinstance(s, sympy.Derivative) and s.derivative_count] symbols = [] for symbol in toposort(self.connectivity_mapping): symbols.extend(sorted(symbol, key=key_func)) return symbols @cached_property def vars(self): """ :return: Returns a list of dependent, independent and sigma variables, in that order. """ return self.independent_vars + self.dependent_vars + [self.sigmas[var] for var in self.dependent_vars] @property def bounds(self): """ :return: List of tuples of all bounds on parameters. """ bounds = [] for p in self.params: if p.fixed: if p.value >= 0.0: bounds.append([np.nextafter(p.value, 0), p.value]) else: bounds.append([p.value, np.nextafter(p.value, 0)]) else: bounds.append([p.min, p.max]) return bounds @property def shared_parameters(self): """ :return: bool, indicating if parameters are shared between the vector components of this model. """ if len(self) == 1: # Not a vector return False else: params_thusfar = [] for component in self.values(): vars, params = seperate_symbols(component) if set(params).intersection(set(params_thusfar)): return True else: params_thusfar += params else: return False @property def free_params(self): """ :return: ordered list of the subset of variable params """ return [p for p in self.params if not p.fixed] def __str__(self): """ Printable representation of a Mapping model. :return: str """ template = "{}({}; {}) = {}" parts = [] for var, expr in self.items(): # Print every component as a function of only the dependencies it # has. We can deduce these from the connectivity mapping. params_sorted = sorted((x for x in self.connectivity_mapping[var] if isinstance(x, Parameter)), key=lambda x: x.name) vars_sorted = sorted((x for x in self.connectivity_mapping[var] if x not in params_sorted), key=lambda x: x.name) parts.append(template.format( var, ', '.join([x.name for x in vars_sorted]), ', '.join([x.name for x in params_sorted]), expr ) ) return '[{}]'.format(",\n ".join(parts)) def _repr_latex_(self): """IPython/Jupyter LaTeX printing""" from sympy.printing.latex import latex parts = [] for var, expr in self.items(): # Print every component as a function of only the dependencies it # has. We can deduce these from the connectivity mapping. params_sorted = sorted((x for x in self.connectivity_mapping[var] if isinstance(x, Parameter)), key=lambda x: x.name) vars_sorted = sorted((x for x in self.connectivity_mapping[var] if x not in params_sorted), key=lambda x: x.name) vars_str = ', '.join([latex(x) for x in vars_sorted]) params_str = ', '.join([latex(x) for x in params_sorted]) part = f"{latex(var)}({vars_str}; {params_str}) & = {latex(expr)}" parts.append(part) return '\\begin{align}' + '\\\\'.join(parts) + '\\end{align}' def __getstate__(self): # Remove cached_property values from the state, they need to be # re-calculated after pickle. state = self.__dict__.copy() del state['__signature__'] for key in self.__dict__: if key.startswith(cached_property.base_str): del state[key] return state def __setstate__(self, state): self.__dict__.update(state) self.__signature__ = self._make_signature() class BaseNumericalModel(BaseModel): """ ABC for Numerical Models. These are models whose components are generic python callables. """ @keywordonly(connectivity_mapping=None) def __init__(self, model, independent_vars=None, params=None, **kwargs): """ :param model: dict of ``callable``, where dependent variables are the keys. If instead of a dict a (sequence of) ``callable`` is provided, it will be turned into a dict automatically. :param independent_vars: The independent variables of the model. (Deprecated, use ``connectivity_mapping`` instead.) :param params: The parameters of the model. (Deprecated, use ``connectivity_mapping`` instead.) :param connectivity_mapping: Mapping indicating the dependencies of every variable in the model. For example, a model_dict ``{y: lambda x, a, b: a * x + b}`` needs a connectivity_mapping ``{y: {x, a, b}}``. (Note that the values of this dict have to be sets.) This only has to be provided for the non-symbolic components. The part corresponding to the symbolic components of the model is inferred automatically. """ connectivity_mapping = kwargs.pop('connectivity_mapping') if (connectivity_mapping is None and independent_vars is not None and params is not None): # Make model into a mapping if needed. if not isinstance(model, Mapping): try: iter(model) except TypeError: model = [model] # make model iterable model = {Variable(): expr for expr in model} warnings.warn(DeprecationWarning( '`independent_vars` and `params` have been deprecated.' ' Use `connectivity_mapping` instead.' )) self.independent_vars = sorted(independent_vars, key=str) self.params = sorted(params, key=str) self.connectivity_mapping = {var: set(independent_vars + params) for var in model} elif connectivity_mapping: if not isinstance(model, Mapping): raise TypeError('Please provide the model as a mapping, ' 'corresponding to `connectivity_mapping`.') # Infer the connectivity mapping corresponding to the symbolical # part automatically sub_model = {} for var, expr in model.items(): if isinstance(expr, sympy.Basic): sub_model[var] = expr if sub_model: sub_model = BaseModel(sub_model) # Update with the users input. In case of conflict, this # prioritizes the info given by the user. sub_model.connectivity_mapping.update(connectivity_mapping) connectivity_mapping = sub_model.connectivity_mapping self.connectivity_mapping = connectivity_mapping.copy() else: raise TypeError('Please provide `connectivity_mapping`.') super(BaseNumericalModel, self).__init__(model, **kwargs) @property def connectivity_mapping(self): return self._connectivity_mapping @connectivity_mapping.setter def connectivity_mapping(self, value): self._connectivity_mapping = value def __eq__(self, other): if self.connectivity_mapping != other.connectivity_mapping: return False for key, func in self.model_dict.items(): if func != other[key]: return False return True def __neg__(self): """ :return: new model with opposite sign. Does not change the model in-place, but returns a new copy. """ new_model_dict = {} for key, callable_expr in self.model_dict.values(): new_model_dict[key] = lambda *args, **kwargs: - callable_expr(*args, **kwargs) return self.__class__(new_model_dict) @property def shared_parameters(self): """ BaseNumericalModel's cannot infer if parameters are shared. """ raise NotImplementedError( 'Shared parameters can not be inferred for {}'.format(self.__class__.__name__) ) class BaseCallableModel(BaseModel): """ Baseclass for callable models. A callable model is expected to have implemented a `__call__` method which evaluates the model. """ def eval_components(self, *args, **kwargs): """ :return: evaluated lambda functions of each of the components in model_dict, to be used in numerical calculation. """ bound_arguments = self.__signature__.bind(*args, **kwargs) kwargs = bound_arguments.arguments # Only work with kwargs components = dict(zip(self, self.numerical_components)) # Evaluate the variables in topological order. for symbol in self.ordered_symbols: if symbol.name not in kwargs: dependencies = self.connectivity_mapping[symbol] dependencies_kwargs = {d.name: kwargs[d.name] for d in dependencies} kwargs[symbol.name] = components[symbol](**dependencies_kwargs) return [np.atleast_1d(kwargs[var.name]) for var in self] def numerical_components(self): """ :return: A list of callables corresponding to each of the components of the model. """ raise NotImplementedError( ('No `numerical_components` is defined for object of type {}. ' 'Implement either `numerical_components`, or change ' '`eval_components` so it no longer calls ' '`numerical_components`.').format(self.__class__) ) @property def params(self): return self._params @params.setter def params(self, value): self._params = value self.__signature__ = self._make_signature() def _make_signature(self): # Handle args and kwargs according to the allowed names. parameters = [ # Note that these are inspect_sig.Parameter's, not symfit parameters! inspect_sig.Parameter(arg.name, inspect_sig.Parameter.POSITIONAL_OR_KEYWORD) for arg in self.independent_vars + self.params ] return inspect_sig.Signature(parameters=parameters) def _init_from_dict(self, model_dict): super(BaseCallableModel, self)._init_from_dict(model_dict) self.__signature__ = self._make_signature() def __call__(self, *args, **kwargs): """ Evaluate the model for a certain value of the independent vars and parameters. Signature for this function contains independent vars and parameters, NOT dependent and sigma vars. Can be called with both ordered and named parameters. Order is independent vars first, then parameters. Alphabetical order within each group. :param args: :param kwargs: :return: A namedtuple of all the dependent vars evaluated at the desired point. Will always return a tuple, even for scalar valued functions. This is done for consistency. """ return ModelOutput(self.keys(), self.eval_components(*args, **kwargs)) class BaseGradientModel(BaseCallableModel): """ Baseclass for models which have a gradient. Such models are expected to implement an `eval_jacobian` function. Any subclass of this baseclass which does not implement its own `eval_jacobian` will inherit a finite difference gradient. """ @keywordonly(dx=1e-8) def finite_difference(self, *args, **kwargs): """ Calculates a numerical approximation of the Jacobian of the model using the sixth order central finite difference method. Accepts a `dx` keyword to tune the relative stepsize used. Makes 6*n_params calls to the model. :return: A numerical approximation of the Jacobian of the model as a list with length n_components containing numpy arrays of shape (n_params, n_datapoints) """ # See also: scipy.misc.derivative. It might be convinced to work, but # it will make way too many function evaluations dx = kwargs.pop('dx') bound_arguments = self.__signature__.bind(*args, **kwargs) var_vals = [bound_arguments.arguments[var.name] for var in self.independent_vars] param_vals = [bound_arguments.arguments[param.name] for param in self.params] param_vals = np.array(param_vals, dtype=float) f = partial(self, *var_vals) # See also: scipy.misc.central_diff_weights factors = np.array((3/2., -3/5., 1/10.)) orders = np.arange(1, len(factors) + 1) out = [] # TODO: Dark numpy magic. Needs an extra dimension in out, and a sum # over the right axis at the end. # We can't make the output arrays yet, since we don't know the size of # the components. So put a sentinel value. out = None for param_idx, param_val in enumerate(param_vals): for order, factor in zip(orders, factors): h = np.zeros(len(self.params)) # Note: stepsize (h) depends on the parameter values... h[param_idx] = dx * order if abs(param_val) >= 1e-7: # ...but it'd better not be (too close to) 0. h[param_idx] *= param_val up = f(*(param_vals + h)) down = f(*(param_vals - h)) if out is None: # Initialize output arrays. Now that we evaluated f, we # know the size of our data. out = [] # out is a list of length Ncomponents with numpy arrays of # shape (Nparams, Ndata). Part of our misery comes from the # fact that the length of the data may be different for all # the components. Numpy doesn't like ragged arrays, so make # a list of arrays. for comp_idx in range(len(self)): try: len(up[comp_idx]) except TypeError: # output[comp_idx] is a number data_shape = (1,) else: data_shape = up[comp_idx].shape # Initialize at 0 so we can += all the contributions param_grad = np.zeros([len(self.params)] + list(data_shape), dtype=float) out.append(param_grad) for comp_idx in range(len(self)): diff = up[comp_idx] - down[comp_idx] out[comp_idx][param_idx, :] += factor * diff / (2 * h[param_idx]) return out def eval_jacobian(self, *args, **kwargs): """ :return: The jacobian matrix of the function. """ return ModelOutput(self.keys(), self.finite_difference(*args, **kwargs)) class CallableNumericalModel(BaseCallableModel, BaseNumericalModel): """ Callable model, whose components are callables provided by the user. This allows the user to provide the components directly. Example:: x, y = variables('x, y') a, b = parameters('a, b') numerical_model = CallableNumericalModel( {y: lambda x, a, b: a * x + b}, connectivity_mapping={y: {x, a, b}} ) This is identical in functionality to the more traditional:: x, y = variables('x, y') a, b = parameters('a, b') model = CallableModel({y: a * x + b}) but allows power-users a lot more freedom while still interacting seamlessly with the :mod:`symfit` API. When mixing symbolical and non-symbolical components, the ``connectivity_mapping`` only has to be provided for the non-symbolical components, the rest are inferred automatically:: x, y, z = variables('x, y, z') a, b = parameters('a, b') model_dict = {z: lambda y, a, b: a * y + b, y: x ** a} mixed_model = CallableNumericalModel( model_dict, connectivity_mapping={z: {y, a, b}} ) """ @cached_property def numerical_components(self): return [expr if not isinstance(expr, sympy.Expr) else sympy_to_py(expr, self.connectivity_mapping[var]) for var, expr in self.items()] class CallableModel(BaseCallableModel): """ Defines a callable model. The usual rules apply to the ordering of the arguments: * first independent variables, then dependent variables, then parameters. * within each of these groups they are ordered alphabetically. """ @cached_property def numerical_components(self): """ :return: lambda functions of each of the analytical components in model_dict, to be used in numerical calculation. """ # All components must feature the independent vars and params, that's # the API convention. But for those components which also contain # interdependence, we add those vars components = [] for var, expr in self.items(): dependencies = self.connectivity_mapping[var] # vars first, then params, and alphabetically within each group key = lambda arg: [isinstance(arg, Parameter), str(arg)] ordered = sorted(dependencies, key=key) components.append(sympy_to_py(expr, ordered)) return ModelOutput(self.keys(), components) class GradientModel(CallableModel, BaseGradientModel): """ Analytical model which has an analytically computed Jacobian. """ def __init__(self, *args, **kwargs): super(GradientModel, self).__init__(*args, **kwargs) @cached_property def jacobian_model(self): jac_model = jacobian_from_model(self) jac_model.params = self.params return jac_model @cached_property def jacobian(self): """ :return: Jacobian filled with the symbolic expressions for all the partial derivatives. Partial derivatives are of the components of the function with respect to the Parameter's, not the independent Variable's. The return shape is a list over the models components, filled with tha symbolical jacobian for that component, as a list. """ jac = [] for var, expr in self.items(): jac_row = [] for param in self.params: partial_dv = D(var, param) jac_row.append(self.jacobian_model[partial_dv]) jac.append(jac_row) return jac def eval_jacobian(self, *args, **kwargs): """ :return: Jacobian evaluated at the specified point. """ eval_jac_dict = self.jacobian_model(*args, **kwargs)._asdict() # Take zero for component which are not present, happens for Constraints jac = [[np.broadcast_to(eval_jac_dict.get(D(var, param), 0), eval_jac_dict[var].shape) for param in self.params] for var in self ] # Use numpy to broadcast these arrays together and then stack them along # the parameter dimension. We do not include the component direction in # this, because the components can have independent shapes. for idx, comp in enumerate(jac): jac[idx] = np.stack(np.broadcast_arrays(*comp)) return ModelOutput(self.keys(), jac) class HessianModel(GradientModel): """ Analytical model which has an analytically computed Hessian. """ def __init__(self, *args, **kwargs): super(HessianModel, self).__init__(*args, **kwargs) @cached_property def hessian_model(self): hess_model = hessian_from_model(self) hess_model.params = self.params return hess_model @property def hessian(self): """ :return: Hessian filled with the symbolic expressions for all the second order partial derivatives. Partial derivatives are taken with respect to the Parameter's, not the independent Variable's. """ return [[[sympy.diff(partial_dv, param) for param in self.params] for partial_dv in comp] for comp in self.jacobian] def eval_hessian(self, *args, **kwargs): """ :return: Hessian evaluated at the specified point. """ # Evaluate the hessian model and use the resulting Ans namedtuple as a # dict. From this, take the relevant components. eval_hess_dict = self.hessian_model(*args, **kwargs)._asdict() hess = [[[np.broadcast_to(eval_hess_dict.get(D(var, p1, p2), 0), eval_hess_dict[var].shape) for p2 in self.params] for p1 in self.params] for var in self ] # Use numpy to broadcast these arrays together and then stack them along # the parameter dimension. We do not include the component direction in # this, because the components can have independent shapes. for idx, comp in enumerate(hess): hess[idx] = np.stack(np.broadcast_arrays(*comp)) return ModelOutput(self.keys(), hess) class Model(HessianModel): """ Model represents a symbolic function and all it's derived properties such as sum of squares, jacobian etc. Models should be initiated from a dict:: a = Model({y: x**2}) Models are callable. The usual rules apply to the ordering of the arguments: * first independent variables, then parameters. * within each of these groups they are ordered alphabetically. The output of a call to a model is a special kind of namedtuple:: >>> a(x=3) Ans(y=9) When turning this into a dict, however, the dict keys will be Variable objects, not strings:: >>> a(x=3)._asdict() OrderedDict(((y, 9),)) Models are also iterable, behaving as their internal model_dict. For example, ``a[y]`` returns ``x**2``, ``len(a) == 1``, ``y in a == True``, etc. """ class ODEModel(BaseGradientModel): """ Model build from a system of ODEs. When the model is called, the ODE is integrated using the LSODA package. """ def __init__(self, model_dict, initial, *lsoda_args, **lsoda_kwargs): """ :param model_dict: Dictionary specifying ODEs. e.g. model_dict = {D(y, x): a * x**2} :param initial: ``dict`` of initial conditions for the ODE. Must be provided! e.g. initial = {y: 1.0, x: 0.0} :param lsoda_args: args to pass to the lsoda solver. See `scipy's odeint `_ for more info. :param lsoda_kwargs: kwargs to pass to the lsoda solver. """ self.initial = initial self.lsoda_args = lsoda_args self.lsoda_kwargs = lsoda_kwargs sort_func = operator.attrgetter('name') # Mapping from dependent vars to their derivatives self.dependent_derivatives = {d: list(d.free_symbols - set(d.variables))[0] for d in model_dict} self.dependent_vars = sorted( self.dependent_derivatives.values(), key=sort_func ) self.independent_vars = sorted(set(d.variables[0] for d in model_dict), key=sort_func) self.interdependent_vars = [] # TODO: add this support for ODEModels if not len(self.independent_vars) == 1: raise ModelError('ODEModel can only have one independent variable.') self.model_dict = OrderedDict( sorted( model_dict.items(), key=lambda i: sort_func(self.dependent_derivatives[i[0]]) ) ) # We split the parameters into the parameters needed to evaluate the # expression/components (self.model_params), and those that are used for # initial values (self.initial_params). self.params will contain a union # of the two, as expected. # Extract all the params and vars as a sorted, unique list. expressions = model_dict.values() self.model_params = set([]) # Only the ones that have a Parameter as initial parameter. self.initial_params = {value for var, value in self.initial.items() if isinstance(value, Parameter)} for expression in expressions: vars, params = seperate_symbols(expression) self.model_params.update(params) # self.independent_vars.update(vars) # Although unique now, params and vars should be sorted alphabetically to prevent ambiguity self.params = sorted(self.model_params | self.initial_params, key=sort_func) self.model_params = sorted(self.model_params, key=sort_func) self.initial_params = sorted(self.initial_params, key=sort_func) # Make Variable object corresponding to each sigma var. self.sigmas = {var: Variable(name='sigma_{}'.format(var.name)) for var in self.dependent_vars} self.__signature__ = self._make_signature() def __str__(self): """ Printable representation of this model. :return: str """ template = "{}; {}) = {}" parts = [] for var, expr in self.model_dict.items(): parts.append(template.format( str(var)[:-1], ", ".join(arg.name for arg in self.params), expr ) ) return "\n".join(parts) def __getitem__(self, dependent_var): """ Gives the function defined for the derivative of ``dependent_var``. e.g. :math:`y' = f(y, t)`, model[y] -> f(y, t) :param dependent_var: :return: """ for d, f in self.model_dict.items(): if dependent_var == self.dependent_derivatives[d]: return f def __iter__(self): """ :return: iterable over self.model_dict """ return iter(self.dependent_vars) def __neg__(self): """ :return: new model with opposite sign. Does not change the model in-place, but returns a new copy. """ new_model_dict = self.model_dict.copy() for key in new_model_dict: new_model_dict[key] *= -1 return self.__class__(new_model_dict, initial=self.initial) @cached_property def _ncomponents(self): """ :return: The `numerical_components` for an ODEModel. This differs from the traditional `numerical_components`, in that these component can also contain dependent variables, not just the independent ones. Each of these components does not correspond to e.g. `y(t) = ...`, but to `D(y, t) = ...`. The system spanned by these component therefore still needs to be integrated. """ return [sympy_to_py(expr, self.independent_vars + self.dependent_vars + self.model_params) for expr in self.values()] @cached_property def _njacobian(self): """ :return: The `numerical_jacobian` of the components of the ODEModel with regards to the dependent variables. This is not to be confused with the jacobian of the model as a whole, which is 2D and computed with regards to the dependent vars and the fit parameters, and the ODEModel still needs to integrated to compute that. Instead, this function is used by the ODE integrator, and is not meant for human consumption. """ return [ [sympy_to_py( sympy.diff(expr, var), self.independent_vars + self.dependent_vars + self.model_params ) for var in self.dependent_vars ] for _, expr in self.items() ] def eval_components(self, *args, **kwargs): """ Numerically integrate the system of ODEs. :param args: Ordered arguments for the parameters and independent variables :param kwargs: Keyword arguments for the parameters and independent variables :return: """ bound_arguments = self.__signature__.bind(*args, **kwargs) t_like = bound_arguments.arguments[self.independent_vars[0].name] # System of functions to be integrated f = lambda ys, t, *a: [c(t, *(list(ys) + list(a))) for c in self._ncomponents] Dfun = lambda ys, t, *a: [[c(t, *(list(ys) + list(a))) for c in row] for row in self._njacobian] initial_dependent = [self.initial[var] for var in self.dependent_vars] # For the initial values, substitute any parameter for the value passed # to this call. Scipy doesn't really understand Parameter/Symbols for idx, init_var in enumerate(initial_dependent): if init_var in self.initial_params: initial_dependent[idx] = bound_arguments.arguments[init_var.name] assert len(self.independent_vars) == 1 t_initial = self.initial[self.independent_vars[0]] # Assuming there's only one # Check if the time-like data includes the initial value, because integration should start there. try: t_like[0] except (TypeError, IndexError): # Python scalar gives TypeError, numpy scalars IndexError t_like = np.array([t_like]) # Allow evaluation at one point. # The strategy is to split the time axis in a part above and below the # initial value, and to integrate those seperately. At the end we rejoin them. # np.flip is needed because odeint wants the first point to be t_initial # and so t_smaller is a declining series. if t_initial in t_like: t_bigger = t_like[t_like >= t_initial] t_smaller = t_like[t_like <= t_initial][::-1] else: t_bigger = np.concatenate( (np.array([t_initial]), t_like[t_like > t_initial]) ) t_smaller = np.concatenate( (np.array([t_initial]), t_like[t_like < t_initial][::-1]) ) # Properly ordered time axis containing t_initial t_total = np.concatenate((t_smaller[::-1][:-1], t_bigger)) # Call the numerical integrator. Note that we only pass the # model_params, which will be used by sympy_to_py to create something we # can evaluate numerically. ans_bigger = odeint( f, initial_dependent, t_bigger, args=tuple( bound_arguments.arguments[param.name] for param in self.model_params ), Dfun=Dfun, *self.lsoda_args, **self.lsoda_kwargs ) ans_smaller = odeint( f, initial_dependent, t_smaller, args=tuple( bound_arguments.arguments[param.name] for param in self.model_params ), Dfun=Dfun, *self.lsoda_args, **self.lsoda_kwargs ) ans = np.concatenate((ans_smaller[1:][::-1], ans_bigger)) if t_initial in t_like: # The user also requested to know the value at t_initial, so keep it. return ans.T else: # The user didn't ask for the value at t_initial, so exclude it. # (t_total contains all the t-points used for the integration, # and so is t_like with t_initial inserted at the right position). return ans[t_total != t_initial].T def __call__(self, *args, **kwargs): """ Evaluate the model for a certain value of the independent vars and parameters. Signature for this function contains independent vars and parameters, NOT dependent and sigma vars. Can be called with both ordered and named parameters. Order is independent vars first, then parameters. Alphabetical order within each group. :param args: Ordered arguments for the parameters and independent variables :param kwargs: Keyword arguments for the parameters and independent variables :return: A namedtuple of all the dependent vars evaluated at the desired point. Will always return a tuple, even for scalar valued functions. This is done for consistency. """ return ModelOutput(self.keys(), self.eval_components(*args, **kwargs)) def _partial_diff(var, *params): """ Sympy does not handle repeated partial derivation correctly, e.g. D(D(y, a), a) = D(y, a, a) but D(D(y, a), b) = 0. Use this function instead to prevent evaluation to zero. """ if isinstance(var, sympy.Derivative): return sympy.Derivative(var.expr, *(var.variables + params)) else: return D(var, *params) def _partial_subs(func, func2vars): """ Partial-bug proof substitution. Works by making the substitutions on the expression inside the derivative first, and then rebuilding the derivative safely without evaluating it using `_partial_diff`. """ if isinstance(func, sympy.Derivative): new_func = func.expr.xreplace(func2vars) new_variables = tuple(var.xreplace(func2vars) for var in func.variables) return _partial_diff(new_func, *new_variables) else: return func.xreplace(func2vars) def jacobian_from_model(model, as_functions=False): """ Build a :class:`~symfit.core.models.CallableModel` representing the Jacobian of ``model``. This function make sure the chain rule is correctly applied for interdependent variables. :param model: Any symbolical model-type. :param as_functions: If `True`, the result is returned using :class:`sympy.core.function.Function` where needed, e.g. ``{y(x, a): a * x}`` instead of ``{y: a * x}``. :return: :class:`~symfit.core.models.CallableModel` representing the Jacobian of ``model``. """ # Inverse dict so we can turn functions back into vars in the end functions_as_vars = dict((v, k) for k, v in model.vars_as_functions.items()) # Create the jacobian components. The `vars` here in the model_dict are # always of the type D(y, a), but the righthand-side might still contain # functions instead of vars depending on the value of `as_functions`. jac = {} for func, expr in model.function_dict.items(): for param in model.params: target = D(func, param) dfdp = expr.diff(param) if as_functions: jac[_partial_subs(target, functions_as_vars)] = dfdp else: # Turn Function objects back into Variables. dfdp = dfdp.subs(functions_as_vars, evaluate=False) jac[_partial_subs(target, functions_as_vars)] = dfdp # Next lines are needed for the Hessian, where the components of model still # contain functions instead of vars. if as_functions: jac.update(model) else: jac.update({y: expr.subs(functions_as_vars, evaluate=False) for y, expr in model.items()}) jacobian_model = CallableModel(jac) return jacobian_model def hessian_from_model(model): """ Build a :class:`~symfit.core.models.CallableModel` representing the Hessian of ``model``. This function make sure the chain rule is correctly applied for interdependent variables. :param model: Any symbolical model-type. :return: :class:`~symfit.core.models.CallableModel` representing the Hessian of ``model``. """ jac_model = jacobian_from_model(model, as_functions=True) return jacobian_from_model(jac_model)symfit-0.5.4/symfit/core/objectives.py000066400000000000000000000547301412237106600200200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ Objective functions are the functions which are minimized by the :mod:`~symfit.core.minimizers`. Famous examples are `least squares `_, `log-likelihood `_, or minimizing the model itself. ``symfit`` provides objective functions for those cases by default. Custom objectives can also be created, for example by inheriting from :class:`~symfit.core.objectives.BaseObjective`, :class:`~symfit.core.objectives.GradientObjective` or :class:`~symfit.core.objectives.HessianObjective`. """ import abc from collections import OrderedDict import numpy as np from .support import cached_property, keywordonly, key2str class BaseObjective(object, metaclass=abc.ABCMeta): """ ABC for objective functions. Implements basic data handling. """ def __init__(self, model, data): """ :param model: `symfit` style model. :param data: data for all the variables of the model. """ self.model = model self.data = data # Compares the model with the data to see if they are compatible. self._sanity_checking() @cached_property def dependent_data(self): """ Read-only Property :return: Data belonging to each dependent variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ return OrderedDict((var, self.data[var]) for var in self.model.dependent_vars) @cached_property def independent_data(self): """ Read-only Property :return: Data belonging to each independent variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ return OrderedDict((var, self.data[var]) for var in self.model.independent_vars) @cached_property def sigma_data(self): """ Read-only Property :return: Data belonging to each sigma variable as a dict with variable names as key, data as value. :rtype: collections.OrderedDict """ sigmas = self.model.sigmas return OrderedDict( (sigmas[var], self.data[sigmas[var]]) for var in self.model.dependent_vars) @abc.abstractmethod def __call__(self, ordered_parameters=[], **parameters): """ Evaluate the objective function for given parameter values. :param ordered_parameters: List of parameter, in alphabetical order. Typically provided by the minimizer. :param parameters: parameters as keyword arguments. :return: evaluated model. """ # zip will stop when the shortest of the two is exhausted parameters.update(dict(zip(self.model.free_params, ordered_parameters))) parameters.update(self._invariant_kwargs) result = self.model(**key2str(parameters))._asdict() # Return only the components corresponding to the dependent data. return self._shape_of_dependent_data( [comp for var, comp in result.items() if var in self.model.dependent_vars] ) def _shape_of_dependent_data(self, model_output, param_level=0): """ In rare cases, the dependent data and the output of the model do not have the same shape. Think for example about :math:`y_i = a`. This function makes sure both sides of the equation have the same shape. :param model_output: Output of a call to model :param param_level: indicates how many parameter dimensions should be added to the shape. 0 for __call__, 1 for jac, 2 for hess. :return: ``model_output`` reshaped to ``dependent_data``'s shape, if possible. """ shaped_result = [] n_params = len(self.model.params) for dep_var, component in zip(self.model.dependent_vars, model_output): dep_data = self.dependent_data.get(dep_var, None) if dep_data is not None: if dep_data.shape == component.shape: shaped_result.append(component) else: # Add extra dimensions to the component if needed. dim_diff = len(dep_data.shape) - len(component.shape[param_level:]) for _ in range(dim_diff): component = np.expand_dims(component, -1) # Let numpy deal with all the broadcasting shape = param_level * [n_params] + list(dep_data.shape) shaped_result.append(np.broadcast_to(component, shape)) else: shaped_result.append(component) return shaped_result @cached_property def _invariant_kwargs(self): """ Prepares the invariant kwargs to ``self.model`` which are not provided by the minimizers, and are the same for every iteration of the minimization. This means fixed parameters and data, matching the signature of ``self.model``. """ kwargs = {p: p.value for p in self.model.params if p not in self.model.free_params} data_by_name = key2str(self.independent_data) kwargs.update( {p: data_by_name[p] for p in self.model.__signature__.parameters if p in data_by_name} ) return kwargs def __eq__(self, other): """ Objectives are considered equal if they are of the same type, have the same model, and the same data. """ # Class equality is enforced, even though this breaks subclassing. # This is to prevent false positives, which could be way worse. In the # case of subclassing, we leave it up to the subclasser to decide when # equality is achieved. if self.__class__ != other.__class__ or self.model != other.model: return False # Check if the data is also equivalent for key, value in self.data.items(): try: equal = np.allclose(other.data[key], value) except TypeError: equal = other.data[key] == value finally: if not equal: return False return True def _sanity_checking(self): """ Check if the model and the provided data are compatible. Raises a TypeError when this is not the case. """ # Simply checking for existence of these dicts will raise an error if # they cannot be built (because these are properties). self.dependent_data self.independent_data self.sigma_data class GradientObjective(BaseObjective, metaclass=abc.ABCMeta): """ ABC for objectives that support gradient methods. """ @abc.abstractmethod def eval_jacobian(self, ordered_parameters=[], **parameters): """ Evaluate the jacobian for given parameter values. :param ordered_parameters: List of parameter, in alphabetical order. Typically provided by the minimizer. :param parameters: parameters as keyword arguments. :return: evaluated jacobian """ parameters.update(dict(zip(self.model.free_params, ordered_parameters))) parameters.update(self._invariant_kwargs) result = self.model.eval_jacobian(**key2str(parameters))._asdict() # Return only the components corresponding to the dependent data. return self._shape_of_dependent_data( [comp for var, comp in result.items() if var in self.model.dependent_vars], param_level=1 ) class HessianObjective(GradientObjective, metaclass=abc.ABCMeta): """ ABC for objectives that support hessian methods. """ @abc.abstractmethod def eval_hessian(self, ordered_parameters=[], **parameters): """ Evaluate the hessian for given parameter values. :param ordered_parameters: List of parameter, in alphabetical order. Typically provided by the minimizer. :param parameters: parameters as keyword arguments. :return: evaluated hessian """ parameters.update(dict(zip(self.model.free_params, ordered_parameters))) parameters.update(self._invariant_kwargs) result = self.model.eval_hessian(**key2str(parameters))._asdict() # Return only the components corresponding to the dependent data. return self._shape_of_dependent_data( [comp for var, comp in result.items() if var in self.model.dependent_vars], param_level=2 ) class VectorLeastSquares(GradientObjective): """ Implemented for MINPACK only. Returns the residuals/sigma before squaring and summing, rather then chi2 itself. """ @keywordonly(flatten_components=True) def __call__(self, ordered_parameters=[], **parameters): """ Returns the value of the square root of :math:`\\chi^2`, summing over the components. This function now supports setting variables to None. :param flatten_components: If True, summing is performed over the data indices (default). :return: :math:`\\sqrt(\\chi^2)` """ flatten_components = parameters.pop('flatten_components') evaluated_func = super(VectorLeastSquares, self).__call__( ordered_parameters, **parameters ) result = [] # zip together the dependent vars and evaluated component for y, ans in zip(self.model.dependent_vars, evaluated_func): dep_data = self.dependent_data.get(y, None) if dep_data is not None: result.append(((self.dependent_data[y] - ans) / self.sigma_data[self.model.sigmas[y]]) ** 2) if flatten_components: # Flattens *within* a component result[-1] = result[-1].flatten() return np.sqrt(sum(result)) def eval_jacobian(self, ordered_parameters=[], **parameters): chi = self(ordered_parameters, flatten_components=False, **parameters) evaluated_func = super(VectorLeastSquares, self).__call__( ordered_parameters, **parameters ) evaluated_jac = super(VectorLeastSquares, self).eval_jacobian( ordered_parameters, **parameters ) result = len(self.model.params) * [0.0] for ans, y, row in zip(evaluated_func, self.model.dependent_vars, evaluated_jac): dep_data = self.dependent_data.get(y, None) if dep_data is not None: for index, component in enumerate(row): result[index] += component * ( (self.dependent_data[y] - ans) / self.sigma_data[self.model.sigmas[y]] ** 2 ) result *= (1 / chi) result = np.nan_to_num(result) result = [item.flatten() for item in result] return - np.array(result).T class LeastSquares(HessianObjective): """ Objective representing the least-squares deviation of a model, defined as :math:`S = \\frac{1}{2} \\sum_{i} \\sum_{x_i} \\frac{r_i(x_i, \\vec{p})^2}{\\sigma_i(x_i)^2}`, where :math:`i` ranges over all components of the model, :math:`r_i(x_i, \\vec{p})` is the residue of the :math:`i`-th component, :math:`x_i` indicates all the data associated with the :math:`i`-th component, and :math:`\\sigma_i(x_i)` indicates the associated standard deviations. The data for each component does not have to be the same, and it does not have to have the same shape. The only thing that matters is that within each component the shapes have to be compatible. """ @keywordonly(flatten_components=True) def __call__(self, ordered_parameters=[], **parameters): """ :param ordered_parameters: See ``parameters``. :param parameters: values of the :class:`~symfit.core.argument.Parameter`'s to evaluate :math:`S` at. :param flatten_components: if `True`, return the total :math:`S`. If `False`, return the :math:`S` per component of the :class:`~symfit.core.models.BaseModel`. :return: scalar or list of scalars depending on the value of `flatten_components`. """ flatten_components = parameters.pop('flatten_components') evaluated_func = super(LeastSquares, self).__call__( ordered_parameters, **parameters ) chi2 = [0 for _ in evaluated_func] for index, (dep_var, dep_var_value) in enumerate(zip(self.model.dependent_vars, evaluated_func)): dep_data = self.dependent_data.get(dep_var, None) if dep_data is not None: sigma = self.sigma_data[self.model.sigmas[dep_var]] chi2[index] += np.sum( (dep_var_value - dep_data) ** 2 / sigma ** 2 ) chi2 = np.sum(chi2) if flatten_components else chi2 return chi2 / 2 def eval_jacobian(self, ordered_parameters=[], **parameters): """ Jacobian of :math:`S` in the :class:`~symfit.core.argument.Parameter`'s (:math:`\\nabla_\\vec{p} S`). :param parameters: values of the :class:`~symfit.core.argument.Parameter`'s to evaluate :math:`\\nabla_\\vec{p} S` at. :return: ``np.array`` of length equal to the number of parameters.. """ evaluated_func = super(LeastSquares, self).__call__( ordered_parameters, **parameters ) evaluated_jac = super(LeastSquares, self).eval_jacobian( ordered_parameters, **parameters ) result = 0 for var, f, jac_comp in zip(self.model.dependent_vars, evaluated_func, evaluated_jac): y = self.dependent_data.get(var, None) sigma_var = self.model.sigmas[var] if y is not None: sigma = self.sigma_data[sigma_var] pre_sum = jac_comp * ((y - f) / sigma**2)[np.newaxis, ...] axes = tuple(range(1, len(pre_sum.shape))) result -= np.sum(pre_sum, axis=axes, keepdims=False) return np.atleast_1d(np.squeeze(np.array(result))) def eval_hessian(self, ordered_parameters=[], **parameters): """ Hessian of :math:`S` in the :class:`~symfit.core.argument.Parameter`'s (:math:`\\nabla_\\vec{p}^2 S`). :param parameters: values of the :class:`~symfit.core.argument.Parameter`'s to evaluate :math:`\\nabla_\\vec{p} S` at. :return: ``np.array`` of length equal to the number of parameters.. """ evaluated_func = super(LeastSquares, self).__call__( ordered_parameters, **parameters ) evaluated_jac = super(LeastSquares, self).eval_jacobian( ordered_parameters, **parameters ) evaluated_hess = super(LeastSquares, self).eval_hessian( ordered_parameters, **parameters ) result = 0 for var, f, jac_comp, hess_comp in zip(self.model.dependent_vars, evaluated_func, evaluated_jac, evaluated_hess): y = self.dependent_data.get(var, None) sigma_var = self.model.sigmas[var] if y is not None: sigma = self.sigma_data[sigma_var] p1 = hess_comp * ((y - f) / sigma**2)[np.newaxis, np.newaxis, ...] # Outer product p2 = np.einsum('i...,j...->ij...', jac_comp, jac_comp) p2 = p2 / sigma[np.newaxis, np.newaxis, ...]**2 # We sum away everything except the matrices in the axes 0 & 1. axes = tuple(range(2, len(p2.shape))) result += np.sum(p2 - p1, axis=axes, keepdims=False) return np.atleast_2d(np.squeeze(np.array(result))) class HessianObjectiveJacApprox(HessianObjective): """ This object should only be used as a Mixin for covariance matrix estimation. Since the covariance matrix for the least-squares method is proportional to the Hessian of :math:`S`, this function attempts to return the Hessian upon calculating ``eval_hessian``. However, if the model does not have a Hessian defined through ``eval_hessian``, we approximate the Hessian as :math:`J^{T}\cdot J`, where :math:`J` is the Jacobian of the model. This approximation is valid when, amongst other things, the residuals are sufficiently small. It can therefore only be used after fitting, not during. An objective which inherits from this object, will return zeros with the shape of the hessian of the model, when ``eval_hessian`` is called. This code injection will therefore result in the terms proportional to the hessian of the model dropping out, which leaves the famous :math:`J^{T}\cdot J` approximation. """ def eval_hessian(self, ordered_parameters=[], **parameters): """ :return: Zeros with the shape of the Hessian of the model. """ result = super(HessianObjectiveJacApprox, self).__call__( ordered_parameters, **parameters ) num_params = len(self.model.params) return [np.broadcast_to( np.zeros_like(comp), (num_params, num_params) + comp.shape ) for comp in result] class BaseIndependentObjective(BaseObjective): """ Some objective functions dependent only on independent variables, not dependent and sigma variables. In this case, sanity checking is greatly simplified. """ @cached_property def dependent_data(self): """ :return: Empty OrderedDict. :rtype: collections.OrderedDict """ return OrderedDict() @cached_property def sigma_data(self): """ :return: Empty OrderedDict. :rtype: collections.OrderedDict """ return OrderedDict() class LogLikelihood(HessianObjective, BaseIndependentObjective): """ Error function to be minimized by a minimizer in order to *maximize* the log-likelihood. """ def __call__(self, ordered_parameters=[], **parameters): """ :param parameters: values for the fit parameters. :return: scalar value of log-likelihood """ evaluated_func = super(LogLikelihood, self).__call__( ordered_parameters, **parameters ) ans = - np.nansum( [np.nansum(np.log(component)) for component in evaluated_func] ) return ans @keywordonly(apply_func=np.nansum) def eval_jacobian(self, ordered_parameters=[], **parameters): """ Jacobian for log-likelihood is defined as :math:`\\nabla_{\\vec{p}}( \\log( L(\\vec{p} | \\vec{x})))`. :param parameters: values for the fit parameters. :param apply_func: Function to apply to each component before returning it. The default is to sum away along the datapoint dimension using `np.nansum`. :return: array of length number of ``Parameter``'s in the model, with all partial derivatives evaluated at p, data. """ apply_func = parameters.pop('apply_func') evaluated_func = super(LogLikelihood, self).__call__( ordered_parameters, **parameters ) evaluated_jac = super(LogLikelihood, self).eval_jacobian( ordered_parameters, **parameters ) result = [] for component, jac_comp in zip(evaluated_func, evaluated_jac): component_sums = [] for df in jac_comp: component_sums.append( - apply_func( df / component ) ) result.append(component_sums) result = np.sum(result, axis=0) return np.atleast_1d(np.squeeze(np.array(result))) def eval_hessian(self, ordered_parameters=[], **parameters): """ Hessian for log-likelihood is defined as :math:`\\nabla^2_{\\vec{p}}( \\log( L(\\vec{p} | \\vec{x})))`. :param parameters: values for the fit parameters. :return: array of length number of ``Parameter``'s in the model, with all partial derivatives evaluated at p, data. """ evaluated_func = super(LogLikelihood, self).__call__( ordered_parameters, **parameters ) evaluated_jac = super(LogLikelihood, self).eval_jacobian( ordered_parameters, **parameters ) evaluated_hess = super(LogLikelihood, self).eval_hessian( ordered_parameters, **parameters ) result = 0 for f, jac_comp, hess_comp in zip(evaluated_func, evaluated_jac, evaluated_hess): # Outer product jac_outer_jac = np.einsum('i...,j...->ij...', jac_comp, jac_comp) dd_logf = - hess_comp / f[np.newaxis, np.newaxis, ...] + \ (1 / f**2)[np.newaxis, np.newaxis, ...] * jac_outer_jac # We sum away everything except the matrices in the axes 0 & 1. axes = tuple(range(2, len(dd_logf.shape))) result += np.sum(dd_logf, axis=axes, keepdims=False) return np.atleast_2d(np.squeeze(np.array(result))) class MinimizeModel(HessianObjective, BaseIndependentObjective): """ Objective to use when the model itself is the quantity that should be minimized. This is only supported for scalar models. """ def __init__(self, model, *args, **kwargs): if len(model.dependent_vars) > 1: raise TypeError('Only scalar functions are supported by {}'.format(self.__class__)) super(MinimizeModel, self).__init__(model, *args, **kwargs) def __call__(self, ordered_parameters=[], **parameters): evaluated_func = super(MinimizeModel, self).__call__( ordered_parameters, **parameters ) return evaluated_func[0] def eval_jacobian(self, ordered_parameters=[], **parameters): if hasattr(self.model, 'eval_jacobian'): evaluated_jac = super(MinimizeModel, self).eval_jacobian( ordered_parameters, **parameters ) return np.array(evaluated_jac[0]) else: return None def eval_hessian(self, ordered_parameters=[], **parameters): if hasattr(self.model, 'eval_hessian'): evaluated_hess = super(MinimizeModel, self).eval_hessian( ordered_parameters, **parameters ) return np.array(evaluated_hess[0]) else: return Nonesymfit-0.5.4/symfit/core/operators.py000066400000000000000000000101601412237106600176660ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ Monkey Patching module. This module makes ``sympy`` Expressions callable, which makes the whole project feel more consistent. """ import sys from sympy import Eq, Ne from sympy.core.expr import Expr import sympy import warnings from symfit.core.support import sympy_to_py, seperate_symbols from symfit.core.argument import Parameter if sys.version_info >= (3,0): import inspect as inspect_sig else: import funcsigs as inspect_sig # # Overwrite the behavior opun equality checking. But we want to be able to fall # # back on default behavior. # orig_eq = Expr.__class__.__eq__ # orig_ne = Expr.__class__.__ne__ # # def eq(self, other): # """ # Hack to get an Eq object back. Seems to work when used this way, # but backwards compatibility with sympy is not guaranteed. # """ # if isinstance(other, float) or isinstance(other, int): # if abs(other) == 1: # # SymPy's printing check for this and might therefore produce an # # error for fractions. Therefore we raise a warning in this case and # # ask the user to use the Eq object manually. # warnings.warn(str(self) + " == -1 and == 1 are not available for constraints. If you used +/-1 as a constraint, please use the symfit.Eq object manually.", UserWarning) # return orig_eq(self.__class__, other) # else: # return Eq(self, other) # else: # return orig_eq(self.__class__, other) # # def ne(self, other): # if isinstance(other, float) or isinstance(other, int): # return Ne(self, other) # else: # return orig_ne(self.__class__, other) def call(self, *values, **named_values): """ Call an expression to evaluate it at the given point. Future improvements: I would like if func and signature could be buffered after the first call so they don't have to be recalculated for every call. However, nothing can be stored on self as sympy uses __slots__ for efficiency. This means there is no instance dict to put stuff in! And I'm pretty sure it's ill advised to hack into the __slots__ of Expr. However, for the moment I don't really notice a performance penalty in running tests. p.s. In the current setup signature is not even needed since no introspection is possible on the Expr before calling it anyway, which makes calculating the signature absolutely useless. However, I hope that someday some monkey patching expert in shining armour comes by and finds a way to store it in __signature__ upon __init__ of any ``symfit`` expr such that calling inspect_sig.signature on a symbolic expression will tell you which arguments to provide. :param self: Any subclass of sympy.Expr :param values: Values for the Parameters and Variables of the Expr. :param named_values: Values for the vars and params by name. ``named_values`` is allowed to contain too many values, as this sometimes happens when using \*\*fit_result.params on a submodel. The irrelevant params are simply ignored. :return: The function evaluated at ``values``. The type depends entirely on the input. Typically an array or a float but nothing is enforced. """ independent_vars, params = seperate_symbols(self) # Convert to a pythonic function func = sympy_to_py(self, independent_vars + params) # Handle args and kwargs according to the allowed names. parameters = [ # Note that these are inspect_sig.Parameter's, not symfit parameters! inspect_sig.Parameter(arg.name, inspect_sig.Parameter.POSITIONAL_OR_KEYWORD) for arg in independent_vars + params ] arg_names = [arg.name for arg in independent_vars + params] relevant_named_values = { name: value for name, value in named_values.items() if name in arg_names } signature = inspect_sig.Signature(parameters=parameters) bound_arguments = signature.bind(*values, **relevant_named_values) return func(**bound_arguments.arguments) # # Expr.__eq__ = eq # # Expr.__ne__ = ne Expr.__call__ = call Parameter.__call__ = call symfit-0.5.4/symfit/core/printing.py000066400000000000000000000050261412237106600175070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ ``symfit`` occasionally updates the printing of ``sympy`` objects, such that they print into their ``numpy``/``scipy`` equivalent. This is done because sometimes such printing has not been implemented in ``sympy`` yet, or because we want slightly different behavior from the standard one. Users using both ``symfit`` and ``sympy`` should be aware of this. """ import pkg_resources from sympy import HadamardProduct, MatPow, Idx, Inverse from sympy.printing.codeprinter import CodePrinter ########################################################## # Monkeypatch the printers until this is merged upstream # ########################################################## class DontDeleteMe(object): def __init__(self, default_value): self.dont_delete = default_value self.default_value = default_value def __get__(self, instance, owner): return self.dont_delete def __set__(self, instance, value): self.dont_delete = value def __delete__(self, instance): self.dont_delete = self.default_value CodePrinter._not_supported = DontDeleteMe(set()) CodePrinter._number_symbols = DontDeleteMe(set()) def _print_Inverse(self, printer): return "%s(%s)" % (printer._module_format('numpy.linalg.inv'), printer.doprint(self.args[0])) Inverse._numpycode = _print_Inverse def _print_HadamardProduct(self, printer): return "%s*%s" % (printer.doprint(self.args[0]), printer.doprint(self.args[1])) HadamardProduct._numpycode = _print_HadamardProduct if pkg_resources.parse_version(pkg_resources.get_distribution('sympy').version) \ > pkg_resources.parse_version('1.4'): from sympy import HadamardPower def _print_HadamardPower(self, printer): return "%s**%s" % (printer.doprint(self.args[0]), printer.doprint(self.args[1])) HadamardPower._numpycode = _print_HadamardPower def _print_Idx(self, printer): """ Print ``Idx`` objects. """ return "{0}".format(printer.doprint(self.args[0])) Idx._numpycode = _print_Idx def _print_MatPow(self, printer): if self.shape == (1, 1): # Scalar, so we can take a normal power. return printer._print_Pow(self) else: return printer._print_MatPow(self) MatPow._numpycode = _print_MatPow ######################################################### # End of monkeypatch # #########################################################symfit-0.5.4/symfit/core/support.py000066400000000000000000000403511412237106600173710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ This module contains support functions and convenience methods used throughout symfit. Some are used predominantly internally, others are designed for users. """ from __future__ import print_function from collections import OrderedDict import sys import warnings import re import keyword import numpy as np from sympy.utilities.lambdify import lambdify import sympy from sympy.tensor import Idx from sympy import symbols, MatrixExpr from sympy.core.expr import Expr from symfit.core.argument import Parameter, Variable import symfit.core.printing # Overwrites some numpy printing if sys.version_info >= (3,0): import inspect as inspect_sig from functools import wraps else: import funcsigs as inspect_sig from functools32 import wraps if sys.version_info >= (3, 5): from functools import partial else: from ._repeatable_partial import repeatable_partial as partial def isidentifier(s): if hasattr(s, 'isidentifier'): return s.isidentifier() else: # In py27 no such method exists, so we built one ourselves. Notice that # this cannot be used by default because py3 supports unicode # identifiers. if s in keyword.kwlist: return False return re.match(r'^[a-z_][a-z0-9_]*$', s, re.I) is not None class deprecated(object): """ Decorator to raise a DeprecationWarning. """ def __init__(self, replacement=None): """ :param replacement: The function which should now be used instead. """ self.replacement = replacement def __call__(self, func): @wraps(func) def deprecated_func(*args, **kwargs): warnings.warn(DeprecationWarning( '`{}` has been deprecated.'.format(func.__name__) + ' Use `{}` instead.'.format(self.replacement)) if self.replacement else '' ) return func(*args, **kwargs) return deprecated_func def seperate_symbols(func): """ Seperate the symbols in symbolic function func. Return them in alphabetical order. :param func: scipy symbolic function. :return: (vars, params), a tuple of all variables and parameters, each sorted in alphabetical order. :raises TypeError: only symfit Variable and Parameter are allowed, not sympy Symbols. """ params = [] vars = [] for symbol in func.free_symbols: if not isidentifier(str(symbol)): continue # E.g. Indexed objects might print to A[i, j] if isinstance(symbol, Parameter): params.append(symbol) elif isinstance(symbol, Idx): # Idx objects are not seen as parameters or vars. pass elif isinstance(symbol, (MatrixExpr, Expr)): vars.append(symbol) else: raise TypeError('model contains an unknown symbol type, {}'.format(type(symbol))) for der in func.atoms(sympy.Derivative): # Used by jacobians and hessians, where derivatives are treated as # Variables. This way of writing it is purposefully discriminatory # against derivatives wrt variables, since such derivatives should be # performed explicitly in the case of jacs/hess, and are treated # differently in the case of ODEModels. if der.expr in vars and all(isinstance(s, Parameter) for s in der.variables): vars.append(der) params.sort(key=lambda symbol: symbol.name) vars.sort(key=lambda symbol: symbol.name) return vars, params def sympy_to_py(func, args): """ Turn a symbolic expression into a Python lambda function, which has the names of the variables and parameters as it's argument names. :param func: sympy expression :param args: variables and parameters in this model :return: lambda function to be used for numerical evaluation of the model. """ # replace the derivatives with printable variables. derivatives = {var: Variable(var.name) for var in args if isinstance(var, sympy.Derivative)} func = func.xreplace(derivatives) args = [derivatives[var] if isinstance(var, sympy.Derivative) else var for var in args] lambdafunc = lambdify(args, func, dummify=False) # Check if the names of the lambda function are what we expect signature = inspect_sig.signature(lambdafunc) sig_parameters = OrderedDict(signature.parameters) for arg, lambda_arg in zip(args, sig_parameters): if arg.name != lambda_arg: break else: # Lambdifying succesful! return lambdafunc # If we are here (very rare), then one of the lambda arg is still a Dummy. # In this case we will manually handle the naming. lambda_names = sig_parameters.keys() arg_names = [arg.name for arg in args] conversion = dict(zip(arg_names, lambda_names)) # Wrap the lambda such that arg names are translated into the correct dummy # symbol names @wraps(lambdafunc) def wrapped_lambdafunc(*ordered_args, **kwargs): converted_kwargs = {conversion[k]: v for k, v in kwargs.items()} return lambdafunc(*ordered_args, **converted_kwargs) # Update the signature of wrapped_lambdafunc to math our args new_sig_parameters = OrderedDict() for arg_name, dummy_name in conversion.items(): if arg_name == dummy_name: # Already has the correct name new_sig_parameters[arg_name] = sig_parameters[arg_name] else: # Change the dummy inspect.Parameter to the correct name param = sig_parameters[dummy_name] param = param.replace(name=arg_name) new_sig_parameters[arg_name] = param wrapped_lambdafunc.__signature__ = signature.replace( parameters=new_sig_parameters.values() ) return wrapped_lambdafunc def sympy_to_scipy(func, vars, params): """ Convert a symbolic expression to one scipy digs. Not used by ``symfit`` any more. :param func: sympy expression :param vars: variables :param params: parameters :return: Scipy-style function to be used for numerical evaluation of the model. """ lambda_func = sympy_to_py(func, vars, params) def f(x, p): """ Scipy style function. :param x: list of arrays, NxM :param p: tuple of parameter values. """ x = np.atleast_2d(x) y = [x[i] for i in range(len(x))] if len(x[0]) else [] try: ans = lambda_func(*(y + list(p))) except TypeError: # Possibly this is a constant function in which case it only has Parameters. ans = lambda_func(*list(p))# * np.ones(x_shape) return ans return f def variables(names, **kwargs): """ Convenience function for the creation of multiple variables. For more control, consider using ``symbols(names, cls=Variable, **kwargs)`` directly. :param names: string of variable names. Example: x, y = variables('x, y') :param kwargs: kwargs to be passed onto :func:`sympy.core.symbol.symbols` :return: iterable of :class:`symfit.core.argument.Variable` objects """ return symbols(names, cls=Variable, seq=True, **kwargs) def parameters(names, **kwargs): """ Convenience function for the creation of multiple parameters. For more control, consider using ``symbols(names, cls=Parameter, **kwargs)`` directly. The `Parameter` attributes `value`, `min`, `max` and `fixed` can also be provided directly. If given as a single value, the same value will be set for all `Parameter`'s. When a sequence, it must be of the same length as the number of parameters created. Example:: x1, x2 = parameters('x1, x2', value=[2.0, 1.3], min=0.0) :param names: string of parameter names. Example: a, b = parameters('a, b') :param kwargs: kwargs to be passed onto :func:`sympy.core.symbol.symbols`. `value`, `min` and `max` will be handled separately if they are sequences. :return: iterable of :class:`symfit.core.argument.Parameter` objects """ sequence_fields = ['value', 'min', 'max', 'fixed'] sequences = {} for attr in sequence_fields: try: iter(kwargs[attr]) except (TypeError, KeyError): # Not iterable or not provided pass else: sequences[attr] = kwargs.pop(attr) if 'min' in sequences and 'max' in sequences: for min, max in zip(sequences['min'], sequences['max']): if min > max: raise ValueError('The value of `min` should be less than or' ' equal to the value of `max`.') params = symbols(names, cls=Parameter, seq=True, **kwargs) for key, values in sequences.items(): try: assert len(values) == len(params) except AssertionError: raise ValueError( '`len` of keyword-argument `{}` does not match the number of ' '`Parameter`s created.'.format(attr) ) except TypeError: # Iterator do not have a `len` but are allowed. pass finally: for param, value in zip(params, values): setattr(param, key, value) return params class cached_property(property): """ A property which cashes the output of the first ever call and always returns that value from then on, unless delete is called on the attribute. This is typically used in converting `sympy` code into `scipy` compatible code, which is computationally a very expensive step we would like to perform only once. Does not allow setting of the attribute. """ base_str = '_cached' def __init__(self, *args, **kwargs): super(cached_property, self).__init__(*args, **kwargs) self.cache_attr = '{}_{}'.format(self.base_str, self.fget.__name__) def __get__(self, obj, objtype=None): """ In case of a first call, this will call the decorated function and return it's output. On every subsequent call, the same output will be returned. :param obj: the parent object this property is attached to. :param objtype: :return: Output of the first call to the decorated function. """ try: return getattr(obj, self.cache_attr) except AttributeError: # Call the wrapped function with the obj instance as argument setattr(obj, self.cache_attr, self.fget(obj)) return getattr(obj, self.cache_attr) def __delete__(self, obj): """ Calling delete on the attribute will delete the cache. :param obj: parent object. """ try: delattr(obj, self.cache_attr) except AttributeError: pass def jacobian(expr, symbols): """ Derive a symbolic expr w.r.t. each symbol in symbols. This returns a symbolic jacobian vector. :param expr: A sympy Expr. :param symbols: The symbols w.r.t. which to derive. """ jac = [] for symbol in symbols: # Differentiate to every param f = sympy.diff(expr, symbol) jac.append(f) return jac def key2str(target): """ In ``symfit`` there are many dicts with symbol: value pairs. These can not be used immediately as \*\*kwargs, even though this would make a lot of sense from the context. This function wraps such dict to make them usable as \*\*kwargs immediately. :param target: `Mapping` to be made save :return: `Mapping` of str(symbol): value pairs. """ return target.__class__((str(symbol), value) for symbol, value in target.items()) class RequiredKeyword(object): """ Flag variable to indicate that this is a required keyword. """ class RequiredKeywordError(Exception): """ Error raised in case a keyword-only argument is not treated as such. """ class keywordonly(object): """ Decorator class which wraps a python 2 function into one with keyword-only arguments. Example:: @keywordonly(floor=True) def f(x, **kwargs): floor = kwargs.pop('floor') return np.floor(x**2) if floor else x**2 This decorator is not much more than:: floor = kwargs.pop('floor') if 'floor' in kwargs else True However, I prefer it's usage because: - it's clear from reading the function declaration there is an option to provide this argument. The information on possible keywords is where you'd expect it to be. - you're guaranteed that the pop works. - It is fully inspect compatible such that sphynx is able to index these properly as keyword only arguments just like it would for native py3 keyword only arguments. Please note that this decorator needs a ** argument on the wrapped function in order to work. """ def __init__(self, **kwonly_arguments): self.kwonly_arguments = kwonly_arguments # Mark which are required self.required_keywords = { kw for kw, value in kwonly_arguments.items() if value is RequiredKeyword } # Transform all into keywordonly inspect.Parameter objects. self.keywordonly_parameters = OrderedDict( (kw, inspect_sig.Parameter(kw, kind=inspect_sig.Parameter.KEYWORD_ONLY, default=value) ) for kw, value in kwonly_arguments.items() ) def __call__(self, func): """ Returns a decorated version of `func`, who's signature now includes the keyword-only arguments. :param func: the function to be decorated :return: the decorated function """ sig = inspect_sig.signature(func) params = [] # A var keyword has to be found for this function to be decorated for name, param in sig.parameters.items(): if param.kind == param.VAR_KEYWORD: # Keyword only's go before the **kwargs parameter. params.extend(self.keywordonly_parameters.values()) params.append(param) break params.append(param) else: raise RequiredKeywordError( 'The keywordonly decorator requires the function to ' 'accept a **kwargs argument.' ) # Update signature sig = sig.replace(parameters=params) func.__signature__ = sig @wraps(func) def wrapped_func(*args, **kwargs): """ :param args: args used to call the function :param kwargs: kwargs used to call the function :return: Wrapped function which behaves like it has keyword-only arguments. :raises: ``RequiredKeywordError`` if not all required keywords were specified. """ bound_args = func.__signature__.bind(*args, **kwargs) # Apply defaults for param in sig.parameters.values(): if param.name not in bound_args.arguments: if param.default is RequiredKeyword: raise RequiredKeywordError( 'Keyword `{}` is a required keyword. ' 'Please provide a value.'.format(param.name) ) elif param.kind == inspect_sig.Parameter.VAR_KEYWORD: bound_args.arguments[param.name] = {} elif param.kind == inspect_sig.Parameter.VAR_POSITIONAL: bound_args.arguments[param.name] = tuple() else: bound_args.arguments[param.name] = param.default return func(*bound_args.args, **bound_args.kwargs) return wrapped_func def D(*args, **kwargs): # ToDo: Investigate sympy's inheritance properly to see if I can give a # .name atribute to Derivative objects or subclasses. return sympy.Derivative(*args, **kwargs) def name(self): """ Save name which can be used for alphabetic sorting and can be turned into a kwarg. """ base_str = 'd{}{}_'.format(self.derivative_count if self.derivative_count > 1 else '', self.expr) for var, count in self.variable_count: base_str += 'd{}{}'.format(var, count if count > 1 else '') return base_str sympy.Derivative.name = property(name)symfit-0.5.4/symfit/distributions.py000066400000000000000000000042341412237106600176270ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ Some common distributions are defined in this module. That way, users can easily build more complicated expressions without making them look hard. I have deliberately chosen to start these function with a capital, e.g. Gaussian instead of gaussian, because this makes the resulting expressions more readable. """ import sympy def Gaussian(x, mu, sig): """ .. math:: f(x) = \\frac{1}{\\sqrt{2 \\pi \\sigma^2}} e^{- \\frac{(x - \\mu)^2}{2 \\sigma^2}} Gaussian pdf. :param x: free variable. :param mu: mean of the distribution. :param sig: standard deviation of the distribution. :return: sympy.Expr for a Gaussian pdf. """ return sympy.exp(-(x - mu)**2/(2*sig**2))/sympy.sqrt(2*sympy.pi*sig**2) def BivariateGaussian(x, y, mu_x, mu_y, sig_x, sig_y, rho): """ `Bivariate Gaussian pdf `_. :param x: :class:`symfit.core.argument.Variable` :param y: :class:`symfit.core.argument.Variable` :param mu_x: :class:`symfit.core.argument.Parameter` for the mean of `x` :param mu_y: :class:`symfit.core.argument.Parameter` for the mean of `y` :param sig_x: :class:`symfit.core.argument.Parameter` for the standard deviation of `x` :param sig_y: :class:`symfit.core.argument.Parameter` for the standard deviation of `y` :param rho: :class:`symfit.core.argument.Parameter` for the correlation between `x` and `y`. :return: sympy expression for a Bivariate Gaussian pdf. """ exponent = - 1 / (2 * (1 - rho**2)) exponent *= (x - mu_x)**2 / sig_x**2 + (y - mu_y)**2 / sig_y**2 \ - 2 * rho * (x - mu_x) * (y - mu_y) / (sig_x * sig_y) return sympy.exp(exponent) / (2 * sympy.pi * sig_x * sig_y * sympy.sqrt(1 - rho**2)) def Exp(x, l): """ .. math:: f(x) = l e^{- l x} Exponential Distribution pdf. :param x: free variable. :param l: rate parameter. :return: sympy.Expr for an Exponential Distribution pdf. """ return l * sympy.exp(- l * x) # def Beta(): # sympy.stats.Beta()symfit-0.5.4/tests/000077500000000000000000000000001412237106600141775ustar00rootroot00000000000000symfit-0.5.4/tests/__init__.py000066400000000000000000000001221412237106600163030ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MITsymfit-0.5.4/tests/test_argument.py000066400000000000000000000046401412237106600174360ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pickle import pytest import sys import sympy from symfit import Variable, Parameter if sys.version_info >= (3, 0): import inspect as inspect_sig else: import funcsigs as inspect_sig def test_parameter_add(): """ Makes sure the __add__ method of Parameters behaves as expected. """ a = Parameter('a', value=1.0, min=0.5, max=1.5) b = Parameter('b', value=1.0, min=0.0) new = a + b assert isinstance(new, sympy.Add) def test_argument_unnamed(): """ Make sure the generated parameter names follow the pattern """ with pytest.warns(DeprecationWarning): a = Parameter() b = Parameter('b', 10) with pytest.warns(DeprecationWarning): c = Parameter(value=10) with pytest.warns(DeprecationWarning): x = Variable() y = Variable('y') assert str(a) == '{}_{}'.format(a._argument_name, a._argument_index) assert str(a) == 'par_{}'.format(a._argument_index) assert str(b) != '{}_{}'.format(b._argument_name, b._argument_index) assert str(c) == '{}_{}'.format(c._argument_name, c._argument_index) assert c.value == 10 assert b.value == 10 assert str(x) == 'var_{}'.format(x._argument_index) assert str(y) == 'y' with pytest.raises(TypeError): d = Parameter(10) def test_pickle(): """ Make sure attributes are preserved when pickling """ A = Parameter('A', min=0., max=1e3, fixed=True) new_A = pickle.loads(pickle.dumps(A)) assert (A.min, A.value, A.max, A.fixed, A.name) == (new_A.min, new_A.value, new_A.max, new_A.fixed, new_A.name) with pytest.warns(DeprecationWarning): A = Parameter(min=0., max=1e3, fixed=True) new_A = pickle.loads(pickle.dumps(A)) assert (A.min, A.value, A.max, A.fixed, A.name) == (new_A.min, new_A.value, new_A.max, new_A.fixed, new_A.name) def test_slots(): """ Make sure Parameters and Variables don't have a __dict__ """ P = Parameter('P') # If you only have __slots__ you can't set arbitrary attributes, but # you *should* be able to set those that are in your __slots__ try: P.min = 0 except AttributeError: assert False with pytest.raises(AttributeError): P.foo = None V = Variable('V') with pytest.raises(AttributeError): V.bar = None symfit-0.5.4/tests/test_auto_fit.py000066400000000000000000000240411412237106600174230ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import numpy as np from symfit import ( variables, parameters, Fit, Parameter, Variable, Equality, Model, GradientModel ) from symfit.core.minimizers import BFGS, MINPACK, SLSQP, LBFGSB from symfit.distributions import Gaussian def test_vector_fitting(): """ Test the behavior in the presence of bounds or constraints: `Fit` should select `ConstrainedNumericalLeastSquares` when bounds or constraints are provided, or for vector models in general. For scalar models, use `NumericalLeastSquares`. """ a, b = parameters('a, b') a_i, = variables('a_i') xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) # Make a new scalar model. scalar_model = {a_i: a + b} simple_fit = Fit( model=scalar_model, a_i=xdata[0], minimizer=MINPACK ) assert isinstance(simple_fit.minimizer, MINPACK) constrained_fit = Fit( model=scalar_model, a_i=xdata[0], constraints=[Equality(a + b, 110)] ) assert isinstance(constrained_fit.minimizer, SLSQP) a.min = 0 a.max = 25 a.value = 10 b.min = 80 b.max = 120 b.value = 100 bound_fit = Fit( model=scalar_model, a_i=xdata[0], ) assert isinstance(bound_fit.minimizer, LBFGSB) # Repeat all of the above for the Vector model a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} simple_fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) assert isinstance(simple_fit.minimizer, BFGS) constrained_fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], constraints=[Equality(a + b + c, 180)] ) assert isinstance(constrained_fit.minimizer, SLSQP) a.min = 0 a.max = 25 a.value = 10 b.min = 80 b.max = 120 b.value = 100 bound_fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) assert isinstance(bound_fit.minimizer, LBFGSB) fit_result = bound_fit.execute() assert fit_result.value(a) == pytest.approx(np.mean(xdata[0]), rel=1e-6) assert fit_result.value(b) == pytest.approx(np.mean(xdata[1]), rel=1e-6) assert fit_result.value(c) == pytest.approx(np.mean(xdata[2]), rel=1e-6) def test_vector_fitting_bounds(): """ Tests fitting to a 3 component vector valued function, with bounds. """ a, b, c = parameters('a, b, c') a.min = 0 a.max = 25 b.min = 0 b.max = 500 a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(np.mean(xdata[0]), rel=1e-4) assert fit_result.value(b) == pytest.approx(np.mean(xdata[1]), rel=1e-4) assert fit_result.value(c) == pytest.approx(np.mean(xdata[2]), rel=1e-4) def test_vector_fitting_guess(): """ Tests fitting to a 3 component vector valued function, with guesses. """ a, b, c = parameters('a, b, c') a.value = 10 b.value = 100 a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(np.mean(xdata[0]), rel=1e-4) assert fit_result.value(b) == pytest.approx(np.mean(xdata[1]), rel=1e-4) assert fit_result.value(c) == pytest.approx(np.mean(xdata[2]), rel=1e-4) def test_global_fitting(): """ In case of shared parameters between the components of the model, `Fit` should automatically use `ConstrainedLeastSquares`. :return: """ x_1, x_2, y_1, y_2 = variables('x_1, x_2, y_1, y_2') y0, a_1, a_2, b_1, b_2 = parameters('y0, a_1, a_2, b_1, b_2') # The following vector valued function links all the equations together # as stated in the intro. model = Model({ y_1: a_1 * x_1**2 + b_1 * x_1 + y0, y_2: a_2 * x_2**2 + b_2 * x_2 + y0, }) assert model.shared_parameters # Generate data from this model xdata1 = np.linspace(0, 10) xdata2 = xdata1[::2] # Only every other point. ydata1, ydata2 = model(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) # Add some noise to make it appear like real data np.random.seed(1) ydata1 += np.random.normal(0, 2, size=ydata1.shape) ydata2 += np.random.normal(0, 2, size=ydata2.shape) xdata = [xdata1, xdata2] ydata = [ydata1, ydata2] # Guesses a_1.value = 100 a_2.value = 50 b_1.value = 1 b_2.value = 1 y0.value = 10 fit = Fit( model, x_1=xdata[0], x_2=xdata[1], y_1=ydata[0], y_2=ydata[1] ) assert isinstance(fit.minimizer, BFGS) # The next model does not share parameters, but is still a vector model = Model({ y_1: a_1 * x_1**2 + b_1 * x_1, y_2: a_2 * x_2**2 + b_2 * x_2, }) fit = Fit( model, x_1=xdata[0], x_2=xdata[1], y_1=ydata[0], y_2=ydata[1] ) assert not model.shared_parameters assert isinstance(fit.minimizer, BFGS) # Scalar model, still use bfgs. model = Model({ y_1: a_1 * x_1**2 + b_1 * x_1, }) fit = Fit(model, x_1=xdata[0], y_1=ydata[0]) assert model.shared_parameters is False assert isinstance(fit.minimizer, BFGS) def test_gaussian_2d_fitting(): """ Tests fitting to a scalar gaussian function with 2 independent variables. """ mean = (0.6, 0.4) # x, y mean 0.6, 0.4 cov = [[0.2**2, 0], [0, 0.1**2]] # TODO: Since we bin this data later on in a 100 bins, just evaluate 100 # points on a Gaussian, and add an appropriate amount of noise. This # burns CPU cycles without good reason. data = np.random.multivariate_normal(mean, cov, 1000000) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 0], data[:, 1], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False, indexing='ij') x0 = Parameter('x0', value=mean[0], min=0.0, max=1.0) sig_x = Parameter('sig_x', value=0.2, min=0.0, max=0.3) y0 = Parameter('y0', value=mean[1], min=0.0, max=1.0) sig_y = Parameter('sig_y', value=0.1, min=0.0, max=0.3) A = Parameter('A', value=np.mean(ydata), min=0.0) x = Variable('x') y = Variable('y') g = Variable('g') model = GradientModel( {g: A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y)}) fit = Fit(model, x=xx, y=yy, g=ydata) fit_result = fit.execute() assert fit_result.value(x0) == pytest.approx(np.mean(data[:, 0]), 1e-3) assert fit_result.value(y0) == pytest.approx(np.mean(data[:, 1]), 1e-3) assert np.abs(fit_result.value(sig_x)) == pytest.approx(np.std(data[:, 0]), 1e-2) assert np.abs(fit_result.value(sig_y)) == pytest.approx(np.std(data[:, 1]), 1e-2) assert fit_result.r_squared >= 0.96 def test_gaussian_2d_fitting_background(): """ Tests fitting to a scalar gaussian function with 2 independent variables to data with a background. Added after #149. """ mean = (0.6, 0.4) # x, y mean 0.6, 0.4 cov = [[0.2**2, 0], [0, 0.1**2]] background = 3.0 # TODO: Since we bin this data later on in a 100 bins, just evaluate 100 # points on a Gaussian, and add an appropriate amount of noise. This # burns CPU cycles without good reason. data = np.random.multivariate_normal(mean, cov, 500000) # print(data.shape) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 0], data[:, 1], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 ydata += background # Background # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False, indexing='ij') x0 = Parameter('x0', value=1.1 * mean[0], min=0.0, max=1.0) sig_x = Parameter('sig_x', value=1.1 * 0.2, min=0.0, max=0.3) y0 = Parameter('y0', value=1.1 * mean[1], min=0.0, max=1.0) sig_y = Parameter('sig_y', value=1.1 * 0.1, min=0.0, max=0.3) A = Parameter('A', value=1.1 * np.mean(ydata), min=0.0) b = Parameter('b', value=1.2 * background, min=0.0) x = Variable('x') y = Variable('y') g = Variable('g') model = GradientModel({g: A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y) + b}) # ydata, = model(x=xx, y=yy, x0=mean[0], y0=mean[1], sig_x=np.sqrt(cov[0][0]), sig_y=np.sqrt(cov[1][1]), A=1, b=3.0) fit = Fit(model, x=xx, y=yy, g=ydata) fit_result = fit.execute() assert fit_result.value(x0) / np.mean(data[:, 0]) == pytest.approx(1.0, 1e-2) assert fit_result.value(y0) / np.mean(data[:, 1]) == pytest.approx(1.0, 1e-2) assert np.abs(fit_result.value(sig_x)) / np.std(data[:, 0]) == pytest.approx(1.0, 1e-2) assert np.abs(fit_result.value(sig_y)) / np.std(data[:, 1]) == pytest.approx(1.0, 1e-2) assert background / fit_result.value(b) == pytest.approx(1.0, 1e-1) assert fit_result.r_squared >= 0.96 symfit-0.5.4/tests/test_constrained.py000066400000000000000000001214431412237106600201260ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import numpy as np import sympy from scipy.integrate import simps from scipy.optimize import NonlinearConstraint, minimize from symfit import ( variables, Variable, parameters, Parameter, ODEModel, Fit, Equality, D, Model, log, FitResults, GreaterThan, Eq, Ge, Le, CallableNumericalModel, HadamardProduct, GradientModel ) from symfit.distributions import Gaussian from symfit.core.minimizers import ( SLSQP, MINPACK, TrustConstr, ScipyConstrainedMinimize, COBYLA ) from symfit.core.support import key2str from symfit.core.objectives import MinimizeModel, LogLikelihood, LeastSquares from symfit.core.models import ModelError from symfit import ( Symbol, MatrixSymbol, Inverse, CallableModel, sqrt, Sum, Idx, symbols ) from tests.test_minimizers import subclasses """ Tests for the `Fit` object. This object does everything the normal `NumericalLeastSquares` does and more. Tests should therefore cover the full range of scenarios `symfit` currently handles. """ def test_simple_kinetics(): """ Simple kinetics data to test fitting """ tdata = np.array([10, 26, 44, 70, 120]) adata = 10e-4 * np.array([44, 34, 27, 20, 14]) a, b, t = variables('a, b, t') k, a0 = parameters('k, a0') k.value = 0.01 # a0.value, a0.min, a0.max = 54 * 10e-4, 40e-4, 60e-4 a0 = 54 * 10e-4 model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: a0, b: 0.0}) fit = Fit(ode_model, t=tdata, a=adata, b=None) fit_result = fit.execute(tol=1e-9) assert fit_result.value(k) == pytest.approx(4.302875e-01, 1e-5) assert fit_result.stdev(k) == pytest.approx(6.447068e-03, 1e-5) def test_global_fitting(): """ Test a global fitting scenario with datasets of unequal length. In this scenario, a quartic equation is fitted where the constant term is shared between the datasets. (e.g. identical background noise) """ x_1, x_2, y_1, y_2 = variables('x_1, x_2, y_1, y_2') y0, a_1, a_2, b_1, b_2 = parameters('y0, a_1, a_2, b_1, b_2') # The following vector valued function links all the equations together # as stated in the intro. model = Model({ y_1: a_1 * x_1**2 + b_1 * x_1 + y0, y_2: a_2 * x_2**2 + b_2 * x_2 + y0, }) # Generate data from this model # xdata = np.linspace(0, 10) xdata1 = np.linspace(0, 10) xdata2 = xdata1[::2] # Make the sets of unequal size ydata1, ydata2 = model(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) # Add some noise to make it appear like real data np.random.seed(1) ydata1 += np.random.normal(0, 2, size=ydata1.shape) ydata2 += np.random.normal(0, 2, size=ydata2.shape) xdata = [xdata1, xdata2] ydata = [ydata1, ydata2] # Guesses a_1.value = 100 a_2.value = 50 b_1.value = 1 b_2.value = 1 y0.value = 10 eval_jac = model.eval_jacobian(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) assert len(eval_jac) == 2 for comp in eval_jac: assert len(comp) == len(model.params) sigma_y = np.concatenate((np.ones(20), [2., 4., 5, 7, 3])) fit = Fit(model, x_1=xdata[0], x_2=xdata[1], y_1=ydata[0], y_2=ydata[1], sigma_y_2=sigma_y) fit_result = fit.execute() # fit_curves = model(x_1=xdata[0], x_2=xdata[1], **fit_result.params) assert fit_result.value(y0) == pytest.approx(1.061892e+01, 1e-03) assert fit_result.value(a_1) == pytest.approx(1.013269e+02, 1e-03) assert fit_result.value(a_2) == pytest.approx(5.625694e+01, 1e-03) assert fit_result.value(b_1) == pytest.approx(3.362240e-01, 1e-03) assert fit_result.value(b_2) == pytest.approx(1.565253e+00, 1e-03) def test_named_fitting(): xdata = np.linspace(1, 10, 10) ydata = 3*xdata**2 a = Parameter('a', 1.0) b = Parameter('b', 2.5) x, y = variables('x, y') model = {y: a*x**b} fit = Fit(model, x=xdata, y=ydata) fit_result = fit.execute() assert isinstance(fit_result, FitResults) assert fit_result.value(a) == pytest.approx(3.0, 1e-3) assert fit_result.value(b) == pytest.approx(2.0, 1e-4) def test_param_error_analytical(): """ Take an example in which the parameter errors are known and see if `Fit` reproduces them. It also needs to support the absolute_sigma argument. """ N = 10000 sigma = 25.0 xn = np.arange(N, dtype=np.float) np.random.seed(110) yn = np.random.normal(size=xn.shape, scale=sigma) a = Parameter('a') y = Variable('y') model = {y: a} constr_fit = Fit(model, y=yn, sigma_y=sigma) constr_result = constr_fit.execute() fit = Fit(model, y=yn, sigma_y=sigma, minimizer=MINPACK) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(constr_result.value(a), 1e-5) assert fit_result.stdev(a) == pytest.approx(constr_result.stdev(a), 1e-5) # Analytical answer for mean of N(0,sigma): sigma_mu = sigma/N**0.5 assert fit_result.value(a) == pytest.approx(np.mean(yn), 1e-5) assert fit_result.stdev(a) == pytest.approx(sigma_mu, 1e-5) # Compare for absolute_sigma = False. constr_fit = Fit(model, y=yn, sigma_y=sigma, absolute_sigma=False) constr_result = constr_fit.execute() fit = Fit(model, y=yn, sigma_y=sigma, minimizer=MINPACK, absolute_sigma=False) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(constr_result.value(a), 1e-5) assert fit_result.stdev(a) == pytest.approx(constr_result.stdev(a), 1e-5) def test_grid_fitting(): xdata = np.arange(-5, 5, 1) ydata = np.arange(5, 15, 1) xx, yy = np.meshgrid(xdata, ydata, sparse=False) zdata = (2.5*xx**2 + 3.0*yy**2) a = Parameter('a', value=2.4, max=2.75) b = Parameter('b', value=3.1, min=2.75) x = Variable('x') y = Variable('y') z = Variable('z') new = {z: a*x**2 + b*y**2} fit = Fit(new, x=xx, y=yy, z=zdata) # results = fit.execute(options={'maxiter': 10}) results = fit.execute() assert results.value(a) == pytest.approx(2.5, 1e-4) assert results.value(b) == pytest.approx(3.0, 1e-4) @pytest.mark.skip(reason='Fit fails to compute the covariance matrix for a sparse grid.') def test_grid_fitting_sparse(): xdata = np.arange(-5, 5, 1) ydata = np.arange(5, 15, 1) xx, yy = np.meshgrid(xdata, ydata, sparse=True) zdata = (2.5*xx**2 + 3.0*yy**2) a = Parameter(value=2.4, max=2.75) b = Parameter(value=3.1, min=2.75) x = Variable('x') y = Variable('y') z = Variable('z') new = {z: a*x**2 + b*y**2} fit = Fit(new, x=xx, y=yy, z=zdata) results = fit.execute() assert results.value(a) == pytest.approx(2.5, 1e-4) assert results.value(b) == pytest.approx(3.0, 1e-4) def test_vector_constrained_fitting(): """ Tests `Fit` with vector models. The classical example of fitting measurements of the angles of a triangle is taken. In this case we know they should add up to 180 degrees, so this can be added as a constraint. Additionally, not even all three angles have to be provided with measurement data since the constrained means the angles are not independent. """ a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit_none = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=None, ) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) fit_std = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], minimizer=MINPACK ) fit_constrained = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], constraints=[Equality(a + b + c, 180)] ) fit_none_result = fit_none.execute() fit_new_result = fit.execute() std_result = fit_std.execute() constr_result = fit_constrained.execute() # The total of averages should equal the total of the params by definition mean_total = np.mean(np.sum(xdata, axis=0)) params_tot = std_result.value(a) + std_result.value(b) + std_result.value(c) assert mean_total / params_tot == pytest.approx(1.0, 1e-4) # The total after constraining to 180 should be exactly 180. params_tot = constr_result.value(a) + constr_result.value(b) + constr_result.value(c) assert isinstance(fit_constrained.minimizer, SLSQP) assert 180.0 == pytest.approx(params_tot, 1e-4) # The standard method and the Constrained object called without constraints # should behave roughly the same. assert fit_new_result.value(b) == pytest.approx(std_result.value(b), 1e-4) assert fit_new_result.value(a) == pytest.approx(std_result.value(a), 1e-4) assert fit_new_result.value(c) == pytest.approx(std_result.value(c), 1e-4) # When fitting with a dataset set to None, for this example the value of c # should be unaffected. assert fit_none_result.value(a) == pytest.approx(std_result.value(a), 1e-4) assert fit_none_result.value(b) == pytest.approx(std_result.value(b), 1e-4) assert fit_none_result.value(c) == pytest.approx(c.value) fit_none_constr = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=None, constraints=[Equality(a + b + c, 180)] ) none_constr_result = fit_none_constr.execute() params_tot = none_constr_result.value(a) + none_constr_result.value(b) + none_constr_result.value(c) assert 180.0 == pytest.approx(params_tot, 1e-4) def test_vector_parameter_error(): """ Tests `Fit` parameter error estimation with vector models. This is done by using the typical angles of a triangle example. For completeness, we throw in covariance between the angles. As per 0.5.0 this test has been updated in an important way. Previously the covariance matrix was estimated on a per component basis for global fitting problems. This was incorrect, but no solution was possible at the time. Now, we calculate the covariance matrix from the Hessian of the function being optimized, and so now the covariance is calculated correctly in those scenarios. As a result for this particular test however, it means we lose sensitivity to the error of each parameter separately. This makes sense, since the uncertainty is now being spread out over the components. To regain this, the user should just fit the components separately. """ N = 10000 a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} np.random.seed(1) # Sample from a multivariate normal with correlation. pcov = np.array([[0.4, 0.3, 0.5], [0.3, 0.8, 0.4], [0.5, 0.4, 1.2]]) xdata = np.random.multivariate_normal([10, 100, 70], pcov, N).T fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], ) fit_std = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], minimizer=MINPACK ) fit_new_result = fit.execute() std_result = fit_std.execute() # When no errors are given, we default to `absolute_sigma=False`, since # that is the best we can do. assert not fit.absolute_sigma assert not fit_std.absolute_sigma # The standard method and the Constrained object called without constraints # should give roughly the same parameter values. assert fit_new_result.value(a) == pytest.approx(std_result.value(a), 1e-3) assert fit_new_result.value(b) == pytest.approx(std_result.value(b), 1e-3) assert fit_new_result.value(c) == pytest.approx(std_result.value(c), 1e-3) # in this toy model, fitting is identical to simply taking the average assert fit_new_result.value(a) == pytest.approx(np.mean(xdata[0]), 1e-4) assert fit_new_result.value(b) == pytest.approx(np.mean(xdata[1]), 1e-4) assert fit_new_result.value(c) == pytest.approx(np.mean(xdata[2]), 1e-4) # All stdev's must be equal assert fit_new_result.stdev(a) == pytest.approx(fit_new_result.stdev(b), 1e-3) assert fit_new_result.stdev(a) == pytest.approx(fit_new_result.stdev(c), 1e-3) # Test for a miss on the exact value assert not fit_new_result.stdev(a) == pytest.approx(np.sqrt(pcov[0, 0]/N), 1e-3) assert not fit_new_result.stdev(b) == pytest.approx(np.sqrt(pcov[1, 1]/N), 1e-3) assert not fit_new_result.stdev(c) == pytest.approx(np.sqrt(pcov[2, 2]/N), 1e-3) # The standard object actually does not predict the right values for # stdev, because its method for computing them apparently does not allow # for vector valued functions. # So actually, for vector valued functions its better to use # Fit, though this does not give covariances. # With the correct values of sigma, absolute_sigma=True should be in # agreement with analytical. sigmadata = np.array([ np.sqrt(pcov[0, 0]), np.sqrt(pcov[1, 1]), np.sqrt(pcov[2, 2]) ]) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], sigma_a_i=sigmadata[0], sigma_b_i=sigmadata[1], sigma_c_i=sigmadata[2], ) assert fit.absolute_sigma fit_result = fit.execute() # The standard deviation in the mean is stdev/sqrt(N), # see test_param_error_analytical assert fit_result.stdev(a)/np.sqrt(pcov[0, 0]/N) == pytest.approx(1.0, 1e-4) assert fit_result.stdev(b)/np.sqrt(pcov[1, 1]/N) == pytest.approx(1.0, 1e-4) assert fit_result.stdev(c)/np.sqrt(pcov[2, 2]/N) == pytest.approx(1.0, 1e-4) # Finally, we should confirm that with unrealistic sigma and # absolute_sigma=True, we are no longer in agreement with the analytical result # Let's take everything to be 1 to point out the dangers of doing so. sigmadata = np.array([1, 1, 1]) fit2 = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], sigma_a_i=sigmadata[0], sigma_b_i=sigmadata[1], sigma_c_i=sigmadata[2], absolute_sigma=True ) fit_result = fit2.execute() # Should be off bigly assert not fit_result.stdev(a)/np.sqrt(pcov[0, 0]/N) == pytest.approx(1.0, 1e-1) assert not fit_result.stdev(b)/np.sqrt(pcov[1, 1]/N) == pytest.approx(1.0, 1e-1) assert not fit_result.stdev(c)/np.sqrt(pcov[2, 2]/N) == pytest.approx(1.0, 1e-5) def test_error_advanced(): """ Compare the error propagation of Fit against NumericalLeastSquares. Models an example from the mathematica docs and tries to replicate it: http://reference.wolfram.com/language/howto/FitModelsWithMeasurementErrors.html """ data = [ [0.9, 6.1, 9.5], [3.9, 6., 9.7], [0.3, 2.8, 6.6], [1., 2.2, 5.9], [1.8, 2.4, 7.2], [9., 1.7, 7.], [7.9, 8., 10.4], [4.9, 3.9, 9.], [2.3, 2.6, 7.4], [4.7, 8.4, 10.] ] xdata, ydata, zdata = [np.array(data) for data in zip(*data)] # errors = np.array([.4, .4, .2, .4, .1, .3, .1, .2, .2, .2]) a = Parameter('a', 3.0) b = Parameter('b', 0.9) c = Parameter('c', 5.0) x = Variable('x') y = Variable('y') z = Variable('z') model = {z: a * log(b * x + c * y)} const_fit = Fit(model, xdata, ydata, zdata, absolute_sigma=False) assert len(const_fit.model(x=xdata, y=ydata, a=2, b=2, c=5)) == 1 assert const_fit.model(x=xdata, y=ydata, a=2, b=2, c=5)[0].shape == (10,) assert len(const_fit.model.eval_jacobian(x=xdata, y=ydata, a=2, b=2, c=5)) == 1 assert const_fit.model.eval_jacobian(x=xdata, y=ydata, a=2, b=2, c=5)[0].shape == (3, 10) assert len(const_fit.model.eval_hessian(x=xdata, y=ydata, a=2, b=2, c=5)) == 1 assert const_fit.model.eval_hessian(x=xdata, y=ydata, a=2, b=2, c=5)[0].shape == (3, 3, 10) assert const_fit.objective(a=2, b=2, c=5).shape == tuple() assert const_fit.objective.eval_jacobian(a=2, b=2, c=5).shape == (3,) assert const_fit.objective.eval_hessian(a=2, b=2, c=5).shape == (3, 3) assert const_fit.objective.eval_hessian(a=2, b=2, c=5).dtype != object const_result = const_fit.execute() fit = Fit(model, xdata, ydata, zdata, absolute_sigma=False, minimizer=MINPACK) std_result = fit.execute() assert const_fit.absolute_sigma == fit.absolute_sigma assert const_result.value(a) == pytest.approx(std_result.value(a), 1e-4) assert const_result.value(b) == pytest.approx(std_result.value(b), 1e-4) assert const_result.value(c) == pytest.approx(std_result.value(c), 1e-4) # This used to be a tighter equality test, but since we now use the # Hessian we actually get a more accurate value from the standard fit # then for MINPACK. Hence we check if it is roughly equal, and if our # stdev is greater than that of minpack. assert const_result.stdev(a) / std_result.stdev(a) == pytest.approx(1, 1e-2) assert const_result.stdev(b) / std_result.stdev(b) == pytest.approx(1, 1e-1) assert const_result.stdev(c) / std_result.stdev(c) == pytest.approx(1, 1e-2) assert const_result.stdev(a) >= std_result.stdev(a) assert const_result.stdev(b) >= std_result.stdev(b) assert const_result.stdev(c) >= std_result.stdev(c) def test_gaussian_2d_fitting(): """ Tests fitting to a scalar gaussian function with 2 independent variables. Very sensitive to initial guesses, and if they are chosen too restrictive Fit actually throws a tantrum. It therefore appears to be more sensitive than NumericalLeastSquares. """ mean = (0.6, 0.4) # x, y mean 0.6, 0.4 cov = [[0.2**2, 0], [0, 0.1**2]] np.random.seed(0) data = np.random.multivariate_normal(mean, cov, 100000) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 0], data[:, 1], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False, indexing='ij') x0 = Parameter('x0', value=mean[0], min=0.0, max=1.0) sig_x = Parameter('sig_x', value=0.2, min=0.0, max=0.3) y0 = Parameter('y0', value=mean[1], min=0.0, max=1.0) sig_y = Parameter('sig_y', value=0.1, min=0.0, max=0.3) A = Parameter('A', value=np.mean(ydata), min=0.0) x = Variable('x') y = Variable('y') g = Variable('g') model = GradientModel({g: A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y)}) fit = Fit(model, x=xx, y=yy, g=ydata) fit_result = fit.execute() assert fit_result.value(x0) == pytest.approx(np.mean(data[:, 0]), 1e-3) assert fit_result.value(y0) == pytest.approx(np.mean(data[:, 1]), 1e-3) assert np.abs(fit_result.value(sig_x)) == pytest.approx(np.std(data[:, 0]), 1e-2) assert np.abs(fit_result.value(sig_y)) == pytest.approx(np.std(data[:, 1]), 1e-2) assert (fit_result.r_squared, 0.96) # Compare with industry standard MINPACK fit_std = Fit(model, x=xx, y=yy, g=ydata, minimizer=MINPACK) fit_std_result = fit_std.execute() assert fit_std_result.value(x0) == pytest.approx(fit_result.value(x0), 1e-4) assert fit_std_result.value(y0) == pytest.approx(fit_result.value(y0), 1e-4) assert fit_std_result.value(sig_x) == pytest.approx(fit_result.value(sig_x), 1e-4) assert fit_std_result.value(sig_y) == pytest.approx(fit_result.value(sig_y), 1e-4) assert fit_std_result.r_squared == pytest.approx(fit_result.r_squared, 1e-4) def test_fixed_and_constrained(): """ Taken from #165. Fixing parameters and constraining others caused a TypeError: missing a required argument: 'theta1', which was caused by a mismatch in the shape of the initial guesses given and the number of parameters constraints expected. The initial_guesses no longer contained those corresponding to fixed parameters. """ phi1, phi2, theta1, theta2 = parameters('phi1, phi2, theta1, theta2') x, y = variables('x, y') model_dict = {y: (1 + x * theta1 + theta2 * x ** 2) / (1 + phi1 * x * theta1 + phi2 * theta2 * x ** 2)} constraints = [GreaterThan(theta1, theta2)] xdata = np.array([0., 0.000376, 0.000752, 0.0015, 0.00301, 0.00601, 0.00902]) ydata = np.array([1., 1.07968041, 1.08990638, 1.12151629, 1.13068452, 1.15484109, 1.19883952]) phi1.value = 0.845251484373516 phi1.fixed = True phi2.value = 0.7105427053026403 phi2.fixed = True fit = Fit(model_dict, x=xdata, y=ydata, constraints=constraints, minimizer=SLSQP) fit_result_slsqp = fit.execute() # The data and fixed parameters should be partialed away. objective_kwargs = { phi2.name: phi2.value, phi1.name: phi1.value, x.name: xdata, } constraint_kwargs = { phi2.name: phi2.value, phi1.name: phi1.value, } for index, constraint in enumerate(fit.minimizer.constraints): assert isinstance(constraint, MinimizeModel) assert constraint.model == fit.constraints[index] assert constraint.data == fit.data assert constraint.data == fit.objective.data # Data should be the same memory location so they can share state. assert id(fit.objective.data) == id(constraint.data) # Test if the fixed params have been partialed away assert key2str(constraint._invariant_kwargs).keys() == constraint_kwargs.keys() assert key2str(fit.objective._invariant_kwargs).keys() == objective_kwargs.keys() # Compare the shapes. The constraint shape should now be the same as # that of the objective obj_val = fit.minimizer.objective(fit.minimizer.initial_guesses) obj_jac = fit.minimizer.wrapped_jacobian(fit.minimizer.initial_guesses) # scalars don't have lengths with pytest.raises(TypeError): len(obj_val) assert len(obj_jac) == 2 for index, constraint in enumerate(fit.minimizer.wrapped_constraints): assert constraint['type'] == 'ineq' assert 'args' not in constraint assert callable(constraint['fun']) assert callable(constraint['jac']) # The argument should be the partialed Constraint object assert constraint['fun'] == fit.minimizer.constraints[index] assert isinstance(constraint['fun'], MinimizeModel) assert 'jac' in constraint # Test the shapes cons_val = constraint['fun'](fit.minimizer.initial_guesses) cons_jac = constraint['jac'](fit.minimizer.initial_guesses) assert cons_val.shape == (1,) assert isinstance(cons_val[0], float) assert obj_jac.shape == cons_jac.shape assert obj_jac.shape == (2,) def test_interdependency_constrained(): """ Test a model with interdependent components, and with constraints which depend on the Model's output. This is done in the MatrixSymbol formalism, using a Tikhonov regularization as an example. In this, a matrix inverse has to be calculated and is used multiple times. Therefore we split that term of into a seperate component, so the inverse only has to be computed once per model call. See https://arxiv.org/abs/1901.05348 for a more detailed background. """ N = Symbol('N', integer=True) M = MatrixSymbol('M', N, N) W = MatrixSymbol('W', N, N) I = MatrixSymbol('I', N, N) y = MatrixSymbol('y', N, 1) c = MatrixSymbol('c', N, 1) a, = parameters('a') z, = variables('z') i = Idx('i') model_dict = { W: Inverse(I + M / a ** 2), c: - W * y, z: sqrt(c.T * c) } # Sympy currently does not support derivatives of matrix expressions, # so we use CallableModel instead of Model. model = CallableModel(model_dict) # Generate data iden = np.eye(2) M_mat = np.array([[2, 1], [3, 4]]) y_vec = np.array([[3], [5]]) eval_model = model(I=iden, M=M_mat, y=y_vec, a=0.1) # Calculate the answers 'manually' so I know it was done properly W_manual = np.linalg.inv(iden + M_mat / 0.1 ** 2) c_manual = - np.atleast_2d(W_manual.dot(y_vec)) z_manual = np.atleast_1d(np.sqrt(c_manual.T.dot(c_manual))) assert y_vec.shape == (2, 1) assert M_mat.shape == (2, 2) assert iden.shape == (2, 2) assert W_manual.shape == (2, 2) assert c_manual.shape == (2, 1) assert z_manual.shape == (1, 1) assert W_manual == pytest.approx(eval_model.W) assert c_manual == pytest.approx(eval_model.c) assert z_manual == pytest.approx(eval_model.z) fit = Fit(model, z=z_manual, I=iden, M=M_mat, y=y_vec) fit_result = fit.execute() # See if a == 0.1 was reconstructed properly. Since only a**2 features # in the equations, we check for the absolute value. Setting a.min = 0.0 # is not appreciated by the Minimizer, it seems. assert np.abs(fit_result.value(a)) == pytest.approx(0.1) def test_data_for_constraint(): """ Test the signature handling when constraints are at play. Constraints should take seperate data, but still kwargs that are not found in either the model nor the constraints should raise an error. """ A, mu, sig = parameters('A, mu, sig') x, y, Y = variables('x, y, Y') model = Model({y: A * Gaussian(x, mu=mu, sig=sig)}) constraint = Model.as_constraint(Y, model, constraint_type=Eq) np.random.seed(2) xdata = np.random.normal(1.2, 2, 10) ydata, xedges = np.histogram(xdata, bins=int(np.sqrt(len(xdata))), density=True) # Allowed fit = Fit(model, x=xdata, y=ydata, Y=2, constraints=[constraint]) assert isinstance(fit.objective, LeastSquares) assert isinstance(fit.minimizer.constraints[0], MinimizeModel) fit = Fit(model, x=xdata, y=ydata) assert isinstance(fit.objective, LeastSquares) fit = Fit(model, x=xdata, objective=LogLikelihood) assert isinstance(fit.objective, LogLikelihood) # Not allowed with pytest.raises(TypeError): fit = Fit(model, x=xdata, y=ydata, Y=2) with pytest.raises(TypeError): fit = Fit(model, x=xdata, y=ydata, Y=2, Z=3, constraints=[constraint]) with pytest.raises(TypeError): fit = Fit(model, x=xdata, y=ydata, objective=LogLikelihood) def test_constrained_dependent_on_model(): """ For a simple Gaussian distribution, we test if Models of various types can be used as constraints. Of particular interest are NumericalModels, which can be used to fix the integral of the model during the fit to 1, as it should be for a probability distribution. :return: """ A, mu, sig = parameters('A, mu, sig') x, y, Y = variables('x, y, Y') i = Idx('i', (0, 1000)) sig.min = 0.0 model = GradientModel({y: A * Gaussian(x, mu=mu, sig=sig)}) # Generate data, 100 samples from a N(1.2, 2) distribution np.random.seed(2) xdata = np.random.normal(1.2, 2, 1000) ydata, xedges = np.histogram(xdata, bins=int(np.sqrt(len(xdata))), density=True) xcentres = (xedges[1:] + xedges[:-1]) / 2 # Unconstrained fit fit = Fit(model, x=xcentres, y=ydata) unconstr_result = fit.execute() # Constraints must be scalar models. with pytest.raises(ModelError): Model.as_constraint([A - 1, sig - 1], model, constraint_type=Eq) constraint_exact = Model.as_constraint(A * sqrt(2 * sympy.pi) * sig - 1, model, constraint_type=Eq) # Only when explicitly asked, do models behave as constraints. assert hasattr(constraint_exact, 'constraint_type') assert constraint_exact.constraint_type == Eq assert not hasattr(model, 'constraint_type') # Now lets make some valid constraints and see if they are respected! # FIXME These first two should be symbolical integrals over `y` instead, # but currently this is not converted into a numpy/scipy function. So # instead the first two are not valid constraints. constraint_model = Model.as_constraint(A - 1, model, constraint_type=Eq) constraint_exact = Eq(A, 1) constraint_num = CallableNumericalModel.as_constraint( {Y: lambda x, y: simps(y, x) - 1}, # Integrate using simps model=model, connectivity_mapping={Y: {x, y}}, constraint_type=Eq ) # Test for all these different types of constraint. for constraint in [constraint_model, constraint_exact, constraint_num]: if not isinstance(constraint, Eq): assert constraint.constraint_type == Eq xcentres = (xedges[1:] + xedges[:-1]) / 2 fit = Fit(model, x=xcentres, y=ydata, constraints=[constraint]) # Test if conversion into a constraint was done properly fit_constraint = fit.constraints[0] assert fit.model.params == fit_constraint.params assert fit_constraint.constraint_type == Eq con_map = fit_constraint.connectivity_mapping if isinstance(constraint, CallableNumericalModel): assert con_map == {Y: {x, y}, y: {x, mu, sig, A}} assert fit_constraint.independent_vars == [x] assert fit_constraint.dependent_vars == [Y] assert fit_constraint.interdependent_vars == [y] assert fit_constraint.params == [A, mu, sig] else: # TODO if these constraints can somehow be written as integrals # depending on y and x this if/else should be removed. assert con_map == {fit_constraint.dependent_vars[0]: {A}} assert fit_constraint.independent_vars == [] assert len(fit_constraint.dependent_vars) == 1 assert fit_constraint.interdependent_vars == [] assert fit_constraint.params == [A, mu, sig] # Finally, test if the constraint worked fit_result = fit.execute(options={'eps': 1e-15, 'ftol': 1e-10}) unconstr_value = fit.minimizer.wrapped_constraints[0]['fun'](**unconstr_result.params) constr_value = fit.minimizer.wrapped_constraints[0]['fun'](**fit_result.params) # TODO because of a bug by pytest we have to solve it like this assert constr_value[0] == pytest.approx(0, abs=1e-10) # And if it was very poorly met before assert not unconstr_value[0] == pytest.approx(0.0, 1e-1) def test_constrained_dependent_on_matrixmodel(): """ Similar to test_constrained_dependent_on_model, but now using MatrixSymbols. This is much more powerful, since now the constraint can really be written down as a symbolical one as well. """ A, mu, sig = parameters('A, mu, sig') M = symbols('M', integer=True) # Number of measurements # Create vectors for all the quantities x = MatrixSymbol('x', M, 1) dx = MatrixSymbol('dx', M, 1) y = MatrixSymbol('y', M, 1) I = MatrixSymbol('I', M, 1) # 'identity' vector Y = MatrixSymbol('Y', 1, 1) B = MatrixSymbol('B', M, 1) i = Idx('i', M) # Looks overly complicated, but it's just a simple Gaussian model = CallableModel( {y: A * sympy.exp(- HadamardProduct(B, B) / (2 * sig**2)) / sympy.sqrt(2*sympy.pi*sig**2), B: (x - mu * I)} ) assert model.independent_vars == [I, x] assert model.dependent_vars == [y] assert model.interdependent_vars == [B] assert model.params == [A, mu, sig] # Generate data, sample from a N(1.2, 2) distribution. Has to be 2D. np.random.seed(2) # TODO: sample points on a Guassian and add appropriate noise. xdata = np.random.normal(1.2, 2, size=10000) ydata, xedges = np.histogram(xdata, bins=int(np.sqrt(len(xdata))), density=True) xcentres = np.atleast_2d((xedges[1:] + xedges[:-1]) / 2).T xdiff = np.atleast_2d((xedges[1:] - xedges[:-1])).T ydata = np.atleast_2d(ydata).T Idata = np.ones_like(xcentres) assert xcentres.shape == (int(np.sqrt(len(xdata))), 1) assert xdiff.shape == (int(np.sqrt(len(xdata))), 1) assert ydata.shape == (int(np.sqrt(len(xdata))), 1) fit = Fit(model, x=xcentres, y=ydata, I=Idata) unconstr_result = fit.execute() constraint = CallableModel({Y: Sum(y[i, 0] * dx[i, 0], i) - 1}) with pytest.raises(ModelError): fit = Fit(model, x=xcentres, y=ydata, dx=xdiff, M=len(xcentres), I=Idata, constraints=[constraint]) constraint = CallableModel.as_constraint( {Y: Sum(y[i, 0] * dx[i, 0], i) - 1}, model=model, constraint_type=Eq ) assert constraint.independent_vars == [I, M, dx, x] assert constraint.dependent_vars == [Y] assert constraint.interdependent_vars == [B, y] assert constraint.params == [A, mu, sig] assert constraint.constraint_type == Eq # Provide the extra data needed for the constraints as well fit = Fit(model, x=xcentres, y=ydata, dx=xdiff, M=len(xcentres), I=Idata, constraints=[constraint]) # After treatment, our constraint should have `y` & `b` dependencies assert fit.constraints[0].independent_vars == [I, M, dx, x] assert fit.constraints[0].dependent_vars == [Y] assert fit.constraints[0].interdependent_vars == [B, y] assert fit.constraints[0].params == [A, mu, sig] assert fit.constraints[0].constraint_type == Eq assert isinstance(fit.objective, LeastSquares) assert isinstance(fit.minimizer.constraints[0], MinimizeModel) assert {k for k, v in fit.data.items() if v is not None} == {x, y, dx, M, I, fit.model.sigmas[y]} # These belong to internal variables assert {k for k, v in fit.data.items() if v is None} == {constraint.sigmas[Y], Y} constr_result = fit.execute() # The constraint should not be met for the unconstrained fit assert not fit.minimizer.wrapped_constraints[0]['fun'](**unconstr_result.params)[0] == pytest.approx(0, 1e-3) # And at high precision with constraint # TODO Change after resolve bug at pytest assert fit.minimizer.wrapped_constraints[0]['fun'](**constr_result.params)[0] == pytest.approx(0, abs=1e-8) # Constraining will negatively effect the R^2 value, but... assert constr_result.r_squared < unconstr_result.r_squared # both should be pretty good assert constr_result.r_squared > 0.99 def test_fixed_and_constrained_tc(): """ Taken from #165. Make sure the TrustConstr minimizer can deal with constraints and fixed parameters. """ phi1, phi2, theta1, theta2 = parameters('phi1, phi2, theta1, theta2') x, y = variables('x, y') model_dict = {y: (1 + x * theta1 + theta2 * x ** 2) / (1 + phi1 * x * theta1 + phi2 * theta2 * x ** 2)} constraints = [GreaterThan(theta1, theta2)] xdata = np.array([0., 0.000376, 0.000752, 0.0015, 0.00301, 0.00601, 0.00902]) ydata = np.array([1., 1.07968041, 1.08990638, 1.12151629, 1.13068452, 1.15484109, 1.19883952]) phi1.value = 0.845251484373516 phi1.fixed = True phi2.value = 0.7105427053026403 phi2.fixed = True fit = Fit(model_dict, x=xdata, y=ydata, constraints=constraints, minimizer=TrustConstr) fit_result_tc = fit.execute() # The data and fixed parameters should be partialed away. objective_kwargs = { phi2.name: phi2.value, phi1.name: phi1.value, x.name: xdata, } constraint_kwargs = { phi2.name: phi2.value, phi1.name: phi1.value, } for index, constraint in enumerate(fit.minimizer.constraints): assert isinstance(constraint, MinimizeModel) assert constraint.model == fit.constraints[index] assert constraint.data == fit.data assert constraint.data == fit.objective.data # Data should be the same memory location so they can share state. assert id(fit.objective.data) == id(constraint.data) # Test if the data and fixed params have been partialed away assert key2str(constraint._invariant_kwargs).keys() == constraint_kwargs.keys() assert key2str(fit.objective._invariant_kwargs).keys() == objective_kwargs.keys() # Compare the shapes. The constraint shape should now be the same as # that of the objective obj_val = fit.minimizer.objective(fit.minimizer.initial_guesses) obj_jac = fit.minimizer.wrapped_jacobian(fit.minimizer.initial_guesses) with pytest.raises(TypeError): len(obj_val) # scalars don't have lengths assert len(obj_jac) == 2 for index, constraint in enumerate(fit.minimizer.wrapped_constraints): assert callable(constraint.fun) assert callable(constraint.jac) # The argument should be the partialed Constraint object assert constraint.fun == fit.minimizer.constraints[index] assert isinstance(constraint.fun, MinimizeModel) # Test the shapes cons_val = constraint.fun(fit.minimizer.initial_guesses) cons_jac = constraint.jac(fit.minimizer.initial_guesses) assert cons_val.shape == (1,) assert isinstance(cons_val[0], float) assert obj_jac.shape == cons_jac.shape assert obj_jac.shape == (2,) def test_constrainedminimizers(): """ Compare the different constrained minimizers, to make sure all support constraints, and converge to the same answer. """ minimizers = list(subclasses(ScipyConstrainedMinimize)) x = Parameter('x', value=-1.0) y = Parameter('y', value=1.0) z = Variable('z') model = Model({z: 2 * x * y + 2 * x - x ** 2 - 2 * y ** 2}) # First we try an unconstrained fit results = [] for minimizer in minimizers: fit = Fit(- model, minimizer=minimizer) assert isinstance(fit.objective, MinimizeModel) fit_result = fit.execute(tol=1e-15) results.append(fit_result) # Compare the parameter values. for r1, r2 in zip(results[:-1], results[1:]): assert r1.value(x) == pytest.approx(r2.value(x), 1e-6) assert r1.value(y) == pytest.approx(r2.value(y), 1e-6) assert r1.covariance_matrix == pytest.approx(r2.covariance_matrix) constraints = [ Ge(y - 1, 0), # y - 1 >= 0, Eq(x ** 3 - y, 0), # x**3 - y == 0, ] # Constrained fit. results = [] for minimizer in minimizers: if minimizer is COBYLA: # COBYLA only supports inequality. continue fit = Fit(- model, constraints=constraints, minimizer=minimizer) fit_result = fit.execute(tol=1e-15) results.append(fit_result) for r1, r2 in zip(results[:-1], results[1:]): assert r1.value(x) == pytest.approx(r2.value(x), 1e-6) assert r1.value(y) == pytest.approx(r2.value(y), 1e-6) assert r1.covariance_matrix == pytest.approx(r2.covariance_matrix) def test_trustconstr(): """ Solve the standard constrained example from https://docs.scipy.org/doc/scipy-0.18.1/reference/tutorial/optimize.html#constrained-minimization-of-multivariate-scalar-functions-minimize using the trust-constr method. """ def func(x, sign=1.0): """ Objective function """ return sign*(2*x[0]*x[1] + 2*x[0] - x[0]**2 - 2*x[1]**2) def func_jac(x, sign=1.0): """ Derivative of objective function """ dfdx0 = sign*(-2*x[0] + 2*x[1] + 2) dfdx1 = sign*(2*x[0] - 4*x[1]) return np.array([dfdx0, dfdx1]) def func_hess(x, sign=1.0): """ Hessian of objective function """ dfdx2 = sign*(-2) dfdxdy = sign * 2 dfdy2 = sign * (-4) return np.array([[dfdx2, dfdxdy], [dfdxdy, dfdy2]]) def cons_f(x): return [x[1] - 1, x[0]**3 - x[1]] def cons_J(x): return [[0, 1], [3 * x[0] ** 2, -1]] def cons_H(x, v): return v[0] * np.zeros((2, 2)) + v[1] * np.array([[6 * x[0], 0], [0, 0]]) # Unconstrained fit res = minimize(func, [-1.0, 1.0], args=(-1.0,), jac=func_jac, hess=func_hess, method='trust-constr') assert res.x == pytest.approx([2, 1]) # Constrained fit nonlinear_constraint = NonlinearConstraint(cons_f, 0, [np.inf, 0], jac=cons_J, hess=cons_H) res_constr = minimize(func, [-1.0, 1.0], args=(-1.0,), tol=1e-15, jac=func_jac, hess=func_hess, method='trust-constr', constraints=[nonlinear_constraint]) assert res_constr.x == pytest.approx([1, 1]) # Symfit equivalent code x = Parameter('x', value=-1.0) y = Parameter('y', value=1.0) z = Variable('z') model = Model({z: 2 * x * y + 2 * x - x ** 2 - 2 * y ** 2}) # Unconstrained fit first, see if we get the known result. fit = Fit(-model, minimizer=TrustConstr) fit_result = fit.execute() assert list(fit_result.params.values()) == pytest.approx([2, 1]) # Now we are ready for the constrained fit. constraints = [ Le(- y + 1, 0), # y - 1 >= 0, Eq(x ** 3 - y, 0), # x**3 - y == 0, ] fit = Fit(-model, constraints=constraints, minimizer=TrustConstr) fit_result = fit.execute(tol=1e-15) # Test if the constrained results are equal assert list(fit_result.params.values()) == pytest.approx(res_constr.x) symfit-0.5.4/tests/test_distributions.py000066400000000000000000000023021412237106600205070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division import sympy from symfit import Variable, Parameter from symfit.distributions import Gaussian, Exp def test_gaussian(): """ Make sure that symfit.distributions.Gaussians produces the expected sympy expression. """ x0 = Parameter('x0') sig = Parameter('sig', positive=True) x = Variable('x') new = sympy.exp(-(x - x0)**2/(2*sig**2))/sympy.sqrt((2*sympy.pi*sig**2)) assert isinstance(new, sympy.Expr) g = Gaussian(x, x0, sig) assert issubclass(g.__class__, sympy.Expr) assert new == g # A pdf should always integrate to 1 on its domain assert sympy.integrate(g, (x, -sympy.oo, sympy.oo)) == 1 def test_exp(): """ Make sure that symfit.distributions.Exp produces the expected sympy expression. """ l = Parameter('l', positive=True) x = Variable('x') new = l * sympy.exp(- l * x) assert isinstance(new, sympy.Expr) e = Exp(x, l) assert issubclass(e.__class__, sympy.Expr) assert new == e # A pdf should always integrate to 1 on its domain assert sympy.integrate(e, (x, 0, sympy.oo)) == 1 symfit-0.5.4/tests/test_finite_difference.py000066400000000000000000000160421412237106600212430ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT import symfit as sf import numpy as np import pytest def setup_module(): np.random.seed(0) def test_1_1_model(): '''Tests the case with 1 component and 1 parameter''' x, y = sf.variables('x, y') a = sf.Parameter(name='a') model = sf.Model({y: 3 * a * x**2}) x_data = np.arange(10) exact = model.eval_jacobian(x=x_data, a=3.5) approx = model.finite_difference(x=x_data, a=3.5) _assert_equal(exact, approx) exact = model.eval_jacobian(x=3, a=3.5) approx = model.finite_difference(x=3, a=3.5) _assert_equal(exact, approx) def test_1_multi_model(): '''Tests the case with 1 component and multiple parameters''' x, y = sf.variables('x, y') a, b = sf.parameters('a, b') model = sf.Model({y: 3 * a * x**2 - sf.exp(b) * x}) x_data = np.arange(10) exact = model.eval_jacobian(x=x_data, a=3.5, b=2) approx = model.finite_difference(x=x_data, a=3.5, b=2) _assert_equal(exact, approx) exact = model.eval_jacobian(x=3, a=3.5, b=2) approx = model.finite_difference(x=3, a=3.5, b=2) _assert_equal(exact, approx) def test_multi_1_model(): '''Tests the case with multiple components and one parameter''' x, y, z = sf.variables('x, y, z') a, = sf.parameters('a') model = sf.Model({y: 3 * a * x**2, z: sf.exp(a*x)}) x_data = np.arange(10) exact = model.eval_jacobian(x=x_data, a=3.5) approx = model.finite_difference(x=x_data, a=3.5) _assert_equal(exact, approx) exact = model.eval_jacobian(x=3, a=3.5) approx = model.finite_difference(x=3, a=3.5) _assert_equal(exact, approx) def test_multi_multi_model(): '''Tests the case with multiple components and multiple parameters''' x, y, z = sf.variables('x, y, z') a, b, c = sf.parameters('a, b, c') model = sf.Model({y: 3 * a * x**2 + b * x - c, z: sf.exp(a*x - b) * c}) x_data = np.arange(10) exact = model.eval_jacobian(x=x_data, a=3.5, b=2, c=5) approx = model.finite_difference(x=x_data, a=3.5, b=2, c=5) _assert_equal(exact, approx, rel=1e-3) exact = model.eval_jacobian(x=3, a=3.5, b=2, c=5) approx = model.finite_difference(x=3, a=3.5, b=2, c=5) _assert_equal(exact, approx, rel=1e-3) def test_multi_indep(): ''' Tests the case with multiple components, multiple parameters and multiple independent variables ''' w, x, y, z = sf.variables('w, x, y, z') a, b, c = sf.parameters('a, b, c') model = sf.Model({y: 3 * a * x**2 + b * x * w - c, z: sf.exp(a*x - b) + c*w}) x_data = np.arange(10)/10 w_data = np.arange(10) exact = model.eval_jacobian(x=x_data, w=w_data, a=3.5, b=2, c=5) approx = model.finite_difference(x=x_data, w=w_data, a=3.5, b=2, c=5) _assert_equal(exact, approx) exact = model.eval_jacobian(x=0.3, w=w_data, a=3.5, b=2, c=5) approx = model.finite_difference(x=0.3, w=w_data, a=3.5, b=2, c=5) _assert_equal(exact, approx) exact = model.eval_jacobian(x=0.3, w=5, a=3.5, b=2, c=5) approx = model.finite_difference(x=0.3, w=5, a=3.5, b=2, c=5) _assert_equal(exact, approx) def test_ODE_stdev(): """ Make sure that parameters from ODEModels get standard deviations. """ x, v, t = sf.variables('x, v, t') k = sf.Parameter(name='k') k.min = 0 k.value = 10 a = -k * x model = sf.ODEModel( { sf.D(v, t): a, sf.D(x, t): v, }, initial={v: 0, x: 1, t: 0} ) t_data = np.linspace(0, 10, 150) noise = np.random.normal(1, 0.05, t_data.shape) x_data = model(t=t_data, k=11).x * noise v_data = model(t=t_data, k=11).v * noise fit = sf.Fit(model, t=t_data, x=x_data, v=v_data) result = fit.execute() assert result.stdev(k) is not None assert np.isfinite(result.stdev(k)) def test_unequal_data(): """ Test to make sure finite differences work with data of unequal length. """ x_1, x_2, y_1, y_2 = sf.variables('x_1, x_2, y_1, y_2') y0, a_1, a_2, b_1, b_2 = sf.parameters('y0, a_1, a_2, b_1, b_2') model = sf.Model({ y_1: a_1 * x_1**2 + b_1 * x_1 + y0, y_2: a_2 * x_2**2 + b_2 * x_2 + y0, }) # Generate data from this model xdata1 = np.linspace(0, 10) xdata2 = xdata1[::2] # Only every other point. exact = model.eval_jacobian(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) approx = model.finite_difference(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111, y0=10.8) # First axis is the number of components assert len(exact) == 2 assert len(approx) == 2 # Second axis is the number of parameters, same for all components for exact_comp, approx_comp, xdata in zip(exact, approx, [xdata1, xdata2]): assert len(exact_comp) == len(model.params) assert len(approx_comp) == len(model.params) for exact_elem, approx_elem in zip(exact_comp, approx_comp): assert exact_elem.shape == xdata.shape assert approx_elem.shape == xdata.shape _assert_equal(exact, approx, rel=1e-4) model = sf.Model({ y_1: a_1 * x_1**2 + b_1 * x_1, y_2: a_2 * x_2**2 + b_2 * x_2, }) exact = model.eval_jacobian(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111) approx = model.finite_difference(x_1=xdata1, x_2=xdata2, a_1=101.3, b_1=0.5, a_2=56.3, b_2=1.1111) _assert_equal(exact, approx, rel=1e-4) model = sf.Model({ y_1: a_1 * x_1**2 + b_1 * x_1, }) exact = model.eval_jacobian(x_1=xdata1, a_1=101.3, b_1=0.5) approx = model.finite_difference(x_1=xdata1, a_1=101.3, b_1=0.5) _assert_equal(exact, approx, rel=1e-4) def test_harmonic_oscillator_errors(): """ Make sure the errors produced by fitting ODE's are the same as when fitting an exact solution. """ x, v, t = sf.variables('x, v, t') k = sf.Parameter(name='k', value=100) m = 1 a = -k/m * x ode_model = sf.ODEModel({sf.D(v, t): a, sf.D(x, t): v}, initial={t: 0, v: 0, x: 1}) t_data = np.linspace(0, 10, 250) np.random.seed(2) noise = np.random.normal(1, 0.05, size=t_data.shape) x_data = ode_model(t=t_data, k=100).x * noise ode_fit = sf.Fit(ode_model, t=t_data, x=x_data, v=None) ode_result = ode_fit.execute() phi = 0 A = 1 model = sf.Model({x: A * sf.cos(sf.sqrt(k/m) * t + phi)}) fit = sf.Fit(model, t=t_data, x=x_data) result = fit.execute() assert result.value(k) == pytest.approx(ode_result.value(k), 1e-4) assert result.stdev(k) == pytest.approx(ode_result.stdev(k), 1e-2) assert result.stdev(k) >= ode_result.stdev(k) def _assert_equal(exact, approx, **kwargs): assert len(exact) == len(approx) for exact_comp, approx_comp in zip(exact, approx): assert approx_comp == pytest.approx(exact_comp, **kwargs) symfit-0.5.4/tests/test_fit_result.py000066400000000000000000000230121412237106600177660ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import pickle from collections import OrderedDict import numpy as np from symfit import ( Variable, Parameter, Fit, FitResults, Eq, Ge, CallableNumericalModel, Model ) from symfit.distributions import BivariateGaussian from symfit.core.minimizers import ( BaseMinimizer, MINPACK, BFGS, NelderMead, ChainedMinimizer, BasinHopping ) from symfit.core.objectives import ( LogLikelihood, LeastSquares, VectorLeastSquares, MinimizeModel ) def ge_constraint(a): # Has to be in the global namespace for pickle. return a - 1 class TestTestResult(): @classmethod def setup_class(cls): xdata = np.linspace(1, 10, 10) ydata = 3 * xdata ** 2 cls.a = Parameter('a') cls.b = Parameter('b') x = Variable('x') y = Variable('y') model = Model({y: cls.a * x ** cls.b}) fit = Fit(model, x=xdata, y=ydata) cls.fit_result = fit.execute() fit = Fit(model, x=xdata, y=ydata, minimizer=MINPACK) cls.minpack_result = fit.execute() fit = Fit(model, x=xdata, objective=LogLikelihood) cls.likelihood_result = fit.execute() fit = Fit(model, x=xdata, y=ydata, minimizer=[BFGS, NelderMead]) cls.chained_result = fit.execute() z = Variable('z') constraints = [ Eq(cls.a, cls.b), CallableNumericalModel.as_constraint( {z: ge_constraint}, connectivity_mapping={z: {cls.a}}, constraint_type=Ge, model=model ) ] fit = Fit(model, x=xdata, y=ydata, constraints=constraints) cls.constrained_result = fit.execute() fit = Fit(model, x=xdata, y=ydata, constraints=constraints, minimizer=BasinHopping) cls.constrained_basinhopping_result = fit.execute() def test_params_type(self): assert isinstance(self.fit_result.params, OrderedDict) def test_minimizer_output_type(self): assert isinstance(self.fit_result.minimizer_output, dict) assert isinstance(self.minpack_result.minimizer_output, dict) assert isinstance(self.likelihood_result.minimizer_output, dict) def test_fitting(self): """ Test if the fitting worked in the first place. """ assert isinstance(self.fit_result, FitResults) assert self.fit_result.value(self.a) == pytest.approx(3.0) assert self.fit_result.value(self.b) == pytest.approx(2.0) assert isinstance(self.fit_result.stdev(self.a), float) assert isinstance(self.fit_result.stdev(self.b), float) assert isinstance(self.fit_result.r_squared, float) # by definition since there's no fuzzyness assert self.fit_result.r_squared == 1.0 def test_fitting_2(self): np.random.seed(43) mean = (0.62, 0.71) # x, y mean 0.7, 0.7 cov = [ [0.102**2, 0], [0, 0.07**2] ] data_1 = np.random.multivariate_normal(mean, cov, 10**5) mean = (0.33, 0.28) # x, y mean 0.3, 0.3 cov = [ # rho = 0.25 [0.05 ** 2, 0.25 * 0.05 * 0.101], [0.25 * 0.05 * 0.101, 0.101 ** 2] ] data_2 = np.random.multivariate_normal(mean, cov, 10**5) data = np.vstack((data_1, data_2)) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 1], data[:, 0], bins=200, range=[[0.0, 1.0], [0.0, 1.0]], density=True) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False) x = Variable('x') y = Variable('y') x0_1 = Parameter('x0_1', value=0.6, min=0.5, max=0.7) sig_x_1 = Parameter('sig_x_1', value=0.1, min=0.0, max=0.2) y0_1 = Parameter('y0_1', value=0.7, min=0.6, max=0.8) sig_y_1 = Parameter('sig_y_1', value=0.05, min=0.0, max=0.2) rho_1 = Parameter('rho_1', value=0.0, min=-0.5, max=0.5) A_1 = Parameter('A_1', value=0.5, min=0.3, max=0.7) g_1 = A_1 * BivariateGaussian(x=x, y=y, mu_x=x0_1, mu_y=y0_1, sig_x=sig_x_1, sig_y=sig_y_1, rho=rho_1) x0_2 = Parameter('x0_2', value=0.3, min=0.2, max=0.4) sig_x_2 = Parameter('sig_x_2', value=0.05, min=0.0, max=0.2) y0_2 = Parameter('y0_2', value=0.3, min=0.2, max=0.4) sig_y_2 = Parameter('sig_y_2', value=0.1, min=0.0, max=0.2) rho_2 = Parameter('rho_2', value=0.26, min=0.0, max=0.8) A_2 = Parameter('A_2', value=0.5, min=0.3, max=0.7) g_2 = A_2 * BivariateGaussian(x=x, y=y, mu_x=x0_2, mu_y=y0_2, sig_x=sig_x_2, sig_y=sig_y_2, rho=rho_2) model = g_1 + g_2 fit = Fit(model, xx, yy, ydata) fit_result = fit.execute() assert fit_result.r_squared > 0.95 for param in fit.model.params: try: assert fit_result.stdev(param)**2 == pytest.approx(fit_result.variance(param)) except AssertionError: assert fit_result.variance(param) <= 0.0 assert np.isnan(fit_result.stdev(param)) # Covariance matrix should be symmetric for param_1 in fit.model.params: for param_2 in fit.model.params: assert fit_result.covariance(param_1, param_2) == pytest.approx(fit_result.covariance(param_2, param_1), rel=1e-3) def test_minimizer_included(self): """"The minimizer used should be included in the results.""" assert isinstance(self.constrained_result.minimizer, BaseMinimizer) assert isinstance(self.constrained_basinhopping_result.minimizer, BaseMinimizer) assert isinstance(self.likelihood_result.minimizer, BaseMinimizer) assert isinstance(self.fit_result.minimizer, BaseMinimizer) assert isinstance(self.chained_result.minimizer, ChainedMinimizer) for minimizer, cls in zip(self.chained_result.minimizer.minimizers, [BFGS, NelderMead]): assert isinstance(minimizer, cls) def test_objective_included(self): """"The objective used should be included in the results.""" assert isinstance(self.fit_result.objective, LeastSquares) assert isinstance(self.minpack_result.objective, VectorLeastSquares) assert isinstance(self.likelihood_result.objective, LogLikelihood) assert isinstance(self.constrained_result.objective, LeastSquares) assert isinstance(self.constrained_basinhopping_result.objective, LeastSquares) def test_constraints_included(self): """ Test if the constraints have been properly fed to the results object so we can easily print their compliance. """ # For a constrained fit we expect a list of MinimizeModel objectives. for constrained_result in [self.constrained_result, self.constrained_basinhopping_result]: assert isinstance(constrained_result.constraints, list) for constraint in self.constrained_result.constraints: assert isinstance(constraint, MinimizeModel) def test_message_included(self): """Status message should be included.""" assert isinstance(self.fit_result.status_message, str) assert isinstance(self.minpack_result.status_message, str) assert isinstance(self.likelihood_result.status_message, str) assert isinstance(self.constrained_result.status_message, str) assert isinstance(self.constrained_basinhopping_result.status_message, str) def test_pickle(self): for fit_result in [self.fit_result, self.chained_result, self.constrained_basinhopping_result, self.constrained_result, self.likelihood_result]: dumped = pickle.dumps(fit_result) new_result = pickle.loads(dumped) assert sorted(fit_result.__dict__.keys()) == sorted(new_result.__dict__.keys()) for k, v1 in fit_result.__dict__.items(): v2 = new_result.__dict__[k] if k == 'minimizer': assert type(v1) == type(v2) elif k != 'minimizer_output': # Ignore minimizer_output if isinstance(v1, np.ndarray): assert v1 == pytest.approx(v2, nan_ok=True) def test_gof_presence(self): """ Test if the expected goodness of fit estimators are present. """ assert hasattr(self.fit_result, 'objective_value') assert hasattr(self.fit_result, 'r_squared') assert hasattr(self.fit_result, 'chi_squared') assert not hasattr(self.fit_result, 'log_likelihood') assert not hasattr(self.fit_result, 'likelihood') assert hasattr(self.minpack_result, 'objective_value') assert hasattr(self.minpack_result, 'r_squared') assert hasattr(self.minpack_result, 'chi_squared') assert not hasattr(self.minpack_result, 'log_likelihood') assert not hasattr(self.minpack_result, 'likelihood') assert hasattr(self.likelihood_result, 'objective_value') assert not hasattr(self.likelihood_result, 'r_squared') assert not hasattr(self.likelihood_result, 'chi_squared') assert hasattr(self.likelihood_result, 'log_likelihood') assert hasattr(self.likelihood_result, 'likelihood') symfit-0.5.4/tests/test_general.py000066400000000000000000001001451412237106600172260ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import sys import numpy as np import scipy.stats from scipy.optimize import curve_fit import pytest from symfit import ( Variable, Parameter, Fit, FitResults, log, variables, parameters, Model, exp, integrate, oo, GradientModel ) from symfit.core.minimizers import ( MINPACK, LBFGSB, BoundedMinimizer, DifferentialEvolution, BaseMinimizer, ChainedMinimizer ) from symfit.core.objectives import LogLikelihood, MinimizeModel, LeastSquares from symfit.distributions import Gaussian, Exp, BivariateGaussian from tests.test_minimizers import subclasses if sys.version_info >= (3, 0): import inspect as inspect_sig else: import funcsigs as inspect_sig def setup_module(): np.random.seed(0) def test_callable(): """ Make sure that symfit expressions are callable (with scalars and arrays), and produce the expected results. """ a, b = parameters('a, b') x, y = variables('x, y') func = a*x**2 + b*y**2 result = func(x=2, y=3, a=3, b=9) assert result == 3*2**2 + 9*3**2 result = func(2, 3, a=3, b=9) assert result == 3*2**2 + 9*3**2 xdata = np.arange(1, 10) ydata = np.arange(1, 10) result = func(x=ydata, y=ydata, a=3, b=9) assert np.array_equal(result, 3*xdata**2 + 9*ydata**2) def test_named_fitting(): """ Make sure that fitting with NumericalLeastSquares works using a dict as model and that the resulting fit_result is of the right type. """ xdata = np.linspace(1, 10, 10) ydata = 3*xdata**2 a = Parameter('a', value=1.0) b = Parameter('b', value=2.5) x, y = variables('x, y') model = {y: a*x**b} fit = Fit(model, x=xdata, y=ydata, minimizer=MINPACK) fit_result = fit.execute() assert isinstance(fit_result, FitResults) assert fit_result.value(a) == pytest.approx(3.0) assert fit_result.value(b) == pytest.approx(2.0) def test_backwards_compatible_fitting(): """ In 0.4.2 we replaced the usage of inspect by automatically generated names. This can cause problems for users using named variables to call fit. """ xdata = np.linspace(1, 10, 10) ydata = 3*xdata**2 a = Parameter('a', value=1.0) b = Parameter('b', value=2.5) y = Variable('y') with pytest.warns(DeprecationWarning): x = Variable() model = {y: a*x**b} with pytest.raises(TypeError): fit = Fit(model, x=xdata, y=ydata) def test_vector_fitting(): """ Tests fitting to a 3 component vector valued function, without bounds or guesses. """ a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], minimizer=MINPACK ) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(np.mean(xdata[0]), 1e-5) assert fit_result.value(b) == pytest.approx(np.mean(xdata[1]), 1e-4) assert fit_result.value(c) == pytest.approx(np.mean(xdata[2]), 1e-5) def test_vector_none_fitting(): """ Fit to a 3 component vector valued function with one variables data set to None, without bounds or guesses. """ a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit_none = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=None, minimizer=MINPACK ) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], minimizer=MINPACK ) fit_none_result = fit_none.execute() fit_result = fit.execute() assert fit_none_result.value(b) == pytest.approx(fit_result.value(b), 1e-4) assert fit_none_result.value(a) == pytest.approx(fit_result.value(a), 1e-4) # the parameter without data should be unchanged. assert fit_none_result.value(c) == pytest.approx(1.0) def test_vector_fitting_guess(): """ Tests fitting to a 3 component vector valued function, with guesses. """ a, b, c = parameters('a, b, c') a.value = 10 b.value = 100 a_i, b_i, c_i = variables('a_i, b_i, c_i') model = {a_i: a, b_i: b, c_i: c} xdata = np.array([ [10.1, 9., 10.5, 11.2, 9.5, 9.6, 10.], [102.1, 101., 100.4, 100.8, 99.2, 100., 100.8], [71.6, 73.2, 69.5, 70.2, 70.8, 70.6, 70.1], ]) fit = Fit( model=model, a_i=xdata[0], b_i=xdata[1], c_i=xdata[2], minimizer=MINPACK ) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(np.mean(xdata[0]), 1e-4) assert fit_result.value(b) == pytest.approx(np.mean(xdata[1]), 1e-4) assert fit_result.value(c) == pytest.approx(np.mean(xdata[2]), 1e-4) def test_fitting(): """ Tests fitting with NumericalLeastSquares. Makes sure that the resulting objects and values are of the right type, and that the fit_result does not have unexpected members. """ xdata = np.linspace(1, 10, 10) ydata = 3*xdata**2 a = Parameter('a') # 3.1, min=2.5, max=3.5 b = Parameter('b') x = Variable('x') new = a*x**b fit = Fit(new, xdata, ydata, minimizer=MINPACK) fit_result = fit.execute() assert isinstance(fit_result, FitResults) assert fit_result.value(a) == pytest.approx(3.0) assert fit_result.value(b) == pytest.approx(2.0) assert isinstance(fit_result.stdev(a), float) assert isinstance(fit_result.stdev(b), float) assert isinstance(fit_result.r_squared, float) assert fit_result.r_squared == 1.0 # by definition since there's no fuzzyness def test_grid_fitting(): """ Tests fitting a scalar function with 2 independent variables. """ xdata = np.arange(-5, 5, 1) ydata = np.arange(5, 15, 1) xx, yy = np.meshgrid(xdata, ydata, sparse=False) zdata = (2.5*xx**2 + 3.0*yy**2) a = Parameter('a', value=2.5, max=2.75) b = Parameter('b', value=3.0, min=2.75) x = Variable('x') y = Variable('y') z = Variable('z') new = {z: a*x**2 + b*y**2} fit = Fit(new, x=xx, y=yy, z=zdata) results = fit.execute() assert isinstance(fit.minimizer, LBFGSB) assert results.value(a) == pytest.approx(2.5) assert results.value(b) == pytest.approx(3.) def test_model_callable(): """ Tests if Model objects are callable in the way expected. Calling a model should evaluate it's expression(s) with the given values. The return value is a namedtuple. The signature should also work so inspection is saved. """ a, b = parameters('a, b') x, y = variables('x, y') new = a*x**2 + b*y**2 model = Model(new) ans = model(3, 3, 2, 2) assert isinstance(ans, tuple) z, = ans assert z == 36 for arg_name, name in zip(('x', 'y', 'a', 'b'), inspect_sig.signature(model).parameters): assert arg_name == name @pytest.mark.skip(reason=("Test fails a proportion of the time because `z_1`, " "`z_2` and `z_3` are not necessarily ordered as " "expected so the assert statements fail. `z_1` is " "frequenty equal to either 36 or 72.")) def test_model_callable_from_model_init(): """ Tests if Model objects are callable in the way expected. Calling a model should evaluate it's expression(s) with the given values. The return value is a namedtuple. The signature should also work so inspection is saved. Tests from Model __init__ directly """ a, b = parameters('a, b') x, y = variables('x, y') model = Model([ a*x**2, 4*b*y**2, a*x**2 + b*y**2 ]) z_1, z_2, z_3 = model(3, 3, 2, 2) assert z_1 == 18 assert z_2 == 72 assert z_3 == 36 for arg_name, name in zip(('x', 'y', 'a', 'b'), inspect_sig.signature(model).parameters): assert arg_name == name def test_model_callable_from_dict(): """ Tests if Model objects are callable in the way expected. Calling a model should evaluate it's expression(s) with the given values. The return value is a namedtuple. The signature should also work so inspection is saved. Tests from dict. """ a, b = parameters('a, b') x, y = variables('x, y') z_1, z_2, z_3 = variables('z_1, z_2, z_3') model = Model({ z_1: a*x**2, z_2: 4*b*y**2, z_3: a*x**2 + b*y**2 }) z_1, z_2, z_3 = model(3, 3, 2, 2) assert z_1 == 18 assert z_2 == 72 assert z_3 == 36 for arg_name, name in zip(('x', 'y', 'a', 'b'), inspect_sig.signature(model).parameters): assert arg_name == name def test_2D_fitting(): """ Makes sure that a scalar model with 2 independent variables has the proper signature, and that the fit result is of the correct type. """ xdata = np.random.randint(-10, 11, size=(2, 400)) zdata = 2.5*xdata[0]**2 + 7.0*xdata[1]**2 a = Parameter('a') b = Parameter('b') x = Variable('x') y = Variable('y') new = a*x**2 + b*y**2 fit = Fit(new, xdata[0], xdata[1], zdata) result = fit.model(xdata[0], xdata[1], 2, 3) assert isinstance(result, tuple) for arg_name, name in zip(('x', 'y', 'a', 'b'), inspect_sig.signature(fit.model).parameters): assert arg_name == name fit_result = fit.execute() assert isinstance(fit_result, FitResults) def test_gaussian_fitting(): """ Tests fitting to a gaussian function and fit_result.params unpacking. """ xdata = 2*np.random.rand(10000) - 1 # random betwen [-1, 1] ydata = 5.0 * scipy.stats.norm.pdf(xdata, loc=0.0, scale=1.0) x0 = Parameter('x0') sig = Parameter('sig') A = Parameter('A') x = Variable('x') g = GradientModel(A * Gaussian(x, x0, sig)) fit = Fit(g, xdata, ydata) assert isinstance(fit.objective, LeastSquares) fit_result = fit.execute() assert fit_result.value(A) == pytest.approx(5.0) assert np.abs(fit_result.value(sig)) == pytest.approx(1.0) assert fit_result.value(x0) == pytest.approx(0.0) # raise Exception([i for i in fit_result.params]) sexy = g(x=2.0, **fit_result.params) ugly = g( x=2.0, x0=fit_result.value(x0), A=fit_result.value(A), sig=fit_result.value(sig), ) assert sexy == ugly def test_2_gaussian_2d_fitting(): """ Tests fitting to a scalar gaussian with 2 independent variables with tight bounds. """ mean = (0.3, 0.4) # x, y mean 0.6, 0.4 cov = [[0.01**2, 0], [0, 0.01**2]] # TODO: evaluate gaussian at 100x100 points and add appropriate noise data = np.random.multivariate_normal(mean, cov, 3000000) mean = (0.7, 0.8) # x, y mean 0.6, 0.4 cov = [[0.01**2, 0], [0, 0.01**2]] data_2 = np.random.multivariate_normal(mean, cov, 3000000) data = np.vstack((data, data_2)) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 1], data[:, 0], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False) # xdata = np.dstack((xx, yy)).T x = Variable('x') y = Variable('y') x0_1 = Parameter('x0_1', value=0.7, min=0.6, max=0.9) sig_x_1 = Parameter('sig_x_1', value=0.1, min=0.0, max=0.2) y0_1 = Parameter('y0_1', value=0.8, min=0.6, max=0.9) sig_y_1 = Parameter('sig_y_1', value=0.1, min=0.0, max=0.2) A_1 = Parameter('A_1') g_1 = A_1 * Gaussian(x, x0_1, sig_x_1) * Gaussian(y, y0_1, sig_y_1) x0_2 = Parameter('x0_2', value=0.3, min=0.2, max=0.5) sig_x_2 = Parameter('sig_x_2', value=0.1, min=0.0, max=0.2) y0_2 = Parameter('y0_2', value=0.4, min=0.2, max=0.5) sig_y_2 = Parameter('sig_y_2', value=0.1, min=0.0, max=0.2) A_2 = Parameter('A_2') g_2 = A_2 * Gaussian(x, x0_2, sig_x_2) * Gaussian(y, y0_2, sig_y_2) model = GradientModel(g_1 + g_2) fit = Fit(model, xx, yy, ydata) fit_result = fit.execute() assert isinstance(fit.minimizer, LBFGSB) img = model(x=xx, y=yy, **fit_result.params)[0] img_g_1 = g_1(x=xx, y=yy, **fit_result.params) img_g_2 = g_2(x=xx, y=yy, **fit_result.params) assert img == pytest.approx(img_g_1 + img_g_2) # Equal up to some precision. Not much obviously. assert fit_result.value(x0_1) == pytest.approx(0.7, 1e-3) assert fit_result.value(y0_1) == pytest.approx(0.8, 1e-3) assert fit_result.value(x0_2) == pytest.approx(0.3, 1e-3) assert fit_result.value(y0_2) == pytest.approx(0.4, 1e-3) def test_gaussian_2d_fitting(): """ Tests fitting to a scalar gaussian function with 2 independent variables. """ mean = (0.6, 0.4) # x, y mean 0.6, 0.4 cov = [[0.2**2, 0], [0, 0.1**2]] # TODO: evaluate gaussian at 100x100 points and add appropriate noise data = np.random.multivariate_normal(mean, cov, 1000000) # Insert them as y,x here as np fucks up cartesian conventions. ydata, xedges, yedges = np.histogram2d(data[:, 0], data[:, 1], bins=100, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata xx, yy = np.meshgrid(xcentres, ycentres, sparse=False, indexing='ij') x0 = Parameter('x0', value=mean[0]) sig_x = Parameter('sig_x', min=0.0) x = Variable('x') y0 = Parameter('y0', value=mean[1]) sig_y = Parameter('sig_y', min=0.0) A = Parameter('A', min=1, value=100) y = Variable('y') g = Variable('g') # g = A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y) model = Model({g: A * Gaussian(x, x0, sig_x) * Gaussian(y, y0, sig_y)}) fit = Fit(model, x=xx, y=yy, g=ydata, minimizer=MINPACK) fit_result = fit.execute() assert fit_result.value(x0) == pytest.approx(np.mean(data[:, 0]), 1e-1) assert fit_result.value(y0) == pytest.approx(np.mean(data[:, 1]), 1e-1) assert np.abs(fit_result.value(sig_x)) == pytest.approx(np.std(data[:, 0]), 1e-1) assert np.abs(fit_result.value(sig_y)) == pytest.approx(np.std(data[:, 1]), 1e-1) assert fit_result.r_squared >= 0.99 def test_jacobian_matrix(): """ The jacobian matrix of a model should be a 2D list (matrix) containing all the partial derivatives. """ a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = Model({a_i: 2 * a + 3 * b, b_i: 5 * b, c_i: 7 * c}) assert [[2, 3, 0], [0, 5, 0], [0, 0, 7]] == model.jacobian def test_hessian_matrix(): """ The Hessian matrix of a model should be a 3D list (matrix) containing all the 2nd partial derivatives. """ a, b, c = parameters('a, b, c') a_i, b_i, c_i = variables('a_i, b_i, c_i') model = Model({a_i: 2 * a**2 + 3 * b, b_i: 5 * b**2, c_i: 7 * c*b}) assert [[[4, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 10, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 7], [0, 7, 0]]] == model.hessian def test_likelihood_fitting_exponential(): """ Fit using the likelihood method. """ b = Parameter('b', value=4, min=3.0) x, y = variables('x, y') pdf = {y: Exp(x, 1/b)} # Draw points from an Exp(5) exponential distribution. np.random.seed(100) # TODO: Do we *really* need 1m points? xdata = np.random.exponential(5, 1000000) # Expected parameter values mean = np.mean(xdata) stdev = np.std(xdata) mean_stdev = stdev / np.sqrt(len(xdata)) with pytest.raises(TypeError): fit = Fit(pdf, x=xdata, sigma_y=2.0, objective=LogLikelihood) fit = Fit(pdf, xdata, objective=LogLikelihood) fit_result = fit.execute() pdf_i = fit.model(x=xdata, **fit_result.params).y # probabilities likelihood = np.product(pdf_i) loglikelihood = np.sum(np.log(pdf_i)) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(mean, 1e-3) assert fit_result.value(b) == pytest.approx(stdev, 1e-3) assert fit_result.stdev(b) == pytest.approx(mean_stdev, 1e-3) assert likelihood == pytest.approx(fit_result.likelihood) assert loglikelihood == pytest.approx(fit_result.log_likelihood) def test_likelihood_fitting_gaussian(): """ Fit using the likelihood method. """ mu, sig = parameters('mu, sig') sig.min = 0.01 sig.value = 3.0 mu.value = 50. x = Variable('x') pdf = GradientModel(Gaussian(x, mu, sig)) np.random.seed(10) # TODO: Do we really need 1k points? xdata = np.random.normal(51., 3.5, 10000) # Expected parameter values mean = np.mean(xdata) stdev = np.std(xdata) mean_stdev = stdev/np.sqrt(len(xdata)) fit = Fit(pdf, xdata, objective=LogLikelihood) fit_result = fit.execute() assert fit_result.value(mu) == pytest.approx(mean, 1e-6) assert fit_result.stdev(mu) == pytest.approx(mean_stdev, 1e-3) assert fit_result.value(sig) == pytest.approx(np.std(xdata), 1e-6) def test_likelihood_fitting_bivariate_gaussian(): """ Fit using the likelihood method. """ # Make variables and parameters x = Variable('x') y = Variable('y') x0 = Parameter('x0', value=0.6, min=0.5, max=0.7) sig_x = Parameter('sig_x', value=0.1, max=1.0) y0 = Parameter('y0', value=0.7, min=0.6, max=0.9) sig_y = Parameter('sig_y', value=0.05, max=1.0) rho = Parameter('rho', value=0.001, min=-1, max=1) pdf = BivariateGaussian(x=x, mu_x=x0, sig_x=sig_x, y=y, mu_y=y0, sig_y=sig_y, rho=rho) # Draw 100000 samples from a bivariate distribution mean = [0.59, 0.8] r = 0.6 cov = np.array([[0.11 ** 2, 0.11 * 0.23 * r], [0.11 * 0.23 * r, 0.23 ** 2]]) np.random.seed(42) # TODO: Do we really need 100k points? xdata, ydata = np.random.multivariate_normal(mean, cov, 100000).T fit = Fit(pdf, x=xdata, y=ydata, objective=LogLikelihood) fit_result = fit.execute() assert fit_result.value(x0) == pytest.approx(mean[0], 1e-2) assert fit_result.value(y0) == pytest.approx(mean[1], 1e-2) assert fit_result.value(sig_x) == pytest.approx(np.sqrt(cov[0, 0]), 1e-2) assert fit_result.value(sig_y) == pytest.approx(np.sqrt(cov[1, 1]), 1e-2) assert fit_result.value(rho) == pytest.approx(r, 1e-2) marginal = integrate(pdf, (y, -oo, oo), conds='none') fit = Fit(marginal, x=xdata, objective=LogLikelihood) with pytest.raises(NameError): # Should raise a NameError, not a TypeError, see #219 fit.execute() def test_evaluate_model(): """ Makes sure that models are callable and give the expected answer. """ A = Parameter('A') x = Variable('x') new = A * x ** 2 assert new(x=2, A=2) == 8 assert not new(x=2, A=3) == 8 def test_simple_sigma(): """ Make sure we produce the same results as scipy's curve_fit, with and without sigmas, and compare the results of both to a known value. """ t_data = np.array([1.4, 2.1, 2.6, 3.0, 3.3]) y_data = np.array([10, 20, 30, 40, 50]) sigma = 0.2 n = np.array([5, 3, 8, 15, 30]) sigma_t = sigma / np.sqrt(n) # We now define our model y = Variable('x') g = Parameter('g') t_model = (2 * y / g)**0.5 fit = Fit(t_model, y_data, t_data) # , sigma=sigma_t) fit_result = fit.execute() # h_smooth = np.linspace(0,60,100) # t_smooth = t_model(y=h_smooth, **fit_result.params) # Lets with the results from curve_fit, no weights popt_noweights, pcov_noweights = curve_fit(lambda y, p: (2 * y / p)**0.5, y_data, t_data) assert fit_result.value(g) == pytest.approx(popt_noweights[0]) assert fit_result.stdev(g) == pytest.approx(np.sqrt(pcov_noweights[0, 0]), 1e-6) # Same sigma everywere fit = Fit(t_model, y_data, t_data, 0.0031, absolute_sigma=False) fit_result = fit.execute() popt_sameweights, pcov_sameweights = curve_fit( lambda y, p: (2 * y / p)**0.5, y_data, t_data, sigma=0.0031*np.ones(len(y_data)), absolute_sigma=False ) assert fit_result.value(g) == pytest.approx(popt_sameweights[0], 1e-4) assert fit_result.stdev(g) == pytest.approx(np.sqrt(pcov_sameweights[0, 0]), 1e-4) # Same weight everywere should be the same as no weight when absolute_sigma=False assert fit_result.value(g) == pytest.approx(popt_noweights[0], 1e-4) assert fit_result.stdev(g) == pytest.approx(np.sqrt(pcov_noweights[0, 0]), 1e-4) # Different sigma for every point fit = Fit(t_model, y_data, t_data, 0.1*sigma_t, absolute_sigma=False) fit_result = fit.execute() popt, pcov = curve_fit(lambda y, p: (2 * y / p)**0.5, y_data, t_data, sigma=.1*sigma_t) assert fit_result.value(g) == pytest.approx(popt[0]) assert fit_result.stdev(g) == pytest.approx(np.sqrt(pcov[0, 0]), 1e-6) # according to Mathematica assert fit_result.value(g) == pytest.approx(9.095, 1e-3) assert fit_result.stdev(g) == pytest.approx(0.102, 1e-2) def test_error_advanced(): """ Models an example from the mathematica docs and try's to replicate it using both symfit and scipy's curve_fit. http://reference.wolfram.com/language/howto/FitModelsWithMeasurementErrors.html """ data = [ [0.9, 6.1, 9.5], [3.9, 6., 9.7], [0.3, 2.8, 6.6], [1., 2.2, 5.9], [1.8, 2.4, 7.2], [9., 1.7, 7.], [7.9, 8., 10.4], [4.9, 3.9, 9.], [2.3, 2.6, 7.4], [4.7, 8.4, 10.] ] xdata, ydata, zdata = [np.array(data) for data in zip(*data)] xy = np.vstack((xdata, ydata)) errors = np.array([.4, .4, .2, .4, .1, .3, .1, .2, .2, .2]) # raise Exception(xy, z) a = Parameter('a', value=3.0) b = Parameter('b', value=0.9) c = Parameter('c', value=5) x = Variable('x') y = Variable('y') z = Variable('z') model = {z: a * log(b * x + c * y)} # Use a gradient model because Mathematica uses the Hessian # approximation instead of the exact Hessian. model = GradientModel(model) fit = Fit(model, x=xdata, y=ydata, z=zdata, absolute_sigma=False) fit_result = fit.execute() # Same as Mathematica default behavior. assert fit_result.value(a) == pytest.approx(2.9956, 1e-4) assert fit_result.value(b) == pytest.approx(0.563212, 1e-4) assert fit_result.value(c) == pytest.approx(3.59732, 1e-4) assert fit_result.stdev(a) == pytest.approx(0.278304, 1e-4) assert fit_result.stdev(b) == pytest.approx(0.224107, 1e-4) assert fit_result.stdev(c) == pytest.approx(0.980352, 1e-4) fit = Fit(model, xdata, ydata, zdata, absolute_sigma=True) fit_result = fit.execute() # Same as Mathematica in Measurement error mode, but without suplying # any errors. assert fit_result.value(a) == pytest.approx(2.9956, 1e-4) assert fit_result.value(b) == pytest.approx(0.563212, 1e-4) assert fit_result.value(c) == pytest.approx(3.59732, 1e-4) assert fit_result.stdev(a) == pytest.approx(0.643259, 1e-4) assert fit_result.stdev(b) == pytest.approx(0.517992, 1e-4) assert fit_result.stdev(c) == pytest.approx(2.26594, 1e-4) fit = Fit(model, xdata, ydata, zdata, sigma_z=errors) fit_result = fit.execute() popt, pcov, infodict, errmsg, ier = curve_fit( lambda x_vec, a, b, c: a * np.log(b * x_vec[0] + c * x_vec[1]), xy, zdata, sigma=errors, absolute_sigma=True, full_output=True ) # Same as curve_fit? assert fit_result.value(a) == pytest.approx(popt[0], 1e-4) assert fit_result.value(b) == pytest.approx(popt[1], 1e-4) assert fit_result.value(c) == pytest.approx(popt[2], 1e-4) assert fit_result.stdev(a) == pytest.approx(np.sqrt(pcov[0, 0]), 1e-4) assert fit_result.stdev(b) == pytest.approx(np.sqrt(pcov[1, 1]), 1e-4) assert fit_result.stdev(c) == pytest.approx(np.sqrt(pcov[2, 2]), 1e-4) # Same as Mathematica with MEASUREMENT ERROR assert fit_result.value(a) == pytest.approx(2.68807, 1e-4) assert fit_result.value(b) == pytest.approx(0.941344, 1e-4) assert fit_result.value(c) == pytest.approx(5.01541, 1e-4) assert fit_result.stdev(a) == pytest.approx(0.0974628, 1e-4) assert fit_result.stdev(b) == pytest.approx(0.247018, 1e-4) assert fit_result.stdev(c) == pytest.approx(0.597661, 1e-4) def test_error_analytical(): """ Test using a case where the analytical answer is known. Uses both symfit and scipy's curve_fit. Modeled after: http://nbviewer.ipython.org/urls/gist.github.com/taldcroft/5014170/raw/31e29e235407e4913dc0ec403af7ed524372b612/curve_fit.ipynb """ N = 10000 sigma = 10.0 * np.ones(N) xn = np.arange(N, dtype=np.float) # yn = np.zeros_like(xn) np.random.seed(10) yn = np.random.normal(size=len(xn), scale=sigma) a = Parameter('a') y = Variable('y') model = {y: a} fit = Fit(model, y=yn, sigma_y=sigma) fit_result = fit.execute() popt, pcov = curve_fit(lambda x, a: a * np.ones_like(x), xn, yn, sigma=sigma, absolute_sigma=True) assert fit_result.value(a) == pytest.approx(popt[0], 1e-5) assert fit_result.stdev(a) == pytest.approx(np.sqrt(np.diag(pcov))[0], 1e-2) fit_no_sigma = Fit(model, yn) fit_result_no_sigma = fit_no_sigma.execute() popt, pcov = curve_fit(lambda x, a: a * np.ones_like(x), xn, yn,) # With or without sigma, the bestfit params should be in agreement in case of equal weights assert fit_result.value(a) == pytest.approx(fit_result_no_sigma.value(a), 1e-5) # Since symfit is all about absolute errors, the sigma will not be in agreement assert not fit_result.stdev(a) == fit_result_no_sigma.stdev(a) == 5 assert fit_result_no_sigma.stdev(a) == pytest.approx(pcov[0][0]**0.5, 1e-5) assert fit_result_no_sigma.value(a) == pytest.approx(popt[0], 1e-5) # Analytical answer for mean of N(0,1): mu = 0.0 sigma_mu = sigma[0]/N**0.5 assert fit_result.stdev(a) == pytest.approx(sigma_mu, 1e-5) # TODO: redudant with test_error_analytical? @pytest.mark.skip() def test_straight_line_analytical(): """ Test symfit against a straight line, for which the parameters and their uncertainties are known analytically. Assuming equal weights. """ data = [[0, 1], [1, 0], [3, 2], [5, 4]] x, y = (np.array(i, dtype='float64') for i in zip(*data)) # x = np.arange(0, 100, 0.1) # np.random.seed(10) # y = 3.0*x + 105.0 + np.random.normal(size=x.shape) dx = x - x.mean() dy = y - y.mean() mean_squared_x = np.mean(x**2) - np.mean(x)**2 mean_xy = np.mean(x * y) - np.mean(x)*np.mean(y) a = mean_xy/mean_squared_x b = y.mean() - a * x.mean() assert a == pytest.approx(0.694915, 1e-6) # values form Mathematica assert b == pytest.approx(0.186441, 1e-6) S = np.sum((y - (a*x + b))**2) var_a_exact = S/(len(x) * (len(x) - 2) * mean_squared_x) var_b_exact = var_a_exact*np.mean(x ** 2) a_exact = a b_exact = b # We will now compare these exact results with values from symfit a, b, x_var = Parameter(name='a', value=3.0), Parameter(name='b'), Variable(name='x') model = a*x_var + b fit = Fit(model, x, y, absolute_sigma=False) fit_result = fit.execute() popt, pcov = curve_fit(lambda z, c, d: c * z + d, x, y, Dfun=lambda p, x, y, func: np.transpose([x, np.ones_like(x)])) # Dfun=lambda p, x, y, func: print(p, func, x, y)) # curve_fit assert a_exact == pytest.approx(popt[0], 1e-4) assert b_exact == pytest,approx(popt[1], 1e-4) assert var_a_exact == pytest.approx(pcov[0][0], 1e-6) assert var_b_exact == pytest.approx(pcov[1][1], 1e-6) assert a_exact == pytest.approx(fit_result.params.a, 1e-4) assert b_exact == pytest.approx(fit_result.params.b, 1e-4) assert var_a_exact**0.5 == pytest.approx(fit_result.params.a_stdev, 1e-6) assert var_b_exact**0.5 == pytest.approx(fit_result.params.b_stdev, 1e-6) def test_fixed_parameters(): """ Make sure fixed parameters don't change on fitting """ a, b, c, d = parameters('a, b, c, d') x, y = variables('x, y') c.value = 4.0 a.min, a.max = 1.0, 5.0 # Bounds are needed for DifferentialEvolution b.min, b.max = 1.0, 5.0 c.min, c.max = 1.0, 5.0 d.min, d.max = 1.0, 5.0 c.fixed = True model = Model({y: a * exp(-(x - b)**2 / (2 * c**2)) + d}) # Generate data xdata = np.linspace(0, 100) ydata = model(xdata, a=2, b=3, c=2, d=2).y for minimizer in subclasses(BaseMinimizer): if minimizer is ChainedMinimizer: continue else: fit = Fit(model, x=xdata, y=ydata, minimizer=minimizer) fit_result = fit.execute() # Should still be 4.0, not 2.0! assert 4.0 == fit_result.params['c'] def test_fixed_parameters_2(): """ Make sure parameter boundaries are respected """ x = Parameter('x', min=1) y = Variable('y') model = Model({y: x**2}) bounded_minimizers = list(subclasses(BoundedMinimizer)) for minimizer in bounded_minimizers: if minimizer is MINPACK: # Not a MINPACKable problem because it only has a param continue fit = Fit(model, minimizer=minimizer) assert isinstance(fit.objective, MinimizeModel) if minimizer is DifferentialEvolution: # Also needs a max x.max = 10 fit_result = fit.execute() x.max = None else: fit_result = fit.execute() assert fit_result.value(x) >= 1.0 assert fit_result.value(x) <= 2.0 assert fit.minimizer.bounds == [(1, None)] def test_non_boundaries(): """ Make sure parameter boundaries are not invented """ x = Parameter('x') y = Variable('y') model = Model({y: x**2}) bounded_minimizers = list(subclasses(BoundedMinimizer)) bounded_minimizers = [minimizer for minimizer in bounded_minimizers if minimizer is not DifferentialEvolution] for minimizer in bounded_minimizers: # Not a MINPACKable problem because it only has a param if minimizer is MINPACK: continue fit = Fit(model, minimizer=minimizer) fit_result = fit.execute() assert fit_result.value(x) == pytest.approx(0.0) assert fit.minimizer.bounds == [(None, None)] def test_single_param_model(): """ Added after #161, this tests if models with a single additive parameter are fitted properly. The problem with these models is that their jacobian is in principle just int 1, which is not the correct shape. No news is good news. :return: """ T = Variable('T') l = Variable('l') s = Parameter('s', value=300) a = Parameter('a', value=300) model = {l: s + a + 1 / (1 + exp(- T))} temp_data = [270, 280, 285, 290, 295, 300, 310, 320] length_data = [8.33, 8.41, 8.45, 8.5, 8.54, 9.13, 9.27, 9.4] fit = Fit(model, l=length_data, T=temp_data) fit_result = fit.execute() # Raise the stakes by increasing the dimensionality of the data TT, LL = np.meshgrid(temp_data, length_data) fit = Fit(model, l=LL, T=TT) fit_result = fit.execute() def test_model_from_dict(): """ Tries to create a model from a dictionary. """ x, y_1, y_2 = variables('x, y_1, y_2') a, b = parameters('a, b') # This way the test fails rather than errors. try: Model({ y_1: 2 * a * x, y_2: b * x**2 }) except Exception as error: pytest.fail('test_model_from_dict raised {}'.format(error)) def test_version(): """ Test if __version__ is availabe :return: """ import symfit symfit.__version__ symfit-0.5.4/tests/test_global_opt.py000066400000000000000000000121741412237106600177370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- from __future__ import division, print_function import pytest import sys import numpy as np from symfit import ( Fit, Parameter, Variable, Model, GradientModel ) from symfit.core.minimizers import BFGS, DifferentialEvolution from symfit.distributions import Gaussian if sys.version_info >= (3, 0): import inspect as inspect_sig else: import funcsigs as inspect_sig class TestGlobalOptGaussian: @classmethod def setup_class(cls): np.random.seed(0) mean = (0.4, 0.4) # x, y mean 0.6, 0.4 cov = [[0.01**2, 0], [0, 0.01**2]] # TODO: evaluate gaussian at 200x200 points (?!) and add appropriate noise data = np.random.multivariate_normal(mean, cov, 2500000) # Insert them as y,x here as np fucks up cartesian conventions. cls.ydata, xedges, yedges = np.histogram2d(data[:, 1], data[:, 0], bins=200, range=[[0.0, 1.0], [0.0, 1.0]]) xcentres = (xedges[:-1] + xedges[1:]) / 2 ycentres = (yedges[:-1] + yedges[1:]) / 2 # Make a valid grid to match ydata cls.xx, cls.yy = np.meshgrid(xcentres, ycentres, sparse=False) # xdata = np.dstack((xx, yy)).T def setup_method(self): x = Variable('x') y = Variable('y') xmin, xmax = -5, 5 self.x0_1 = Parameter('x01', value=0, min=xmin, max=xmax) self.sig_x_1 = Parameter('sigx1', value=0, min=0.0, max=1) self.y0_1 = Parameter('y01', value=0, min=xmin, max=xmax) self.sig_y_1 = Parameter('sigy1', value=0, min=0.0, max=1) self.A_1 = Parameter('A1', min=0, max=1000) g_1 = self.A_1 * Gaussian(x, self.x0_1, self.sig_x_1) * Gaussian(y, self.y0_1, self.sig_y_1) self.model = GradientModel(g_1) def test_diff_evo(self): """ Tests fitting to a scalar gaussian with 2 independent variables with wide bounds. """ fit = Fit(self.model, self.xx, self.yy, self.ydata, minimizer=BFGS) fit_result = fit.execute() assert isinstance(fit.minimizer, BFGS) # Make sure a local optimizer doesn't find the answer. assert not fit_result.value(self.x0_1) == pytest.approx(0.4, 1e-1) assert not fit_result.value(self.y0_1) == pytest.approx(0.4, 1e-1) # On to the main event fit = Fit(self.model, self.xx, self.yy, self.ydata, minimizer=DifferentialEvolution) fit_result = fit.execute(polish=True, seed=0, tol=1e-4, maxiter=100) # Global minimizers are really bad at finding local minima though, so # roughly equal is good enough. assert fit_result.value(self.x0_1) == pytest.approx(0.4, 1e-1) assert fit_result.value(self.y0_1) == pytest.approx(0.4, 1e-1) def test_chained_min(self): """Test fitting with a chained minimizer""" curvals = [p.value for p in self.model.params] fit = Fit(self.model, self.xx, self.yy, self.ydata, minimizer=[DifferentialEvolution, BFGS]) fit_result = fit.execute( DifferentialEvolution={'seed': 0, 'tol': 1e-4, 'maxiter': 10} ) assert fit_result.value(self.x0_1) == pytest.approx(0.4, 1e-4) assert fit_result.value(self.y0_1) == pytest.approx(0.4, 1e-4) assert curvals == [p.value for p in self.model.params] def test_chained_min_signature(self): """ Test the automatic generation of the signature for ChainedMinimizer """ minimizers = [ BFGS, DifferentialEvolution, BFGS, DifferentialEvolution, BFGS ] fit = Fit(self.model, self.xx, self.yy, self.ydata, minimizer=minimizers) names = [ 'BFGS', 'DifferentialEvolution', 'BFGS_2', 'DifferentialEvolution_2', 'BFGS_3' ] for name, param_name in zip(names, fit.minimizer.__signature__.parameters): assert name == param_name # Check for equal lengths because zip is slippery that way assert len(names) == len(fit.minimizer.__signature__.parameters) for param in fit.minimizer.__signature__.parameters.values(): assert param.kind == inspect_sig.Parameter.KEYWORD_ONLY # Make sure keywords end up at the right minimizer. with pytest.raises(TypeError): # This is not a valid kwarg to DiffEvo, but it is to BFGS. Check if # we really go by name of the Minimizer, not by order. fit.execute(DifferentialEvolution={'return_all': False}) def test_mexican_hat(): """ Test that global minimisation finds the global minima, and doesn't affect the value of parameters. """ x = Parameter('x') x.min, x.max = -100, 100 x.value = -2.5 y = Variable('y') model = Model({y: x**4 - 10 * x**2 - x}) # Skewed Mexican hat fit = Fit(model, minimizer=[DifferentialEvolution, BFGS]) fit_result1 = fit.execute(DifferentialEvolution={'seed': 0}) fit = Fit(model) fit_result2 = fit.execute() assert fit_result1.value(x) > 0 assert fit_result2.value(x) < 0 symfit-0.5.4/tests/test_minimize.py000066400000000000000000000230751412237106600174400ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import numpy as np from scipy.optimize import minimize, basinhopping from symfit import ( Variable, Parameter, Eq, Ge, Le, Lt, Gt, Ne, parameters, ModelError, Fit, Model, cos, CallableNumericalModel ) from symfit.core.objectives import MinimizeModel from symfit.core.minimizers import BFGS, BasinHopping, LBFGSB, SLSQP, NelderMead from symfit.core.support import partial def setup_module(): np.random.seed(0) @pytest.mark.usefixtures("minimize_fixture") class TestMinimize: """ Tests maximizing a function with and without constraints, taken from the scipy `minimize` tutorial. Compare the symfit result with the scipy result. https://docs.scipy.org/doc/scipy-0.18.1/reference/tutorial/optimize.html#constrained-minimization-of-multivariate-scalar-functions-minimize """ @pytest.fixture(autouse=True) def minimize_fixture(self): """ Set up the parameters, model, constraints for the minimization fits. These tests used to purposefully use unamed variable, however this has been changed as this feature is now being depricated in a future version of Symfit. """ x = Parameter('x', value=-1.0) y = Parameter('y', value=1.0) self.x = x self.y = y self.model = Model(2 * x * y + 2 * x - x ** 2 - 2 * y ** 2) self.constraints = [ Ge(y - 1, 0), # y - 1 >= 0, Eq(x**3 - y, 0), # x**3 - y == 0, ] self.cons = ( {'type': 'eq', 'fun': lambda x: np.array([x[0]**3 - x[1]]), 'jac': lambda x: np.array([3.0 * (x[0]**2.0), -1.0])}, {'type': 'ineq', 'fun': lambda x: np.array([x[1] - 1]), 'jac': lambda x: np.array([0.0, 1.0])} ) @staticmethod def func(x, sign=1.0): """ Objective function """ return sign*(2*x[0]*x[1] + 2*x[0] - x[0]**2 - 2*x[1]**2) @staticmethod def func_deriv(x, sign=1.0): """ Derivative of objective function """ dfdx0 = sign*(-2*x[0] + 2*x[1] + 2) dfdx1 = sign*(2*x[0] - 4*x[1]) return np.array([dfdx0, dfdx1]) def test_minimize_unconstrained(self): """ Test an unconstrained fit. """ res = minimize(self.func, [-1.0, 1.0], args=(-1.0,), jac=self.func_deriv, method='BFGS', options={'disp': False}) fit = Fit(model=-self.model) assert isinstance(fit.objective, MinimizeModel) assert isinstance(fit.minimizer, BFGS) fit_result = fit.execute() assert fit_result.value(self.x) == pytest.approx(res.x[0], 1e-6) assert fit_result.value(self.y) == pytest.approx(res.x[1], 1e-6) def test_minimize_constrained(self): """ Test a constrained fit. """ res = minimize(self.func, [-1.0, 1.0], args=(-1.0,), jac=self.func_deriv, constraints=self.cons, method='SLSQP', options={'disp': False}) fit = Fit(-self.model, constraints=self.constraints) assert fit.constraints[0].constraint_type == Ge assert fit.constraints[1].constraint_type == Eq fit_result = fit.execute() assert fit_result.value(self.x) == pytest.approx(res.x[0], 1e-6) assert fit_result.value(self.y) == pytest.approx(res.x[1], 1e-6) def test_constraint_types(): x = Parameter('x', value=-1.0) y = Parameter('y', value=1.0) z = Variable('z') model = Model({z: 2*x*y + 2*x - x**2 - 2*y**2}) # These types are not allowed constraints. for relation in [Lt, Gt, Ne]: with pytest.raises(ModelError): Fit(model, constraints=[relation(x, y)]) # Should execute without problems. for relation in [Eq, Ge, Le]: Fit(model, constraints=[relation(x, y)]) fit = Fit(model, constraints=[Le(x, y)]) # Le should be transformed to Ge assert fit.constraints[0].constraint_type is Ge # Redo the standard test as a Le constraints = [ Le(- y + 1, 0), # y - 1 >= 0, Eq(x**3 - y, 0), # x**3 - y == 0, ] std_constraints = [ Ge(y - 1, 0), # y - 1 >= 0, Eq(x**3 - y, 0), # x**3 - y == 0, ] fit = Fit(-model, constraints=constraints) std_fit = Fit(-model, constraints=std_constraints) assert fit.constraints[0].constraint_type == Ge assert fit.constraints[1].constraint_type == Eq assert fit.constraints[0].params == [x, y] assert fit.constraints[1].params == [x, y] assert fit.constraints[0].jacobian_model.params == [x, y] assert fit.constraints[1].jacobian_model.params == [x, y] assert fit.constraints[0].hessian_model.params == [x, y] assert fit.constraints[1].hessian_model.params == [x, y] assert fit.constraints[0].__signature__ == fit.constraints[1].__signature__ fit_result = fit.execute() std_result = std_fit.execute() assert fit_result.value(x) == pytest.approx(std_result.value(x)) assert fit_result.value(y) == pytest.approx(std_result.value(y)) def test_basinhopping_large(): """ Test the basinhopping method of scipy.minimize. This is based of scipy's docs as found here: https://docs.scipy.org/doc/scipy-0.13.0/reference/generated/scipy.optimize.anneal.html """ def f1(z, *params): x, y = z a, b, c, d, e, f, g, h, i, j, k, l, scale = params return (a * x ** 2 + b * x * y + c * y ** 2 + d * x + e * y + f) def f2(z, *params): x, y = z a, b, c, d, e, f, g, h, i, j, k, l, scale = params return (-g * np.exp(-((x - h) ** 2 + (y - i) ** 2) / scale)) def f3(z, *params): x, y = z a, b, c, d, e, f, g, h, i, j, k, l, scale = params return (-j * np.exp(-((x - k) ** 2 + (y - l) ** 2) / scale)) def func(z, *params): x, y = z a, b, c, d, e, f, g, h, i, j, k, l, scale = params return f1(z, *params) + f2(z, *params) + f3(z, *params) def f_symfit(x1, x2, params): z = [x1, x2] return func(z, *params) params = (2, 3, 7, 8, 9, 10, 44, -1, 2, 26, 1, -2, 0.5) x0 = np.array([2., 2.]) np.random.seed(555) res = basinhopping(func, x0, minimizer_kwargs={'args': params}) np.random.seed(555) x1, x2 = parameters('x1, x2', value=x0) fit = BasinHopping(partial(f_symfit, params=params), [x1, x2]) fit_result = fit.execute() assert res.x[0] == fit_result.value(x1) assert res.x[1] == fit_result.value(x2) assert res.fun == fit_result.objective_value def test_basinhopping(): def func(x): return np.cos(14.5 * x - 0.3) + (x + 0.2) * x x0 = [1.] np.random.seed(555) res = basinhopping(func, x0, minimizer_kwargs={"method": "BFGS"}, niter=200) np.random.seed(555) x, = parameters('x') fit = BasinHopping(func, [x], local_minimizer=BFGS) fit_result = fit.execute(niter=200) # fit_result = fit.execute(minimizer_kwargs={"method": "BFGS"}, niter=200) assert res.x == fit_result.value(x) assert res.fun == fit_result.objective_value def test_basinhopping_2d(): def func2d(x): f = np.cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0] df = np.zeros(2) df[0] = -14.5 * np.sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2 df[1] = 2. * x[1] + 0.2 return f, df def func2d_symfit(x1, x2): f = np.cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 return f def jac2d_symfit(x1, x2): df = np.zeros(2) df[0] = -14.5 * np.sin(14.5 * x1 - 0.3) + 2. * x1 + 0.2 df[1] = 2. * x2 + 0.2 return df np.random.seed(555) minimizer_kwargs = {'method': 'BFGS', 'jac': True} x0 = [1.0, 1.0] res = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs, niter=200) np.random.seed(555) x1, x2 = parameters('x1, x2', value=x0) with pytest.raises(TypeError): fit = BasinHopping( func2d_symfit, [x1, x2], local_minimizer=NelderMead(func2d_symfit, [x1, x2], jacobian=jac2d_symfit) ) fit = BasinHopping( func2d_symfit, [x1, x2], local_minimizer=BFGS(func2d_symfit, [x1, x2], jacobian=jac2d_symfit) ) fit_result = fit.execute(niter=200) assert isinstance(fit.local_minimizer.jacobian, MinimizeModel) assert isinstance(fit.local_minimizer.jacobian.model, CallableNumericalModel) assert res.x[0] == fit_result.value(x1) assert res.x[1] == fit_result.value(x2) assert res.fun == fit_result.objective_value # Now compare with the symbolic equivalent np.random.seed(555) model = cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 fit = Fit(model, minimizer=BasinHopping) fit_result = fit.execute() assert res.x[0] == fit_result.value(x1) assert res.x[1] == fit_result.value(x2) assert res.fun == fit_result.objective_value assert isinstance(fit.minimizer.local_minimizer, BFGS) # Impose constrains np.random.seed(555) model = cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 fit = Fit(model, minimizer=BasinHopping, constraints=[Eq(x1, x2)]) fit_result = fit.execute() assert fit_result.value(x1) == fit_result.value(x2) assert isinstance(fit.minimizer.local_minimizer, SLSQP) # Impose bounds np.random.seed(555) x1.min = 0.0 model = cos(14.5 * x1 - 0.3) + (x2 + 0.2) * x2 + (x1 + 0.2) * x1 fit = Fit(model, minimizer=BasinHopping) fit_result = fit.execute() assert fit_result.value(x1) >= x1.min assert isinstance(fit.minimizer.local_minimizer, LBFGSB) symfit-0.5.4/tests/test_minimizers.py000066400000000000000000000353541412237106600200100ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import warnings import numpy as np import pickle import multiprocessing as mp from symfit import ( Variable, Parameter, Eq, Ge, parameters, Fit, Model, FitResults, variables, CallableNumericalModel ) from symfit.core.minimizers import * from symfit.core.objectives import LeastSquares, MinimizeModel, VectorLeastSquares # Defined at the global level because local functions can't be pickled. def f(x, a, b): return a * x + b def chi_squared(x, y, a, b, sum=True): if sum: return np.sum((y - f(x, a, b)) ** 2) else: return (y - f(x, a, b)) ** 2 def worker(fit_obj): return fit_obj.execute() class SqrtLeastSquares(LeastSquares): """ Minimizes the square root of LeastSquares. This seems to help SLSQP in particular, and is considered good practise since the square can grow to rapidly, leading to numerical errors. """ # TODO: Make this a standard objective, and perhaps even THE standard # objective. This lightweight version is given without proper testing # because only the call is relevant, and this makes our multiprocessing test # work. def __call__(self, *args, **kwargs): chi2 = super(SqrtLeastSquares, self).__call__(*args, **kwargs) return np.sqrt(chi2) def eval_jacobian(self, *args, **kwargs): sqrt_chi2 = self(*args, **kwargs) chi2_jac = super(SqrtLeastSquares, self).eval_jacobian(*args, **kwargs) return 0.5 * (1 / sqrt_chi2) * chi2_jac def eval_hessian(self, *args, **kwargs): sqrt_chi2 = self(*args, **kwargs) sqrt_chi2_jac = self.eval_jacobian(*args, **kwargs) chi2 = super(SqrtLeastSquares, self).__call__(*args, **kwargs) chi2_jac = super(SqrtLeastSquares, self).eval_jacobian(*args, **kwargs) chi2_hess = super(SqrtLeastSquares, self).eval_hessian(*args, **kwargs) return - 0.5 * (1 / chi2) * np.outer(sqrt_chi2_jac, chi2_jac) + 0.5 * (1 / sqrt_chi2) * chi2_hess def subclasses(base, leaves_only=True): """ Recursively create a set of subclasses of ``object``. :param object: Class :param leaves_only: If ``True``, return only the leaves of the subclass tree :return: (All leaves of) the subclass tree. """ base_subs = set(base.__subclasses__()) if not base_subs or not leaves_only: all_subs = {base} else: all_subs = set() for sub in list(base_subs): sub_subs = subclasses(sub, leaves_only=leaves_only) all_subs.update(sub_subs) return all_subs def setup_function(): np.random.seed(0) def test_custom_objective(recwarn): """ Compare the result of a custom objective with the symbolic result. :return: """ # Create test data xdata = np.linspace(0, 100, 25) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 # Normal symbolic fit a = Parameter('a', value=0, min=0.0, max=1000) b = Parameter('b', value=0, min=0.0, max=1000) x = Variable('x') y = Variable('y') model = {y: a * x + b} fit = Fit(model, xdata, ydata, minimizer=BFGS) fit_result = fit.execute() def f(x, a, b): return a * x + b def chi_squared(a, b): return np.sum((ydata - f(xdata, a, b))**2) # Should no longer raise warnings, because internally we practice # what we preach. fit_custom = BFGS(chi_squared, [a, b]) assert len(recwarn) == 0 fit_custom_result = fit_custom.execute() assert isinstance(fit_custom_result, FitResults) assert fit_custom_result.value(a) == pytest.approx(fit_result.value(a), 1e-5) assert fit_custom_result.value(b) == pytest.approx(fit_result.value(b), 1e-4) # New preferred usage, multi component friendly. with pytest.raises(TypeError): callable_model = CallableNumericalModel( chi_squared, connectivity_mapping={y: {a, b}} ) callable_model = CallableNumericalModel( {y: chi_squared}, connectivity_mapping={y: {a, b}} ) assert callable_model.params == [a, b] assert callable_model.independent_vars == [] assert callable_model.dependent_vars == [y] assert callable_model.interdependent_vars == [] assert callable_model.connectivity_mapping == {y: {a, b}} fit_custom = BFGS(callable_model, [a, b]) fit_custom_result = fit_custom.execute() assert isinstance(fit_custom_result, FitResults) assert fit_custom_result.value(a) == pytest.approx(fit_result.value(a), 1e-5) assert fit_custom_result.value(b) == pytest.approx(fit_result.value(b), 1e-4) def test_custom_parameter_names(): """ For cusom objective functions you still have to provide a list of Parameter objects to use with the same name as the keyword arguments to your function. """ a = Parameter('a') c = Parameter('c') def chi_squared(a, b): """ Dummy function with different keyword argument names """ pass fit_custom = BFGS(chi_squared, [a, c]) with pytest.raises(TypeError): fit_custom.execute() def test_powell(): """ Powell with a single parameter gave an error because a 0-d array was returned by scipy. So no error here is winning. """ x, y = variables('x, y') a, b = parameters('a, b') b.fixed = True model = Model({y: a * x + b}) xdata = np.linspace(0, 10) ydata = model(x=xdata, a=5.5, b=15.0).y + np.random.normal(0, 1) fit = Fit({y: a * x + b}, x=xdata, y=ydata, minimizer=Powell) fit_result = fit.execute() assert fit_result.value(b) == pytest.approx(1.0) def test_jac_hess(): """ Make sure both the Jacobian and Hessian are passed to the minimizer. """ x, y = variables('x, y') a, b = parameters('a, b') b.fixed = True model = Model({y: a * x + b}) xdata = np.linspace(0, 10) ydata = model(x=xdata, a=5.5, b=15.0).y + np.random.normal(0, 1) fit = Fit({y: a * x + b}, x=xdata, y=ydata, minimizer=TrustConstr) assert isinstance(fit.minimizer.objective, LeastSquares) assert isinstance(fit.minimizer.jacobian.__self__, LeastSquares) assert isinstance(fit.minimizer.hessian.__self__, LeastSquares) fit_result = fit.execute() assert fit_result.value(b) == pytest.approx(1.0) def test_pickle(): """ Test the picklability of the different minimizers. """ # Create test data xdata = np.linspace(0, 100, 100) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 # Normal symbolic fit a = Parameter('a', value=0, min=0.0, max=1000) b = Parameter('b', value=0, min=0.0, max=1000) x, y = variables('x, y') # Make a set of all ScipyMinimizers, and add a chained minimizer. scipy_minimizers = list(subclasses(ScipyMinimize)) chained_minimizer = (DifferentialEvolution, BFGS) scipy_minimizers.append(chained_minimizer) constrained_minimizers = subclasses(ScipyConstrainedMinimize) # Test for all of them if they can be pickled. for minimizer in scipy_minimizers: if minimizer in constrained_minimizers: constraints = [Ge(b, a)] else: constraints = [] model = CallableNumericalModel( {y: f}, connectivity_mapping={y: {x, a, b}}, ) fit = Fit(model, x=xdata, y=ydata, minimizer=minimizer, constraints=constraints) if minimizer is not MINPACK: assert isinstance(fit.objective, LeastSquares) assert isinstance(fit.minimizer.objective, LeastSquares) else: assert isinstance(fit.objective, VectorLeastSquares) assert isinstance(fit.minimizer.objective, VectorLeastSquares) fit = fit.minimizer # Just check if the minimizer pickles dump = pickle.dumps(fit) pickled_fit = pickle.loads(dump) problematic_attr = [ 'objective', '_pickle_kwargs', 'wrapped_objective', 'constraints', 'wrapped_constraints', 'local_minimizer', 'minimizers' ] for key, value in fit.__dict__.items(): new_value = pickled_fit.__dict__[key] try: assert value == new_value except AssertionError as err: if key not in problematic_attr: raise err # These attr are new instances, and therefore do not # pass an equality test. All we can do is see if they # are at least the same type. if isinstance(value, (list, tuple)): for val1, val2 in zip(value, new_value): assert isinstance(val1, val2.__class__) if key == 'constraints': assert val1.model.constraint_type == val2.model.constraint_type assert list(val1.model.model_dict.values())[0] == list(val2.model.model_dict.values())[0] assert val1.model.independent_vars == val2.model.independent_vars assert val1.model.params == val2.model.params assert val1.model.__signature__ == val2.model.__signature__ elif key == 'wrapped_constraints': if isinstance(val1, dict): assert val1['type'] == val2['type'] assert set(val1.keys()) == set(val2.keys()) elif isinstance(val1, NonlinearConstraint): # For trust-ncg we manually check if # their dicts are equal, because no # __eq__ is implemented on # NonLinearConstraint assert len(val1.__dict__) == len(val2.__dict__) for key in val1.__dict__: try: assert val1.__dict__[key] == val2.__dict__[key] except AssertionError: assert isinstance(val1.__dict__[key], val2.__dict__[key].__class__) else: raise NotImplementedError('No such constraint type is known.') elif key == '_pickle_kwargs': FitResults._array_safe_dict_eq(value, new_value) else: assert isinstance(new_value, value.__class__) assert set(fit.__dict__.keys()) == set(pickled_fit.__dict__.keys()) # Test if we converge to the same result. np.random.seed(2) res_before = fit.execute() np.random.seed(2) res_after = pickled_fit.execute() assert FitResults._array_safe_dict_eq(res_before.__dict__, res_after.__dict__) def test_multiprocessing(): """ To make sure pickling truly works, try multiprocessing. No news is good news. """ np.random.seed(2) x = np.arange(100, dtype=float) a_values = np.array([1, 2, 3]) np.random.shuffle(a_values) def gen_fit_objs(x, a, minimizer): """Generates linear fits with different a parameter values.""" for a_i in a: a_par = Parameter('a', 4.0, min=0.0, max=20) b_par = Parameter('b', 1.2, min=0.0, max=2) x_var = Variable('x') y_var = Variable('y') con_map = {y_var: {x_var, a_par, b_par}} model = CallableNumericalModel({y_var: f}, connectivity_mapping=con_map) fit = Fit( model, x, a_i * x + 1, minimizer=minimizer, objective=SqrtLeastSquares if minimizer is not MINPACK else VectorLeastSquares ) yield fit minimizers = subclasses(ScipyMinimize) chained_minimizer = (DifferentialEvolution, BFGS) minimizers.add(chained_minimizer) pool = mp.Pool() for minimizer in minimizers: results = pool.map(worker, gen_fit_objs(x, a_values, minimizer)) a_results = [res.params['a'] for res in results] # Check the results assert a_values == pytest.approx(a_results, 1e-2) for result in results: # Check that we are actually using the right minimizer if isinstance(result.minimizer, ChainedMinimizer): for used, target in zip(result.minimizer.minimizers, minimizer): assert isinstance(used, target) else: assert isinstance(result.minimizer, minimizer) assert isinstance(result.iterations, int) or result.iterations is None def test_minimizer_constraint_compatibility(): """ Test if #156 has been solved, and test all the other constraint styles. """ x, y, z = variables('x, y, z') a, b, c = parameters('a, b, c') b.fixed = True model = Model({z: a * x**2 - b * y**2 + c}) # Generate data, z has to be scalar for MinimizeModel to be happy xdata = 3 # np.linspace(0, 10) ydata = 5 # np.linspace(0, 10) zdata = model(a=2, b=3, c=5, x=xdata, y=ydata).z data_dict = {x: xdata, y: ydata, z: zdata} # Equivalent ways of defining the same constraint constraint_model = Model.as_constraint(a - c, model, constraint_type=Eq) constraint_model.params = model.params constraints = [ Eq(a, c), MinimizeModel(constraint_model, data=data_dict), constraint_model ] objective = MinimizeModel(model, data=data_dict) for constraint in constraints: fit = SLSQP(objective, parameters=[a, b, c], constraints=[constraint]) wrapped_constr = fit.wrapped_constraints[0]['fun'].model assert isinstance(wrapped_constr, Model) assert wrapped_constr.params == model.params assert wrapped_constr.jacobian_model.params == model.params assert wrapped_constr.hessian_model.params == model.params # Set the data for the dependent var of the constraint to None # Normally this is handled by Fit because here we interact with the # Minimizer directly, it is up to us. constraint_var = fit.wrapped_constraints[0]['fun'].model.dependent_vars[0] objective.data[constraint_var] = None fit.execute() # No scipy style dicts allowed. with pytest.raises(TypeError): fit = SLSQP(MinimizeModel(model, data=data_dict), parameters=[a, b, c], constraints=[{'type': 'eq', 'fun': lambda a, b, c: a - c}]) symfit-0.5.4/tests/test_model.py000066400000000000000000000404641412237106600167200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest from collections import OrderedDict import pickle try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest import numpy as np from symfit import ( Fit, parameters, variables, Model, ODEModel, D, Eq, CallableModel, CallableNumericalModel, Inverse, MatrixSymbol, Symbol, sqrt, Function, diff ) from symfit.core.models import ( jacobian_from_model, hessian_from_model, ModelError, ModelOutput ) """ Tests for Model objects. """ def test_model_as_dict(): x, y_1, y_2 = variables('x, y_1, y_2') a, b = parameters('a, b') model_dict = OrderedDict([(y_1, a * x**2), (y_2, 2 * x * b)]) model = Model(model_dict) assert model[y_1] is model_dict[y_1] assert model[y_2] is model_dict[y_2] assert len(model) == len(model_dict) assert model.items() == model_dict.items() assert model.keys() == model_dict.keys() assert list(model.values()) == list(model_dict.values()) assert y_1 in model assert not model[y_1] in model def test_order(): """ The model has to behave like an OrderedDict. This is of the utmost importance! """ x, y_1, y_2 = variables('x, y_1, y_2') a, b = parameters('a, b') model_dict = {y_2: a * x**2, y_1: 2 * x * b} model = Model(model_dict) assert model.dependent_vars == list(model.keys()) def test_neg(): """ Test negation of all model types """ x, y_1, y_2 = variables('x, y_1, y_2') a, b = parameters('a, b') model_dict = {y_2: a * x ** 2, y_1: 2 * x * b} model = Model(model_dict) model_neg = - model for key in model: assert model[key] == - model_neg[key] # Constraints constraint = Model.as_constraint(Eq(a * x, 2), model) constraint_neg = - constraint # for key in constraint: assert constraint[constraint.dependent_vars[0]] == - constraint_neg[constraint_neg.dependent_vars[0]] # ODEModel odemodel = ODEModel({D(y_1, x): a * x}, initial={a: 1.0}) odemodel_neg = - odemodel for key in odemodel: assert odemodel[key] == - odemodel_neg[key] # For models with interdependency, negation should only change the # dependent components. model_dict = {x: y_1**2, y_1: a * y_2 + b} model = Model(model_dict) model_neg = - model for key in model: if key in model.dependent_vars: assert model[key] == - model_neg[key] elif key in model.interdependent_vars: assert model[key] == model_neg[key] else: pytest.fail() def test_CallableNumericalModel(): x, y, z = variables('x, y, z') a, b = parameters('a, b') model = CallableModel({y: a * x + b}) numerical_model = CallableNumericalModel( {y: lambda x, a, b: a * x + b}, connectivity_mapping={y: {x, a, b}} ) assert model.__signature__ == numerical_model.__signature__ xdata = np.linspace(0, 10) ydata = model(x=xdata, a=5.5, b=15.0).y + np.random.normal(0, 1) symbolic_answer = np.array(model(x=xdata, a=5.5, b=15.0)) numerical_answer = np.array(numerical_model(x=xdata, a=5.5, b=15.0)) assert numerical_answer == pytest.approx(symbolic_answer) faulty_model = CallableNumericalModel({y: lambda x, a, b: a * x + b}, connectivity_mapping={y: {a, b}}) assert not model.__signature__ == faulty_model.__signature__ with pytest.raises(TypeError): # This is an incorrect signature, even though the lambda function is # correct. Should fail. faulty_model(xdata, 5.5, 15.0) # Faulty model whose components do not all accept all of the args with pytest.warns(DeprecationWarning): faulty_model = CallableNumericalModel( {y: lambda x, a, b: a * x + b, z: lambda x, a: x**a}, [x], [a, b] ) assert model.__signature__ == faulty_model.__signature__ with pytest.raises(TypeError): # Lambda got an unexpected keyword 'b' faulty_model(xdata, 5.5, 15.0) # Faulty model with a wrongly named argument faulty_model = CallableNumericalModel( {y: lambda x, a, c=5: a * x + c}, connectivity_mapping={y: {x, a, b}} ) assert model.__signature__ == faulty_model.__signature__ with pytest.raises(TypeError): # Lambda got an unexpected keyword 'b' faulty_model(xdata, 5.5, 15.0) # Correct version of the previous model numerical_model = CallableNumericalModel( {y: lambda x, a, b: a * x + b, z: lambda x, a: x ** a}, connectivity_mapping={y: {a, b, x}, z: {x, a}} ) # Correct version of the previous model mixed_model = CallableNumericalModel( {y: lambda x, a, b: a * x + b, z: x ** a}, connectivity_mapping={y: {x, a, b}} ) numberical_answer = np.array(numerical_model(x=xdata, a=5.5, b=15.0)) mixed_answer = np.array(mixed_model(x=xdata, a=5.5, b=15.0)) assert numberical_answer == pytest.approx(mixed_answer) zdata = mixed_model(x=xdata, a=5.5, b=15.0).z + np.random.normal(0, 1) # Check if the fits are the same fit = Fit(mixed_model, x=xdata, y=ydata, z=zdata) mixed_result = fit.execute() fit = Fit(numerical_model, x=xdata, y=ydata, z=zdata) numerical_result = fit.execute() for param in [a, b]: assert mixed_result.value(param) == pytest.approx(numerical_result.value(param)) if mixed_result.stdev(param) is not None and numerical_result.stdev(param) is not None: assert mixed_result.stdev(param) == pytest.approx(numerical_result.stdev(param)) else: assert mixed_result.stdev(param) is None and numerical_result.stdev(param) is None assert mixed_result.r_squared == pytest.approx(numerical_result.r_squared) # Test if the constrained syntax is supported fit = Fit(numerical_model, x=xdata, y=ydata, z=zdata, constraints=[Eq(a, b)]) constrained_result = fit.execute() assert constrained_result.value(a) == pytest.approx(constrained_result.value(b)) def test_CallableNumericalModel_infer_connectivity(): """ When a CallableNumericalModel is initiated with symbolical and non-symbolical components, only the connectivity mapping for non-symbolical part has to be provided. """ x, y, z = variables('x, y, z') a, b = parameters('a, b') model_dict = {z: lambda y, a, b: a * y + b, y: x ** a} mixed_model = CallableNumericalModel( model_dict, connectivity_mapping={z: {y, a, b}} ) assert mixed_model.connectivity_mapping == {z: {y, a, b}, y: {x, a}} def test_CallableNumericalModel2D(): """ Apply a CallableNumericalModel to 2D data, to see if it is agnostic to data shape. """ shape = (30, 40) def function(a, b): out = np.ones(shape) * a out[15:, :] += b return out a, b = parameters('a, b') y, = variables('y') model = CallableNumericalModel({y: function}, connectivity_mapping={y: {a, b}}) data = 15 * np.ones(shape) data[15:, :] += 20 fit = Fit(model, y=data) fit_result = fit.execute() assert fit_result.value(a) == pytest.approx(15) assert fit_result.value(b) == pytest.approx(20) def flattened_function(a, b): out = np.ones(shape) * a out[15:, :] += b return out.flatten() model = CallableNumericalModel({y: flattened_function}, connectivity_mapping={y: {a, b}}) data = 15 * np.ones(shape) data[15:, :] += 20 data = data.flatten() fit = Fit(model, y=data) flat_result = fit.execute() assert fit_result.value(a) == pytest.approx(flat_result.value(a)) assert fit_result.value(b) == pytest.approx(flat_result.value(b)) assert fit_result.stdev(a) is None and flat_result.stdev(a) is None assert fit_result.stdev(b) is None and flat_result.stdev(b) is None assert fit_result.r_squared == pytest.approx(flat_result.r_squared) def test_pickle(): """ Make sure models can be pickled are preserved when pickling """ a, b = parameters('a, b') x, y = variables('x, y') exact_model = Model({y: a * x ** b}) constraint = Model.as_constraint(Eq(a, b), exact_model) with pytest.warns(DeprecationWarning): num_model = CallableNumericalModel( {y: a * x ** b}, independent_vars=[x], params=[a, b] ) connected_num_model = CallableNumericalModel( {y: a * x ** b}, connectivity_mapping={y: {x, a, b}} ) # Test if lsoda args and kwargs are pickled too ode_model = ODEModel({D(y, x): a * x + b}, {x: 0.0}, 3, 4, some_kwarg=True) models = [exact_model, constraint, num_model, ode_model, connected_num_model] for model in models: new_model = pickle.loads(pickle.dumps(model)) # Compare signatures assert model.__signature__ == new_model.__signature__ # Trigger the cached vars because we compare `__dict__` s model.vars new_model.vars # Explicitly make sure the connectivity mapping is identical. assert model.connectivity_mapping == new_model.connectivity_mapping if not isinstance(model, ODEModel): model.function_dict model.vars_as_functions new_model.function_dict new_model.vars_as_functions assert model.__dict__ == new_model.__dict__ def test_MatrixSymbolModel(): """ Test a model which is defined by ModelSymbols, see #194 """ N = Symbol('N', integer=True) M = MatrixSymbol('M', N, N) W = MatrixSymbol('W', N, N) I = MatrixSymbol('I', N, N) y = MatrixSymbol('y', N, 1) c = MatrixSymbol('c', N, 1) a, b = parameters('a, b') z, x = variables('z, x') model_dict = { W: Inverse(I + M / a ** 2), c: - W * y, z: sqrt(c.T * c) } # TODO: This should be a Model in the future, but sympy is not yet # capable of computing Matrix derivatives at the time of writing. model = CallableModel(model_dict) assert model.params == [a] assert model.independent_vars == [I, M, y] assert model.dependent_vars == [z] assert model.interdependent_vars == [W, c] assert model.connectivity_mapping == {W: {I, M, a}, c: {W, y}, z: {c}} # Generate data iden = np.eye(2) M_mat = np.array([[2, 1], [3, 4]]) y_vec = np.array([3, 5]) eval_model = model(I=iden, M=M_mat, y=y_vec, a=0.1) W_manual = np.linalg.inv(iden + M_mat / 0.1 ** 2) c_manual = - W_manual.dot(y_vec) z_manual = np.atleast_1d(np.sqrt(c_manual.T.dot(c_manual))) assert eval_model.W == pytest.approx(W_manual) assert eval_model.c == pytest.approx(c_manual) assert eval_model.z == pytest.approx(z_manual) # Now try to retrieve the value of `a` from a fit a.value = 0.2 fit = Fit(model, z=z_manual, I=iden, M=M_mat, y=y_vec) fit_result = fit.execute() eval_model = model(I=iden, M=M_mat, y=y_vec, **fit_result.params) assert 0.1 == pytest.approx(np.abs(fit_result.value(a))) assert eval_model.W == pytest.approx(W_manual) assert eval_model.c == pytest.approx(c_manual) assert eval_model.z == pytest.approx(z_manual) # TODO: add constraints to Matrix model. But since Matrix expressions # can not yet be derived, this needs #154 to be solved first. def test_interdependency_invalid(): """ Create an invalid model with interdependency. """ a, b, c = parameters('a, b, c') x, y, z = variables('x, y, z') with pytest.raises(ModelError): # Invalid, parameters can not be keys model_dict = { c: a ** 3 * x + b ** 2, z: c ** 2 + a * b } model = Model(model_dict) with pytest.raises(ModelError): # Invalid, parameters can not be keys model_dict = {c: a ** 3 * x + b ** 2} model = Model(model_dict) def test_interdependency(): a, b = parameters('a, b') x, y, z = variables('x, y, z') model_dict = { y: a**3 * x + b**2, z: y**2 + a * b } callable_model = CallableModel(model_dict) assert callable_model.independent_vars == [x] assert callable_model.interdependent_vars == [y] assert callable_model.dependent_vars == [z] assert callable_model.params == [a, b] assert callable_model.connectivity_mapping == {y: {a, b, x}, z: {a, b, y}} assert callable_model(x=3, a=1, b=2) == pytest.approx(np.atleast_2d([7, 51]).T) for var, func in callable_model.vars_as_functions.items(): # TODO comment on what this does str_con_map = set(x.name for x in callable_model.connectivity_mapping[var]) str_args = set(str(x.__class__) if isinstance(x, Function) else x.name for x in func.args) assert str_con_map == str_args jac_model = jacobian_from_model(callable_model) assert jac_model.params == [a, b] assert jac_model.dependent_vars == [D(z, a), D(z, b), z] assert jac_model.interdependent_vars == [D(y, a), D(y, b), y] assert jac_model.independent_vars == [x] for p1, p2 in zip_longest(jac_model.__signature__.parameters, [x, a, b]): assert str(p1) == str(p2) # The connectivity of jac_model should be that from it's own components # plus that of the model. The latter is needed to properly compute the # Hessian. jac_con_map = {D(y, a): {a, x}, D(y, b): {b}, D(z, a): {b, y, D(y, a)}, D(z, b): {a, y, D(y, b)}, y: {a, b, x}, z: {a, b, y}} assert jac_model.connectivity_mapping == jac_con_map jac_model_dict = {D(y, a): 3 * a**2 * x, D(y, b): 2 * b, D(z, a): b + 2 * y * D(y, a), D(z, b): a + 2 * y * D(y, b), y: callable_model[y], z: callable_model[z]} assert jac_model.model_dict == jac_model_dict for var, func in jac_model.vars_as_functions.items(): str_con_map = set(x.name for x in jac_model.connectivity_mapping[var]) str_args = set(str(x.__class__) if isinstance(x, Function) else x.name for x in func.args) assert str_con_map == str_args hess_model = hessian_from_model(callable_model) # Result according to Mathematica hess_as_dict = { D(y, (a, 2)): 6 * a * x, D(y, a, b): 0, D(y, b, a): 0, D(y, (b, 2)): 2, D(z, (a, 2)): 2 * D(y, a)**2 + 2 * y * D(y, (a, 2)), D(z, a, b): 1 + 2 * D(y, b) * D(y, a) + 2 * y * D(y, a, b), D(z, b, a): 1 + 2 * D(y, b) * D(y, a) + 2 * y * D(y, a, b), D(z, (b, 2)): 2 * D(y, b)**2 + 2 * y * D(y, (b, 2)), D(y, a): 3 * a ** 2 * x, D(y, b): 2 * b, D(z, a): b + 2 * y * D(y, a), D(z, b): a + 2 * y * D(y, b), y: callable_model[y], z: callable_model[z] } assert dict(hess_model) == hess_as_dict assert hess_model.params == [a, b] assert hess_model.dependent_vars == [D(z, (a, 2)), D(z, a, b), D(z, (b, 2)), D(z, b, a), D(z, a), D(z, b), z] assert hess_model.interdependent_vars == [D(y, (a, 2)), D(y, a), D(y, b), y] assert hess_model.independent_vars == [x] model = Model(model_dict) assert model(x=3, a=1, b=2) == pytest.approx(np.atleast_2d([7, 51]).T) assert model.eval_jacobian(x=3, a=1, b=2) == pytest.approx(np.array([[[9], [4]], [[128], [57]]])) assert model.eval_hessian(x=3, a=1, b=2) == pytest.approx(np.array([[[[18], [0]], [[0], [2]]],[[[414], [73]], [[73], [60]]]])) assert model.__signature__ == model.jacobian_model.__signature__ assert model.__signature__ == model.hessian_model.__signature__ def test_ModelOutput(): """ Test the ModelOutput object. To prevent #267 from recurring, we attempt to make a model with more than 255 variables. """ params = parameters(','.join('a{}'.format(i) for i in range(300))) data = np.ones(300) output = ModelOutput(params, data) assert len(output) == 300 assert isinstance(output._asdict(), OrderedDict) assert output._asdict() is not output.output_dict assert output._asdict() == output.output_dict def test_model_output_pickle(): """ Test that ModelOutput objects can be pickled and unpickled. """ params = parameters(','.join('a{}'.format(i) for i in range(10))) data = np.ones(10) output = ModelOutput(params, data) jar = pickle.dumps(output) result = pickle.loads(jar) assert output._asdict() == result._asdict() symfit-0.5.4/tests/test_objectives.py000066400000000000000000000251311412237106600177470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import pickle import numpy as np from symfit import ( Variable, Parameter, parameters, Fit, Model, FitResults, variables, Idx, symbols, Sum, log, exp, cos, pi, besseli ) from symfit.core.objectives import ( VectorLeastSquares, LeastSquares, LogLikelihood, MinimizeModel, BaseIndependentObjective ) from symfit.distributions import Exp # Overwrite the way Sum is printed by numpy just while testing. Is not # general enough to be moved to symfit.core.printing, but has to be used # in this test. This way of summing completely ignores the summation indices and # the dimensions, and instead just flattens everything to a scalar. Only used # in this test to build the analytical equivalents of our LeastSquares # and LogLikelihood class FlattenSum(Sum): """ Just a sum which is printed differently: by flattening the whole array and summing it. Used in tests only. """ def _numpycode(self, printer): return "%s(%s)" % (printer._module_format('numpy.sum'), printer.doprint(self.function)) def setup_module(): np.random.seed(0) def test_pickle(): """ Test the picklability of the built-in objectives. """ # Create test data xdata = np.linspace(0, 100, 100) # From 0 to 100 in 100 steps a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) ydata = a_vec * xdata + b_vec # Point scattered around the line 15 * x + 100 # Normal symbolic fit a = Parameter('a', value=0, min=0.0, max=1000) b = Parameter('b', value=0, min=0.0, max=1000) x, y = variables('x, y') model = Model({y: a * x + b}) for objective in [VectorLeastSquares, LeastSquares, LogLikelihood, MinimizeModel]: if issubclass(objective, BaseIndependentObjective): data = {x: xdata} else: data = {x: xdata, y: ydata, model.sigmas[y]: np.ones_like(ydata)} obj = objective(model, data=data) new_obj = pickle.loads(pickle.dumps(obj)) assert FitResults._array_safe_dict_eq(obj.__dict__, new_obj.__dict__) def test_LeastSquares(): """ Tests if the LeastSquares objective gives the right shapes of output by comparing with its analytical equivalent. """ i = Idx('i', 100) x, y = symbols('x, y', cls=Variable) X2 = symbols('X2', cls=Variable) a, b = parameters('a, b') model = Model({y: a * x**2 + b * x}) xdata = np.linspace(0, 10, 100) ydata = model(x=xdata, a=5, b=2).y + np.random.normal(0, 5, xdata.shape) # Construct a LeastSquares objective and its analytical equivalent chi2_numerical = LeastSquares(model, data={ x: xdata, y: ydata, model.sigmas[y]: np.ones_like(xdata) }) chi2_exact = Model({X2: FlattenSum(0.5 * ((a * x ** 2 + b * x) - y) ** 2, i)}) eval_exact = chi2_exact(x=xdata, y=ydata, a=2, b=3) jac_exact = chi2_exact.eval_jacobian(x=xdata, y=ydata, a=2, b=3) hess_exact = chi2_exact.eval_hessian(x=xdata, y=ydata, a=2, b=3) eval_numerical = chi2_numerical(x=xdata, a=2, b=3) jac_numerical = chi2_numerical.eval_jacobian(x=xdata, a=2, b=3) hess_numerical = chi2_numerical.eval_hessian(x=xdata, a=2, b=3) # Test model jacobian and hessian shape assert model(x=xdata, a=2, b=3)[0].shape == ydata.shape assert model.eval_jacobian(x=xdata, a=2, b=3)[0].shape == (2, 100) assert model.eval_hessian(x=xdata, a=2, b=3)[0].shape == (2, 2, 100) # Test exact chi2 shape assert eval_exact[0].shape, (1,) assert jac_exact[0].shape, (2, 1) assert hess_exact[0].shape, (2, 2, 1) # Test if these two models have the same call, jacobian, and hessian assert eval_exact[0] == pytest.approx(eval_numerical) assert isinstance(eval_numerical, float) assert isinstance(eval_exact[0][0], float) assert np.squeeze(jac_exact[0], axis=-1) == pytest.approx(jac_numerical) assert isinstance(jac_numerical, np.ndarray) assert np.squeeze(hess_exact[0], axis=-1) == pytest.approx(hess_numerical) assert isinstance(hess_numerical, np.ndarray) fit = Fit(chi2_exact, x=xdata, y=ydata, objective=MinimizeModel) fit_exact_result = fit.execute() fit = Fit(model, x=xdata, y=ydata, absolute_sigma=True) fit_num_result = fit.execute() assert fit_exact_result.value(a) == fit_num_result.value(a) assert fit_exact_result.value(b) == fit_num_result.value(b) assert fit_exact_result.stdev(a) == pytest.approx(fit_num_result.stdev(a)) assert fit_exact_result.stdev(b) == pytest.approx(fit_num_result.stdev(b)) def test_LogLikelihood(): """ Tests if the LeastSquares objective gives the right shapes of output by comparing with its analytical equivalent. """ # TODO: update these tests to use indexed variables in the future a, b = parameters('a, b') i = Idx('i', 100) x, y = variables('x, y') pdf = Exp(x, 1 / a) * Exp(x, b) np.random.seed(10) xdata = np.random.exponential(3.5, 100) # We use minus loglikelihood for the model, because the objective was # designed to find the maximum when used with a *minimizer*, so it has # opposite sign. Also test MinimizeModel at the same time. logL_model = Model({y: pdf}) logL_exact = Model({y: - FlattenSum(log(pdf), i)}) logL_numerical = LogLikelihood(logL_model, {x: xdata, y: None}) logL_minmodel = MinimizeModel(logL_exact, data={x: xdata, y: None}) # Test model jacobian and hessian shape eval_exact = logL_exact(x=xdata, a=2, b=3) jac_exact = logL_exact.eval_jacobian(x=xdata, a=2, b=3) hess_exact = logL_exact.eval_hessian(x=xdata, a=2, b=3) eval_minimizemodel = logL_minmodel(a=2, b=3) jac_minimizemodel = logL_minmodel.eval_jacobian(a=2, b=3) hess_minimizemodel = logL_minmodel.eval_hessian(a=2, b=3) eval_numerical = logL_numerical(a=2, b=3) jac_numerical = logL_numerical.eval_jacobian(a=2, b=3) hess_numerical = logL_numerical.eval_hessian(a=2, b=3) # TODO: These shapes should not have the ones! This is due to the current # convention that scalars should be returned as a 1d array by Model's. assert eval_exact[0].shape == (1,) assert jac_exact[0].shape == (2, 1) assert hess_exact[0].shape == (2, 2, 1) # Test if identical to MinimizeModel assert eval_exact[0] == pytest.approx(eval_minimizemodel) assert jac_exact[0] == pytest.approx(jac_minimizemodel) assert hess_exact[0] == pytest.approx(hess_minimizemodel) # Test if these two models have the same call, jacobian, and hessian. # Since models always have components as their first dimension, we have # to slice that away. assert eval_exact.y == pytest.approx(eval_numerical) assert isinstance(eval_numerical, float) assert isinstance(eval_exact.y[0], float) assert np.squeeze(jac_exact[0], axis=-1) == pytest.approx(jac_numerical) assert isinstance(jac_numerical, np.ndarray) assert np.squeeze(hess_exact[0], axis=-1) == pytest.approx(hess_numerical) assert isinstance(hess_numerical, np.ndarray) fit = Fit(logL_exact, x=xdata, objective=MinimizeModel) fit_exact_result = fit.execute() fit = Fit(logL_model, x=xdata, objective=LogLikelihood) fit_num_result = fit.execute() assert fit_exact_result.value(a) == pytest.approx(fit_num_result.value(a)) assert fit_exact_result.value(b) == pytest.approx(fit_num_result.value(b)) assert fit_exact_result.stdev(a) == pytest.approx(fit_num_result.stdev(a)) assert fit_exact_result.stdev(b) == pytest.approx(fit_num_result.stdev(b)) def test_data_sanity(): """ Tests very basicly the data sanity for different objective types. :return: """ # Create test data xdata = np.linspace(0, 100, 25) # From 0 to 100 in 25 steps a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 # Normal symbolic fit a = Parameter('a', value=0, min=0.0, max=1000) b = Parameter('b', value=0, min=0.0, max=1000) x, y, z = variables('x, y, z') model = Model({y: a * x + b}) for objective in [VectorLeastSquares, LeastSquares, LogLikelihood, MinimizeModel]: if issubclass(objective, BaseIndependentObjective): incomplete_data = {} data = {x: xdata} overcomplete_data = {x: xdata, z: ydata} else: incomplete_data = {x: xdata, y: ydata} data = {x: xdata, y: ydata, model.sigmas[y]: np.ones_like(ydata)} overcomplete_data = {x: xdata, y: ydata, z: ydata, model.sigmas[y]: np.ones_like(ydata)} with pytest.raises(KeyError): obj = objective(model, data=incomplete_data) obj = objective(model, data=data) # Overcomplete data has to be allowed, since constraints share their # data with models. obj = objective(model, data=overcomplete_data) def test_LogLikelihood_global(): """ This is a test for global likelihood fitting to multiple data sets. Based on SO question 56006357. """ # creating the data mu1, mu2 = .05, -.05 sigma1, sigma2 = 3.5, 2.5 n1, n2 = 80, 90 np.random.seed(42) x1 = np.random.vonmises(mu1, sigma1, n1) x2 = np.random.vonmises(mu2, sigma2, n2) n = 2 # number of components xs = variables('x,' + ','.join('x_{}'.format(i) for i in range(1, n + 1))) x, xs = xs[0], xs[1:] ys = variables(','.join('y_{}'.format(i) for i in range(1, n + 1))) mu, kappa = parameters('mu, kappa') kappas = parameters(','.join('k_{}'.format(i) for i in range(1, n + 1)), min=0, max=10) mu.min, mu.max = - np.pi, np.pi template = exp(kappa * cos(x - mu)) / (2 * pi * besseli(0, kappa)) model = Model( {y_i: template.subs({kappa: k_i, x: x_i}) for y_i, x_i, k_i in zip(ys, xs, kappas)} ) all_data = {xs[0]: x1, xs[1]: x2, ys[0]: None, ys[1]: None} all_params = {'mu': 1} all_params.update({k_i.name: 1 for k_i in kappas}) # Evaluate the loglikelihood and its jacobian and hessian logL = LogLikelihood(model, data=all_data) eval_numerical = logL(**all_params) jac_numerical = logL.eval_jacobian(**all_params) hess_numerical = logL.eval_hessian(**all_params) # Test the types and shapes of the components. assert isinstance(eval_numerical, float) assert isinstance(jac_numerical, np.ndarray) assert isinstance(hess_numerical, np.ndarray) assert eval_numerical.shape == tuple() # Empty tuple -> scalar assert jac_numerical.shape == (3,) assert hess_numerical.shape == (3, 3,) symfit-0.5.4/tests/test_ode.py000066400000000000000000000173401412237106600163640ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT from __future__ import division, print_function import pytest import numpy as np from symfit import parameters, variables, ODEModel, exp, Fit, D, Model, GradientModel, Parameter from symfit.core.minimizers import MINPACK """ Tests for the FitResults object. """ def setup_module(): np.random.seed(6) def test_known_solution(): p, c1 = parameters('p, c1') y, t = variables('y, t') p.value = 3.0 model_dict = { D(y, t): - p * y, } # Lets say we know the exact solution to this problem sol = Model({y: exp(- p * t)}) # Generate some data tdata = np.linspace(0, 3, 10001) ydata = sol(t=tdata, p=3.22)[0] ydata += np.random.normal(0, 0.005, ydata.shape) ode_model = ODEModel(model_dict, initial={t: 0.0, y: ydata[0]}) fit = Fit(ode_model, t=tdata, y=ydata) ode_result = fit.execute() c1.value = ydata[0] fit = Fit(sol, t=tdata, y=ydata) fit_result = fit.execute() assert ode_result.value(p) == pytest.approx(fit_result.value(p), 1e-2) assert ode_result.r_squared == pytest.approx(fit_result.r_squared, 1e-4) assert ode_result.stdev(p) == pytest.approx(fit_result.stdev(p), 1e-2) def test_van_der_pol(): """ http://hplgit.github.io/odespy/doc/pub/tutorial/html/main_odespy.html """ u_0, u_1, t = variables('u_0, u_1, t') model_dict = { D(u_0, t): u_1, D(u_1, t): 3 * (1 - u_0**2) * u_1 - u_1 } ode_model = ODEModel(model_dict, initial={t: 0.0, u_0: 2.0, u_1: 1.0}) # # Generate some data # tdata = np.linspace(0, 1, 101) # plt.plot(tdata, ode_model(tdata)[0], color='red') # plt.plot(tdata, ode_model(tdata)[1], color='blue') # plt.show() def test_polgar(): """ Analysis of data published here: This whole ODE support was build to do this analysis in the first place """ a, b, c, d, t = variables('a, b, c, d, t') k, p, l, m = parameters('k, p, l, m') a0 = 10 b = a0 - d + a model_dict = { D(d, t): l * c * b - m * d, D(c, t): k * a * b - p * c - l * c * b + m * d, D(a, t): - k * a * b + p * c, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: a0, c: 0.0, d: 0.0}) # Generate some data tdata = np.linspace(0, 3, 1000) # Eval AA, AAB, BAAB = ode_model(t=tdata, k=0.1, l=0.2, m=.3, p=0.3) # plt.plot(tdata, AA, color='red', label='[AA]') # plt.plot(tdata, AAB, color='blue', label='[AAB]') # plt.plot(tdata, BAAB, color='green', label='[BAAB]') # plt.plot(tdata, b(d=BAAB, a=AA), color='pink', label='[B]') # plt.plot(tdata, AA + AAB + BAAB, color='black', label='total') # plt.legend() # plt.show() def test_simple_kinetics(): """ Simple kinetics data to test fitting """ tdata = np.array([10, 26, 44, 70, 120]) adata = 10e-4 * np.array([44, 34, 27, 20, 14]) a, b, t = variables('a, b, t') k, a0 = parameters('k, a0') k.value = 0.01 # a0.value, a0.min, a0.max = 54 * 10e-4, 40e-4, 60e-4 a0 = 54 * 10e-4 model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: a0, b: 0.0}) # Analytical solution model = GradientModel({a: 1 / (k * t + 1 / a0)}) fit = Fit(model, t=tdata, a=adata) fit_result = fit.execute() fit = Fit(ode_model, t=tdata, a=adata, b=None, minimizer=MINPACK) ode_result = fit.execute() assert ode_result.value(k) == pytest.approx(fit_result.value(k), 1e-4) assert ode_result.stdev(k) == pytest.approx(fit_result.stdev(k), 1e-4) assert ode_result.r_squared == pytest.approx(fit_result.r_squared, 1e-4) fit = Fit(ode_model, t=tdata, a=adata, b=None) ode_result = fit.execute() assert ode_result.value(k) == pytest.approx(fit_result.value(k), 1e-4) assert ode_result.stdev(k) == pytest.approx(fit_result.stdev(k), 1e-4) assert ode_result.r_squared == pytest.approx(fit_result.r_squared, 1e-4) def test_single_eval(): """ Eval an ODEModel at a single value rather than a vector. """ x, y, t = variables('x, y, t') k, = parameters('k') # C is the integration constant. # The harmonic oscillator as a system, >1st order is not supported yet. harmonic_dict = { D(x, t): - k * y, D(y, t): k * x, } # Make a second model to prevent caching of integration results. # This also means harmonic_dict should NOT be a Model object. harmonic_model_array = ODEModel( harmonic_dict, initial={t: 0.0, x: 1.0, y: 0.0}) harmonic_model_points = ODEModel( harmonic_dict, initial={t: 0.0, x: 1.0, y: 0.0}) tdata = np.linspace(-100, 100, 101) X, Y = harmonic_model_array(t=tdata, k=0.1) # Shuffle the data to prevent using the result at time t to calculate # t+dt random_order = np.random.permutation(len(tdata)) for idx in random_order: t = tdata[idx] X_val = X[idx] Y_val = Y[idx] X_point, Y_point = harmonic_model_points(t=t, k=0.1) assert X_point[0] == pytest.approx(X_val) assert Y_point[0] == pytest.approx(Y_val) def test_full_eval_range(): """ Test if ODEModels can be evaluated at t < t_initial. A bit of a no news is good news test. """ tdata = np.array([0, 10, 26, 44, 70, 120]) adata = 10e-4 * np.array([54, 44, 34, 27, 20, 14]) a, b, t = variables('a, b, t') k, a0 = parameters('k, a0') k.value = 0.01 t0 = tdata[2] a0 = adata[2] b0 = 0.02729855 # Obtained from evaluating from t=0. model_dict = { D(a, t): - k * a**2, D(b, t): k * a**2, } ode_model = ODEModel(model_dict, initial={t: t0, a: a0, b: b0}) fit = Fit(ode_model, t=tdata, a=adata, b=None) ode_result = fit.execute() assert ode_result.r_squared > 0.95 # Now start from a timepoint that is not in the t-array such that it # triggers another pathway to be taken in integrating it. # Again, no news is good news. ode_model = ODEModel(model_dict, initial={t: t0 + 1e-5, a: a0, b: b0}) fit = Fit(ode_model, t=tdata, a=adata, b=None) ode_result = fit.execute() assert ode_result.r_squared > 0.95 def test_odemodel_sanity(): """ If a user provides an ODE like model directly to fit without explicitly turning it into one, give a warning. """ tdata = np.array([0, 10, 26, 44, 70, 120]) adata = 10e-4 * np.array([54, 44, 34, 27, 20, 14]) a, t = variables('a, t') k, a0 = parameters('k, a0') model_dict = { D(a, t): - k * a * t, } with pytest.raises(RuntimeWarning): fit = Fit(model_dict, t=tdata, a=adata) def test_initial_parameters(): """ Identical to test_polgar, but with a0 as free Parameter. """ a, b, c, d, t = variables('a, b, c, d, t') k, p, l, m = parameters('k, p, l, m') a0 = Parameter('a0', min=0, value=10, fixed=True) c0 = Parameter('c0', min=0, value=0.05) b = a0 - d + a model_dict = { D(d, t): l * c * b - m * d, D(c, t): k * a * b - p * c - l * c * b + m * d, D(a, t): - k * a * b + p * c, } ode_model = ODEModel(model_dict, initial={t: 0.0, a: a0, c: c0, d: 0.0}) # Generate some data tdata = np.linspace(0, 3, 1000) # Eval AA, AAB, BAAB = ode_model(t=tdata, k=0.1, l=0.2, m=.3, p=0.3, a0=10, c0=0) fit = Fit(ode_model, t=tdata, a=AA, c=AAB, d=BAAB) results = fit.execute() print(results) acceptable_abs_tol = 2.5e-5 assert results.value(a0) == pytest.approx(10, abs=acceptable_abs_tol) assert results.value(c0) == pytest.approx(0, abs=acceptable_abs_tol) assert ode_model.params == [a0, c0, k, l, m, p] assert ode_model.initial_params == [a0, c0] assert ode_model.model_params == [a0, k, l, m, p] symfit-0.5.4/tests/test_support.py000066400000000000000000000155531412237106600173350ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2014-2020 Martin Roelfs # # SPDX-License-Identifier: MIT """ This module contains tests for functions in the :mod:`symfit.core.support` module. """ from __future__ import division, print_function import pytest import sys from itertools import repeat from symfit.core.support import ( keywordonly, RequiredKeyword, RequiredKeywordError, partial, parameters, cached_property ) if sys.version_info >= (3, 0): import inspect as inspect_sig else: import funcsigs as inspect_sig @keywordonly(c=2, d=RequiredKeyword) def f(a, b, *args, **kwargs): c = kwargs.pop('c') d = kwargs.pop('d') return a + b + c + d class A(object): @keywordonly(c=2, d=RequiredKeyword) def __init__(self, a, b, **kwargs): pass class B(A): @keywordonly(e=5) def __init__(self, *args, **kwargs): e = kwargs.pop('e') super(B, self).__init__(*args, **kwargs) def test_keywordonly_signature(): """ Test the keywordonly decorators ability to update the signature of the function it wraps. """ kinds = { 'a': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'b': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'args': inspect_sig.Parameter.VAR_POSITIONAL, 'kwargs': inspect_sig.Parameter.VAR_KEYWORD, 'c': inspect_sig.Parameter.KEYWORD_ONLY, 'd': inspect_sig.Parameter.KEYWORD_ONLY, } sig_f = inspect_sig.signature(f) for param in sig_f.parameters.values(): assert param.kind == kinds[param.name] def test_keywordonly_call(): """ Call our test function with some values to see if it behaves as expected. """ assert f(4, 3, c=5, d=6) == 4 + 3 + 5 + 6 # In the next case the 5 is left behind since it ends up in *args. assert f(4, 3, 5, d=6) == 4 + 3 + 2 + 6 def test_keywordonly_norequiredkeyword(): """ Try to not provide a RequiredKeyword with a value and get away with it. (we shouldn't get away with it if all is well.) """ with pytest.raises(RequiredKeywordError): f(4, 3, 5, 6) def test_keywordonly_nokwagrs(): """ Decorating a function with no **kwargs-like argument should not be allowed. """ with pytest.raises(RequiredKeywordError): @keywordonly(c=2, d=RequiredKeyword) def g(a, b, *args): pass def test_keywordonly_class(): """ Decorating a function with no **kwargs-like argument should not be allowed. """ kinds = { 'self': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'a': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'b': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'args': inspect_sig.Parameter.VAR_POSITIONAL, 'kwargs': inspect_sig.Parameter.VAR_KEYWORD, 'c': inspect_sig.Parameter.KEYWORD_ONLY, 'd': inspect_sig.Parameter.KEYWORD_ONLY, } sig = inspect_sig.signature(A.__init__) for param in sig.parameters.values(): assert param.kind == kinds[param.name] def test_keywordonly_inheritance(): """ Tests if the decorator deals with inheritance properly. """ kinds_B = { 'self': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'args': inspect_sig.Parameter.VAR_POSITIONAL, 'kwargs': inspect_sig.Parameter.VAR_KEYWORD, 'e': inspect_sig.Parameter.KEYWORD_ONLY, } kinds_A = { 'self': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'a': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'b': inspect_sig.Parameter.POSITIONAL_OR_KEYWORD, 'kwargs': inspect_sig.Parameter.VAR_KEYWORD, 'c': inspect_sig.Parameter.KEYWORD_ONLY, 'd': inspect_sig.Parameter.KEYWORD_ONLY, } sig_B = inspect_sig.signature(B.__init__) for param in sig_B.parameters.values(): assert param.kind == kinds_B[param.name] assert len(sig_B.parameters) == len(kinds_B) sig_A = inspect_sig.signature(A.__init__) for param in sig_A.parameters.values(): assert param.kind == kinds_A[param.name] assert len(sig_A.parameters) == len(kinds_A) with pytest.raises(TypeError): b = B(3, 5, 7, d=2, e=6) def test_repeatable_partial(): """ Test the custom repeatable partial, which makes partial behave the same in older python versions as in the most recent. """ def partial_me(a, b, c=None): return a, b, c partialed_one = partial(partial_me, a=2) partialed_two = partial(partialed_one, b='string') assert isinstance(partialed_one, partial) assert partialed_one.func == partial_me assert not partialed_one.args assert partialed_one.keywords == {'a': 2} # For the second partial, all should remain the same except the keywords # are extended by one item. assert isinstance(partialed_two, partial) assert partialed_two.func == partial_me assert not partialed_two.args assert partialed_two.keywords == {'a': 2, 'b': 'string'} def test_parameters(): """ Test the `parameter` convenience function. """ x1, x2 = parameters('x1, x2', value=[2.0, 1.3], min=0.0) assert x1.value == 2.0 assert x2.value == 1.3 assert x1.min == 0.0 assert x2.min == 0.0 assert not x1.fixed assert not x2.fixed with pytest.raises(ValueError): x1, x2 = parameters('x1, x2', value=[2.0, 1.3, 3.0], min=0.0) x1, x2 = parameters('x1, x2', value=[2.0, 1.3], min=[-30, -10], max=[300, 100], fixed=[True, False]) assert x1.min == -30 assert x2.min == -10 assert x1.max == 300 assert x2.max == 100 assert x1.value == 2.0 assert x2.value == 1.3 assert x1.fixed assert not x2.fixed # Illegal bounds with pytest.raises(ValueError): x1, x2 = parameters('x1, x2', value=[2.0, 1.3], min=[400, -10], max=[300, 100]) # Should not raise any error, as repeat is an endless source of values x1, x2 = parameters('x1, x2', value=[2.0, 1.3], min=repeat(0.0)) def test_cached_property(): class A(object): def __init__(self): self.counter = 0 @cached_property def f(self): self.counter += 1 return 2 a = A() # Delete a.f before a cache was set will fail silently. del a.f with pytest.raises(AttributeError): # Cache does not exist before f is called a._f assert a.f == 2 assert hasattr(a, '{}_f'.format(cached_property.base_str)) del a.f # check that deletion was successful with pytest.raises(AttributeError): # Does not exist before f is called a._f # However, the function should still be there assert a.f == 2 with pytest.raises(AttributeError): # Setting is not allowed. a.f = 3 # Counter should read 2 at this point, the number of calls since # object creation. assert a.counter == 2 for _ in range(10): a.f # Should be returning from cache, so a.f is not actually called assert a.counter == 2