kawin.GenericModel
kawin.kawin.GenericModel
GenericModel
class GenericModel(self):
Abstract model that new models can inherit from to interface with the Solver
The model is intended to be defined by an ordinary differential equation or a set of them
The differential equations are defined by dX/dt = f(t,X)
Where t is time and X is the set of time-dependent variables at time t
Required functions to be implemented:
getCurrentX(self) - should return time and all time-dependent variables
getdXdt(self, t, x) - should return all time-dependent derivatives
getDt(self, dXdt) - should return a suitable time step
Functions that can be implemented but not necessary:
_getVarDict(self) - returns a dictionary of {variable name : member name}
_addExtraSaveVariables(self, saveDict) - adds to saveDict additional variables to save
_loadExtraVariables(self, data) - loads additional data to model
setup(self) - ran before solver is called
correctdXdt(self, dt, x, dXdt) - does not need to return anything, but should modify dXdt
preProcess(self) - preprocessing before each iteration
postProcess(self, time, x) - postprocessing after each iteration
printHeader(self) - initial output statements before solver is called
printStatus(self, iteration, modelTime, simTimeElapsed) - output states made after n iterations
def GenericModel.toDict(self):
Creates a dictionary data set of the following:
- this will only save the data that was solved for and not model parameters
TODO: eventually support saving model parameters. This is a bit tough with all the nested parameters right now
GenericModel
def GenericModel.save(self, filename: str | Path):
Saves model data into file
1. Store model attributes into saveDict using mapping defined from _getVarDict
2. Add extra variables to saveDict if needed
3. Save data into .npz format
Parameters
----------
filename : str
File name to save to
compressed : bool (defaults to True)
Whether to save in compressed format
def GenericModel.load(self, filename: str | Path):
def GenericModel.addCouplingModel(self, model):
Adds a coupling model to the KWN model
These will be updated after each iteration with the new values of the model
Parameters
----------
model : object
Must have a function called updateCoupledModel that takes in a KWNBase or KWNEuler object
def GenericModel.clearCouplingModels(self):
Clears list of coupling models
Note - this will not reset the coupling models, just removes them from the list
def GenericModel.updateCoupledModels(self):
Updates coupled models with current values
def GenericModel.reset(self):
def GenericModel.setup(self):
Sets up model before being solved
This is the first thing that is called when the solve function is called
Note: this will be called each time the solve function called, so if setup only needs to
be called once, then make sure there's a check in the model implementation to prevent
setup from being called more than once
def GenericModel.getCurrentX(self):
Gets values of time-dependent variables at current time
The required format of X is not strict as long as it matches dXdt
Example: if X is a nested list of [[a, b], c], then dXdt should be [[da/dt, db/dt], dc/dt]
Note: X should only be for variables that are solved by dX/dt = f(t,X)
Variables that can be computed directly from X should be calculated in the preProcess or postProcess functions
Returns
-------
t : current time of model
X : unformatted list of floats
def GenericModel.getDt(self, dXdt):
Gets suitable time step based off dXdt
Parameters
----------
dXdt : unformated list of floats
Time derivatives that may be used to find dt
Returns
-------
dt : float
def GenericModel.getdXdt(self, t, x):
Gets dXdt from current time and X
Parameters
----------
t : float
Current time
x : unformated list of floats
Current values of time-dependent variables
Returns
-------
dXdt : unformated list of floats
Must be in same format as x
def GenericModel.correctdXdt(self, dt, x, dXdt):
Intended for cases where dXdt can only be corrected once dt is known
For example, the time derivatives in the population balance model in PrecipitateModel needs to be
adjusted to avoid negative bins, but this can only be done once dt is known
If dXdt can be corrected without knowing dt, then it is recommended to be done during the getdXdt function
No return value, dXdt is to be modified directly
def GenericModel.preProcess(self):
Performs any pre-processing before an iteration. This may include some calculations or storing temporary variables
def GenericModel.postProcess(self, time, x):
Post processing done after an iteration
This should at least involve storing the new values of time and X
But this can also include additional calculations or return a signal to stop simulations
Parameters
----------
time : float
New time
x : unformatted list of floats
New values of X
Returns
-------
x : unformatted list of floats
This is in case X was modified in postProcess
stop : bool
If the simulation needs to end early (ex. a stopping condition is met), then return True to stop solving
def GenericModel.printHeader(self):
First output to be printed when solve is called
verbose must be True when calling solve
def GenericModel.printStatus(self, iteration, modelTime, simTimeElapsed):
Output to be printed after n iterations (defined by vIt in solve)
verbose must be True when calling solve
def GenericModel.setTimeInfo(self, currTime, simTime):
Store time variables for starting, final and delta time
This is sometimes useful for determining the time step
def GenericModel.flattenX(self, X):
Since X can be a nested list of values or arrays (or anything),
we want some instructions for the solver and Iterator for how to convert X
to a 1D array
By default, we'll assume X is a list of either floats or 1D arrays
For more complex nesting, this function should be overloaded
Parameters
----------
X : list of arrays
Returns
-------
X_flat : 1D numpy array
def GenericModel.unflattenX(self, X_flat, X_ref):
Converts flattened X array to original nested X
Parameters
----------
X_flat : 1D numpy array
Flattened array
X_ref : list of arrays
Template to convert X_flat to
Returns
-------
X_new : unflattened list in the same format as X_ref
def GenericModel.solve(self, simTime, iterator = rk4Iterator, verbose=False, vIt=10, minDtFrac = 1e-8, maxDtFrac = 1):
Solves model using the DESolver
Steps:
1. Call setup
2. Create DESolver object and set necessary functions
3. Get current values of t and X
4. Solve from current t to t+simTime
Parameters
----------
simTime : float
Simulation time (as a delta from current time)
iterator : Iterator function (defaults to rk4Iterator)
Defines what iteration scheme to use
verbose : bool (defaults to False)
Outputs status if true
vIt : integer (defaults to 10)
Number of iterations before printing status
minDtFrac : float (defaults to 1e-8)
Minimum dt as fraction of simulation time
maxDtFrac : float (defaults to 1)
Maximum dt as fraction of simulation time
def GenericModel.postSolve(self):
Coupler(GenericModel)
class Coupler(self, models : List[GenericModel]):
Class for coupling multiple GenericModel objects together
Note:
coupleddXdt, coupledPreProcess and coupledPostProcess aren't really necessary since
tighter coupling can also be done by overloading the getdXdt, preProcess and/or postProcess
functions and calling the method of the Coupler before anything else
Ex. tighter coupling can be done by
a) Overloading coupleddXdt as
def coupleddXdt(self, dXdt):
===
Modify dXdt here
===
b) Overriding getdXdt as
def getdXdt(self, t, x):
dXdt = super().getdXdt(t, x)
---
modify dXdt here
---
return dXdt
Parameters
----------
models : List[GenericModel]
List of models to be solved
def Coupler.setup(self):
Sets up each model
def Coupler.setTimeInfo(self, currTime, simTime):
Sets time info for the CoupledModel class and each model
def Coupler.flattenX(self, X):
Instructions for converting X to 1D array
We grab the flattened x array of each model and concatenate them
Thus we don't have to care about the structure of x in each model as
long as the model itself has the instructions to flatten its x array
Also record the length of each flattened x in each model so we know what
indices to used for unflattening
def Coupler.unflattenX(self, X_flat, X_ref):
Instructions for converting X_flat to list of x of each model
We take the subset of X_flat corresponding to each model and unflatten it
based off the instructions in the model. Then we just return a list containing
each unflattened x
def Coupler.getCurrentX(self):
Get current time and x for each model
def Coupler.getDt(self, dXdt):
Get the minimum dt out of all models
def Coupler.getdXdt(self, t, x):
Get dXdt for each model
def Coupler.correctdXdt(self, dt, x, dXdt):
Corrects dXdt for each model
Note - dXdt has to be modified since we don't return dXdt in this function
Since dXdt here is composed of a nested list of dXdts of each model, these
will be passed by reference
def Coupler.preProcess(self):
Pre process on each model
def Coupler.postProcess(self, time, x):
Post process on each model and records new time
def Coupler.coupledXdt(self, t, x, dXdt):
Empty function where inherited classes can do extra operations on
the time derivatives each models or between models
def Coupler.couplePreProcess(self):
Empty function where inherited classes can do extra operations on
each models or between models for an iteration
def Coupler.couplePostProcess(self):
Empty function where inherited classes can do extra operations on
each models or between models after an iteration
def Coupler.postSolve(self):
Post solve function for each model