def odeint(func, y0, tspan, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0): 'Wrapped scipy.integrate.odeint with units' def wrappedfunc(y, t, *args): y = Unit(y, y0.exponents, y0.label) t = Unit(tspan, tspan.exponents, tspan.label) return float(func(y, t, *args)) if full_output: y, infodict = _odeint(wrappedfunc, y0, tspan, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0) else: y = _odeint(wrappedfunc, y0, tspan, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0) y = Unit(y, y0.exponents, y0.label) return y
def odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0): """wrapper for scipy.integrate.odeint to work with quantities.""" def wrapped_func(Y0, T, *args): # put units on T if they are on the original t # check for units so we don't put them on twice if not hasattr(T, 'units') and hasattr(t, 'units'): T = T * t.units # now for the dependent variable units. Y0 may be a scalar or # a list or an array. we want to check each element of y0 for # units, and add them to the corresponding element of Y0 if we # need to. try: uY0 = [x for x in Y0] # a list copy of contents of Y0 # this works if y0 is an iterable, eg. a list or array for i, yi in enumerate(y0): if not hasattr(uY0[i], 'units') and hasattr(yi, 'units'): uY0[i] = uY0[i] * yi.units except TypeError: # we have a scalar if not hasattr(Y0, 'units') and hasattr(y0, 'units'): uY0 = Y0 * y0.units # It is necessary to rescale this to prevent issues with non-simplified # units. val = func(uY0, t, *args).rescale(y0.units / t.units) try: return np.array([float(x) for x in val]) except TypeError: return float(val) if full_output: y, infodict = _odeint(wrapped_func, y0, t, args, Dfun, col_deriv, full_output, ml, mu, rtol, atol, tcrit, h0, hmax, hmin, ixpr, mxstep, mxhnil, mxordn, mxords, printmessg) else: y = _odeint(wrapped_func, y0, t, args, Dfun, col_deriv, full_output, ml, mu, rtol, atol, tcrit, h0, hmax, hmin, ixpr, mxstep, mxhnil, mxordn, mxords, printmessg) # now we need to put units onto the solution units should be the # same as y0. We cannot put mixed units in an array, so, we return a list m, n = y.shape # y is an ndarray, so it has a shape if n > 1: # more than one equation, we need a list uY = [0 for yi in range(n)] for i, yi in enumerate(y0): if not hasattr(uY[i], 'units') and hasattr(yi, 'units'): uY[i] = y[:, i] * yi.units else: uY[i] = y[:, i] else: uY = y * y0.units y = uY if full_output: return y, infodict else: return y
def run(self, params={}, return_columns=[], return_timestamps=[], initial_condition='original', collect=False, **intg_kwargs): """ Simulate the model's behavior over time. Return a pandas dataframe with timestamps as rows, model elements as columns. Parameters ---------- params : dictionary Keys are strings of model component names. Values are numeric or pandas Series. Numeric values represent constants over the model integration. Timeseries will be interpolated to give time-varying input. return_timestamps : list, numeric, numpy array(1-D) Timestamps in model execution at which to return state information. Defaults to model-file specified timesteps. return_columns : list of string model component names Returned dataframe will have corresponding columns. Defaults to model stock values. initial_condition : 'original'/'o', 'current'/'c', (t, {state}) The starting time, and the state of the system (the values of all the stocks) at that starting time. * 'original' (default) uses model-file specified initial condition * 'current' uses the state of the model after the previous execution * (t, {state}) lets the user specify a starting time and (possibly partial) list of stock values. collect: binary (T/F) When running multiple simulations, collect the results in a way that we can access down the road. intg_kwargs: keyword arguments for odeint Provides precice control over the integrator by passing through keyword arguments to scipy's odeint function. The most interesting of these will be `tcrit`, `hmax`, `mxstep`. Examples -------- >>> model.run(params={'exogenous_constant':42}) >>> model.run(params={'exogenous_variable':timeseries_input}) >>> model.run(return_timestamps=[1,2,3.1415,4,10]) >>> model.run(return_timestamps=10) >>> model.run(return_timestamps=np.linspace(1,10,20)) See Also -------- pysd.set_components : handles setting model parameters pysd.set_initial_condition : handles setting initial conditions """ #if not self.components._stocknames: #raise RuntimeError('Cannnot integrate no-stock models.') if params: self.set_components(params) if initial_condition != 'current': self.set_initial_condition(initial_condition) tseries = self._build_timeseries(return_timestamps) # the odeint expects the first timestamp in the tseries to be the initial condition, # so we may need to add the t0 if it is not present in the tseries array addtflag = tseries[0] != self.components.t if addtflag: tseries = np.insert(tseries, 0, self.components.t) if self.components._stocknames: res = _odeint(func=self.components.d_dt, y0=self.components.state_vector(), t=tseries, **intg_kwargs) #hmax=self.components.time_step()) state_df = _pd.DataFrame(data=res, index=tseries, columns=self.components._stocknames) else: state_df = _pd.DataFrame(index=tseries, data=1, columns=['dummy']) return_df = self.extend_dataframe(state_df, return_columns) if return_columns else state_df if addtflag: return_df.drop(return_df.index[0], inplace=True) if collect: self.record.append(return_df) #we could just record the state, and expand it later... # The integrator takes us past the last point in the tseries. # Go back to it, in order to maintain the state at a predictable location. # This may take up more time than we're willing to spend... if self.components.t != tseries[-1]: self.set_state(tseries[-1], dict(state_df.iloc[-1])) return return_df
def run(self, params={}, return_columns=[], return_timestamps=[], initial_condition='original', collect=False): """ Runs the model from the initial time all the way through the final time, returning a pandas dataframe of results. If ::return_timestamps:: is set, the dataframe will have these as its index. Otherwise, the index will be every timestep from the start of simulation to the finish. if ::return_columns:: is set, the dataframe will have these columns. Otherwise, it will return the value of the stocks. If ::params:: is set, modifies the model according to parameters before execution. format for params should be: params={'parameter1':value, 'parameter2':value} initial_condition can take a variety of values: - None or 'original' reinitializes the model at the initial conditions specified in the model file, and resets the simulation time accordingly - 'current' preserves the current state of the model, including the simulaion time - a tuple (t, dict) sets the simulation time to t, and the """ ### Todo: # # - check that the return_timestamps is a collection - a list of timestamps, or something. # if not(ie, a single value), make it one. if params: self.set_components(params) ##### Setting timestamp options if return_timestamps: tseries = return_timestamps else: tseries = np.arange(self.components.initial_time(), self.components.final_time(), self.components.time_step()) ##### Setting initial condition options if isinstance(initial_condition, tuple): self.components.t = initial_condition[0] #this is brittle! self.components.state.update(initial_condition[1]) #this is also brittle. Assumes that the dictionary passed in has valid values elif isinstance(initial_condition, str): if initial_condition.lower() in ['original','o']: self.components.reset_state() #resets the state of the system to what the model contains (but not the parameters) elif initial_condition.lower() in ['current', 'c']: pass #placeholder - this is a valid option, but we don't modify anything else: assert False #we should throw an error if there is an unrecognized option else: assert False initial_values = self.components.state_vector() addtflag = False if tseries[0] != self.components.t: tseries = [self.components.t]+tseries addtflag = True ######Setting integrator options: # # - we may choose to use the odeint parameter ::tcrit::, which identifies singularities # near which the integrator should exercise additional caution. # we could get these values from any time-type parameters passed to a step/pulse type function # # - there may be a better way to use the integrator that lets us report additional values at # different timestamps. We may also want to use pydstool. # # the odeint expects the first timestamp in the tseries to be the initial condition, # so we may need to add the t0 if it is not present in the tseries array res = _odeint(self.components.d_dt, initial_values, tseries, hmax=self.components.time_step()) stocknames = sorted(self.components.state.keys()) values = _pd.DataFrame(data=res, index=tseries, columns=stocknames) if return_columns: # this is a super slow, super bad way to do this. Recode ASAP out = [] for t, row in zip(values.index, values.to_dict(orient='records')): self.components.state.update(row) self.components.t = t for column in return_columns: func = getattr(self.components, column) row[column] = func() out.append(row) values = _pd.DataFrame(data=out, index=values.index)[return_columns] #this is ugly if addtflag: values = values.iloc[1:] #there is probably a faster way to drop the initial row if collect: self.record.append(values) return values
def run(self, params={}, return_columns=[], return_timestamps=[], initial_condition='original', collect=False, **intg_kwargs): """ Simulate the model's behavior over time. Return a pandas dataframe with timestamps as rows, model elements as columns. Parameters ---------- params : dictionary Keys are strings of model component names. Values are numeric or pandas Series. Numeric values represent constants over the model integration. Timeseries will be interpolated to give time-varying input. return_timestamps : list, numeric, numpy array(1-D) Timestamps in model execution at which to return state information. Defaults to model-file specified timesteps. return_columns : list of string model component names Returned dataframe will have corresponding columns. Defaults to model stock values. initial_condition : 'original'/'o', 'current'/'c', (t, {state}) The starting time, and the state of the system (the values of all the stocks) at that starting time. * 'original' (default) uses model-file specified initial condition * 'current' uses the state of the model after the previous execution * (t, {state}) lets the user specify a starting time and (possibly partial) list of stock values. collect: binary (T/F) When running multiple simulations, collect the results in a way that we can access down the road. intg_kwargs: keyword arguments for odeint Provides precice control over the integrator by passing through keyword arguments to scipy's odeint function. The most interesting of these will be `tcrit`, `hmax`, `mxstep`. Examples -------- >>> model.run(params={'exogenous_constant':42}) >>> model.run(params={'exogenous_variable':timeseries_input}) >>> model.run(return_timestamps=[1,2,3.1415,4,10]) >>> model.run(return_timestamps=10) >>> model.run(return_timestamps=np.linspace(1,10,20)) See Also -------- pysd.set_components : handles setting model parameters pysd.set_initial_condition : handles setting initial conditions """ if params: self.set_components(params) if initial_condition != 'current': self.set_initial_condition(initial_condition) tseries = self._build_timeseries(return_timestamps) # the odeint expects the first timestamp in the tseries to be the initial condition, # so we may need to add the t0 if it is not present in the tseries array addtflag = tseries[0] != self.components.t if addtflag: tseries = np.insert(tseries, 0, self.components.t) res = _odeint(func=self.components.d_dt, y0=self.components.state_vector(), t=tseries, **intg_kwargs) #hmax=self.components.time_step()) state_df = _pd.DataFrame(data=res, index=tseries, columns=self.components._stocknames) return_df = self.extend_dataframe( state_df, return_columns) if return_columns else state_df if addtflag: return_df.drop(return_df.index[0], inplace=True) if collect: self.record.append( return_df ) #we could just record the state, and expand it later... # The integrator takes us past the last point in the tseries. # Go back to it, in order to maintain the state at a predictable location. # This may take up more time than we're willing to spend... if self.components.t != tseries[-1]: self.set_state(tseries[-1], dict(state_df.iloc[-1])) return return_df