Nested Looping

The whole numerical simulation is controlled by an Outer Loop and many Inner Loops. SOLVCON materializes them with MeshCase and MeshSolver, respectively.

Outer Loop

SOLVCON simulation is orchestrated by MeshCase, which should be subclassed to implement control logic for a specific application. The application can be a concrete model for a certain physical process, or an abstraction of a group of related physical processes, which can be further subclassed.

Because a case controls the whole process of a simulation run, for parallel execution, there can be only one MeshCase object residing in the controller (head) node.

class solvcon.MeshCase(**kw)

init() and run() are the two primary methods responsible for the execution of the simulation case object. Both methods accept a keyword parameter “level”:

  • run level 0: fresh run (default),
  • run level 1: restart run,
  • run level 2: initialization only.

Initialize

MeshCase.init(level=0)
Parameters:level (int) – Run level; higher level does less work.
Returns:Nothing

Load a block and initialize the solver from the geometry information in the block and conditions in the self case. If parallel run is specified (through domaintype), split the domain and perform corresponding tasks.

For a MeshCase to be initialized, some information needs to be supplied to the constructor:

>>> cse = MeshCase()
>>> cse.info.muted = True
>>> cse.init() 
Traceback (most recent call last):
    ...
TypeError: ...
  1. Mesh information. We can provide meshfn that specifying the path of a valid mesh file, or provide mesher, which is a function that generates the mesh and returns the solvcon.block.Block object, like the following code:

    >>> from solvcon.testing import create_trivial_2d_blk
    >>> blk = create_trivial_2d_blk()
    >>> cse = MeshCase(mesher=lambda *arg: blk)
    >>> cse.info.muted = True
    >>> cse.init() 
    Traceback (most recent call last):
        ...
    TypeError: isinstance() arg 2 must be ...
    
  2. Type of the spatial domain. This information is used for detemining sequential or parallel execution, and performing related operations:

    >>> cse = MeshCase(mesher=lambda *arg: blk, domaintype=domain.Domain)
    >>> cse.info.muted = True
    >>> cse.init()
    Traceback (most recent call last):
        ...
    TypeError: 'NoneType' object is not callable
    
  3. The type of solver. It is used to specify the underlying numerical method:

    >>> from solvcon.solver import MeshSolver
    >>> cse = MeshCase(mesher=lambda *arg: blk,
    ...                domaintype=domain.Domain,
    ...                solvertype=MeshSolver)
    >>> cse.info.muted = True
    >>> cse.init() 
    Traceback (most recent call last):
        ...
    TypeError: ...
    
  4. The base name. It is used to name its output files:

    >>> cse = MeshCase(
    ...     mesher=lambda *arg: blk, domaintype=domain.Domain,
    ...     solvertype=MeshSolver, basefn='meshcase')
    >>> cse.info.muted = True
    >>> cse.init()
    

Time-March

MeshCase.run(level=0)
Parameters:level (int) – Run level; higher level does less work.
Returns:Nothing

Temporal loop for the incorporated solver. A simple example:

>>> from .testing import create_trivial_2d_blk
>>> from .solver import MeshSolver
>>> blk = create_trivial_2d_blk()
>>> cse = MeshCase(basefn='meshcase', mesher=lambda *arg: blk,
...                domaintype=domain.Domain, solvertype=MeshSolver)
>>> cse.info.muted = True
>>> cse.init()
>>> cse.run()

Arrangement

MeshCase.arrangements

The class-level registry for arrangements.

classmethod MeshCase.register_arrangement(func, casename=None)
Returns:Simulation function.
Return type:callable

This class method is a decorator that creates a closure (internal function) that turns the decorated function to an arrangement, and registers the arrangement into the module-level registry and the class-level registry. The decorator function should return a MeshCase object cse, and the closure performs a simulation run by the following code:

try:
    signal.signal(signal.SIGTERM, cse.cleanup)
    signal.signal(signal.SIGINT, cse.cleanup)
    cse.init(level=runlevel)
    cse.run(level=runlevel)
    cse.cleanup()
except:
    cse.cleanup()
    raise

The usage of this decorator can be exemplified by the following code, which creates four arrangements (although the first three are erroneous):

