Skip to content

Commit

Permalink
TL: updated docs + simplified QDelta generators implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tlunet committed Jun 23, 2024
1 parent 964a1d6 commit 92ca00e
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ c, b, A = genQCoeffs("ERK4")
in particular the [**step by step tutorials**](https://qmat.readthedocs.io/en/latest/notebooks.html)_

For any contribution, please checkout out (very cool) [Contribution Guidelines](https://qmat.readthedocs.io/en/latest/contributing.html)
and the [current Development Roadmap](https://qmat.readthedocs.io/en/latest/devdoc/roadmap.html).
and the current [Development Roadmap](https://qmat.readthedocs.io/en/latest/devdoc/roadmap.html).

## Links

Expand Down
2 changes: 1 addition & 1 deletion docs/devdoc/addRK.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ But this automatic time-step size selection may not be adapted for methods with
to actually see the theoretical order.
In that case, simply add a `CONV_TEST_NSTEPS` _class attribute_ storing a list with **higher numbers of time-steps** in increasing order, high enough so the convergence test passes.

> 📜 See [SDIRK2_2 implementation](https://github.com/Parallel-in-Time/qmat/blob/e17e2dd2aebff1b09188f4314a82338355a55582/qmat/qcoeff/butcher.py#L326) for an example ...
> 📜 See [SDIRK2_2 implementation](https://github.com/Parallel-in-Time/qmat/blob/e17e2dd2aebff1b09188f4314a82338355a55582/qmat/qcoeff/butcher.py#L269) for an example ...

## Embedded scheme
Expand Down
94 changes: 60 additions & 34 deletions docs/devdoc/structure.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# General code structure
# Generic code structure

📜 _Quick introduction on the code design and how to extend it ..._

## Generator registration
## 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.
Expand Down Expand Up @@ -46,33 +46,36 @@ from qmat.qcoeff import QGenerator, register
class MyGenerator(QGenerator):

@property
def nodes(self):
# TODO : implementation
def nodes(self)
# TODO : returns a np.1darray

@property
def weights(self):
# TODO : implementation
# TODO : returns a np.1darray

@property
def Q(self):
# TODO : implementation
# TODO : returns a np.2darray:

@property
def order(self):
# TODO : implementation
# 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 :

1. `nodes` : 1D vector of size `nNodes`
2. `weights` : 1D vector of size `nNides`
2. `weights` : 1D vector of size `nNodes`
3. `Q` : 2D matrix of size `(nNodes,nNodes)`

While `nNodes` is a variable depending on the generator instance, later on the tests checks if the for each $Q$-generators, dimensions of `nodes`, `weights` and `Q` are consistent.
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).
The later is used in the [test series for convergence](https://github.com/Parallel-in-Time/qmat/blob/main/tests/test_qcoeff/test_convergence.py) to check the coefficients.
Value returned by `order` is used in the [test series for convergence](https://github.com/Parallel-in-Time/qmat/blob/main/tests/test_qcoeff/test_convergence.py) 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 ...](./addRK.md)
Even if not it's not mandatory, $Q$-generators can implement a constructor to store parameters, _e.g_ :

Expand All @@ -96,7 +99,7 @@ class MyGenerator(QGenerator):
You can provide required parameters (like `param1`) or optional ones with default value (like `param2`).

> ⚠️ For required parameters, you must provide a default value in the class attribute `DEFAULT_PARAMS`, such that the `QGenerator.getInstance()` class method works.
> The later is used by to create a default instance of the $Q$-generator, by setting required parameters values using `DEFAULT_PARAMS`.
> The later is used during testing to create a default instance of the $Q$-generator, by setting required parameters values using `DEFAULT_PARAMS`.
After implementing a new generator, you should test is by running the following test :

Expand All @@ -106,20 +109,39 @@ 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 ...](./testing.md)

> 🔔 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_NSTEPS` class 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](./addRK.md#convergence-testing) for more details ...
## $Q_\Delta$-generators implementation

First, know that the base `QDeltaGenerator` class implement the following constructor :
By default, the base `QDeltaGenerator` class implement those base methods, that may be used by any
specialized $Q_\Delta$ generator.

```python
class QDeltaGenerator(object):

def __init__(self, Q, **kwargs):
self.Q = np.asarray(Q, dtype=float)
self.QDelta = np.zeros_like(self.Q)

@property
def size(self):
return self.Q.shape[0]

@property
def zeros(self):
M = self.size
return np.zeros((M, M), dtype=float)
```
This default constructor is actually 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.

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 :

Expand All @@ -129,27 +151,28 @@ from qmat.qdelta import QDeltaGenerator, register
@register
class MyGenerator(QDeltaGenerator):

def getQDelta(self, k=None):
# TODO : implementation
return self.QDelta
def computeQDelta(self, k=None):
# TODO : returns a np.2darray with shape (self.size, self.size)
```

In practice, `getQDelta` must modify the current `QDelta` attribute (initialized with zeros) and return it.
You may implement a check avoiding to recompute `QDelta` when already computed, _e.g_
The `computeQDelta` must simply returns the $Q_\Delta$ approximation for this generator,
eventually using the `zeros` property as starting basis.

```python
@register
class MyGenerator(QDeltaGenerator):
**📣 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 :

def getQDelta(self, k=None):
if hasattr(self, "_computed"):
return self.QDelta
# TODO : implementation
self._computed = "ouiii"
return self.QDelta
- `None` if $Q_\Delta$ does not vary with `k`
- **any other value** you see fit if $Q_\Delta$ varies with `k`. For instance, using `1` as default value :

```python
def computeQDelta(self, k=1):
if k is None: k=1
# TODO : returns a np.2darray with shape (self.size, self.size)
```

> ⚠️ Even if this may not be used by your generator, the `getQDelta` method should always take a `k` optional parameter (with the default value you see fit, `None` is enough if you don't use `k`).
> ⚠️ The `computeQDelta` method must be able to take `k=None` as argument, and eventually replace it by its default value.
You can also redefine the constructor of your generator like this :
```python
Expand All @@ -158,13 +181,16 @@ class MyGenerator(QDeltaGenerator):

def __init__(self, param1, param2, **kwargs):
# TODO : implementation
self.QDelta = ...

@property
def size(self):
# TODO : proper redefinition
```

But then it is necessary to :

1. add the `**kwargs` arguments to your constructor, but don't use it for your generator's parameters : `**kwargs` is only used when series of $Q_\Delta$ matrices are generated from different types of generators
2. zero-initialize a `QDelta` `numpy.ndarray` with the appropriate shape (square matrix).
1. add the `**kwargs` arguments to your constructor, but don't use it for your generator's parameters : `**kwargs` is only used when $Q_\Delta$ matrices are generated from different types of generators using one single call
2. properly redefine the `size` property if you don't store any $Q$ matrix attribute in your constructor


## Additional submodules
Expand Down
16 changes: 15 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,23 @@ It allows to generate :math:`Q`-coefficients for multi-stages methods (equivalen
and many different **lower-triangular** approximation of the :math:`Q` matrix, named :math:`Q_\Delta`,
which are key elements for Spectral Deferred Correction (SDC), or more general Iterated Runge-Kutta Methods.

.. raw:: html

📜 *If you are already familiar with those concepts, you can use this package like this :*
<a href="https://zenodo.org/doi/10.5281/zenodo.11956478">
<img alt="DOI" src="https://zenodo.org/badge/804826743.svg">
</a>


Package can be installed using `pip` :

.. code-block:: bash
pip install qmat # basic installation through
... but you can also use `conda` or installation from sources, see the :doc:`Installation Instructions 💾<installation>`


📜 *If you are already familiar with those concepts, you can use this package like this :*

.. code-block:: python
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

## Using PyPI

You can download the latest version from [`pypi`](https://pypi.org/) :
You can download the latest version from [`pypi`](https://pypi.org/) using `pip` :

```bash
pip install qmat
Expand Down
15 changes: 12 additions & 3 deletions qmat/qdelta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
"""
Base module for QDelta coefficients generation
"""
import inspect
import numpy as np

from qmat.utils import checkOverriding, storeClass, importAll, checkGenericConstr


class QDeltaGenerator(object):
_K_DEP = False
_K_DEPENDENT = False

def __init__(self, Q, **kwargs):
self.Q = np.asarray(Q, dtype=float)
Expand All @@ -29,11 +30,11 @@ def computeQDelta(self, k=None) -> np.ndarray:

def getQDelta(self, k=None, copy=True):
try:
QDelta = self._QDelta[k] if self._K_DEP else self._QDelta
QDelta = self._QDelta[k] if self._K_DEPENDENT else self._QDelta
except Exception as e:
QDelta = self.computeQDelta(k)
if type(e) == AttributeError:
self._QDelta = {k: QDelta} if self._K_DEP else QDelta
self._QDelta = {k: QDelta} if self._K_DEPENDENT else QDelta
elif type(e) == KeyError:
self._QDelta[k] = QDelta
else:
Expand All @@ -59,6 +60,14 @@ def genCoeffs(self, k=None, dTau=False):
def register(cls:QDeltaGenerator)->QDeltaGenerator:
checkGenericConstr(cls)
checkOverriding(cls, "computeQDelta", isProperty=False)
try:
sig = inspect.signature(cls.computeQDelta)
par = sig.parameters["k"]
assert par.kind == par.POSITIONAL_OR_KEYWORD
if par.default is not None:
cls._K_DEPENDENT = True
except (KeyError, AssertionError):
raise AssertionError(f"{cls.__name__} class does not properly override the computeQDelta method")
storeClass(cls, QDELTA_GENERATORS)
return cls

Expand Down
6 changes: 2 additions & 4 deletions qmat/qdelta/min.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,10 @@ def computeQDelta(self, k=None):

@register
class MIN_SR_FLEX(MIN_SR_S):
_K_DEP = True
aliases = ["MIN-SR-FLEX"]

def computeQDelta(self, k=None):
if k is None:
k = 1
def computeQDelta(self, k=1):
if k is None: k = 1
if k < 1:
raise ValueError(f"k must be greater than 0 ({k})")
if k <= self.size:
Expand Down

0 comments on commit 92ca00e

Please sign in to comment.