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