Code structure
📜 Quick introduction on how the package is designed and how to extend it …
Registration mechanism
The two main features, namely the generation of \(Q\)-coefficients and \(Q_\Delta\) approximations,
are respectively implemented in the qmat.qcoeff and qmat.qdelta sub-packages.
Different categories of generators are implemented in dedicated submodules of their respective sub-packages,
e.g :
qmat.qcoeff.collocationfor Collocation-based \(Q\)-generatorsqmat.qdelta.algebraicfor algebraic based \(Q_\Delta\) approximations…
Each sub-package contains a __init__.py file implementing the generic parent class for all generators.
In their submodules, generators are implemented using a registration mechanism,
e.g for the Collocation-based \(Q\)-generators :
from qmat.qcoeff import QGenerator, register
@register
class Collocation(QGenerator):
aliases = ["coll"]
# ...
A similar mechanism is used for \(Q_\Delta\) generators. The register function is used as class decorator which :
checks that the implemented class properly overrides the method of its parent class (more specific details below)
stores it in a centralized dictionary allowing a quick access using the class name or one of its aliases :
qmat.Q_GENERATORSfor \(Q\)-coefficientsqmat.QDELTA_GENERATORSfor the \(Q_\Delta\) approximations
💡 Different aliases for the generator can be provided with the
aliasesclass attribute, but are not mandatory (defining the class attribute is optional).
\(Q\)-generators implementation
To implement a new \(Q\)-generator (in an existing or new category), new classes must at least follow this template :
from qmat.qcoeff import QGenerator, register
@register
class MyGenerator(QGenerator):
@property
def nodes(self)
# TODO : returns a np.1darray
@property
def weights(self):
# TODO : returns a np.1darray
@property
def Q(self):
# TODO : returns a np.2darray:
@property
def order(self):
# TODO : returns an int
The nodes, weights, and Q properties have to be overridden
(register actually raises an error if not) and return
the expected arrays in numpy.ndarray format :
nodes: 1D vector of sizenNodesweights: 1D vector of sizenNodesQ: 2D matrix of size(nNodes,nNodes)
While nNodes is directly determined from the nodes property, later on the tests checks if the for each \(Q\)-generators, dimensions of nodes, weights and Q are consistent.
Finally, you should implement the order property, that returns the theoretical accuracy order of the associated scheme (global truncation error).
Value returned by order is used in the test series for convergence to check the coefficients.
🔔 For Runge-Kutta type generators, their implementation use an additional abstract layer to simplify the addition of new schemes, see specific documentation to add RK schemes …
Even if not it’s not mandatory, \(Q\)-generators can implement a constructor to store parameters, e.g :
from qmat.qcoeff import QGenerator, register
@register
class MyGenerator(QGenerator):
DEFAULT_PARAMS = {
"param1": 0.5,
}
def __init__(self, param1, param2=1):
self.param1 = param1
self.param2 = param2
# Implementation of nodes, weights and Q properties
You can provide required parameters (e.g param1) or optional ones with default value (e.g param2).
⚠️ For required parameters, you must provide a default value in the class attribute
DEFAULT_PARAMS, such that theQGenerator.getInstance()class method works. The later is used during testing to create a default instance of the \(Q\)-generator, by setting required parameters values usingDEFAULT_PARAMS.
After implementing a new generator, you should test is by running the following test :
pytest -v ./tests/test_qcoeff
This will run all consistency and convergence check tests on all generators (including yours), more details on how to run the tests are provided here …
🔔 Convergence tests for new \(Q\)-generators are automatically done depending on its order. In some particular case, you may have to add a
CONV_TEST_NSTEPSclass variable to your generator class for those tests to pass (e.g, if your generator has a high error constant). See documentation on adding RK schemes for more details …
\(Q_\Delta\)-generators implementation
By default, the base QDeltaGenerator class implement those base methods, that may be used by any
specialized \(Q_\Delta\) generator.
class QDeltaGenerator(object):
def __init__(self, Q, **kwargs):
self.Q = np.asarray(Q, dtype=float)
@property
def size(self):
return self.Q.shape[0]
@property
def zeros(self):
M = self.size
return np.zeros((M, M), dtype=float)
The default constructor stores the \(Q\) matrix that is approximated,
and the size property is used to determine the shape of generated \(Q_\Delta\) approximation,
and the zeros property can be used to generate the initial basis for \(Q_\Delta\).
🔔 The default constructor is used by all the specialized generators implemented in
qmat.qdelta.algebraic, as their \(Q_\Delta\) approximation is build directly from the \(Q\) matrix given as parameter.
To implement a new \(Q_\Delta\)-generator (in an existing or new category), new classes must at least follow this template :
from qmat.qdelta import QDeltaGenerator, register
@register
class MyGenerator(QDeltaGenerator):
def computeQDelta(self, k=None):
# TODO : returns a np.2darray with shape (self.size, self.size)
The computeQDelta must simply returns the \(Q_\Delta\) approximation for this generator,
potentially using the zeros property as starting basis.
📣 Important : even if this may not be used by your generator, the computeQDelta method must always
take a k optional parameter corresponding to a sweep or iteration number in SDC or iterated RK methods,
starting at \(k=1\) for the first sweep.
The default value for this parameter must be :
Noneif \(Q_\Delta\) does not vary withkany other value you see fit if \(Q_\Delta\) varies with
k. For instance, using1as default value :
def computeQDelta(self, k=1):
if k is None: k=1
# TODO : returns a np.2darray with shape (self.size, self.size)
⚠️ The
computeQDeltamethod must be able to takek=Noneas argument, and potentially replace it by its default value.
You can also redefine the constructor of your generator like this :
@register
class MyGenerator(QDeltaGenerator):
def __init__(self, param1, param2, **kwargs):
# TODO : implementation
@property
def size(self):
# TODO : proper redefinition
But then it is necessary to :
add the
**kwargsarguments to your constructor, but don’t use it for your generator’s parameters :**kwargsis only used when \(Q_\Delta\) matrices are generated from different types of generators using one single callproperly redefine the
sizeproperty if you don’t store any \(Q\) matrix attribute in your constructor
Additional sub-packages
qmat.solvers: implements various generic ODE making use ofqmat-generated coefficients. Can be modified to add new differential operators or add new \(\phi\)-based integratorsqmat.playgrounds: can be modified to add a playground, i.e non-tested experiments or examples script
Additional submodules
qmat.nodes: can be modified to add new functionalities to theNodesGeneratorclass, or improve the current implementationsqmat.lagrange: can be modified to add new functionalities to theLagrangeApproximationclass, or improve the current implementationsqmat.mathutils: can be modified to add additional mathematical utility functions used by some parts inqmat(like array operations, regression tools, etc …)qmat.utils: can be modified to add additional (non mathematical) utility functions used by some parts inqmat(like timers, implementation check functions, etc …)