├── requirements.txt ├── demo ├── sine.png ├── gauss.png ├── sin_cobwebs.png └── demo.py ├── chaotic_maps ├── __init__.py ├── map_library.py └── visualizations.py ├── setup.py ├── LICENSE ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | -------------------------------------------------------------------------------- /demo/sine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eowagner/chaotic_maps/HEAD/demo/sine.png -------------------------------------------------------------------------------- /demo/gauss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eowagner/chaotic_maps/HEAD/demo/gauss.png -------------------------------------------------------------------------------- /chaotic_maps/__init__.py: -------------------------------------------------------------------------------- 1 | from .map_library import * 2 | from .visualizations import * 3 | 4 | -------------------------------------------------------------------------------- /demo/sin_cobwebs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eowagner/chaotic_maps/HEAD/demo/sin_cobwebs.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='chaotic_maps', 4 | version='0.1', 5 | description='Easily create cobweb plots and bifurcations diagrams for one-dimensional maps.', 6 | url='http://github.com/eowagner/chaotic_maps', 7 | author='Elliott Wagner', 8 | author_email='e.o.wagner@gmail.com', 9 | license='MIT', 10 | packages=['chaotic_maps'], 11 | install_requires=[ 12 | 'numpy', 13 | 'scipy', 14 | 'matplotlib' 15 | ], 16 | zip_safe=False) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 eowagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/demo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import chaotic_maps as cm 4 | 5 | fig, axes = plt.subplots(2, 2) 6 | cm.cobweb_plot(cm.sin_map(.7), .1, 50, domain=np.linspace(0, 1), ax=axes[0][0], ylabel=r'$x_{n+1}$') 7 | cm.cobweb_plot(cm.sin_map(.8), .1, 10, domain=np.linspace(0, 1), ax=axes[0][1]) 8 | cm.cobweb_plot(cm.sin_map(.85), .1, 50, domain=np.linspace(0, 1), ax=axes[1][0], xlabel=r'$x_n$', ylabel=r'$x_{n+1}$') 9 | cm.cobweb_plot(cm.sin_map(.9), .1, 150, domain=np.linspace(0, 1), ax=axes[1][1], xlabel=r'$x_n$') 10 | axes[0][0].set_title(r'$\mu = 0.7$') 11 | axes[0][1].set_title(r'$\mu = 0.8$') 12 | axes[1][0].set_title(r'$\mu = 0.85$') 13 | axes[1][1].set_title(r'$\mu = 0.9$') 14 | plt.tight_layout() 15 | plt.savefig("sin_cobwebs.png", dpi=300) 16 | 17 | 18 | 19 | cm.bifurcation_and_lyapunov_plots( 20 | cm.sin_map, 21 | init=0.2, 22 | parameter_range=np.linspace(0.6, 1, num=10000), 23 | deriv_generator=cm.sin_map_deriv, 24 | y_points=500, 25 | xlabel=r"$\mu$", 26 | ylabel=r"$x$", 27 | file_name="sine.png", 28 | ) 29 | 30 | fig, axes = plt.subplots(1,2, figsize=(12,5)) 31 | 32 | cm.bifurcation_plot( 33 | cm.gauss_map_family(alpha=5), 34 | init=0.1, 35 | parameter_range=np.linspace(-0.9, 1, 10000), 36 | y_points=500, 37 | xlabel=r"$\beta$", 38 | ylabel=r"$x$", 39 | set_title=r"$\alpha = 5$", 40 | ax=axes[0] 41 | ) 42 | 43 | cm.bifurcation_plot( 44 | cm.gauss_map_family(alpha=6.5), 45 | init=0.1, 46 | parameter_range=np.linspace(-0.9, 1, 10000), #10000 47 | y_points=500, 48 | xlabel=r"$\beta$", 49 | ylabel=r"$x$", 50 | set_title=r"$\alpha = 6.5$", 51 | ax=axes[1] 52 | ) 53 | 54 | plt.savefig("gauss.png", dpi=300) 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /chaotic_maps/map_library.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def logistic_map(r): 5 | def dynamic(x): 6 | return r * x * (1 - x) 7 | 8 | return dynamic 9 | 10 | 11 | def logistic_map_deriv(r): 12 | def deriv(x): 13 | return r - 2 * r * x 14 | 15 | return deriv 16 | 17 | 18 | def gauss_map_family(alpha): 19 | def generator(beta): 20 | def map(x): 21 | return np.exp(-1 * alpha * (x ** 2)) + beta 22 | 23 | return map 24 | 25 | return generator 26 | 27 | 28 | def sin_map(mu): 29 | def dynamic(x): 30 | return mu * np.sin(np.pi * x) 31 | 32 | return dynamic 33 | 34 | 35 | def sin_map_deriv(mu): 36 | def deriv(x): 37 | return mu * np.cos(np.pi * x) * np.pi 38 | 39 | return deriv 40 | 41 | 42 | def cos_map(mu): 43 | def dynamic(x): 44 | return mu * np.cos(np.pi * x) 45 | 46 | return dynamic 47 | 48 | 49 | def cos_map_deriv(mu): 50 | def deriv(x): 51 | return -1 * mu * np.sin(np.pi * x) * np.pi 52 | 53 | 54 | def tent_map(mu): 55 | def dynamic(x): 56 | if x[0] < 0.5: 57 | return mu * x 58 | else: 59 | return mu * (1 - x) 60 | 61 | return dynamic 62 | 63 | 64 | def cubic_map(r): 65 | def dynamic(x): 66 | return r * x - x ** 3 67 | 68 | return dynamic 69 | 70 | 71 | def henon_map_family(b): 72 | def generator(a): 73 | def dynamic(x): 74 | # Yields overflow errors in numpy after more than ~25 iterations 75 | prime = np.array((1 - (a * x[0] * x[0]) + (b * x[1]), x[0])) 76 | return prime 77 | 78 | return dynamic 79 | 80 | return generator 81 | 82 | 83 | def discrete_time_imitative_logit_family(A): 84 | A = np.array(A) 85 | 86 | def generator(beta): 87 | def dynamic(x): 88 | u = (x, 1 - x) 89 | adjusted_payoffs = np.exp(A.dot(u) * beta) 90 | bar = np.inner(u, adjusted_payoffs) 91 | top = np.multiply(u, adjusted_payoffs) 92 | return top[0] / bar 93 | 94 | return dynamic 95 | 96 | return generator 97 | 98 | 99 | def discrete_time_replicator(A): 100 | A = np.array(A) 101 | 102 | def dynamic(x): 103 | u = (x, 1 - x) 104 | payoffs = A.dot(u) 105 | bar = np.inner(u, payoffs) 106 | return x * payoffs[0] / bar 107 | 108 | return dynamic 109 | 110 | 111 | def smoothed_best_response_family(A): 112 | A = np.array(A) 113 | 114 | def generator(beta): 115 | def dynamic(x): 116 | u = (x, 1 - x) 117 | smoothed_payoffs = np.exp(A.dot(u) * beta) 118 | denom = sum(smoothed_payoffs) 119 | return smoothed_payoffs[0] / denom 120 | 121 | return dynamic 122 | 123 | return generator 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chaotic_maps 2 | A python library for creating cobweb plots, bifurcation diagrams, and calculating the Lyapunov exponents of one-dimensional maps. 3 | 4 | The library includes several common one-dimensional maps and discrete-time evolutionary/learning dynamics such as the replicator dynamic. 5 | 6 | When calculating Lyapunov exponents you can either pass the map's first-order derivative, or the plotting function can calculate the derivative using scipy.misc.derivative. Unfortunately, scipy's derivate function can be very slow and may raise errors depending on the map's shape. 7 | 8 | # Bifurcation diagram with Lyapunov exponents 9 | 10 | To create a bifurcation diagram paired with Lyapunov exponents for the sine map... 11 | 12 | ```python 13 | import numpy as np 14 | import chaotic_maps as cm 15 | 16 | cm.bifurcation_and_lyapunov_plots( 17 | cm.sin_map, 18 | init=0.2, 19 | parameter_range=np.linspace(0.6, 1, num=10000), 20 | deriv_generator=cm.sin_map_deriv, 21 | y_points=500, 22 | xlabel=r"$\mu$", 23 | ylabel=r"$x$", 24 | file_name="sine.png", 25 | ) 26 | 27 | ``` 28 | 29 | ![The Sine Map](https://github.com/eowagner/chaotic_maps/raw/master/demo/sine.png) 30 | 31 | # Two bifurcation diagrams side-by-side 32 | 33 | The package can generate whole plots, or you can feed it axes to populate. For example, the following will show two bifurcation diagrams fror the Gauss Iterated Map side-by-side... 34 | 35 | ```python 36 | import numpy as np 37 | import matplotlib.pyplot as plt 38 | import chaotic_maps as cm 39 | 40 | fig, axes = plt.subplots(1,2, figsize=(12,5)) 41 | 42 | cm.bifurcation_plot( 43 | cm.gauss_map_family(alpha=5), 44 | init=0.1, 45 | parameter_range=np.linspace(-0.9, 1, 10000), 46 | y_points=500, 47 | xlabel=r"$\beta$", 48 | ylabel=r"$x$", 49 | set_title=r"$\alpha = 5$", 50 | ax=axes[0] 51 | ) 52 | 53 | cm.bifurcation_plot( 54 | cm.gauss_map_family(alpha=6.5), 55 | init=0.1, 56 | parameter_range=np.linspace(-0.9, 1, 10000), 57 | y_points=500, 58 | xlabel=r"$\beta$", 59 | ylabel=r"$x$", 60 | set_title=r"$\alpha = 6.5$", 61 | ax=axes[1] 62 | ) 63 | 64 | plt.savefig("gauss.png", dpi=300) 65 | ``` 66 | 67 | ![The Gauss Iterated Map](https://raw.githubusercontent.com/eowagner/chaotic_maps/master/demo/gauss.png) 68 | 69 | # Cobweb plots 70 | 71 | You can also use chaotic_maps to create cobweb plots. Here are four cobweb plots for the Sine map showing long-run behavior under different parameters. 72 | 73 | ```python 74 | import numpy as np 75 | import matplotlib.pyplot as plt 76 | import chaotic_maps as cm 77 | 78 | fig, axes = plt.subplots(2, 2) 79 | 80 | cm.cobweb_plot(cm.sin_map(.7), .1, 50, domain=np.linspace(0, 1), ax=axes[0][0], ylabel=r'$x_{n+1}$') 81 | cm.cobweb_plot(cm.sin_map(.8), .1, 10, domain=np.linspace(0, 1), ax=axes[0][1]) 82 | cm.cobweb_plot(cm.sin_map(.85), .1, 50, domain=np.linspace(0, 1), ax=axes[1][0], xlabel=r'$x_n$', ylabel=r'$x_{n+1}$') 83 | cm.cobweb_plot(cm.sin_map(.9), .1, 150, domain=np.linspace(0, 1), ax=axes[1][1], xlabel=r'$x_n$') 84 | 85 | axes[0][0].set_title(r'$\mu = 0.7$') 86 | axes[0][1].set_title(r'$\mu = 0.8$') 87 | axes[1][0].set_title(r'$\mu = 0.85$') 88 | axes[1][1].set_title(r'$\mu = 0.9$') 89 | 90 | plt.tight_layout() 91 | plt.savefig("sin_cobwebs.png", dpi=300) 92 | ``` 93 | 94 | ![The Sine Map](https://github.com/eowagner/chaotic_maps/raw/master/demo/sin_cobwebs.png) 95 | -------------------------------------------------------------------------------- /chaotic_maps/visualizations.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from scipy.misc import derivative 4 | 5 | def bifurcation_data_fixed_x( 6 | f, init, y_points=100, dropped_steps=2000, lyapunov=False, deriv=None, **kwargs 7 | ): 8 | x = init 9 | lyap = 0 10 | 11 | for i in range(dropped_steps): 12 | x = f(x) 13 | 14 | res = np.zeros(y_points) 15 | 16 | for i in range(y_points): 17 | x = f(x) 18 | res[i] = x 19 | 20 | if lyapunov: 21 | if deriv is None: 22 | lyap += np.log(np.absolute(derivative(f, x))) 23 | else: 24 | lyap += np.log(np.absolute(deriv(x))) 25 | 26 | lyap /= dropped_steps + y_points 27 | 28 | return res, lyap 29 | 30 | 31 | def bifurcation_plot_data( 32 | dynamic_generator, init, parameter_range, y_points=100, **kwargs 33 | ): 34 | x_list = np.array([]) 35 | y_list = np.array([]) 36 | lyap_x = [] 37 | lyap_y = [] 38 | 39 | for p in parameter_range: 40 | f = dynamic_generator(p) 41 | 42 | deriv = None 43 | if "deriv_generator" in kwargs: 44 | deriv = kwargs["deriv_generator"](p) 45 | 46 | y_res, lyap = bifurcation_data_fixed_x( 47 | f, init=init, y_points=y_points, deriv=deriv, **kwargs 48 | ) 49 | 50 | x_pad = np.full(y_points, p) 51 | x_list = np.append(x_list, x_pad) 52 | y_list = np.append(y_list, y_res) 53 | 54 | lyap_x.append(p) 55 | lyap_y.append(lyap) 56 | 57 | return (x_list, y_list, lyap_x, lyap_y) 58 | 59 | 60 | def bifurcation_and_lyapunov_plots(dynamic_generator, init, parameter_range, **kwargs): 61 | 62 | if not "alpha" in kwargs: 63 | kwargs["alpha"] = 0.25 64 | 65 | bif_x, bif_y, lya_x, lya_y = bifurcation_plot_data( 66 | dynamic_generator, 67 | init=init, 68 | parameter_range=parameter_range, 69 | lyapunov=True, 70 | **kwargs 71 | ) 72 | 73 | fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 9)) 74 | 75 | ax1.plot(bif_x, bif_y, "k,", alpha=kwargs["alpha"]) 76 | ax1.set_title("Bifurcation Diagram") 77 | ax1.set_xlim(parameter_range[0], parameter_range[-1]) 78 | 79 | if "xlabel" in kwargs: 80 | plt.xlabel(kwargs["xlabel"]) 81 | 82 | if "ylabel" in kwargs: 83 | plt.ylabel(kwargs["ylabel"]) 84 | 85 | ax2.plot(lya_x, lya_y, "k-", linewidth=1) 86 | ax2.set_title("Lyapunov Exponents") 87 | ax2.axhline(color="k", linewidth=0.5) 88 | ax2.set_xlim(parameter_range[0], parameter_range[-1]) 89 | 90 | if "xlabel" in kwargs: 91 | plt.xlabel(kwargs["xlabel"]) 92 | 93 | if "xlabel" in kwargs: 94 | plt.ylabel("Lyapunov exponent") 95 | 96 | if "suptitle" in kwargs: 97 | fig.suptitle(kwargs["suptitle"]) 98 | 99 | plt.tight_layout() 100 | 101 | if "file_name" in kwargs: 102 | plt.savefig(kwargs["file_name"], dpi=300) 103 | else: 104 | plt.show() 105 | 106 | 107 | def bifurcation_plot( 108 | dynamic_generator, 109 | init=np.array([0.4]), 110 | parameter_range=np.linspace(0, 1, num=1000), 111 | **kwargs 112 | ): 113 | if not "alpha" in kwargs: 114 | kwargs["alpha"] = 0.25 115 | 116 | if not "figsize" in kwargs: 117 | kwargs["figsize"] = (8, 4.5) 118 | 119 | if not "ax" in kwargs: 120 | fig, ax1 = plt.subplots(1, 1, figsize=kwargs["figsize"]) 121 | else: 122 | ax1 = kwargs['ax'] 123 | 124 | bif_x, bif_y, lya_x, lya_y = bifurcation_plot_data( 125 | dynamic_generator, init=init, parameter_range=parameter_range, **kwargs 126 | ) 127 | 128 | 129 | ax1.plot(bif_x, bif_y, "k,", alpha=kwargs["alpha"]) 130 | 131 | if "set_title" in kwargs: 132 | ax1.set_title(kwargs["set_title"]) 133 | 134 | ax1.set_xlim(parameter_range[0], parameter_range[-1]) 135 | 136 | if "xlabel" in kwargs: 137 | ax1.set_xlabel(kwargs["xlabel"]) 138 | 139 | if "ylabel" in kwargs: 140 | ax1.set_ylabel(kwargs["ylabel"]) 141 | 142 | plt.tight_layout() 143 | 144 | if "file_name" in kwargs: 145 | plt.savefig(kwargs["file_name"], dpi=300) 146 | elif not "ax" in kwargs: 147 | plt.show() 148 | 149 | 150 | def cobweb_plot(dynamic, init, iterations, domain=np.linspace(0, 1), **kwargs): 151 | 152 | if not "alpha" in kwargs: 153 | kwargs["alpha"] = 0.5 154 | 155 | if not "ax" in kwargs: 156 | fig, ax = plt.subplots(1, 1) 157 | else: 158 | ax = kwargs["ax"] 159 | 160 | # Plot the map 161 | func = list(map(dynamic, domain)) 162 | # ax.plot(domain, dynamic(domain), 'k', linewidth=2) # fails on maps that operate on vectors 163 | ax.plot(domain, func, "k", linewidth=2) 164 | 165 | # Plot the y=x line of reflection 166 | ax.plot([domain[0], domain[-1]], [domain[0], domain[-1]], "k--", linewidth=1) 167 | 168 | x = init 169 | 170 | for i in range(iterations): 171 | xPrime = dynamic(x) 172 | 173 | # from reflection line to curve 174 | ax.plot([x, x], [x, xPrime], "k", linewidth=kwargs["alpha"]) 175 | 176 | # from curve to reflection line 177 | ax.plot([x, xPrime], [xPrime, xPrime], "k", linewidth=1) 178 | 179 | ax.plot([x], [xPrime], "ok", alpha=kwargs["alpha"]) 180 | 181 | x = xPrime 182 | 183 | if "xlabel" in kwargs: 184 | ax.set_xlabel(kwargs["xlabel"]) 185 | 186 | if "ylabel" in kwargs: 187 | ax.set_ylabel(kwargs["ylabel"]) 188 | 189 | if "set_title" in kwargs: 190 | ax.set_title(kwargs["set_title"]) 191 | 192 | if not "ax" and not "file_name" in kwargs: 193 | plt.show() 194 | elif "file_name" in kwargs: 195 | plt.savefig(kwargs["file_name"], dpi=300) 196 | --------------------------------------------------------------------------------