>>> @MeshCase.register_arrangement
... def arg1():
...     return None
>>> @MeshCase.register_arrangement
... def arg2(wrongname):
...     return None
>>> @MeshCase.register_arrangement
... def arg3(casename):
...     return None
>>> @MeshCase.register_arrangement
... def arg4(casename):
...     from .testing import create_trivial_2d_blk
...     from .solver import MeshSolver
...     blk = create_trivial_2d_blk()
...     cse = MeshCase(basefn='meshcase', mesher=lambda *arg: blk,
...                    domaintype=domain.Domain, solvertype=MeshSolver)
...     cse.info.muted = True
...     return cse

The created arrangements are collected to a class attribute arrangements, i.e., the class-level registry:

>>> sorted(MeshCase.arrangements.keys())
['arg1', 'arg2', 'arg3', 'arg4']

The arrangements in the class attribute arrangements are also put into a module-level attribute solvcon.case.arrangements:

>>> arrangements == MeshCase.arrangements
True

The first example arrangement is a bad one, because it allows no argument:

>>> arrangements.arg1() 
Traceback (most recent call last):
  ...
TypeError: arg1() ...

The second example arrangement is still a bad one, because although it has an argument, the name of the argument is incorrect:

>>> arrangements.arg2()
Traceback (most recent call last):
  ...
TypeError: arg2() got an unexpected keyword argument 'casename'

The third example arrangement is a bad one for another reason. It doesn’t return a MeshCase:

>>> arrangements.arg3()
Traceback (most recent call last):
  ...
AttributeError: 'NoneType' object has no attribute 'cleanup'

The fourth example arrangement is finally good:

>>> arrangements.arg4()

Hooks on Cases

MeshHook performs custom operations at certain pre-defined stages.

class solvcon.MeshHook(cse, **kw)

Base type for hooks needing a MeshCase.

MeshCase.defer(delayed, replace=False, **kw)
Parameters:
Returns:

Nothing.

Insert (append or replace) hooks.

>>> import solvcon as sc
>>> from solvcon.testing import create_trivial_2d_blk
>>> cse = MeshCase() # No arguments because of demonstration.
>>> len(cse.runhooks)
0
>>> # Insert a hook.
>>> cse.defer(sc.MeshHook, dummy='name1')
>>> cse.runhooks[0].kws['dummy']
'name1'
>>> # Insert the second hook to replace the first one.
>>> cse.defer(sc.MeshHook, replace=True, dummy='name2')
>>> cse.runhooks[0].kws['dummy'] # Got replaced.
'name2'
>>> len(cse.runhooks) # Still only one hook in the list.
1
>>> # Insert the third hook without replace.
>>> cse.defer(sc.MeshHook, dummy='name3')
>>> cse.runhooks[1].kws['dummy'] # Got replaced.
'name3'

Inner Loops

Numerical methods should be implemented by subclassing MeshSolver. The base class is defined as:

class solvcon.MeshSolver(blk, time=0.0, time_increment=0.0, enable_mesg=False, debug=False, **kw)

Base class for all solving code that take Mesh, which is usually needed to write efficient C/C++ code for implementing numerical methods.

Here’re some examples about using MeshSolver. The first example shows that we can’t directly use it. A vanilla MeshSolver can’t march:

>>> from . import testing
>>> svr = MeshSolver(testing.create_trivial_2d_blk())
>>> svr.march(0.0, 0.1, 1) 
Traceback (most recent call last):
    ...
TypeError: 'NoneType' object ...

Of course the above solver does nothing. Let’s see another example for a non-trivial solver:

>>> class ExampleSolver(MeshSolver):
...     @MeshSolver.register_marcher
...     def calcsomething(self, worker=None):
...         self.marchret['key'] = 'value'
>>> svr = ExampleSolver(testing.create_trivial_2d_blk())
>>> svr.march(0.0, 0.1, 1)
{'key': 'value'}

Two instance attributes are used to record the temporal information:

time = None
time_increment = None

Four instance attributes are used to track the status of time-marching:

step_current = None
step_global = None
substep_run = None
substep_current = None

Time-marchers:

static register_marcher(func)

Any time-marching method in a derived class of MeshSolver should be decorated by this function.

mmnames

Generator of time-marcher names.

Useful entities are attached to the class MeshSolver:

MeshSolver.ALMOST_ZERO

A positive floating-point number close to zero. The value is not DBL_MIN, which can be accessed through sys.float_info.

Time-Marching

