コード例 #1
0
ファイル: slump.py プロジェクト: szwieback/FVEH
    def __init__(self,
                 tsmesh,
                 time_step_module=None,
                 output_step_module=None,
                 forcing_module=None,
                 thermal_properties=None,
                 time_initial=None):
        self.mesh = tsmesh
        self.variables = {}
        self.variables_store = []
        self.diagnostic_modules = {}
        self.diagnostic_update_order = []
        self.eq = None
        self.boundary_condition_collection = None
        self._time = Variable(value=0)
        self._time_step_module = time_step_module
        self._timeref = None  # will generally be set by forcing_module; otherwise manually
        if forcing_module is not None:
            self.initializeForcing(forcing_module)
            if time_initial is not None:
                self.time = time_initial
        if thermal_properties is not None:
            self.initializeThermalProperties(thermal_properties)

        self._output_step_module = output_step_module
        self._output_module = SlumpOutput()
        if output_step_module is None:
            self._output_step_module = OutputStep()
コード例 #2
0
ファイル: domain.py プロジェクト: achennu/microbenthos
    def create_mesh(self):
        """
        Create the :mod:`fipy` mesh for the domain using :attr:`cell_size` and :attr:`total_cells`.

        The arrays :attr:`depths` and :attr:`distances` are created, which provide the
        coordinates and distances of the mesh cells.
        """

        self.logger.info(
            'Creating UniformGrid1D with {} sediment and {} DBL cells of {}'.
            format(self.sediment_cells, self.DBL_cells, self.cell_size))
        self.mesh = Grid1D(
            dx=self.cell_size.numericValue,
            nx=self.total_cells,
        )
        self.logger.debug('Created domain mesh: {}'.format(self.mesh))

        #: An array of the scaled cell distances of the mesh
        self.distances = Variable(value=self.mesh.scaledCellDistances[:-1],
                                  unit='m',
                                  name='distances')
        Z = self.mesh.x()
        Z = Z - Z[self.idx_surface]

        #: An array of the cell center coordinates, with the 0 set at the sediment surface
        self.depths = Variable(Z, unit='m', name='depths')
コード例 #3
0
    def test_snapshot_due(self):
        sim = Simulation()

        model = mock.MagicMock(MicroBenthosModel)
        clock = mock.MagicMock(ModelClock)
        clock.return_value = C = PhysicalField(3, 'h')
        model.clock = clock
        model.full_eqn = feqn = mock.Mock()
        sim.model = model

        sim._prev_snapshot = Variable(C - sim.snapshot_interval / 2.0)
        assert not sim.snapshot_due()

        sim._prev_snapshot = Variable(C - sim.snapshot_interval * 1.01)
        assert sim.snapshot_due()
コード例 #4
0
ファイル: slump.py プロジェクト: szwieback/FVEH
 def __init__(self,
              values_inp,
              t_inp=None,
              variables=None,
              key_time='time'):
     if t_inp is None:
         t_inp_int = values_inp[key_time]
     else:
         t_inp_int = t_inp
     self.t_inp = t_inp_int
     self._timeref = t_inp_int[0]
     t_inp_rel = [tj - self._timeref for tj in self.t_inp]
     try:
         self.t_inp_rel = np.array([tj.total_seconds() for tj in t_inp_rel])
     except:
         self.t_inp_rel = np.array(t_inp_rel)
     if variables is None:
         self.variables = [vj for vj in values_inp if vj != key_time]
     else:
         self.variables = variables
     self.variables = {
         vj: Variable(value=values_inp[vj][0])
         for vj in self.variables
     }
     self.values = {vj: values_inp[vj] for vj in self.variables}
コード例 #5
0
    def test_setup(self):
        proc = mock.MagicMock(spec=Process)
        proc.name = mock.Mock(return_value='Proc')
        proc.evaluate.return_value = CONDITION = object()

        model = mock.MagicMock(MicroBenthosModel)
        CLOCK_VAL = Variable(2.5, 'h')
        model.clock = mock.Mock(return_value=CLOCK_VAL)
        model.clock.copy.return_value = CLOCK_VAL

        expr = mock.MagicMock(Expression)
        expr.expr.return_value = EXPR = object()

        pe = ProcessEvent(expr)
        pe.setup(process=proc, model=model)

        assert pe.process == proc
        model.domain.create_var.assert_called_once_with(name=repr(pe),
                                                        store=False,
                                                        unit='s',
                                                        value=model.clock)
        assert isinstance(pe._prev_clock, Variable)
        expr.expr.assert_called_once()

        proc.evaluate.assert_called_once_with(EXPR)
        assert pe.condition is CONDITION
コード例 #6
0
ファイル: slump.py プロジェクト: szwieback/FVEH
 def __init__(self,
              values_inp,
              timeref=datetime.datetime(2012, 1, 1),
              variables=None):
     if variables is None:
         self.variables = [vj for vj in values_inp]
     else:
         self.variables = variables
     self._timeref = timeref
     self.variables = {
         vj: Variable(value=values_inp[vj])
         for vj in self.variables
     }
     self.values = {vj: values_inp[vj] for vj in self.variables}
