├── .gitignore ├── LICENSE.txt ├── README.md ├── doc ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── images │ ├── simple_model_mcmc.png │ ├── simple_model_mcmc_right.png │ ├── simple_model_pdf.png │ ├── simple_model_pdf_family.png │ └── simple_model_smc.png │ ├── index.rst │ ├── install.rst │ ├── math.rst │ ├── reference.rst │ ├── tutorial.rst │ └── videos │ └── smc_movie.mp4 ├── examples ├── diffusion_inverse.py ├── diffusion_inverse_model.py ├── diffusion_solver.py ├── observed_data ├── reaction_kinetics_data.pickle ├── reaction_kinetics_model.py ├── reaction_kinetics_plot.py ├── reaction_kinetics_run.py ├── reaction_kinetics_run_nompi.py ├── reaction_kinetics_solver.py ├── simple_model.py ├── simple_model_run.py ├── simple_model_run_mpi.py ├── test_lognormal.py ├── true_data ├── true_locations └── true_noise ├── pysmc ├── __init__.py ├── _db.py ├── _mcmc_wrapper.py ├── _misc.py ├── _mpi.py ├── _particle_approximation.py ├── _plot.py ├── _smc.py └── _step_methods.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.~ 2 | *.dot 3 | *.txt 4 | *.pyc 5 | *.html 6 | *.pickle 7 | *.inv 8 | *.js 9 | *.out 10 | *doctree* 11 | *.so 12 | *.swp 13 | src/CMakeFiles/ 14 | src/Makefile 15 | src/cmake_install.cmake 16 | build 17 | backup 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pysmc 2 | ===== 3 | 4 | Sequential Monte Carlo working on top of pymc. 5 | 6 | **Compatible with Python3.** 7 | 8 | The complete documentation can be found here: http://predictivesciencelab.github.io/pysmc/ 9 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = $(PREFIX)/src/pysmcdocs 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pysmc.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pysmc.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pysmc" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pysmc" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pysmc.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pysmc.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pysmc documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 26 09:29:15 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | sys.path.insert(0, os.path.abspath('../../')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.pngmath' 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'pysmc' 52 | copyright = u'2013, Ilias Bilionis' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = '0.0' 60 | # The full version, including alpha/beta/rc tags. 61 | release = '0.0' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = [] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all 78 | # documents. 79 | #default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | #add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | #add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | #show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | #modindex_common_prefix = [] 97 | 98 | # If true, keep warnings as "system message" paragraphs in the built documents. 99 | #keep_warnings = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | html_theme = 'pyramid' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | #html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | #html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | #html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | #html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | #html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | #html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # Add any extra paths that contain custom files (such as robots.txt or 138 | # .htaccess) here, relative to this directory. These files are copied 139 | # directly to the root of the documentation. 140 | #html_extra_path = [] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | #html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | #html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | #html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | #html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | #html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | #html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | #html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | #html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | #html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | #html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | #html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | #html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'pysmcdoc' 185 | 186 | 187 | # -- Options for LaTeX output --------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | #'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, 202 | # author, documentclass [howto/manual]). 203 | latex_documents = [ 204 | ('index', 'pysmc.tex', u'pysmc Documentation', 205 | u'Ilias Bilionis', 'manual'), 206 | ] 207 | 208 | # The name of an image file (relative to this directory) to place at the top of 209 | # the title page. 210 | #latex_logo = None 211 | 212 | # For "manual" documents, if this is true, then toplevel headings are parts, 213 | # not chapters. 214 | #latex_use_parts = False 215 | 216 | # If true, show page references after internal links. 217 | #latex_show_pagerefs = False 218 | 219 | # If true, show URL addresses after external links. 220 | #latex_show_urls = False 221 | 222 | # Documents to append as an appendix to all manuals. 223 | #latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | #latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output --------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [ 234 | ('index', 'pysmc', u'pysmc Documentation', 235 | [u'Ilias Bilionis'], 1) 236 | ] 237 | 238 | # If true, show URL addresses after external links. 239 | #man_show_urls = False 240 | 241 | 242 | # -- Options for Texinfo output ------------------------------------------- 243 | 244 | # Grouping the document tree into Texinfo files. List of tuples 245 | # (source start file, target name, title, author, 246 | # dir menu entry, description, category) 247 | texinfo_documents = [ 248 | ('index', 'pysmc', u'pysmc Documentation', 249 | u'Ilias Bilionis', 'pysmc', 'One line description of project.', 250 | 'Miscellaneous'), 251 | ] 252 | 253 | # Documents to append as an appendix to all manuals. 254 | #texinfo_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | #texinfo_domain_indices = True 258 | 259 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 260 | #texinfo_show_urls = 'footnote' 261 | 262 | # If true, do not generate a @detailmenu in the "Top" node's menu. 263 | #texinfo_no_detailmenu = False 264 | 265 | 266 | # Example configuration for intersphinx: refer to the Python standard library. 267 | intersphinx_mapping = {'http://docs.python.org/': None} 268 | -------------------------------------------------------------------------------- /doc/source/images/simple_model_mcmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/images/simple_model_mcmc.png -------------------------------------------------------------------------------- /doc/source/images/simple_model_mcmc_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/images/simple_model_mcmc_right.png -------------------------------------------------------------------------------- /doc/source/images/simple_model_pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/images/simple_model_pdf.png -------------------------------------------------------------------------------- /doc/source/images/simple_model_pdf_family.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/images/simple_model_pdf_family.png -------------------------------------------------------------------------------- /doc/source/images/simple_model_smc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/images/simple_model_smc.png -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pysmc documentation master file, created by 2 | sphinx-quickstart on Thu Sep 26 09:29:15 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pysmc's documentation! 7 | ================================= 8 | 9 | :mod:`pysmc` is a Python package for sampling complicated probability 10 | densities using the celebrated Sequential Monte Carlo method. 11 | The core of this package was written during my PhD at 12 | `Cornell University `_ 13 | at the 14 | `Materials Process Design and Controll Laboratory `_ 15 | under the 16 | supervision of Prof. `Nicholas Zabaras `_. It is part of the 17 | `Bayesian Exploration Statistical Toolbox (BEST) 18 | `_ initiative. 19 | The coupling of the code with `PyMC `_ 20 | took part at 21 | the `Mathematics and Computer Science Division `_ 22 | at `Argonne National Laboratory `_. 23 | 24 | Contents: 25 | 26 | .. toctree:: 27 | :maxdepth: 3 28 | 29 | install 30 | tutorial 31 | reference 32 | math 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | 42 | .. _PyMC: 43 | http://pymc-devs.github.io/pymc/ 44 | -------------------------------------------------------------------------------- /doc/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | We describe the necessary steps required to succesfully install 8 | :mod:`pysmc`. The most important prior step is to satisfy the dependencies 9 | of the code. 10 | 11 | 12 | .. _depend: 13 | 14 | ----------- 15 | Dependecies 16 | ----------- 17 | 18 | There are two different categories of packages on which we rely. The 19 | :ref:`required` ones have to be there no matter what. The :ref:`optional` 20 | ones can be skipped but without them you will loose some of the 21 | functionality of :mod:`pysmc` or/and you will not be able to run all the 22 | examples in the :mod:`tutorial`. 23 | 24 | .. _required: 25 | 26 | Required 27 | ++++++++ 28 | The following packages are required: 29 | 30 | + `Numpy `_ for linear algebra. 31 | + `SciPy `_ for some root finding methods. 32 | + `PyMC `_ for general probabilistic 33 | model definition and MCMC sampling. We suggest that you install the 34 | `latest PyMC version from GitHub `_ 35 | . 36 | 37 | 38 | .. _optional: 39 | 40 | Optional 41 | ++++++++ 42 | The following packages are optional but highly recommended: 43 | 44 | + `MPI4PY `_ to enable the parallel capabilities of 45 | :mod:`pysmc`. 46 | + `matplotlib `_ for plotting. 47 | + `FiPy `_ in order to run the 48 | :ref:`diffusion_example`. 49 | 50 | .. _final_steps: 51 | 52 | ----------- 53 | Final Steps 54 | ----------- 55 | As soon as you are done installing the packages above, you can fetch 56 | :mod:`pysmc` from `GitHub `_ by:: 57 | 58 | git clone https://github.com/ebilionis/pysmc.git 59 | 60 | Then, all you have to do is enter the ``pysmc`` directory that was created 61 | and run:: 62 | 63 | python setup.py install 64 | 65 | If you want to put the code in an non default location, simply do:: 66 | 67 | python setup.py install --prefix=/path/to/your/directory 68 | 69 | If you do the latter, make sure you update your ``PYTHONPATH`` variable:: 70 | 71 | export PYTHONPATH=/path/to/your/directory/lib/python2.7/site-packages:$PYTHONPATH 72 | 73 | -------------------------------------------------------------------------------- /doc/source/math.rst: -------------------------------------------------------------------------------- 1 | .. _math: 2 | 3 | ==================== 4 | Mathematical Details 5 | ==================== 6 | -------------------------------------------------------------------------------- /doc/source/reference.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | ========= 4 | Reference 5 | ========= 6 | 7 | The goal of :mod:`pysmc` is to implement Sequential Monte Carlo (SMC) 8 | techniques on top of the Monte Carlo (MC) package 9 | `PyMC `_. The manual assumes that the user 10 | is already familiar with the way PyMC works. You are advised to read their 11 | tutorial before going on. A nice place to start with :mod:`pysmc` is 12 | our :ref:`tutorial`. 13 | 14 | .. automodule:: pysmc 15 | -------------------------------------------------------------------------------- /doc/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial: 2 | 3 | ======== 4 | Tutorial 5 | ======== 6 | 7 | 8 | Before moving forward, make sure you understand: 9 | 10 | + What is probability? That's a big question... If you feel like it, 11 | I suggest you skim through `E. T. Jaynes`_'s book 12 | `Probability Theory: The Logic of Science`_. 13 | + What is `MCMC`_? 14 | + Read the tutorial of `PyMC`_. 15 | To the very least, you need to be able to construct probabilistic 16 | models using this package. For advanced applications, you need to be 17 | able to construct your own `MCMC step methods`_. 18 | + Of course, I have to assume some familiarity with the so called 19 | Sequential Monte Carlo (SMC) or Particle Methods. Many resources can 20 | be found online at `Arnaud Doucet's collection`_ or in his book 21 | `Sequential Monte Carlo Methods in Practice`_. What exactly :mod:`pysmc` 22 | does is documented in :ref:`math`. 23 | 24 | 25 | .. _what is smc: 26 | 27 | ------------------------------- 28 | What is Sequential Monte Carlo? 29 | ------------------------------- 30 | 31 | Sequential Monte Carlo (SMC) is a very efficient and effective way to sample 32 | from complicated probability distributions known up to a normalizing constant. 33 | The most important complicating factor is multi-modality. That is, probability 34 | distributions that do not look at all like Gaussians. 35 | 36 | The complete details can be found in :ref:`math`. However, let us give some 37 | insights on what is really going on. Assume that we want so sample 38 | from a probability distribution :math:`p(x)`, known up to a normalizing 39 | constant: 40 | 41 | .. math:: 42 | 43 | p(x) \propto \pi(x). 44 | 45 | Normally, we construct a variant of `MCMC`_ to sample from this 46 | distribution. If :math:`p(x)` is multi-modal, it is certain that the Markov 47 | Chain will get attracted to one of the modes. Theoretically, if the modes are 48 | connected, it is guaranteed that they will all be visited 49 | *as the number of MCMC steps goes to infinity*. However, depending on the 50 | probability of the paths that connect the modes, the chain might 51 | never escape during the finite number of MCMC steps that we can actually afford 52 | to perform. 53 | 54 | SMC attempts to alleviate this problem. The way it does it is similar to the 55 | ideas found in `Simulated Annealing`_. The user defines a family of probability 56 | densities: 57 | 58 | .. math:: 59 | p_{i}(x) \propto \pi_{i}(x),\;i=1,\dots,n, 60 | :label: smc_sequence 61 | 62 | such that: 63 | 64 | + it is easy to sample from :math:`p_0(x)` (either directly or using MCMC), 65 | + the probability densities :math:`p_i(x)` and :math:`p_{i+1}(x)` are 66 | *similar*, 67 | + the last probability density of the sequence is the target, i.e., 68 | :math:`p_n(x) = p(x)`. 69 | 70 | There are many ways to define such a sequence. Usually, the exact sequence that 71 | needs to be followed is obvious from the definition of the problem. An obvious 72 | choice is: 73 | 74 | .. math:: 75 | p_i(x) \propto \pi^{\gamma_i}(x),\;i=1,\dots,n, 76 | :label: smc_sequence_power 77 | 78 | where :math:`\gamma_0` is a non-negative number that makes :math:`p_i(x)` look 79 | flat (e.g., if :math:`p(x)` has a compact support, you may choose 80 | :math:`\gamma_0=0` which makes :math:`p_0(x)` the uniform density. For 81 | the general case a choice like :math:`\gamma_0=10^{-3}` would still do a good 82 | job) and :math:`\gamma_n=1`. If :math:`n` is chosen sufficiently large and 83 | :math:`gamma_i < \gamma_{i+1}` then indeed :math:`p_i(x)` and :math:`p_{i+1}(x)` 84 | will look similar. 85 | 86 | Now we are in a position to discuss what SMC does. We represent each one of the 87 | probability densities :math:`p_i(x)` 88 | :eq:`smc_sequence` with a *particle approximation* 89 | :math:`\left\{\left(w^{(j)_i}, x^{(j)_i}\right)\right\}_{j=1}^N`, where: 90 | 91 | + :math:`N` is known as the *number of particles*, 92 | + :math:`w^{(j)}_i` is known as the *weight* of particle :math:`j` 93 | (normalized so that :math:`\sum_{j=1}^Nw^{(j)}_i=1`), 94 | + :math:`x^{(j)}_i` is known as the *particle* :math:`j`. 95 | 96 | Typically we write: 97 | 98 | .. math:: 99 | p_i(x) \approx \sum_{j=1}^Nw^{(j)}_i\delta\left(x - x^{(j)}_i\right), 100 | :label: smc_approx 101 | 102 | but what we really mean is that for any measurable 103 | function of the state space :math:`f(x)` the following holds: 104 | 105 | .. math:: 106 | \lim_{N\rightarrow\infty}\sum_{j=1}^Nw_i^{(j)}f\left(x^{(j)}_i\right) = \ 107 | \int f(x) p_i(x)dx, 108 | :label: smc_approx_def 109 | 110 | almost surely. 111 | 112 | So far so good. The only issue here is actually constructing a particle 113 | approximation satisfying :eq:`smc_approx_def`. This is a little bit involved 114 | and thus described in :ref:`math`. Here it suffices to say that it more or less 115 | goes like this: 116 | 117 | 1. Start with :math:`i=0` (i.e., the easy to sample distribution). 118 | 2. Sample :math:`x_0^{(j)}` from :math:`p_0(x)` either directly (if possible) or 119 | using MCMC and set the weights equal to :math:`w_0^{(j)} = 1 / N`. Then 120 | :eq:`smc_approx_def` is satisfied for :math:`i=0`. 121 | 3. Compute the weights :math:`w_{i+1}(j)` and sample -using an appropriate MCMC 122 | kernel- the particles of the next step :math:`x_i^{(j+1)}` so that they 123 | corresponding particle approximation satisfies :eq:`smc_approx_def`. 124 | 4. Set :math:`i=i+1`. 125 | 5. If :math:`i=n` stop. Otherwise go to 3. 126 | 127 | .. _what is in pysmc: 128 | 129 | ------------------------------------ 130 | What is implemented in :mod:`pysmc`? 131 | ------------------------------------ 132 | 133 | :mod:`pysmc` implements something a little bit more complicated than what is 134 | described in :ref:`what is smc`. The full description can be found in 135 | :ref:`math`. Basically, we assume that the user has defined a one-parameter 136 | family of probability densities: 137 | 138 | .. math:: 139 | p_{\gamma}(x) \propto \pi_{\gamma}(x). 140 | :label: p_gamma 141 | 142 | The code must be initialized with a particle approximation at a desired value 143 | of :math:`\gamma=\gamma_0`. This can be done either manually by the user or 144 | automatically by :mod:`pysmc` (e.g. by direct sampling or MCMC). 145 | Having constructed an initial particle approximation, the code can be instructed 146 | to move it to another :math:`\gamma=\gamma_1`. If the two probability densities 147 | :math:`p_{\gamma_0}(x)` and :math:`p_{\gamma_1}(x)` are close, then the code 148 | will jump directly into the construction of the particle approximation at 149 | :math:`\gamma=\gamma_1`. If not, then it will adaptively construct a finite 150 | sequence of :math:`\gamma`'s connecting :math:`\gamma_0` and :math:`\gamma_1` 151 | and jump from one to the other. Therefore, the user only needs to specify: 152 | 153 | + the initial, easy-to-sample-from probability density, 154 | + the target density, 155 | + a one-parametric family of densities that connect the two. 156 | 157 | We will see how this can be achieved through a bunch of examples. 158 | 159 | 160 | .. _simple example: 161 | 162 | ---------------- 163 | A Simple Example 164 | ---------------- 165 | 166 | We will start with a probability density with two modes, namely a mixture of 167 | two normal densities: 168 | 169 | .. math:: 170 | p(x) = \pi_1 \mathcal{N}\left(x | \mu_1, \sigma_1^2 \right) + \ 171 | \pi_2 \mathcal{N}\left(x | \mu_2, \sigma_2^2 \right), 172 | :label: simple_model_pdf 173 | 174 | where :math:`\mathcal{N}(x|\mu, \sigma^2)` denotes the probability density of a 175 | normal random variable with mean :math:`\mu` and variance :math:`\sigma^2`. 176 | :math:`\pi_i>0` is the weight given to the :math:`i`-th normal 177 | (:math:`\pi_1 + \pi_2 = 1`) and :math:`\mu_i, \sigma_i^2` are the corresponding 178 | mean and variance. We pick the following parameters: 179 | 180 | + :math:`\pi_1=0.2, \mu_1=-1, \sigma_1=0.01`, 181 | + :math:`\pi_2=0.8, \mu_2=2, \sigma_2=0.01`. 182 | 183 | This probability density is shown in `Simple Example PDF Figure`_. It is obvious 184 | that sampling this probability density using MCMC will be very problematic. 185 | 186 | .. _Simple Example PDF Figure: 187 | .. figure:: images/simple_model_pdf.png 188 | :align: center 189 | 190 | Plot of :eq:`simple_model_pdf` with 191 | :math:`\pi_1=0.2, \mu_1=-1, \sigma_1=0.01` and 192 | :math:`\pi_2=0.8, \mu_2=2, \sigma_2=0.01`. 193 | 194 | .. _simple example pdf family: 195 | 196 | ++++++++++++++++++++++++++++++++++++++++++++++++++ 197 | Defining a family of probability densities for SMC 198 | ++++++++++++++++++++++++++++++++++++++++++++++++++ 199 | 200 | Remember that our goal is to sample :eq:`simple_model_pdf` using SMC. Towards 201 | this goal we need to define a one-parameter family of probability densities 202 | :eq:`p_gamma` starting from a simple one to our target. The simplest choice 203 | is probably this: 204 | 205 | .. math:: 206 | \pi_{\gamma}(x) = p^{\gamma}(x). 207 | :label: simple_model_pdf_family 208 | 209 | Notice that: 1) for :math:`\gamma=1` we obtain :math:`p_\gamma(x)` and 2) for 210 | :math:`\gamma` small (say :math:`\gamma=10^{-2}`) we obtain a relatively flat 211 | probability density. See `Simple Example Family of PDF's Figure`_. 212 | 213 | .. _Simple Example Family of PDF's Figure: 214 | .. figure:: images/simple_model_pdf_family.png 215 | :align: center 216 | 217 | Plot of :math:`\pi_\gamma(x)` of :eq:`simple_model_pdf_family` for 218 | various :math:`\gamma`'s. 219 | 220 | .. _simple example model: 221 | 222 | ++++++++++++++++++++++++ 223 | Defining a `PyMC`_ model 224 | ++++++++++++++++++++++++ 225 | 226 | Since, this is our very first example we will use it as an opportunity to show 227 | how `PyMC`_ can be used to define probabilistic models as well as MCMC sampling 228 | algorithms. First of all let us mention that a `PyMC` model has to be packaged 229 | either in a class or in a module. For the simple example we are considering, we 230 | choose to use the module approach (see 231 | :download:`examples/simple_model.py <../../examples/simple_model.py>`). 232 | The model can be trivially defined using `PyMC` decorators. All we 233 | have to do is define the logarithm of :math:`\pi_{\gamma}(x)`. We will call it 234 | ``mixture``. The contents of that module are: 235 | 236 | .. code-block:: python 237 | :linenos: 238 | :emphasize-lines: 6,7 239 | 240 | import pymc 241 | import numpy as np 242 | import math 243 | 244 | @pymc.stochastic(dtype=float) 245 | def mixture(value=1., gamma=1., pi=[0.2, 0.8], mu=[-1., 2.], 246 | sigma=[0.01, 0.01]): 247 | """ 248 | The log probability of a mixture of normal densities. 249 | 250 | :param value: The point of evaluation. 251 | :type value : float 252 | :param gamma: The parameter characterizing the SMC one-parameter 253 | family. 254 | :type gamma : float 255 | :param pi : The weights of the components. 256 | :type pi : 1D :class:`numpy.ndarray` 257 | :param mu : The mean of each component. 258 | :type mu : 1D :class:`numpy.ndarray` 259 | :param sigma: The standard deviation of each component. 260 | :type sigma : 1D :class:`numpy.ndarray` 261 | """ 262 | # Make sure everything is a numpy array 263 | pi = np.array(pi) 264 | mu = np.array(mu) 265 | sigma = np.array(sigma) 266 | # The number of components in the mixture 267 | n = pi.shape[0] 268 | # pymc.normal_like requires the precision not the variance: 269 | tau = np.sqrt(1. / sigma ** 2) 270 | # The following looks a little bit awkward because of the need for 271 | # numerical stability: 272 | p = np.log(pi) 273 | p += np.array([pymc.normal_like(value, mu[i], tau[i]) 274 | for i in range(n)]) 275 | p = math.fsum(np.exp(p)) 276 | # logp should never be negative, but it can be zero... 277 | if p <= 0.: 278 | return -np.inf 279 | return gamma * math.log(p) 280 | 281 | This might look a little bit complicated but unfortunately one has to take care 282 | of round-off errors when sump small numbers... 283 | Notice that, we have defined pretty much every part of the mixture as an 284 | independent variable. The essential variable that defines the family of 285 | :eq:`simple_model_pdf_family` is ``gamma``. Well, you don't actually have to 286 | call it ``gamma``, but we will talk about this later... 287 | 288 | Let's import that module and see what we can do with it:: 289 | 290 | >>> import simple_model as model 291 | >>> print model.mixture.parents 292 | {'mu': [-1.0, 2.0], 'pi': [0.2, 0.8], 'sigma': [0.01, 0.01], 'gamma': 1.0} 293 | 294 | The final command shows you all the parents of the stochastic variable 295 | ``mixture``. 296 | The stochastic variable mixture was assigned a value by default (see line 4 297 | at the code block above). You can see the current value of the stochastic 298 | variable at any time by doing:: 299 | 300 | >>> print model.mixture.value 301 | 1.0 302 | 303 | If we started a `MCMC` chain at this point, this would be the initial value of 304 | the chain. You can change it to anything you want by simply doing:: 305 | 306 | >>> model.mixture.value = 0.5 307 | >>> print model.mixture.value 308 | 0.5 309 | 310 | To see the logarithm of the probability at the current state of the stochastic 311 | variable, do:: 312 | 313 | >>> print model.mixture.logp 314 | -111.11635344 315 | 316 | Now, if you want to change, let's say, ``gamma`` to ``0.5`` all 317 | you have to do is:: 318 | 319 | >>> model.mixture.parents['gamma'] = 0.5 320 | >>> print model.mixture.gamma 321 | 0.5 322 | 323 | The logarithm of the probability should have changed also:: 324 | 325 | >>> print model.mixture.logp 326 | -55.5581767201 327 | 328 | .. _mcmc_attempt: 329 | 330 | ++++++++++++++++++++++++ 331 | Attempting to do `MCMC`_ 332 | ++++++++++++++++++++++++ 333 | 334 | Let's load the model again and attempt to do `MCMC`_ using `PyMC`_'s 335 | functionality:: 336 | 337 | >>> import simple_model as model 338 | >>> import pymc 339 | >>> mcmc_sampler = pymc.MCMC(model) 340 | >>> mcmc_sampler.sample(1000000, thin=1000, burn=1000) 341 | 342 | You should see a progress bar measuring the number of samples taken. It should 343 | take about a minute to finish. We are actually doing :math:`10^6` `MCMC`_ steps, 344 | we burn the first ``burn = 1000`` samples and we are looking at the chain 345 | every ``thin = 1000`` samples (i.e., we are dropping everything in between). 346 | `PyMC`_ automatically picks a proposal (see `MCMC step methods`_) for you. For 347 | this particular example it should have picked 348 | :class:`pymc.step_methods.Metropolis` which corresponds to a simple random walk 349 | proposal. There is no need to tune the parameters of the random walk since 350 | `PyMC`_ is supposed to do that for you. In any case, it is possible to find the 351 | right variance for the random walk, but you need to know exactly how far apart 352 | the modes are... 353 | 354 | You may look at the samples we've got by doing:: 355 | 356 | >>> print mcmc_sampler.trace('mixture')[:] 357 | [ 1.9915846 1.93300521 2.09291872 2.05159841 2.06620882 1.88901709 358 | 1.89521431 1.9631256 2.0363258 1.9756637 2.04818845 1.85036634 359 | 1.98907666 1.82212356 1.97678175 1.99854311 1.92124829 2.02077581 360 | 2.08536334 2.16664208 2.08328293 2.05378638 1.89437676 2.09555348 361 | ... 362 | 363 | Now, let us plot the results:: 364 | 365 | >>> import matplotlib.pyplot as plt 366 | >>> pymc.plot(mcmc_sampler) 367 | >>> plt.show() 368 | 369 | The results are shown in `Simple Example MCMC Figure`_. Unless, you are 370 | extremely lucky, you should have missed one of the modes... 371 | 372 | .. _Simple Example MCMC Figure: 373 | .. figure:: images/simple_model_mcmc.png 374 | :align: center 375 | 376 | MCMC fails to capture one of the modes of :eq:`simple_model_pdf`. 377 | 378 | Now, let's see what it takes to make it work. Basically, what we need to 379 | do is find the right step for the random walk proposal. Looking at 380 | `Simple Example Family of PDF's Figure`_, it is easy to guess that the 381 | right step is :math:`3`. So, let's try this:: 382 | 383 | >>> import simple_model as model 384 | >>> import pymc 385 | >>> mcmc_sampler = pymc.MCMC(simple_model) 386 | >>> proposal = pymc.Metropolis(model.mixture, proposal_sd=3.) 387 | >>> mcmc_sampler.step_method_dict[model.mixture][0] = proposal 388 | >>> mcmc_sampler.sample(1000000, thin=1000, burn=0, 389 | tune_throughout=False) 390 | 391 | For more details on selecting/tuning step methods see 392 | `MCMC step methods`_. In the last line we have asked `PyMC`_ not to tune 393 | the parameters of the step method. If we didn't do that, it would be 394 | fooled again. The results are shown in 395 | `Simple Example MCMC Picked Proposal Figure`_. 396 | 397 | .. _Simple Example MCMC Picked Proposal Figure: 398 | .. figure:: images/simple_model_mcmc_right.png 399 | :align: center 400 | 401 | Multi-modal distributions can be captured by simple `MCMC`_ only if 402 | you have significant prior knowledge about them. 403 | 404 | You see that the two modes can be captured by plain `MCMC`_ if we 405 | actually know how far appart they are. Of course, this is completely 406 | useless in a real problem. Most of the times, we are not be able to 407 | draw the probability density function and see where the modes are. 408 | SMC is here to save the day! 409 | 410 | .. _smc_attempt: 411 | 412 | +++++++++ 413 | Doing SMC 414 | +++++++++ 415 | 416 | To finish this example, let's just see how SMC behaves. As we mentioned 417 | earlier, SMC requires: 418 | 419 | + a one-parameter family of probability densities connecting a simple 420 | probability density to our target (we created this in 421 | :ref:`simple_example_model`), and 422 | + an `MCMC` sampler (we saw how to create one in :ref:`mcmc_attempt`). 423 | 424 | Now, let's put everything together using the functionality of 425 | :class:`pysmc.SMC`: 426 | 427 | .. code-block:: python 428 | :linenos: 429 | 430 | import simple_model as model 431 | import pymc 432 | import pysmc 433 | 434 | # Construct the MCMC sampler 435 | mcmc_sampler = pymc.MCMC(model) 436 | # Construct the SMC sampler 437 | smc_sampler = pysmc.SMC(mcmc_sampler, num_particles=1000, 438 | num_mcmc=10, verbose=1) 439 | # Initialize SMC at gamma = 0.01 440 | smc_sampler.initialize(0.01) 441 | # Move the particles to gamma = 1.0 442 | smc_sampler.move_to(1.0) 443 | # Get a particle approximation 444 | p = smc_sampler.get_particle_approximation() 445 | # Plot a histogram 446 | pysmc.hist(p, 'mixture') 447 | plt.show() 448 | 449 | This code can be found in 450 | :download:`examples/simple_model_run.py <../../examples/simple_model_run.py>`. 451 | 452 | In lines 8-9, we initialize the SMC class. Of course, it requires a 453 | ``mcmc_sampler`` which is a :class:`pymc.MCMC` object. 454 | ``num_particles`` specifies the number of particles we wish to use and 455 | ``num_mcmc`` the number of `MCMC`_ steps we are going to perform at each 456 | different value of :math:`\gamma`. The ``verbose`` parameter specifies 457 | the amount of text the algorithm prints to the standard output. 458 | There are many more parameters which are fully documented in 459 | :class:`pysmc.SMC`. In line 11, we initialize the algorithm at 460 | :math:`\gamma=10^{-2}`. This essentially performs a number of `MCMC`_ 461 | steps at this easy-to-sample-from probability density. It constructs the 462 | initialial particle approximation. See 463 | :meth:`pysmc.SMC.initialize()` the complete list of arguments. 464 | In line 13, we instruct the object to move the particle 465 | approximation to :math:`\gamma=1`, i.e., to the target probability 466 | density of this particular example. To see the weights of the final 467 | particle approximation, we use :attr:`pysmc.SMC.weights` 468 | (e.g., ``smc_sampler.weights``). To get the particles themselves we may 469 | use :attr:`pysmc.SMC.particles` (e.g., ``smc_sampler.particles``) which 470 | returns a dictionary of the particles. However, it is usually most 471 | convenient to access them via :meth:`pysmc.SMC.get_particle_approximation()` 472 | which returns a :class:`pysmc.ParticleApproximation` object. 473 | 474 | The output of the algorithm looks like this:: 475 | 476 | ------------------------ 477 | START SMC Initialization 478 | ------------------------ 479 | - initializing at gamma : 0.01 480 | - initializing by sampling from the prior: FAILURE 481 | - initializing via MCMC 482 | - taking a total of 10000 483 | - creating a particle every 10 484 | [---------------- 43% ] 4392 of 10000 complete in 0.5 sec 485 | [-----------------83%----------- ] 8322 of 10000 complete in 1.0 sec 486 | ---------------------- 487 | END SMC Initialization 488 | ---------------------- 489 | ----------------- 490 | START SMC MOVE TO 491 | ----------------- 492 | initial gamma : 0.01 493 | final gamma : 1.0 494 | ess reduction: 0.9 495 | - moving to gamma : 0.0204271802459 496 | - performing 10 MCMC steps per particle 497 | [------------ 31% ] 3182 of 10000 complete in 0.5 sec 498 | [-----------------62%--- ] 6292 of 10000 complete in 1.0 sec 499 | [-----------------93%--------------- ] 9322 of 10000 complete in 1.5 sec 500 | - moving to gamma : 0.0382316662144 501 | - performing 10 MCMC steps per particle 502 | [---------- 28% ] 2822 of 10000 complete in 0.5 sec 503 | [-----------------55%- ] 5542 of 10000 complete in 1.0 sec 504 | [-----------------81%----------- ] 8162 of 10000 complete in 1.5 sec 505 | - moving to gamma : 0.0677677161458 506 | - performing 10 MCMC steps per particle 507 | [--------- 24% ] 2442 of 10000 complete in 0.5 sec 508 | [-----------------47% ] 4732 of 10000 complete in 1.0 sec 509 | [-----------------70%------ ] 7032 of 10000 complete in 1.5 sec 510 | [-----------------91%-------------- ] 9172 of 10000 complete in 2.0 sec 511 | - moving to gamma : 0.118156872826 512 | - performing 10 MCMC steps per particle 513 | [-------- 21% ] 2122 of 10000 complete in 0.5 sec 514 | [--------------- 42% ] 4202 of 10000 complete in 1.0 sec 515 | [-----------------62%--- ] 6232 of 10000 complete in 1.5 sec 516 | [-----------------81%----------- ] 8172 of 10000 complete in 2.0 sec 517 | - moving to gamma : 0.206496296978 518 | - performing 10 MCMC steps per particle 519 | [----- 15% ] 1502 of 10000 complete in 0.5 sec 520 | [------------ 33% ] 3332 of 10000 complete in 1.0 sec 521 | [-----------------48% ] 4882 of 10000 complete in 1.5 sec 522 | [-----------------66%----- ] 6602 of 10000 complete in 2.0 sec 523 | [-----------------82%----------- ] 8252 of 10000 complete in 2.5 sec 524 | [-----------------96%---------------- ] 9692 of 10000 complete in 3.0 sec 525 | - moving to gamma : 0.326593174468 526 | - performing 10 MCMC steps per particle 527 | [------ 16% ] 1642 of 10000 complete in 0.5 sec 528 | [------------ 32% ] 3252 of 10000 complete in 1.0 sec 529 | [-----------------48% ] 4842 of 10000 complete in 1.5 sec 530 | [-----------------64%---- ] 6412 of 10000 complete in 2.0 sec 531 | [-----------------79%---------- ] 7942 of 10000 complete in 2.5 sec 532 | [-----------------94%--------------- ] 9432 of 10000 complete in 3.0 sec 533 | - moving to gamma : 0.494246909688 534 | - performing 10 MCMC steps per particle 535 | [----- 14% ] 1402 of 10000 complete in 0.5 sec 536 | [---------- 28% ] 2832 of 10000 complete in 1.0 sec 537 | [---------------- 42% ] 4232 of 10000 complete in 1.5 sec 538 | [-----------------56%- ] 5622 of 10000 complete in 2.0 sec 539 | [-----------------69%------ ] 6992 of 10000 complete in 2.5 sec 540 | [-----------------83%----------- ] 8342 of 10000 complete in 3.0 sec 541 | [-----------------96%---------------- ] 9662 of 10000 complete in 3.5 sec 542 | - moving to gamma : 0.680964613537 543 | - performing 10 MCMC steps per particle 544 | [---- 13% ] 1302 of 10000 complete in 0.5 sec 545 | [--------- 25% ] 2552 of 10000 complete in 1.0 sec 546 | [-------------- 38% ] 3832 of 10000 complete in 1.5 sec 547 | [-----------------50% ] 5032 of 10000 complete in 2.0 sec 548 | [-----------------62%--- ] 6272 of 10000 complete in 2.5 sec 549 | [-----------------75%-------- ] 7502 of 10000 complete in 3.0 sec 550 | [-----------------86%------------ ] 8672 of 10000 complete in 3.5 sec 551 | [-----------------98%----------------- ] 9842 of 10000 complete in 4.0 sec 552 | - moving to gamma : 1.0 553 | - performing 10 MCMC steps per particle 554 | [---- 11% ] 1172 of 10000 complete in 0.5 sec 555 | [-------- 23% ] 2302 of 10000 complete in 1.0 sec 556 | [------------ 34% ] 3412 of 10000 complete in 1.5 sec 557 | [-----------------44% ] 4482 of 10000 complete in 2.0 sec 558 | [-----------------55%- ] 5582 of 10000 complete in 2.5 sec 559 | [-----------------67%----- ] 6702 of 10000 complete in 3.0 sec 560 | [-----------------77%--------- ] 7792 of 10000 complete in 3.5 sec 561 | [-----------------88%------------- ] 8892 of 10000 complete in 4.0 sec 562 | [-----------------99%----------------- ] 9972 of 10000 complete in 4.5 sec 563 | --------------- 564 | END SMC MOVE TO 565 | --------------- 566 | 567 | The figure you should see is shown in 568 | `Simple Example SMC Histogram Figure`_. You see that SMC has no problem 569 | discovering the two modes, even though we have not hand-picked the 570 | parameters of the `MCMC`_ proposal. 571 | 572 | .. _Simple Example SMC Histogram Figure: 573 | .. figure:: images/simple_model_smc.png 574 | :align: center 575 | 576 | SMC easily discovers both modes of :eq:`simple_model_pdf` 577 | 578 | .. _smc_without_mcmc: 579 | 580 | ++++++++++++++++++++++++++++++++++++ 581 | Initializing SMC with just the model 582 | ++++++++++++++++++++++++++++++++++++ 583 | 584 | It is also possible to initialize SMC without explicitly specifying a 585 | :class:`pymc.MCMC`. This allows `PyMC`_ to select everything for you. 586 | It can be done like this: 587 | 588 | .. code-block:: python 589 | :linenos: 590 | 591 | import simple_model as model 592 | import pysmc 593 | 594 | # Construct the SMC sampler 595 | smc_sampler = pysmc.SMC(model, num_particles=1000, 596 | num_mcmc=10, verbose=1) 597 | # Do whatever you want to smc_sampler... 598 | 599 | .. _database: 600 | 601 | ++++++++++++++++++++++++++++++ 602 | Dumping the data to a database 603 | ++++++++++++++++++++++++++++++ 604 | 605 | At the moment we support only a very simple database (see 606 | :class:`pysmc.DataBase`) which is built on :mod:`cPickle`. There are two 607 | options of :class:`pysmc.SMC` that you need to specify when you want to use 608 | the database functionality: ``db_filename`` and ``update_db``. See the 609 | documentation of :class:`pysmc.SMC` for their complete details. If you are 610 | using a database, you can force the current state of SMC to be written to 611 | it by calling :meth:`pysmc.commit()`. If you want to access a database, 612 | the all you have to do is use the attribute :attr:`pysmc.db`. 613 | Here is a very simple illustration of what you can do with this: 614 | 615 | .. code-block:: python 616 | :linenos: 617 | 618 | import simple_model as model 619 | import pysmc 620 | 621 | smc_sampler = pysmc.SMC(model, num_particles=1000, 622 | num_mcmc=10, verbose=1, 623 | db_filename='db.pickle', 624 | update_db=True) 625 | smc_sampler.initialize(0.01) 626 | smc_sampler.move(0.5) 627 | 628 | As you have probably grasped until now, this will construct an initial 629 | particle approximation at :math:`\gamma=0.01` and it will move it to 630 | :math:`\gamma=0.5`. The ``db_filename`` option specifies the file on which 631 | the database will write data. Let us assume that the file does not exist. 632 | Then, it will be created. The option ``update_db`` specifies that SMC should 633 | commit data to the database everytime it moves to another :math:`\gamma`. 634 | Now, assume that you quit python. The database is saved in ``db.pickle``. 635 | If you want to continue from the point you had stopped, all you have to do 636 | is: 637 | 638 | .. code-block:: python 639 | :linenos: 640 | 641 | import simple_model as model 642 | import pysmc 643 | 644 | smc_sampler = pysmc.SMC(model, num_particles=1000, 645 | num_mcmc=10, verbose=1, 646 | db_filename='db.pickle', 647 | update_db=True) 648 | smc_sampler.move(1.) 649 | 650 | Now, we have just skipped the initialization line. Sine the database already 651 | exists, :class:`pysmc.SMC` looks at it and initializes the particle 652 | approximation from the last state stored in it. Therefore, you actually 653 | start moving from :math:`\gamma=0.5` to :math:`\gamma=1`. 654 | 655 | .. _db_movie: 656 | 657 | +++++++++++++++++++++++++++++++ 658 | Making a movie of the particles 659 | +++++++++++++++++++++++++++++++ 660 | 661 | If you have stored the particle approximations of the various 662 | :math:`\gamma`'s in a database, then it is possible to make a movie out of 663 | them using :func:`pysmc.make_movie_from_db()`. Here is how: 664 | 665 | .. code-block:: python 666 | :linenos: 667 | 668 | import pysmc 669 | import matplotlib.pyplot as plt 670 | 671 | # Load the database 672 | db = pysmc.DataBase.load('db.pickle') 673 | # Make the movie 674 | movie = make_movie_from_db(db, 'mixture') 675 | # See the movie 676 | plt.show() 677 | # Save the movie 678 | movie.save('smc_movie.mp4') 679 | 680 | The movie created in this way can be downloaded from 681 | :download:`videos/smc_movie.mp4`. 682 | 683 | .. _parallel_sampling: 684 | 685 | -------------------- 686 | Sampling in Parallel 687 | -------------------- 688 | 689 | With :mod:`pysmc` it is possible to carry out the sampling procedure in 690 | parallel. As is well known, SMC is embarransingly parallelizable. Taking it to 691 | the extreme, its process has to deal with only one particle. Communication is 692 | only necessary when there is a need to resample the particle approximation 693 | and for finding the next :math:`\gamma` in the sequence of probability 694 | densities. Parallelization in :mod:`pysmc` is achieved with the help of 695 | `mpi4py `_. Here is a simple example: 696 | 697 | .. code-block:: python 698 | :linenos: 699 | :emphasize-lines: 3, 9, 18, 19 700 | 701 | import pysmc 702 | import simple_model as model 703 | import mpi4py.MPI as mpi 704 | import matplotlib.pyplot as plt 705 | 706 | # Construct the SMC sampler 707 | smc_sampler = pysmc.SMC(model, num_particles=1000, 708 | num_mcmc=10, verbose=1, 709 | mpi=mpi, gamma_is_an_exponent=True) 710 | # Initialize SMC at gamma = 0.01 711 | smc_sampler.initialize(0.01) 712 | # Move the particles to gamma = 1.0 713 | smc_sampler.move_to(1.) 714 | # Get a particle approximation 715 | p = smc_sampler.get_particle_approximation() 716 | # Plot a histogram 717 | pysmc.hist(p, 'mixture') 718 | if mpi.COMM_WORLD.Get_rank() == 0: 719 | plt.show() 720 | 721 | As you can see the only difference is at line 3 where :mod:`mpi4py` is 722 | instantiated, line 8 where mpi is passed as an argument to :class:`pysmc.SMC` 723 | and the last two lines where we simply specify that only one process should 724 | attempt to plot the histogram. 725 | 726 | .. _E. T. Jaynes: 727 | E. T. Jaynes' http://en.wikipedia.org/wiki/Edwin_Thompson_Jaynes> 728 | .. _Probability Theory\: The Logic of Science: 729 | http://omega.albany.edu:8008/JaynesBook.html 730 | .. _MCMC: 731 | http://en.wikipedia.org/wiki/Markov_chain_Monte_Carlo 732 | .. _PyMC: 733 | http://pymc-devs.github.io/pymc/ 734 | .. _MCMC step methods: 735 | http://pymc-devs.github.io/pymc/extending.html#user-defined-step-methods 736 | .. _Arnaud Doucet's collection: 737 | http://www.stats.ox.ac.uk/~doucet/smc_resources.html 738 | .. _Sequential Monte Carlo Methods in Practice: 739 | http://books.google.com/books/about/Sequential_Monte_Carlo_Methods_in_Practi.html?id=BnWAcgAACAAJ 740 | .. _Simulated Annealing: 741 | http://en.wikipedia.org/wiki/Simulated_annealing 742 | -------------------------------------------------------------------------------- /doc/source/videos/smc_movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/pysmc/fc6f1b2473d9067c108d6b3d7f27f29d3b521228/doc/source/videos/smc_movie.mp4 -------------------------------------------------------------------------------- /examples/diffusion_inverse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Solving an inverse problem with MCMC. 3 | ---------------------------------------------- 4 | """ 5 | 6 | 7 | import warnings 8 | warnings.filterwarnings("ignore") 9 | import diffusion_inverse_model 10 | import matplotlib.pyplot as plt 11 | import sys 12 | import os 13 | sys.path.insert(0, os.path.abspath('..')) 14 | import pysmc as ps 15 | import pymc as pm 16 | import mpi4py.MPI as mpi 17 | 18 | 19 | if __name__ == '__main__': 20 | model = diffusion_inverse_model.make_model() 21 | mcmc = pm.MCMC(model) 22 | mcmc.use_step_method(ps.GaussianMixtureStep, model['location']) 23 | mcmc.use_step_method(ps.LognormalRandomWalk, model['alpha'], 24 | proposal_sd=1.) 25 | mcmc.use_step_method(ps.LognormalRandomWalk, model['beta'], 26 | proposal_sd=1.) 27 | mcmc.use_step_method(ps.LognormalRandomWalk, model['tau'], 28 | proposal_sd=1.) 29 | try: 30 | smc_sampler = ps.SMC(mcmc, num_particles=64, num_mcmc=1, 31 | verbose=3, mpi=mpi, 32 | db_filename='diffusion_inverse.pickle', 33 | update_db=True, 34 | gamma_is_an_exponent=True) 35 | smc_sampler.initialize(0.) 36 | smc_sampler.move_to(1.) 37 | except Exception as e: 38 | print(str(e)) 39 | mpi.COMM_WORLD.Abort(1) 40 | p = smc_sampler.get_particle_approximation() 41 | m = p.mean 42 | v = p.variance 43 | if mpi.COMM_WORLD.Get_rank() == 0: 44 | print(m) 45 | print(v) 46 | -------------------------------------------------------------------------------- /examples/diffusion_inverse_model.py: -------------------------------------------------------------------------------- 1 | import pymc 2 | import numpy as np 3 | from diffusion_solver import DiffusionSourceLocationOnly as Solver 4 | 5 | def make_model(): 6 | # Construct the prior term 7 | location = pymc.Uniform('location', lower=[0,0], upper=[1,1]) 8 | # The locations of the sensors 9 | X = [[0., 0.], [0., 1.], [1., 0.], [1., 1.]] 10 | # The output of the model 11 | solver = Solver(X=X) 12 | @pymc.deterministic(plot=False) 13 | def model_output(value=None, loc=location): 14 | return solver(loc) 15 | # The hyper-parameters of the noise 16 | alpha = pymc.Exponential('alpha', beta=1.) 17 | beta = pymc.Exponential('beta', beta=1.) 18 | tau = pymc.Gamma('tau', alpha=alpha, beta=beta) 19 | # Load the observed data 20 | data = np.loadtxt('observed_data') 21 | # The observations at the sensor locations 22 | @pymc.stochastic(dtype=float, observed=True) 23 | def sensors(value=data, mu=model_output, tau=tau, gamma=1.): 24 | """The value of the response at the sensors.""" 25 | return gamma * pymc.normal_like(value, mu=mu, tau=tau) 26 | return locals() 27 | -------------------------------------------------------------------------------- /examples/diffusion_solver.py: -------------------------------------------------------------------------------- 1 | """A class that solves the following diffusion problem: 2 | 3 | dphi/dt = div(grad(phi)) + S(x,t), 4 | 5 | where S(x, t) is the source term and it is modelled as: 6 | 7 | S(x, t) = s / (2 * PI * h ** 2) * exp( - ||xc - x||^2/(2 * h ** 2), for 8 | t in [0, tau] 9 | S(x, t) = 0, for t > tau. 10 | 11 | Author: 12 | Ilias Bilionis 13 | 14 | Date: 15 | 1/26/2013 16 | """ 17 | 18 | from fipy import Grid2D 19 | from fipy import CellVariable 20 | from fipy import TransientTerm 21 | from fipy import DiffusionTerm 22 | from fipy import ExplicitDiffusionTerm 23 | from fipy import numerix 24 | import numpy as np 25 | import math 26 | 27 | 28 | class Diffusion(object): 29 | """Solve the Diffusion problem.""" 30 | 31 | # Number of cells 32 | _num_cells = None 33 | 34 | # The discretization step 35 | _dx = None 36 | 37 | # The mesh 38 | _mesh = None 39 | 40 | # The solution vector 41 | _phi = None 42 | 43 | # The source term 44 | _source = None 45 | 46 | # The equation 47 | _eq = None 48 | 49 | # The time step 50 | _dt = None 51 | 52 | # The maximum solution time 53 | _max_time = None 54 | 55 | # The indices of the variables you want to observe 56 | _idx = None 57 | 58 | # The sensor times 59 | _T = None 60 | 61 | @property 62 | def num_cells(self): 63 | """Get the number of cells.""" 64 | return self._num_cells 65 | 66 | @property 67 | def dx(self): 68 | """Get the discretization steps.""" 69 | return self._dx 70 | 71 | @property 72 | def mesh(self): 73 | """Get the mesh.""" 74 | return self._mesh 75 | 76 | @property 77 | def phi(self): 78 | """Get the solution.""" 79 | return self._phi 80 | 81 | @property 82 | def source(self): 83 | """Get the source term.""" 84 | return self._source 85 | 86 | @property 87 | def eq(self): 88 | """Get the equation.""" 89 | return self._eq 90 | 91 | @property 92 | def dt(self): 93 | """Get the timestep.""" 94 | return self._dt 95 | 96 | @property 97 | def idx(self): 98 | """Get the indices of the observed variables.""" 99 | return self._idx 100 | 101 | @property 102 | def max_time(self): 103 | """Get the maximum solution time.""" 104 | return self._max_time 105 | 106 | @property 107 | def T(self): 108 | """Get the sensor measurement times.""" 109 | return self._T 110 | 111 | def __init__(self, X=None, T=None, time_steps=5, max_time=0.4, num_cells=25, L=1.): 112 | """Initialize the objet. 113 | 114 | Keyword Arguments: 115 | X --- The sensor locations. 116 | T --- The sensor measurment times. 117 | time_steps --- How many timesteps do you want to measure. 118 | max_time --- The maximum solution time. 119 | num_cells --- The number of cells per dimension. 120 | L --- The size of the computational domain. 121 | """ 122 | assert isinstance(num_cells, int) 123 | self._num_cells = num_cells 124 | assert isinstance(L, float) and L > 0. 125 | self._dx = L / self.num_cells 126 | self._mesh = Grid2D(dx=self.dx, dy=self.dx, nx=self.num_cells, 127 | ny=self.num_cells) 128 | self._phi = CellVariable(name='solution variable', mesh=self.mesh) 129 | self._source = CellVariable(name='source term', mesh=self.mesh, 130 | hasOld=True) 131 | self._eqX = TransientTerm() == ExplicitDiffusionTerm(coeff=1.) + self.source 132 | self._eqI = TransientTerm() == DiffusionTerm(coeff=1.) + self.source 133 | self._eq = self._eqX + self._eqI 134 | assert isinstance(max_time, float) and max_time > 0. 135 | self._max_time = max_time 136 | #self.max_time / time_steps #. 137 | if X is None: 138 | idx = range(self.num_cells ** 2) 139 | else: 140 | idx = [] 141 | x1, x2 = self.mesh.cellCenters 142 | for x in X: 143 | dist = (x1 - x[0]) ** 2 + (x2 - x[1]) ** 2 144 | idx.append(np.argmin(dist)) 145 | self._idx = idx 146 | if T is None: 147 | T = np.linspace(0, self.max_time, time_steps)[1:] 148 | self._max_time = T[-1] 149 | self._T = T 150 | self._dt = self.T[0] / time_steps 151 | self.num_input = 5 152 | self.num_output = len(self.T) * len(self.idx) 153 | 154 | def __call__(self, x): 155 | """Evaluate the solver.""" 156 | self.phi.value = 0. 157 | x_center = x[:2] 158 | tau = x[2] 159 | h = x[3] 160 | s = x[4] 161 | x, y = self.mesh.cellCenters 162 | self.source.value = 0.5 * s / (math.pi * h ** 2) * numerix.exp(-0.5 * 163 | ((x_center[0] - x) ** 2 + (x_center[1] - y) ** 2) 164 | / (2. * h ** 2)) 165 | y_all = [] 166 | t = 0. 167 | self._times = [] 168 | next_time = 0 169 | while t < self.max_time: 170 | t += self.dt 171 | add = False 172 | if next_time < len(self.T) and t >= self.T[next_time]: 173 | t = self.T[next_time] 174 | next_time += 1 175 | add = True 176 | if t >= tau: 177 | self.source.value = 0. 178 | self.eq.solve(var=self.phi, dt=self.dt) 179 | if add: 180 | self._times.append(t) 181 | y_all.append([self.phi.value[i] for i in self.idx]) 182 | y = np.hstack(y_all) 183 | y = y.reshape((self.T.shape[0], len(self.idx))).flatten(order='F') 184 | return y 185 | 186 | 187 | class DiffusionSourceLocationOnly(Diffusion): 188 | """A Diffusion solver that allows only the source location to vary.""" 189 | 190 | # The source signal 191 | _s = None 192 | 193 | # The source spread 194 | _h = None 195 | 196 | # The source time 197 | _tau = None 198 | 199 | @property 200 | def s(self): 201 | """Get the source signal.""" 202 | return self._s 203 | 204 | @property 205 | def h(self): 206 | """Get the source spread.""" 207 | return self._h 208 | 209 | @property 210 | def tau(self): 211 | """Get the source time.""" 212 | return self._tau 213 | 214 | def __init__(self, s=2., h=0.05, tau=0.3, X=None, T=None, time_steps=5, 215 | max_time=0.4, num_cells=25, L=1.): 216 | """Initialize the object. 217 | 218 | Keyword Arguments: 219 | s --- The source signal. 220 | h --- The source spread. 221 | tau --- The source time. 222 | The rest are as in Diffusion. 223 | """ 224 | assert isinstance(s, float) and s > 0. 225 | self._s = s 226 | assert isinstance(h, float) and h > 0. 227 | self._h = h 228 | assert isinstance(tau, float) and tau > 0. 229 | self._tau = tau 230 | super(DiffusionSourceLocationOnly, self).__init__(X=X, T=T, 231 | time_steps=time_steps, max_time=max_time, num_cells=num_cells, 232 | L=L) 233 | self.num_input = 2 234 | 235 | def __call__(self, x): 236 | """Evaluate the object.""" 237 | x = np.array(x) 238 | x = np.hstack([x, [self.tau, self.h, self.s]]) 239 | return super(DiffusionSourceLocationOnly, self).__call__(x) 240 | -------------------------------------------------------------------------------- /examples/observed_data: -------------------------------------------------------------------------------- 1 | 1.922305257888734520e+00 2 | 2.770166710198965543e+00 3 | 2.809706095142195270e+00 4 | 1.658624254362745409e+00 5 | 4.336902295949712077e-02 6 | 4.133754103287081705e-01 7 | 7.306368605789490545e-01 8 | 1.111944228636938758e+00 9 | 1.871047490616701281e-01 10 | 5.710760870963726976e-01 11 | 9.882620229541744239e-01 12 | 1.004500133058143341e+00 13 | 9.912941162989732935e-02 14 | 2.509745863203715066e-01 15 | 5.015556993810352804e-01 16 | 7.083945323962459462e-01 17 | -------------------------------------------------------------------------------- /examples/reaction_kinetics_data.pickle: -------------------------------------------------------------------------------- 1 | (dp1 2 | S'y_obs' 3 | p2 4 | cnumpy.core.multiarray 5 | _reconstruct 6 | p3 7 | (cnumpy 8 | ndarray 9 | p4 10 | (I0 11 | tS'b' 12 | tRp5 13 | (I1 14 | (I10 15 | tcnumpy 16 | dtype 17 | p6 18 | (S'f8' 19 | I0 20 | I1 21 | tRp7 22 | (I3 23 | S'<' 24 | NNNI-1 25 | I-1 26 | I0 27 | tbI00 28 | S'L\x07\x15\xabYU\xe5?\x05\xfd\xdcZUU\xe5?\x80v\xa0ZUU\xe5?\xe5\x85\x9d\x8eUU\xe5??\xdc\xbb[UU\xe5?h\xf1\xd5\xa9LU\xd5?\xf8\x05FJUU\xd5?\x02\x13\xbfJUU\xd5?9\xf4\xc4\xe2TU\xd5?\x85G\x88HUU\xd5?' 29 | tbs. -------------------------------------------------------------------------------- /examples/reaction_kinetics_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | The definition of the posterior of the reaction kinetics problem. 3 | """ 4 | 5 | 6 | import pymc 7 | import warnings 8 | warnings.simplefilter('ignore') 9 | from reaction_kinetics_solver import * 10 | 11 | 12 | def make_model(): 13 | import pickle 14 | with open('reaction_kinetics_data.pickle', 'rb') as fd: 15 | data = pickle.load(fd, encoding='latin1') 16 | y_obs = data['y_obs'] 17 | # The priors for the reaction rates: 18 | k1 = pymc.Lognormal('k1', mu=2, tau=1./(10. ** 2), value=5.) 19 | k2 = pymc.Lognormal('k2', mu=4, tau=1./(10. ** 2), value=5.) 20 | # The noise term 21 | #sigma = pymc.Uninformative('sigma', value=1.) 22 | sigma = pymc.Exponential('sigma', beta=1.) 23 | # The forward model 24 | re_solver = ReactionKineticsSolver() 25 | @pymc.deterministic 26 | def model_output(value=None, k1=k1, k2=k2): 27 | return re_solver(k1, k2) 28 | # The likelihood term 29 | @pymc.stochastic(observed=True) 30 | def output(value=y_obs, mod_out=model_output, sigma=sigma, gamma=1.): 31 | return gamma * pymc.normal_like(y_obs, mu=mod_out, tau=1/sigma ** 2) 32 | return locals() 33 | 34 | 35 | if __name__ == '__main__': 36 | # Generate the observations 37 | import pickle 38 | re_solver = ReactionKineticsSolver() 39 | k1 = 2 40 | k2 = 4 41 | data = {} 42 | data['y_obs'] = re_solver(k1, k2) 43 | with open('reaction_kinetics_data.pickle', 'wb') as fd: 44 | pickle.dump(data, fd) 45 | -------------------------------------------------------------------------------- /examples/reaction_kinetics_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plots the results of the reaction kinetics example. 3 | """ 4 | 5 | 6 | import sys 7 | import os 8 | sys.path.insert(0, os.path.abspath('..')) 9 | import pysmc 10 | import matplotlib.pyplot as plt 11 | import pickle as pickle 12 | 13 | 14 | if __name__ == '__main__': 15 | with open('reaction_kinetics_particle_approximation.pickle', 'rb') as fd: 16 | p = pickle.load(fd) 17 | print(p.num_particles) 18 | plt.plot(p.k1, p.k2, '.') 19 | plt.show() 20 | -------------------------------------------------------------------------------- /examples/reaction_kinetics_run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Solve the reaction kinetics inverse problem. 3 | """ 4 | 5 | 6 | import reaction_kinetics_model 7 | import sys 8 | import os 9 | sys.path.insert(0, os.path.abspath('..')) 10 | import pysmc 11 | import mpi4py.MPI as mpi 12 | import matplotlib.pyplot as plt 13 | import pickle 14 | 15 | 16 | if __name__ == '__main__': 17 | model = reaction_kinetics_model.make_model() 18 | # Construct the SMC sampler 19 | smc_sampler = pysmc.SMC(model, num_particles=100, 20 | num_mcmc=1, verbose=1, 21 | mpi=mpi, gamma_is_an_exponent=True) 22 | # Initialize SMC at gamma = 0.01 23 | smc_sampler.initialize(0.) 24 | # Move the particles to gamma = 1.0 25 | smc_sampler.move_to(1.) 26 | # Get a particle approximation 27 | p = smc_sampler.get_particle_approximation() 28 | #m = p.mean 29 | #v = p.variance 30 | #if mpi.COMM_WORLD.Get_rank() == 0: 31 | # print m 32 | # print v 33 | #lp = p.allgather() 34 | # Plot a histogram 35 | #pysmc.hist(p, 'mixture') 36 | p_l = p.allgather() 37 | if mpi.COMM_WORLD.Get_rank() == 0: 38 | with open('reaction_kinetics_particle_approximation.pickle', 'wb') as fd: 39 | pickle.dump(p_l, fd, pickle.HIGHEST_PROTOCOL) 40 | -------------------------------------------------------------------------------- /examples/reaction_kinetics_run_nompi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Solve the reaction kinetics inverse problem. 3 | """ 4 | 5 | 6 | import reaction_kinetics_model 7 | import sys 8 | import os 9 | import pymc 10 | sys.path.insert(0, os.path.abspath('..')) 11 | import pysmc 12 | import matplotlib.pyplot as plt 13 | import pickle 14 | 15 | 16 | if __name__ == '__main__': 17 | model = reaction_kinetics_model.make_model() 18 | # Construct the SMC sampler 19 | mcmc = pymc.MCMC(model) 20 | mcmc.use_step_method(pysmc.LognormalRandomWalk, model['k1']) 21 | mcmc.use_step_method(pysmc.LognormalRandomWalk, model['k2']) 22 | mcmc.use_step_method(pysmc.LognormalRandomWalk, model['sigma']) 23 | smc_sampler = pysmc.SMC(mcmc, num_particles=100, 24 | num_mcmc=1, verbose=1, 25 | gamma_is_an_exponent=True) 26 | # Initialize SMC at gamma = 0.01 27 | smc_sampler.initialize(0.0) 28 | # Move the particles to gamma = 1.0 29 | smc_sampler.move_to(1.) 30 | # Get a particle approximation 31 | p = smc_sampler.get_particle_approximation() 32 | print(p.mean) 33 | print(p.variance) 34 | 35 | data = [p.particles[i]['stochastics']['k1'] for i in range(p.num_particles)] 36 | data = np.array(data) 37 | plt.plot(data, np.zeros(data.shape), 'ro', markersize=10) 38 | pysmc.hist(p, 'mixture') 39 | 40 | data = [p.particles[i]['stochastics']['k2'] for i in range(p.num_particles)] 41 | data = np.array(data) 42 | plt.plot(data, np.zeros(data.shape), 'bo', markersize=10) 43 | pysmc.hist(p, 'mixture') 44 | -------------------------------------------------------------------------------- /examples/reaction_kinetics_solver.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _exa_rk: 4 | 5 | +++++++++++++++++ 6 | Reaction Kinetics 7 | +++++++++++++++++ 8 | 9 | We are trying to infer the forward and reverse rates of reaction 10 | (:math:`k_1, k_2`) in a chemical system. The forward model is given by a set 11 | of differential equations: 12 | 13 | .. math:: 14 | \\frac{du}{dt} = -k_1 u + k_2 v, 15 | \\frac{dv}{dt} = k_1 u - k_2 v. 16 | 17 | The initial condition is fixed at :math:`u(0) = 1` and :math:`v(0) = 0`. 18 | """ 19 | 20 | 21 | __all__ = ['ReactionKineticsSolver'] 22 | 23 | 24 | import numpy as np 25 | from scipy.integrate import ode 26 | 27 | 28 | class ReactionKineticsSolver(object): 29 | 30 | """ 31 | Implements the forward model. 32 | """ 33 | 34 | # Observation times 35 | _t = None 36 | 37 | @property 38 | def t(self): 39 | """ 40 | :getter: Get the observation times. 41 | :type: 1D numpy.ndarray 42 | """ 43 | return self._t 44 | 45 | def __init__(self, t=[2, 4, 5, 8, 10]): 46 | """ 47 | Initialize the model. 48 | """ 49 | self._t = np.array(t, dtype='float32') 50 | 51 | def __call__(self, k1, k2): 52 | """ 53 | Evaluate the solver at ``k1`` and ``k2``. 54 | """ 55 | def f(t, y, k1, k2): 56 | return [-k1 * y[0] + k2 * y[1], k1 * y[0] - k2 * y[1]] 57 | def jac(t, y, k1, k2): 58 | return [[-k1, k2], [k1, -k2]] 59 | r = ode(f).set_integrator('dopri5') 60 | r.set_initial_value([1, 0], 0).set_f_params(k1, k2) 61 | dt = 1e-2 62 | y = [] 63 | for t in self.t: 64 | r.integrate(t) 65 | y.append(r.y) 66 | return np.vstack(y).flatten(order='F') 67 | 68 | 69 | if __name__ == '__main__': 70 | """ 71 | This main simply produces the obseved data for the inverse problem. 72 | """ 73 | import cPickle as pickle 74 | re_solver = ReactionKineticsSolver() 75 | # The **real** reaction rates 76 | k1 = 2 77 | k2 = 4 78 | # The **true** response 79 | y = re_solver(k1, k2) 80 | # Add some noise to it 81 | noise = 0.1 82 | y_obs = y + noise * np.random.rand(*y.shape) 83 | # Save these to a file 84 | data = {} 85 | data['k1'] = k1 86 | data['k2'] = k2 87 | data['y'] = y 88 | data['noise'] = noise 89 | data['y_obs'] = y_obs 90 | with open('reaction_kinetics_data.pickle', 'wb') as fd: 91 | pickle.dump(data, fd, pickle.HIGHEST_PROTOCOL) 92 | -------------------------------------------------------------------------------- /examples/simple_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple mixture model to test the capabilities of SMC. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 9/22/2013 9 | """ 10 | 11 | 12 | import pymc 13 | import numpy as np 14 | import math 15 | 16 | 17 | def make_model(): 18 | # The gamma parameter 19 | gamma = 1. 20 | 21 | @pymc.stochastic(dtype=float) 22 | def mixture(value=1., gamma=gamma, pi=[0.2, 0.8], mu=[-2., 3.], 23 | sigma=[0.01, 0.01]): 24 | """ 25 | The log probability of a mixture of normal densities. 26 | 27 | :param value: The point of evaluation. 28 | :type value : float 29 | :param gamma: The parameter characterizing the SMC one-parameter 30 | family. 31 | :type gamma : float 32 | :param pi : The weights of the components. 33 | :type pi : 1D :class:`numpy.ndarray` 34 | :param mu : The mean of each component. 35 | :type mu : 1D :class:`numpy.ndarray` 36 | :param sigma: The standard deviation of each component. 37 | :type sigma : 1D :class:`numpy.ndarray` 38 | """ 39 | # Make sure everything is a numpy array 40 | pi = np.array(pi) 41 | mu = np.array(mu) 42 | sigma = np.array(sigma) 43 | # The number of components in the mixture 44 | n = pi.shape[0] 45 | # pymc.normal_like requires the precision not the variance: 46 | tau = np.sqrt(1. / sigma ** 2) 47 | # The following looks a little bit awkward because of the need for 48 | # numerical stability: 49 | p = np.log(pi) 50 | p += np.array([pymc.normal_like(value, mu[i], tau[i]) 51 | for i in range(n)]) 52 | p = math.fsum(np.exp(p)) 53 | # logp should never be negative, but it can be zero... 54 | if p <= 0.: 55 | return -np.inf 56 | return gamma * math.log(p) 57 | 58 | return locals() 59 | 60 | 61 | def eval_stochastic_variable(var, values): 62 | """ 63 | Evaluate the logarithm of the probability of ``var`` at ``values``. 64 | 65 | :param var : The stochastic variable whose probability should be 66 | evaluated. 67 | :type var : :class:`pymc.Stochastic` 68 | :param values: The points of evalulation. 69 | :type values : list or :class:`numpy.ndarray` 70 | :returns : The logarithm of the probabilities of the variable 71 | at all ``values``. 72 | :rtype : 1D :class:`numpy.ndarray` 73 | """ 74 | n = len(values) 75 | y = np.zeros(n) 76 | for i in range(n): 77 | var.value = values[i] 78 | y[i] = math.exp(var.logp) 79 | return y 80 | 81 | if __name__ == '__main__': 82 | import matplotlib.pyplot as plt 83 | x = np.linspace(-2, 3, 200) 84 | 85 | # Plot the original probability density 86 | m = make_model() 87 | y = eval_stochastic_variable(m['mixture'], x) 88 | plt.plot(x, y, linewidth=2) 89 | plt.xlabel('$x$', fontsize=16) 90 | plt.ylabel('$p(x)$', fontsize=16) 91 | plt.savefig('../doc/source/images/simple_model_pdf.png') 92 | 93 | # Plot some members of the one-parameter SMC family of probability densities 94 | plt.clf() 95 | gammas = [1., 0.7, 0.5, 0.1, 0.05, 0.01] 96 | for gamma in gammas: 97 | # m['mixture'].parents['gamma'] = gamma 98 | m['gamma'] = gamma 99 | y = eval_stochastic_variable(m['mixture'], x) 100 | plt.plot(x, y, linewidth=2) 101 | plt.xlabel('$x$', fontsize=16) 102 | plt.ylabel('$\pi_\gamma(x)$', fontsize=16) 103 | legend_labels = ['$\gamma = %1.2f$' % gamma for gamma in gammas] 104 | plt.legend(legend_labels, loc='upper left') 105 | plt.show() 106 | # plt.savefig('../doc/source/images/simple_model_pdf_family.png') 107 | -------------------------------------------------------------------------------- /examples/simple_model_run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run SMC on the simple model. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 9/28/2013 9 | 10 | """ 11 | 12 | 13 | import simple_model 14 | import pymc 15 | import sys 16 | import os 17 | sys.path.insert(0, os.path.abspath('..')) 18 | import pysmc 19 | import matplotlib.pyplot as plt 20 | import pickle 21 | import numpy as np 22 | 23 | 24 | if __name__ == '__main__': 25 | # Construct the SMC sampler 26 | model = simple_model.make_model() 27 | mcmc = pymc.MCMC(model) 28 | mcmc.use_step_method(pysmc.RandomWalk, model['mixture']) 29 | smc_sampler = pysmc.SMC(mcmc, num_particles=1000, 30 | num_mcmc=1, verbose=4) 31 | # Initialize SMC at gamma = 0.01 32 | smc_sampler.initialize(0.001) 33 | # Move the particles to gamma = 1.0 34 | smc_sampler.move_to(1.) 35 | print(smc_sampler.log_Zs) 36 | # Get a particle approximation 37 | p = smc_sampler.get_particle_approximation() 38 | print(p.mean) 39 | print(p.variance) 40 | # Plot a histogram 41 | data = [p.particles[i]['stochastics']['mixture'] for i in range(p.num_particles)] 42 | data = np.array(data) 43 | plt.plot(data, np.zeros(data.shape), 'ro', markersize=10) 44 | pysmc.hist(p, 'mixture') 45 | plt.show() 46 | -------------------------------------------------------------------------------- /examples/simple_model_run_mpi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run SMC on the simple model. 4 | 5 | Author: 6 | Ilias Bilionis 7 | 8 | Date: 9 | 9/28/2013 10 | 11 | """ 12 | 13 | 14 | import simple_model 15 | import sys 16 | import os 17 | import pymc as pm 18 | sys.path.insert(0, os.path.abspath('..')) 19 | import pysmc as ps 20 | import mpi4py.MPI as mpi 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | 24 | 25 | if __name__ == '__main__': 26 | do_init = not os.path.exists('test_db.pickle') 27 | # Construct the SMC sampler 28 | model = simple_model.make_model() 29 | mcmc = pm.MCMC(model) 30 | mcmc.use_step_method(ps.RandomWalk, model['mixture']) 31 | smc_sampler = ps.SMC(mcmc, num_particles=512, 32 | num_mcmc=1, verbose=1, 33 | mpi=mpi, gamma_is_an_exponent=True, 34 | ess_reduction=0.9, 35 | db_filename='test_db.pickle', 36 | update_db=True) 37 | # Initialize SMC at gamma = 0.01 38 | if do_init: 39 | smc_sampler.initialize(0.001, num_mcmc_per_particle=100) 40 | # Move the particles to gamma = 1.0 41 | smc_sampler.move_to(1.) 42 | # Get a particle approximation 43 | p = smc_sampler.get_particle_approximation().gather() 44 | # Plot a histogram 45 | #if mpi.COMM_WORLD.Get_rank() == 0: 46 | # x = np.linspace(-5, 5, 200) 47 | # y = simple_model.eval_stochastic_variable(model['mixture'], x) 48 | # plt.plot(x, y, linewidth=2) 49 | # ps.hist(p, 'mixture') 50 | # plt.show() 51 | -------------------------------------------------------------------------------- /examples/test_lognormal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the step method when trying to sample a lognormal random variable. 3 | """ 4 | 5 | 6 | import os 7 | import sys 8 | sys.path.insert(0, os.path.abspath('..')) 9 | import pysmc 10 | import pymc 11 | import math 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | 15 | 16 | def make_model(): 17 | #c = pymc.Container([pymc.Lognormal('x', mu=math.log(1.), tau=1.), 18 | # pymc.Lognormal('z', mu=math.log(2.), tau=3.)]) 19 | #x = pymc.Exponential('x', beta=0.1) 20 | x = pymc.Lognormal('x', mu=np.array([math.log(1.), math.log(2.)]), 21 | tau=np.array([1., 3.])) 22 | @pymc.stochastic(observed=True) 23 | def y(value=0.001, x=x): 24 | return pymc.lognormal_like(value, mu=x[0], tau=1.) 25 | return locals() 26 | 27 | 28 | if __name__ == '__main__': 29 | m = make_model() 30 | mcmc = pymc.MCMC(m) 31 | mcmc.assign_step_methods() 32 | print(mcmc.step_method_dict) 33 | mcmc.sample(100000, thin=100, burn=10000) 34 | s = mcmc.step_method_dict[m['x']][0] 35 | print('\n', s.accepted / (s.accepted + s.rejected)) 36 | pymc.Matplot.plot(mcmc) 37 | plt.show() 38 | -------------------------------------------------------------------------------- /examples/true_data: -------------------------------------------------------------------------------- 1 | 1.866507885606724315e+00 2 | 2.740475085017091139e+00 3 | 2.790475185620288290e+00 4 | 1.591504738028788779e+00 5 | 7.680130622515170258e-02 6 | 4.543750582891544210e-01 7 | 8.283730407227198889e-01 8 | 1.085951974233539730e+00 9 | 1.122672110118582978e-01 10 | 5.308306121373320696e-01 11 | 9.158601489019295716e-01 12 | 1.132437310917811635e+00 13 | 1.343241885575042864e-02 14 | 2.063628880834675883e-01 15 | 5.045938472307064382e-01 16 | 8.283356974967712727e-01 17 | -------------------------------------------------------------------------------- /examples/true_locations: -------------------------------------------------------------------------------- 1 | 2.000000000000000111e-01 2 | 1.300000000000000044e-01 3 | -------------------------------------------------------------------------------- /examples/true_noise: -------------------------------------------------------------------------------- 1 | 1.000000000000000056e-01 2 | -------------------------------------------------------------------------------- /pysmc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. moduleauthor:: Ilias Bilionis 3 | 4 | .. _classes: 5 | 6 | ------- 7 | Classes 8 | ------- 9 | Here is complete reference of all the classes included in :mod:`pysmc`. 10 | 11 | .. automodule:: pysmc._mcmc_wrapper 12 | .. autoclass:: pysmc.MCMCWrapper 13 | :members: 14 | 15 | .. automodule:: pysmc._mpi 16 | .. autoclass:: pysmc.DistributedObject 17 | :members: 18 | 19 | .. automodule:: pysmc._particle_approximation 20 | .. autoclass:: pysmc.ParticleApproximation 21 | :members: 22 | 23 | .. automodule:: pysmc._smc 24 | .. autoclass:: pysmc.SMC 25 | :members: 26 | 27 | .. automodule:: pysmc._db 28 | .. autoclass:: pysmc.DataBase 29 | :members: 30 | 31 | .. automodule:: pysmc._step_methods 32 | .. autoclass:: pysmc.LognormalRandomWalk 33 | :members: 34 | 35 | .. _methods: 36 | 37 | ------- 38 | Methods 39 | ------- 40 | 41 | .. automodule:: pysmc._plot 42 | .. autofunction:: pysmc.hist 43 | 44 | .. automodule:: pysmc._misc 45 | .. autofunction:: pysmc.try_to_array 46 | .. autofunction:: pysmc.hist 47 | .. autofunction:: pysmc.make_movie_from_db 48 | .. autofunction:: pysmc.multinomial_resample 49 | .. autofunction:: pysmc.kde 50 | 51 | """ 52 | 53 | 54 | __docformat__ = 'reStructuredText' 55 | 56 | 57 | __all__ = ['MCMCWrapper', 'SMC', 'ParticleApproximation', 'try_to_array', 58 | 'get_var_from_particle_list', 'DataBase', 'DistributedObject', 59 | 'hist', 'make_movie_from_db', 'multinomial_resample', 'kde', 60 | 'LognormalRandomWalk'] 61 | 62 | 63 | from pysmc._misc import * 64 | from pysmc._mpi import * 65 | from pysmc._mcmc_wrapper import * 66 | from pysmc._step_methods import * 67 | from pysmc._particle_approximation import * 68 | from pysmc._db import * 69 | from pysmc._smc import * 70 | from pysmc._plot import * 71 | -------------------------------------------------------------------------------- /pysmc/_db.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _db: 4 | 5 | ++++++++ 6 | DataBase 7 | ++++++++ 8 | 9 | The class :class:`pysmc.DataBase` implements a simple database for dumping 10 | SMC steps. 11 | """ 12 | 13 | 14 | __all__ = ['DataBase', 'SerialDataBase'] 15 | 16 | 17 | import pickle 18 | import os 19 | import warnings 20 | 21 | 22 | class DataBase(object): 23 | 24 | """ 25 | A data base storing the evolution of SMC particles as gamma changes. 26 | 27 | :param gamma_name: The name we are using for gamma. 28 | :type gamma_name: str 29 | :param filename: The desired filename for the output. If ``None`` 30 | then everything is stored on the ram. 31 | :type filename: str 32 | """ 33 | 34 | # The gammas visited so far (list) 35 | _gammas = None 36 | 37 | # The name we used for gamma (str) 38 | _gamma_name = None 39 | 40 | # The particle approximations associated with each gamma (list) 41 | _particle_approximations = None 42 | 43 | # The parameters of each step method 44 | _step_method_params = None 45 | 46 | # The filename you have selected for dumping the data (str) 47 | _filename = None 48 | 49 | # The file handler 50 | _fd = None 51 | 52 | # The Pickler 53 | _pickler = None 54 | 55 | # Last commited particle approximation 56 | _last_commited = None 57 | 58 | @property 59 | def gammas(self): 60 | """ 61 | The list of gammas we have visited. 62 | 63 | :getter: Get the list of gammas we have visited. 64 | :type: list 65 | """ 66 | return self._gammas 67 | 68 | @property 69 | def gamma_name(self): 70 | """ 71 | The name we used for gamma. 72 | 73 | :getter: Get the name we used for gamma. 74 | :type: str 75 | """ 76 | return self._gamma_name 77 | 78 | @property 79 | def particle_approximations(self): 80 | """ 81 | The particle approximations associated with each gamma. 82 | 83 | :getter: Get the particle approximations associated with each gamma. 84 | :type: list 85 | """ 86 | return self._particle_approximations 87 | 88 | @property 89 | def step_method_params(self): 90 | """ 91 | The adaptive scale factors of each step method. 92 | 93 | :getter: Get the adaptive scale factors associated with each 94 | MCMC step method. 95 | :type: list 96 | """ 97 | return self._step_method_params 98 | 99 | @property 100 | def num_gammas(self): 101 | """ 102 | The number of gammas added to the database. 103 | 104 | :getter: Get the number of gammas added to the data base. 105 | :type: int 106 | """ 107 | return len(self._gammas) 108 | 109 | @property 110 | def filename(self): 111 | """ 112 | The filename you have selected for dumping the data. 113 | 114 | :getter: Get the filename you have selected for dumping the data. 115 | :type: str 116 | """ 117 | return self._filename 118 | 119 | @property 120 | def write_to_disk(self): 121 | """ 122 | ``True`` if the class writes data to disk, ``False`` otherwise. 123 | """ 124 | return not self.filename is None 125 | 126 | @property 127 | def gamma(self): 128 | """ 129 | The current gamma of the database. 130 | 131 | :getter: Get the current gamma of the database. 132 | :type: unknown 133 | """ 134 | if self.num_gammas == 0: 135 | raise RuntimeError( 136 | 'The db is empty!') 137 | return self.gammas[-1] 138 | 139 | @property 140 | def particle_approximation(self): 141 | """ 142 | The current particle approximation of the database. 143 | 144 | :getter: Get the current particle approximation of the database. 145 | :type: unknown 146 | """ 147 | return self._particle_approximations[-1] 148 | 149 | @property 150 | def step_method_param(self): 151 | return self._step_method_params[-1] 152 | 153 | def __init__(self, gamma_name=None, filename=None): 154 | """ 155 | Initialize the object. 156 | 157 | See doc string for parameters. 158 | """ 159 | if gamma_name is not None: 160 | self._initialize(gamma_name, filename) 161 | 162 | def _initialize(self, gamma_name, filename): 163 | """ 164 | Initialize the object given a name for gamma and a filename. 165 | """ 166 | if not isinstance(gamma_name, str): 167 | raise TypeError( 168 | 'The name of the \'gamma\' parameter must be a str object.') 169 | self._gamma_name = gamma_name 170 | if filename is not None: 171 | if not isinstance(filename, str): 172 | raise TypeError( 173 | 'The filename has to be a str object.') 174 | filename = os.path.abspath(filename) 175 | if os.path.exists(filename): 176 | warnings.warn( 177 | 'Filename already exists. I will overwrite it!\n' 178 | + ' Use pysmc.Database.load(filename) if your intentions are to' 179 | + ' append the file with new data!') 180 | self._filename = filename 181 | if self.write_to_disk: 182 | with open(self.filename, 'wb') as fd: 183 | pickle.dump(gamma_name, fd, 184 | protocol=pickle.HIGHEST_PROTOCOL) 185 | self._gammas = [] 186 | self._particle_approximations = [] 187 | self._step_method_params = [] 188 | self._last_commited = 0 189 | 190 | def add(self, gamma, particle_approximation, 191 | step_method_params): 192 | """ 193 | Add the ``particle_approximation`` corresponding to ``gamma`` to the 194 | database. 195 | 196 | :param gamma: The gamma parameter. 197 | :type gamma: any 198 | :param particle_approximation: particle_approximation 199 | :type particle_approximation: any 200 | """ 201 | self._gammas.append(gamma) 202 | self._particle_approximations.append(particle_approximation) 203 | self._step_method_params.append(step_method_params) 204 | 205 | def _dump_part_of_list(self, idx, values, fd): 206 | """ 207 | Dump part of a list to fd. 208 | """ 209 | for i in range(len(idx)): 210 | pickle.dump(values[idx[i]], fd, 211 | protocol=pickle.HIGHEST_PROTOCOL) 212 | 213 | def commit(self): 214 | """ 215 | Commit everything we have so far to the database. 216 | """ 217 | if self.write_to_disk: 218 | idx = list(range(self._last_commited, self.num_gammas)) 219 | if len(idx) == 0: 220 | return 221 | with open(self.filename, 'ab') as fd: 222 | self._dump_part_of_list(idx, self.gammas, fd) 223 | self._dump_part_of_list(idx, self.particle_approximations, fd) 224 | self._dump_part_of_list(idx, self.step_method_params, fd) 225 | self._last_commited += len(idx) 226 | 227 | @staticmethod 228 | def load(filename): 229 | """ 230 | This is a static method. It loads a database from ``filename``. 231 | """ 232 | filename = os.path.abspath(filename) 233 | with open(filename, 'rb') as fd: 234 | try: 235 | gamma_name = pickle.load(fd) 236 | except: 237 | raise RuntimeError('File %s: Not a valid database!' % filename) 238 | gammas = [] 239 | particle_approximations = [] 240 | step_method_params = [] 241 | while True: 242 | try: 243 | gammas.append(pickle.load(fd)) 244 | particle_approximations.append(pickle.load(fd)) 245 | step_method_params.append(pickle.load(fd)) 246 | except EOFError: 247 | break 248 | last_commited = len(gammas) 249 | db = DataBase() 250 | db._gamma_name = gamma_name 251 | db._gammas = gammas 252 | db._particle_approximations = particle_approximations 253 | db._step_method_params = step_method_params 254 | db._last_commited = last_commited 255 | db._filename = filename 256 | return db 257 | 258 | 259 | class SerialDataBase(object): 260 | 261 | """ 262 | Just like DataBase but stores in memory only the latest particle 263 | approximation. 264 | 265 | :param gamma_name: The name we are using for gamma. 266 | :type gamma_name: str 267 | :param filename: The desired filename for the output. If ``None`` 268 | then everything is stored on the ram. 269 | :type filename: str 270 | """ 271 | 272 | # The last gamma 273 | _gamma = None 274 | 275 | # The name we used for gamma (str) 276 | _gamma_name = None 277 | 278 | # The particle approximations associated with gamma 279 | _particle_approximation = None 280 | 281 | # The parameters of each step method 282 | _step_method_param = None 283 | 284 | # The filename you have selected for dumping the data (str) 285 | _filename = None 286 | 287 | # The file handler 288 | _fd = None 289 | 290 | # The Pickler 291 | _pickler = None 292 | 293 | # Last commited particle approximation 294 | _last_commited = None 295 | 296 | # The total number of gammas 297 | _num_gammas = None 298 | 299 | @property 300 | def gamma_name(self): 301 | """ 302 | The name we used for gamma. 303 | 304 | :getter: Get the name we used for gamma. 305 | :type: str 306 | """ 307 | return self._gamma_name 308 | 309 | @property 310 | def num_gammas(self): 311 | """ 312 | The number of gammas added to the database. 313 | 314 | :getter: Get the number of gammas added to the data base. 315 | :type: int 316 | """ 317 | return self._num_gammas 318 | 319 | @property 320 | def filename(self): 321 | """ 322 | The filename you have selected for dumping the data. 323 | 324 | :getter: Get the filename you have selected for dumping the data. 325 | :type: str 326 | """ 327 | return self._filename 328 | 329 | @property 330 | def gamma(self): 331 | """ 332 | The current gamma of the database. 333 | 334 | :getter: Get the current gamma of the database. 335 | :type: unknown 336 | """ 337 | return self._gamma 338 | 339 | @property 340 | def particle_approximation(self): 341 | """ 342 | The current particle approximation of the database. 343 | 344 | :getter: Get the current particle approximation of the database. 345 | :type: unknown 346 | """ 347 | return self._particle_approximation 348 | 349 | @property 350 | def step_method_param(self): 351 | return self._step_method_param 352 | 353 | def __init__(self, gamma_name=None, filename=None): 354 | """ 355 | Initialize the object. 356 | 357 | See doc string for parameters. 358 | """ 359 | if gamma_name is not None: 360 | self._initialize(gamma_name, filename) 361 | self._num_gammas = 0 362 | 363 | def _initialize(self, gamma_name, filename): 364 | """ 365 | Initialize the object given a name for gamma and a filename. 366 | """ 367 | if not isinstance(gamma_name, str): 368 | raise TypeError( 369 | 'The name of the \'gamma\' parameter must be a str object.') 370 | self._gamma_name = gamma_name 371 | if filename is not None: 372 | if not isinstance(filename, str): 373 | raise TypeError( 374 | 'The filename has to be a str object.') 375 | filename = os.path.abspath(filename) 376 | if os.path.exists(filename): 377 | warnings.warn( 378 | 'Filename already exists. I will overwrite it!\n' 379 | + ' Use pysmc.Database.load(filename) if your intentions are to' 380 | + ' append the file with new data!') 381 | self._filename = filename 382 | with open(self.filename, 'wb') as fd: 383 | pickle.dump(gamma_name, fd, protocol=pickle.HIGHEST_PROTOCOL) 384 | 385 | def add(self, gamma, particle_approximation, 386 | step_method_params): 387 | """ 388 | Add the ``particle_approximation`` corresponding to ``gamma`` to the 389 | database. 390 | 391 | :param gamma: The gamma parameter. 392 | :type gamma: any 393 | :param particle_approximation: particle_approximation 394 | :type particle_approximation: any 395 | """ 396 | self._gamma = gamma 397 | self._particle_approximations = particle_approximation 398 | self._step_method_param = step_method_params 399 | self._num_gammas += 1 400 | 401 | def _dump_part_of_list(self, idx, values, fd): 402 | """ 403 | Dump part of a list to fd. 404 | """ 405 | for i in range(len(idx)): 406 | pickle.dump(values[idx[i]], fd, 407 | protocol=pickle.HIGHEST_PROTOCOL) 408 | 409 | def commit(self): 410 | """ 411 | Commit everything we have so far to the database. 412 | """ 413 | with open(self.filename, 'ab') as fd: 414 | pickle.dump(self.gamma, fd, protocol=pickle.HIGHEST_PROTOCOL) 415 | pickle.dump(self.particle_approximation, fd, 416 | protocol=pickle.HIGHEST_PROTOCOL) 417 | pickle.dump(self.step_method_param, fd, 418 | protocol=pickle.HIGHEST_PROTOCOL) 419 | 420 | @staticmethod 421 | def load(filename, go_to_last=True): 422 | """ 423 | This is a static method. It loads a database from ``filename``. 424 | """ 425 | filename = os.path.abspath(filename) 426 | fd = open(filename, 'rb') 427 | try: 428 | gamma_name = pickle.load(fd) 429 | except: 430 | raise RuntimeError('File %s: Not a valid database!' % filename) 431 | gamma = pickle.load(fd) 432 | particle_approximation = pickle.load(fd) 433 | step_method_param = pickle.load(fd) 434 | db = SerialDataBase() 435 | db._fd = fd 436 | if go_to_last: 437 | db.go_to_last() 438 | return db 439 | 440 | 441 | def __next__(self): 442 | """ 443 | Read the next state from the file opened file. 444 | """ 445 | if self._fd is None: 446 | return False 447 | try: 448 | gamma = pickle.load(self._fd) 449 | particle_approximation = pickle.load(self._fd) 450 | step_method_param = pickle.load(self._fd) 451 | except EOFError: 452 | self._fd.close() 453 | self._fd = None 454 | return False 455 | if self.num_gammas >= 1: 456 | del self._gamma 457 | del self._particle_approximation 458 | del self._step_method_param 459 | self._gamma = gamma 460 | self._particle_approximation = particle_approximation 461 | self._step_method_param = step_method_param 462 | self._num_gammas += 1 463 | return True 464 | 465 | def go_to_last(self): 466 | """ 467 | Go through all the values of the database until you reach the last one. 468 | """ 469 | while next(self): 470 | pass 471 | -------------------------------------------------------------------------------- /pysmc/_mcmc_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _mcmc_wrapper: 4 | 5 | +++++++++++ 6 | MCMCWrapper 7 | +++++++++++ 8 | 9 | :class:`pysmc.MCMCWrapper` 10 | provides exactly the same functionality as :class:`pymc.MCMC`. 11 | The only new thing is that it has the ability to get and set the current 12 | state of MCMC from a dictionary describing the state. Basically, you should 13 | not construct this class your self, :class:`pymc.SMC` will do it 14 | automatically. 15 | 16 | .. note:: It does not inherit from :class:`pymc.MCMC`. It simply stores 17 | a reference to a :class:`pymc.MCMC` object internally. 18 | 19 | Here is a complete reference of the public members: 20 | """ 21 | 22 | 23 | __all__ = ['MCMCWrapper'] 24 | 25 | 26 | from pymc import MCMC 27 | import itertools 28 | import warnings 29 | 30 | 31 | class MCMCWrapper(object): 32 | 33 | """ 34 | This is a wrapper class for :class:`pymc.MCMC`. 35 | 36 | :param mcmc_sampler: The underlying MCMC sampler. If ``None``, 37 | then it **must** be specified before using 38 | an object of this class. 39 | :type mcmc_sampler: :class:`pymc.MCMC` 40 | """ 41 | 42 | # The underlying pymc.MCMC object. 43 | _mcmc_sampler = None 44 | 45 | # The step methods 46 | _step_methods = None 47 | 48 | @property 49 | def mcmc_sampler(self): 50 | """ 51 | The underlying :class:`pymc.MCMC` object. 52 | 53 | :getter: Get the underlying MCMC object. 54 | :setter: Set the underlying MCMC object. 55 | :raises: :exc:`exceptions.TypeError` 56 | :type: :class:`pymc.MCMC` 57 | """ 58 | return self._mcmc_sampler 59 | 60 | @mcmc_sampler.setter 61 | def mcmc_sampler(self, value): 62 | if not isinstance(value, MCMC): 63 | raise TypeError('You must provide a pymc.MCMC object!') 64 | value.assign_step_methods() 65 | self._mcmc_sampler = value 66 | 67 | 68 | @property 69 | def nodes(self): 70 | """The nodes of the model.""" 71 | return self.mcmc_sampler.nodes 72 | 73 | @property 74 | def stochastics(self): 75 | """The stochastic variables of the model.""" 76 | return self.mcmc_sampler.stochastics 77 | 78 | @property 79 | def deterministics(self): 80 | """The deterministic variables of the model.""" 81 | return self.mcmc_sampler.deterministics 82 | 83 | @property 84 | def db(self): 85 | """The database of the MCMC sampler.""" 86 | return self.mcmc_sampler.db 87 | 88 | @property 89 | def logp(self): 90 | """ 91 | The log of the probability of the current state of the MCMC sampler. 92 | """ 93 | return self.mcmc_sampler.logp 94 | 95 | @property 96 | def step_methods(self): 97 | """The step methods of the MCMC sampler.""" 98 | return self._step_methods 99 | 100 | def __init__(self, mcmc_sampler=None, comm=None): 101 | """See doc of class.""" 102 | self.mcmc_sampler = mcmc_sampler 103 | if comm is None: 104 | self._step_methods = self.mcmc_sampler.step_methods 105 | return 106 | rank = comm.Get_rank() 107 | if rank == 0: 108 | names = [var.__name__ for var in self.stochastics] 109 | names.sort() 110 | else: 111 | names = None 112 | names = comm.bcast(names) 113 | sms = [] 114 | for name in names: 115 | for var in self.mcmc_sampler.step_method_dict.keys(): 116 | if var.__name__ == name: 117 | sms.append(self.mcmc_sampler.step_method_dict[var]) 118 | sms_flat = [] 119 | for sm in sms: 120 | for s in sm: 121 | sms_flat.append(s) 122 | self._step_methods = sms_flat 123 | 124 | def get_state(self): 125 | """ 126 | Get a dictionary describing the state of the sampler. 127 | 128 | Keep in mind that we do not copy the internal parameters of the 129 | sampler (i.e., the step methods, their parameters, etc.). We 130 | only copy the values of all the Stochastics and all the 131 | Deterministics. On contrast pymc.MCMC.get_state() copies the 132 | internal parameters and the Stochastics. It does not copy the 133 | Deterministics. The deterministics are needed because in most 134 | of our examples they are going to be very expensive to 135 | revaluate. 136 | 137 | :returns: A dictionary ``state`` containing the current state of 138 | MCMC. The keys of the dictionary are as follows: 139 | 140 | - ``state['stochastics']``: A dictionary keeping the values of 141 | all stochastic variables. 142 | - ``state['deterministics']``: A dictionary keeping the values 143 | of all deterministic variables. 144 | 145 | :rtype: :class:`dict` 146 | 147 | """ 148 | state = dict(stochastics={}, deterministics={}) 149 | 150 | # The state of each stochastic parameter 151 | for s in self.stochastics: 152 | state['stochastics'][s.__name__] = s.value 153 | 154 | # The state of each deterministic 155 | for d in self.deterministics: 156 | state['deterministics'][d.__name__] = d.value 157 | 158 | return state 159 | 160 | def set_state(self, state): 161 | """ 162 | Set the state of the sampler. 163 | 164 | :parameter state: A dictionary describing the state of the 165 | sampler. Look at 166 | :meth:`pysmc.MCMCWrapper.get_state()` for the 167 | appropriate format. 168 | :type state: :class:`dict` 169 | """ 170 | # Restore stochastic parameters state 171 | stoch_state = state.get('stochastics', {}) 172 | for sm in self.stochastics: 173 | try: 174 | sm.value = stoch_state[sm.__name__] 175 | except: 176 | warnings.warn( 177 | 'Failed to restore state of stochastic %s from %s backend' % 178 | (sm.__name__, self.db.__name__)) 179 | 180 | # Restore the deterministics 181 | det_state = state.get('deterministics', {}) 182 | for dm in self.deterministics: 183 | try: 184 | dm._value.force_cache(det_state[dm.__name__]) 185 | except: 186 | warnings.warn( 187 | 'Failed to restore state of deterministic %s from %s backend' % 188 | (dm.__name__, self.db.__name__)) 189 | 190 | def get_params(self, comm=None): 191 | """ 192 | Get a list of dictionaries describing the parameters of each 193 | step method. 194 | """ 195 | states = [] 196 | for sm in self.step_methods: 197 | states.append(sm.get_params(comm=comm)) 198 | return states 199 | 200 | def set_params(self, states): 201 | """ 202 | Set the parameters of each step method. 203 | """ 204 | for sm, state in itertools.izip(self.step_methods, states): 205 | sm.set_params(state) 206 | 207 | def sample(self, iter, burn=0, thin=None, tune_interval=1, 208 | tune_throughout=False, save_interval=None, 209 | burn_till_tuned=False, stop_tuning_after=0, 210 | verbose=0, progress_bar=False): 211 | """ 212 | Sample ``iter`` times from the model. 213 | 214 | This is basically exactly the same call as 215 | :meth:`pymc.MCMC.sample()` but with defaults that are more suitable 216 | for :class:`pysmc.SMC`. For example, you typically do not want to allow 217 | :meth:`pymc.MCMC.sample()` 218 | to tune the parameters of the step methods. 219 | It is :class:`SMC` that should do this. 220 | This is because the transition kernels need to retain their 221 | invariance properties. They can't have parameters that change 222 | on the fly. 223 | 224 | :param iter: The number of iterations to be run. 225 | :type iter: int 226 | :param burn: The number of samples to be burned before we 227 | start storing things to the database. There 228 | is no point in :class:`pysmc.SMC` to have this 229 | equal to anything else than 0. 230 | :type burn: int 231 | :param thin: Store to the database every ``thin`` 232 | samples. If ``None`` then we just store the 233 | last sample. That is the method will set 234 | ``thin = iter``. 235 | :type thin: int 236 | :param tune_interval: The tuning interval. The default is not to 237 | tune anything. Do not change it. 238 | :type tune_interval: int 239 | :param tune_throughout: Tune during all the samples we take. The 240 | default is ``False``. Do not change it. 241 | :type tune_throughout: bool 242 | :param burn_till_tuned: Burn samples until the parameters get tuned. 243 | The default is no. Do not change it. 244 | :type burn_till_tuned: bool 245 | :param stop_tuning_after: Stop tuning after a certain number of 246 | iterations. No point in setting this. 247 | :type stop_tuning_after: int 248 | :param verbose: How much verbosity you like. 249 | :type verbose: int 250 | :param progress_bar: Show the progress bar or not. 251 | :type progress_bar: bool 252 | """ 253 | if thin is None: 254 | thin = iter 255 | self.mcmc_sampler.sample(iter, burn=burn, thin=thin, 256 | tune_interval=tune_interval, 257 | tune_throughout=tune_throughout, 258 | save_interval=save_interval, 259 | burn_till_tuned=burn_till_tuned, 260 | stop_tuning_after=stop_tuning_after, 261 | verbose=verbose, 262 | progress_bar=progress_bar) 263 | 264 | def draw_from_prior(self): 265 | """ 266 | Draw from the prior of the model. 267 | 268 | :raises: :exc:`exceptions.AttributeError` if the action is not 269 | possible. 270 | """ 271 | self.mcmc_sampler.draw_from_prior() 272 | -------------------------------------------------------------------------------- /pysmc/_misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _misc: 4 | 5 | ++++++++++++++++++++++ 6 | Miscellaneous routines 7 | ++++++++++++++++++++++ 8 | 9 | This contains methods that do not fit into the other sections of the 10 | reference manual. 11 | 12 | """ 13 | 14 | 15 | __all__ = ['try_to_array', 'get_var_from_particle_list', 16 | 'multinomial_resample', 'kde'] 17 | 18 | 19 | import numpy as np 20 | from scipy.stats import gaussian_kde 21 | 22 | 23 | def try_to_array(data): 24 | """ 25 | Try to turn the data into a numpy array. 26 | 27 | :returns: If possible, a :class:`numpy.ndarray` containing the 28 | data. Otherwise, it just returns the data. 29 | :rtype: :class:`numpy.ndarray` or ``type(data)`` 30 | """ 31 | try: 32 | return np.array(data) 33 | except: 34 | return data 35 | 36 | 37 | def get_var_from_particle_list(particle_list, var_name, type_of_var): 38 | """ 39 | Get the particles pertaining to variable ``var_name`` of type 40 | ``type_of_var``. 41 | 42 | :param var_name: The name of the variable whose particles you want to 43 | get. 44 | :type var_name: str 45 | :param type_of_var: The type of variables you want to get. This can be 46 | either 'stochastics' or 'deterministics' if you are 47 | are using :mod:`pymc`. The default type is 'stochastics'. 48 | However, I do not restrict its value, in case you 49 | would like to define other types by extending 50 | :mod:`pymc`. 51 | :type type_of_var: str 52 | """ 53 | data = [particle_list[i][type_of_var][var_name] 54 | for i in range(len(particle_list))] 55 | return try_to_array(data) 56 | 57 | 58 | def multinomial_resample(p): 59 | """ 60 | Sample the multinomial according to ``p``. 61 | 62 | :param p: A numpy array of positive numbers that sum to one. 63 | :type p: 1D :class:`numpy.ndarray` 64 | :returns: A set of indices sampled according to p. 65 | :rtype: 1D :class:`numpy.ndarray` of int 66 | """ 67 | p = np.array(p) 68 | assert p.ndim == 1 69 | assert (p >= 0.).all() 70 | births = np.random.multinomial(p.shape[0], p) 71 | idx_list = [] 72 | for i in range(p.shape[0]): 73 | idx_list += [i] * births[i] 74 | return np.array(idx_list, dtype='i') 75 | 76 | 77 | def kde(particle_approximation, var_name): 78 | """ 79 | Construct a kernel density approximation of the probability density 80 | of ``var_name`` of ``particle_approximation``. 81 | 82 | :param particle_approximation: A particle approximation. 83 | :type particle_approximation: :class:`pysmc.ParticleApproximation` 84 | :returns: A kernel density approximation. 85 | :rtype: :class:`scipy.stats.gaussian_kde` 86 | """ 87 | x = getattr(particle_approximation, var_name) 88 | x = np.atleast_2d(x).T 89 | w = particle_approximation.weights 90 | idx = multinomial_resample(w) 91 | return gaussian_kde(x[idx, :].T) 92 | -------------------------------------------------------------------------------- /pysmc/_mpi.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. _distributed_object: 3 | 4 | +++++++++++++++++ 5 | DistributedObject 6 | +++++++++++++++++ 7 | 8 | The :class:`pysmc.DistributedObject` class implements some basic 9 | functionality that is used by every distributed object in :mod:`pysmc`. 10 | 11 | Here follows its complete reference: 12 | 13 | """ 14 | 15 | 16 | __docformat__ = 'reStructuredText' 17 | 18 | 19 | __all__ = ['DistributedObject'] 20 | 21 | 22 | class DistributedObject(object): 23 | 24 | """ 25 | This is a class that represents an object that is (potentially) 26 | distributed in parallel. 27 | """ 28 | 29 | # The mpi class 30 | _mpi = None 31 | 32 | # The communicator 33 | _comm = None 34 | 35 | # The rank 36 | _rank = None 37 | 38 | # The size 39 | _size = None 40 | 41 | @property 42 | def mpi(self): 43 | """ 44 | The MPI class. 45 | 46 | :getter: Get the MPI class. 47 | :type: :class:`mpi4py.MPI` 48 | """ 49 | return self._mpi 50 | 51 | @property 52 | def comm(self): 53 | """ 54 | The MPI communicator. 55 | 56 | :getter: Get the MPI communicator. 57 | :type: :class:`mpi4py.COMM` 58 | """ 59 | return self._comm 60 | 61 | @property 62 | def size(self): 63 | """ 64 | The size of the MPI pool. 65 | 66 | :getter: Get the size of MPI pool. 67 | :type: int 68 | """ 69 | return self._size 70 | 71 | @property 72 | def rank(self): 73 | """ 74 | The rank of this process. 75 | 76 | :getter: Get the rank of this process. 77 | :type: int 78 | """ 79 | return self._rank 80 | 81 | @property 82 | def use_mpi(self): 83 | """ 84 | Check if MPI is being used. 85 | 86 | :returns: ``True`` if MPI is used and ``False`` otherwise. 87 | :rtype: bool 88 | """ 89 | return self.comm is not None 90 | 91 | def __init__(self, mpi=None, comm=None): 92 | """ 93 | Initialize the object. 94 | 95 | See docstring for full details. 96 | """ 97 | self._mpi = mpi 98 | if self.mpi is not None and comm is None: 99 | comm = self.mpi.COMM_WORLD 100 | self._comm = comm 101 | if self.use_mpi: 102 | self._rank = self.comm.Get_rank() 103 | self._size = self.comm.Get_size() 104 | else: 105 | self._rank = 0 106 | self._size = 1 107 | -------------------------------------------------------------------------------- /pysmc/_particle_approximation.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _particle_approx: 4 | 5 | +++++++++++++++++++++ 6 | ParticleApproximation 7 | +++++++++++++++++++++ 8 | 9 | :class:`pysmc.ParticleApproximation` is a class that implements a 10 | a particle approximation. 11 | 12 | Here is the complete reference of the public members: 13 | 14 | """ 15 | 16 | 17 | __all__ = ['ParticleApproximation'] 18 | 19 | 20 | from pysmc._mpi import DistributedObject 21 | from pysmc._misc import get_var_from_particle_list 22 | import numpy as np 23 | import warnings 24 | from copy import deepcopy 25 | import math 26 | 27 | 28 | class ParticleApproximation(DistributedObject): 29 | 30 | """ 31 | Initialize a particle approximation. 32 | 33 | If :math:`x` denotes collectively all the variables involved in the 34 | particle approximation, then this object represents :math:`p(x)` as 35 | discussed in the :ref:`tutorial`. 36 | 37 | **Base class:** :class:`pysmc.DistributedObject` 38 | 39 | :param log_w: The logarithms of the weights of the particle 40 | approximation. 41 | :type log_w: 1D :class:`numpy.ndarray` 42 | :param particles: The particles. 43 | :type particles: list of dict 44 | :param mpi: Specify this if you are creating a distributed particle 45 | approximation. 46 | :type mpi: :class:`mpi4py.MPI` 47 | :param comm: The MPI communicator. 48 | :type comm: :class:`mpi4py.COMM` 49 | 50 | .. note:: 51 | 52 | When creating a distributed object, the particles must already be 53 | scattered. 54 | 55 | """ 56 | # The logarithms of the weights 57 | _log_w = None 58 | 59 | # The weights of the approximation 60 | _weights = None 61 | 62 | # The particles as returned from pysmc.SMC 63 | _particles = None 64 | 65 | # The mean of the approximation 66 | _mean = None 67 | 68 | # The variance of the approximation 69 | _variance = None 70 | 71 | @property 72 | def log_w(self): 73 | """ 74 | The logarithms of the weights of the particle approximation. 75 | 76 | :getter: Get the logarithms of the weights of the particle 77 | approximation. 78 | :type: 1D :class:`numpy.ndarray` 79 | """ 80 | return self._log_w 81 | 82 | @property 83 | def weights(self): 84 | """ 85 | The weights of the particle approximation. 86 | 87 | :getter: Get the weights of the particle approximation. 88 | :type: 1D :class:`numpy.ndarray` 89 | """ 90 | return self._weights 91 | 92 | @property 93 | def particles(self): 94 | """ 95 | The particles of the particle approximation. 96 | 97 | :getter: Get the particles of the particle approximation. 98 | :type: A list of whatever types the approximation has. 99 | """ 100 | return self._particles 101 | 102 | @property 103 | def my_num_particles(self): 104 | """ 105 | The number of particles owned by this process. 106 | 107 | :getter: Get the number of particles owned by this process. 108 | :type: int 109 | """ 110 | return len(self.particles) 111 | 112 | @property 113 | def num_particles(self): 114 | """ 115 | The number of particles. 116 | 117 | :getter: Get the number of particles. 118 | :type: int 119 | """ 120 | return self.my_num_particles * self.size 121 | 122 | @property 123 | def mean(self): 124 | """ 125 | The mean of the variables of all types of the particle approximation. 126 | 127 | The mean of a variable :math:`x` is computed as: 128 | 129 | .. math:: 130 | m(x) := \\sum_{j=1}^N w^{(j)} x^{(j)}. 131 | :label: x_mean 132 | 133 | :getter: Get the mean of the particles. 134 | :type: dict 135 | """ 136 | self.compute_all_means() 137 | return self._mean 138 | 139 | @property 140 | def variance(self): 141 | """ 142 | The variance of all the variables of all types of the particle 143 | approximation. 144 | 145 | The variance of a variable :math:`x` is computed as: 146 | 147 | .. math:: 148 | v(x) := \\sum_{j=1}^N w^{(j)} \\left(x^{(j)}\\right)^2 - m^2(x), 149 | :label: x_var 150 | 151 | where :math:`m(x)` is given in :eq:`x_mean`. 152 | 153 | :getter: Get the variance of all particles. 154 | :type: dict 155 | """ 156 | self.compute_all_variances() 157 | return self._variance 158 | 159 | def _fix_particles_of_type_and_name(self, type_of_var, var_name): 160 | """ 161 | Expose the particles themselves. 162 | """ 163 | var_data = get_var_from_particle_list(self.particles, var_name, 164 | type_of_var=type_of_var) 165 | if (var_data.ndim == 2 and (var_data.shape[0] == 1 166 | or var_data.shape[1] == 0)): 167 | var_data = var_data.flatten() 168 | setattr(self, var_name, var_data) 169 | getattr(self, type_of_var)[var_name] = var_data 170 | self._mean[type_of_var][var_name] = None 171 | self._variance[type_of_var][var_name] = None 172 | 173 | def _fix_particles_of_type(self, type_of_var): 174 | """ 175 | Expose the dictionary of the type of vars. 176 | """ 177 | setattr(self, type_of_var, dict()) 178 | self._mean[type_of_var] = {} 179 | self._variance[type_of_var] = {} 180 | for var_name in list(self.particles[0][type_of_var].keys()): 181 | self._fix_particles_of_type_and_name(type_of_var, var_name) 182 | 183 | def _fix_particles(self): 184 | """ 185 | Fix the local variables based on the particles stored locally. 186 | """ 187 | self._mean = dict() 188 | self._variance = dict() 189 | for type_of_var in list(self.particles[0].keys()): 190 | self._fix_particles_of_type(type_of_var) 191 | 192 | def __init__(self, log_w=None, particles=None, mpi=None, comm=None): 193 | """ 194 | Initialize the particle approximation. 195 | 196 | See the doc of this class for further details. 197 | """ 198 | super(ParticleApproximation, self).__init__(mpi=mpi, comm=comm) 199 | if particles is not None: 200 | self._particles = particles[:] 201 | if log_w is None: 202 | log_w = (np.ones(self.my_num_particles) * 203 | (-math.log(self.num_particles))) 204 | self._log_w = log_w[:] 205 | self._weights = np.exp(log_w) 206 | self._fix_particles() 207 | 208 | def __getstate__(self): 209 | """ 210 | Get the state of the object so that it can be stored. 211 | """ 212 | state = dict() 213 | state['log_w'] = self.log_w 214 | state['particles'] = self.particles 215 | return state 216 | 217 | def __setstate__(self, state): 218 | """ 219 | Set the state of the object. 220 | """ 221 | self.__init__(log_w=state['log_w'], 222 | particles=state['particles']) 223 | 224 | def _check_if_valid_type_of_var(self, type_of_var): 225 | """ 226 | Check if ``type_of_var`` is a valid type of variable. 227 | """ 228 | if type_of_var not in self.particles[0]: 229 | raise RuntimeError( 230 | 'The particles do not have a \'%s\' type of variables!' % type_of_var) 231 | 232 | def _check_if_valid_var(self, var_name, type_of_var): 233 | """ 234 | Check if ``var_name`` of type ``type_of_var`` exists. 235 | """ 236 | self._check_if_valid_type_of_var(type_of_var) 237 | if var_name not in self.particles[0][type_of_var]: 238 | raise RuntimeError( 239 | 'The particles do not have a \'%s\' variable of type \'%s\'!' 240 | % (var_name, type_of_var)) 241 | 242 | def get_particle_approximation_of(self, func, var_name, 243 | type_of_var='stochastics', 244 | func_name='func'): 245 | """ 246 | Returns the particle approximation of a function of ``var_name`` 247 | variable of type ``type_of_var`` of the particle approximation. 248 | 249 | Let the variable and the function we are referring to be :math:`x` and 250 | :math:`f(x)`, respectively. Then, let :math:`y = f(x)` denote the 251 | induced random variable when we pass :math:`x` through the function. 252 | The method returns the following particle approximation to the 253 | probability density of :math:`y`: 254 | 255 | .. math:: 256 | p(y) \\approx \\sum_{j=1}^N w^{(j)} 257 | \\delta\\left(y - f\\left(x^{(j)} \\right)\\right) 258 | 259 | :param func: A function of the desired variable. 260 | :type func: function 261 | :param var_name: The name of the desired variable. 262 | :type var_name: str 263 | :param type_of_var: The type of the variable. 264 | :type type_of_var: str 265 | :param func_name: A name for the function. The new variable will be 266 | named ``func_name + '_' + var_name``. 267 | :type func_name: str 268 | :returns: A particle approximation representing the random 269 | variable ``func(var_name)``. 270 | :rtype: :class:`pysmc.ParticleApproximation` 271 | """ 272 | self._check_if_valid_var(var_name, type_of_var) 273 | func_var_name = func_name + '_' + var_name 274 | weights = self.weights 275 | particles = [dict() for i in range(self.my_num_particles)] 276 | for i in range(self.my_num_particles): 277 | particles[i][type_of_var] = dict() 278 | func_part_i = func(self.particles[i][type_of_var][var_name]) 279 | particles[i][type_of_var][func_var_name] = func_part_i 280 | return ParticleApproximation(weights=weights, particles=particles) 281 | 282 | def get_mean_of_func(self, func, var_name, type_of_var): 283 | """ 284 | Get the mean of the ``func`` applied on ``var_name`` which is of type 285 | ``type_of_var``. 286 | 287 | Let the variable and the function we are referring to be :math:`x` and 288 | :math:`f(x)`, respectively. Then the method computes and returns: 289 | 290 | .. math:: 291 | \sum_{j=1}^Nw^{(j)}f\left(x^{(j)}\\right). 292 | 293 | :param func: A function of one variable. 294 | :type func: function 295 | :param var_name: The name of the variable. 296 | :type var_name: str 297 | :param type_of_var: The type of the variable. 298 | :type type_of_var: str 299 | :returns: The mean of the random variable :math:`y = f(x)`. 300 | :rtype: unknown 301 | """ 302 | self._check_if_valid_var(var_name, type_of_var) 303 | res = 0. 304 | for i in range(self.my_num_particles): 305 | res += (self.weights[i] * 306 | func(self.particles[i][type_of_var][var_name])) 307 | if self.use_mpi: 308 | res = self.comm.reduce(res, op=self.mpi.SUM) 309 | return res 310 | 311 | def compute_mean_of_var(self, var_name, type_of_var, 312 | force_calculation=False): 313 | """ 314 | Compute the mean of the particle approximation. 315 | 316 | :param var_name: The name of the variable. 317 | :type var_name: str 318 | :param type_of_var: The type of the variable. 319 | :type type_of_var: str 320 | :param force_calculation: Computes the statistics even if a previous 321 | calculation was already made. 322 | :type force_calculation: bool 323 | """ 324 | self._check_if_valid_var(var_name, type_of_var) 325 | if (self._mean[type_of_var][var_name] is not None 326 | and not force_calculation): 327 | return 328 | self._mean[type_of_var][var_name] = self.get_mean_of_func( 329 | lambda x: x, var_name, type_of_var) 330 | 331 | def compute_all_means_of_type(self, type_of_var, 332 | force_calculation=False): 333 | """ 334 | Compute the means of every variable of a type ``type_of_var``. 335 | 336 | :param type_of_var: The type of the variable. 337 | :type type_of_var: str 338 | :param force_calculation: Computes the statistics even if a previous 339 | calculation was already made. 340 | :type force_calculation: bool 341 | """ 342 | var_names = list(self.particles[0][type_of_var].keys()) 343 | var_names.sort() 344 | for var_name in var_names: 345 | self.compute_mean_of_var(var_name, type_of_var, 346 | force_calculation=force_calculation) 347 | 348 | def compute_all_means(self, force_calculation=False): 349 | """ 350 | Compute all the means associated with the particle approximation. 351 | 352 | :param force_calculation: Computes the statistics even if a previous 353 | calculation was already made. 354 | :type force_calculation: bool 355 | """ 356 | type_of_vars = list(self.particles[0].keys()) 357 | type_of_vars.sort() 358 | for type_of_var in type_of_vars: 359 | self.compute_all_means_of_type(type_of_var, 360 | force_calculation=force_calculation) 361 | 362 | def compute_variance_of_var(self, var_name, type_of_var, 363 | force_calculation=False): 364 | """ 365 | Compute the variance of ``var_name``. 366 | 367 | :param var_name: The name of the variable. 368 | :type var_name: str 369 | :param type_of_var: The type of the variable. 370 | :type type_of_var: str 371 | 372 | :param force_calculation: Computes the statistics even if a previous 373 | calculation was already made. 374 | :type force_calculation: bool 375 | """ 376 | self._check_if_valid_var(var_name, type_of_var) 377 | if (self._variance[type_of_var][var_name] is not None 378 | and not force_calculation): 379 | return 380 | self._variance[type_of_var][var_name] = self.get_mean_of_func( 381 | lambda x: x ** 2, var_name, type_of_var) 382 | self.compute_mean_of_var(var_name, type_of_var, 383 | force_calculation=force_calculation) 384 | if self.rank == 0: 385 | self._variance[type_of_var][var_name] -= ( 386 | self._mean[type_of_var][var_name] ** 2) 387 | 388 | def compute_all_variances_of_type(self, type_of_var, 389 | force_calculation=False): 390 | """ 391 | Compute all variances of type ``type_of_var``. 392 | 393 | :param type_of_var: The type of the variable. 394 | :type type_of_var: str 395 | :param force_calculation: Computes the statistics even if a previous 396 | calculation was already made. 397 | :type force_calculation: bool 398 | """ 399 | var_names = list(self.particles[0][type_of_var].keys()) 400 | var_names.sort() 401 | for var_name in var_names: 402 | self.compute_variance_of_var(var_name, type_of_var, 403 | force_calculation=force_calculation) 404 | 405 | def compute_all_variances(self, force_calculation=False): 406 | """ 407 | Compute all the variances. 408 | 409 | :param force_calculation: Computes the statistics even if a previous 410 | calculation was already made. 411 | :type force_calculation: bool 412 | """ 413 | type_of_vars = list(self.particles[0].keys()) 414 | type_of_vars.sort() 415 | for type_of_var in type_of_vars: 416 | self.compute_all_variances_of_type(type_of_var, 417 | force_calculation=force_calculation) 418 | 419 | def compute_all_statistics(self, force_calculation=False): 420 | """ 421 | Compute all the statistics of the particle approximation. 422 | 423 | :param force_calculation: Computes the statistics even if a previous 424 | calculation was already made. 425 | :type force_calculation: bool 426 | """ 427 | self.compute_all_means(force_calculation=force_calculation) 428 | self.compute_all_variances(force_calculation=force_calculation) 429 | 430 | def resample(self): 431 | """ 432 | Resample the particles. After calling this, all particles will have 433 | the same weight. 434 | """ 435 | idx_list = [] 436 | log_w_all = np.ndarray(self.num_particles) 437 | if self.use_mpi: 438 | self.comm.Gather([self.log_w, self.mpi.DOUBLE], 439 | [log_w_all, self.mpi.DOUBLE]) 440 | else: 441 | log_w_all = self.log_w 442 | if self.rank == 0: 443 | births = np.random.multinomial(self.num_particles, 444 | np.exp(log_w_all)) 445 | for i in range(self.num_particles): 446 | idx_list += [i] * births[i] 447 | if self.rank == 0: 448 | idx = np.array(idx_list, 'i') 449 | else: 450 | idx = np.ndarray(self.num_particles, 'i') 451 | if self.use_mpi: 452 | self.comm.Bcast([idx, self.mpi.INT]) 453 | self.comm.barrier() 454 | num_particles = self.num_particles 455 | my_num_particles = self.my_num_particles 456 | old_particles = self._particles 457 | self._particles = [] 458 | for i in range(num_particles): 459 | to_whom = i // my_num_particles 460 | from_whom = idx[i] // my_num_particles 461 | if from_whom == to_whom and to_whom == self.rank: 462 | my_idx = idx[i] % my_num_particles 463 | self._particles.append(old_particles[my_idx].copy()) 464 | elif to_whom == self.rank: 465 | self._particles.append(self.comm.recv( 466 | source=from_whom, tag=i)) 467 | elif from_whom == self.rank: 468 | my_idx = idx[i] % my_num_particles 469 | self.comm.send(old_particles[my_idx], dest=to_whom, tag=i) 470 | self.comm.barrier() 471 | else: 472 | self._particles = [self._particles[i].copy() for i in idx] 473 | self.log_w.fill(-math.log(self.num_particles)) 474 | self._weights = np.exp(self.log_w) 475 | self._fix_particles() 476 | 477 | def copy(self): 478 | """ 479 | Copy the particle approximation. 480 | 481 | :returns: A copy of the current particle approximation. 482 | :rtype: :class:`pysmc.ParticleApproximation` 483 | """ 484 | new_pa = ParticleApproximation(self.log_w, self.particles, 485 | mpi=self.mpi, comm=self.comm) 486 | new_pa._mean = deepcopy(self.mean) 487 | new_pa._variance = deepcopy(self.variance) 488 | return new_pa 489 | 490 | def allgather(self): 491 | """ 492 | Get a particle approximation on every process. 493 | 494 | If we are not using MPI, it will simply return a copy of the object. 495 | 496 | :returns: A fully functional particle approximation on a single 497 | process. 498 | :rtype: :class:`pysmc.ParticleApproximation` 499 | """ 500 | if not self.use_mpi: 501 | return self.copy() 502 | log_w = np.hstack(self.comm.allgather(self._log_w)) 503 | tmp = self.comm.allgather(self.particles) 504 | particles = [t[i] for t in tmp for i in range(len(t))] 505 | return ParticleApproximation(log_w=log_w, particles=particles) 506 | 507 | def gather(self, root=0): 508 | """ 509 | Get a particle approximation on the root process. 510 | 511 | If we are not using MPI, it will simply return a copy of the object. 512 | 513 | :param root: The id of the root process. 514 | :returns: A fully functional particle approximation on a single 515 | process. 516 | :rtype: :class:`pysmc.ParticleApproximation` 517 | """ 518 | if not self.use_mpi: 519 | return self.copy() 520 | log_w = self.comm.gather(self._log_w) 521 | tmp = self.comm.gather(self.particles) 522 | if self.rank == 0: 523 | log_w = np.hstack(log_w) 524 | particles = [t[i] for t in tmp for i in range(len(t))] 525 | return ParticleApproximation(log_w=log_w, particles=particles) 526 | else: 527 | return None 528 | 529 | @staticmethod 530 | def scatter(pa, mpi=None, comm=None): 531 | """ 532 | Scatter the particle approximation across the tasks. 533 | 534 | Assuming here that only the root processor (rank == 0) has it. 535 | """ 536 | if mpi is None: 537 | return pa 538 | if comm is None: 539 | comm = mpi.COMM_WORLD 540 | rank = comm.Get_rank() 541 | size = comm.Get_size() 542 | if rank == 0: 543 | chunk = pa.num_particles / size 544 | log_w = [pa.log_w[i * chunk:(i + 1) * chunk] 545 | for i in range(size)] 546 | particles = [pa.particles[i * chunk:(i + 1) * chunk] 547 | for i in range(size)] 548 | else: 549 | log_w = None 550 | particles = None 551 | my_log_w = comm.scatter(log_w) 552 | my_particles = comm.scatter(particles) 553 | return ParticleApproximation(log_w=my_log_w, particles=my_particles, 554 | mpi=mpi, comm=comm) 555 | -------------------------------------------------------------------------------- /pysmc/_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _plot: 4 | 5 | ++++++++ 6 | Plotting 7 | ++++++++ 8 | 9 | """ 10 | 11 | 12 | __all__ = ['hist', 'make_movie_from_db'] 13 | 14 | 15 | import os 16 | import numpy as np 17 | import matplotlib.pyplot as plt 18 | import matplotlib.animation as animation 19 | from pysmc._misc import multinomial_resample 20 | from pysmc._misc import kde 21 | 22 | 23 | def hist(particle_approximation, var_name, normed=True): 24 | """ 25 | Plot the histogram of variable of a particle approximation. 26 | 27 | :param particle_approximation: A particle approximation. 28 | :type particle_approximation: :class:`pysmc.ParticleApproximation` 29 | :param var_name: The name of the variable you want to plot. 30 | :type var_name: str 31 | :param bins: The number of bins you want to use. 32 | :type bins: int 33 | :param normed: ``True`` if you want the histogram to be normalized, 34 | ``False`` otherwise. 35 | :type normed: bool 36 | """ 37 | x = getattr(particle_approximation, var_name) 38 | w = particle_approximation.weights 39 | bins = w.shape[0] // 10 40 | plt.xlabel(var_name, fontsize=16) 41 | plt.ylabel('p(%s)' % var_name, fontsize=16) 42 | return plt.hist(x, weights=w, bins=bins, density=normed) 43 | 44 | 45 | def make_movie_from_db(db, var_name): 46 | """ 47 | Make a movie from a database. 48 | """ 49 | k = kde(db.particle_approximations[0], var_name) 50 | x01 = k.dataset.min() 51 | x02 = k.dataset.max() 52 | x0 = np.linspace(x01, x02, 100) 53 | y0 = k(x0) 54 | k = kde(db.particle_approximations[-1], var_name) 55 | yl = k(x0) 56 | yl1 = yl.min() 57 | yl2 = yl.max() 58 | fig = plt.figure() 59 | ax = fig.add_subplot(111, autoscale_on=False, 60 | xlim=(x01, x02), ylim=(yl1, yl2)) 61 | line, = ax.plot([], [], linewidth=2) 62 | particles, = ax.plot([], [], 'ro', markersize=5) 63 | gamma_text = ax.text(0.02, 0.95, '', transform=ax.transAxes, 64 | fontsize=16) 65 | ax.set_xlabel(var_name, fontsize=16) 66 | ax.set_ylabel('p(%s)' % var_name, fontsize=16) 67 | 68 | def init(): 69 | line.set_data([], []) 70 | particles.set_data([], []) 71 | gamma_text.set_text('') 72 | return line, particles, gamma_text 73 | 74 | def animate(i): 75 | k = kde(db.particle_approximations[i], var_name) 76 | line.set_data(x0, k(x0)) 77 | p = getattr(db.particle_approximations[i], var_name) 78 | particles.set_data(p, np.zeros(p.shape) + yl1 + 0.01 * (yl2 - yl1)) 79 | gamma_text.set_text('%s = %1.4f' % (db.gamma_name, db.gammas[i])) 80 | return line, particles, gamma_text 81 | 82 | ani = animation.FuncAnimation(fig, animate, frames=db.num_gammas, 83 | interval=200, blit=True, init_func=init) 84 | return ani 85 | -------------------------------------------------------------------------------- /pysmc/_smc.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _smc: 4 | 5 | +++ 6 | SMC 7 | +++ 8 | 9 | :class:`pysmc.SMC` is the class that makes everything happen. 10 | 11 | Here is a complete reference of the public members: 12 | """ 13 | 14 | 15 | __all__ = ['SMC'] 16 | 17 | 18 | from pysmc._mcmc_wrapper import MCMCWrapper 19 | from pysmc._mpi import DistributedObject 20 | from pysmc._particle_approximation import ParticleApproximation 21 | from pysmc._db import DataBase 22 | from pysmc._db import SerialDataBase 23 | import pymc 24 | import numpy as np 25 | from scipy.optimize import brentq 26 | import math 27 | import itertools 28 | import sys 29 | import warnings 30 | import os 31 | 32 | 33 | class SMC(DistributedObject): 34 | """ 35 | Use Sequential Monte Carlo (SMC) to sample from a distribution. 36 | 37 | **Base class:** :class:`pysmc.DistributedObject` 38 | 39 | :param mcmc_sampler: This is an essential part in initializing the 40 | object. It can either be a ready to go 41 | MCMC sampler or a module/class representing 42 | a :mod:`pymc` model. In the latter case, the 43 | MCMC sampler will be initialized automatically. 44 | :type mcmc_sampler: :class:`pymc.MCMC`, :class:`pysmc.MCMCWrapper` 45 | or a :mod:`pymc` model 46 | :param num_particles: The number of particles. 47 | :type num_particles: int 48 | :param num_mcmc: The number of MCMC steps per gamma. 49 | :type num_mcmc: int 50 | :param ess_threshold: The ESS threshold below which resampling 51 | takes place. 52 | :type ess_threshold: float 53 | :param ess_reduction: The ESS reduction that adaptively specifies 54 | the next ``gamma``. 55 | :type ess_reduction: float 56 | :param adapt_proposal_step: Adapt or not the proposal step by 57 | monitoring the acceptance rate. 58 | :type adapt_proposal_step: bool 59 | :param verbose: How much output to print (1, 2 and 3). 60 | :type verbose: int 61 | :param gamma_name: The name with which the ``gamma`` parameter is 62 | refered to in your :mod:`pymc` model. The 63 | default value is ``'gamma'``, but you can 64 | change it to whatever you want. 65 | :type gamma_name: str 66 | :param gamma_is_an_exponent: A flag that should be ``True`` if ``gamma`` 67 | appears as an exponent in the probability 68 | density, e.g., 69 | :math:`p(x | y) \\propto p(y | x)^{\\gamma} p(x)`. 70 | The default value is ``False``. However, if 71 | your model is of the right form **it pays off** 72 | to set it to ``True``. Then we can solve the 73 | problem of finding the next :math:``\gamma`` 74 | in the sequence a lot faster. 75 | :type gamma_is_an_exponent: bool 76 | :param db_filename: The filename of a database for the object. If 77 | the database exists and is a valid one, then 78 | the object will be initialized at each last 79 | state. If the parameter ``update_db`` is also 80 | set, then the algorithm will dump the state of 81 | each ``gamma`` it visits and commit it to the 82 | data base. Otherwise, commits can be forced by 83 | calling :meth:`pysmc.SMC.commit()`. 84 | :type db_filename: str 85 | :param mpi: The MPI class (see :mod:`mpi4py` and 86 | :ref:`mpi_example`). If ``None``, then no 87 | parallelism is used. 88 | :param comm: Set this to the MPI communicator. If ``None``, 89 | then ``mpi.COMM_WORLD`` is used. 90 | """ 91 | 92 | # The logarithm of the weights 93 | _log_w = None 94 | 95 | # The current effective sample size 96 | _ess = None 97 | 98 | # The number of MCMC steps for each gamma 99 | _num_mcmc = None 100 | 101 | # The thresshold of the effective sample size (percentage) 102 | _ess_threshold = None 103 | 104 | # The reduction of the effective sample size per gamma step 105 | _ess_reduction = None 106 | 107 | # Do you want to adaptively select the MCMC proposal step? 108 | _adapt_proposal_step = None 109 | 110 | # The amount of verbosity 111 | _verbose = None 112 | 113 | # The monte carlo sampler 114 | _mcmc_sampler = None 115 | 116 | # The particles 117 | _particles = None 118 | 119 | # The underlying MCMC sampler 120 | _mcmc_sampler = None 121 | 122 | # The observed random variable 123 | _gamma_rv = None 124 | 125 | # The true name of the gamma parameter 126 | _gamma_name = None 127 | 128 | # A database containing all the particles at all gammas 129 | _db = None 130 | 131 | # Update the database or not 132 | _update_db = False 133 | 134 | # Count the total number of MCMC samples taken so far 135 | _total_num_mcmc = None 136 | 137 | # Does gamma appear as an exponent in the probability density? 138 | _gamma_is_an_exponent = None 139 | 140 | @property 141 | def gamma_is_an_exponent(self): 142 | """ 143 | The Flag that determines if gamma is an exponent in the probability 144 | density. 145 | 146 | :getter: Get the flag. 147 | :setter: Set the flag. 148 | :type: bool 149 | """ 150 | return self._gamma_is_an_exponent 151 | 152 | @gamma_is_an_exponent.setter 153 | def gamma_is_an_exponent(self, value): 154 | """Set the value of the flag.""" 155 | value = bool(value) 156 | self._gamma_is_an_exponent = value 157 | 158 | @property 159 | def my_num_particles(self): 160 | """ 161 | :getter: The number of particles associated with each process. 162 | :type: int 163 | 164 | .. note:: 165 | 166 | If not using MPI, then it is the same as 167 | :meth:`pysmc.SMC.num_particles`. Otherwise is it is equal to 168 | ``num_particles / size``, where ``size`` is the total number of MPI 169 | processes. 170 | 171 | """ 172 | return len(self.particles) 173 | 174 | @property 175 | def num_particles(self): 176 | """ 177 | :getter: Get the number of particles. 178 | :type: int 179 | """ 180 | return self.my_num_particles * self.size 181 | 182 | @property 183 | def log_w(self): 184 | """ 185 | :getter: The logarithm of the weights of the particles. 186 | :type: 1D :class:`numpy.ndarray` 187 | """ 188 | return self._log_w 189 | 190 | @property 191 | def ess(self): 192 | """ 193 | :getter: The current Effective Sample Size. 194 | :type: float 195 | """ 196 | return self._ess 197 | 198 | @property 199 | def num_mcmc(self): 200 | """ 201 | :getter: Get the number of MCMC steps per SMC step. 202 | :setter: Set the number of MCMC steps per SMC step. 203 | :type: int 204 | :raises: :exc:`exceptions.ValueError` 205 | """ 206 | return self._num_mcmc 207 | 208 | @num_mcmc.setter 209 | def num_mcmc(self, value): 210 | """Set the number of MCMC steps per gamma.""" 211 | value = int(value) 212 | if value <= 0: 213 | raise ValueError('num_mcmc <= 0!') 214 | self._num_mcmc = value 215 | 216 | @property 217 | def ess_threshold(self): 218 | """ 219 | The threshold of the Effective Sample Size is a number between 0 and 1 220 | representing the percentage of the total number of particles. If the 221 | Effective Sample Size falls bellow ``ess_threshold * num_particles``, 222 | then the particles are automatically resampled. 223 | 224 | :getter: Get the threshold of the Effective Sample Size. 225 | :setter: Set the threshold of the Effective Sample Size. 226 | :type: float 227 | :raises: :exc:`exceptions.ValueError` 228 | """ 229 | return self._ess_threshold 230 | 231 | @ess_threshold.setter 232 | def ess_threshold(self, value): 233 | """Set the threshold of the effective sample size.""" 234 | value = float(value) 235 | if value <= 0. or value >= 1.: 236 | raise ValueError('The ESS threshold must be in (0, 1).') 237 | self._ess_threshold = value 238 | 239 | @property 240 | def ess_reduction(self): 241 | """ 242 | It is a number between 0 and 1 representing the desired 243 | percent reduction 244 | of the effective sample size when we perform a SMC step. 245 | The next ``gamma`` will be selected adaptively so that the prescribed 246 | reduction is achieved. 247 | 248 | :getter: Get the reduction of the Effective Sample Size per SMC 249 | step. 250 | :setter: Set the reduction of the Effective Sample Size per SMC 251 | step. 252 | :type: float 253 | :raises: :exc:`exceptions.ValueError` 254 | """ 255 | return self._ess_reduction 256 | 257 | @ess_reduction.setter 258 | def ess_reduction(self, value): 259 | """Set the reduction of the effective sample size per SMC step.""" 260 | value = float(value) 261 | if value <= 0. or value >= 1.: 262 | raise ValueError('The ESS reduction must be in (0, 1).') 263 | self._ess_reduction = value 264 | 265 | @property 266 | def adapt_proposal_step(self): 267 | """ 268 | If the ``adapt proposal step`` is set to ``True``, each of the step 269 | methods of the underlying :class:`pymc.MCMC` class are adaptively 270 | tuned by monitoring the acceptance rate. 271 | 272 | :getter: Get the adapt flag. 273 | :setter: Set the adapt flag. 274 | :type: bool 275 | """ 276 | return self._adapt_proposal_step 277 | 278 | @adapt_proposal_step.setter 279 | def adapt_proposal_step(self, value): 280 | """Set the adapt flag.""" 281 | value = bool(value) 282 | self._adapt_proposal_step = value 283 | 284 | @property 285 | def verbose(self): 286 | """ 287 | Specify the amount of output printed by the class. There are three 288 | levels: 289 | 290 | + 0: Print nothing. 291 | + 1: Print info from methods you call. 292 | + 2: Print info from methods the methods you call call... 293 | + 3: Guess what... 294 | 295 | :getter: Get the verbosity flag. 296 | :setter: Set the verbosity flag. 297 | :type: int 298 | """ 299 | if self.rank == 0: 300 | return self._verbose 301 | else: 302 | return 0 303 | 304 | @verbose.setter 305 | def verbose(self, value): 306 | """Set the verbosity flag.""" 307 | value = int(value) 308 | self._verbose = value 309 | 310 | @property 311 | def mcmc_sampler(self): 312 | """ 313 | The underlying :class:`pymc.MCMC` object. 314 | 315 | :getter: Get the underlying MCMC object. 316 | :setter: Set the underlying MCMC object. 317 | :raises: :exc:`exceptions.TypeError` 318 | :type: :class:`pymc.MCMC` 319 | """ 320 | return self._mcmc_sampler 321 | 322 | @property 323 | def db(self): 324 | """ 325 | The database containing info about all the particles we visited. 326 | 327 | :getter: Get the database. 328 | :type: dict 329 | """ 330 | return self._db 331 | 332 | @property 333 | def update_db(self): 334 | """ 335 | Update the database or not. 336 | 337 | :getter: Get the ``update_db`` flag. 338 | :setter: Set the ``update_db`` flag. 339 | :type: bool 340 | """ 341 | return self._update_db 342 | 343 | @update_db.setter 344 | def update_db(self, value): 345 | """ 346 | Set the ``update_db`` flag. 347 | """ 348 | value = bool(value) 349 | self._update_db = value 350 | 351 | def _update_gamma_rv(self): 352 | """Update the variable that points to the observed rv.""" 353 | self._gamma_rv = [] 354 | for rv in self.mcmc_sampler.nodes: 355 | if self.gamma_name in rv.parents: 356 | self._gamma_rv.append(rv) 357 | 358 | @property 359 | def gamma_rv(self): 360 | """ 361 | The random variable of the :mod:`pymc` model that has a parameter named 362 | ``gamma_name``. 363 | 364 | :getter: Get the value of the ``gamma_name`` parameter. 365 | :type: :class:`pymc.Stochastic` 366 | """ 367 | return self._gamma_rv 368 | 369 | @property 370 | def gamma(self): 371 | """ 372 | The value of the ``gamma_name`` parameter. 373 | 374 | :getter: Get the value of the ``gamma_name`` parameter. 375 | :setter: Set the value of the ``gamma_name`` parameter. 376 | :type: float 377 | """ 378 | return self._gamma_rv[0].parents[self.gamma_name] 379 | 380 | def _set_gamma(self, value): 381 | """ 382 | Set the value of gamma. 383 | """ 384 | for rv in self._gamma_rv: 385 | rv.parents[self.gamma_name] = value 386 | 387 | @property 388 | def gamma_name(self): 389 | """ 390 | The true name of the gamma parameter in the :mod:`pymc` model. 391 | 392 | :getter: Get the name of the gamma parameter. 393 | :setter: Set the name of the gamma parameter. 394 | :type: str 395 | """ 396 | return self._gamma_name 397 | 398 | @gamma_name.setter 399 | def gamma_name(self, value): 400 | """Set the true name of the gamma parameter.""" 401 | self._gamma_name = gamma_name 402 | if self._mcmc_sampler is not None: 403 | self._update_gamma_rv() 404 | 405 | @property 406 | def particles(self): 407 | """ 408 | The SMC particles. 409 | 410 | :getter: Get the SMC particles. 411 | :type: list of whatever objects your method supports. 412 | """ 413 | return self._particles 414 | 415 | @property 416 | def total_num_mcmc(self): 417 | """ 418 | The total number of MCMC steps performed so far. 419 | This is zeroed, everytime you call :meth:`pysmc.SMC.initialize()`. 420 | 421 | :getter: The total number of MCMC steps performed so far. 422 | :type: int 423 | """ 424 | if self.use_mpi: 425 | return self.comm.allreduce(self._total_num_mcmc, op=self.mpi.SUM) 426 | else: 427 | return self._total_num_mcmc 428 | 429 | def _logsumexp(self, log_x): 430 | """Perform the log-sum-exp of the weights.""" 431 | my_max_exp = log_x.max() 432 | if self.use_mpi: 433 | max_exp = self.comm.allreduce(my_max_exp, op=self.mpi.MAX) 434 | else: 435 | max_exp = my_max_exp 436 | my_sum = np.exp(log_x - max_exp).sum() 437 | if self.use_mpi: 438 | all_sum = self.comm.allreduce(my_sum) 439 | else: 440 | all_sum = my_sum 441 | return math.log(all_sum) + max_exp 442 | 443 | def _normalize(self, log_w): 444 | """Normalize the weights.""" 445 | c = self._logsumexp(log_w) 446 | return log_w - c 447 | 448 | def _get_ess_at(self, log_w): 449 | """Calculate the ESS at given the log weights. 450 | 451 | Precondition 452 | ------------ 453 | The weights are assumed to be normalized. 454 | """ 455 | log_w_all = log_w 456 | if self.use_mpi: 457 | log_w_all = np.ndarray(self.num_particles) 458 | self.comm.Gather([log_w, self.mpi.DOUBLE], 459 | [log_w_all, self.mpi.DOUBLE]) 460 | if self.rank == 0: 461 | ess = 1. / math.fsum(np.exp(2. * log_w_all)) 462 | else: 463 | ess = None 464 | if self.use_mpi: 465 | ess = self.comm.bcast(ess) 466 | return ess 467 | 468 | def _get_log_of_weight_factor_at(self, gamma): 469 | """Return the log of the weight factor when going to the new gamma.""" 470 | if self.gamma_is_an_exponent: 471 | return (gamma - self.gamma)* self._loglike 472 | logp_new = self._get_logp_at_gamma(gamma) 473 | return logp_new - self._logp_prev 474 | 475 | def _get_unormalized_weights_at(self, gamma): 476 | """Return the unormalized weights at a given gamma.""" 477 | return self.log_w + self._get_log_of_weight_factor_at(gamma) 478 | 479 | def _get_ess_given_gamma(self, gamma): 480 | """Calculate the ESS at a given gamma. 481 | 482 | Returns 483 | ------- 484 | The ess and the normalized weights corresponding to that 485 | gamma. 486 | """ 487 | log_w = self._get_unormalized_weights_at(gamma) 488 | log_w_normalized = self._normalize(log_w) 489 | return self._get_ess_at(log_w_normalized) 490 | 491 | def _resample(self): 492 | """Resample the particles. 493 | 494 | Precondition 495 | ------------ 496 | The weights are assumed to be normalized. 497 | """ 498 | if self.verbose > 1: 499 | sys.stdout.write('- resampling: ') 500 | idx_list = [] 501 | log_w_all = np.ndarray(self.num_particles) 502 | if self.use_mpi: 503 | self.comm.Gather([self.log_w, self.mpi.DOUBLE], 504 | [log_w_all, self.mpi.DOUBLE]) 505 | else: 506 | log_w_all = self.log_w 507 | if self.rank == 0: 508 | births = np.random.multinomial(self.num_particles, 509 | np.exp(log_w_all)) 510 | for i in range(self.num_particles): 511 | idx_list += [i] * births[i] 512 | if self.rank == 0: 513 | idx = np.array(idx_list, 'i') 514 | else: 515 | idx = np.ndarray(self.num_particles, 'i') 516 | if self.use_mpi: 517 | self.comm.Bcast([idx, self.mpi.INT]) 518 | self.comm.barrier() 519 | num_particles = self.num_particles 520 | my_num_particles = self.my_num_particles 521 | old_particles = self._particles 522 | self._particles = [] 523 | for i in range(num_particles): 524 | to_whom = i // my_num_particles 525 | from_whom = idx[i] // my_num_particles 526 | if from_whom == to_whom and to_whom == self.rank: 527 | my_idx = idx[i] % my_num_particles 528 | self._particles.append(old_particles[my_idx].copy()) 529 | elif to_whom == self.rank: 530 | self._particles.append(self.comm.recv( 531 | source=from_whom, tag=i)) 532 | elif from_whom == self.rank: 533 | my_idx = idx[i] % my_num_particles 534 | self.comm.send(old_particles[my_idx], dest=to_whom, tag=i) 535 | self.comm.barrier() 536 | else: 537 | self._particles = [self._particles[i].copy() for i in idx] 538 | self.log_w.fill(-math.log(self.num_particles)) 539 | self._ess = self.num_particles 540 | if self.verbose > 1: 541 | sys.stdout.write('SUCCESS\n') 542 | 543 | def _tune(self): 544 | """Tune the parameters of the proposals..""" 545 | # TODO: Make sure this actually works! 546 | if self.verbose > 1: 547 | print('- tuning the MCMC parameters:') 548 | for sm in self.mcmc_sampler.step_methods: 549 | if self.verbose > 1: 550 | sys.stdout.write('\t- tuning step method: %s\n' % str(sm)) 551 | if sm.tune(self.get_particle_approximation(), 552 | comm=self.comm, 553 | verbose=self.verbose): 554 | if self.verbose > 1: 555 | sys.stdout.write('\n\t\tSUCCESS\n') 556 | else: 557 | if self.verbose > 1: 558 | sys.stdout.write('\n\t\tFAILURE\n') 559 | 560 | def _get_logp_of_particle(self, i): 561 | """ 562 | Get the logp of a particle. 563 | """ 564 | self.mcmc_sampler.set_state(self.particles[i]) 565 | return self.mcmc_sampler.logp 566 | 567 | def _get_logp_of_particles(self): 568 | """ 569 | Get the logp of all particles. 570 | """ 571 | return np.array([self._get_logp_of_particle(i) 572 | for i in range(self.my_num_particles)]) 573 | 574 | def _get_logp_at_gamma(self, gamma): 575 | """ 576 | Get the logp at gamma. 577 | """ 578 | if gamma is not self.gamma: 579 | old_gamma = self.gamma 580 | self._set_gamma(gamma) 581 | logp = self._get_logp_of_particles() 582 | self._set_gamma(old_gamma) 583 | else: 584 | logp = self._get_logp_of_particles() 585 | return logp 586 | 587 | def _get_loglike(self, gamma0, gamma1): 588 | """ 589 | Get the log likelihood assuming that gamma appears in the exponent. 590 | """ 591 | logp0 = self._get_logp_at_gamma(gamma0) 592 | logp1 = self._get_logp_at_gamma(gamma1) 593 | return (logp1 - logp0) / (gamma1 - gamma0) 594 | 595 | def _find_next_gamma(self, gamma): 596 | """ 597 | Find the next gamma. 598 | 599 | Parameters 600 | ---------- 601 | gamma : float 602 | The next gamma is between the current one and ``gamma``. 603 | 604 | Returns 605 | ------- 606 | The next gamma. 607 | 608 | """ 609 | if self.verbose > 1: 610 | print('- finding next gamma.') 611 | 612 | if self.gamma_is_an_exponent: 613 | self._loglike = self._get_loglike(self.gamma, gamma) 614 | else: 615 | self._logp_prev = self._get_logp_at_gamma(self.gamma) 616 | 617 | # Define the function whoose root we are seeking 618 | def f(test_gamma, args): 619 | ess_test_gamma = args._get_ess_given_gamma(test_gamma) 620 | return ess_test_gamma - args.ess_reduction * args.ess 621 | 622 | if f(gamma, self) > 0: 623 | if self.verbose > 1: 624 | print('- \twe can move directly to the target gamma...') 625 | return gamma 626 | else: 627 | # Solve for the optimal gamma using the bisection algorithm 628 | next_gamma, r = brentq(f, self.gamma, gamma, self, disp=False, 629 | full_output=True) 630 | if self.use_mpi: 631 | self.comm.barrier() 632 | return next_gamma 633 | 634 | def _initialize_db(self, db_filename, update_db): 635 | """ 636 | Initialize the database. 637 | """ 638 | if db_filename is not None: 639 | if self.rank == 0: 640 | db_filename = os.path.abspath(db_filename) 641 | if self.verbose > 0: 642 | print('- db: ' + db_filename) 643 | if os.path.exists(db_filename): 644 | if self.verbose > 0: 645 | print('- db exists') 646 | print('- assuming this is a restart run') 647 | self._db = DataBase.load(db_filename) 648 | db_exists = True 649 | else: 650 | if self.verbose > 0: 651 | print('- db does not exist') 652 | print('- creating db file') 653 | self._db = DataBase(gamma_name=self.gamma_name, 654 | filename=db_filename) 655 | db_exists = False 656 | else: 657 | db_exists = None 658 | if self.use_mpi: 659 | db_exists = self.comm.bcast(db_exists) 660 | if db_exists: 661 | if self.rank == 0: 662 | if self.verbose > 0: 663 | print('- db:') 664 | print('\t- num. of %s: %d' % (self.gamma_name, 665 | self.db.num_gammas)) 666 | print('\t- first %s: %1.4f' % (self.gamma_name, 667 | self.db.gammas[0])) 668 | print('\t- last %s: %1.4f' % (self.gamma_name, 669 | self.db.gammas[-1])) 670 | print('- initializing the object at the last state of db') 671 | gamma = self.db.gamma 672 | pa = self.db.particle_approximation 673 | sm_param = self.db.step_method_param 674 | else: 675 | gamma = None 676 | pa = None 677 | sm_param = None 678 | if self.use_mpi: 679 | gamma = self.comm.bcast(gamma) 680 | pa = ParticleApproximation.scatter(pa, mpi=self.mpi, 681 | comm=self.comm) 682 | sm_param = self.comm.bcast(sm_param) 683 | self.mcmc_sampler.set_params(sm_param) 684 | self.initialize(gamma, particle_approximation=pa) 685 | if self.verbose > 0: 686 | if update_db: 687 | print('- commiting to the database at every step') 688 | else: 689 | print('- manually commiting to the database') 690 | elif update_db: 691 | if self.verbose > 0: 692 | warnings.warn( 693 | '- update_db flag is on but no db_filename was specified\n' 694 | + '- setting the update_db flag to off') 695 | update_db = False 696 | self._update_db = update_db 697 | 698 | def _set_gamma_name(self, gamma_name): 699 | """ 700 | Safely, set the gamma_name parameter. 701 | """ 702 | if not isinstance(gamma_name, str): 703 | raise TypeError('The \'gamma_name\' parameter must be a str!') 704 | self._gamma_name = gamma_name 705 | 706 | def _set_mcmc_sampler(self, mcmc_sampler): 707 | """ 708 | Safely, set the MCMC sampler. 709 | """ 710 | if (not isinstance(mcmc_sampler, pymc.MCMC) 711 | and not isinstance(mcmc_sampler, MCMCWrapper)): 712 | # Try to make an MCMC sampler out of it (it will work if it is a 713 | # valid model). 714 | try: 715 | if self.rank == 0: 716 | warnings.warn( 717 | '- mcmc_sampler is not a pymc.MCMC.\n' 718 | + '- attempting to make it one!') 719 | mcmc_sampler = pymc.MCMC(mcmc_sampler) 720 | except: 721 | raise RuntimeError( 722 | 'The mcmc_sampler object could not be converted to a pymc.MCMC!') 723 | if not isinstance(mcmc_sampler, MCMCWrapper): 724 | mcmc_sampler = MCMCWrapper(mcmc_sampler, comm=self.comm) 725 | self._mcmc_sampler = mcmc_sampler 726 | self._update_gamma_rv() 727 | 728 | def _set_initial_particles(self, num_particles): 729 | """ 730 | Safely, set the initial particles. 731 | """ 732 | num_particles = int(num_particles) 733 | if num_particles <= 0: 734 | raise ValueError('num_particles <= 0!') 735 | my_num_particles = int(num_particles / self.size) 736 | if my_num_particles * self.size < num_particles: 737 | warnings.warn( 738 | '- number of particles (%d) not supported on %d mpi processes' % 739 | (num_particles, self.size)) 740 | num_particles = my_num_particles * self.size 741 | warnings.warn( 742 | '- changing the number of particles to %d' % num_particles) 743 | self._particles = [None for i in range(my_num_particles)] 744 | self._log_w = (np.ones(my_num_particles) 745 | * (-math.log(num_particles))) 746 | 747 | def __init__(self, mcmc_sampler=None, 748 | num_particles=10, num_mcmc=10, 749 | ess_threshold=0.67, 750 | ess_reduction=0.90, 751 | adapt_proposal_step=True, 752 | verbose=0, 753 | mpi=None, 754 | comm=None, 755 | gamma_name='gamma', 756 | db_filename=None, 757 | update_db=False, 758 | gamma_is_an_exponent=False): 759 | """ 760 | Initialize the object. 761 | 762 | See the doc of the class for the description. 763 | """ 764 | super(SMC, self).__init__(mpi=mpi, comm=comm) 765 | self._set_gamma_name(gamma_name) 766 | self._set_mcmc_sampler(mcmc_sampler) 767 | self._set_initial_particles(num_particles) 768 | self.num_mcmc = num_mcmc 769 | self.ess_threshold = ess_threshold 770 | self.ess_reduction = ess_reduction 771 | self.verbose = verbose 772 | self.adapt_proposal_step = adapt_proposal_step 773 | self.gamma_is_an_exponent = gamma_is_an_exponent 774 | self._initialize_db(db_filename, update_db) 775 | 776 | def initialize(self, gamma, particle_approximation=None, 777 | num_mcmc_per_particle=10): 778 | """ 779 | Initialize SMC at a particular ``gamma``. 780 | 781 | The method has basically three ways of initializing the particles: 782 | 783 | + If ``particles_approximation`` is not ``None``, 784 | then it is assumed to contain the 785 | particles at the corresponding value of ``gamma``. 786 | + If ``particles_approximation`` is ``None`` and the 787 | MCMC sampler class has a method 788 | called ``draw_from_prior()`` that works, then it is called to 789 | initialize the particles. 790 | + In any other case, MCMC sampling is used to initialize the particles. 791 | We are assuming that the MCMC sampler has already been tuned for 792 | that particular gamma and that a sufficient burning period has past. 793 | Then we record the current state as the first particle, we sample 794 | ``num_mcmc_per_particle`` times and record the second particle, and 795 | so on. 796 | 797 | :param gamma: The initial ``gamma`` parameter. It must, of 798 | course, be within the right range of 799 | ``gamma``. 800 | :type gamma: float 801 | :param particles_approximation: A dictionary of MCMC states representing 802 | the particles. When using MPI, we are 803 | assuming that each one of the CPU's 804 | has each own collection of particles. 805 | :type particles_approximation: :class:`pysmc.ParticleApproximation` 806 | :param num_mcmc_per_particle: This parameter is ignored if 807 | ``particles`` is not ``None``. If the 808 | only way to initialize the particles is 809 | to use MCMC, then this is the number of 810 | of mcmc samples we drop before getting 811 | a SMC particle. 812 | """ 813 | if self.verbose > 0: 814 | print('------------------------') 815 | print('START SMC Initialization') 816 | print('------------------------') 817 | print('- initializing at', self.gamma_name, ':', gamma) 818 | # Zero out the MCMC step counter 819 | self._total_num_mcmc = 0 820 | # Set gamma 821 | self._set_gamma(gamma) 822 | # Set the weights and ESS 823 | self.log_w.fill(-math.log(self.num_particles)) 824 | self._ess = float(self.num_particles) 825 | if particle_approximation is not None: 826 | if self.verbose > 0: 827 | print('- initializing with a particle approximation.') 828 | self._particles = particle_approximation.particles 829 | self._log_w = particle_approximation.log_w 830 | self._ess = self._get_ess_at(self.log_w) 831 | return 832 | else: 833 | self.particles[0] = self.mcmc_sampler.get_state() 834 | try: 835 | if self.verbose > 0: 836 | sys.stdout.write( 837 | '- initializing by sampling from the prior: ') 838 | if not gamma == 0.: 839 | raise AttributeError() 840 | for i in range(1, self.my_num_particles): 841 | self.mcmc_sampler.draw_from_prior() 842 | self.particles[i] = self.mcmc_sampler.get_state() 843 | if self.verbose > 0: 844 | sys.stdout.write('SUCCESS\n') 845 | except AttributeError: 846 | if self.verbose > 0: 847 | sys.stdout.write('FAILURE\n') 848 | print('- initializing via MCMC') 849 | if self.use_mpi: 850 | total_samples = (self.my_num_particles 851 | * num_mcmc_per_particle) 852 | print('- taking a total of', total_samples, 'samples per process') 853 | else: 854 | total_samples = (self.num_particles 855 | * num_mcmc_per_particle) 856 | print('- taking a total of', total_samples, 'samples') 857 | print('- creating a particle every', num_mcmc_per_particle) 858 | if self.verbose > 0: 859 | pb = pymc.progressbar.ProgressBar(self.num_particles * 860 | num_mcmc_per_particle) 861 | # Only rank 0 keeps the first particle 862 | if self.rank == 0: 863 | start_idx = 1 864 | else: 865 | start_idx = 0 866 | for i in range(start_idx, self.my_num_particles): 867 | self.mcmc_sampler.sample(num_mcmc_per_particle) 868 | self.particles[i] = self.mcmc_sampler.get_state() 869 | self._total_num_mcmc += num_mcmc_per_particle 870 | # TODO: Find bug in PyMC bar 871 | #if self.verbose > 0: 872 | # pb.update((i + 2) * self.size * num_mcmc_per_particle) 873 | if self.verbose > 0: 874 | print('') 875 | pa = self.get_particle_approximation().gather() 876 | sm_params = self.mcmc_sampler.get_params(comm=self.comm) 877 | if self.update_db and self.rank == 0: 878 | self.db.add(self.gamma, pa, sm_params) 879 | self.db.commit() 880 | if self.verbose > 0: 881 | print('----------------------') 882 | print('END SMC Initialization') 883 | print('----------------------') 884 | 885 | 886 | def move_to(self, gamma): 887 | """ 888 | Move the current particle approximation to ``gamma``. 889 | 890 | :param gamma: The new ``gamma`` you wish to reach. 891 | :type gamma: float 892 | 893 | .. note:: 894 | 895 | There must already be a valid particle approximation. See 896 | :meth:`pysmc.SMC.initialize()` for ways of doing this. 897 | 898 | """ 899 | if self.verbose > 0: 900 | print('-----------------') 901 | print('START SMC MOVE TO') 902 | print('-----------------') 903 | print('initial ', self.gamma_name, ':', self.gamma) 904 | print('final', self.gamma_name, ':', gamma) 905 | print('ess reduction: ', self.ess_reduction) 906 | self.log_Zs = [] 907 | while self.gamma < gamma: 908 | if self.adapt_proposal_step: 909 | self._tune() 910 | new_gamma = self._find_next_gamma(gamma) 911 | log_w = self._get_unormalized_weights_at(new_gamma) 912 | self.log_Z2_Z1 = self._logsumexp(log_w) 913 | self.log_Zs.append(self.log_Z2_Z1) 914 | self._log_w = self._normalize(log_w) 915 | self._ess = self._get_ess_at(self.log_w) 916 | self._set_gamma(new_gamma) 917 | if self.ess < self.ess_threshold * self.num_particles: 918 | if self.verbose > 0: 919 | print('- resampling') 920 | self._resample() 921 | if self.verbose > 0: 922 | print('- moving to', self.gamma_name, ':', self.gamma) 923 | pb = pymc.progressbar.progress_bar((self.num_particles-1) * self.num_mcmc) 924 | print('- performing', self.num_mcmc, 'MCMC step(s) per particle') 925 | print('- ESS = {0:3.2f} %'.format(self.ess / self.num_particles * 100)) 926 | print('- logZ = {0:1.3e}'.format(self.log_Z2_Z1)) 927 | for i in range(self.my_num_particles): 928 | self.mcmc_sampler.set_state(self.particles[i]) 929 | self.mcmc_sampler.sample(self.num_mcmc) 930 | self.particles[i] = self.mcmc_sampler.get_state() 931 | self._total_num_mcmc += self.num_mcmc 932 | if self.verbose > 0: 933 | pb.update(i * self.size * self.num_mcmc) 934 | if self.verbose > 0: 935 | print('') 936 | if self.update_db: 937 | p = self.get_particle_approximation().gather() 938 | sm_params = self.mcmc_sampler.get_params(comm=self.comm) 939 | if self.rank == 0: 940 | self.db.add(self.gamma, p, sm_params) 941 | self.db.commit() 942 | for sm in self.mcmc_sampler.step_methods: 943 | acc_rate = sm.get_acceptance_rate(comm=self.comm) 944 | if self.verbose > 1: 945 | print('- acceptance rate for each step method:') 946 | print('\t-', str(sm), ':', acc_rate) 947 | total_num_mcmc = self.total_num_mcmc 948 | if self.verbose > 0: 949 | print('- total number of MCMC steps:', total_num_mcmc) 950 | print('---------------') 951 | print('END SMC MOVE TO') 952 | print('---------------') 953 | 954 | def get_particle_approximation(self): 955 | """ 956 | Get a :class:`pysmc.ParticleApproximation` representing the current 957 | state of SMC. 958 | 959 | :returns: A particle approximation of the current state. 960 | :rtype: :class:`pysmc.ParticleApproximation` 961 | """ 962 | return ParticleApproximation(log_w=self.log_w, particles=self.particles, 963 | mpi=self.mpi, comm=self.comm) 964 | -------------------------------------------------------------------------------- /pysmc/_step_methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | .. _step_methods: 4 | 5 | ============ 6 | Step Methods 7 | ============ 8 | 9 | In PySMC we define a few step methods for the Metropolis-Hastings algorithm 10 | that extend the capabilities of PyMC. 11 | 12 | Here is a list of what we offer: 13 | 14 | """ 15 | 16 | 17 | __all__ = ['RandomWalk', 'LognormalRandomWalk', 'DiscreteRandomWalk', 18 | 'GaussianMixtureStep'] 19 | 20 | 21 | import pymc as pm 22 | import numpy as np 23 | from numpy.random import poisson as rpoisson 24 | from sklearn import mixture 25 | 26 | 27 | class RandomWalk(pm.Metropolis): 28 | 29 | _adapt_increase_factor = None 30 | 31 | _adapt_decrease_factor = None 32 | 33 | _adapt_upper_ac_rate = None 34 | 35 | _adapt_lower_ac_rate = None 36 | 37 | _min_adaptive_scale_factor = None 38 | 39 | _max_adaptive_scale_factor = None 40 | 41 | _adaptive_scale_factor = None 42 | 43 | _STATE_VARIABLES = None 44 | 45 | _accepted = None 46 | 47 | _rejected = None 48 | 49 | @property 50 | def STATE_VARIABLES(self): 51 | """ 52 | Get the names of the variables required to store the state of the 53 | object. 54 | """ 55 | return self._STATE_VARIABLES 56 | 57 | @property 58 | def adapt_increase_factor(self): 59 | return self._adapt_increase_factor 60 | 61 | @property 62 | def adapt_decrease_factor(self): 63 | return self._adapt_decrease_factor 64 | 65 | @property 66 | def adapt_upper_ac_rate(self): 67 | return self._adapt_upper_ac_rate 68 | 69 | @property 70 | def adapt_lower_ac_rate(self): 71 | return self._adapt_lower_ac_rate 72 | 73 | @property 74 | def min_adaptive_scale_factor(self): 75 | return self._min_adaptive_scale_factor 76 | 77 | @property 78 | def max_adaptive_scale_factor(self): 79 | return self._max_adaptive_scale_factor 80 | 81 | def __init__(self, stochastic, 82 | adapt_increase_factor=1.1, 83 | adapt_decrease_factor=0.7, 84 | adapt_upper_ac_rate=0.7, 85 | adapt_lower_ac_rate=0.3, 86 | min_adaptive_scale_factor=1e-32, 87 | max_adaptive_scale_factor=1e99, 88 | *args, **kwargs): 89 | """Initialize the object.""" 90 | assert adapt_decrease_factor <= 1. 91 | self._adapt_decrease_factor = adapt_decrease_factor 92 | assert adapt_increase_factor >= 1. 93 | self._adapt_increase_factor = adapt_increase_factor 94 | assert adapt_upper_ac_rate <= 1. 95 | assert adapt_lower_ac_rate >= 0. 96 | assert adapt_lower_ac_rate <= adapt_upper_ac_rate 97 | self._adapt_upper_ac_rate = adapt_upper_ac_rate 98 | self._adapt_lower_ac_rate = adapt_lower_ac_rate 99 | assert min_adaptive_scale_factor > 0. 100 | assert min_adaptive_scale_factor <= max_adaptive_scale_factor 101 | self._min_adaptive_scale_factor = min_adaptive_scale_factor 102 | self._max_adaptive_scale_factor = max_adaptive_scale_factor 103 | pm.Metropolis.__init__(self, stochastic, *args, **kwargs) 104 | #super(RandomWalk, self).__init__(stochastic, *args, **kwargs) 105 | self._adaptive_scale_factor = self.adaptive_scale_factor 106 | self._STATE_VARIABLES = ['_adapt_increase_factor', 107 | '_adapt_decrease_factor', 108 | '_adapt_upper_ac_rate', 109 | '_adapt_lower_ac_rate', 110 | '_min_adaptive_scale_factor', 111 | '_max_adaptive_scale_factor', 112 | '_adaptive_scale_factor', 113 | 'adaptive_scale_factor', 114 | 'proposal_sd', 115 | '_old_accepted', 116 | '_old_rejected' 117 | ] 118 | self._old_accepted = 0. 119 | self._old_rejected = 0. 120 | self.proposal_sd = 1e-1 121 | self.adaptive_scale_factor = 1. 122 | 123 | def tune(self, pa=None, comm=None, divergence_threshold=1e10, verbose=0): 124 | ac = self.get_acceptance_rate(comm=comm) 125 | if ac == -1: 126 | return False 127 | self.reset_counters() 128 | use_mpi = comm is not None 129 | rank = comm.Get_rank() if use_mpi else 0 130 | if ac <= self.adapt_lower_ac_rate: 131 | self._adaptive_scale_factor = max(self._adaptive_scale_factor * 132 | self.adapt_decrease_factor, 133 | self.min_adaptive_scale_factor) 134 | elif ac >= self.adapt_upper_ac_rate: 135 | self._adaptive_scale_factor = min(self._adaptive_scale_factor * 136 | self.adapt_increase_factor, 137 | self.max_adaptive_scale_factor) 138 | self.adaptive_scale_factor = self._adaptive_scale_factor 139 | if verbose >= 2 and rank == 0: 140 | print(('\n\t\tadaptive_scale_factor:', self.adaptive_scale_factor)) 141 | return True 142 | 143 | @staticmethod 144 | def competence(s): 145 | """ 146 | Tell PyMC that this step method is better than its random walk. 147 | """ 148 | return 3 149 | 150 | def get_params(self, comm=None): 151 | """ 152 | Get the state of the step method. 153 | """ 154 | state = {} 155 | for var in self.STATE_VARIABLES: 156 | state[var] = getattr(self, var) 157 | if comm is not None: 158 | state['_old_accepted'] = comm.allreduce(state['_old_accepted']) 159 | state['_old_rejected'] = comm.allreduce(state['_old_rejected']) 160 | size = comm.Get_size() 161 | state['_old_accepted'] /= size 162 | state['_old_rejected'] /= size 163 | return state 164 | 165 | def set_params(self, state): 166 | """ 167 | Set the state from a dictionary. 168 | """ 169 | for var in list(state.keys()): 170 | setattr(self, var, state[var]) 171 | self._old_accepted *= -1. 172 | self._old_rejected *= -1. 173 | 174 | def reset_counters(self): 175 | """ 176 | Reset the counters that count accepted and rejected steps. 177 | """ 178 | self._old_accepted = self.accepted 179 | self._old_rejected = self.rejected 180 | 181 | def get_acceptance_rate(self, comm=None): 182 | """ 183 | Get the acceptance rate of the step method sm. 184 | """ 185 | accepted = self.accepted - self._old_accepted 186 | rejected = self.rejected - self._old_rejected 187 | if comm is not None: 188 | accepted = comm.allreduce(accepted) 189 | rejected = comm.allreduce(rejected) 190 | if (accepted + rejected) == 0.: 191 | return -1 192 | return accepted / (accepted + rejected) 193 | 194 | 195 | class LognormalRandomWalk(RandomWalk): 196 | """ 197 | This is a step method class that is good for positive random variables. 198 | It is a essentially a random walk in the logarithmic scale. 199 | 200 | **Base class:** :class:`pm.Metropolis` 201 | """ 202 | 203 | def __init__(self, stochastic, 204 | adapt_increase_factor=1.3, 205 | adapt_decrease_factor=0.7, 206 | adapt_upper_ac_rate=0.7, 207 | adapt_lower_ac_rate=0.3, 208 | min_adaptive_scale_factor=1e-32, 209 | max_adaptive_scale_factor=1e99, 210 | *args, **kwargs): 211 | """Initialize the object.""" 212 | super(LognormalRandomWalk, self).__init__( 213 | stochastic, 214 | adapt_increase_factor=adapt_increase_factor, 215 | adapt_decrease_factor=adapt_decrease_factor, 216 | adapt_upper_ac_rate=adapt_upper_ac_rate, 217 | adapt_lower_ac_rate=adapt_lower_ac_rate, 218 | min_adaptive_scale_factor=min_adaptive_scale_factor, 219 | max_adaptive_scale_factor=max_adaptive_scale_factor, 220 | *args, **kwargs) 221 | @staticmethod 222 | def competence(s): 223 | """ 224 | Tell PyMC that this step method is better than its random walk. 225 | """ 226 | if isinstance(s, pm.Lognormal) or \ 227 | isinstance(s, pm.Exponential): 228 | return 5 229 | 230 | def propose(self): 231 | """ 232 | Propose a move. 233 | """ 234 | tau = (self.adaptive_scale_factor * self.proposal_sd) ** 2 235 | self.stochastic.value = \ 236 | pm.rlognormal(np.log(self.stochastic.value), tau) 237 | 238 | def hastings_factor(self): 239 | """ 240 | Compute the hastings factor. 241 | """ 242 | tau = (self.adaptive_scale_factor * self.proposal_sd) ** 2 243 | cur_val = self.stochastic.value 244 | last_val = self.stochastic.last_value 245 | 246 | lp_for = pm.lognormal_like(cur_val, mu=np.log(last_val), tau=tau) 247 | lp_bak = pm.lognormal_like(last_val, mu=np.log(cur_val), tau=tau) 248 | 249 | if self.verbose > 1: 250 | print((self._id + ': Hastings factor %f' % (lp_bak - lp_for))) 251 | return lp_bak - lp_for 252 | 253 | 254 | class GaussianMixtureStep(RandomWalk): 255 | """ 256 | This is a test. 257 | """ 258 | 259 | # A gaussian mixtures model 260 | _gmm = None 261 | 262 | # Covariance type 263 | _covariance_type = None 264 | 265 | # The valid covariance types 266 | _VALID_COVARIANCE_TYPES = None 267 | 268 | # Maximum number of components for the Gaussian mixture 269 | _n_components = None 270 | 271 | # The number of iterations we should do while training the 272 | # Gaussian mixture 273 | _n_iter = None 274 | 275 | # The mean when we do scale 276 | _mean = None 277 | 278 | # The std when we do scale 279 | _std = None 280 | 281 | @property 282 | def n_iter(self): 283 | """ 284 | Get the number of Gaussian mixture training iterations. 285 | """ 286 | return self._n_iter 287 | 288 | @property 289 | def n_components(self): 290 | """ 291 | Get the maximum number of Gaussian mixture components. 292 | """ 293 | return self._n_components 294 | 295 | @property 296 | def VALID_COVARIANCE_TYPES(self): 297 | """ 298 | Get the valid covariance types. 299 | """ 300 | return self._VALID_COVARIANCE_TYPES 301 | 302 | @property 303 | def covariance_type(self): 304 | """ 305 | Get the covariance type. 306 | """ 307 | return self._covariance_type 308 | 309 | @property 310 | def gmm(self): 311 | """ 312 | Get the Gaussian process model. 313 | """ 314 | return self._gmm 315 | 316 | def __init__(self, stochastic, 317 | adapt_upper_ac_rate=1., 318 | adapt_lower_ac_rate=0.5, 319 | covariance_type='full', 320 | n_components=50, 321 | n_iter=1000, 322 | *args, **kwargs): 323 | """Initialize the object.""" 324 | RandomWalk.__init__(self, 325 | stochastic, 326 | adapt_upper_ac_rate=adapt_upper_ac_rate, 327 | adapt_lower_ac_rate=adapt_lower_ac_rate, 328 | *args, **kwargs) 329 | self._VALID_COVARIANCE_TYPES = ['diag', 'full', 'spherical', 'tied'] 330 | assert covariance_type in self.VALID_COVARIANCE_TYPES 331 | self._covariance_type = covariance_type 332 | n_components = int(n_components) 333 | assert n_components >= 1 334 | self._n_components = n_components 335 | n_iter = int(n_iter) 336 | assert n_iter >= 0 337 | self._n_iter = n_iter 338 | self._tuned = False 339 | self._STATE_VARIABLES += ['_covariance_type', 340 | '_n_components', 341 | '_n_iter', 342 | '_tuned', 343 | '_gmm', 344 | '_mean', 345 | '_std'] 346 | self._accepted = 0. 347 | self._rejected = 0. 348 | 349 | def propose(self): 350 | """ 351 | Propose a move. 352 | """ 353 | if not self._tuned: 354 | return super(GaussianMixtureStep, self).propose() 355 | x = self.gmm.sample() 356 | self.stochastic.value = x.flatten('F') 357 | 358 | def hastings_factor(self): 359 | """ 360 | Compute the hastings factor. 361 | """ 362 | 363 | if not self._tuned: 364 | return super(GaussianMixtureStep, self).hastings_factor() 365 | 366 | cur_val = np.atleast_2d(self.stochastic.value) 367 | last_val = np.atleast_2d(self.stochastic.last_value) 368 | 369 | lp_for = self._gmm.score(cur_val)[0] 370 | lp_bak = self._gmm.score(last_val)[0] 371 | 372 | if self.verbose > 1: 373 | print((self._id + ': Hastings factor %f' % (lp_bak - lp_for))) 374 | return lp_bak - lp_for 375 | 376 | def tune(self, pa=None, comm=None, divergence_threshold=1e10, verbose=0): 377 | """ 378 | Tune the step... 379 | """ 380 | if pa is None: 381 | raise RuntimeError('This step method works only in pysmc.') 382 | ac = self.get_acceptance_rate(comm=comm) 383 | if ac == -1: 384 | return False 385 | self.reset_counters() 386 | if (self._tuned and 387 | ac >= self.adapt_lower_ac_rate and 388 | ac <= self.adapt_upper_ac_rate): 389 | return False 390 | use_mpi = comm is not None 391 | if use_mpi: 392 | rank = comm.Get_rank() 393 | size = comm.Get_size() 394 | else: 395 | rank = 0 396 | size = 1 397 | pa = pa.gather() 398 | # Only the root should run train the mixture 399 | if rank == 0: 400 | pa.resample() 401 | data = [pa.particles[i]['stochastics'][self.stochastic.__name__] 402 | for i in range(pa.num_particles)] 403 | data = np.array(data, dtype='float') 404 | if data.ndim == 1: 405 | data = np.atleast_2d(data).T 406 | self._gmm = mixture.DPGMM(n_components=self.n_components, 407 | covariance_type=self.covariance_type, 408 | n_iter=self.n_iter) 409 | self.gmm.fit(data) 410 | Y_ = self.gmm.predict(data) 411 | n_comp = 0 412 | for i in range(self.n_components): 413 | if np.any(Y_ == i): 414 | n_comp += 1 415 | self._gmm = mixture.GMM(n_components=n_comp, 416 | covariance_type=self.covariance_type, 417 | n_iter=self.n_iter) 418 | self.gmm.fit(data) 419 | Y_ = self.gmm.predict(data) 420 | if verbose >= 2: 421 | for i, (mean, covar) in enumerate(zip( 422 | self.gmm.means_, self.gmm._get_covars())): 423 | if not np.any(Y_ == i): 424 | continue 425 | print(('\n', mean, covar)) 426 | if use_mpi: 427 | self._gmm = comm.bcast(self._gmm) 428 | self.gmm.covars_ = self.gmm._get_covars() 429 | self._tuned = True 430 | return True 431 | 432 | 433 | class DiscreteRandomWalk(RandomWalk): 434 | """ 435 | This is a step method class that is good for discrete random variables. 436 | 437 | Good only for non-negative discrete random variables. 438 | 439 | **Base class:** :class:`pysmc.RandomWalk` 440 | """ 441 | 442 | def __init__(self, stochastic, 443 | prop_dist='poisson', 444 | adapt_increase_factor=1.3, 445 | adapt_decrease_factor=0.7, 446 | adapt_upper_ac_rate=0.7, 447 | adapt_lower_ac_rate=0.3, 448 | min_adaptive_scale_factor=1e-32, 449 | max_adaptive_scale_factor=1e99, 450 | *args, **kwargs): 451 | """Initialize the object.""" 452 | super(DiscreteRandomWalk, self).__init__( 453 | stochastic, 454 | adapt_increase_factor=adapt_increase_factor, 455 | adapt_decrease_factor=adapt_decrease_factor, 456 | adapt_upper_ac_rate=adapt_upper_ac_rate, 457 | adapt_lower_ac_rate=adapt_lower_ac_rate, 458 | min_adaptive_scale_factor=min_adaptive_scale_factor, 459 | max_adaptive_scale_factor=max_adaptive_scale_factor, 460 | *args, **kwargs) 461 | self.prop_dist = prop_dist 462 | self._STATE_VARIABLES.append('prop_dist') 463 | 464 | def propose(self): 465 | """ 466 | Propose a move. 467 | """ 468 | if self.prop_dist == 'poisson': 469 | k = self.stochastic.value.shape 470 | new_val = self.stochastic.value + rpoisson( 471 | self.adaptive_scale_factor * self.proposal_sd) * ( 472 | -np.ones(k)) ** (np.random.random(k) > 0.5) 473 | self.stochastic.value = np.abs(new_val) 474 | elif self.prop_dist == 'prior': 475 | self.stochastic.random() 476 | 477 | 478 | # Assign methods to the registry of pymc 479 | pm.StepMethodRegistry = [] 480 | for method in __all__: 481 | pm.StepMethodRegistry.append(eval(method)) 482 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from numpy.distutils.core import setup 5 | 6 | 7 | setup(name='PySMC', 8 | description='Sequential Monte Carlo (SMC) for sampling complicated probability densities.', 9 | author='Ilias Bilionis', 10 | author_email='ibilion@purdue.edu', 11 | url='https://github.com/ebilionis/pysmc', 12 | download_url='https://github.com/ebilionis/pysmc/tarball/1.0', 13 | keywords=['sequential monte carlo', 'markov chain monte carlo', 'metropolis-hastings', 14 | 'multimodal probability densities', 'particle methods'], 15 | version='2.1', 16 | packages=['pysmc']) 17 | --------------------------------------------------------------------------------