MeshSolver.march(time_current, time_increment, steps_run, worker=None)
Parameters:
  • time_current (float) – Starting time of this set of marching steps.
  • time_increment (float) – Temporal interval \(\Delta t\) of the time step.
  • steps_run (int) – The count of time steps to run.
Returns:

marchret

This method performs time-marching. The parameters time_current and time_increment are used to reset the instance attributes time and time_increment, respectively. In each invokation step_current is reset to 0.

There is a nested two-level loop in this method for time-marching. The outer loop iterates for time steps, and the inner loop iterates for sub time steps. The outer loop runs steps_run times, while the inner loop runs substep_run times. In total, the inner loop runs steps_run * substep_run times. In each sub time step (in the inner loop), the increment of the attribute time is time_increment/substep_run. The temporal increment per time step is effectively time_increment, with a slight error because of round-off.

Before entering and after leaving the outer loop, premarch and postmarch anchors will be run (through the attribute runanchors). Similarly, before entering and after leaving the inner loop, prefull and postfull anchors will be run. Inside the inner loop of sub steps, before and after executing all the marching methods, presub and postsub anchors will be run. Lastly, before and after invoking every marching method, a pair of anchors will be run. The anchors for a marching method are related to the name of the marching method itself. For example, if a marching method is named “calcsome”, anchor precalcsome will be run before the invocation, and anchor postcalcsome will be run afterward.

Derived classes can set marchret dictionary, and march() will return the dictionary at the end of execution. The dictionary is reset to empty at the begninning of the execution.

MeshSolver.marchret

Values to be returned by this solver. It will be set to a dict in march().

MeshSolver.runanchors

This attribute is of type MeshAnchorList, and the foundation of the anchor mechanism of SOLVCON. An MeshAnchorList object like this collects a set of MeshAnchor objects, and is callable. When being called, runanchors iterates the contained MeshAnchor objects and invokes the corresponding methods of the individual MeshAnchor.

MeshSolver.der

Derived data container as a dict.

Parallel Computing

MeshSolver.svrn

This member indicates the serial number (0-based) of the MeshSolver object.

MeshSolver.nsvr

The total number of collaborative solvers in the parallel run, and is initialized to None.

Anchors on Solvers

class solvcon.MeshAnchor(svr, **kw)

Callback class to be invoked by MeshSolver objects at various stages.

svr

The associated MeshSolver instance.

class solvcon.MeshAnchorList(svr, *args, **kw)

Sequence container for MeshAnchor instances.

svr

The associated MeshSolver instance.

Boundary-Condition Treatments

class solvcon.BC(bc=None, fpdtype=None)

Generic boundary condition abstract class; the base class that all boundary condition classes should subclass.

FIXME: provide doctests as examples.

facn = None
value = None
nvalue

Return the length of vnames as number of values per boundary face. It should be equivalent to the second shape element of value.

FIXME: provide doctests.

__len__()

Return the first shape element of facn.

FIXME: provide doctests.

cloneTo(another)
Parameters:another (solvcon.boundcond.BC) – Another BC object.
Returns:Nothing.

Clone self to another BC object.

create_bcd()
Returns:An object contains the sc_bound_t variable for C interfacing.
Return type:solvcon.mesh.Bound

The following code shows how and when to use this method:

>>> import numpy as np
>>> # craft some face numbers for testing.
>>> bndfcs = [0,1,2]
>>> # craft the BC object for testing.
>>> bc = BC()
>>> bc.name = 'some_name'
>>> bc.facn = np.empty((len(bndfcs), BC.BFREL), dtype='int32')
>>> bc.facn.fill(-1)
>>> bc.facn[:,0] = bndfcs
>>> bc.sern = 0
>>> bc.blk = None # should be set to a block.
>>> # test for this method.
>>> bcd = bc.create_bcd()
BC.vnames = []

Settable value names.

BC.vdefaults = {}

Default values.

sc_bound_t

This struct contains essential information of a BC object in C.

int nbound

Number of boundary faces. It’s equivalent to what BC.__len__() returns.

int nvalue

Number of values per boundary face.

int* facn

Pointer to the data storage of BC.facn.

double* value

Pointer to the data storage of BC.value.

class solvcon.mesh.Bound

This class associates the C functions for mesh operations to the mesh data and exposes the functions to Python.

_bcd

This attribute holds a C struct sc_bound_t for internal use.

setup_bound(bc)
Parameters:bc – The BC object that supplies information.