コード例 #7
0
    def run(self, initialConditions, step_n):
        pinit = np.zeros(
            self.nx * self.ny *
            self.nz)  #TODO: how to map from the ABM domain to this form
        if initialConditions == None:
            for i in range(self.nx * self.ny * self.nz):
                pinit[i] = float(
                    decimal.Decimal(random.randrange(8, 50)) /
                    1000000)  #ng/mm3
        else:
            pass

        print("Initial values:")
        print(pinit)
        phi = CellVariable(name="Concentration (ng/ml)",
                           mesh=self.mesh,
                           value=pinit)
        t = Variable(0.)
        for step in range(step_n):
            print("Time = {}".format(t.value))
            self.eq.solve(var=phi, dt=self.timeStepDuration)
            t.setValue(t.value + self.timeStepDuration)
        return phi.value
コード例 #8
0
    def test_add_diffusion_term_from(self, model):
        # input should be path, coeff
        eqn = ModelEquation(model, 'domain.abc', coeff=5)

        model.get_object.return_value = rv = mock.Mock(CellVariable)
        rv.as_term = rvt = mock.Mock()
        rvt.return_value = v = Variable(1.5)

        varpath = 'domain.var1'
        coeff = 1.1

        assert eqn.term_diffusion is None
        assert eqn.diffusion_def is ()

        eqn.add_diffusion_term_from(varpath, coeff)
        assert eqn.term_diffusion == DiffusionTerm(var=eqn.var,
                                                   coeff=v * coeff)
        assert eqn.diffusion_def == (varpath, coeff)
コード例 #9
0
ファイル: slump.py プロジェクト: szwieback/FVEH
class ThawSlump(object):  # 1D

    # time_initial only works when forcing is provided
    def __init__(self,
                 tsmesh,
                 time_step_module=None,
                 output_step_module=None,
                 forcing_module=None,
                 thermal_properties=None,
                 time_initial=None):
        self.mesh = tsmesh
        self.variables = {}
        self.variables_store = []
        self.diagnostic_modules = {}
        self.diagnostic_update_order = []
        self.eq = None
        self.boundary_condition_collection = None
        self._time = Variable(value=0)
        self._time_step_module = time_step_module
        self._timeref = None  # will generally be set by forcing_module; otherwise manually
        if forcing_module is not None:
            self.initializeForcing(forcing_module)
            if time_initial is not None:
                self.time = time_initial
        if thermal_properties is not None:
            self.initializeThermalProperties(thermal_properties)

        self._output_step_module = output_step_module
        self._output_module = SlumpOutput()
        if output_step_module is None:
            self._output_step_module = OutputStep()

    @property
    def time(self):
        return float(self._time.value)

    @time.setter
    def time(self, t):  # can also handle date objects
        try:
            self.date = t
        except:
            self._time.setValue(t)

    @property
    def timeStep(self):
        return self._time_step_module.calculate(self)

    @property
    def date(self):
        return self._internal_time_to_date(self.time)

    def _internal_time_to_date(self, internal_time):
        return self._timeref + datetime.timedelta(seconds=internal_time)

    @date.setter
    def date(self, d):
        dtsec = self._date_to_internal_time(d)
        self._time.setValue(dtsec)

    def _date_to_internal_time(self, d):
        dt = d - self._timeref
        dtsec = dt.days * 24 * 3600 + dt.seconds + dt.microseconds * 1e-6
        return dtsec

    def initializeTimeReference(self, timeref):
        # timeref is a datetime object
        self._timeref = timeref

    def initializePDE(self, tseq=None):
        self.eq = tseq

    def initializeTimeStepModule(self, time_step_module):
        self._time_step_module = time_step_module

    def _initializeSourcesZero(self, source_name='S'):
        self.variables[source_name] = CellVariable(name=source_name,
                                                   mesh=self.mesh.mesh,
                                                   value=0.0)

    def initializeDiagnostic(self,
                             variable,
                             funpointer,
                             default=0.0,
                             face_variable=False,
                             output_variable=True):
        if not face_variable:
            self.variables[variable] = CellVariable(name=variable,
                                                    mesh=self.mesh.mesh,
                                                    value=default)
        else:
            self.variables[variable] = FaceVariable(name=variable,
                                                    mesh=self.mesh.mesh,
                                                    value=default)
        self.diagnostic_modules[variable] = DiagnosticModule(funpointer, self)
        if output_variable:
            self.variables_store.append(variable)
        self.diagnostic_update_order.append(variable)

    def initializeOutputStepModule(self, output_step_module):
        self._output_step_module = output_step_module

    def initializeThermalProperties(self, thermal_properties):
        self.thermal_properties = thermal_properties
        self.thermal_properties.initializeVariables(self)
        self.initializeTright()

    def initializeForcing(self, forcing_module):
        self.forcing_module = forcing_module
        for varj in self.forcing_module.variables:
            assert varj not in self.variables
            self.variables[varj] = self.forcing_module.variables[varj]
        self.initializeTimeReference(self.forcing_module._timeref)

    def initializeEnthalpyTemperature(self,
                                      T_initial,
                                      proportion_frozen=None,
                                      time=None):
        # time can be internal time or also a datetime object
        pf = 0.0 if proportion_frozen is None else proportion_frozen
        assert pf >= 0.0 and pf <= 1.0
        self.variables['T'].setValue(T_initial)
        self.variables['h'].setValue(
            self.thermal_properties.enthalpyFromTemperature(
                self, T=T_initial, proportion_frozen=pf))
        self.updateDiagnostics()
        if time is not None:
            self.time = time

    def updateDiagnostic(self, variable):
        self.variables[variable].setValue(
            self.diagnostic_modules[variable].evaluate())

    def updateDiagnostics(self, variables=None):
        if variables is not None:
            variablesorder = variables
        else:
            variablesorder = self.diagnostic_update_order
        for variable in variablesorder:
            self.updateDiagnostic(variable)

    def specifyBoundaryConditions(self, boundary_condition_collection):
        self.boundary_condition_collection = boundary_condition_collection
        self.updateGeometryBoundaryConditions()
        self.invokeBoundaryConditions()
        self.initializePDE()

    def updateGeometryBoundaryConditions(self):
        self.boundary_condition_collection.updateGeometry(self)

    def updateBoundaryConditions(self, bc_data, invoke=True):
        self.boundary_condition_collection.update(bc_data)
        if invoke:
            self.invokeBoundaryConditions()

    def invokeBoundaryConditions(self):
        self.boundary_condition_collection.invoke(self)

    def updateGeometry(self):
        self.boundary_condition_collection.updateGeometry(self)

    def nextOutput(self):
        return self._output_step_module.next(self)

    def updateOutput(self, datanew={}):
        for v in self.variables_store:
            datanew[v] = np.copy(self.variables[v].value)
        # boundary condition outputs:
        # separate routine: total source, source components, or for basic b.c. just value)
        datanew.update(self.boundary_condition_collection.output())
        self._output_module.update(self.date, datanew)

    def exportOutput(self, fn):
        self._output_module.export(fn)

    def addStoredVariable(self, varname):
        # varname can also be list
        if isinstance(varname, str):
            if varname not in self.variables_store:
                self.variables_store.append(varname)
        else:  # tuple/list,etc.
            for varnamej in varname:
                self.addStoredVariable(varnamej)
