Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/jupyter/modsim.py
Views: 531
"""1Code from Modeling and Simulation in Python.23Copyright 2020 Allen Downey45MIT License: https://opensource.org/licenses/MIT6"""78import logging910logger = logging.getLogger(name="modsim.py")1112# make sure we have Python 3.6 or better13import sys1415if sys.version_info < (3, 6):16logger.warning("modsim.py depends on Python 3.6 features.")1718import inspect1920import matplotlib.pyplot as plt21import numpy as np22import pandas as pd23import scipy2425from scipy.interpolate import interp1d26from scipy.interpolate import InterpolatedUnivariateSpline2728from scipy.integrate import odeint29from scipy.integrate import solve_ivp3031from types import SimpleNamespace32from copy import copy3334import pint3536units = pint.UnitRegistry()37Quantity = units.Quantity383940def flip(p=0.5):41"""Flips a coin with the given probability.4243p: float 0-14445returns: boolean (True or False)46"""47return np.random.random() < p484950def cart2pol(x, y, z=None):51"""Convert Cartesian coordinates to polar.5253x: number or sequence54y: number or sequence55z: number or sequence (optional)5657returns: theta, rho OR theta, rho, z58"""59x = np.asarray(x)60y = np.asarray(y)6162rho = np.hypot(x, y)63theta = np.arctan2(y, x)6465if z is None:66return theta, rho67else:68return theta, rho, z697071def pol2cart(theta, rho, z=None):72"""Convert polar coordinates to Cartesian.7374theta: number or sequence in radians75rho: number or sequence76z: number or sequence (optional)7778returns: x, y OR x, y, z79"""80x = rho * np.cos(theta)81y = rho * np.sin(theta)8283if z is None:84return x, y85else:86return x, y, z8788from numpy import linspace8990def linrange(start, stop, step=1, **options):91"""Make an array of equally spaced values.9293start: first value94stop: last value (might be approximate)95step: difference between elements (should be consistent)9697returns: NumPy array98"""99n = int(round((stop-start) / step))100return linspace(start, stop, n+1, **options)101102103def leastsq(error_func, x0, *args, **options):104"""Find the parameters that yield the best fit for the data.105106`x0` can be a sequence, array, Series, or Params107108Positional arguments are passed along to `error_func`.109110Keyword arguments are passed to `scipy.optimize.leastsq`111112error_func: function that computes a sequence of errors113x0: initial guess for the best parameters114args: passed to error_func115options: passed to leastsq116117:returns: Params object with best_params and ModSimSeries with details118"""119# override `full_output` so we get a message if something goes wrong120options["full_output"] = True121122# run leastsq123t = scipy.optimize.leastsq(error_func, x0=x0, args=args, **options)124best_params, cov_x, infodict, mesg, ier = t125126# pack the results into a ModSimSeries object127details = ModSimSeries(infodict)128details.set(cov_x=cov_x, mesg=mesg, ier=ier)129130# if we got a Params object, we should return a Params object131if isinstance(x0, Params):132best_params = Params(Series(best_params, x0.index))133134# return the best parameters and details135return best_params, details136137138def minimize_scalar(min_func, bounds, *args, **options):139"""Finds the input value that minimizes `min_func`.140141Wrapper for142https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html143144min_func: computes the function to be minimized145bounds: sequence of two values, lower and upper bounds of the range to be searched146args: any additional positional arguments are passed to min_func147options: any keyword arguments are passed as options to minimize_scalar148149returns: ModSimSeries object150"""151try:152min_func(bounds[0], *args)153except Exception as e:154msg = """Before running scipy.integrate.minimize_scalar, I tried155running the function you provided with the156lower bound, and I got the following error:"""157logger.error(msg)158raise (e)159160underride(options, xatol=1e-3)161162res = scipy.optimize.minimize_scalar(163min_func,164bracket=bounds,165bounds=bounds,166args=args,167method="bounded",168options=options,169)170171if not res.success:172msg = (173"""scipy.optimize.minimize_scalar did not succeed.174The message it returned is %s"""175% res.message176)177raise Exception(msg)178179return res180181182def maximize_scalar(max_func, bounds, *args, **options):183"""Finds the input value that maximizes `max_func`.184185Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html186187min_func: computes the function to be maximized188bounds: sequence of two values, lower and upper bounds of the189range to be searched190args: any additional positional arguments are passed to max_func191options: any keyword arguments are passed as options to minimize_scalar192193returns: ModSimSeries object194"""195def min_func(*args):196return -max_func(*args)197198res = minimize_scalar(min_func, bounds, *args, **options)199200# we have to negate the function value before returning res201res.fun = -res.fun202return res203204205def minimize_golden(min_func, bracket, *args, **options):206"""Find the minimum of a function by golden section search.207208Based on209https://en.wikipedia.org/wiki/Golden-section_search#Iterative_algorithm210211:param min_func: function to be minimized212:param bracket: interval containing a minimum213:param args: arguments passes to min_func214:param options: rtol and maxiter215216:return: ModSimSeries217"""218maxiter = options.get("maxiter", 100)219rtol = options.get("rtol", 1e-3)220221def success(**kwargs):222return ModSimSeries(dict(success=True, **kwargs))223224def failure(**kwargs):225return ModSimSeries(dict(success=False, **kwargs))226227a, b = bracket228ya = min_func(a, *args)229yb = min_func(b, *args)230231phi = 2 / (np.sqrt(5) - 1)232h = b - a233c = b - h / phi234yc = min_func(c, *args)235236d = a + h / phi237yd = min_func(d, *args)238239if yc > ya or yc > yb:240return failure(message="The bracket is not well-formed.")241242for i in range(maxiter):243244# check for convergence245if abs(h / c) < rtol:246return success(x=c, fun=yc)247248if yc < yd:249b, yb = d, yd250d, yd = c, yc251h = b - a252c = b - h / phi253yc = min_func(c, *args)254else:255a, ya = c, yc256c, yc = d, yd257h = b - a258d = a + h / phi259yd = min_func(d, *args)260261# if we exited the loop, too many iterations262return failure(root=c, message="maximum iterations = %d exceeded" % maxiter)263264265def maximize_golden(max_func, bracket, *args, **options):266"""Find the maximum of a function by golden section search.267268:param min_func: function to be maximized269:param bracket: interval containing a maximum270:param args: arguments passes to min_func271:param options: rtol and maxiter272273:return: ModSimSeries274"""275276def min_func(*args):277return -max_func(*args)278279res = minimize_golden(min_func, bracket, *args, **options)280281# we have to negate the function value before returning res282res.fun = -res.fun283return res284285286def minimize_powell(min_func, x0, *args, **options):287"""Finds the input value that minimizes `min_func`.288Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html289min_func: computes the function to be minimized290x0: initial guess291args: any additional positional arguments are passed to min_func292options: any keyword arguments are passed as options to minimize_scalar293returns: ModSimSeries object294"""295underride(options, tol=1e-3)296297res = scipy.optimize.minimize(min_func, x0, *args, **options)298299return ModSimSeries(res)300301302# make aliases for minimize and maximize303minimize = minimize_golden304maximize = maximize_golden305306307def run_solve_ivp(system, slope_func, **options):308"""Computes a numerical solution to a differential equation.309310`system` must contain `init` with initial conditions,311`t_0` with the start time, and `t_end` with the end time.312313It can contain any other parameters required by the slope function.314315`options` can be any legal options of `scipy.integrate.solve_ivp`316317system: System object318slope_func: function that computes slopes319320returns: TimeFrame321"""322system = remove_units(system)323324# make sure `system` contains `init`325if not hasattr(system, "init"):326msg = """It looks like `system` does not contain `init`327as a system variable. `init` should be a State328object that specifies the initial condition:"""329raise ValueError(msg)330331# make sure `system` contains `t_end`332if not hasattr(system, "t_end"):333msg = """It looks like `system` does not contain `t_end`334as a system variable. `t_end` should be the335final time:"""336raise ValueError(msg)337338# the default value for t_0 is 0339t_0 = getattr(system, "t_0", 0)340341# try running the slope function with the initial conditions342try:343slope_func(t_0, system.init, system)344except Exception as e:345msg = """Before running scipy.integrate.solve_ivp, I tried346running the slope function you provided with the347initial conditions in `system` and `t=t_0` and I got348the following error:"""349logger.error(msg)350raise (e)351352# get the list of event functions353events = options.get('events', [])354355# if there's only one event function, put it in a list356try:357iter(events)358except TypeError:359events = [events]360361for event_func in events:362# make events terminal unless otherwise specified363if not hasattr(event_func, 'terminal'):364event_func.terminal = True365366# test the event function with the initial conditions367try:368event_func(t_0, system.init, system)369except Exception as e:370msg = """Before running scipy.integrate.solve_ivp, I tried371running the event function you provided with the372initial conditions in `system` and `t=t_0` and I got373the following error:"""374logger.error(msg)375raise (e)376377# get dense output unless otherwise specified378underride(options, dense_output=True)379380# run the solver381bunch = solve_ivp(slope_func, [t_0, system.t_end], system.init,382args=[system], **options)383384# separate the results from the details385y = bunch.pop("y")386t = bunch.pop("t")387388# get the column names from `init`, if possible389if hasattr(system.init, 'index'):390columns = system.init.index391else:392columns = range(len(system.init))393394# evaluate the results at equally-spaced points395if options.get('dense_output', False):396try:397num = system.num398except AttributeError:399num = 51400t_final = t[-1]401t_array = linspace(t_0, t_final, num)402y_array = bunch.sol(t_array)403404# pack the results into a TimeFrame405results = TimeFrame(y_array.T, index=t_array,406columns=columns)407else:408results = TimeFrame(y.T, index=t,409columns=columns)410411return results, bunch412413414def check_system(system, slope_func):415"""Make sure the system object has the fields we need for run_ode_solver.416417:param system:418:param slope_func:419:return:420"""421# make sure `system` contains `init`422if not hasattr(system, "init"):423msg = """It looks like `system` does not contain `init`424as a system variable. `init` should be a State425object that specifies the initial condition:"""426raise ValueError(msg)427428# make sure `system` contains `t_end`429if not hasattr(system, "t_end"):430msg = """It looks like `system` does not contain `t_end`431as a system variable. `t_end` should be the432final time:"""433raise ValueError(msg)434435# the default value for t_0 is 0436t_0 = getattr(system, "t_0", 0)437438# get the initial conditions439init = system.init440441# get t_end442t_end = system.t_end443444# if dt is not specified, take 100 steps445try:446dt = system.dt447except AttributeError:448dt = t_end / 100449450return init, t_0, t_end, dt451452453def run_euler(system, slope_func, **options):454"""Computes a numerical solution to a differential equation.455456`system` must contain `init` with initial conditions,457`t_end` with the end time, and `dt` with the time step.458459`system` may contain `t_0` to override the default, 0460461It can contain any other parameters required by the slope function.462463`options` can be ...464465system: System object466slope_func: function that computes slopes467468returns: TimeFrame469"""470# the default message if nothing changes471msg = "The solver successfully reached the end of the integration interval."472473# get parameters from system474init, t_0, t_end, dt = check_system(system, slope_func)475476# make the TimeFrame477frame = TimeFrame(columns=init.index)478frame.row[t_0] = init479ts = linrange(t_0, t_end, dt) * get_units(t_end)480481# run the solver482for t1 in ts:483y1 = frame.row[t1]484slopes = slope_func(y1, t1, system)485y2 = [y + slope * dt for y, slope in zip(y1, slopes)]486t2 = t1 + dt487frame.row[t2] = y2488489details = ModSimSeries(dict(message="Success"))490return frame, details491492493def run_ralston(system, slope_func, **options):494"""Computes a numerical solution to a differential equation.495496`system` must contain `init` with initial conditions,497and `t_end` with the end time.498499`system` may contain `t_0` to override the default, 0500501It can contain any other parameters required by the slope function.502503`options` can be ...504505system: System object506slope_func: function that computes slopes507508returns: TimeFrame509"""510# the default message if nothing changes511msg = "The solver successfully reached the end of the integration interval."512513# get parameters from system514init, t_0, t_end, dt = check_system(system, slope_func)515516# make the TimeFrame517frame = TimeFrame(columns=init.index)518frame.row[t_0] = init519ts = linrange(t_0, t_end, dt) * get_units(t_end)520521event_func = options.get("events", None)522z1 = np.nan523524def project(y1, t1, slopes, dt):525t2 = t1 + dt526y2 = [y + slope * dt for y, slope in zip(y1, slopes)]527return y2, t2528529# run the solver530for t1 in ts:531y1 = frame.row[t1]532533# evaluate the slopes at the start of the time step534slopes1 = slope_func(y1, t1, system)535536# evaluate the slopes at the two-thirds point537y_mid, t_mid = project(y1, t1, slopes1, 2 * dt / 3)538slopes2 = slope_func(y_mid, t_mid, system)539540# compute the weighted sum of the slopes541slopes = [(k1 + 3 * k2) / 4 for k1, k2 in zip(slopes1, slopes2)]542543# compute the next time stamp544y2, t2 = project(y1, t1, slopes, dt)545546# check for a terminating event547if event_func:548z2 = event_func(y2, t2, system)549if z1 * z2 < 0:550scale = magnitude(z1 / (z1 - z2))551y2, t2 = project(y1, t1, slopes, scale * dt)552frame.row[t2] = y2553msg = "A termination event occurred."554break555else:556z1 = z2557558# store the results559frame.row[t2] = y2560561details = ModSimSeries(dict(success=True, message=msg))562return frame, details563564565run_ode_solver = run_ralston566567# TODO: Implement leapfrog568569570def fsolve(func, x0, *args, **options):571"""Return the roots of the (non-linear) equations572defined by func(x) = 0 given a starting estimate.573574Uses scipy.optimize.fsolve, with extra error-checking.575576func: function to find the roots of577x0: scalar or array, initial guess578args: additional positional arguments are passed along to fsolve,579which passes them along to func580581returns: solution as an array582"""583# make sure we can run the given function with x0584try:585func(x0, *args)586except Exception as e:587msg = """Before running scipy.optimize.fsolve, I tried588running the error function you provided with the x0589you provided, and I got the following error:"""590logger.error(msg)591raise (e)592593# make the tolerance more forgiving than the default594underride(options, xtol=1e-6)595596# run fsolve597result = scipy.optimize.fsolve(func, x0, args=args, **options)598599return result600601602def crossings(series, value):603"""Find the labels where the series passes through value.604605The labels in series must be increasing numerical values.606607series: Series608value: number609610returns: sequence of labels611"""612values = series.values - value613interp = InterpolatedUnivariateSpline(series.index, values)614return interp.roots()615616617def has_nan(a):618"""Checks whether the an array contains any NaNs.619620:param a: NumPy array or Pandas Series621:return: boolean622"""623return np.any(np.isnan(a))624625626def is_strictly_increasing(a):627"""Checks whether the elements of an array are strictly increasing.628629:param a: NumPy array or Pandas Series630:return: boolean631"""632return np.all(np.diff(a) > 0)633634635def interpolate(series, **options):636"""Creates an interpolation function.637638series: Series object639options: any legal options to scipy.interpolate.interp1d640641returns: function that maps from the index to the values642"""643if has_nan(series.index):644msg = """The Series you passed to interpolate contains645NaN values in the index, which would result in646undefined behavior. So I'm putting a stop to that."""647raise ValueError(msg)648649if not is_strictly_increasing(series.index):650msg = """The Series you passed to interpolate has an index651that is not strictly increasing, which would result in652undefined behavior. So I'm putting a stop to that."""653raise ValueError(msg)654655# make the interpolate function extrapolate past the ends of656# the range, unless `options` already specifies a value for `fill_value`657underride(options, fill_value="extrapolate")658659# call interp1d, which returns a new function object660x = series.index661y = series.values662interp_func = interp1d(x, y, **options)663return interp_func664665666def interpolate_inverse(series, **options):667"""Interpolate the inverse function of a Series.668669series: Series object, represents a mapping from `a` to `b`670options: any legal options to scipy.interpolate.interp1d671672returns: interpolation object, can be used as a function673from `b` to `a`674"""675inverse = Series(series.index, index=series.values)676interp_func = interpolate(inverse, **options)677return interp_func678679680def gradient(series, **options):681"""Computes the numerical derivative of a series.682683If the elements of series have units, they are dropped.684685series: Series object686options: any legal options to np.gradient687688returns: Series, same subclass as series689"""690x = series.index691y = series.values692693a = np.gradient(y, x, **options)694return series.__class__(a, series.index)695696697def source_code(obj):698"""Prints the source code for a given object.699700obj: function or method object701"""702print(inspect.getsource(obj))703704705def underride(d, **options):706"""Add key-value pairs to d only if key is not in d.707708If d is None, create a new dictionary.709710d: dictionary711options: keyword args to add to d712"""713if d is None:714d = {}715716for key, val in options.items():717d.setdefault(key, val)718719return d720721722def contour(df, **options):723"""Makes a contour plot from a DataFrame.724725Wrapper for plt.contour726https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.contour.html727728Note: columns and index must be numerical729730df: DataFrame731options: passed to plt.contour732"""733fontsize = options.pop("fontsize", 12)734underride(options, cmap="viridis")735x = df.columns736y = df.index737X, Y = np.meshgrid(x, y)738cs = plt.contour(X, Y, df, **options)739plt.clabel(cs, inline=1, fontsize=fontsize)740741742def savefig(filename, **options):743"""Save the current figure.744745Keyword arguments are passed along to plt.savefig746747https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html748749filename: string750"""751print("Saving figure to file", filename)752plt.savefig(filename, **options)753754755def decorate(**options):756"""Decorate the current axes.757758Call decorate with keyword arguments like759decorate(title='Title',760xlabel='x',761ylabel='y')762763The keyword arguments can be any of the axis properties764https://matplotlib.org/api/axes_api.html765"""766ax = plt.gca()767ax.set(**options)768769handles, labels = ax.get_legend_handles_labels()770if handles:771ax.legend(handles, labels)772773plt.tight_layout()774775776def remove_from_legend(bad_labels):777"""Removes some labels from the legend.778779bad_labels: sequence of strings780"""781ax = plt.gca()782handles, labels = ax.get_legend_handles_labels()783handle_list, label_list = [], []784for handle, label in zip(handles, labels):785if label not in bad_labels:786handle_list.append(handle)787label_list.append(label)788ax.legend(handle_list, label_list)789790791class SettableNamespace(SimpleNamespace):792"""Contains a collection of parameters.793794Used to make a System object.795796Takes keyword arguments and stores them as attributes.797"""798def __init__(self, namespace=None, **kwargs):799super().__init__()800if namespace:801self.__dict__.update(namespace.__dict__)802self.__dict__.update(kwargs)803804def get(self, name, default=None):805"""Look up a variable.806807name: string varname808default: value returned if `name` is not present809"""810try:811return self.__getattribute__(name, default)812except AttributeError:813return default814815def set(self, **variables):816"""Make a copy and update the given variables.817818returns: Params819"""820new = copy(self)821new.__dict__.update(variables)822return new823824825def magnitude(x):826"""Returns the magnitude of a Quantity or number.827828x: Quantity or number829830returns: number831"""832return x.magnitude if hasattr(x, 'magnitude') else x833834835def remove_units(namespace):836"""Removes units from the values in a Namespace.837838Only removes units from top-level values;839does not traverse nested values.840841returns: new Namespace object842"""843res = copy(namespace)844for label, value in res.__dict__.items():845if isinstance(value, pd.Series):846value = remove_units_series(value)847res.__dict__[label] = magnitude(value)848return res849850851def remove_units_series(series):852"""Removes units from the values in a Series.853854Only removes units from top-level values;855does not traverse nested values.856857returns: new Series object858"""859res = copy(series)860for label, value in res.iteritems():861res[label] = magnitude(value)862return res863864865class System(SettableNamespace):866"""Contains system parameters and their values.867868Takes keyword arguments and stores them as attributes.869"""870pass871872873class Params(SettableNamespace):874"""Contains system parameters and their values.875876Takes keyword arguments and stores them as attributes.877"""878pass879880881def State(**variables):882"""Contains the values of state variables."""883return pd.Series(variables)884885886def TimeSeries(*args, **kwargs):887"""888"""889if args or kwargs:890series = pd.Series(*args, **kwargs)891else:892series = pd.Series([], dtype=np.float64)893894series.index.name = 'Time'895if 'name' not in kwargs:896series.name = 'Quantity'897return series898899900def SweepSeries(*args, **kwargs):901"""902"""903if args or kwargs:904series = pd.Series(*args, **kwargs)905else:906series = pd.Series([], dtype=np.float64)907908series.index.name = 'Parameter'909if 'name' not in kwargs:910series.name = 'Metric'911return series912913914def TimeFrame(*args, **kwargs):915"""DataFrame that maps from time to State.916"""917return pd.DataFrame(*args, **kwargs)918919920def SweepFrame(*args, **kwargs):921"""DataFrame that maps from parameter value to SweepSeries.922"""923return pd.DataFrame(*args, **kwargs)924925926def Vector(x, y, z=None, **options):927"""928"""929if z is None:930return pd.Series(dict(x=x, y=y), **options)931else:932return pd.Series(dict(x=x, y=y, z=z), **options)933934935## Vector functions (should work with any sequence)936937def vector_mag(v):938"""Vector magnitude."""939return np.sqrt(np.dot(v, v))940941942def vector_mag2(v):943"""Vector magnitude squared."""944return np.dot(v, v)945946947def vector_angle(v):948"""Angle between v and the positive x axis.949950Only works with 2-D vectors.951952returns: angle in radians953"""954assert len(v) == 2955x, y = v956return np.arctan2(y, x)957958959def vector_polar(v):960"""Vector magnitude and angle.961962returns: (number, angle in radians)963"""964return vector_mag(v), vector_angle(v)965966967def vector_hat(v):968"""Unit vector in the direction of v.969970returns: Vector or array971"""972# check if the magnitude of the Quantity is 0973mag = vector_mag(v)974if mag == 0:975return v976else:977return v / mag978979980def vector_perp(v):981"""Perpendicular Vector (rotated left).982983Only works with 2-D Vectors.984985returns: Vector986"""987assert len(v) == 2988x, y = v989return Vector(-y, x)990991992def vector_dot(v, w):993"""Dot product of v and w.994995returns: number or Quantity996"""997return np.dot(v, w)9989991000def vector_cross(v, w):1001"""Cross product of v and w.10021003returns: number or Quantity for 2-D, Vector for 3-D1004"""1005res = np.cross(v, w)10061007if len(v) == 3:1008return Vector(*res)1009else:1010return res101110121013def vector_proj(v, w):1014"""Projection of v onto w.10151016returns: array or Vector with direction of w and units of v.1017"""1018w_hat = vector_hat(w)1019return vector_dot(v, w_hat) * w_hat102010211022def scalar_proj(v, w):1023"""Returns the scalar projection of v onto w.10241025Which is the magnitude of the projection of v onto w.10261027returns: scalar with units of v.1028"""1029return vector_dot(v, vector_hat(w))103010311032def vector_dist(v, w):1033"""Euclidean distance from v to w, with units."""1034if isinstance(v, list):1035v = np.asarray(v)1036return vector_mag(v - w)103710381039def vector_diff_angle(v, w):1040"""Angular difference between two vectors, in radians.1041"""1042if len(v) == 2:1043return vector_angle(v) - vector_angle(w)1044else:1045# TODO: see http://www.euclideanspace.com/maths/algebra/1046# vectors/angleBetween/1047raise NotImplementedError()104810491050def plot_segment(A, B, **options):1051"""Plots a line segment between two Vectors.10521053For 3-D vectors, the z axis is ignored.10541055Additional options are passed along to plot().10561057A: Vector1058B: Vector1059"""1060xs = A.x, B.x1061ys = A.y, B.y1062plot(xs, ys, **options)106310641065from time import sleep1066from IPython.display import clear_output10671068def animate(results, draw_func, *args, interval=None):1069"""Animate results from a simulation.10701071results: TimeFrame1072draw_func: function that draws state1073interval: time between frames in seconds1074"""1075plt.figure()1076try:1077for t, state in results.iterrows():1078draw_func(t, state, *args)1079plt.show()1080if interval:1081sleep(interval)1082clear_output(wait=True)1083draw_func(t, state, *args)1084plt.show()1085except KeyboardInterrupt:1086pass108710881089