コード例 #10
0
eq = (TransientTerm() == DiffusionTerm(coeff= alpha* D) + ExplicitDiffusionTerm(coeff = (1.0 - alpha)*D))


# dt = 0.00018 should be stable for forward Euler with the default simulation conditions
dt = 0.00018

# We might not want to see the output from every single timestep, so we define a stride parameter
time_stride = 1
timestep = 0
run_time = 1


# We need to implement the analytical solution. We can do two solutions: 1) Full solution, 2) S.S. solution
pi = numerix.pi
x = mesh.cellCenters[0]
t = Variable(0.0)

# This part of the code may be a little too much, dont worry about it, essentially we are recreating the analytical solution

# First we define a function that gives me an individual (nth) fourier mode: 
def one_fourier (n):
    mode = (-2.0/(n*pi))*numerix.sin(n*pi*x)*numerix.exp(-t*(pi**2)*(n**2))
    return mode

# Next we define a second function that gives me an truncated version of the analytical solution with k modes
def analytical_expression (k):
    Fourier = 0
    for mode in range(1, k+1):
        Fourier = Fourier + one_fourier(mode)
    Approx = 1.0 - x + Fourier
    return Approx
コード例 #11
0
ファイル: simulation.py プロジェクト: achennu/microbenthos
    def evolution(self):
        """
        Evolves the model clock through the time steps for the simulation, i.e.
        by calling :meth:`.run_timestep` and :meth:`.model.clock.increment_time`
        repeatedly while ``model.clock() <= self.simtime_total``.

        This is a generator that yields the step number, and the state of the
        evolution after each time step. If :meth:`snapshot_due` is true, then
        also the model snapshot is included in the state.

        Yields:
            `(step, state)` tuple of step number and simulation state

        Raises:
            RuntimeError: if :attr:`.started` is already True

        """
        if self.started:
            raise RuntimeError(
                'Simulation already started. Cannot run parallel evolutions!')

        self.logger.debug(
            'simtime_total={o.simtime_total} simtime_step={o.simtime_step}, '
            'max_sweeps={o.max_sweeps} max_residual={o.max_residual}'.format(
                o=self))

        self.logger.debug('Solving: {}'.format(self.model.full_eqn))

        self._create_solver()
        self._started = True
        self.logger.info('Simulation evolution starting')

        self.model.update_vars()

        self._prev_snapshot = Variable(self.model.clock.copy(),
                                       name='prev_snapshot')
        step = 0
        self.simtime_step = self.simtime_lims[0]

        while self.model.clock() <= self.simtime_total:
            self.logger.debug('Running step #{} {}'.format(
                step, self.model.clock))

            dt = self.simtime_step

            tic = time.time()
            residual, num_sweeps = self.run_timestep()
            toc = time.time()
            self.logger.debug(
                f'For dt={dt} residual={residual:.4g} with sweeps={num_sweeps}'
            )

            # if residual == 0:
            #     raise RuntimeError(f'Residual perfect 0 for dt={dt}. Problem in domain!')

            self._sweepsQ.appendleft(num_sweeps)
            self._residualQ.appendleft(residual)

            if residual >= self.max_residual:

                self.logger.info(
                    f'Ignoring dt={dt}: res={residual:.4g} > {self.max_residual:.4g}'
                )
                self.update_simtime_step(residual, num_sweeps)
                self.model.revert_vars()

                # just go back to while loop start, now that dt has been made smaller
                continue

            else:
                self.model.update_vars()
                self.model.update_equations(dt)

                step += 1

            self.update_simtime_step(residual, num_sweeps)

            calc_time = 1000 * (toc - tic)
            self.logger.debug('Time step {} done in {:.2f} msec'.format(
                self.simtime_step, calc_time))

            if self.snapshot_due():

                self.logger.debug('Snapshot in step #{}'.format(step))

                state = self.get_state(state=self.model.snapshot(),
                                       calc_time=calc_time,
                                       residual=residual,
                                       num_sweeps=num_sweeps)

                yield (step, state)

                # now set the prev_snapshot so that snapshot_due() will
                # remain true for processing
                self._prev_snapshot.setValue(self.model.clock.copy())
                self.logger.debug('Prev snapshot set: {}'.format(
                    self._prev_snapshot))

            else:
                # create a minimal state
                # this is the model clock and current residual
                state = self.get_state(calc_time=calc_time,
                                       residual=residual,
                                       num_sweeps=num_sweeps)

                yield (step, state)

            self.model.clock.increment_time(dt)

        self.logger.info('Simulation evolution completed')
        self._started = False
コード例 #12
0
beta = math.radians(30.0)

# [bool] Wenn True, wird direkt das Gleichgewicht berechnet
steady_state = True

# Berechnete Werte ------------------------------------------------------------

# Anzahl der Zeitschritte
intervals = int(total_time / dt)

# Temperaturleitfaehigkeit in x und y Richtung in Tensor (D) zusammenfassen
Dx = material["lambda_x"] / (material["cp"] * material["dichte"])
Dy = material["lambda_y"] / (material["cp"] * material["dichte"])

# D kann einfache Variable sein, da sie Ortsunabhängig ist
D = Variable(value=((Dx, 0), (0, Dy)))

# FiPy init -------------------------------------------------------------------

geometryTemplate = '''
cellSize = {0};

Point(1) = {{ 0,   0,   0,  cellSize}};
Point(2) = {{ {1}, 0,   0,  cellSize}};
Point(3) = {{ {2}, {3}, 0,  cellSize}};
Point(4) = {{ {4}, {3}, 0,  cellSize}};
Point(5) = {{ {4}, {5}, 0,  cellSize}};

Line(10) = {{1,2}};
Line(11) = {{2,3}};
Line(12) = {{3,4}};
コード例 #13
0
# coding: utf-8

from fipy import CellVariable, DiffusionTerm, Grid2D, parallelComm, TransientTerm, Variable
from fipy.tools import dump, numerix
import numpy as np

# Numerical parameters
nx = ny = 100         # domain size
dx = dy = 1.0        # mesh resolution
dt = Variable(0.1) # initial timestep

# Physical parameters
mm = 4.               # anisotropic symmetry
epsilon_m = 0.025     # degree of anisotropy
theta_0 = 0.0         # tilt w.r.t. x-axis
tau_0 = 1.            # numerical mobility
DD = 10.              # thermal diffusivity
W_0 = 1.              # isotropic well height
lamda = DD * tau_0 / 0.6267 / W_0**2
delta = 0.05          # undercooling

# Mesh and field variables
mesh = Grid2D(nx=nx, ny=ny, dx=dx, dy=dy)
phase = CellVariable(mesh=mesh, hasOld=True)
uu = CellVariable(mesh=mesh, hasOld=True)
uu.constrain(-delta, mesh.exteriorFaces)


def initialize():
    phase[:] = -1.0
コード例 #14
0
#%% Parameter Setup
# ALPHA = 0.015 # Alpha CONSTANT
C_ani = 0.02  # Component of (D) the anisotropic diffusion tensor in 2D
N = 6.  # Symmetry
THETA = np.pi / 8  # Orientation

psi = THETA + np.arctan2(phase.faceGrad[1], phase.faceGrad[0])

PHI = np.tan(N * psi / 2)
PHI_SQ = PHI**2
BETA = (1. - PHI_SQ) / (1. + PHI_SQ)
D_BETA_D_PSI = -N * 2 * PHI / (1 + PHI_SQ)
D_DIAG = (1 + C_ani * BETA)
D_OFF = C_ani * D_BETA_D_PSI
I0 = Variable(value=((1, 0), (0, 1)))
I1 = Variable(value=((0, -1), (1, 0)))
DIF_COEF = ALPHA**2 * (1. + C_ani * BETA) * (D_DIAG * I0 + D_OFF * I1)

TAU = 0.0003
KAPPA_1 = 0.9
KAPPA_2 = 20.

phase_EQ = (TransientTerm(TAU) == DiffusionTerm(DIF_COEF) + ImplicitSourceTerm(
    (phase - 0.5 - KAPPA_1 / np.pi * np.arctan(KAPPA_2 * D_temp)) *
    (1 - phase)))

#%% Circular Solidified Region in the Center
radius = DX * 5.0
C_circ = (NX * DX / 2, NY * DY / 2)
X, Y = mesh.cellCenters
コード例 #15
0
    def __init__(self,
                 hours_total=24,
                 day_fraction=0.5,
                 channels=None,
                 **kwargs):
        """
        Initialize an irradiance source in the model domain

        Args:
            hours_total (int, float, PhysicalField): Number of hours in a diel period

            day_fraction (float): Fraction (between 0 and 1) of diel period which is illuminated
                (default: 0.5)

            channels: See :meth:`.create_channel`

            **kwargs: passed to superclass

        """
        self.logger = kwargs.get('logger') or logging.getLogger(__name__)
        self.logger.debug('Init in {}'.format(self.__class__.__name__))
        kwargs['logger'] = self.logger
        super(Irradiance, self).__init__(**kwargs)

        #: the channels in the irradiance entity
        self.channels = {}

        #: the number of hours in a diel period
        self.hours_total = PhysicalField(hours_total, 'h')
        if not (1 <= self.hours_total.value <= 48):
            raise ValueError('Hours total {} should be between (1, 48)'.format(
                self.hours_total))
        # TODO: remove (1, 48) hour constraint on hours_total

        day_fraction = float(day_fraction)
        if not (0 < day_fraction < 1):
            raise ValueError("Day fraction should be between 0 and 1")

        #: fraction of diel period that is illuminated
        self.day_fraction = day_fraction
        #: numer of hours in the illuminated fraction
        self.hours_day = day_fraction * self.hours_total
        #: the time within the diel period which is the zenith of radiation
        self.zenith_time = self.hours_day
        #: the intensity level at the zenith time
        self.zenith_level = 100.0

        C = 1.0 / numerix.sqrt(2 * numerix.pi)
        # to scale the cosine distribution from 0 to 1 (at zenith)

        self._profile = cosine(loc=self.zenith_time,
                               scale=C**2 * self.hours_day)
        # This profile with loc=zenith means that the day starts at "midnight" and zenith occurs
        # in the center of the daylength

        #: a :class:`Variable` for the momentary radiance level at the surface
        self.surface_irrad = Variable(name='irrad_surface',
                                      value=0.0,
                                      unit=None)

        if channels:
            for chinfo in channels:
                self.create_channel(**chinfo)

        self.logger.debug('Created Irradiance: {}'.format(self))
コード例 #16
0
l3 = 31.784 / 1000.0

# [rad] Winkel der aeusseren Kante
alpha = math.radians(25.0)
# [rad] Winkel der inneren Kante
beta = math.radians(30.0)

# Berechnete Werte ------------------------------------------------------------

# Temperaturleitfaehigkeit in x und y Richtung in Tensor (D) zusammenfassen
Dx = material["lambda_x"] / (material["cp"] * material["dichte"])
Dy = material["lambda_y"] / (material["cp"] * material["dichte"])
Dz = material["lambda_y"] / (material["cp"] * material["dichte"])

# D als Urtsunabhängigen Tensor
D = Variable(value=((Dx, 0, 0), (0, Dy, 0), (0, 0, Dz)))

# FiPy init -------------------------------------------------------------------

geometryTemplate = '''
cellSize = {0};

Point(1) = {{ 0,   0,   0,  cellSize}};
Point(2) = {{ {1}, 0,   0,  cellSize}};
Point(3) = {{ {2}, {3}, 0,  cellSize}};
Point(4) = {{ {4}, {3}, 0,  cellSize}};
Point(5) = {{ {4}, {5}, 0,  cellSize}};

Line(10) = {{1,2}};
Line(11) = {{2,3}};
Line(12) = {{3,4}};
コード例 #17
0
ファイル: simulation.py プロジェクト: achennu/microbenthos
class Simulation(CreateMixin):
    """
    This class enables the process of repeatedly solving the model's
    equations for a (small) time step to a certain numerical accuracy,
    and then incrementing the model clock. During the evolution of the
    simulation, the state of the model as well as the simulation is yielded
    repeatedly.

    Numerically approximating the solution to a set of partial differential
    equations requires that the solver system has a reasonable target accuracy
    ("residual") and enough attempts ("sweeps") to reach both a stable and
    accurate approximation for a time step. This class attempts to abstract out
    these optimizations for the user, by performing adaptive time-stepping. The
    user needs to specify a worst-case residual ( :attr:`.max_residual`),
    maximum number of sweeps per time-step ( :attr:`.max_sweeps`) and the range
    of time-step values to explore during evolution (:attr:`.simtime_lims`).
    During the evolution of the simulation, the time-step is penalized if the
    max residual is overshot or max sweeps reached. If not, the reward is a bump
    up in the time-step duration, allowing for faster evolution of the
    simulation.

    See Also:
         The scheme of simulation :meth:`.evolution`.

         The adaptive scheme to :meth:`.update_time_step`.

    """
    schema_key = 'simulation'

    FIPY_SOLVERS = ('scipy', 'pyAMG', 'trilinos', 'pysparse')

    def __init__(
        self,
        simtime_total=6,
        simtime_days=None,
        simtime_lims=(0.1, 120),
        snapshot_interval=60,
        fipy_solver='scipy',
        max_sweeps=100,
        max_residual=1e-12,
    ):
        """
        Args:
            simtime_total (float, PhysicalField): The number of hours for the
            simulation to run

            simtime_days (float): The number of days (in terms of the
                model's irradiance cycle) the simulation should run for. Note
                that specifying this
                will override the given :attr:`.simtime_total` when the
                :attr:`.model` is supplied.

            simtime_lims (float, PhysicalField): The minimum and maximum
            limits for the
                :attr:`simtime_step` for adaptive time-stepping. This should
                be supplied as a
                pair of values, which are assumed to be in seconds and cast
                into PhysicalField
                internally. (default: 0.01, 240)

            max_sweeps (int): Maximum number of sweeps to attempt per
            timestep (default: 50)

            max_residual (float): Maximum residual value for the solver at a
            timestep (default:
                1e-14)

            snapshot_interval (int, float, :class:`PhysicalField`): the
            duration in seconds
                of the model clock between yielding snapshots of the model
                state for exporters
                (default: 60)

            fipy_solver (str): Name of the fipy solver to use. One of
                            ``('scipy', 'pyAMG', 'trilinos','pysparse')`` (default: "scipy")

        """
        super(Simulation, self).__init__()
        # the __init__ call is deliberately empty. will implement
        # cooeperative inheritance only
        # when necessary
        self.logger = logging.getLogger(__name__)
        self._started = False
        self._solver = None

        self._fipy_solver = None
        self.fipy_solver = fipy_solver

        self._simtime_lims = None
        self._simtime_total = None
        self._simtime_step = None

        #: Numer of days to simulate in terms of the model's irradiance source
        self.simtime_days = None

        if simtime_days:
            simtime_days = float(simtime_days)
            if simtime_days <= 0:
                raise ValueError(
                    'simtime_days should be >0, not {:.2f}'.format(
                        simtime_days))
            self.simtime_days = simtime_days

        self.simtime_lims = simtime_lims

        self.simtime_total = simtime_total

        self.simtime_step = self.simtime_lims[0]

        self.snapshot_interval = PhysicalField(snapshot_interval, 's')

        self.max_residual = float(max_residual)
        if not (0 < self.max_residual < 1e-3):
            raise ValueError('Max residual should be a small positive number, '
                             'not {:.3g}'.format(self.max_residual))

        self._residualQ = deque([], maxlen=10)

        self._max_sweeps = None
        self.max_sweeps = max_sweeps
        self._sweepsQ = deque([], maxlen=5)

        self._model = None

    @property
    def started(self):
        """
        Returns:
            bool: Flag for if the sim evolution has started

        """
        return self._started

    @property
    def fipy_solver(self):
        return self._fipy_solver

    @fipy_solver.setter
    def fipy_solver(self, val):
        if val not in self.FIPY_SOLVERS:
            raise ValueError('Solver {!r} not in {}'.format(
                val, self.FIPY_SOLVERS))

        if self.started:
            raise RuntimeError('Fipy solver cannot be changed after started')

        self._fipy_solver = val

    @property
    def simtime_total(self):
        """
        The number of hours of the model clock the simulation should be
        evolved for.

        The supplied value must be larger than the time-steps allowed. Also, it
        may be over-ridden by supplying :attr:`.simtime_days`.

        Returns:
            PhysicalField: duration in hours

        """
        return self._simtime_total

    @simtime_total.setter
    def simtime_total(self, val):
        try:
            val = PhysicalField(val, 'h')
        except TypeError:
            raise ValueError(
                'simtime_total {!r} not compatible with time units'.format(
                    val))

        if val <= 0:
            raise ValueError('simtime_total should be > 0')

        if self.simtime_step is not None:
            if val <= self.simtime_lims[0]:
                raise ValueError('simtime_total {} should be > step {}'.format(
                    val, self.simtime_lims[0]))

        self._simtime_total = val

    @property
    def simtime_step(self):
        """
        The current time-step duration. While setting, the supplied value will
        be clipped to within :attr:`simtime_lims`.

        Returns:
            PhysicalField: in seconds

        """
        return self._simtime_step

    @simtime_step.setter
    def simtime_step(self, val):
        try:
            val = PhysicalField(val, 's')
        except TypeError:
            raise ValueError(
                'simtime_step {!r} not compatible with time units'.format(val))

        dtMin, dtMax = self.simtime_lims
        # val = min(max(val, dtMin), dtMax)
        val = min(val, dtMax)
        assert hasattr(val, 'unit')

        if self.simtime_total is not None:
            if self.simtime_total <= val:
                raise ValueError('simtime_total {} should be > step {}'.format(
                    self.simtime_total, val))

        self._simtime_step = val

    @property
    def simtime_lims(self):
        """
        The limits for the time-step duration allowed during evolution.

        This parameter determines the degree to which the simulation evolution
        can be speeded up. In phases of the model evolution where the numerical
        solution is reached within a few sweeps, the clock would run at the max
        limit, whereas when a large number of sweeps are required, it would be
        penalized towards the min limit.

        A high max value enables faster evolution, but can also lead to
        numerical inaccuracy ( higher residual) or solution breakdown (numerical
        error) during :meth:`.run_timestep`. A small enough min value allows
        recovery, but turning back the clock to the previous time step and
        restarting with the min timestep and allowing subsequent relaxation.

        Args:
            vals (float, PhysicalField): the (min, max) durations in seconds

        Returns:
            lims (tuple): The (min, max) limits of :attr:`simtime_step` each
            as a :class:`.PhysicalField`

        """

        return self._simtime_lims

    @simtime_lims.setter
    def simtime_lims(self, vals):
        if vals is None:
            lmin = PhysicalField(0.1, 's')
            # lmax = (self.simtime_total / 25.0).inUnitsOf('s').floor()
            lmax = PhysicalField(120, 's')
        else:
            lmin, lmax = [PhysicalField(float(_), 's') for _ in vals]
        if not (0 < lmin < lmax):
            raise ValueError(
                'simtime_lims ({}, {}) are not positive and in order'.format(
                    lmin, lmax))
        self._simtime_lims = (lmin, lmax)
        self.logger.debug('simtime_lims set: {}'.format(self._simtime_lims))

    @property
    def max_sweeps(self):
        """
        The maximum number of sweeps allowed for a timestep

        Args:
            val (int): should be > 0

        Returns:
            int

        """
        return self._max_sweeps

    @max_sweeps.setter
    def max_sweeps(self, val):
        try:
            val = int(val)
            assert val > 0
            self._max_sweeps = val
        except:
            raise ValueError('max_sweeps {} should be > 0'.format(val))

    @property
    def model(self) -> MicroBenthosModel:
        """
        The model to run the simulation on. This is typically an instance of
        :class:`~microbenthos.MicroBenthosModel` or its subclasses. The
        interface it must
        provide is:

            * a method :meth:`create_full_equation()`

            * an attribute :attr:`full_eqn` created by above method, which is a
              :class:`~fipy.terms.binaryTerm._BinaryTerm` that has a
              :meth:`sweep()` method.

            * method :meth:`model.update_vars()` which is called before each
            timestep

            * method :meth:`model.clock.increment_time(dt)` which is called
            after each timestep

        Additionally, if :attr:`.simtime_days` is set, then setting the model
        will try to find the ``"env.irradiance:`` object and use its
        :attr:`.hours_total` attribute to set the :attr:`.simtime_total`.

        Args:
            model (:class:`~microbenthos.MicroBenthosModel`): model instance

        Returns:
            :class:`~microbenthos.MicroBenthosModel`

        Raises:
            RuntimeError: if model has already been set
            ValueError: if modes interface does not match
            ValueError: if model :attr:`.model.full_eqn` does not get created
                even after :meth:`.model.create_full_equation` is called.

        """
        return self._model

    @model.setter
    def model(self, m):
        if self.model:
            raise RuntimeError('Model already set')

        full_eqn = getattr(m, 'full_eqn', None)
        if full_eqn is None:
            if hasattr(m, 'create_full_equation'):
                m.create_full_equation()
        full_eqn = getattr(m, 'full_eqn', None)
        if full_eqn is None:
            raise ValueError(
                'Model {!r} (type={}) does not have a valid equation'.format(
                    m, type(m)))

        def recursive_hasattr(obj, path, is_callable=False):
            parts = path.split('.')
            S = obj
            FOUND = False
            for p in parts:
                if hasattr(S, p):
                    S = getattr(S, p)
                    FOUND = True
                else:
                    FOUND = False
                    break

            if not FOUND:
                return False
            else:
                if is_callable:
                    return callable(S)
                else:
                    return True

        expected_attrs = ['clock', 'full_eqn']
        expected_callables = [
            'full_eqn.sweep', 'update_vars', 'clock.increment_time'
        ]
        failed_attrs = tuple(
            filter(lambda x: not recursive_hasattr(m, x), expected_attrs))
        failed_callables = tuple(
            filter(lambda x: not recursive_hasattr(m, x, is_callable=True),
                   expected_callables))

        if failed_attrs:
            self.logger.error(
                'Model is missing required attributes: {}'.format(
                    failed_attrs))
        if failed_callables:
            self.logger.error('Model is missing required callables: {}'.format(
                failed_callables))

        if failed_callables or failed_attrs:
            raise ValueError('Model interface is missing: {}'.format(
                set(failed_attrs + failed_callables)))

        self._model = m

        # if simtime_days is given, override the simtime_total with it
        if self.simtime_days is not None:
            I = self.model.get_object('env.irradiance')
            simtime_total = self.simtime_days * I.hours_total
            self.logger.info(
                'Setting simtime_total={} for {} days of simtime'.format(
                    simtime_total, self.simtime_days))
            self.simtime_total = simtime_total

    def _create_solver(self):
        """
        Create the fipy solver to be used
        """
        solver_module = importlib.import_module('fipy.solvers.{}'.format(
            self.fipy_solver))
        Solver = getattr(solver_module, 'DefaultSolver')
        self._solver = Solver()
        self.logger.debug('Created fipy {} solver: {}'.format(
            self.fipy_solver, self._solver))

    def run_timestep(self):
        """
        Evolve the model through a single timestep
        """
        if not self.started:
            raise RuntimeError(
                'Simulation timestep cannot be run since started=False')

        if self.model is None:
            raise RuntimeError('Simulation model is None, cannot run timestep')

        dt = self.simtime_step
        self.logger.info('Running timestep {} + {}'.format(
            self.model.clock, dt))

        num_sweeps = 0

        EQN = self.model.full_eqn
        retry = True

        res = 100.0

        while (res > self.max_residual) and (num_sweeps < self.max_sweeps) \
            and retry:

            try:
                res = EQN.sweep(solver=self._solver, dt=float(dt.numericValue))
                num_sweeps += 1
                res = float(res)
                self.logger.debug('Sweeps: {}  residual: {:.2g}'.format(
                    num_sweeps, res))

            except (TypeError, RuntimeError):
                self.logger.warning(f'Error with simulation timestep dt={dt}')
                res = self.max_residual * 100
                break

        return res, num_sweeps

    def evolution(self):
        """
        Evolves the model clock through the time steps for the simulation, i.e.
        by calling :meth:`.run_timestep` and :meth:`.model.clock.increment_time`
        repeatedly while ``model.clock() <= self.simtime_total``.

        This is a generator that yields the step number, and the state of the
        evolution after each time step. If :meth:`snapshot_due` is true, then
        also the model snapshot is included in the state.

        Yields:
            `(step, state)` tuple of step number and simulation state

        Raises:
            RuntimeError: if :attr:`.started` is already True

        """
        if self.started:
            raise RuntimeError(
                'Simulation already started. Cannot run parallel evolutions!')

        self.logger.debug(
            'simtime_total={o.simtime_total} simtime_step={o.simtime_step}, '
            'max_sweeps={o.max_sweeps} max_residual={o.max_residual}'.format(
                o=self))

        self.logger.debug('Solving: {}'.format(self.model.full_eqn))

        self._create_solver()
        self._started = True
        self.logger.info('Simulation evolution starting')

        self.model.update_vars()

        self._prev_snapshot = Variable(self.model.clock.copy(),
                                       name='prev_snapshot')
        step = 0
        self.simtime_step = self.simtime_lims[0]

        while self.model.clock() <= self.simtime_total:
            self.logger.debug('Running step #{} {}'.format(
                step, self.model.clock))

            dt = self.simtime_step

            tic = time.time()
            residual, num_sweeps = self.run_timestep()
            toc = time.time()
            self.logger.debug(
                f'For dt={dt} residual={residual:.4g} with sweeps={num_sweeps}'
            )

            # if residual == 0:
            #     raise RuntimeError(f'Residual perfect 0 for dt={dt}. Problem in domain!')

            self._sweepsQ.appendleft(num_sweeps)
            self._residualQ.appendleft(residual)

            if residual >= self.max_residual:

                self.logger.info(
                    f'Ignoring dt={dt}: res={residual:.4g} > {self.max_residual:.4g}'
                )
                self.update_simtime_step(residual, num_sweeps)
                self.model.revert_vars()

                # just go back to while loop start, now that dt has been made smaller
                continue

            else:
                self.model.update_vars()
                self.model.update_equations(dt)

                step += 1

            self.update_simtime_step(residual, num_sweeps)

            calc_time = 1000 * (toc - tic)
            self.logger.debug('Time step {} done in {:.2f} msec'.format(
                self.simtime_step, calc_time))

            if self.snapshot_due():

                self.logger.debug('Snapshot in step #{}'.format(step))

                state = self.get_state(state=self.model.snapshot(),
                                       calc_time=calc_time,
                                       residual=residual,
                                       num_sweeps=num_sweeps)

                yield (step, state)

                # now set the prev_snapshot so that snapshot_due() will
                # remain true for processing
                self._prev_snapshot.setValue(self.model.clock.copy())
                self.logger.debug('Prev snapshot set: {}'.format(
                    self._prev_snapshot))

            else:
                # create a minimal state
                # this is the model clock and current residual
                state = self.get_state(calc_time=calc_time,
                                       residual=residual,
                                       num_sweeps=num_sweeps)

                yield (step, state)

            self.model.clock.increment_time(dt)

        self.logger.info('Simulation evolution completed')
        self._started = False

    def get_state(self, state=None, metrics=None, **kwargs):
        """
        Get the state of the simulation evolution

        Args:
            state (None, dict):
                If state is given (from ``model.snapshot()``), then that is used.
                If None, then just the time info is created by using
                :attr:`.model.clock`.

            metrics (None, dict):
                a dict to get the simulation metrics from,
                else from `kwargs`

            **kwargs:
                parameters to build metrics dict. Currently the keys
                `"calc_time"`, `"residual"` and `"num_sweeps"` are used,
                if available.

        Returns:
            dict: the simulation state

        """

        if state is None:
            state = dict(time=dict(data=snapshot_var(self.model.clock)))

        if metrics is None:
            metrics = dict(
                calc_time=dict(data=(kwargs.get('calc_time', 0.0),
                                     dict(unit='ms'))),
                residual=dict(data=(kwargs.get('residual', 0.0), None)),
                num_sweeps=dict(data=(kwargs.get('num_sweeps', 0), None)),
            )

        state['metrics'] = metrics
        return state

    def snapshot_due(self):
        """
        Returns:
            bool: If the current model clock time has exceeded
            :attr:`.snapshot_interval` since the last snapshot time
        """
        return self.model.clock() - self._prev_snapshot() >= \
               self.snapshot_interval

    def update_simtime_step(self, residual, num_sweeps):
        """
        Update the :attr:`.simtime_step` to be adaptive to the current
        residual and sweeps.

        A multiplicative factor for the time-step is determined based on the
        number of sweeps and residual. If the `residual` is more than
        :attr:`.max_residual`, then the time-step is quartered. If not, it is
        boosted by up to double, depending on the `num_sweeps` and
        :attr:`.max_sweeps`. Once a new timestep is determined, it is limited to
        the time left in the model simulation.

        Args:
            residual (float): the residual from the last equation step
            num_sweeps (int): the number of sweeps from the last equation step

        """
        self.logger.info(
            'Updating step {} after {}/{} sweeps and {:.3g}/{:.3g} '
            'residual'.format(self.simtime_step, num_sweeps, self.max_sweeps,
                              residual, self.max_residual))

        old_step = self.simtime_step
        mult = 1.0

        if residual >= self.max_residual:
            mult = 0.5

        else:
            # if the last N simtime steps have produced residuals lesser than max, then boost the
            # timestep
            if all([r < self.max_residual for r in list(self._residualQ)]):
                mult = 1.25
            else:
                mult = 1.0

        new_step = self.simtime_step * max(0.01, mult)
        self.simtime_step = min(new_step,
                                self.simtime_total - self.model.clock())
        self.logger.info(f'Residual={residual} max={self.max_residual}')
        self.logger.info('Time-step update {} x {:.2g} = {}'.format(
            old_step, mult, self.simtime_step))