diff --git a/CHANGELOG.md b/CHANGELOG.md
index 199474be..c9f5eed4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Documentation for the `Domain` class, its API reference, theoretical
+ background, and examples
+- Domain support in interpolation module: `interpolate()`, `Interpolator`, and
+ `Interpolant` now accept `bounds` parameter to specify custom rectangular
+ domains directly without manually creating `Domain` objects;
+ the underlying interpolating polynomials are constructed with domain
+ awareness
+- `interpolate_values()` method in `Interpolator` class to interpolate
+ pre-computed function values at unisolvent nodes, enabling reuse of
+ function evaluations
+- Domain-aware polynomial differentiation: `diff()` and `partial_diff()` now
+ automatically apply chain rule scaling factors when differentiating
+ polynomials over user-defined domains
+- Domain-aware polynomial integration: `integrate_over()` now automatically
+ applies Jacobian scaling factors when integrating polynomials over
+ user-defined domains
+- Domain-aware polynomial evaluation: `__call__()` now accepts query points
+ in the (user) domain with automatic transformation to the internal domain
+ (currently `[-1, 1]^m`)
+- `eval_on_internal()` method for direct polynomial evaluation
+ in the internal domain, bypassing coordinate transformation
+- `Domain` class for handling transformation between user-defined rectangular
+ domains and internal (reference) domains
+ - Support for coordinate transformations via `map_to_internal()`
+ and `map_from_internal()`
+ - Automatic scaling factor computation for differentiation and integration
+ - Domain validation via `contains()` method
+ - Factory methods: `uniform()` and `normalized()`
+ - The property `is_identity` indicates if the user-defined domain
+ is identical to the internal domain
+- Domain support integrated into `Grid` class
+ - `domain` parameter in Grid constructor (defaults to normalized domain
+ in `[-1, 1]^m`)
+ - Grid operations (`*`, `|`) now validate domain consistency
+- Domain property access in all polynomial classes via `poly.domain`
+ obtained from the corresponding `Grid` instance
+- Internal domain infrastructure in `Domain` class with `internal_bounds`
+ property
+
+### Changed
+
+- Relevant tutorials have been updated to reflect the new domain support
+- Refactored `Interpolator` class to use modern `attrs.define` syntax
+ instead of legacy `attr.ib` decorators
+- Interpolation tests now cover both default (internal reference)
+ and custom domain cases
+- Zero-order differentiation now returns a copy of the polynomial
+ (identity operation)
+- Centralized polynomial differentiation tests into a dedicated test module
+ (`test_polynomial_differentiation.py`); relevant tests from basis-specific
+ test modules have been removed
+- Refactored `partial_diff()` as syntactic sugar for `diff()`, removing
+ duplicate `_partial_diff()` static methods from the polynomial class
+ hierarchy
+- Centralized polynomial integration tests into a dedicated test module
+ (`test_polynomial_integration.py`); relevant tests from dedicated test
+ modules (with respect to each basis) have been removed.
+- Grid generating points validation now uses `Domain` class for bound checking
+- Refactored polynomial arithmetic into modular components
+- Optimized `MultiIndexSet` equality check with identity short-circuit
+- Improved Newton polynomial monomial-based multiplication logic
+
+### Removed
+
+- Strict bounds validation in polynomial integration: integration outside
+ domain bounds is now allowed (extrapolation) but should be used with care
+- `internal_domain` and `user_domain` properties from polynomial abstract class
+- `check_domain_fit()` verification function (replaced by `Domain.contains()`)
+- the module `minterpy.utils.polynomials.interface`
+
## [Version 0.3.1] - 2025-04-30
This minor release incorporates feedback from the review process of
@@ -233,4 +305,4 @@ that neither everything works as expected,
nor if further releases will break the current API.
[Unreleased]: https://github.com/minterpy-project/minterpy/compare/main...dev
-[0.3.1]: https://github.com/minterpy-project/minterpy/compare/v0.3.0...v0.3.1
+[Version 0.3.1]: https://github.com/minterpy-project/minterpy/compare/v0.3.0...v0.3.1
diff --git a/README.md b/README.md
index c19fbe0c..6aa85809 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-[](https://rodare.hzdr.de/record/2062)
+[](https://rodare.hzdr.de/record/3725)
[](https://joss.theoj.org/papers/96208a133980e518cdfdc36abdc504de)
[![Code style: black][black-badge]][black-link]
[](https://choosealicense.com/licenses/mit/)
@@ -9,20 +9,27 @@
# Minterpy: Multivariate Polynomial Interpolation in Python
| Branches | Status |
-| :-----------------------------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| :-----------------------------------------------------------------------: |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`main`](https://github.com/minterpy-project/minterpy/tree/main) (stable) | [](https://github.com/minterpy-project/minterpy/actions/workflows/build.yaml?query=branch%3Amain+) [](https://codecov.io/gh/minterpy-project/minterpy) [](https://minterpy-project.github.io/minterpy/stable/) |
| [`dev`](https://github.com/minterpy-project/minterpy/tree/dev) (latest) | [](https://github.com/minterpy-project/minterpy/actions/workflows/build.yaml?query=branch%3Adev) [](https://codecov.io/gh/minterpy-project/minterpy) [](https://minterpy-project.github.io/minterpy/latest/) |
Minterpy is an open-source Python package designed for constructing
and manipulating multivariate interpolating polynomials
-with the goal of lifting the curse of dimensionality from interpolation tasks.
+with the goal of addressing the curse of dimensionality
+from interpolation tasks.
Minterpy is being continuously extended and improved,
-with new functionalities added to address the bottlenecks involving
-interpolations in various computational tasks.
+with new functionalities added to address the computational bottlenecks
+in accuracy, stability, and performance of multidimensional interpolation
+tasks.
## Installation
+You can install Minterpy from [PyPI](https://pypi.org/project/minterpy/)
+or from [source](https://github.com/minterpy-project/minterpy).
+
+### From PyPI
+
You can obtain the stable release of Minterpy directly
from [PyPI](https://pypi.org/project/minterpy/) using `pip`:
@@ -30,70 +37,79 @@ from [PyPI](https://pypi.org/project/minterpy/) using `pip`:
pip install minterpy
```
-Alternatively, you can also obtain the latest version of Minterpy
-from the [GitHub repository](https://github.com/minterpy-project/minterpy):
+### From source
+
+To install the latest development version of Minterpy,
+you can clone the [GitHub repository](https://github.com/minterpy-project/minterpy):
```bash
git clone https://github.com/minterpy-project/minterpy
```
-Then from the source directory, you can install Minterpy:
+Then install Minterpy from the source directory:
+
+```bash
+pip install .[all,dev,docs]
+```
+
+For an editable installation, use the flag `-e`:
```bash
-pip install [-e] .[all,dev,docs]
+pip install -e .[all,dev,docs]
```
-where the flag `-e` means the package is directly linked into
-the python site-packages of your Python version.
The options `[all,dev,docs]` refer to the requirements defined
in the `options.extras_require` section in `setup.cfg`.
-A best practice is to first create a virtual environment with the help of
-a tool like [mamba], [conda], [venv], [virtualenv] or [pyenv-virtualenv].
+### Virtual environments
+
+We recommend installing Minterpy in a virtual environment.
+Tools like [mamba], [conda], [venv], [virtualenv] or [pyenv-virtualenv]
+can help you set one up.
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
-**NOTE**: **Do not** use the command `python setup.py install`
-to install Minterpy, as we cannot guarantee that the file `setup.py`
-will always be present in the further development of Minterpy.
+> **NOTE**: **Do not** use the command `python setup.py install`
+> to install Minterpy, as `setup.py` may not be present in the future releases.
## Quickstart
-Using Minterpy, you can easily interpolate a given function.
-For instance, take the one-dimensional function $`f(x) = x \, \sin{(10x)}`$
-with $x \in [-1, 1]$:
+Using Minterpy, you can interpolate a given function.
+For instance, take the one-dimensional function $`f(x) = x \, \sin{(x)}`$
+with $x \in [0, 15]$:
```python
import numpy as np
-def test_function(x):
- return x * np.sin(10*x)
+def func(x):
+ return x * np.sin(x)
```
-To interpolate the function, you can use the top-level function `interpolate()`:
+To interpolate the function, you can use the function `interpolate()`:
```python
import minterpy as mp
-interpolant = mp.interpolate(test_function, spatial_dimension=1, poly_degree=64)
+interpolant = mp.interpolate(func, spatial_dimension=1, poly_degree=64, bounds=[0, 15])
```
`interpolate()` takes as arguments the function to interpolate,
the number of dimensions (`spatial_dimension`),
-and the degree of the underlying polynomial interpolant (`poly_degree`).
-You may adjust this parameter in order to get higher accuracy.
+the degree of the underlying polynomial interpolant (`poly_degree`),
+and the bounds of the domain (`bounds`).
+You may adjust the polynomial degree parameter to get higher accuracy.
The resulting `interpolant` is a Python callable,
-which can be used as an approximation of `test_function`.
+which can be used as an approximation of `func`.
In this example, an interpolating polynomial of degree $64$ produces
-an approximation of `test_function` to near machine precision:
+an approximation of `func` to near machine precision:
```python
import matplotlib.pyplot as plt
-xx = np.linspace(-1, 1, 150)
+xx = np.linspace(0, 15, 100)
+plt.plot(xx, func(xx), "k.",label="function")
plt.plot(xx, interpolant(xx), label="interpolant")
-plt.plot(xx, test_function(xx), "k.",label="test function")
plt.legend()
plt.show()
```
@@ -106,28 +122,29 @@ you can carry out common numerical operations on the approximations
like multiplication and differentiation:
```python
-# Access the underlying Newton interpolating polynomial
-nwt_poly = interpolant.to_newton()
-# Multiply the polynomial -> obtained another polynomial
-prod_poly = nwt_poly * nwt_poly
-# Differentiate the polynomial once -> obtained another polynomial
-diff_poly = nwt_poly.diff(1)
-# Reference function for the (once) differentiated test function
-diff_fun = lambda xx: np.sin(10 * xx) + xx * 10 * np.cos(10 * xx)
-
-fig, axs = plt.subplots(1, 2, figsize=(10, 5))
-
+# Extract the underlying Newton interpolating polynomial
+nwt_poly = interpolant.to_newton()
+# Multiply the polynomial -> results in another polynomial
+prod_poly = nwt_poly * nwt_poly
+# Differentiate the polynomial once -> results in another polynomial
+diff_poly = nwt_poly.diff(1)
+# Analytical derivative of the function
+diff_func = lambda xx: np.sin(xx) + xx * np.cos(xx)
+
+fig, axs = plt.subplots(1, 2, figsize=(10, 5))
+
+axs[0].plot(xx, func(xx)**2, "k.", label="product function")
axs[0].plot(xx, prod_poly(xx), label="product polynomial")
-axs[0].plot(xx, fun(xx)**2, "k.", label="product test function")
axs[0].legend()
axs[0].set_xlabel("$x$")
axs[0].set_ylabel("$y$")
+
+axs[1].plot(xx, diff_func(xx), "k.", label="differentiated function")
axs[1].plot(xx, diff_poly(xx), label="differentiated polynomial")
-axs[1].plot(xx, diff_fun(xx), "k.", label="differentiated test function")
axs[1].legend()
axs[1].set_xlabel("$x$")
-
-plt.show()
+
+plt.show()
```
@@ -140,7 +157,7 @@ on interpolating polynomials, including multidimensional cases.
For detailed guidance,
please refer to the online documentation ([stable](https://minterpy-project.github.io/minterpy/stable/)
-or [latest](https://minterpy-project.github.io/minterpy/stable/)).
+or [latest](https://minterpy-project.github.io/minterpy/latest/)).
It includes detailed installation instructions, usage examples, API references,
and contributors guide.
@@ -159,22 +176,37 @@ of the documentation.
## Citing Minterpy
-If you use Minterpy in your research or projects,
-please consider citing the archived version
-in [RODARE](https://rodare.hzdr.de/record/3354).
+If you use Minterpy in your research or projects, please cite our paper
+published in the [Journal of Open Source Software](https://doi.org/10.21105/joss.07702):
+
+```bibtex
+@article{Wicaksono2025,
+ author = {Wicaksono, Damar and Acosta, Uwe Hernandez and Veettil, Sachin Krishnan Thekke and Kissinger, Jannik and Hecht, Michael},
+ title = {{Minterpy}: multivariate polynomial interpolation in {Python}},
+ journal = {Journal of Open Source Software},
+ year = {2025},
+ volume = {10},
+ number = {109},
+ pages = {7702},
+ doi = {10.21105/joss.07702},
+ url = {https://doi.org/10.21105/joss.07702}
+}
+```
-The citation for the current public version is:
+For reproducibility, please also cite the specific version of Minterpy
+that you used. The current archived version is available
+on [RODARE](https://rodare.hzdr.de/record/3725):
```bibtex
-@software{Minterpy_0_3_0,
+@software{Minterpy_0_3_1,
author = {Hernandez Acosta, Uwe and Thekke Veettil, Sachin Krishnan and Wicaksono, Damar Canggih and Michelfeit, Jannik and Hecht, Michael},
title = {{Minterpy} - multivariate polynomial interpolation},
- month = dec,
- year = 2024,
+ month = apr,
+ year = 2025,
publisher = {RODARE},
- version = {v0.3.0},
- doi = {10.14278/rodare.3354},
- url = {http://doi.org/10.14278/rodare.3354}
+ version = {v0.3.1},
+ doi = {10.14278/rodare.3725},
+ url = {https://doi.org/10.14278/rodare.3725}
}
```
@@ -183,7 +215,7 @@ The citation for the current public version is:
This work was partly funded by the Center for Advanced Systems Understanding ([CASUS]),
an institute of the Helmholtz-Zentrum Dresden-Rossendorf ([HZDR]),
financed by Germany’s Federal Ministry of Education and Research ([BMBF])
-and by the Saxony Ministry for Science, Culture and Tourism ([SMWK])
+and by the Saxony Ministry for Science, Culture, and Tourism ([SMWK])
with tax funds on the basis of the budget approved
by the Saxony State Parliament.
diff --git a/docs/api/core/ABC/abc-polynomial.rst b/docs/api/core/ABC/abc-polynomial.rst
index e22fbe1d..60b32455 100644
--- a/docs/api/core/ABC/abc-polynomial.rst
+++ b/docs/api/core/ABC/abc-polynomial.rst
@@ -32,4 +32,4 @@ multivariate_polynomial_abstract
.. rubric:: Methods
.. classautosummary:: minterpy.core.ABC.multivariate_polynomial_abstract.MultivariatePolynomialSingleABC
- :methods:
\ No newline at end of file
+ :methods:
diff --git a/docs/api/core/domain.rst b/docs/api/core/domain.rst
new file mode 100644
index 00000000..6a043fe7
--- /dev/null
+++ b/docs/api/core/domain.rst
@@ -0,0 +1,24 @@
+======
+domain
+======
+
+.. currentmodule::minterpy.core.domain
+
+.. automodule:: minterpy.core.domain
+
+.. autoclass:: Domain
+ :members:
+ :private-members:
+ :inherited-members:
+ :special-members:
+ :exclude-members: __init__, __hash__, __weakref__
+
+ .. rubric:: Properties
+
+ .. classautosummary:: minterpy.core.domain.Domain
+ :properties:
+
+ .. rubric:: Methods
+
+ .. classautosummary:: minterpy.core.domain.Domain
+ :methods:
diff --git a/docs/api/core/index.rst b/docs/api/core/index.rst
index 4c3950e6..5aec4ab1 100644
--- a/docs/api/core/index.rst
+++ b/docs/api/core/index.rst
@@ -9,6 +9,7 @@ minterpy.core
:hidden:
Multi-Index Set
+ Domain
Grid
Abstract Base Classes
Multi-Index Tree
diff --git a/docs/api/interpolation.rst b/docs/api/interpolation.rst
index 2e400cd0..6613f6ae 100644
--- a/docs/api/interpolation.rst
+++ b/docs/api/interpolation.rst
@@ -2,5 +2,42 @@
minterpy.interpolation
======================
+.. currentmodule::minterpy.interpolation
+
.. automodule:: minterpy.interpolation
+
+.. autofunction:: interpolate
+
+.. autoclass:: Interpolant
:members:
+ :private-members:
+ :inherited-members:
+ :special-members: __call__
+ :exclude-members: __init__
+
+ .. rubric:: Properties
+
+ .. classautosummary:: minterpy.interpolation.Interpolant
+ :properties:
+
+ .. rubric:: Methods
+
+ .. classautosummary:: minterpy.interpolation.Interpolant
+ :methods:
+
+.. autoclass:: Interpolator
+ :members:
+ :private-members:
+ :inherited-members:
+ :special-members: __call__
+ :exclude-members: __init__
+
+ .. rubric:: Properties
+
+ .. classautosummary:: minterpy.interpolation.Interpolator
+ :properties:
+
+ .. rubric:: Methods
+
+ .. classautosummary:: minterpy.interpolation.Interpolator
+ :methods:
\ No newline at end of file
diff --git a/docs/api/polynomials/newton.rst b/docs/api/polynomials/newton.rst
index 92229bc4..42003717 100644
--- a/docs/api/polynomials/newton.rst
+++ b/docs/api/polynomials/newton.rst
@@ -34,6 +34,4 @@ newton_polynomial
.. autofunction:: diff_newton
-.. autofunction:: partial_diff_newton
-
.. autofunction:: integrate_over_newton
diff --git a/docs/assets/images/xsinx-prod-diff.png b/docs/assets/images/xsinx-prod-diff.png
index 7b31898e..42cce7cb 100644
Binary files a/docs/assets/images/xsinx-prod-diff.png and b/docs/assets/images/xsinx-prod-diff.png differ
diff --git a/docs/assets/images/xsinx.png b/docs/assets/images/xsinx.png
index 37ef4399..8add8990 100644
Binary files a/docs/assets/images/xsinx.png and b/docs/assets/images/xsinx.png differ
diff --git a/docs/fundamentals/domain.rst b/docs/fundamentals/domain.rst
new file mode 100644
index 00000000..1e9eca68
--- /dev/null
+++ b/docs/fundamentals/domain.rst
@@ -0,0 +1,199 @@
+=========================
+User and Internal Domains
+=========================
+
+The main goal of Minterpy is to approximate a function
+
+.. math::
+
+ f: \Omega \subset \mathbb{R}^m \rightarrow \mathbb{R}
+
+defined on some domain :math:`\Omega` (called *user domain*)
+via a polynomial approximant :math:`Q_f`.
+
+Rather than constructing :math:`Q_f` directly on :math:`\Omega`, Minterpy
+builds a polynomial :math:`Q` on the internal domain :math:`[-1, 1]^m`
+such that
+
+.. math::
+
+ Q_f(\boldsymbol{x}) = Q \circ \mathcal{T} (\boldsymbol{x})
+ = Q(\mathcal{T}(\boldsymbol{x})), \quad \boldsymbol{x} \in \Omega,
+
+where :math:`\mathcal{T}: \Omega \rightarrow [-1, 1]^m` is a coordinate
+transformation from the user domain to the internal domain.
+
+The approximation goal is then
+
+.. math::
+
+ f(\boldsymbol{x}) \approx Q_f(\boldsymbol{x})
+ = Q(\mathcal{T}(\boldsymbol{x})), \quad \boldsymbol{x} \in \Omega.
+
+Working internally on :math:`[-1, 1]^m` is not an arbitrary choice.
+Classical polynomial approximation theory,
+including the properties of Chebyshev polynomials and the analysis
+of interpolation error, is naturally developed on this interval.
+From a numerical standpoint, rescaling to :math:`[-1, 1]^m` prevents
+large growth of monomials and maintains values
+in a numerically well-behaved range.
+Minterpy inherits these theoretical results and best practices
+by anchoring the underlying computations to :math:`[-1, 1]^m`.
+
+In Minterpy, :math:`Q` is a multivariate polynomial defined
+on the internal domain :math:`[-1, 1]^m`.
+The polynomial :math:`Q_f = Q \circ \mathcal{T}` is therefore the object
+that approximates :math:`f` defined on :math:`\Omega`. :math:`Q_f` is what users
+evaluate, differentiate, and integrate.
+
+The remainder of this page examines the consequences of this composition,
+which depend on the structure of :math:`\Omega`, for each of these operations.
+
+Rectangular domains and affine separable transformations
+========================================================
+
+We focus on the case where :math:`\Omega` is a *rectangular domain*
+
+.. math::
+
+ \Omega = [a_1, b_1] \times \cdots \times [a_m, b_m],
+
+where :math:`a_i < b_i` for each dimension :math:`i = 1, \ldots, m`.
+
+For such domains, the transformation
+:math:`\mathcal{T}: \Omega \rightarrow [-1, 1]^m`
+can be constructed as an affine map [#affine]_ applied
+*independently per dimension*.
+Specifically, :math:`\mathcal{T}` decomposes into separable components
+
+.. math::
+
+ \mathcal{T}(\boldsymbol{x}) =
+ \left( \mathcal{T}_1(x_1), \ldots, \mathcal{T}_m(x_m) \right)
+
+where each component map :math:`\mathcal{T}_i: [a_i, b_i] \rightarrow [-1, 1]`
+is given by
+
+.. math::
+
+ \mathcal{T}_i(x_i) = -1 + \frac{2}{b_i - a_i} (x_i - a_i),
+ \quad x_i \in [a_i, b_i].
+
+Given a point :math:`\boldsymbol{x} \in \Omega`, evaluating :math:`Q_f`
+thus means first transforming :math:`x`
+to :math:`\mathcal{T}(\boldsymbol{x}) \in [-1, 1]^m`
+and then evaluating :math:`Q` there.
+
+The inverse transformation
+:math:`\mathcal{T}^{-1}_i: [-1, 1] \rightarrow [a_i, b_i]` is given by:
+
+.. math::
+
+ \mathcal{T}^{-1}_i(x_{t, i}) = a_i + \frac{b_i - a_i}{2} (x_{t, i} + 1),
+ \quad x_{t, i} \in [-1, 1].
+
+In practice, the inverse transformation is useful in transforming
+the unisolvent nodes given in :math:`[-1, 1]^m` to the corresponding values
+in the function domain.
+
+Integration
+===========
+
+The definite integral of :math:`Q_f` over :math:`\Omega` can be written as
+
+.. math::
+
+ \int_\Omega Q_f(\boldsymbol{x}) \, d\boldsymbol{x} =
+ \int_\Omega Q(\mathcal{T}(\boldsymbol{x})) \, d\boldsymbol{x}.
+
+Applying the change of variables :math:`\boldsymbol{x}_t = \mathcal{T}(\boldsymbol{x})`
+turns the integral over :math:`\Omega` into an integral over :math:`[-1, 1]^m`,
+
+.. math::
+
+ \int_\Omega Q_f(\boldsymbol{x}) \, d\boldsymbol{x} =
+ \int_{[-1, 1]^m} Q(\boldsymbol{x}_t)
+ \lvert \frac{\partial \boldsymbol{x}}{\partial \boldsymbol{x}_t} \rvert
+ \, d\boldsymbol{x}_t.
+
+where :math:`\lvert \frac{\partial \boldsymbol{x}}{\partial \boldsymbol{x}_T} \rvert`
+is the Jacobian determinant of the inverse transformation :math:`\mathcal{T}^{-1}`.
+
+Due to the separable structure of :math:`\mathcal{T}`,
+the Jacobian matrix is diagonal,
+and its determinant reduces to the product of the diagonal entries,
+
+.. math::
+
+ \lvert \frac{\partial \boldsymbol{x}}{\partial \boldsymbol{x}_t} \rvert =
+ \prod_{i = 1}^m \frac{\partial x_i}{\partial x_{t, i}} =
+ \prod_{i = 1}^m \frac{b_i - a_i}{2}.
+
+The integral of :math:`Q_f` over :math:`\Omega` therefore reduces
+to the integral of :math:`Q` over :math:`[-1, 1]^m` scaled
+by a constant factor,
+
+.. math::
+
+ \int_\Omega Q_f(\boldsymbol{x}) \, d\boldsymbol{x} =
+ \left( \prod_{i = 1}^m \frac{b_i - a_i}{2} \right)
+ \int_{[-1, 1]^m} Q(\boldsymbol{x}_t) \, d\boldsymbol{x}_t.
+
+Differentiation
+===============
+
+Differentiating :math:`Q_f = Q \circ \mathcal{T}` with respect to :math:`x_i`
+by the chain rule gives
+
+.. math::
+
+ \frac{\partial Q_f}{\partial x_i} =
+ \frac{\partial Q}{\partial x_{t,i}} \frac{\partial x_{t,i}}{\partial x_i}.
+
+Since :math:`\mathcal{T}_i` is affine, its derivative with respect to
+:math:`x_i` is a constant:
+
+.. math::
+
+ \frac{\partial \mathcal{T}_i}{\partial x_i} = \frac{2}{b_i - a_i},
+
+so that the derivative of :math:`Q_f` in the user domain is the derivative of
+:math:`Q` in :math:`[-1, 1]^m` scaled by this constant factor.
+
+For mixed partial derivatives of order
+:math:`\boldsymbol{k} = \left(k_1, \ldots, k_m \right)`
+with :math:`k_i \geq 0`, the separable structure of :math:`\mathcal{T}` means
+that the chain rule factors apply independently per dimension, giving
+
+.. math::
+
+ \frac{\partial^{k_1 + \cdots + k_m} Q_f}{\partial x_1^{k_1} \cdots
+ \partial x_m^{k_m}} =
+ \frac{\partial^{k_1 + \cdots + k_m} Q}{\partial x_{t,1}^{k_1} \cdots
+ \partial x_{t,m}^{k_m}} \cdot \prod_{i=1}^{m}
+ \left(\frac{2}{b_i - a_i}\right)^{k_i}.
+
+Summary
+=======
+
+The object that approximates $f$ on the rectangular user domain :math:`\Omega`
+is :math:`Q_f = Q \circ \mathcal{T}`. It is what the user evaluates,
+integrates, and differentiates in their own coordinates.
+In Minterpy, :math:`Q` is specifically a multivariate polynomial defined
+on the internal reference domain :math:`[-1, 1]^m`, and the coefficients stored
+internally belong to :math:`Q`. All user-facing operations, however,
+act on :math:`Q_f`. This means:
+
+- evaluation on :math:`\Omega` transparently applies :math:`\mathcal{T}`
+ before passing the points to :math:`Q`,
+- integration takes into account the Jacobian factor arising
+ from the change of variables, and
+- differentiation takes into account the chain rule scaling factors.
+
+In each case, the separable affine structure of :math:`\mathcal{T}`
+keeps the computation of these factors relatively simple:
+closed-form and dimension-wise separable.
+
+.. rubric:: Footnotes
+
+.. [#affine] A map of the form :math:`\boldsymbol{x} \mapsto \boldsymbol{A} \boldsymbol{x} + \boldsymbol{b}`, a linear map with translation.
diff --git a/docs/fundamentals/index.rst b/docs/fundamentals/index.rst
index 5ffc6dd0..53dac143 100644
--- a/docs/fundamentals/index.rst
+++ b/docs/fundamentals/index.rst
@@ -63,6 +63,20 @@ behind Minterpy.
The answer depends on the basis in which the polynomial is represented.
+ .. grid-item-card:: Internal and User Domains
+ :link: domain
+ :link-type: doc
+ :columns: 12 8 8 6
+ :class-card: sd-border-0, sd-card-hover-1
+
+ *Working with custom domains*
+ ^^^
+ Minterpy polynomials are defined on an internal reference domain,
+ but functions of interest often live on custom rectangular domains.
+
+ This page covers the coordinate transformations that bridge the two domains and their implications for evaluation, differentiation,
+ and integration.
+
.. grid-item-card:: Transformation between Bases
:link: transformation
:link-type: doc
@@ -124,6 +138,7 @@ behind Minterpy.
mD Polynomial Bases
interpolation-at-unisolvent-nodes
Evaluation of mD Polynomials
+ Internal and User Domains
transformation
polynomial-regression
mD Divided Difference Scheme
diff --git a/docs/getting-started/1d-polynomial-interpolation.ipynb b/docs/getting-started/1d-polynomial-interpolation.ipynb
index 666334b1..0517d573 100644
--- a/docs/getting-started/1d-polynomial-interpolation.ipynb
+++ b/docs/getting-started/1d-polynomial-interpolation.ipynb
@@ -48,7 +48,7 @@
"source": [
"## Import Minterpy\n",
"\n",
- "After installing {doc}`installing Minterpy `, you can import it as follows:"
+ "After {doc}`installing Minterpy `, you can import it as follows:"
]
},
{
@@ -129,14 +129,20 @@
"source": [
"## Motivating function\n",
"\n",
- "In Minterpy, polynomials are typically used to approximate functions of interest by interpolating them. This is actually one of the main goals of Minterpy: to approximate a (multidimensional) function using a polynomial.\n",
+ "Polynomials in Minterpy are typically created by interpolating a function of interest; function approximation by polynomial interpolation is one of the package's main use cases.\n",
"\n",
- "For now, however, consider the following one-dimensional function:\n",
+ "Consider the following one-dimensional function, known as the Runge function:\n",
"\n",
"$$\n",
- "f(x) = \\frac{1}{1 + 25 x^2}, x \\in [-1, 1].\n",
- "$$\n",
- "\n",
+ "f(x) = \\frac{1}{1 + 25 \\, x^2}, x \\in [-1, 1].\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f002da50-d4d8-40be-ad29-9ae31ef7aa82",
+ "metadata": {},
+ "source": [
"Define this function in Python:"
]
},
@@ -186,9 +192,10 @@
"outputs": [],
"source": [
"xx = np.linspace(-1, 1, 300)\n",
- "plt.plot(xx, fun(xx))\n",
+ "plt.plot(xx, fun(xx), label=r\"$f$\")\n",
"plt.xlabel(\"$x$\", fontsize=14)\n",
"plt.ylabel(\"$y$\", fontsize=14)\n",
+ "plt.legend()\n",
"plt.tick_params(axis='both', which='major', labelsize=12)"
]
},
@@ -219,16 +226,16 @@
"source": [
"## Create an interpolating polynomial\n",
"\n",
- "To create an interpolating polynomial from scratch in Minterpy, follow these four main steps:\n",
+ "To create an interpolating polynomial in Minterpy, follow these four main steps:\n",
"\n",
"1. **Define the multi-index set**: Specify the multi-index set that determines the structure of the polynomial.\n",
"2. **Construct an interpolation grid**: Create an interpolation grid, which consists of the points at which the function of interest will be evaluated.\n",
- "3. **Evaluate the function of interest at the grid points**: Compute the function values at the grid points (also known as the unisolvent nodes)\n",
+ "3. **Evaluate the function at the grid points**: Compute the function values at the grid points, also known as the _unisolvent nodes_\n",
"4. **Create a polynomial in the Lagrange basis**: Construct the interpolating polynomial in the Lagrange basis based on the evaluated points.\n",
"\n",
"To perform additional operations with the interpolating polynomial, such as evaluating it at a set of query points, follow this extra step:\n",
"\n",
- "5. **Transform the polynomial to another basis**: Change the basis of the interpolating polynomial from the Lagrange basis to another basis, preferably the Newton basis, for convenient evaluation and manipulation.\n",
+ "5. **Transform the polynomial to another basis**: Change the basis of the interpolating polynomial from the Lagrange basis to another basis, typically the Newton basis, which is well-suited for evaluation and manipulation.\n",
"\n",
"Let’s go through these steps one at a time."
]
@@ -246,14 +253,20 @@
"source": [
"### Define a multi-index set\n",
"\n",
- "In Minterpy, the exponents of a multivariate polynomial are represented using multi-indices within a set known as the multi-index set. This set generalizes the notion of polynomial degree to multiple dimensions. For more details, see the the {ref}`relevant section ` of the documentation.\n",
+ "In Minterpy, the exponents of a multivariate polynomial are represented using multi-indices within a set known as the multi-index set. This set generalizes the notion of polynomial degree to multiple dimensions. For more details, see the {ref}`relevant section ` of the documentation.\n",
"\n",
- "Even though our function is one-dimensional, defining the multi-index set (in this case, of dimension $1$) is still the first steps of constructing a Minterpy polynomial.\n",
+ "Even though our function is one-dimensional, defining the multi-index set (in this case, of dimension $1$) is still the first step of constructing a Minterpy polynomial.\n",
"\n",
"In Minterpy, multi-index sets are represented by the {py:class}`MultiIndexSet <.core.multi_index.MultiIndexSet>` class.\n",
- "You can create a complete multi-index set using the class method {py:meth}`from_degree() <.MultiIndexSet.from_degree>`, which requires the spatial dimension (first argument) and the polynomial degree (second argument).\n",
- "\n",
- "To construct a one-dimensional multi-index set of polynomial degree, say, $10$, type:"
+ "You can create a complete multi-index set using the class method {py:meth}`from_degree() <.MultiIndexSet.from_degree>`, which requires the spatial dimension (first argument) and the polynomial degree (second argument). The third argument, the $l_p$-degree, does not affect the result for one-dimensional polynomials."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "54e2ffce-f2ed-4ab3-be5a-39a02d97ad2d",
+ "metadata": {},
+ "source": [
+ "To construct a one-dimensional multi-index set of polynomial degree $10$, type:"
]
},
{
@@ -315,7 +328,7 @@
"source": [
"### Construct an interpolation grid\n",
"\n",
- "An interpolation grid is where an interpolating polynomial lives on. The bases of interpolating polynomials, such as the Lagrange or the Newton basis, are built with respect to these grids.\n",
+ "An interpolation grid defines the set of points on which an interpolating polynomial is built. The bases of interpolating polynomials, such as the Lagrange or Newton bases, are built with respect to these grids.\n",
"\n",
"In Minterpy, interpolation grids are represented by the {py:class}`Grid <.core.grid.Grid>` class.\n",
"The default constructor allows you to create an interpolation grid for a specified multi-index set (the first argument).\n",
@@ -350,7 +363,6 @@
"tags": []
},
"source": [
- "An instance of {py:class}`Grid <.core.grid.Grid>` is always defined with respect to a multi-index set.\n",
"In simple terms, the multi-index set of an interpolation grid determines the structure of multivariate polynomials that the grid can support.\n",
"\n",
"A key property of a {py:class}`Grid <.core.grid.Grid>` instance is {py:meth}`unisolvent_nodes <.core.grid.Grid.unisolvent_nodes>` (i.e., interpolating nodes or grid points).\n",
@@ -386,7 +398,7 @@
"tags": []
},
"source": [
- "which distributed like the following:"
+ "which are distributed as follows:"
]
},
{
@@ -425,7 +437,15 @@
"tags": []
},
"source": [
- "Notice that there are more points in the boundaries of the domain."
+ "Notice that there are more points near the boundaries of the domain.\n",
+ "\n",
+ "```{note}\n",
+ "No domain is specified when constructing the grid above. In that case, Minterpy defaults to the internal reference domain $[-1, 1]^m$. o learn how to define a custom domain, see {doc}`/getting-started/md-polynomial-interpolation`.\n",
+ "\n",
+ "---\n",
+ "\n",
+ "Unisolvent nodes are always expressed in the canonical domain $[-1, 1]^m$, regardless of the user-defined domain.\n",
+ "```"
]
},
{
@@ -441,17 +461,23 @@
"source": [
"### Evaluate the function at the unisolvent nodes\n",
"\n",
- "An interpolating polynomial $Q$ with a domain of $[-1, 1]$ of a function $f$ is a polynomial that satisfies the following condition:\n",
+ "An interpolating polynomial $Q$ of a function $f$ on $[-1, 1]$ is a polynomial that satisfies the following condition:\n",
"\n",
"$$\n",
"Q(p_{\\alpha}) = f(p_{\\alpha}), \\forall p_{\\alpha} \\in P_A,\n",
"$$\n",
"\n",
- "where $P_A = \\{ p_{\\alpha} \\}$ is the set of unisolvent nodes.\n",
- "\n",
- "The function values at the unisolvent nodes are therefore crucial to construct an interpolating polynomial.\n",
+ "where $P_A = \\{ p_{\\alpha}: \\alpha \\in A \\}$ is the set of unisolvent nodes.\n",
"\n",
- "In Minterpy, calling an instance of {py:meth}`Grid <.core.grid.Grid.__call__>` with a given function evaluates the function at the unisolvent nodes:"
+ "The function values at the unisolvent nodes are therefore crucial for constructing an interpolating polynomial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "925206f2-d818-40ec-a8f4-07aa68c5dc57",
+ "metadata": {},
+ "source": [
+ "In Minterpy, calling an instance of {py:meth}`Grid <.core.grid.Grid.__call__>` with a given function evaluates the function at the unisolvent nodes and returns the values as an array"
]
},
{
@@ -471,6 +497,14 @@
"coeffs"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "dc44eff5-41a9-42e3-a821-85cdcc0e5347",
+ "metadata": {},
+ "source": [
+ "these are the coefficients of the interpolating polynomial in the Lagrange basis."
+ ]
+ },
{
"cell_type": "markdown",
"id": "b3092be0-2c28-4545-8aa0-de6cd40a5225",
@@ -484,23 +518,30 @@
"source": [
"### Create a Lagrange polynomial\n",
"\n",
+ "\n",
+ "\"where δ⋅,⋅\\delta{\\cdot, \\cdot}\n",
+ "δ⋅,⋅ denotes the Kronecker delta\" — missing the comma in the notation, should be δ⋅,⋅\\delta_{\\cdot, \\cdot}\n",
+ "δ⋅,⋅\n",
+ "\"You can create an instance of LagrangePolynomial class with various constructors. Relevant to the present tutorial, from_grid() constructor accepts...\" — \"Relevant to the present tutorial\" is a slightly awkward opener. Consider: \"For this tutorial, the from_grid() constructor is the most relevant — it accepts...\"\n",
+ "The three consecutive sentences that each contain the full cross-reference to LagrangePolynomial make the paragraph feel heavy. The second and third mentions could just use the class name without the full cross-reference since it's already been linked once\n",
+ "\n",
"The function values at the unisolvent nodes are equal to the coefficients of a polynomial in the Lagrange basis $c_{\\mathrm{lag}, \\alpha}$:\n",
"\n",
"$$\n",
"Q(x) = \\sum_{\\alpha \\in A} c_{\\mathrm{lag}, \\alpha} \\Psi_{\\mathrm{lag}, \\alpha}(x),\n",
"$$\n",
"\n",
- "where $A$ is the multi-index set and $\\Psi_{\\alpha}$'s are Lagrange basis polynomials, each of which satisfies:\n",
+ "where $A$ is the multi-index set and $\\Psi_{\\mathrm{lag}, \\alpha}$'s are Lagrange basis polynomials, where each of the Lagrange basis polynomial satisfies:\n",
"\n",
"$$\n",
"\\Psi_{\\mathrm{lag}, \\alpha}(p_{\\beta}) = \\delta_{\\alpha, \\beta}, p_\\beta \\in P_A,\n",
"$$\n",
"\n",
- "where $\\delta{\\cdot, \\cdot}$ denotes the Kronecker delta.\n",
+ "where $\\delta_{\\cdot, \\cdot}$ denotes the Kronecker delta.\n",
"\n",
"Polynomials in the Lagrange basis are represented by the {py:class}`LagrangePolynomial <.polynomials.lagrange_polynomial.LagrangePolynomial>` class in Minterpy.\n",
- "You can create an instance of {py:class}`LagrangePolynomial <.polynomials.lagrange_polynomial.LagrangePolynomial>` class with various constructors.\n",
- "Relevant to the present tutorial, {py:meth}`from_grid() <.polynomials.lagrange_polynomial.LagrangePolynomial.from_grid>` constructor accepts an instance of {py:class}`Grid <.core.grid.Grid>` and the corresponding Lagrange coefficients (i.e., function values at the unisolvent nodes) to create an instance of {py:class}`LagrangePolynomial <.polynomials.lagrange_polynomial.LagrangePolynomial>`."
+ "You can create an instance of `LagrangePolynomial` class with various constructors.\n",
+ "For this tutorial, the {py:meth}`from_grid() <.polynomials.lagrange_polynomial.LagrangePolynomial.from_grid>` constructor is the most relevant. Specifically, it accepts an instance of {py:class}`Grid <.core.grid.Grid>` and the corresponding Lagrange coefficients (i.e., function values at the unisolvent nodes) to create an instance of the class."
]
},
{
@@ -549,22 +590,22 @@
"Unfortunately, polynomials in the Lagrange basis have limited functionality; for instance, you can't directly evaluate them at a set of query points.\n",
"\n",
"In Minterpy, the Lagrange basis primarily serves as a convenient starting point for constructing an interpolating polynomial.\n",
- "The coefficients in this basis are straightforwardly defined as the function values at the unisolvent nodes (grid points).\n",
+ "The coefficients in this basis are simply the function values at the unisolvent nodes (grid points).\n",
"\n",
- "To perform more advanced operations with the polynomial in Minterpy, you need to transform it into the Newton basis.\n",
+ "To perform more advanced operations in Minterpy, the polynomial must be transformed to the Newton basis.\n",
"In the Newton basis, the polynomial takes the form:\n",
"\n",
"$$\n",
"Q(x) = \\sum_{\\alpha \\in A} c_{\\mathrm{nwt}, \\alpha} \\, \\Psi_{\\mathrm{nwt}, \\alpha}(x),\n",
"$$\n",
"\n",
- "where $c_{\\mathrm{nwt}, \\alpha}$'s are the coefficients of the polynomial in the Newton basis and $\\Psi_{\\mathrm{nwt}, \\alpha}$ is the the Newton basis polynomial associated with multi-index element $\\alpha$:\n",
+ "where $c_{\\mathrm{nwt}, \\alpha}$'s are the coefficients of the polynomial in the Newton basis and $\\Psi_{\\mathrm{nwt}, \\alpha}$ is the (one-dimensional) Newton basis polynomial associated with multi-index element $\\alpha$:\n",
"\n",
"$$\n",
"\\Psi_{\\mathrm{nwt}, \\alpha}(x) = \\prod_{i = 0}^{\\alpha - 1} (x - p_i),\n",
"$$\n",
"\n",
- "where $p_i$'s are the interpolation points (unisolvent nodes)."
+ "where $p_i$ denotes the $i$-th unisolvent nodes."
]
},
{
@@ -625,7 +666,7 @@
},
"source": [
"```{note}\n",
- "Minterpy offers other polynomial bases as well, but for most practical purposes, we recommend to use the Newton basis.\n",
+ "Minterpy offers other polynomial bases as well, but for most practical purposes, we recommend using the Newton basis.\n",
"```"
]
},
@@ -642,7 +683,7 @@
"source": [
"### Evaluate the polynomial at a set of query points\n",
"\n",
- "Once a polynomial in the Newton basis is obtained, you can evaluate it at a set of query points; this one of the most routine tasks involving polynomial approximation.\n",
+ "Once a polynomial in the Newton basis is obtained, you can evaluate it at a set of query points; this is one of the most routine tasks involving polynomial approximation.\n",
"\n",
"First, create a set of equally spaced test points in $[-1, 1]$:"
]
@@ -704,7 +745,7 @@
"tags": []
},
"source": [
- "Calling an instance of {py:class}`NewtonPolynomial <.polynomials.newton_polynomial.NewtonPolynomial>` at these query points evaluate the polynomial at the query points:"
+ "Calling an instance of {py:class}`NewtonPolynomial <.polynomials.newton_polynomial.NewtonPolynomial>` at these query points evaluates the polynomial and returns the results as an array:"
]
},
{
@@ -735,7 +776,7 @@
"tags": []
},
"source": [
- "As expected, evaluating the polynomial on $1'000$ query points returns an array $1'000$ points:"
+ "As expected, evaluating the polynomial on $1'000$ query points returns an array of $1'000$ points:"
]
},
{
@@ -766,7 +807,7 @@
},
"source": [
"```{note}\n",
- "For evaluation, an instance of polynomial expects as its input an array; as the polynomial is one dimensional, the array should either be one-dimensional or two-dimensional with a single column (the column of a two-dimensional array indicates the values along each spatial dimension).\n",
+ "For evaluation, a one dimensional polynomial expects a one-dimensional array or a two-dimensional array with a single column as input. The column of a two-dimensional input array indicates the values along each spatial dimension.\n",
"```"
]
},
@@ -803,7 +844,7 @@
"plt.plot(xx_test, yy_poly, label=\"interpolant ($Q$)\")\n",
"plt.xlabel(\"$x$\", fontsize=14)\n",
"plt.ylabel(\"$y$\", fontsize=14)\n",
- "plt.scatter(grd.unisolvent_nodes, lag_poly.coeffs, label=\"unisolvent nodes\")\n",
+ "plt.scatter(grd.unisolvent_nodes[:, 0], lag_poly.coeffs, label=\"unisolvent nodes\")\n",
"plt.tick_params(axis='both', which='major', labelsize=12)\n",
"plt.legend();"
]
@@ -819,7 +860,7 @@
"tags": []
},
"source": [
- "The plot also shows the location of the unisolvent nodes and their corresponding function values; the interpolant is indeed interpolatory."
+ "The plot also shows the location of the unisolvent nodes and their corresponding function values confirming that the polynomial passes through the unisolvent nodes."
]
},
{
@@ -841,9 +882,15 @@
"The norm is defined as:\n",
"\n",
"$$\n",
- "\\lVert f - Q \\rVert_\\infty = \\sup_{-1 \\leq x \\leq 1} \\lvert f(x) - Q(x) \\rvert\n",
- "$$\n",
- "\n",
+ "\\lVert f - Q \\rVert_\\infty = \\sup_{-1 \\leq x \\leq 1} \\lvert f(x) - Q(x) \\rvert.\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c4d37bf3-0558-4a89-9f21-49582d2dc28d",
+ "metadata": {},
+ "source": [
"The infinity norm of $Q$ can be approximated using the $1'000$ testing points created above:"
]
},
@@ -874,7 +921,7 @@
"tags": []
},
"source": [
- "which is hardly a numerical convergence."
+ "indicating that the interpolating polynomial of degree $10$ is insufficient to accurately approximate the Runge function."
]
},
{
@@ -890,16 +937,20 @@
"source": [
"## The `interpolate()` function\n",
"\n",
- "As an alternative to constructing an interpolating polynomial from scratch using the steps outlined above, you can use the {py:func}`.interpolate` function.\n",
- "The function allows you to specify:\n",
+ "As an alternative to constructing an interpolating polynomial from scratch using the steps outlined above, you can use the {py:func}`.interpolate` function. The function allows you to specify:\n",
"\n",
"- the function to interpolate\n",
"- the number of dimensions\n",
"- the degree of polynomial interpolant\n",
"\n",
- "All in a single function call.\n",
- "The function returns a callable object that you can use to evaluate the (underlying) interpolating polynomial at a set of query points.\n",
- "\n",
+ "All in a single function call. The function returns a callable object that you can use to evaluate the (underlying) interpolating polynomial at a set of query points."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2624a5b9-7a7c-4242-ba28-0fa70bea36c9",
+ "metadata": {},
+ "source": [
"To create an interpolant using the {py:func}`.interpolate` function with the same structure as before, use the following command:"
]
},
@@ -960,7 +1011,7 @@
"tags": []
},
"source": [
- "With this shortcut to create an interpolant where evaluation is all that is needed, let's investigate the convergence of interpolating polynomials with increasing polynomial degrees.,"
+ "With this shortcut in hand, let's investigate the convergence behavior of interpolating polynomials with increasing polynomial degrees."
]
},
{
@@ -994,7 +1045,7 @@
"tags": []
},
"source": [
- "The interpolants of increasing degrees looks like:"
+ "The interpolants of increasing degrees are shown below:"
]
},
{
@@ -1082,7 +1133,7 @@
"tags": []
},
"source": [
- "The absolute error of the degree 64 polynomials in the domain is shown below:"
+ "The absolute error of the degree 256 polynomials in the domain is shown below:"
]
},
{
@@ -1101,7 +1152,7 @@
"outputs": [],
"source": [
"plt.plot(xx_test, np.abs(yy_test - yy_poly[:, -1]))\n",
- "plt.ylim(1e-18,1)\n",
+ "plt.ylim(1e-18, 1)\n",
"plt.ylabel(r\"$| f - Q |$\", fontsize=14)\n",
"plt.xlabel(\"$x$\", fontsize=14)\n",
"plt.yscale(\"log\");"
@@ -1118,7 +1169,7 @@
"tags": []
},
"source": [
- "The seemingly random behavior of the (very small) absolute error indicates that machine precision has been reached. Compare it with the absolute error of degree 4 polynomials:"
+ "The seemingly random behavior of the (very small) absolute error indicates that machine precision has been reached. For comparison, the absolute error of the degree $4$ polynomial is shown below:"
]
},
{
@@ -1151,13 +1202,11 @@
"tags": []
},
"source": [
- "## From Interpolant to polynomial\n",
+ "## From interpolant to polynomial\n",
"\n",
- "The callable produced by {py:func}`interpolate() <.interpolation.interpolate>`\n",
- "is an instance of {py:class}`Interpolant <.interpolation.Interpolant>`;\n",
- "it is not a Minterpy polynomial!\n",
+ "The callable produced by {py:func}`interpolate() <.interpolation.interpolate>` is an instance of {py:class}`Interpolant <.interpolation.Interpolant>`; it is not a Minterpy polynomial per se.\n",
"\n",
- "There are, however, convenient methods to represent the interpolant in one of the Minterpy polynomials.\n",
+ "There are, however, convenient methods to represent the interpolant as one of the Minterpy polynomial types.\n",
"These methods are summarized in the table below.\n",
"\n",
"| Method | Return the interpolant as a polynomial in the... |\n",
@@ -1167,10 +1216,15 @@
"| {py:meth}`to_canonical() <.interpolation.Interpolant.to_canonical>` | {py:class}`CanonicalPolynomial <.polynomials.canonical_polynomial.CanonicalPolynomial>` |\n",
"| {py:meth}`to_chebyshev() <.interpolation.Interpolant.to_chebyshev>` | {py:class}`ChebyshevPolynomial <.polynomials.chebyshev_polynomial.ChebyshevPolynomial>` |\n",
"\n",
- "These bases will be the topic of {doc}`/getting-started/polynomial-bases-and-transformations`\n",
- "in-depth tutorial.\n",
- "\n",
- "For now to obtain the interpolating polynomial in the Newton basis, call:"
+ "These polynomial bases are covered in more detail in {doc}`/getting-started/polynomial-bases-and-transformations` in-depth tutorial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "00111ceb-dd54-43c9-a655-139d05540bc0",
+ "metadata": {},
+ "source": [
+ "For now, to obtain the lastly created interpolating polynomial in the Newton basis, call:"
]
},
{
@@ -1204,7 +1258,7 @@
"2. **Constructing an interpolation grid**: Creating the grid points (unisolvent nodes) for interpolation.\n",
"3. **Evaluating the given function on the grid points**: Computing function values at the nodes.\n",
"4. **Constructing a polynomial in the Lagrange basis**: Creating an interpolating polynomial in the Lagrange basis with the function values as the coefficients.\n",
- "5. **Transform the polynomial into the Newton basis**: Changing the basis from the Lagrange basis to the Newton basis for more flexibility.\n",
+ "5. **Transforming the polynomial into the Newton basis**: Changing the basis from the Lagrange basis to the Newton basis for more evaluation and further manipulation.\n",
"\n",
"You've also explored the {py:func}`.interpolate` function, which simplifies the process of creating an interpolant.\n",
"\n",
@@ -1212,7 +1266,7 @@
"\n",
"---\n",
"\n",
- "Minterpy, as its name implies, supports the construction of multivariate polynomials as well. The process for constructing these polynomials follows similar steps to those described here.\n",
+ "As its name implies, Minterpy is designed for multivariate polynomial interpolation. The process for constructing these polynomials follows similar steps to those described here.\n",
"\n",
"In the next tutorial, you'll learn how to create multivariate polynomials to interpolate multidimensional functions using Minterpy."
]
@@ -1234,7 +1288,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.19"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/getting-started/arithmetic-operations-with-polynomials.ipynb b/docs/getting-started/arithmetic-operations-with-polynomials.ipynb
index 4a23f228..8556521e 100644
--- a/docs/getting-started/arithmetic-operations-with-polynomials.ipynb
+++ b/docs/getting-started/arithmetic-operations-with-polynomials.ipynb
@@ -25,27 +25,30 @@
"tags": []
},
"source": [
- "Once you have constructed a polynomial in Minterpy, you can perform various operations on it, depending on the basis in which the polynomial is represented.\n",
- "For instance, as demonstrated in the previous tutorials, you can evaluate a Minterpy polynomial (in the Newton basis) at a set of query points.\n",
+ "Once you've constructed a polynomial in Minterpy, you can do more than just evaluate it. This tutorial shows how to perform arithmetic operations with Minterpy polynomials. Specifically, you'll learn that Minterpy polynomials are **closed** under the following operations:\n",
"\n",
- "This tutorial will show you how to extend beyond evaluation by performing arithmetic operations with Minterpy polynomials.\n",
- "Specifically, you'll learn that Minterpy polynomials can be:\n",
- "\n",
- "- Added or subtracted from other polynomials\n",
- "- Multiplied by other polynomials or real scalar numbers\n",
- "- Raised to a power (as long as it's a non-negative integer)\n",
- "\n",
- "These operations result in another Minterpy polynomial in the same basis.\n",
- "In other words, Minterpy polynomials are **closed** under the following arithmetic operations:\n",
- "\n",
- "- Multiplying a polynomial by another polynomial\n",
- "- Multiplying a polynomial by a scalar (a real number)\n",
"- Adding or subtracting one polynomial to/from another\n",
- "- Adding or subtracting a scalar (a real number) to/from a polynomial\n",
+ "- Adding or subtracting a real scalar number to/from a polynomial\n",
+ "- Multiplying a polynomial by another polynomial\n",
+ "- Multiplying a polynomial by a real scalar number\n",
"- Raising a polynomial to a non-negative integer power\n",
"\n",
- "To keep things simple, this tutorial uses one-dimensional polynomials as examples. However, the principles and behaviors we'll cover apply to Minterpy polynomials of any dimensions.\n",
+ "In other words, each of these operations produces another Minterpy polynomial in the same basis.\n",
"\n",
+ "To keep things simple, the examples in this tutorial use one-dimensional polynomials, but the principles apply to Minterpy polynomials of any dimension."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "06b4ba74-09bf-4f52-a602-8ae802024106",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"Before you start, make sure to import the necessary packages to follow along with this guide."
]
},
@@ -281,7 +284,7 @@
"tags": []
},
"source": [
- "Finally, check the infinity norm to decide if the approximation is accurate enough."
+ "Finally, check the approximation accuracy via the infinity norm:"
]
},
{
@@ -331,7 +334,7 @@
},
"outputs": [],
"source": [
- "xx_plot = np.linspace(-1, 1, 1000)\n",
+ "xx_plot = np.linspace(-1, 1, 500)\n",
"\n",
"fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))\n",
"\n",
@@ -367,7 +370,7 @@
"tags": []
},
"source": [
- "The plots show that there no notable differences between the two true functions and their corresponding interpolating polynomials\n",
+ "The plots show that there are no notable differences between the two true functions and their corresponding interpolating polynomials\n",
"\n",
"---\n",
"\n",
@@ -387,8 +390,20 @@
"source": [
"## Addition and subtraction\n",
"\n",
- "Minterpy polynomials may be added or subtracted by a real scalar number; this operation returns another polynomial.\n",
- "\n",
+ "Minterpy polynomials support addition and subtraction with real scalar numbers; the result is another polynomial in the same basis."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5c9f3e6e-ec36-4648-8c5f-f9690906aefa",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"For instance:"
]
},
@@ -469,7 +484,7 @@
},
"outputs": [],
"source": [
- "xx_plot = np.linspace(-1, 1, 1000)\n",
+ "# Use the same points for plotting\n",
"\n",
"fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))\n",
"\n",
@@ -584,7 +599,7 @@
},
"outputs": [],
"source": [
- "xx_plot = np.linspace(-1, 1, 1000)\n",
+ "# Use the same points for plotting\n",
"\n",
"fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))\n",
"\n",
@@ -606,6 +621,22 @@
"fig.tight_layout();"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "de9c4aae-4e05-4a04-9c78-85fc7574fd37",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ "```{note}\n",
+ "Addition and subtraction between two polynomials requires both to be defined on the same domain. If the domains differ, Minterpy raises a `DomainMismatchError`. When no domain is specified, polynomials are defined on the default internal reference domain $[-1, 1]^m$. This, therefore, is not a concern in the examples above.\n",
+ "```"
+ ]
+ },
{
"cell_type": "markdown",
"id": "51453789-8664-4a5b-8939-f1d0250e67a5",
@@ -619,8 +650,20 @@
"source": [
"## Multiplication\n",
"\n",
- "Minterpy polynomials may also be multiplied by a real scalar number; the operation returns another polynomial.\n",
- "\n",
+ "Minterpy polynomials may also be multiplied by a real scalar number; the operation returns another polynomial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0c039279-090d-4873-a596-24880c50506f",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"Consider $5 \\times Q_2$:"
]
},
@@ -652,7 +695,7 @@
"tags": []
},
"source": [
- "Scalar multiplication uniformly and vertically stretches the polynomial across its domain as shown in the plot below."
+ "Scalar multiplication uniformly scales the polynomial across its domain as shown in the plot below."
]
},
{
@@ -691,8 +734,20 @@
"tags": []
},
"source": [
- "Furthermore, a multiplication between Minterpy polynomials is also a valid operation that returns a polynomial.\n",
- "\n",
+ "Furthermore, a multiplication between Minterpy polynomials is a valid operation that returns a polynomial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5d131457-b84c-484c-a553-dc0356693d17",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"For instance, $Q_{\\mathrm{prod}} = Q_1 \\times Q_2$:"
]
},
@@ -742,8 +797,6 @@
},
"outputs": [],
"source": [
- "xx_plot = np.linspace(-1, 1, 1000)\n",
- "\n",
"fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(12, 4))\n",
"\n",
"axs[0].plot(xx_plot, poly_1(xx_plot), label=\"$Q_1$\")\n",
@@ -762,7 +815,6 @@
"axs[2].plot(xx_plot, poly_prod(xx_plot), label=\"$Q_1 \\\\times Q_2$\")\n",
"axs[2].set_xlabel(\"$x$\", fontsize=14)\n",
"axs[2].tick_params(axis='both', which='major', labelsize=12)\n",
- "axs[2].legend(fontsize=14);\n",
"axs[2].legend(fontsize=14, loc=\"upper right\")\n",
"axs[2].set_ylim([-2.0, 2.25])\n",
"\n",
@@ -782,7 +834,7 @@
]
},
"source": [
- "As expected the product polynomial is an exponentially decaying sine function."
+ "The product polynomial is an exponentially decaying sine function. This is a consequence of multiplying a sine by a decaying exponential."
]
},
{
@@ -798,8 +850,20 @@
"source": [
"## Division\n",
"\n",
- "Minterpy polynomials may be divided by a real scalar number; this operation returns another polynomial.\n",
- "\n",
+ "Minterpy polynomials may be divided by a real scalar number; this operation returns another polynomial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "07f9677c-35cd-4d05-bf77-fa542b3da4c2",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"For instance, $Q_1 / 4.0$:"
]
},
@@ -831,7 +895,7 @@
"tags": []
},
"source": [
- "Division by a real scalar number uniformly and vertically contracts the polynomial across its domain as shown in the plot below."
+ "Division by a real scalar number uniformly scales the polynomial across its domain as shown in the plot below."
]
},
{
@@ -870,13 +934,13 @@
"tags": []
},
"source": [
- "Minterpy, however, does **not support** polynomial-polynomial division (rational function). Minterpy cannot evaluate the resulting function of the expression:\n",
+ "Minterpy, however, does **not support** polynomial-polynomial division. Specifically the expression\n",
"\n",
"$$\n",
"Q_3 = \\frac{Q_1}{Q_2}\n",
"$$\n",
"\n",
- "and return the resulting rational function."
+ "is a rational function which, in general, cannot be represented as a polynomial."
]
},
{
@@ -891,7 +955,7 @@
},
"source": [
"```{note}\n",
- "That being said, you can still evaluate the evaluation of the expression above at a given set of query points (as long as $Q_2(x) \\neq 0$).\n",
+ "That being said, you can still evaluate the expression above at a given set of query points where $Q_2(x) \\neq 0$.\n",
"```"
]
},
@@ -910,8 +974,20 @@
"source": [
"## Exponentiation\n",
"\n",
- "Finally, Minterpy polynomials may also be exponentiated by a **non-negative integer**. As all the other arithmetic operations above, polynomial exponentiation returns another polynomial.\n",
- "\n",
+ "Finally, Minterpy polynomials may also be exponentiated by a **non-negative integer**. As with all the other arithmetic operations above, polynomial exponentiation returns another polynomial."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "78373ea5-1d77-4e92-ae2e-f3f769aebc4e",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"For instance, $Q_1^2$:"
]
},
@@ -968,8 +1044,20 @@
"tags": []
},
"source": [
- "Raising a polynomial to a non-negative integer power is equivalent to performing multiple-self multiplications of the polynomial.\n",
- "\n",
+ "Raising a polynomial to a non-negative integer power is equivalent to repeatedly multiplying the polynomial by itself."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11fea1e9-9dc1-4673-9b2d-a677ad8f8232",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
"For instance, $Q_2^2 = Q_2 \\times Q_2$:"
]
},
@@ -1065,8 +1153,6 @@
},
"outputs": [],
"source": [
- "xx_plot = np.linspace(-1, 1, 1000)\n",
- "\n",
"fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))\n",
"\n",
"axs[0].plot(xx_plot, poly_1(xx_plot), label=\"$Q_1$\")\n",
@@ -1099,7 +1185,7 @@
},
"source": [
"```{warning}\n",
- "Note that Minterpy polynomials do not support exponentiation by another polynomial, a negative number, or a non-integer value. If you attempt to perform such an operation, an exception will be raised.\n",
+ "Minterpy polynomials do not support exponentiation by another polynomial, a negative number, or a non-integer value. If you attempt to perform such an operation, an exception will be raised.\n",
"```"
]
},
@@ -1117,7 +1203,7 @@
"## Summary\n",
"\n",
"In this in-depth tutorial, you've learned about the basic arithmetic operations involving Minterpy polynomials.\n",
- "Minterpy polynomials are **closed** under the following arithmetic operations, meaning that the result of the performing these operations is always another Minterpy polynomial:\n",
+ "Minterpy polynomials are **closed** under the following arithmetic operations, meaning that the result of performing these operations is always another Minterpy polynomial:\n",
"\n",
"- Polynomial-scalar addition and subtraction\n",
"- Polynomial-polynomial addition and subtraction\n",
@@ -1126,13 +1212,15 @@
"- Polynomial-scalar division\n",
"- Polynomial exponentiation by a non-negative integer\n",
"\n",
- "Throughout the tutorial, one-dimensional polynomials were used for illustration, but the principles apply similarly to Minterpy polynomials of higher dimensions.\n",
+ "Throughout the tutorial, one-dimensional polynomials were used for illustration, but the same principles apply to Minterpy polynomials of any dimension.\n",
"\n",
- "Please note that Minterpy currently **does not support**:\n",
+ "Minterpy currently **does not support**:\n",
"\n",
"- Polynomial-polynomial division (i.e., forming _rational functions_)\n",
"- Polynomial exponentiation by another polynomial or by a real scalar (including negative integers and non-integer numbers)\n",
"\n",
+ "Additionally, polynomial-polynomial operations require both polynomials to share the same domain; otherwise a `DomainMismatchError` is raised.\n",
+ "\n",
"---\n",
"\n",
"In the next tutorial, you will explore additional features of Minterpy, including basic calculus operations such as differentiation and integration."
@@ -1155,7 +1243,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.19"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/getting-started/calculus-operations-with-polynomials.ipynb b/docs/getting-started/calculus-operations-with-polynomials.ipynb
index 1546b1e2..3a4738a4 100644
--- a/docs/getting-started/calculus-operations-with-polynomials.ipynb
+++ b/docs/getting-started/calculus-operations-with-polynomials.ipynb
@@ -87,7 +87,7 @@
"\\end{align}\n",
"$$\n",
"\n",
- "where $\\boldsymbol{x} = (x_1, x_2) \\in [-1, 1]^2$."
+ "where $\\boldsymbol{x} = (x_1, x_2) \\in [0, 1]^2$."
]
},
{
@@ -102,7 +102,7 @@
},
"source": [
"```{note}\n",
- "This function is known in the literature as the Franke function[^franke]. Here, however, the domain has been redefined to be $[-1, 1]^2$ instead of $[0, 1]^2$.\n",
+ "This function is known in the literature as the Franke function[^franke].\n",
"\n",
"[^franke]: Richard Franke, \"A critical comparison of some methods for interpolation of scattered data,\" Naval Postgraduate School, Monterey, Canada, Technical Report No. NPS53-79-003, 1979. URL: https://core.ac.uk/reader/36727660\n",
"```"
@@ -184,7 +184,7 @@
"from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
"\n",
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 500)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 500)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_2d = fun(xx_2d)\n",
@@ -255,8 +255,10 @@
"source": [
"# Multi-index set of polynomial degree\n",
"mi = mp.MultiIndexSet.from_degree(2, 75, 1.0)\n",
+ "# Custom domain [0, 1]^2\n",
+ "dom = mp.Domain.uniform(2, 0, 1)\n",
"# Interpolation grid\n",
- "grd = mp.Grid(mi)\n",
+ "grd = mp.Grid(mi, domain=dom)\n",
"# Coefficients of the Lagrange polynomial\n",
"coeffs = grd(fun)\n",
"# Lagrange polynomial given grid and coefficients\n",
@@ -292,7 +294,7 @@
},
"outputs": [],
"source": [
- "xx_test = -1 + 2 * np.random.rand(10000, 2)\n",
+ "xx_test = np.random.rand(10000, 2)\n",
"yy_test = fun(xx_test)\n",
"yy_poly = poly(xx_test)\n",
"print(np.max(np.abs(yy_poly - yy_test)))"
@@ -319,7 +321,7 @@
"I[Q] = \\int_{\\Omega} Q(\\boldsymbol{x}) \\, d\\boldsymbol{x},\n",
"$$\n",
"\n",
- "where $\\Omega$ is the domain of the polynomial which in this case is $[-1, 1]^2$."
+ "where $\\Omega$ is the domain of the polynomial which in this case is $[0, 1]^2$."
]
},
{
@@ -376,7 +378,7 @@
"For instance, to compute:\n",
"\n",
"$$\n",
- "\\int_{-0.5}^{0.5} \\int_{-0.25}^{0.75} Q(\\boldsymbol{x}) \\, dx_2 dx_1,\n",
+ "\\int_{0}^{0.5} \\int_{0.25}^{0.75} Q(\\boldsymbol{x}) \\, dx_2 dx_1,\n",
"$$\n",
"\n",
"you would specify the bounds for each dimension as follows:"
@@ -395,7 +397,7 @@
},
"outputs": [],
"source": [
- "poly.integrate_over(bounds=[[-0.5, 0.5], [-0.25, 0.75]])"
+ "poly.integrate_over(bounds=[[0, 0.5], [0.25, 0.75]])"
]
},
{
@@ -554,7 +556,7 @@
"outputs": [],
"source": [
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 500)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 500)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_diff_0_1 = poly_diff_0_1(xx_2d)\n",
@@ -701,7 +703,7 @@
"outputs": [],
"source": [
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 500)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 500)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_diff_0_1_1_1 = poly_diff_0_1_1_1(xx_2d)\n",
@@ -762,7 +764,7 @@
"\n",
"In this in-depth tutorial, you've learned the basic calculus operations involving Minterpy polynomials, specifically:\n",
"\n",
- "- **Definite integration**: Computes the integral over specified bounds, returning a numeric result.\n",
+ "- **Definite integration**: Computes the integral over specified bounds in the domain of the function, returning a numeric result.\n",
"- **Differentiation**: Computes the derivative of the polynomial, resulting in another polynomial (the differentiated one).\n",
"\n",
"These operations were demonstrated using a two-dimensional polynomial, but they apply similarly to polynomials of higher dimensions.\n",
@@ -777,7 +779,7 @@
"\n",
"In the examples we've explored so far, we've started with a polynomial in the Lagrange basis and then converted it to a polynomial in the Newton basis. However, Minterpy offers more flexibility than that.\n",
"\n",
- "Polynomials can be represented in a variety of bases, extending beyond just Lagrange and Newton. In the next tutorial, we'll delve into different bases available in Minterpy and discuss how to transform polynomials between the."
+ "Polynomials can be represented in a variety of bases, extending beyond just Lagrange and Newton. In the next tutorial, we'll delve into different bases available in Minterpy and discuss how to transform polynomials between them."
]
}
],
@@ -797,7 +799,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.19"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/getting-started/function-approximations.ipynb b/docs/getting-started/function-approximations.ipynb
index aee8dd9c..68442825 100644
--- a/docs/getting-started/function-approximations.ipynb
+++ b/docs/getting-started/function-approximations.ipynb
@@ -27,7 +27,7 @@
"source": [
"Welcome to the Quickstart Guide to Minterpy!\n",
"\n",
- "If you're reading this, chances are you want to create an interpolant that approximates a function, possibly in multiple dimensions. This guide provides a quick walkthrough on how to achieve this using Minterpy."
+ "If you're reading this, chances are you want to create an interpolant that approximates a function, possibly across multiple dimensions. This guide provides a quick walkthrough on how to achieve this using Minterpy."
]
},
{
@@ -76,7 +76,7 @@
"The shorthand `mp` is the convention we, the developer team, like to use.\n",
"You're free to choose any shorthand you prefer, but we find `mp` to be both short and recognizable (similar to `np` for NumPy and `pd` for pandas).\n",
"\n",
- "While you're at it, you will also need to import Numpy and Matplotlib to follow along with this guide."
+ "While you're at it, you will also need to import NumPy and Matplotlib to follow along with this guide."
]
},
{
@@ -112,24 +112,10 @@
"Let's start with interpolating the following one-dimensional function:\n",
"\n",
"$$\n",
- "f(x) = x \\, \\sin{(12 x)}, x \\in [-1, 1].\n",
- "$$"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "57bc44f0-84fc-4b7f-ba13-53d70a69b655",
- "metadata": {
- "editable": true,
- "slideshow": {
- "slide_type": ""
- },
- "tags": []
- },
- "source": [
- "```{note}\n",
- "Currently, Minterpy only supports interpolating functions defined in $[-1, 1]^m$ where $m$ is the spatial dimension. If the domain of your function differs, then you're responsible to carry out the domain transformation.\n",
- "```"
+ "f(x) = \\frac{1}{1 + 25 \\, x^2},\n",
+ "$$\n",
+ "\n",
+ "defined on $x \\in [-1, 1]$."
]
},
{
@@ -159,7 +145,7 @@
},
"outputs": [],
"source": [
- "fun_1d = lambda xx: xx * np.sin(12 * xx)"
+ "fun_1d = lambda xx: 1 / (1 + 25 * xx**2)"
]
},
{
@@ -213,17 +199,13 @@
"source": [
"### One-dimensional interpolant\n",
"\n",
- "To create an interpolant in Minterpy, use the {py:func}`.interpolate` function which accepts the following arguments in order:\n",
- "\n",
- "1. The function to interpolate (a callable that takes an array of input values and returns an array of output values of the same length)\n",
- "2. The spatial dimension of the function\n",
- "3. The degree of the polynomial interpolant\n",
+ "To create an interpolant in Minterpy, use the {py:func}`.interpolate` function. It takes three arguments in order: the function to interpolate, the spatial dimension, and the polynomial degree.\n",
"\n",
- "The lambda function you've defined above already satisfies the first requirement: as a callable, it takes an array of input values and returns an array of output values.\n",
+ "The function must be a callable that accepts a two-dimensional array of shape `(N, m)` where `m` is the number of dimensions and returns a one-dimensional array of length `N`. The lambda function you defined above already satisfies this.\n",
"\n",
- "The spatial dimension of the function, which represents the number of input variables it accepts, is $1$. Since Minterpy cannot automatically infer the dimensionality of your function, you must specify this value explicitly.\n",
+ "The spatial dimension is $1$ since $f$ takes a single input variable. Minterpy cannot infer this automatically, so you must specify it explicitly.\n",
"\n",
- "Finally, you need to specify the degree of the polynomial interpolant. Because Minterpy interpolants are based on polynomials, it is necessary to choose the polynomial degree in advance. As a best practice, it is recommended to verify the accuracy of the interpolant after creation to ensure it meets your requirements."
+ "For the polynomial degree, higher degrees give better approximations at the cost of more function evaluations. As a best practice, always verify the accuracy of the interpolant after construction."
]
},
{
@@ -360,15 +342,15 @@
"source": [
"### Assessing 1D interpolant accuracy\n",
"\n",
- "Looking at the plot above, you might conclude that the interpolant is not accurate enough as an approximation of the original function. However, relying on a plot to draw such conclusions is not always possible, especially when dealing with functions in higher dimensions.\n",
+ "Looking at the plot above, you might conclude that the interpolant is not accurate enough. But relying on a plot alone is not always possible, especially in higher dimensions.\n",
"\n",
- "The infinity norm provides a quantitative measure of the greatest error of the interpolant over the entire domain of the function. It is defined as:\n",
+ "The infinity norm provides a quantitative measure of the greatest (_worst-case_) error over the entire domain of the function:\n",
"\n",
"$$\n",
"\\lVert f - Q \\rVert_\\infty = \\sup_{-1 \\leq x \\leq 1} \\lvert f(x) - Q(x) \\rvert\n",
"$$\n",
"\n",
- "where $Q$ is the interpolant."
+ "where $Q$ is the Minterpy interpolant and $\\sup$ denotes the supremum, i.e., the least upper bound of the error across all points in the domain."
]
},
{
@@ -382,7 +364,7 @@
"tags": []
},
"source": [
- "You can estimate the infinity norm based on the $1'000$ query points as follows:"
+ "You can estimate the infinity norm based on a fine grid of test points, say $1'000$ points:"
]
},
{
@@ -412,7 +394,7 @@
"tags": []
},
"source": [
- "Whether the given norm is low enough depends on what you're using the interpolant for. But for now, let's agree that the number above isn't exactly a numerical convergence."
+ "Whether that's acceptable depends on your application. But for now, let's agree that the current norm is far from numerically converged."
]
},
{
@@ -426,7 +408,7 @@
"tags": []
},
"source": [
- "To have a more accurate interpolant, increase the polynomial degree, say to $32$:"
+ "To have a more accurate interpolant, increase the polynomial degree, say to $128$:"
]
},
{
@@ -442,7 +424,7 @@
},
"outputs": [],
"source": [
- "interp_1d_32 = mp.interpolate(fun_1d, spatial_dimension=1, poly_degree=32)"
+ "interp_1d_128 = mp.interpolate(fun_1d, spatial_dimension=1, poly_degree=128)"
]
},
{
@@ -476,7 +458,7 @@
"source": [
"plt.plot(xx_1d, fun_1d(xx_1d), label=\"original\")\n",
"plt.plot(xx_1d, interp_1d(xx_1d), label=\"interpolant (deg=8)\")\n",
- "plt.plot(xx_1d, interp_1d_32(xx_1d), label=\"interpolant (deg=32)\")\n",
+ "plt.plot(xx_1d, interp_1d_128(xx_1d), label=\"interpolant (deg=128)\")\n",
"plt.xlabel(\"$x$\", fontsize=14)\n",
"plt.ylabel(\"$y$\", fontsize=14)\n",
"plt.tick_params(axis='both', which='major', labelsize=12)\n",
@@ -494,7 +476,7 @@
"tags": []
},
"source": [
- "Looking at the graph, it's hard to see any difference between the interpolant of degree $32$ and the original function."
+ "Looking at the graph, it's hard to see any difference between the interpolant of degree $64$ and the original function."
]
},
{
@@ -520,7 +502,7 @@
},
"outputs": [],
"source": [
- "np.max(np.abs(fun_1d(xx_test_1d) - interp_1d_32(xx_test_1d)))"
+ "np.max(np.abs(fun_1d(xx_test_1d) - interp_1d_128(xx_test_1d)))"
]
},
{
@@ -550,11 +532,15 @@
"source": [
"## Two-dimensional function\n",
"\n",
- "Consider now a two-dimensional function:\n",
+ "Consider now a two-dimensional function taken from Cheng and Sandu (2010)[^Cheng]:\n",
"\n",
"$$\n",
- "f(x_1, x_2) = \\sin{\\pi (1.5 x_1 + 2.5 x_2)}, x_1, x_2 \\in [-1, 1].\n",
- "$$"
+ "f(x_1, x_2) = \\cos{(x_1 + x_2)} \\, e^{x_1 \\, x_2},\n",
+ "$$\n",
+ "\n",
+ "where $x_1, x_2 \\in [0, 1]$.\n",
+ "\n",
+ "[^Cheng]: Haiyan Cheng and Adrian Sandu. Collocation least-squares polynomial chaos method. In *Proceedings of the 2010 Spring Simulation Multiconference*, SpringSim '10, 1–6. Society for Computer Simulation International, 2010. doi:[10.1145/1878537.1878621](https://doi.org/10.1145/1878537.1878621)."
]
},
{
@@ -585,7 +571,7 @@
"outputs": [],
"source": [
"def fun_2d(xx):\n",
- " return np.sin(np.pi * (1.5 * xx[:, 0] + 2.5 * xx[:, 1]))"
+ " return np.cos(np.sum(xx, axis=1)) * np.exp(np.prod(xx, axis=1))"
]
},
{
@@ -620,7 +606,7 @@
"from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
"\n",
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 1000)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 1000)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_2d = fun_2d(xx_2d)\n",
@@ -672,14 +658,18 @@
"tags": []
},
"source": [
- "### Two-dimension interpolant\n",
+ "### Two-dimensional interpolant\n",
+ "\n",
+ "{py:func}`.interpolate` can also handle two-dimensional functions. A few things to keep in mind when moving to two dimensions:\n",
"\n",
- "The function {py:func}`.interpolate` can also handle two-dimensional functions. Here are a few things to note:\n",
"\n",
- "- Your function should take a two-dimensional (2D) array as input, where each row represents a point in space and each column corresponds to a spatial dimension. For functions defined in 2D space, this means the input array must have exactly two columns. The function should return an array with the same number of rows as the input.\n",
- "- As mentioned earlier, make sure to specify that your function has a spatial dimension of $2$ when calling {py:func}`.interpolate`. \n",
+ "**Function to interpolate**. The callable requirement is the same as before. The only difference is that the input array now has two columns, one per spatial dimension.\n",
"\n",
- "One more thing to note: when you're working with functions of more than one spatial dimension, you can also specify the $l_p$-degree argument. This controls the underlying multi-index set of exponents used in the interpolation. Different $l_p$-degree values can affect how well the interpolant approximates the original function. Checkout the {ref}`relevant section ` for details. If you don't specify an $l_p$-degree, it defaults to $2.0$."
+ "**Spatial dimension**. Set `spatial_dimension` to $2$ as Minterpy cannot infer this automatically.\n",
+ "\n",
+ "**Domain bounds**. Since $f$ is defined on $[0, 1]^2$ rather than the default $[-1, 1]^2$, you must specify the domain bounds explicitly via the `bounds` argument. The bounds are specified for every dimension. For the current example, `bounds=[[0, 1], [0, 1]]`.\n",
+ "\n",
+ "**$l_p$-degree**. For functions in more than one dimension, you can also specify the `lp_degree` argument, which controls the underlying multi-index set of exponents. Different values affect the structure and accuracy of the interpolant. See the {ref}`relevant section ` for details. If not specified, it defaults to $2.0$."
]
},
{
@@ -687,7 +677,7 @@
"id": "36208d48-84e6-40fc-8e89-c96834401593",
"metadata": {},
"source": [
- "To create a two-dimensional interpolant with a polynomial degree of $32$, use the following command:"
+ "To create a two-dimensional interpolant with a polynomial degree of $16$, use the following command:"
]
},
{
@@ -703,7 +693,12 @@
},
"outputs": [],
"source": [
- "interp_2d = mp.interpolate(fun_2d, spatial_dimension=2, poly_degree=32)"
+ "interp_2d = mp.interpolate(\n",
+ " fun_2d,\n",
+ " spatial_dimension=2,\n",
+ " poly_degree=16,\n",
+ " bounds=[[0, 1], [0,1]],\n",
+ ")"
]
},
{
@@ -738,7 +733,7 @@
"from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
"\n",
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 500)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0.0, 1.0, 500)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_2d = interp_2d(xx_2d)\n",
@@ -801,7 +796,7 @@
"metadata": {},
"source": [
"As explained previously, you can use the infinity norm to quantify the accuracy of the interpolant.\n",
- "To estimate it using, say, $100'000$ random points in $[-1, 1]^2$:"
+ "To estimate it using, say, $100'000$ random points in $[0, 1]^2$:"
]
},
{
@@ -818,7 +813,7 @@
"outputs": [],
"source": [
"rng = np.random.default_rng(194)\n",
- "xx_test_2d = -1 + 2 * rng.random((100000, 2))\n",
+ "xx_test_2d = rng.random((100000, 2))\n",
"np.max(np.abs(fun_2d(xx_test_2d) - interp_2d(xx_test_2d)))"
]
},
@@ -854,13 +849,15 @@
"Consider now the $m$-dimensional Runge function:\n",
"\n",
"$$\n",
- "f(\\boldsymbol{x}) = \\frac{1}{1 + 1.0 \\lVert \\boldsymbol{x} \\rVert_2^2}, \\boldsymbol{x} \\in [-1, 1]^m.\n",
+ "f(\\boldsymbol{x}) = \\frac{1}{1 + \\lVert \\boldsymbol{x} \\rVert_2^2},\n",
"$$\n",
"\n",
- "The function can be computed for any arbitrary dimension, but for this tutorial, the dimension is set to $4$ which already makes it hard to visualize.\n",
+ "defined on $[-1, 1]^m.$\n",
+ "\n",
+ "The function can be computed for any dimension, but for this tutorial we set $m = 4$. The dimension is high enough to make direct visualization impractical.\n",
"\n",
"```{note}\n",
- "The classic Runge function has a factor of $25$ instead of $1.0$ which makes the above function \"easier\" to interpolate.\n",
+ "The classic Runge function has a factor of $25$ instead of $1.0$ which makes the classic function sharper and thus harder to interpolate accurately.\n",
"```"
]
},
@@ -905,13 +902,13 @@
"tags": []
},
"source": [
- "### Four-dimension interpolant\n",
+ "### Four-dimensional interpolant\n",
"\n",
- "The $m$-dimensional Runge function defined above is chosen to show that {py:func}`.interpolate` works the same way for higher-dimensional functions as it does for 1D and 2D ones.\n",
+ "The $m$-dimensional Runge function is chosen to show that {py:func}`.interpolate` works the same way regardless of dimension. The callable requirement is the same as before; the input array now has four columns, one per spatial dimension.\n",
"\n",
- "Just make sure your function meets Minterpy's requirements: it should be a callable that takes a 2D array as input, where each row represents a point in multidimensional space and each column corresponds to a spatial dimension (so in this case, four columns).\n",
+ "Since $f$ is defined on $[-1, 1]^4$, which is the default domain, you don't need to specify the bounds argument explicitly.\n",
"\n",
- "Just like with 2D functions, you can also tweak the $l_p$-degree of the underlying multi-index set for higher-dimensional functions ($m > 1$). This is optional, but can affect the accuracy of the interpolant."
+ "Just like with 2D functions, you can also tweak the $l_p$-degree of the underlying multi-index set for higher-dimensional functions ($m > 1$). While this is optional and can affect the accuracy of the interpolant, the default value of $2.0$ is a reasonable choice."
]
},
{
@@ -999,13 +996,13 @@
"source": [
"## Summary\n",
"\n",
- "Congratulations on completing this Quickstart Guide!\n",
+ "That's it for the Quickstart Guide!\n",
"\n",
- "We hope you now know how to interpolate a function in one or multiple dimensions using Minterpy and that you find Minterpy useful for your work.\n",
+ "You've seen how to interpolate functions in one, two, and four dimensions using {py:func}`.interpolate` and how to specify custom domain bounds.\n",
"\n",
"---\n",
"\n",
- "Minterpy offers more than just function approximation through interpolation. To fully appreciate its capabilities, we encourage you to explore further. We have prepared a series of tutorials designed to help you become familiar with the more advanced features of Minterpy."
+ "Minterpy offers much more than what's covered here. If you'd like to explore further, we've prepared a series of tutorials covering the more advanced features of Minterpy."
]
}
],
@@ -1025,7 +1022,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.19"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/getting-started/md-polynomial-interpolation.ipynb b/docs/getting-started/md-polynomial-interpolation.ipynb
index 6e5ac3a0..85689eff 100644
--- a/docs/getting-started/md-polynomial-interpolation.ipynb
+++ b/docs/getting-started/md-polynomial-interpolation.ipynb
@@ -25,12 +25,9 @@
"tags": []
},
"source": [
- "In the {doc}`previous guide `,\n",
- "you learned the basics of polynomial interpolation in Minterpy for approximating one-dimensional functions.\n",
- "As the name \"Minterpy\" suggests, the package also supports constructing multivariate polynomials to approximate multidimensional functions.\n",
+ "In the {doc}`previous guide `, you learned the basics of polynomial interpolation in Minterpy for approximating one-dimensional functions. As the name \"Minterpy\" suggests, the package also supports constructing multivariate polynomials to approximate multidimensional functions.\n",
"\n",
- "In this in-depth tutorial, you will learn how to create an interpolating polynomial that approximates a two-dimensional function.\n",
- "We use a two-dimensional example for ease of visualization, but the main steps you'll learn are applicable to polynomials of higher dimensions as well."
+ "In this in-depth tutorial, you will learn how to create an interpolating polynomial that approximates a two-dimensional function defined on a custom rectangular domain. We use a two-dimensional example for ease of visualization, but the main steps you'll learn are applicable to polynomials of higher dimensions as well."
]
},
{
@@ -82,14 +79,14 @@
"\n",
"$$\n",
"\\begin{align}\n",
- "\t\\mathcal{M}(\\boldsymbol{x}) = & 0.75 \\exp{\\left( -0.25 \\left( (x_1 - 2)^2 + (x_2 - 2)^2 \\right) \\right) } \\\\\n",
- " & + 0.75 \\exp{\\left( -1.00 \\left( \\frac{(x_1 + 1)^2}{49} + \\frac{(x_2 + 1)^2}{10} \\right) \\right)} \\\\\n",
- "\t\t\t\t\t\t\t\t & + 0.50 \\exp{\\left( -0.25 \\left( (x_1 - 7)^2 + (x_2 - 3)^2 \\right) \\right)} \\\\\n",
- "\t\t\t\t\t\t\t\t & - 0.20 \\exp{\\left( -1.00 \\left( (x_1 - 4)^2 + (x_2 - 7)^2 \\right) \\right)} \\\\\n",
+ "\t\\mathcal{M}(\\boldsymbol{x}) = & 0.75 \\exp{\\left( -0.25 \\left( (9 x_1 - 2)^2 + (9 x_2 - 2)^2 \\right) \\right) } \\\\\n",
+ " & + 0.75 \\exp{\\left( -1.00 \\left( \\frac{(9 x_1 + 1)^2}{49} + \\frac{(9 x_2 + 1)^2}{10} \\right) \\right)} \\\\\n",
+ "\t\t\t\t\t\t\t\t & + 0.50 \\exp{\\left( -0.25 \\left( (9 x_1 - 7)^2 + (9 x_2 - 3)^2 \\right) \\right)} \\\\\n",
+ "\t\t\t\t\t\t\t\t & - 0.20 \\exp{\\left( -1.00 \\left( (9 x_1 - 4)^2 + (9 x_2 - 7)^2 \\right) \\right)} \\\\\n",
"\\end{align}\n",
"$$\n",
"\n",
- "where $\\boldsymbol{x} = (x_1, x_2) \\in [-1, 1]^2$."
+ "where $\\boldsymbol{x} = (x_1, x_2) \\in [0, 1]^2$."
]
},
{
@@ -104,7 +101,7 @@
},
"source": [
"```{note}\n",
- "This function is known in the literature as the Franke function[^franke]. Here, however, the domain has been redefined to be $[-1, 1]^2$ instead of $[0, 1]^2$.\n",
+ "This function is known in the literature as the Franke function[^franke].\n",
"\n",
"[^franke]: Richard Franke, \"A critical comparison of some methods for interpolation of scattered data,\" Naval Postgraduate School, Monterey, Canada, Technical Report No. NPS53-79-003, 1979. URL: https://core.ac.uk/reader/36727660\n",
"```"
@@ -186,7 +183,7 @@
"from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
"\n",
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 1000)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 1000)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_2d = fun(xx_2d)\n",
@@ -242,13 +239,42 @@
"\n",
"The steps to create a multidimensional interpolating polynomial in Minterpy from scratch are similar to those used for the {doc}`one-dimensional case <1d-polynomial-interpolation>`. The steps are:\n",
"\n",
- "1. Define the multi-index set of the polynomial\n",
- "2. Construct an interpolation grid\n",
- "3. Evaluate the function of interest at the grid points (so-called unisolvent nodes)\n",
- "4. Create a polynomial in the Lagrange basis\n",
- "5. Transformation from the Lagrange basis to another basis, preferrably the Newton basis, for further manipulation.\n",
+ "1. Define the domain of the polynomial\n",
+ "2. Define the multi-index set of the polynomial\n",
+ "3. Construct an interpolation grid\n",
+ "4. Evaluate the function of interest at the grid points (so-called unisolvent nodes)\n",
+ "5. Create a polynomial in the Lagrange basis\n",
+ "6. Transformation from the Lagrange basis to another basis, preferrably the Newton basis, for further manipulation.\n",
"\n",
- "As before, we will go through these steps one at a time, highlighting any differences that arise compared to the one-dimensional case."
+ "As before, we will go through these steps one at a time. One key difference from the one-dimensional case is that our function lives on a custom rectangular domain $[0, 1]^2$ rather than the default $[-1, 1]^2$. We, therefore, begin by informing Minterpy about that domain explicitly."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d0dc60ed-56e9-47a5-a101-433df11208c3",
+ "metadata": {},
+ "source": [
+ "### Define the domain\n",
+ "\n",
+ "In Minterpy, a rectangular domain $\\Omega = [a_1, b_1] \\times \\cdots \\times [a_m, b_m]$ is represented by the {py:class}`Domain <.core.domain.Domain>` class. Each dimension has its own lower and upper bound, allowing for non-uniform domains. Internally, Minterpy works in the internal reference domain $[-1, 1]^m$, but once a `Domain` is defined, all coordinates transformations are handled automatically. For more details, see the {doc}`relevant section ` of the documentation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "befc91d8-c1da-49cb-82a4-38c67e9810ba",
+ "metadata": {},
+ "source": [
+ "To define a custom domain $[0, 1]^2$, we use the default constructor and pass an array of shape `(m, 2)` (here, `(2, 2)`) where each row specifies the lower and upper bounds of one dimension:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ff469f80-87af-46ee-b703-af1dce4bfbdd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(np.array([[0, 1], [0, 1]]))"
]
},
{
@@ -264,16 +290,20 @@
"source": [
"### Define the multi-index set\n",
"\n",
- "As demonstrated in the previous tutorial, multi-index sets in Minterpy are represented by the {py:class}`MultiIndexSet <.core.multi_index.MultiIndexSet>` class.\n",
- "You can use the class method {py:meth}`from_degree() <.MultiIndexSet.from_degree>` to create a complete multi-index set for a given spatial dimension (first argument), polynomial degree and polynomial degree (second argument).\n",
- "For multivariate polynomials, you also need to specify the $l_p$-degree ($> 0.0$) of the multi-index set (third argument).\n",
+ "As demonstrated in the previous tutorial, multi-index sets in Minterpy are represented by the {py:class}`MultiIndexSet <.core.multi_index.MultiIndexSet>` class. You can use the class method {py:meth}`from_degree() <.MultiIndexSet.from_degree>` to create a complete multi-index set for a given spatial dimension (first argument) and polynomial degree (second argument). For multivariate polynomials, you also need to specify the $l_p$-degree ($> 0.0$) of the multi-index set (third argument).\n",
"\n",
"A complete multi-index set with spatial dimension $m$, polynomial degree $n$, and $l_p$-degree $p$ denoted by $A_{m, n, p}$ includes all multi-indices $\\boldsymbol{\\alpha}$ that satisfy the condition $\\lVert \\boldsymbol{\\alpha} \\rVert_p \\leq n$, specifically\n",
"\n",
"$$\n",
"A_{m, n, p} = \\{ \\boldsymbol{\\alpha} \\in \\mathbb{N}^m: \\lVert \\boldsymbol{\\alpha} \\lVert_ p = ( \\alpha_1^p + \\ldots + \\alpha_m^p )^{1/p} \\leq n \\}.\n",
- "$$\n",
- "\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "39be1c6e-a0ff-4ceb-a5f5-22c71d94b8fc",
+ "metadata": {},
+ "source": [
"For instance, the complete multi-index set for $m = 2$, $n = 5$, $p = 2.0$:"
]
},
@@ -412,9 +442,15 @@
"source": [
"### Construct an interpolation grid\n",
"\n",
- "An interpolating polynomial, one-dimensional or otherwise, lives on an interpolation grid.\n",
- "\n",
- "To construct an interpolation grid, pass the previously defined multi-index set to the default constructor of the {py:class}`Grid <.core.grid.Grid>` class:"
+ "An interpolating polynomial, one-dimensional or otherwise, lives on an interpolation grid."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "175f447f-4f17-46e8-920d-5c15608400be",
+ "metadata": {},
+ "source": [
+ "To construct an interpolation grid, pass the previously defined multi-index set and domain to the default constructor of the {py:class}`Grid <.core.grid.Grid>` class:"
]
},
{
@@ -430,7 +466,15 @@
},
"outputs": [],
"source": [
- "grd = mp.Grid(mi)"
+ "grd = mp.Grid(mi, domain=dom)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "42498f5f-c255-4438-bc1f-2aa6e014fd0c",
+ "metadata": {},
+ "source": [
+ "The domain tells Minterpy that the function of interest lives on $[0, 1]^2$ rather than the default internal reference domain $[-1, 1]^2$."
]
},
{
@@ -444,8 +488,7 @@
"tags": []
},
"source": [
- "The unisolvent nodes associated with the interpolating grid has the same spatial dimension as its defining multi-index set. By default, Minterpy generates unisolvent nodes according to the {ref}`Leja-ordered Chebyshev-Lobatto points `.\n",
- "The unisolvent nodes that correspond to the multi-index set are:"
+ "The unisolvent nodes associated with the interpolating grid has the same spatial dimension as its defining multi-index set. By default, Minterpy generates unisolvent nodes according to the {ref}`Leja-ordered Chebyshev-Lobatto points `. Internally, Minterpy works in the internal reference domain $[-1, 1]^2$, so the unisolvent nodes are stored in that domain:"
]
},
{
@@ -539,7 +582,14 @@
"source": [
"### Evaluate the function at the unisolvent nodes\n",
"\n",
- "An interpolating polynomial satisfies the condition that the polynomial values at the unisolvent nodes coincide with the value of the given function at the same nodes.\n",
+ "An interpolating polynomial satisfies the condition that the polynomial values at the unisolvent nodes coincide with the value of the given function at the same nodes."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "97a19adb-e55f-4107-a745-41ed2a88ddfa",
+ "metadata": {},
+ "source": [
"Calling an instance of {py:class}`Grid <.core.grid.Grid>` with a given function evaluates the function at the unisolvent nodes:"
]
},
@@ -572,7 +622,7 @@
},
"source": [
"```{note}\n",
- "Make sure that the given function accepts a two-dimensional array whose each column corresponds to the values per spatial dimension.\n",
+ "Make sure that the given function accepts a two-dimensional array whose each column corresponds to the values per spatial dimension. The function will receive points already in the user domain $[0, 1]^2$; there is no manual transformation from $[-1, 1]^2$ needed.\n",
"```"
]
},
@@ -601,7 +651,7 @@
"L_{\\boldsymbol{\\alpha}}(p_{\\boldsymbol{\\beta}}) = \\delta_{\\boldsymbol{\\alpha}, \\boldsymbol{\\beta}}, p_{\\boldsymbol{\\beta}} \\in P_{A_{m, n, p}},\n",
"$$\n",
"\n",
- "where $\\delta{\\cdot, \\cdot}$ denotes the Kronecker delta.\n",
+ "where $\\delta_{\\cdot, \\cdot}$ denotes the Kronecker delta.\n",
"\n",
"To create a polynomial in the Lagrange basis from the given grid and coefficients, use the {py:meth}`from_grid() <.polynomials.lagrange_polynomial.LagrangePolynomial.from_grid>` class method of {py:class}`LagrangePolynomial <.polynomials.lagrange_polynomial.LagrangePolynomial>` class:"
]
@@ -622,6 +672,14 @@
"lag_poly = mp.LagrangePolynomial.from_grid(grd, coeffs)"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "502b5930-e8dc-4e35-b7f0-6c3dd28dad08",
+ "metadata": {},
+ "source": [
+ "Note that the resulting polynomial inherits the domain information from the grid."
+ ]
+ },
{
"cell_type": "markdown",
"id": "87129795-5373-4ea5-a9df-74fe4dea11b2",
@@ -721,7 +779,7 @@
"source": [
"### Evaluate the polynomial at a set of query points\n",
"\n",
- "Create a set of random query points in $[-1, 1]^2$:"
+ "Create a set of random query points in $[0, 1]^2$:"
]
},
{
@@ -737,7 +795,7 @@
},
"outputs": [],
"source": [
- "xx_test = -1 + 2 * np.random.rand(1000, 2)"
+ "xx_test = np.random.rand(1000, 2)"
]
},
{
@@ -876,12 +934,15 @@
"outputs": [],
"source": [
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 250)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 250)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_plot_test = fun(xx_2d)\n",
"yy_plot_poly = nwt_poly(xx_2d)\n",
"\n",
+ "# Transform unisolvent nodes to the user domain\n",
+ "unisolvent_nodes = dom.map_from_internal(grd.unisolvent_nodes)\n",
+ "\n",
"# --- Create two-dimensional plots\n",
"fig, axs = plt.subplots(\n",
" nrows=1,\n",
@@ -920,7 +981,7 @@
"axs[1].set_ylabel(\"$x_2$\", fontsize=14)\n",
"axs[1].set_title(\"Interpolating polynomial\", fontsize=16)\n",
"axs[1].tick_params(axis='both', which='major', labelsize=12)\n",
- "axs[1].scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1], coeffs, color=\"k\");"
+ "axs[1].scatter(unisolvent_nodes[:, 0], unisolvent_nodes[:, 1], coeffs, color=\"k\");"
]
},
{
@@ -942,9 +1003,17 @@
"The norm is defined as:\n",
"\n",
"$$\n",
- "\\lVert f - Q \\rVert_\\infty = \\sup_{\\boldsymbol{x} \\in [-1, 1]^2} \\lvert f(\\boldsymbol{x}) - Q(\\boldsymbol{x}) \\rvert\n",
+ "\\lVert f - Q \\rVert_\\infty = \\sup_{\\boldsymbol{x} \\in \\Omega} \\lvert f(\\boldsymbol{x}) - Q(\\boldsymbol{x}) \\rvert,\n",
"$$\n",
"\n",
+ "where $\\Omega$ is the domain of the function and for the Franke function it is $[0, 1]^2$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e3f6fb4c-3fa2-4280-96b8-d91e5b0770eb",
+ "metadata": {},
+ "source": [
"The infinity norm of $Q$ can be approximated using the $1'000$ test points created above:"
]
},
@@ -1013,7 +1082,7 @@
"yy_poly = np.empty((len(xx_test), len(poly_degrees), len(lp_degrees)))\n",
"for i, n in enumerate(poly_degrees):\n",
" for j, p in enumerate(lp_degrees):\n",
- " fx_interp = mp.interpolate(fun, spatial_dimension=2, poly_degree=n, lp_degree=p)\n",
+ " fx_interp = mp.interpolate(fun, spatial_dimension=2, poly_degree=n, lp_degree=p, bounds=[[0, 1], [0, 1]])\n",
" yy_poly[:, i, j] = fx_interp(xx_test)"
]
},
@@ -1123,7 +1192,7 @@
"from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
"\n",
"# --- Create 2D data\n",
- "xx_1d = np.linspace(-1.0, 1.0, 250)[:, np.newaxis]\n",
+ "xx_1d = np.linspace(0, 1.0, 250)[:, np.newaxis]\n",
"mesh_2d = np.meshgrid(xx_1d, xx_1d)\n",
"xx_2d = np.array(mesh_2d).T.reshape(-1, 2)\n",
"yy_fun = fun(xx_2d)\n",
@@ -1220,12 +1289,13 @@
"source": [
"## Summary\n",
"\n",
- "In this tutorial, you learned how to create a two-dimensional (2D) interpolating polynomial from scratch to approximate a given function then evaluate it at a set of query points in Minterpy.\n",
+ "In this tutorial, you learned how to create a two-dimensional (2D) interpolating polynomial from scratch to approximate a given function defined on a custom rectangular domain, then evaluate it at a set of query points in Minterpy.\n",
"\n",
"The steps are:\n",
"\n",
+ "1. Define the domain\n",
"1. Define a multi-index set\n",
- "2. Construct an interpolation grid\n",
+ "2. Construct an interpolation grid with domain information\n",
"3. Evaluate the given function on the grid points (i.e., the unisolvent nodes)\n",
"4. Construct an interpolating polynomial in the Lagrange basis\n",
"5. Transform the polynomial into the equivalent Newton basis\n",
@@ -1236,6 +1306,8 @@
"- Your function of interest should take a 2D array as input, where each row represents a point in multidimensional space and each column corresponds to a value in each dimension.\n",
"- When evaluating an interpolating polynomial, make sure your input has the same 2D array structure.\n",
"\n",
+ "If your function lives on a domain other than $[-1, 1]^m$, define a {py:class}`Domain <.core.domain.Domain>` object and pass it to the {py:class}`Grid <.core.grid.Grid>` constructor or use the `bounds` parameter of {py:func}`interpolate() <.interpolation.interpolate>` function. Minterpy handles all coordinate transformation transparently.\n",
+ "\n",
"While the example here is two-dimensional, the same principles apply to polynomials in higher dimensions.\n",
"\n",
"Finally, you saw that for the chosen function, an interpolating polynomial approximate it with sufficiently high polynomial degree. \n",
@@ -1264,7 +1336,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.19"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/domain/domain-check-containment.ipynb b/docs/how-to/domain/domain-check-containment.ipynb
new file mode 100644
index 00000000..6ef2081c
--- /dev/null
+++ b/docs/how-to/domain/domain-check-containment.ipynb
@@ -0,0 +1,248 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Use Domain to Check Points Containment"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Once a ``Domain`` instance is constructed, it can be used to verify whether a given set of points lies within the domain.\n",
+ "\n",
+ "This guide demonstrates how to carry out such a check."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Three-dimensional rectangular domain\n",
+ "\n",
+ "Create a three-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [0.0, 5.0] \\times [10.0, 15.0] \\times [0.5, 3.0]\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea68fa6c-daf4-41af-ae87-de8f71dc76da",
+ "metadata": {},
+ "source": [
+ "### Domain instance\n",
+ "\n",
+ "The rectangular domain is defined by three pairs of bounds (each forms an interval) one for each dimension. We can specify them in a single two-dimensional array of three rows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1254453-8c4e-439d-8fd7-b5378e20b1f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bounds = np.array([\n",
+ " [0.0, 5.0],\n",
+ " [10.0, 15.0],\n",
+ " [0.5, 3.0],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "An instance of `Domain` given an array of bounds can be constructed using the default constructor:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is three dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8cb2de8d-b119-40bc-8c3c-9d0ec9b924d5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "source": [
+ "## Check containment with respect to the user domain\n",
+ "\n",
+ "A set of points can be verified to lie within the user domain using the `contains()` method. For instance, to check if the following set of points"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38b38a20-e3fd-4725-97a5-9337f7b3794d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xx = np.array([\n",
+ " [0.1, 11.0, 1.75], # True\n",
+ " [0.4, 13, 2.75], # True\n",
+ " [-1, 11, 0.5], # False\n",
+ " [0.2, 16.0, 2.0], # False\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3eb6e5df-de2a-44a9-a5cf-607252e60a9d",
+ "metadata": {},
+ "source": [
+ "lie within the domain:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "25ef90b4-400e-41ab-9c1c-1fc045e1ef58",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.contains(xx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55f334f6-da91-499c-aed5-d1f95932a9be",
+ "metadata": {},
+ "source": [
+ "Notice that the check is carried for each point."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "27d829aa-ef3a-4ce4-ade1-103e73b8a358",
+ "metadata": {},
+ "source": [
+ "## Check containment with respect to the internal domain\n",
+ "\n",
+ "Alternatively, a set of points can also be verified to lie within the internal domain using the same method and passing `internal=True` argument. For instance, to check if the following set of points"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06dc709c-bd1e-4d32-8f9c-2361d173ecc3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xx = np.array([\n",
+ " [0.0, 0.0, 0.0], # True\n",
+ " [-1, -1, -1], # True\n",
+ " [-0.75, 0.75, 0.5], # True\n",
+ " [-2, -2, 2], # False\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b2f23eff-a846-4889-a128-cc0aea716277",
+ "metadata": {},
+ "source": [
+ "lie within the internal domain:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0383f74c-81d8-48b7-83cc-2284f6edc298",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.contains(xx, internal=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a1fe8af4-5c78-4b7d-80ca-d773b8f9ee93",
+ "metadata": {},
+ "source": [
+ "Once again, notice that the check is carried for each point."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-check-equality.ipynb b/docs/how-to/domain/domain-check-equality.ipynb
new file mode 100644
index 00000000..e5325733
--- /dev/null
+++ b/docs/how-to/domain/domain-check-equality.ipynb
@@ -0,0 +1,193 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Check for Equality between Domain Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8cfee992-8ff3-4127-bb50-2f1f1af5b660",
+ "metadata": {},
+ "source": [
+ "Two instances of `Domain` may be checked for equality in value via the `==` operator as demonstrated in this guide.\n",
+ "\n",
+ "Two instances of `Domain` are equal in value if and only if:\n",
+ "\n",
+ "- the underlying bounds have the same shape (i.e., the same number of spatial dimensions)\n",
+ "- all the corresponding bounds, including:\n",
+ " - **User bounds**: The rectangular bounds of the domain as specified by the user during instance construction, e.g., $[0, 5] \\times [1, 2]$.\n",
+ " - **Internal bounds**: The bounds of the reference domain used internally by Minterpy. Currently they are always set to $[-1, 1]^m$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "83783a9a-a498-4711-895c-6a664004e346",
+ "metadata": {},
+ "source": [
+ "## Example: Two equal domains"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a089c0ce-f8df-46a4-9731-da0248de4053",
+ "metadata": {},
+ "source": [
+ "The two domains below are equal:\n",
+ "\n",
+ "- $\\Omega_1 = [2, 3] \\times [4, 5]$\n",
+ "- $\\Omega_2 = [2, 3] \\times [4, 5]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "20ac5f1a-ed7f-4019-adc6-c0347e8180a7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 = mp.Domain(np.array([[2, 3], [4, 5]]))\n",
+ "dom_2 = mp.Domain(np.array([[2, 3], [4, 5]]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6c67fb77-1750-413f-ae0c-2ad3d6e26800",
+ "metadata": {},
+ "source": [
+ "To check the equality between two instances, use the `==` operator. So:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d6ea4a4f-af5a-4b72-98e0-abe71aef243d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 == dom_2 # True"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e34cfbc5-dea6-4519-9c6c-c94b95aba61a",
+ "metadata": {},
+ "source": [
+ "Note that `==` checks for equality in value, not object identity. Even though the two instances are equal in value, they are distinct instances, as confirmed below"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6addce53-baa1-4ccb-9d3b-f8b491589555",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 is dom_2 # False"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e1a3a564-36bb-4dad-a01a-8e20fb907363",
+ "metadata": {},
+ "source": [
+ "## Example: Unequal domains - differing bounds\n",
+ "\n",
+ "The two domains below have the same spatial dimension but differ in the bounds of the second dimension:\n",
+ "\n",
+ "- $\\Omega_3 = [1, 2] \\times [4, 5]$\n",
+ "- $\\Omega_4 = [1, 2] \\times [4, 6]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d1d253e6-4bac-4998-9108-c222f281c4d1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 = mp.Domain(np.array([[1, 2], [4, 5]]))\n",
+ "dom_4 = mp.Domain(np.array([[1, 2], [4, 6]]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f1e801a6-f1cb-4317-b5d2-b1f556770422",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 == dom_4 # False"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "60942d82-304a-4730-93d6-83a73948c5ad",
+ "metadata": {},
+ "source": [
+ "## Example: Unequal domains - differing spatial dimensions\n",
+ "\n",
+ "The two domains below have the same bounds up to the common dimension but differ in their spatial dimension:\n",
+ "\n",
+ "- $\\Omega_5 = [1, 2] \\times [3, 4] \\times [5, 6]$\n",
+ "- $\\Omega_6 = [1, 2] \\times [3, 4]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c18d601a-4747-4d63-9117-5e97d9de1fdd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_5 = mp.Domain(np.array([[1, 2], [3, 4], [5, 6]]))\n",
+ "dom_6 = mp.Domain(np.array([[1, 2], [3, 4]]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "542debc1-c8a1-4f22-9527-d2f334314964",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_5 == dom_6 # False"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-check-partial-matching.ipynb b/docs/how-to/domain/domain-check-partial-matching.ipynb
new file mode 100644
index 00000000..ae50c13b
--- /dev/null
+++ b/docs/how-to/domain/domain-check-partial-matching.ipynb
@@ -0,0 +1,328 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Check for Partial Matching between Domain Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Two instances of `Domain` may be checked for partial matching via the `partial_matching()` method as demonstrated in this guide.\n",
+ "\n",
+ "Two instances of `Domain` are partially matching if both their user-defined bounds and their internal bounds, up to the common spatial dimension, are approximately equal as determined by `numpy.allclose()`:\n",
+ "\n",
+ "- **User bounds**: The rectangular bounds of the domain as specified by the user during instance construction, e.g., $[0, 5] \\times [1, 2]$.\n",
+ "- **Internal bounds**: The bounds of the reference domain used internally by Minterpy. Currently they are always set to $[-1, 1]^m$.\n",
+ "\n",
+ "Two instances of `Domain` are partially matching if the bounds, both internal and user, up to a common dimension are approximately closed as defined by the NumPy function `allclose()`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ecb0b18c-1563-4c30-b866-29787db27d3b",
+ "metadata": {},
+ "source": [
+ "## Example: Two equal domains"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cb1a6a05-8264-4ea5-b97d-7108c24c5a2f",
+ "metadata": {},
+ "source": [
+ "As a baseline, two equal instances of `Domain` are by definition partially matching.\n",
+ "\n",
+ "- $\\Omega_1 = [1, 2] \\times [1, 2]$\n",
+ "- $\\Omega_2 = [1, 2] \\times [1, 2]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c6fa236a-6ca6-44ca-97f3-4d793253f0a1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 = mp.Domain(np.array([[1, 2], [1, 2]]))\n",
+ "dom_2 = mp.Domain(np.array([[1, 2], [1, 2]]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ac2c12d-67c5-4343-9c2f-c79432872a15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 == dom_2 # True"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "854e7917-2497-4293-b464-80cd1d013f4d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1.partial_matching(dom_2) # True"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "792d9afd-818a-46a0-8c03-c798300d949c",
+ "metadata": {},
+ "source": [
+ "## Example: Two partially matching domains"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bfa1433d-e6f8-4fff-bbf9-921a2fb6689c",
+ "metadata": {},
+ "source": [
+ "The two instances below are not equal in values but partially matching:\n",
+ "\n",
+ "- $\\Omega_3 = [1, 2] \\times [3, 4] \\times [5, 6]$\n",
+ "- $\\Omega_4 = [1, 2] \\times [3, 4]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7bcff62-0c12-4034-8dc6-3f677405f2b8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 = mp.Domain(np.array([[1, 2], [3, 4], [5, 6]]))\n",
+ "dom_4 = mp.Domain(np.array([[1, 2], [3, 4]]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6e5c8f2f-766a-4023-a897-778c4dc41b5f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 == dom_4 # False"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06d77fa7-d1eb-4cbf-bef5-bce70eb97b82",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3.partial_matching(dom_4) # True"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9b875e32-7df5-4b0c-9dbf-033ce8864f84",
+ "metadata": {},
+ "source": [
+ "This is because up to the common dimension, i.e., $m = 2$ all the bounds of the two domains are equal or close to each other."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5ecc24ab-14ed-4be8-a2de-ed6495849cf9",
+ "metadata": {},
+ "source": [
+ "Note that `partial_matching()` is symmetric; the result is the same regardless of which instance the method is called on, because the common dimension is determined by the minimum of the two spatial dimensions."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c7f34cc6-ad91-434f-9ded-1f48a8752c8a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_4.partial_matching(dom_3) # True"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ec089f6f-05e8-494f-8abb-99e014b8f654",
+ "metadata": {},
+ "source": [
+ "## Example: Domains with no partial matching"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7f23b6eb-5b98-4f93-8272-e4231d26fd8d",
+ "metadata": {},
+ "source": [
+ "The two instances below are not equal in values and has no partial matching:\n",
+ "\n",
+ "- $\\Omega_5 = [1, 2]$\n",
+ "- $\\Omega_6 = [5, 6] \\times [1, 2]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e5b62552-0485-4233-ab35-5e30dd907c2c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_5 = mp.Domain(np.array([[1, 2]]))\n",
+ "dom_6 = mp.Domain(np.array([[5, 6], [1, 2]]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a6cf8a76-d92c-477a-bdf0-44807860300c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_5 == dom_6 # False"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f3d7c16a-7cc8-4976-b038-c27d97772f74",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_5.partial_matching(dom_6) # False"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6c97c7ef-22f7-44f6-80f7-edd416212669",
+ "metadata": {},
+ "source": [
+ "Since the common dimension is $m = 1$ and the bounds of the first and second domains up to $m=1$ do not match, the two domains are note partially matching."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4ab67d0d-d8c1-47a1-b512-57526c2bb79e",
+ "metadata": {},
+ "source": [
+ "## Tolerance control\n",
+ "\n",
+ "The comparison uses approximate equality via `numpy.allclose()`. The default tolerances can be overridden via the `rtol` and `atol` parameters."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2eec19b4-3132-4d97-b0ef-016e64ca9fda",
+ "metadata": {},
+ "source": [
+ "For example, the two domains below differ by a tiny floating-point amount in the first dimension."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fa0656f7-a0f0-4fea-86de-04db12635bc2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_a = mp.Domain(np.array([[1, 2], [3, 4]]))\n",
+ "dom_b = mp.Domain(np.array([[1 + 1e-10, 2], [3, 4]]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cf6a4822-1d95-4d82-8f11-165aa868fd03",
+ "metadata": {},
+ "source": [
+ "With default tolerances, they are considered partially matching:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9961b1f2-e894-4623-a18f-4d574d74d519",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_a.partial_matching(dom_b) # True"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ecd10a1-aa08-4d1f-8cda-67257884d6c5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_b.partial_matching(dom_a) # True"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e78d056-4891-43ab-af59-bd77f5f5291d",
+ "metadata": {},
+ "source": [
+ "With a very tight tolerance, they are not:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "74e3a208-eef1-489d-8981-48ee0dd147f9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_a.partial_matching(dom_b, rtol=0) # False"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "afa7db11-ffef-4ecc-a51e-19a73f4a5bb2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_b.partial_matching(dom_a, rtol=0) # False"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-compute-diff-factor.ipynb b/docs/how-to/domain/domain-compute-diff-factor.ipynb
new file mode 100644
index 00000000..baada35b
--- /dev/null
+++ b/docs/how-to/domain/domain-compute-diff-factor.ipynb
@@ -0,0 +1,216 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Compute Differentiation Scaling Factor"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Differentiating a polynomial in Minterpy involves two main steps:\n",
+ "\n",
+ "- Differentiating the internal polynomial representation with respect to the variables of the internal reference domain (currently $[-1, 1]^m$), given the specified orders of differentiation\n",
+ "- Scaling the result by the differentiation factor, which accounts for the change of variables from $\\Omega$ to $[-1, 1]^m$ via the chain rule\n",
+ "\n",
+ "While Minterpy handles this scaling automatically, this guide demonstrates how the differentiation scaling factor can be computed directly from a `Domain` instance."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Four-dimensional rectangular domain\n",
+ "\n",
+ "Create a four-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [0.0, 5.0] \\times [10.0, 15.0] \\times [5.0, 10.0] \\times [3.0, 7.0]\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea68fa6c-daf4-41af-ae87-de8f71dc76da",
+ "metadata": {},
+ "source": [
+ "## Domain instance\n",
+ "\n",
+ "The rectangular domain is defined four pair of bounds, each of which forms and interval for a dimension. We can specify them in a single two-dimensional array of two rows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1254453-8c4e-439d-8fd7-b5378e20b1f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bounds = np.array([\n",
+ " [0.0, 5.0],\n",
+ " [10.0, 15.0],\n",
+ " [5.0, 10.0],\n",
+ " [3.0, 7.0],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "An instance of `Domain` given an array of bounds can be constructed using the default constructor:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is indeed four dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "## Differentiation factor\n",
+ "\n",
+ "The differentiation factor can be computed via the `diff_factor()` method, which takes the order of derivatives as an argument. Applying the chain rule to the affine separable transformation, the differentiation scaling factor is:\n",
+ "\n",
+ "$$\n",
+ "c = \\prod_{i=1}^m \\left(\\frac{1 - (-1)}{b_i - a_i}\\right)^{k_i} = \\left(\\frac{2}{b_i - a_i}\\right)^{k_i},\n",
+ "$$\n",
+ "\n",
+ "where $k_i$ is the order of derivative for dimension $i$.\n",
+ "\n",
+ "For instance, the factor for the mixed partial derivative of $f$ with respect to $x_1$ and $x_4$ is obtained via:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d9d27795-eb63-48aa-ba0a-54338d3e86cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "deriv_order = np.array([1, 0, 0, 1]) # Differentiation w.r.t x_1 and x_4"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9b624b19-3be8-49a8-ad25-121ade7edd69",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.diff_factor(deriv_order))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ba1ce94d-dbb1-428c-9128-687d579bbf68",
+ "metadata": {},
+ "source": [
+ "As another example, the factor for the mixed partial derivative $\\frac{\\partial^3 f}{\\partial x_2^2 \\partial x_3}$ is obtained via:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c6535361-42a1-491e-b839-3e037e1d077b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "deriv_order = np.array([0, 2, 1, 0]) # Differentiation w.r.t x_1 and x_4"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7df24e84-c10b-4ee5-a034-c771d90b7558",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.diff_factor(deriv_order))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-compute-int-factor.ipynb b/docs/how-to/domain/domain-compute-int-factor.ipynb
new file mode 100644
index 00000000..a98a3fa2
--- /dev/null
+++ b/docs/how-to/domain/domain-compute-int-factor.ipynb
@@ -0,0 +1,174 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Compute Integration Scaling Factor"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Integrating a polynomial in Minterpy involves two main steps:\n",
+ "\n",
+ "- Integrating the internal polynomial representation over the internal reference domain (currently $[-1, 1]^m$)\n",
+ "- Scaling the result by the integration factor, which accounts for the change of variables from $\\Omega$ to $[-1, 1]^m$\n",
+ "\n",
+ "While Minterpy handles this scaling automatically, this guide demonstrates how the integration scaling factor can be computed directly from a `Domain` instance."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Four-dimensional rectangular domain\n",
+ "\n",
+ "Create a four-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [0.0, 5.0] \\times [10.0, 15.0] \\times [5.0, 10.0] \\times [3.0, 7.0]\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea68fa6c-daf4-41af-ae87-de8f71dc76da",
+ "metadata": {},
+ "source": [
+ "## Domain instance\n",
+ "\n",
+ "The rectangular domain is defined four pair of bounds, each of which forms and interval for a dimension. We can specify them in a single two-dimensional array of two rows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1254453-8c4e-439d-8fd7-b5378e20b1f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bounds = np.array([\n",
+ " [0.0, 5.0],\n",
+ " [10.0, 15.0],\n",
+ " [5.0, 10.0],\n",
+ " [3.0, 7.0],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "An instance of `Domain` given an array of bounds can be constructed using the default constructor:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is indeed four dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "## Integration factor\n",
+ "\n",
+ "The integration factor can be computed via the `int_factor()` method. The method does not take any arguments and assumes that the integration is carried out over all dimensions. Due to the affine separable transformation, the Jacobian of the transformation is diagonal and its determinant reduces to a product, giving the integration factor:\n",
+ "\n",
+ "$$\n",
+ "c = \\prod_{i = 1}^m \\frac{b_i - a_i}{1 - (-1)} = \\prod_{i = 1}^m \\frac{b_i - a_i}{2}.\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d9d27795-eb63-48aa-ba0a-54338d3e86cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.int_factor())"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-create-bounds.ipynb b/docs/how-to/domain/domain-create-bounds.ipynb
new file mode 100644
index 00000000..7b7e3615
--- /dev/null
+++ b/docs/how-to/domain/domain-create-bounds.ipynb
@@ -0,0 +1,223 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Create a Domain with Bounds"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Approximating a function $f: \\Omega \\rightarrow \\mathbb{R}$ where $\\Omega$ is a custom hyper-rectangular domain in Minterpy involves constructing an (interpolating) polynomial $Q$ such that:\n",
+ "\n",
+ "$$\n",
+ "f(\\boldsymbol{x}) \\approx Q \\circ \\mathcal{T} (\\boldsymbol{x}),\n",
+ "$$\n",
+ "\n",
+ "where $Q: [-1, 1]^m \\rightarrow \\mathbb{R}$ is the polynomial defined on the internal (reference) domain and $\\mathcal{T}$ is the affine separable transformation from $\\Omega$ to $[-1, 1]^m$. Both the user-defined domain and the internal domain are encapsulated in the `Domain` class.\n",
+ "\n",
+ "An instance of `Domain` can be constructed via different constructors:\n",
+ "\n",
+ "- {py:class}`.Domain`: create an instance by specifying bounds for each dimension (this page)\n",
+ "- {py:meth}`.Domain.uniform`: create an instance with uniform bounds (see the {doc}`example `)\n",
+ "- {py:meth}`.Domain.identity`: create an instance with bounds identical to the internal domain (see the {doc}`example `)\n",
+ "\n",
+ "In this guide, you will construct a `Domain` instance using the default constructor."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85fd616a-78b2-4218-b94d-dd07d7500d25",
+ "metadata": {},
+ "source": [
+ "## About the default constructor\n",
+ "\n",
+ "The default constructor returns an instance of `Domain` based on a given set of bounds for each spatial dimension. Specifically, it accepts the bounds as a two-dimensional array of shape `(m, 2)` where `m` is the number of dimensions. Each row of the array corresponds to the interval of a dimension defined by the lower and upper bounds. The bounds must be finite real numbers with the lower bound strictly smaller than the upper bound."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Two-dimensional rectangular domain\n",
+ "\n",
+ "Create a two-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [0.0, 5.0] \\times [10.0, 15.0]\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea68fa6c-daf4-41af-ae87-de8f71dc76da",
+ "metadata": {},
+ "source": [
+ "## Bounds\n",
+ "\n",
+ "The rectangular domain is defined by two bounds (intervals) one for each dimension. We can specify them in a single two-dimensional array of two rows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1254453-8c4e-439d-8fd7-b5378e20b1f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bounds = np.array([\n",
+ " [0.0, 5.0],\n",
+ " [10.0, 15.0],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "### Domain instance\n",
+ "\n",
+ "An instance of `Domain` given an array of bounds can be constructed using the default constructor:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5d53d6bc-64cd-46a4-bfdd-38a46f41c163",
+ "metadata": {},
+ "source": [
+ "### Spatial dimension"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is two dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "082b1501-566a-42e9-8987-64c02c27eca1",
+ "metadata": {},
+ "source": [
+ "### Uniform domain\n",
+ "\n",
+ "A domain is a uniform domain if it has identical bounds across all dimensions ($[a, b]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c5a1a44c-ccf4-400f-a44c-bb4f10294d6b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_uniform"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "### Identity domain"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f2a1ec1-edd4-4c59-8b48-cd475c8d941d",
+ "metadata": {},
+ "source": [
+ "A domain is an identity domain if the user-defined bounds match the internal reference domain ($[-1, 1]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "834ae49f-96a3-43f7-bfc1-74814495d68e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_identity"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-create-identity.ipynb b/docs/how-to/domain/domain-create-identity.ipynb
new file mode 100644
index 00000000..bfc08c47
--- /dev/null
+++ b/docs/how-to/domain/domain-create-identity.ipynb
@@ -0,0 +1,211 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Create a Domain with Internal Bounds"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Approximating a function $f: \\Omega \\rightarrow \\mathbb{R}$ where $\\Omega$ is a custom hyper-rectangular domain in Minterpy involves constructing an (interpolating) polynomial $Q$ such that:\n",
+ "\n",
+ "$$\n",
+ "f(\\boldsymbol{x}) \\approx Q \\circ \\mathcal{T} (\\boldsymbol{x}),\n",
+ "$$\n",
+ "\n",
+ "where $Q: [-1, 1]^m \\rightarrow \\mathbb{R}$ is the polynomial defined on the internal (reference) domain and $\\mathcal{T}$ is the affine separable transformation from $\\Omega$ to $[-1, 1]^m$. Both the user-defined domain and the internal domain are encapsulated in the `Domain` class.\n",
+ "\n",
+ "An instance of `Domain` can be constructed via different constructors:\n",
+ "\n",
+ "- {py:class}`.Domain`: create an instance by specifying bounds for each dimension (see the {doc}`example `)\n",
+ "- {py:meth}`.Domain.uniform`: create an instance with uniform bounds (see the {doc}`example `)\n",
+ "- {py:meth}`.Domain.identity`: create an instance with bounds identical to the internal domain (this page)\n",
+ "\n",
+ "In this guide, you will construct a `Domain` instance using the factory method {py:meth}`.Domain.identity`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85fd616a-78b2-4218-b94d-dd07d7500d25",
+ "metadata": {},
+ "source": [
+ "## About the factory method `identity()`\n",
+ "\n",
+ "The factory method {py:meth}`.Domain.identity` constructs a `Domain` instance with bounds corresponds to the internal domain (currently $[-1, 1]^m$), given a spatial dimension `m`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Three-dimensional rectangular domain\n",
+ "\n",
+ "Create a three-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [-1.0, 1.0]^3\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "### Domain instance\n",
+ "\n",
+ "An instance of `Domain` with bounds identical to the internal domain can be constructed using the factory method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "spatial_dimension = 3\n",
+ "dom = mp.Domain.identity(spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5d53d6bc-64cd-46a4-bfdd-38a46f41c163",
+ "metadata": {},
+ "source": [
+ "### Spatial dimension"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is three dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "082b1501-566a-42e9-8987-64c02c27eca1",
+ "metadata": {},
+ "source": [
+ "### Uniform domain\n",
+ "\n",
+ "A domain is a uniform domain if it has identical bounds across all dimensions ($[a, b]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c5a1a44c-ccf4-400f-a44c-bb4f10294d6b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_uniform"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "### Identity domain"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f2a1ec1-edd4-4c59-8b48-cd475c8d941d",
+ "metadata": {},
+ "source": [
+ "A domain is an identity domain if the user-defined bounds match the internal reference domain ($[-1, 1]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "834ae49f-96a3-43f7-bfc1-74814495d68e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_identity"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a3fb744-26c9-40d2-bd57-3e74ae1ef5fd",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "An identity domain (currently $[-1, 1]^m$) is by construction a uniform domain.\n",
+ "```"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-create-uniform.ipynb b/docs/how-to/domain/domain-create-uniform.ipynb
new file mode 100644
index 00000000..e33a53f6
--- /dev/null
+++ b/docs/how-to/domain/domain-create-uniform.ipynb
@@ -0,0 +1,203 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Create a Domain with Uniform Bounds"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Approximating a function $f: \\Omega \\rightarrow \\mathbb{R}$ where $\\Omega$ is a custom hyper-rectangular domain in Minterpy involves constructing an (interpolating) polynomial $Q$ such that:\n",
+ "\n",
+ "$$\n",
+ "f(\\boldsymbol{x}) \\approx Q \\circ \\mathcal{T} (\\boldsymbol{x}),\n",
+ "$$\n",
+ "\n",
+ "where $Q: [-1, 1]^m \\rightarrow \\mathbb{R}$ is the polynomial defined on the internal (reference) domain and $\\mathcal{T}$ is the affine separable transformation from $\\Omega$ to $[-1, 1]^m$. Both the user-defined domain and the internal domain are encapsulated in the `Domain` class.\n",
+ "\n",
+ "An instance of `Domain` can be constructed via different constructors:\n",
+ "\n",
+ "- {py:class}`.Domain`: create an instance by specifying bounds for each dimension (see the {doc}`example `)\n",
+ "- {py:meth}`.Domain.uniform`: create an instance with uniform bounds (this page)\n",
+ "- {py:meth}`.Domain.identity`: create an instance with bounds identical to the internal domain (see the {doc}`example `)\n",
+ "\n",
+ "In this guide, you will construct a `Domain` instance with uniform bounds using the factory method {py:meth}`.Domain.uniform`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85fd616a-78b2-4218-b94d-dd07d7500d25",
+ "metadata": {},
+ "source": [
+ "## About the factory method `uniform()`\n",
+ "\n",
+ "The factory method {py:meth}`.Domain.uniform` constructs a `Domain` instance with identical bounds `[a, b]` applied across all dimensions, given a spatial dimension `m`, a lower bound `a`, and an upper bound `b`. The lower bound must be strictly less than the upper bound."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Six-dimensional rectangular domain\n",
+ "\n",
+ "Create a six-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [1.0, 5.0]^6\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "### Domain instance\n",
+ "\n",
+ "An instance of `Domain` of a given dimension with a uniform bound can be constructed using the factory method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "spatial_dimension = 6\n",
+ "lower = 1.0\n",
+ "upper = 5.0\n",
+ "dom = mp.Domain.uniform(spatial_dimension, lower, upper)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5d53d6bc-64cd-46a4-bfdd-38a46f41c163",
+ "metadata": {},
+ "source": [
+ "### Spatial dimension"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is six dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "082b1501-566a-42e9-8987-64c02c27eca1",
+ "metadata": {},
+ "source": [
+ "### Uniform domain\n",
+ "\n",
+ "A domain is a uniform domain if it has identical bounds across all dimensions ($[a, b]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c5a1a44c-ccf4-400f-a44c-bb4f10294d6b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_uniform"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "### Identity domain"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f2a1ec1-edd4-4c59-8b48-cd475c8d941d",
+ "metadata": {},
+ "source": [
+ "A domain is an identity domain if the user-defined bounds match the internal reference domain ($[-1, 1]^m$). This can be verified for the current instance using the following property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "834ae49f-96a3-43f7-bfc1-74814495d68e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.is_identity"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-create-union.ipynb b/docs/how-to/domain/domain-create-union.ipynb
new file mode 100644
index 00000000..87d9c4f7
--- /dev/null
+++ b/docs/how-to/domain/domain-create-union.ipynb
@@ -0,0 +1,196 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Create the Union of Domain Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f30c0b94-4903-40b1-ba17-43cab297d59b",
+ "metadata": {},
+ "source": [
+ "This guide demonstrates how to take the union of `Domain` instances and the expected outcome of such an operation. The union between to instances of `Domain` may be obtained via the `|` operator.\n",
+ "\n",
+ "The union is possible if and only if the two instances are {doc}`partially matching `. If they are, the result is a new instance equal in value to the higher-dimensional operand. If they are not partially matching, a `DomainMismatchError` is raised."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "351c97a5-4b37-4bf3-81e7-655eae253cd5",
+ "metadata": {},
+ "source": [
+ "## Example: Union of two equal domains\n",
+ "\n",
+ "The two domains below are equal:\n",
+ "\n",
+ "- $\\Omega_1 = [1, 2] \\times [3, 4]$\n",
+ "- $\\Omega_2 = [1, 2] \\times [3, 4]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1247c129-01a3-4a26-b094-974ec8f493f7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 = mp.Domain(np.array([[1, 2], [3, 4]]))\n",
+ "dom_2 = mp.Domain(np.array([[1, 2], [3, 4]]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85125027-79f1-4e8c-9ab5-35dc1e9a8907",
+ "metadata": {},
+ "source": [
+ "Their union is a new instance equal in value to both operands"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6e3b3d46-ee3f-4e75-8d09-7e3d9aeebbae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_union = dom_1 | dom_2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "469a5c71-1601-4bba-90d8-ece557a9bf2d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 == dom_union, dom_2 == dom_union # (True, True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7a0a9f5-781e-4283-be26-dce952f0462f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 is dom_union, dom_2 is dom_union # (False, False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20a74b3d-402e-422d-bfa9-db94eae056b9",
+ "metadata": {},
+ "source": [
+ "**Exception**: When an instance is unioned with itself, it returns the same object rather creating a new one:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "384751d9-95f9-4f86-a810-c8980a2eb124",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_1 is dom_1 | dom_1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5692b48d-8481-430f-aa0e-c31dd342479c",
+ "metadata": {},
+ "source": [
+ "## Example: Union of unequal, but partially matching domains\n",
+ "\n",
+ "The two domains below are unequal but partially matching:\n",
+ "\n",
+ "- $\\Omega_3 = [1, 2] \\times [4, 5]$\n",
+ "- $\\Omega_4 = [1, 2] \\times [4, 5] \\times [0, 1]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9ea4963d-83b1-4f69-b4f7-cfdeb4cee351",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 = mp.Domain(np.array([[1, 2], [4, 5]]))\n",
+ "dom_4 = mp.Domain(np.array([[1, 2], [4, 5], [0, 1]]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d31f435e-09f9-4289-aa75-a95c4c1ca138",
+ "metadata": {},
+ "source": [
+ "Their union is a new instance equal in value to the higher-dimensional operand:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "98c54ed9-6c14-47ac-a5c7-072040c5e0f2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 | dom_4 == dom_4 # True"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "70794f43-4433-4bb0-ae3b-3dc38c173c37",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom_3 | dom_4 is dom_4 # False"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7bd5d763-7a3d-4c3a-b667-f6edbceca038",
+ "metadata": {},
+ "source": [
+ "## Non-partially-matching domains\n",
+ "\n",
+ "If the two instances are note partially matching, the `|` operator raises a `DomainMismatchError`. For example, a domain $[-1, 1]$ and a domain $[0, 1] \\times [0, 1]$ are not partially matching because the first dimension bounds differ such that their union cannot be formed."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/domain-map-to-from.ipynb b/docs/how-to/domain/domain-map-to-from.ipynb
new file mode 100644
index 00000000..dec0183d
--- /dev/null
+++ b/docs/how-to/domain/domain-map-to-from.ipynb
@@ -0,0 +1,251 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
+ "metadata": {},
+ "source": [
+ "# Map Values from and to Internal Domain"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b86f737a-bdc7-432e-ab3f-939491391220",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import minterpy as mp\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81abe18-3ed4-4925-a09e-bdd95e4b0880",
+ "metadata": {},
+ "source": [
+ "Once a `Domain` instance is constructed, it can be used to map (i.e., _transform_) values between the user-defined domain $\\Omega$ and the internal reference domain\n",
+ "(currently $[-1, 1]^m$) in both directions.\n",
+ "\n",
+ "This guide demonstrates how to carry out such mapping."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c376d436-9c3d-46c6-bb5a-a026e9749ca2",
+ "metadata": {},
+ "source": [
+ "## Example: Three-dimensional rectangular domain\n",
+ "\n",
+ "Create a three-dimensional rectangular domain that corresponds to:\n",
+ "\n",
+ "$$\n",
+ "\\Omega = [0.0, 5.0] \\times [10.0, 15.0] \\times [0.5, 3.0]\n",
+ "$$\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea68fa6c-daf4-41af-ae87-de8f71dc76da",
+ "metadata": {},
+ "source": [
+ "### Domain instance\n",
+ "\n",
+ "The rectangular domain is defined by three pairs of bounds (each forms an interval) one for each dimension. We can specify them in a single two-dimensional array of three rows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e1254453-8c4e-439d-8fd7-b5378e20b1f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bounds = np.array([\n",
+ " [0.0, 5.0],\n",
+ " [10.0, 15.0],\n",
+ " [0.5, 3.0],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5c0a4a8-9fa3-4c62-b38a-609533d0f116",
+ "metadata": {},
+ "source": [
+ "An instance of `Domain` given an array of bounds can be constructed using the default constructor:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "192b538b-a494-4dd0-a0a9-d6dc18e5cd64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom = mp.Domain(bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d227c41-5618-4148-bb93-dc048cbcf713",
+ "metadata": {},
+ "source": [
+ "We can verify that the domain is three dimensional:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7ebf254-c0cf-4078-a293-0a4908248608",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.spatial_dimension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0b9a2d0-f581-4c58-8d6d-6c5c9a9b1a09",
+ "metadata": {},
+ "source": [
+ "and with the appropriate bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "571abc8a-6e75-4434-83e1-e9fc76445dd7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(dom.bounds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a71550a8-9958-4926-a524-a4097cd62b89",
+ "metadata": {},
+ "source": [
+ "## Map values from the internal domain\n",
+ "\n",
+ "We can use the instance to transform a set of values in the internal domain to the corresponding values in the user domain. For instance, consider the set of values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5665014b-8f94-4ef6-a944-6b06ea2864d9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xx = np.array([\n",
+ " [-1.0, 1.0, 0.5],\n",
+ " [0.5, 0.5, 0.5],\n",
+ " [0.25, 0.75, -0.75],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f509fe2d-af7f-46b5-9abb-be8aad4c4019",
+ "metadata": {},
+ "source": [
+ "in which each row corresponds to a point in the three dimensional space in the internal domain. The corresponding values in the user domain"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ba34964-fb89-4c3d-9067-fe4cd31a792e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.map_from_internal(xx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "46b30565-ae64-4687-9ffa-a37456476a73",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Such a transformation is required, for instance, when evaluating the function of interest at the unisolvent nodes, which Minterpy provides in the internal reference domain (currently $[-1, 1]^m$).\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2b442701-477b-48dd-8eb8-131432ac70b7",
+ "metadata": {},
+ "source": [
+ "## Map values to the internal domain\n",
+ "\n",
+ "We can also use the instance to transform a set of values in the user domain to the corresponding values in the internal domain. For instance, consider the set of values in the user domain"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "60f79a2f-0755-47f4-8ac7-d8ad912c338b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xx = np.array([\n",
+ " [0.5, 11.0, 1.75],\n",
+ " [1.0, 14.0, 2.5],\n",
+ " [0, 15, 0.5],\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2f13f31f-e9dd-4518-a019-b373e4c63227",
+ "metadata": {},
+ "source": [
+ "in which each row corresponds to a point in the three-dimensional space in the user domain. The corresponding values in the internal domain can be obtained with"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f754c32f-0dee-4586-99eb-10bc31926333",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dom.map_to_internal(xx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0bde684e-0428-4c54-a1fa-7fd210b755a5",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Such a transformation is required, for instance, when evaluating the internal polynomial representation at the points defined in the user domain.\n",
+ "```"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.19"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/how-to/domain/index.rst b/docs/how-to/domain/index.rst
new file mode 100644
index 00000000..56013c54
--- /dev/null
+++ b/docs/how-to/domain/index.rst
@@ -0,0 +1,19 @@
+######
+Domain
+######
+
+This page lists all the available How-To Guides related to instance of the :py:class:`.Domain` class.
+
+.. toctree::
+ :maxdepth: 1
+
+ Create with Bounds
+ Create with Uniform Bounds
+ Create with Internal Bounds
+ Map Values from and to Internal Domain
+ Compute Integration Scaling Factor
+ Compute Differentiation Scaling Factor
+ Check whether Points Lie within the Domain
+ Check for Equality in Value
+ Check for Partial Matching
+ Create the Union of Domain
diff --git a/docs/how-to/grid/grid-call.ipynb b/docs/how-to/grid/grid-call.ipynb
index 1ddecc92..6c3894fd 100644
--- a/docs/how-to/grid/grid-call.ipynb
+++ b/docs/how-to/grid/grid-call.ipynb
@@ -1,38 +1,38 @@
{
"cells": [
{
- "metadata": {},
"cell_type": "markdown",
- "source": "# Evaluate a Function on a Grid",
- "id": "5223970d3d19e2f7"
+ "id": "5223970d3d19e2f7",
+ "metadata": {},
+ "source": [
+ "# Evaluate a Function on a Grid"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "d32b739de18f3508",
+ "metadata": {},
+ "outputs": [],
"source": [
"import minterpy as mp\n",
"import numpy as np"
- ],
- "id": "d32b739de18f3508",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "aeee3be84ec26284",
+ "metadata": {},
"source": [
- "Calling an instance of `Grid` with a function or a `Callable` evaluates\n",
- "the given function on the unisolvent nodes and returns the corresponding\n",
- "function values. In the context of polynomial interpolation, these function\n",
- "values are the coefficients of a polynomial in the Lagrange basis.\n",
+ "Calling an instance of `Grid` with a function or a `Callable` evaluates the given function on the unisolvent nodes transformed to the grid's domain and returns the corresponding function values. In the context of polynomial interpolation, these function values are the coefficients of a polynomial in the Lagrange basis. If no domain is specified, the default domain $[-1, 1]^m$ is used and no transformation takes place.\n",
"\n",
"This guide demonstrates how to call an instance of `Grid` on a function."
- ],
- "id": "aeee3be84ec26284"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "9882a42c341e5e4e",
+ "metadata": {},
"source": [
"## Example: Function with one-dimensional output\n",
"\n",
@@ -46,12 +46,12 @@
"\n",
"on the interpolation grid that corresponds to a complete multi-index set of\n",
"polynomial degree $3$ with respect to the $l_p$-degree $2.0$. "
- ],
- "id": "9882a42c341e5e4e"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "2b8c0abb1154afb9",
+ "metadata": {},
"source": [
"### Function to evaluate\n",
"\n",
@@ -67,92 +67,96 @@
"or keyword.\n",
"\n",
"The function as required above can be defined as follows:"
- ],
- "id": "2b8c0abb1154afb9"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "2f23b787c2fe8c22",
+ "metadata": {},
+ "outputs": [],
"source": [
"def fun_one_dim(xx: np.ndarray) -> np.ndarray:\n",
- " \"\"\"Compute the sum of squared.\"\"\"\n",
+ " \"\"\"Compute the sum of squares.\"\"\"\n",
" return np.sum(xx**2, axis=1)"
- ],
- "id": "2f23b787c2fe8c22",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "b7887f75656036e1",
+ "metadata": {},
"source": [
"### Grid\n",
"\n",
"The interpolation grid that corresponds to the complete multi-index set\n",
"can be created using `from_degree()` factory method:"
- ],
- "id": "b7887f75656036e1"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "5423c818eaed954e",
+ "metadata": {},
+ "outputs": [],
"source": [
"spatial_dimension = 3\n",
- "poly_degree = 3\n",
+ "poly_degree = 2\n",
"lp_degree = 2.0\n",
"grd = mp.Grid.from_degree(spatial_dimension, poly_degree, lp_degree)"
- ],
- "id": "5423c818eaed954e",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "d003c89440503b74",
+ "metadata": {},
"source": [
"### Function values on the grid\n",
"\n",
- "By calling the `Grid` instance on the function defined above, we evaluate\n",
- "the function on the unisolvent nodes of the grid:"
- ],
- "id": "d003c89440503b74"
+ "By calling the `Grid` instance on the function defined above, we evaluate the function on the unisolvent nodes transformed to the domain of the grid:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "fun_values = grd(fun_one_dim)",
+ "execution_count": null,
"id": "6ad1d0d7058539a7",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "fun_values = grd(fun_one_dim)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "fun_values",
+ "execution_count": null,
"id": "21bb8e646d1ae1a4",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "fun_values"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "3c67fa94-b2df-4979-9113-12c3a0e6a644",
+ "metadata": {},
"source": [
- "As expected, these values are the same values from evaluating the given function\n",
- "on the unisolvent nodes:"
- ],
- "id": "889aa4320e90604f"
+ "Because the domain of the grid is the default domain $[-1, 1]^m$, there is no transformation involved and the above values are the same as evaluating the function directly on the unisolvent nodes of the grid:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "fun_one_dim(grd.unisolvent_nodes)",
- "id": "9354556474471ec8",
+ "execution_count": null,
+ "id": "2fd4310b-82f4-4563-8330-ef549f6c64ae",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "fun_one_dim(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "71a6221aba37affd",
+ "metadata": {},
"source": [
"## Example: Function with multi-dimensional output\n",
"\n",
@@ -169,19 +173,19 @@
"$$\n",
"\\begin{aligned}\n",
"f_1(\\boldsymbol{x}) & = \\sum_{i = 1}^{3} x_i^2,\\\\\n",
- "f_1(\\boldsymbol{x}) & = \\prod_{i = 1}^{3} x_i^2,\n",
+ "f_2(\\boldsymbol{x}) & = \\prod_{i = 1}^{3} x_i^2,\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"on the same interpolation grid as before.\n",
"\n",
"Notice that the function now returns two outputs per input value."
- ],
- "id": "71a6221aba37affd"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "9f8917e774d22024",
+ "metadata": {},
"source": [
"The function or callable passed to the `Grid` instance may also return \n",
"multiple outputs. The function must be defined such that it returns an array \n",
@@ -189,12 +193,14 @@
"\n",
"\n",
"The required function can therefore be defined as follows:"
- ],
- "id": "9f8917e774d22024"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "1f220138fc59421",
+ "metadata": {},
+ "outputs": [],
"source": [
"def fun_two_dim(xx: np.ndarray) -> np.ndarray:\n",
" \"\"\"Return the sum and product of squared.\"\"\"\n",
@@ -204,131 +210,232 @@
" yy[:, 1] = np.prod(xx**2, axis=1)\n",
" \n",
" return yy"
- ],
- "id": "1f220138fc59421",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "32b066c1caa57773",
+ "metadata": {},
"source": [
"The previous instance of `Grid` can be directly used to obtain the values\n",
"of the multiple-output function:"
- ],
- "id": "32b066c1caa57773"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd(fun_two_dim)",
+ "execution_count": null,
"id": "204a1fde2ba35f34",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd(fun_two_dim)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "25d39b5f53446e96",
+ "metadata": {},
"source": [
"As expected, calling the `Grid` instance with the function returns a two-dimensional\n",
- "array whose each column corresponds to each outputs and each row corresponds to each unisolvent nodes."
- ],
- "id": "25d39b5f53446e96"
+ "array whose each column corresponds to a different output and each row corresponds to a different unisolvent node."
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "## Example: Function with additional arguments",
- "id": "37f16c72892dec46"
+ "id": "37f16c72892dec46",
+ "metadata": {},
+ "source": [
+ "## Example: Function with additional arguments"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "953e870db5b16a3d",
+ "metadata": {},
"source": [
"While the function passed to a `Grid` instance must take as its first\n",
"argument a two-dimensional array, additional arguments may also be passed\n",
"to the function by passing positional and keyword arguments to the call.\n",
"\n",
"For instance, suppose the function to be evaluated is defined as follows:"
- ],
- "id": "953e870db5b16a3d"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "76a29ffab6728dc",
+ "metadata": {},
+ "outputs": [],
"source": [
"def fun_with_args(xx: np.ndarray, p: float) -> np.ndarray:\n",
" \"\"\"Return the row-wise lp-norm.\"\"\"\n",
" return np.sum(np.abs(xx**p), axis=1)**(1/p)"
- ],
- "id": "76a29ffab6728dc",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "c79e415a72686bd3",
+ "metadata": {},
"source": [
"To change the behavior of the function call via one of its argument\n",
"when the function is evaluated on the grid, pass the additional arguments\n",
"to the call to the `Grid` instance."
- ],
- "id": "c79e415a72686bd3"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "For instance, with the additional argument to `fun_with_args()` as a positional argument:",
- "id": "3814d67a935e699d"
+ "id": "3814d67a935e699d",
+ "metadata": {},
+ "source": [
+ "For instance, with the additional argument to `fun_with_args()` as a positional argument:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd(fun_with_args, 1.0)",
+ "execution_count": null,
"id": "5ebaf4167d2f639a",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd(fun_with_args, 1.0)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd(fun_with_args, 2.0)",
+ "execution_count": null,
"id": "308c1f2cbeaaf680",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd(fun_with_args, 2.0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b81880175b82b934",
+ "metadata": {},
+ "source": [
+ "...and as a keyword argument:\n"
+ ]
},
{
+ "cell_type": "markdown",
+ "id": "e8705dfacb4345a2",
"metadata": {},
+ "source": [
+ "grd(fun_with_args, p=3.0)"
+ ]
+ },
+ {
"cell_type": "markdown",
- "source": "...and as a keyword argument:\n",
- "id": "b81880175b82b934"
+ "id": "50a320ed-df42-4515-9a59-1ddbd49a85c0",
+ "metadata": {},
+ "source": [
+ "## Example: Function on a custom rectangular domain\n",
+ "\n",
+ "Consider now the same function defined on a custom rectangular domain:\n",
+ "\n",
+ "$$\n",
+ "f(\\boldsymbol{x}) = \\sum_{i = 1}^3 x_i^2, \\; \\boldsymbol{x} = [0, 1] \\times [1, 2] \\times [-1, 1].\n",
+ "$$"
+ ]
},
{
+ "cell_type": "markdown",
+ "id": "2d4c9ce1-8d9d-48e6-a06e-65359b855267",
"metadata": {},
+ "source": [
+ "The function definition remains unchanged but now a custom domain must be defined:"
+ ]
+ },
+ {
"cell_type": "code",
- "source": "grd(fun_with_args, p=3.0)",
- "id": "e8705dfacb4345a2",
+ "execution_count": null,
+ "id": "a1d222b5-b5a7-4178-b889-88def92a8ae0",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "dom = mp.Domain(\n",
+ " np.array([\n",
+ " [0, 1],\n",
+ " [1, 2],\n",
+ " [-1, 1],\n",
+ " ])\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1369adcf-615e-4d0d-99b4-c6b9d6dfa04e",
+ "metadata": {},
+ "source": [
+ "and passed to the grid:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8cbd7aa9-9028-49b8-a779-f8f20361e58d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "grd_custom_domain = mp.Grid.from_degree(spatial_dimension, poly_degree, lp_degree, domain=dom)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e7931862-3dd1-4668-8ac2-e9504b689b61",
+ "metadata": {},
+ "source": [
+ "By calling the `Grid` instance on the function defined above, we evaluate the function on the unisolvent nodes of the grid transformed to the domain of the function:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d584886e-1554-4982-918e-47282491e62e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "grd_custom_domain(fun_one_dim)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "57f01ecc-adcc-4294-a2fa-bc9cd0208759",
+ "metadata": {},
+ "source": [
+ "These values are the same as:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0637abb4-563c-412c-93db-107196d4a9c6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fun_one_dim(dom.map_from_internal(grd_custom_domain.unisolvent_nodes))"
+ ]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/grid/grid-create-default-constructor.ipynb b/docs/how-to/grid/grid-create-default-constructor.ipynb
index 67f8e781..a91e0dac 100644
--- a/docs/how-to/grid/grid-create-default-constructor.ipynb
+++ b/docs/how-to/grid/grid-create-default-constructor.ipynb
@@ -4,19 +4,21 @@
"cell_type": "markdown",
"id": "d6a11f9a-d895-4cc7-befa-bac340b8915d",
"metadata": {},
- "source": "# Create a Grid using the Default Constructor"
+ "source": [
+ "# Create a Grid using the Default Constructor"
+ ]
},
{
"cell_type": "code",
+ "execution_count": null,
"id": "b86f737a-bdc7-432e-ab3f-939491391220",
"metadata": {},
+ "outputs": [],
"source": [
"import minterpy as mp\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt"
- ],
- "outputs": [],
- "execution_count": null
+ ]
},
{
"cell_type": "markdown",
@@ -42,8 +44,9 @@
]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "3c9e532ec1ddc624",
+ "metadata": {},
"source": [
"## About the default constructor\n",
"\n",
@@ -56,7 +59,7 @@
"- `generating_function`: either a `Callable` or a string that selects a built-in function.\n",
" This argument is optional; if not specified, the Leja-ordered Chebyshev-Lobatto\n",
" generating function is selected by default. If a `Callable` is specified,\n",
- " it must satisfies several conditions as explained\n",
+ " it must satisfy several conditions as explained\n",
" in the {doc}`corresponding example `.\n",
"- `generating_points`: a two-dimensional array of floats that stores \n",
" the interpolation points per dimension (in each column of the array).\n",
@@ -64,6 +67,7 @@
" default and will be created on the fly from the generating function.\n",
" If specified, the array must satisfy several conditions as explained\n",
" in the {doc}`corresponding example `.\n",
+ "- `domain`: the user-defined domain of the interpolation grid. This argument is optional; if not specified, the domain defaults to the internal reference domain $[-1, 1]^m$. Regardless of the specified domain, however, generating points must lie in $[-1, 1]^m$ and any generating function must return values in $[-1, 1]^m$.\n",
"\n",
"The default constructor provides the most general way to create an instance\n",
"of `Grid`. However, when a specific argument is required and known in advance,\n",
@@ -75,12 +79,12 @@
"In practice, if `generating_function` is already specified, it is not necessary\n",
"to also explicitly specify `generating_points` as it becomes redundant.\n",
"```"
- ],
- "id": "3c9e532ec1ddc624"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "9ccf5d4b565e2097",
+ "metadata": {},
"source": [
"## Example: Two-dimensional interpolation grid\n",
"\n",
@@ -89,22 +93,24 @@
"- a two-dimensional multi-index set $A = \\{ (0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (0, 3) \\}$\n",
" with respect to $l_p$-degree of $2.0$, and\n",
"- equidistant generating points."
- ],
- "id": "9ccf5d4b565e2097"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "96ccbd6ed8d3a784",
+ "metadata": {},
"source": [
"### Multi-index set\n",
"\n",
"First, create an instance of `MultiIndexSet` following the above specification:"
- ],
- "id": "96ccbd6ed8d3a784"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "563408b592b9c311",
+ "metadata": {},
+ "outputs": [],
"source": [
"exponents = np.array([\n",
" [0, 0],\n",
@@ -119,40 +125,44 @@
" [1, 2],\n",
" [0, 3],\n",
"])"
- ],
- "id": "563408b592b9c311",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "mi = mp.MultiIndexSet(exponents, lp_degree=2.0)",
+ "execution_count": null,
"id": "518d515c92788210",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "mi = mp.MultiIndexSet(exponents, lp_degree=2.0)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(mi)",
+ "execution_count": null,
"id": "b5a31941b0530e50",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(mi)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "42b7bceef885c51a",
+ "metadata": {},
"source": [
"### Generating function\n",
"\n",
"The generating function for equidistant points can be defined as follows:"
- ],
- "id": "42b7bceef885c51a"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "b85829f53026e76e",
+ "metadata": {},
+ "outputs": [],
"source": [
"def equidistant_gen_function(\n",
" poly_degree: int,\n",
@@ -162,90 +172,108 @@
" xx = np.linspace(-1, 1, poly_degree + 1)[:, np.newaxis]\n",
" \n",
" return np.tile(xx, spatial_dimension)"
- ],
- "id": "b85829f53026e76e",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "34b375bb534333f0",
+ "metadata": {},
"source": [
"### Grid instance\n",
"\n",
"An instance of `Grid` given a multi-index set and an array of generating points\n",
"can be constructed via `from_points()` method as follows:"
- ],
- "id": "34b375bb534333f0"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd = mp.Grid(mi, generating_function=equidistant_gen_function)",
+ "execution_count": null,
"id": "d3fa6d6c6402c1d5",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd = mp.Grid(mi, generating_function=equidistant_gen_function)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The grid has the following unisolvent nodes:",
- "id": "f904358004f535e9"
+ "id": "f904358004f535e9",
+ "metadata": {},
+ "source": [
+ "The grid has the following unisolvent nodes:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(grd.unisolvent_nodes)",
+ "execution_count": null,
"id": "af07f4a74b40945e",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The two-dimensional interpolation grid is plotted below:",
- "id": "98df02f7174b43f4"
+ "id": "98df02f7174b43f4",
+ "metadata": {},
+ "source": [
+ "The two-dimensional interpolation grid is plotted below:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "eca700e1dd5b29b8",
+ "metadata": {},
+ "outputs": [],
"source": [
"plt.scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "eca700e1dd5b29b8",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "eee53bb5641dd731",
+ "metadata": {},
"source": [
"The interpolation grid can be compared with the grid having the same multi-index set\n",
"but with the Leja-ordered Chebyshev-Lobatto as the generating function (i.e., the default):"
- ],
- "id": "eee53bb5641dd731"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd_def = mp.Grid(mi)",
+ "execution_count": null,
"id": "e4d531fd251eed7",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd_def = mp.Grid(mi)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "1e75e47a9d979339",
+ "metadata": {},
+ "outputs": [],
"source": [
"plt.scatter(grd_def.unisolvent_nodes[:, 0], grd_def.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "1e75e47a9d979339",
- "outputs": [],
- "execution_count": null
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d0f1cdf6-ac4b-457a-badd-b5e02c5588b2",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Regardless of the specified domain, the unisolvent nodes are stored in the internal reference domain $[-1, 1]^m$. When the domain is not $[-1, 1]^m$, Minterpy automatically transforms the nodes to the specified domain before passing them to the function, for example, when calling\n",
+ "`grid(func)`.\n",
+ "```"
+ ]
}
],
"metadata": {
@@ -264,7 +292,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.18"
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/grid/grid-create-from-degree.ipynb b/docs/how-to/grid/grid-create-from-degree.ipynb
index e1713c83..325e76f1 100644
--- a/docs/how-to/grid/grid-create-from-degree.ipynb
+++ b/docs/how-to/grid/grid-create-from-degree.ipynb
@@ -1,26 +1,29 @@
{
"cells": [
{
- "metadata": {},
"cell_type": "markdown",
- "source": "# Create a Grid with a Complete Multi-Index Set",
- "id": "160078106f70cdaf"
+ "id": "160078106f70cdaf",
+ "metadata": {},
+ "source": [
+ "# Create a Grid with a Complete Multi-Index Set"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "d69f103857a35c1",
+ "metadata": {},
+ "outputs": [],
"source": [
"import numpy as np\n",
"import minterpy as mp\n",
"import matplotlib.pyplot as plt"
- ],
- "id": "d69f103857a35c1",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "96575b3d1eceacf0",
+ "metadata": {},
"source": [
"Minterpy interpolating polynomials (i.e., in the Lagrange or Newton basis)\n",
"lives on a grid that holds the so-called _unisolvent nodes_.\n",
@@ -38,12 +41,12 @@
"\n",
"This guide provides an example on how to construct a `Grid` instance with\n",
"a complete multi-index set using the `from_degree()` method."
- ],
- "id": "96575b3d1eceacf0"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "66a43bef3eec5873",
+ "metadata": {},
"source": [
"## About complete multi-index sets\n",
"\n",
@@ -54,12 +57,12 @@
"\n",
"A complete multi-index set is a typical way (but by no means, the only way)\n",
"to define a multi-index set of exponents in Minterpy."
- ],
- "id": "66a43bef3eec5873"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "630829f3a1dce754",
+ "metadata": {},
"source": [
"## About `from_degree()` factory method\n",
"\n",
@@ -83,17 +86,18 @@
" This argument is optional; if not specified, the points are generated\n",
" by the generating function as required. For detail regarding valid generating\n",
" points, see the {doc}`corresponding example `.\n",
+ "- `domain`: the user-defined domain of the interpolation grid. This argument is optional; if not specified, the domain defaults to the internal reference domain $[-1, 1]^m$. Regardless of the specified domain, however, generating points must lie in $[-1, 1]^m$ and any generating function must return values in $[-1, 1]^m$.\n",
"\n",
"The `from_degree()` method is a shortcut for constructing a grid that corresponds\n",
"to a complete multi-index set; it avoids the separate construction of a complete\n",
"`MultiIndexSet` instance that is then passed to the other constructors of the\n",
"`Grid` class."
- ],
- "id": "630829f3a1dce754"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "994375cb184334e7",
+ "metadata": {},
"source": [
"## Example: Two-dimensional interpolation grid\n",
"\n",
@@ -101,97 +105,115 @@
"defined by a complete multi-index set of polynomial degree $3$ with respect\n",
"to $l_p$-degree $\\infty$ and with Leja-ordered Chebyshev-Lobatto points as\n",
"the generating points. "
- ],
- "id": "994375cb184334e7"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "bdae42a7c1207dd9",
+ "metadata": {},
"source": [
"An instance of `Grid` that satisfies the above condition can be created using\n",
"`from_degree()` method as follows:"
- ],
- "id": "bdae42a7c1207dd9"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "ab2ead3058749fab",
+ "metadata": {},
+ "outputs": [],
"source": [
"grd = mp.Grid.from_degree(\n",
" spatial_dimension=2,\n",
" poly_degree=3,\n",
" lp_degree=np.inf,\n",
")"
- ],
- "id": "ab2ead3058749fab",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "175e378f45c8dc9c",
+ "metadata": {},
"source": [
"As the Leja-ordered Chebyshev-Lobatto points are the generating\n",
"function selected by default, no specification is required."
- ],
- "id": "175e378f45c8dc9c"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The grid has the following unisolvent nodes:",
- "id": "182f774f24ea80f0"
+ "id": "182f774f24ea80f0",
+ "metadata": {},
+ "source": [
+ "The grid has the following unisolvent nodes:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(grd.unisolvent_nodes)",
+ "execution_count": null,
"id": "abb43629d035920a",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The two-dimensional interpolation grid is plotted below:",
- "id": "54868f1e37d0b07"
+ "id": "54868f1e37d0b07",
+ "metadata": {},
+ "source": [
+ "The two-dimensional interpolation grid is plotted below:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "428c889c1af7b3a3",
+ "metadata": {},
+ "outputs": [],
"source": [
"plt.scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "428c889c1af7b3a3",
- "outputs": [],
- "execution_count": null
+ ]
},
{
+ "cell_type": "markdown",
+ "id": "2deaf1b4ceaa3472",
"metadata": {},
+ "source": [
+ "This interpolation grid corresponds to the full tensorial grid of two-dimensional polynomials with polynomial degree of $3$."
+ ]
+ },
+ {
"cell_type": "markdown",
- "source": "This interpolation grid corresponds to the full tensorial grid of two-dimensional polynomials with polynomial degree of $3$.",
- "id": "2deaf1b4ceaa3472"
+ "id": "abfae5eb-6f47-435a-900b-171ef98ddf45",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Regardless of the specified domain, the unisolvent nodes are stored in the internal reference domain $[-1, 1]^m$. When the domain is not $[-1, 1]^m$, Minterpy automatically transforms the nodes to the specified domain before passing them to the function, for example, when calling\n",
+ "`grid(func)`.\n",
+ "```"
+ ]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/grid/grid-create-from-function.ipynb b/docs/how-to/grid/grid-create-from-function.ipynb
index 27597ef8..d19c85f7 100644
--- a/docs/how-to/grid/grid-create-from-function.ipynb
+++ b/docs/how-to/grid/grid-create-from-function.ipynb
@@ -1,26 +1,29 @@
{
"cells": [
{
- "metadata": {},
"cell_type": "markdown",
- "source": "# Create a Grid with a Generating Function",
- "id": "2be84de1e0a3b90e"
+ "id": "2be84de1e0a3b90e",
+ "metadata": {},
+ "source": [
+ "# Create a Grid with a Generating Function"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": 1,
+ "id": "80d67622f636af48",
+ "metadata": {},
+ "outputs": [],
"source": [
"import numpy as np\n",
"import minterpy as mp\n",
"import matplotlib.pyplot as plt"
- ],
- "id": "80d67622f636af48",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "a83abcedf02e16e1",
+ "metadata": {},
"source": [
"Minterpy interpolating polynomials (i.e., in the Lagrange or Newton basis)\n",
"lives on a grid that holds the so-called _unisolvent nodes_.\n",
@@ -38,12 +41,12 @@
"\n",
"This guide provides an example on how to construct a `Grid` instance based\n",
"on a given generating function using the `from_function()` method."
- ],
- "id": "a83abcedf02e16e1"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "cf7e6168829e5e2b",
+ "metadata": {},
"source": [
"## About a generating function\n",
"\n",
@@ -60,62 +63,74 @@
" ...\n",
"```\n",
"\n",
- "It must return an array with the aforementioned shape and each column of the\n",
- "array must have unique values.\n",
- "\n",
- "Take for instance, the default generating function in Minterpy, i.e.,\n",
- "the Leja-ordered Chebyshev-Lobatto generating function.\n",
- "The function returns the following generating points in two dimensions\n",
- "with maximum polynomial degree of $3$ in every dimension:"
- ],
- "id": "cf7e6168829e5e2b"
+ "It must return an array with the aforementioned shape and each column of the array must have unique values. The values in the returned array must lie within $[-1, 1]$, that is, all generating points must be in the internal reference domain $[-1, 1]^m$."
+ ]
},
{
+ "cell_type": "markdown",
+ "id": "42bc2fbf-858f-4ac4-8f36-5916e720199f",
"metadata": {},
+ "source": [
+ "Take for instance, the default generating function in Minterpy, i.e., the Leja-ordered Chebyshev-Lobatto generating function. The function returns the following generating points in two dimensions\n",
+ "with maximum polynomial degree of $3$ in every dimension:"
+ ]
+ },
+ {
"cell_type": "code",
- "source": "mp.gen_points.gen_points_chebyshev(poly_degree=3, spatial_dimension=2)",
+ "execution_count": 2,
"id": "6b6bf1693e8f01a0",
- "outputs": [],
- "execution_count": null
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 1. , -1. ],\n",
+ " [-1. , 1. ],\n",
+ " [ 0.5, -0.5],\n",
+ " [-0.5, 0.5]])"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "mp.gen_points.gen_points_chebyshev(poly_degree=3, spatial_dimension=2)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "6041a18b159cac12",
+ "metadata": {},
"source": [
"Notice that the array has $4$ rows (i.e., $n + 1$, $n = 3$ as\n",
"one-dimensional polynomials of degree $3$ require $4$ points)\n",
"and $2$ columns (i.e., $m = 2$), and that each column has unique values."
- ],
- "id": "6041a18b159cac12"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "50c96a68e8c901e0",
+ "metadata": {},
"source": [
"## About the `from_function()` factory method\n",
"\n",
- "The method `from_function()` of the `Grid` class returns an instance of Grid\n",
- "based on a given generating function.\n",
+ "The method `from_function()` of the `Grid` class returns an instance of Grid based on a given generating function.\n",
"\n",
- "The method accepts two mandatory arguments, namely, the multi-index set\n",
- "of exponents that defines the polynomials that grid can support,\n",
- "and the generating function that creates generating points\n",
- "as required by the multi-index set.\n",
+ "The method accepts two mandatory arguments, namely, the multi-index set of exponents that defines the polynomials that grid can support, and the generating function that creates generating points as required by the multi-index set.\n",
"\n",
- "Any callable that is a valid generating function (see above) may be passed\n",
- "as the second argument to `from_function()`.\n",
- "Alternatively, a string as a key to a dictionary of built-in generating\n",
- "functions may be specified.\n",
+ "Any callable that is a valid generating function (see above) may be passed as the second argument to `from_function()`. Alternatively, a string as a key to a dictionary of built-in generating functions may be specified.\n",
"\n",
- "The `from_function()` method is a shortcut to create a grid with a given\n",
- "multi-index set and a particular generating function that is, possibly, defined\n",
- "by the users."
- ],
- "id": "50c96a68e8c901e0"
+ "The method also accepts an optional `domain` argument. If not specified the domain defaults to the internal reference domain $[-1, 1]^m$. Regardless of the specified domain, however, the generating function must always return values in $[-1, 1]^m$.\n",
+ "\n",
+ "The `from_function()` method is a shortcut to create a grid with a given multi-index set and a particular generating function that is, possibly, defined by the users."
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "f2e7ab16bd7b05cc",
+ "metadata": {},
"source": [
"## Example: Two-dimensional interpolation grid\n",
"\n",
@@ -123,22 +138,24 @@
"polynomials having a multi-index set \n",
"$A = \\{ (0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (0, 2), (1, 2), (0, 3), (0, 4) \\}$ \n",
"(defined with respect to $l_p$-degree $2.0$)."
- ],
- "id": "f2e7ab16bd7b05cc"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "6a35e4ca9db3b8b0",
+ "metadata": {},
"source": [
"### Multi-index set\n",
"\n",
"Create an instance of `MultiIndexSet` following the above specification:"
- ],
- "id": "6a35e4ca9db3b8b0"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": 6,
+ "id": "30bdd334f359d36a",
+ "metadata": {},
+ "outputs": [],
"source": [
"exponents = np.array([\n",
" [0, 0],\n",
@@ -152,40 +169,62 @@
" [0, 3],\n",
" [0, 4],\n",
"])"
- ],
- "id": "30bdd334f359d36a",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "mi = mp.MultiIndexSet(exponents, lp_degree=2.0)",
+ "execution_count": 7,
"id": "d3ce0ffcefd7a222",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "mi = mp.MultiIndexSet(exponents, lp_degree=2.0)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(mi)",
+ "execution_count": 8,
"id": "8d5412adddc4b0a1",
- "outputs": [],
- "execution_count": null
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "MultiIndexSet(m=2, n=4, p=2.0)\n",
+ "[[0 0]\n",
+ " [1 0]\n",
+ " [2 0]\n",
+ " [3 0]\n",
+ " [0 1]\n",
+ " [1 1]\n",
+ " [0 2]\n",
+ " [1 2]\n",
+ " [0 3]\n",
+ " [0 4]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(mi)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "725427e2a48988ea",
+ "metadata": {},
"source": [
"### Generating function\n",
"\n",
"The generating function for equidistant points can be defined as follows:"
- ],
- "id": "725427e2a48988ea"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": 9,
+ "id": "84e9c48050e35d8a",
+ "metadata": {},
+ "outputs": [],
"source": [
"def equidistant_gen_function(\n",
" poly_degree: int,\n",
@@ -195,98 +234,163 @@
" xx = np.linspace(-1, 1, poly_degree + 1)[:, np.newaxis]\n",
" \n",
" return np.tile(xx, spatial_dimension)"
- ],
- "id": "84e9c48050e35d8a",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "c059a88286b4fa53",
+ "metadata": {},
"source": [
"Given the function above, the generating points for polynomial degree $3$ in every dimension\n",
"and spatial dimension $3$ are:"
- ],
- "id": "c059a88286b4fa53"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(equidistant_gen_function(3, 3))",
+ "execution_count": 10,
"id": "2399d4f46cf1f248",
- "outputs": [],
- "execution_count": null
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[-1. -1. -1. ]\n",
+ " [-0.33333333 -0.33333333 -0.33333333]\n",
+ " [ 0.33333333 0.33333333 0.33333333]\n",
+ " [ 1. 1. 1. ]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(equidistant_gen_function(3, 3))"
+ ]
},
{
+ "cell_type": "markdown",
+ "id": "051ef6f5-5708-4410-8aa1-186fbe2c4f05",
"metadata": {},
+ "source": [
+ "Remember that these points must lie within $[-1, 1]^m."
+ ]
+ },
+ {
"cell_type": "markdown",
+ "id": "dd4ba554f203b33e",
+ "metadata": {},
"source": [
"### Grid instance\n",
"\n",
"Given the multi-index set and the generating function,\n",
"an instance of `Grid` can be constructed via the `from_function()` method\n",
"as follows:"
- ],
- "id": "dd4ba554f203b33e"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd = mp.Grid.from_function(mi, equidistant_gen_function)",
+ "execution_count": 11,
"id": "1fb54f2c699b50c4",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd = mp.Grid.from_function(mi, equidistant_gen_function)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The grid has the following unisolvent nodes:",
- "id": "513bc88db90b7505"
+ "id": "513bc88db90b7505",
+ "metadata": {},
+ "source": [
+ "The grid has the following unisolvent nodes:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(grd.unisolvent_nodes)",
+ "execution_count": 12,
"id": "a04b54f6e38e440",
- "outputs": [],
- "execution_count": null
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[-1. -1. ]\n",
+ " [-0.5 -1. ]\n",
+ " [ 0. -1. ]\n",
+ " [ 0.5 -1. ]\n",
+ " [-1. -0.5]\n",
+ " [-0.5 -0.5]\n",
+ " [-1. 0. ]\n",
+ " [-0.5 0. ]\n",
+ " [-1. 0.5]\n",
+ " [-1. 1. ]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "Finally, the two-dimensional interpolation grid is plotted below:",
- "id": "77a00b903229ac94"
+ "id": "77a00b903229ac94",
+ "metadata": {},
+ "source": [
+ "Finally, the two-dimensional interpolation grid is plotted below:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": 13,
+ "id": "a7c41c4712b66521",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAG2CAYAAABf1dN5AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMcNJREFUeJzt3QucjfW+x/HfzGDGbYaJMaYI2ZFyy2VIpbAZOeIcFeUSL7FTtIuKOTuE2oNsu5TN2crtpTbpINIZbJckwxRbUdi55TaXJDPGZTDznNfvv89aZxYzY/zNZa01n/fr9bTmeZ7/88x61jzW+va/rQDHcRwBAADADQm8seIAAAAgRAEAAFiiJgoAAMACIQoAAMACIQoAAMACIQoAAMACIQoAAMBCGZuDUDDZ2dly8uRJqVy5sgQEBPCyAQDgA3QKzbNnz0pUVJQEBuZd30SIKkIaoGrVqlWUvwIAABSRY8eOyW233ZbnfkJUEdIaKNcfITQ0tCh/FQAAKCTp6emmEsT1OZ4XQlQRcjXhaYAiRAEA4Fuu1xWHjuUAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWmLHcx2RlO5J4+LSknr0oEZVDpHXdcAkK5MuNAQAobn5RE7V582bp3r27+bZlnaJ9xYoV1z1m06ZNcu+990pwcLDUr19f5s+ff02ZmTNnSp06dSQkJESio6MlMTFRSlL8niS5f8oGeXLONvn94l3mUdd1OwAAKF5+EaLOnTsnTZs2NaGnIA4fPizdunWThx9+WHbt2iUvvviiPPPMM7JmzRp3mSVLlsjIkSNl/PjxsnPnTnP+Ll26SGpqqpQEDUrDFu2UpLSLHtuT0y6a7QQpAACKV4DjOI74Ea2JWr58ufTs2TPPMqNHj5bVq1fLnj173Nv69OkjZ86ckfj4eLOuNU+tWrWS9957z6xnZ2ebb3QeMWKEjBkzpsDfAh0WFiZpaWk39QXE2oSnNU5XBygXbcyLDAuRLaM70LQHAMBNKujnt1/URN2ohIQE6dSpk8c2rWXS7erSpUuyY8cOjzKBgYFm3VUmN5mZmeaFz7kUBu0DlVeAUpqCdb+WAwAAxaNUhqjk5GSpUaOGxzZd19Bz4cIFOXXqlGRlZeVaRo/NS1xcnEmurkVrrgqDdiIvzHIAAODmlcoQVVRiY2NN1Z9rOXbsWKGcV0fhFWY5AABw80rlFAeRkZGSkpLisU3Xtd2zfPnyEhQUZJbcyuixedGRfroUNp3GoGZYiOlE7uTTJ0rLAQCA4lEqa6Latm0r69ev99i2bt06s12VK1dOWrRo4VFGO5bruqtMcdJ5oMZ3b2R+vnpGKNe67me+KAAAio9fhKiMjAwzVYEurikM9OejR4+6m9kGDBjgLv/ss8/KoUOH5NVXX5V9+/bJX/7yF/n444/lpZdecpfR6Q3mzJkjCxYskL1798qwYcPMVAqDBg0qgSsUibmnpszqd6+pccpJ13W77gcAAMXHL5rzvvnmGzPnU84ApJ5++mkziWZSUpI7UKm6deuaKQ40NL3zzjty2223yfvvv29G6Ln07t1bfv75Zxk3bpzpTN6sWTMz/cHVnc2Lkwal3zaKZMZyAAC8gN/NE+VNCmueKAAAUHyYJwoAAKAI+UWfKAAAgOJGiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAALBAiAIAACjNIWrmzJlSp04dCQkJkejoaElMTMyz7EMPPSQBAQHXLN26dXOXGThw4DX7Y2JiiulqAACAtysjfmDJkiUycuRImT17tglQb7/9tnTp0kX2798vERER15RftmyZXLp0yb3+yy+/SNOmTeXxxx/3KKehad68ee714ODgIr4SAADgK/yiJmr69OkyZMgQGTRokDRq1MiEqQoVKsjcuXNzLR8eHi6RkZHuZd26dab81SFKQ1POclWrVi2mKwIAAN7O50OU1ijt2LFDOnXq5N4WGBho1hMSEgp0jg8++ED69OkjFStW9Ni+adMmU5PVoEEDGTZsmKmxyk9mZqakp6d7LAAAwD/5fIg6deqUZGVlSY0aNTy263pycvJ1j9e+U3v27JFnnnnmmqa8hQsXyvr162XKlCnyxRdfSNeuXc3vyktcXJyEhYW5l1q1at3ElQEAAG/mF32ibobWQjVu3Fhat27tsV1rplx0f5MmTeSOO+4wtVMdO3bM9VyxsbGmb5aL1kQRpAAA8E8+XxNVrVo1CQoKkpSUFI/tuq79mPJz7tw5Wbx4sQwePPi6v6devXrmdx04cCDPMtqHKjQ01GMBAAD+yedDVLly5aRFixam2c0lOzvbrLdt2zbfY5cuXWr6MfXr1++6v+f48eOmT1TNmjUL5XkDAADf5vMhSmkT2pw5c2TBggWyd+9e0wlca5l0tJ4aMGCAaWrLrSmvZ8+ecsstt3hsz8jIkFdeeUW2bdsmR44cMYGsR48eUr9+fTN1AgAAgF/0ierdu7f8/PPPMm7cONOZvFmzZhIfH+/ubH706FEzYi8nnUNqy5Ytsnbt2mvOp82D3333nQllZ86ckaioKOncubNMmjSJuaIAAIAR4DiO868fUdi0Y7mO0ktLS6N/FAAAfvb57RfNeQAAAMWNEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGChjM1BKDlZ2Y4kHj4tqWcvSkTlEGldN1yCAgP4kwAAUMz8piZq5syZUqdOHQkJCZHo6GhJTEzMs+z8+fMlICDAY9HjcnIcR8aNGyc1a9aU8uXLS6dOneTHH3+UkhS/J0nun7JBnpyzTX6/eJd51HXdDgAAipdfhKglS5bIyJEjZfz48bJz505p2rSpdOnSRVJTU/M8JjQ0VJKSktzLTz/95LF/6tSpMmPGDJk9e7Zs375dKlasaM558eJFKQkalIYt2ilJaZ6/PzntotlOkAIAoHj5RYiaPn26DBkyRAYNGiSNGjUywadChQoyd+7cPI/R2qfIyEj3UqNGDY9aqLfffltee+016dGjhzRp0kQWLlwoJ0+elBUrVkhJNOFNWPWDOLnsc23T/VoOAAAUD58PUZcuXZIdO3aY5jaXwMBAs56QkJDncRkZGXL77bdLrVq1TFD6/vvv3fsOHz4sycnJHucMCwszzYT5nTMzM1PS09M9lsKgfaCuroHKSaOT7tdyAACgePh8iDp16pRkZWV51CQpXdcglJsGDRqYWqpPP/1UFi1aJNnZ2XLffffJ8ePHzX7XcTdyThUXF2fClmvRgFYYtBN5YZYDAAA3z+dDlI22bdvKgAEDpFmzZtK+fXtZtmyZVK9eXf7rv/7rps4bGxsraWlp7uXYsWOF8nx1FF5hlgMAADfP50NUtWrVJCgoSFJSUjy267r2dSqIsmXLSvPmzeXAgQNm3XXcjZ4zODjYdFjPuRQGncagZliI5DWRgW7X/VoOAAAUD58PUeXKlZMWLVrI+vXr3du0eU7XtcapILQ5cPfu3WY6A1W3bl0TlnKeU/s36Si9gp6zMOk8UOO7NzI/Xx2kXOu6n/miAAAoPj4fopRObzBnzhxZsGCB7N27V4YNGybnzp0zo/WUNt1pU5vLxIkTZe3atXLo0CEzJUK/fv3MFAfPPPOMe+Teiy++KG+88YasXLnSBCw9R1RUlPTs2bNErjHmnpoyq9+9Ehnm2WSn67pd9wMAgOLjFzOW9+7dW37++WczOaZ2/Na+TvHx8e6O4UePHjUj9lx+/fVXMyWClq1ataqpydq6dauZHsHl1VdfNUFs6NChcubMGbn//vvNOa+elLM4aVD6baNIZiwHAMALBDg6KRKKhDYB6ig97WReWP2jAACAd3x++0VzHgAAQHEjRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAAFggRAEAABCiAAAAigc1UQAAABYIUQAAABYIUQAAABYIUQAAABYIUQAAABYIUQAAABYIUQAAABYIUQAAABYIUQAAAKU5RM2cOVPq1KkjISEhEh0dLYmJiXmWnTNnjjzwwANStWpVs3Tq1Oma8gMHDpSAgACPJSYmphiuBAAA+AK/CFFLliyRkSNHyvjx42Xnzp3StGlT6dKli6SmpuZaftOmTfLkk0/Kxo0bJSEhQWrVqiWdO3eWEydOeJTT0JSUlORe/va3vxXTFQEAAG8X4DiOIz5Oa55atWol7733nlnPzs42wWjEiBEyZsyY6x6flZVlaqT0+AEDBrhros6cOSMrVqywfl7p6ekSFhYmaWlpEhoaan0eAABQfAr6+e3zNVGXLl2SHTt2mCY5l8DAQLOutUwFcf78ebl8+bKEh4dfU2MVEREhDRo0kGHDhskvv/yS73kyMzPNC59zAQAA/snnQ9SpU6dMTVKNGjU8tut6cnJygc4xevRoiYqK8ghi2pS3cOFCWb9+vUyZMkW++OIL6dq1q/ldeYmLizPJ1bVobRgAAPBPZaSUmzx5sixevNjUOmmndJc+ffq4f27cuLE0adJE7rjjDlOuY8eOuZ4rNjbW9M1y0ZooghQAAP7J52uiqlWrJkFBQZKSkuKxXdcjIyPzPXbatGkmRK1du9aEpPzUq1fP/K4DBw7kWSY4ONi0neZcAACAf/L5EFWuXDlp0aKFaXZz0Y7lut62bds8j5s6dapMmjRJ4uPjpWXLltf9PcePHzd9omrWrFlozx0AAPgunw9RSpvQdO6nBQsWyN69e00n8HPnzsmgQYPMfh1xp01tLtrHaezYsTJ37lwzt5T2ndIlIyPD7NfHV155RbZt2yZHjhwxgaxHjx5Sv359M3UCAACAX/SJ6t27t/z8888ybtw4E4aaNWtmaphcnc2PHj1qRuy5zJo1y4zqe+yxxzzOo/NMvf7666Z58LvvvjOhTKc50E7nOo+U1lxpkx0AAIBfzBPlrZgnCgAA31Nq5okCAAAoCYQoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAAAAC4QoAACAkghRP//8s3z77beSkZGR6/6zZ8/K5s2bb/bXAAAA+EeIunLligwaNEgiIyPl3nvvlerVq8uLL74oFy5c8Cj3ww8/yMMPP1wYzxUAAMD3Q9SMGTNkyZIlMnHiRFm9erUJUHPmzJH77rtPUlJSCvdZAgAA+EuImjt3rowdO1b+8Ic/SExMjMTFxcnXX38t586dM0HqwIEDhftMAQAA/CFEHT582ISlnBo1aiQJCQkSHh4u7dq1k507dxbGc0QOWdmOJBz8RT7ddcI86jpQGLi3AKCYQlS1atVybba75ZZbZOPGjXL33XebvlDr16+X4jBz5kypU6eOhISESHR0tCQmJuZbfunSpdKwYUNTvnHjxvL555977HccR8aNGyc1a9aU8uXLS6dOneTHH3+UkhS/J0nun7JBnpyzTX6/eJd51HXdDnBvAYCPhKgWLVrIihUrct1XqVIliY+Plw4dOshrr70mRU37Zo0cOVLGjx9var+aNm0qXbp0kdTU1FzLb926VZ588kkZPHiw/OMf/5CePXuaZc+ePe4yU6dONf2+Zs+eLdu3b5eKFSuac168eFFKggalYYt2SlKa5+9PTrtothOkwL0FAMUrwNEqFwuffPKJ/OlPf5LPPvvM1D7lJjs7W5577jlZs2aNaf4rKlrz1KpVK3nvvffcv7dWrVoyYsQIGTNmzDXle/fubfpu6XN3adOmjTRr1syEJn1JoqKiZNSoUfLyyy+b/WlpaVKjRg2ZP3++9OnTp0DPKz09XcLCwsyxoaGhN9XMojVOVwcolwARiQwLkS2jO0hQoK4B3FsAYKugn983VBOlJ3N57LHHTP+nvAKUOXlgoAklRRmgLl26JDt27DDNbTl/r67r88uNbs9ZXmktk6u8Pt/k5GSPMvpialjL65wqMzPTvPA5l8KQePh0ngFKaQrW/VoO4N4CgOJxQyFK+zj98ssv4k1OnTolWVlZppYoJ13XIJQb3Z5fedfjjZxT6QhFDVuuRWvDCkPq2YuFWg7g3gKAYg5Ru3btkgcffDDfIOFy+fJlKW1iY2NNbZ1rOXbsWKGcN6JySKGWA7i3AKCYQ9Srr74qe/fulQceeECOHj2ab0dvHflWHHSUYFBQ0DUjBXVdZ1PPjW7Pr7zr8UbOqYKDg03bac6lMLSuGy41w0JM36fc6Hbdr+UA7i0A8MIQNXnyZHnzzTfl4MGDJkhdPeR/27ZtZu6op556So4cOSLFoVy5cmakYM6pFLRjua63bds212N0+9VTL6xbt85dvm7duiYs5Syj/Zt0lF5e5yxK2ll8fPdG5uerg5RrXffTqRzcWwDgxVMcaJOVzsl0/Phx07S3e/duE5h0xJtOsKlBqnbt2rJgwQIpLjq9gX7ljP5OrSkbNmyYGX2n3+2nBgwYYJ63y+9//3szBYOOLty3b5+8/vrr8s0338jw4cPN/oCAAPM1Nm+88YasXLnSXKOeQ0fs6VQIJSHmnpoyq9+9ZhReTrqu23U/wL0FAMXIsbRo0SKnbNmyTlhYmBMSEuIEBAQ4t9xyi/OnP/3JyczMdIrbu+++69SuXdspV66c07p1a2fbtm3ufe3bt3eefvppj/Iff/yxc+edd5ryd999t7N69WqP/dnZ2c7YsWOdGjVqOMHBwU7Hjh2d/fv339BzSktL04Fz5rGwXMnKdrYeOOWs+Mdx86jrAPcWABSegn5+W80Tpc1l77//vqnd+fXXX03NjdZEzZo1y4xKQ+HOEwUAAHx8nii1fPlyueeee0yTmQYo1/fn/f3vfy/S+aAAAAC8yQ2FKA1MOsmm9iNq3ry5+Y68LVu2mFopDVT6NS9fffVV0T1bAAAAXwxR2mn81ltvNR24tSN2+/btzXbtwP3RRx+Zztw687eOdAMAAPBnNxSiJk2aJP/85z+lf//+1+x7/PHHTVOf9pd69NFHzc8AAAD+yvoLiPPyxRdfSPfu3c33yOlSmtGxHAAA31NkHcuvR5v4tJN55cqVC/vUAAAAXqPQQ5Rq3bq1bNq0qShODQAA4L8hSuk0CAAAAP6qyEIUAACAPyNEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAWCBEAQAAlMYQdfr0aenbt6+EhoZKlSpVZPDgwZKRkZFv+REjRkiDBg2kfPnyUrt2bXnhhRckLS3No1xAQMA1y+LFi4vhigAAgC8oIz5OA1RSUpKsW7dOLl++LIMGDZKhQ4fKRx99lGv5kydPmmXatGnSqFEj+emnn+TZZ5812z755BOPsvPmzZOYmBj3uoY0AAAAFeA4juOrL8XevXtNEPr666+lZcuWZlt8fLw88sgjcvz4cYmKiirQeZYuXSr9+vWTc+fOSZky/8qVWvO0fPly6dmzp/XzS09Pl7CwMFPLpTVlAADA+xX089unm/MSEhJM7ZArQKlOnTpJYGCgbN++vcDncb1IrgDl8vzzz0u1atWkdevWMnfuXLle3szMzDQvfM4FAAD4J59uzktOTpaIiAiPbRqEwsPDzb6COHXqlEyaNMk0AeY0ceJE6dChg1SoUEHWrl0rzz33nOlrpf2n8hIXFycTJkywvBoAAOBLvLImasyYMbl27M657Nu376Z/j9YUdevWzTQJvv766x77xo4dK+3atZPmzZvL6NGj5dVXX5W33nor3/PFxsaaWi3XcuzYsZt+jgAAwDt5ZU3UqFGjZODAgfmWqVevnkRGRkpqaqrH9itXrpgReLovP2fPnjWdxitXrmz6PpUtWzbf8tHR0abGSpvsgoODcy2j2/PaBwAA/ItXhqjq1aub5Xratm0rZ86ckR07dkiLFi3Mtg0bNkh2drYJPfnVQHXp0sUEnpUrV0pISMh1f9euXbukatWqhCQAAOC9Iaqg7rrrLlObNGTIEJk9e7aZ4mD48OHSp08f98i8EydOSMeOHWXhwoWmg7gGqM6dO8v58+dl0aJFHh3ANbgFBQXJqlWrJCUlRdq0aWMClk6f8Mc//lFefvnlEr5iAADgLXw6RKkPP/zQBCcNSjoqr1evXjJjxgz3fg1W+/fvN6FJ7dy50z1yr379+h7nOnz4sNSpU8c07c2cOVNeeuklMyJPy02fPt2ENQAAAJ+fJ8rbMU8UAAC+p1TMEwUAAFBSCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWCFEAAAAWytgchJKTle1I4uHTknr2okRUDpHWdcMlKDCAPwm4twCgmPl8TdTp06elb9++EhoaKlWqVJHBgwdLRkZGvsc89NBDEhAQ4LE8++yzHmWOHj0q3bp1kwoVKkhERIS88sorcuXKFSlJ8XuS5P4pG+TJOdvk94t3mUdd1+0A9xYAFC+fD1EaoL7//ntZt26dfPbZZ7J582YZOnTodY8bMmSIJCUluZepU6e692VlZZkAdenSJdm6dassWLBA5s+fL+PGjZOSokFp2KKdkpR20WN7ctpFs50gBe4tAChePh2i9u7dK/Hx8fL+++9LdHS03H///fLuu+/K4sWL5eTJk/keqzVMkZGR7kVrslzWrl0rP/zwgyxatEiaNWsmXbt2lUmTJsnMmTNNsCqJJrwJq34QJ5d9rm26X8sB3FsAUDx8OkQlJCSYJryWLVu6t3Xq1EkCAwNl+/bt+R774YcfSrVq1eSee+6R2NhYOX/+vMd5GzduLDVq1HBv69Kli6Snp5tar7xkZmaaMjmXwqB9oK6ugcpJo5Pu13IA9xYAFA+f7lienJxs+ivlVKZMGQkPDzf78vLUU0/J7bffLlFRUfLdd9/J6NGjZf/+/bJs2TL3eXMGKOVaz++8cXFxMmHCBCls2om8MMsB3FsA4KchasyYMTJlypTrNuXZytlnSmucatasKR07dpSDBw/KHXfcYX1erdEaOXKke11romrVqiU3S0fhFWY5gHsLAPw0RI0aNUoGDhyYb5l69eqZvkypqake23UEnY7Y030Fpf2p1IEDB0yI0mMTExM9yqSkpJjH/M4bHBxslsKm0xjUDAsxnchz6/WkExxEhv1rugOAewsASnGfqOrVq0vDhg3zXcqVKydt27aVM2fOyI4dO9zHbtiwQbKzs93BqCB27dplHrVGSul5d+/e7RHQdPSfdj5v1KiRFDedB2p893/93qtnhHKt637miwL3FgCU8hBVUHfddZfExMSY6Qq05uirr76S4cOHS58+fUx/J3XixAkTulw1S9pkpyPtNHgdOXJEVq5cKQMGDJAHH3xQmjRpYsp07tzZhKX+/fvLt99+K2vWrJHXXntNnn/++SKpaSqImHtqyqx+95oap5x0XbfrfoB7CwCKT4DjOD49Ll6b7jQ4rVq1yozK69Wrl8yYMUMqVapk9mtQqlu3rmzcuNFMsnns2DHp16+f7NmzR86dO2f6LP37v/+7CUk5pzn46aefZNiwYbJp0yapWLGiPP300zJ58mTTcb2gtE9UWFiYpKWleZz7ZjBjOYoK9xYA3Njnt8+HKG9WFCEKAAB4x+e3TzfnAQAAlBRCFAAAgAVCFAAAgAVCFAAAgAVCFAAAACEKAACgeFATBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAYIEQBQAAUBpD1OnTp6Vv374SGhoqVapUkcGDB0tGRkae5Y8cOSIBAQG5LkuXLnWXy23/4sWLi+mqAACAtysjPk4DVFJSkqxbt04uX74sgwYNkqFDh8pHH32Ua/latWqZ8jn99a9/lbfeeku6du3qsX3evHkSExPjXteQBgAA4PMhau/evRIfHy9ff/21tGzZ0mx799135ZFHHpFp06ZJVFTUNccEBQVJZGSkx7bly5fLE088IZUqVfLYrqHp6rIAAAA+35yXkJBggo4rQKlOnTpJYGCgbN++vUDn2LFjh+zatcs0A17t+eefl2rVqknr1q1l7ty54jhOvufKzMyU9PR0jwUAAPgnn66JSk5OloiICI9tZcqUkfDwcLOvID744AO566675L777vPYPnHiROnQoYNUqFBB1q5dK88995zpa/XCCy/kea64uDiZMGGC5dUAAABf4pU1UWPGjMmz87dr2bdv303/ngsXLpi+U7nVQo0dO1batWsnzZs3l9GjR8urr75q+k3lJzY2VtLS0tzLsWPHbvo5AgAA7+SVNVGjRo2SgQMH5lumXr16pr9Samqqx/YrV66YEXsF6cv0ySefyPnz52XAgAHXLRsdHS2TJk0yTXbBwcG5ltHtee0DAAD+xStDVPXq1c1yPW3btpUzZ86Yfk0tWrQw2zZs2CDZ2dkm9BSkKe/RRx8t0O/SflNVq1YlJAEAAO8NUQWlfZl0CoIhQ4bI7NmzzRQHw4cPlz59+rhH5p04cUI6duwoCxcuNB3EXQ4cOCCbN2+Wzz///Jrzrlq1SlJSUqRNmzYSEhJipk/44x//KC+//HKxXh8AAPBePh2i1IcffmiCkwYlHZXXq1cvmTFjhnu/Bqv9+/ebZrucdLTdbbfdJp07d77mnGXLlpWZM2fKSy+9ZEbk1a9fX6ZPn27CGgAAgApwrjduH9Z0ioOwsDDTyVxnVAcAAP7z+e2Vo/MAAAC8HSEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAAiEKAADAQhmbg1BysrIdSTx8WlLPXpSIyiHSum64BAUG8CcB9xa8Eu9Z8Of7yudD1JtvvimrV6+WXbt2Sbly5eTMmTPXPcZxHBk/frzMmTPHlG/Xrp3MmjVLfvOb37jLnD59WkaMGCGrVq2SwMBA6dWrl7zzzjtSqVIlKSnxe5JkwqofJCntontbzbAQGd+9kcTcU7PEnhd8H/cWuK/gK7zp/crnm/MuXbokjz/+uAwbNqzAx0ydOlVmzJghs2fPlu3bt0vFihWlS5cucvHi//9B+vbtK99//72sW7dOPvvsM9m8ebMMHTpUSvKmGbZop8dNo5LTLprtuh/g3oK34D0LpeG+CnC0WsYPzJ8/X1588cXr1kTp5UZFRcmoUaPk5ZdfNtvS0tKkRo0a5hx9+vSRvXv3SqNGjeTrr7+Wli1bmjLx8fHyyCOPyPHjx83xBZGeni5hYWHm/KGhoTdVbXn/lA3X3DQuWoEZGRYiW0Z3oGkP3Fsocbxnwdfvq4J+fvt8TdSNOnz4sCQnJ0unTp3c2/SFio6OloSEBLOuj1WqVHEHKKXltVlPa67ykpmZaV74nEth0HbfvG4apSlY92s5gHsLJY33LJSW+6rUhSgNUEprnnLSddc+fYyIiPDYX6ZMGQkPD3eXyU1cXJwJZK6lVq1ahfKcteNcYZYDuLdQlHjPQmm5r7wyRI0ZM0YCAgLyXfbt2yfeJjY21lT9uZZjx44Vynl15EFhlgO4t1CUeM9CabmvvHJ0nvZXGjhwYL5l6tWrZ3XuyMhI85iSkiI1a/5/L35db9asmbtMamqqx3FXrlwxI/Zcx+cmODjYLIVNh27qyAPtOOfk0w6s5QDuLZQ03rNQWu4rr6yJql69ujRs2DDfRaczsFG3bl0ThNavX+/epn2XtK9T27Ztzbo+agf1HTt2uMts2LBBsrOzTd+p4qYd5HToprq6q5xrXfczXxS4t+ANeM9CabmvvDJE3YijR4+aOaL0MSsry/ysS0ZGhruMhq7ly5ebn7UpUEfxvfHGG7Jy5UrZvXu3DBgwwIy469mzpylz1113SUxMjAwZMkQSExPlq6++kuHDh5uRewUdmVfYdO6LWf3uNSk7J13X7cwTBe4teBPes1Aa7iufn+JAm/0WLFhwzfaNGzfKQw895A5O8+bNczcRuibb/Otf/2pqnO6//375y1/+Infeeaf7eG260+CUc7JNnVvqRibbLKwpDrxxllb4H+4tcF/BV2QV8WdhQT+/fT5EebOiCFEAAKBoMU8UAABAEfL5PlEAAAAlgRAFAABggRAFAABggRAFAABggRAFAABggRAFAABggRAFAABggRAFAABgoYzNQSgY12TwOvMpAADwDa7P7et9qQshqgidPXvWPNaqVasofw0AACiiz3H9+ra88N15RSg7O1tOnjwplStXNl+CXJgJWYPZsWPHSsV38nG9/o+/sX/j7+vf0v3wM0lroDRARUVFSWBg3j2fqIkqQvrC33bbbUV2fr1Z/eWGLQiu1//xN/Zv/H39W6iffSblVwPlQsdyAAAAC4QoAAAAC4QoHxQcHCzjx483j6UB1+v/+Bv7N/6+/i24lH0m5UTHcgAAAAvURAEAAFggRAEAAFggRAEAAFggRAEAAFggRPmIN998U+677z6pUKGCVKlSpcAzro4bN05q1qwp5cuXl06dOsmPP/4ovuD06dPSt29fM3GbXu/gwYMlIyMj32OSk5Olf//+EhkZKRUrVpR7771X/vu//1v89XpVQkKCdOjQwVyvHvvggw/KhQsXxF+v13Vfd+3a1XwLwIoVK8QX3Oj1avkRI0ZIgwYNzL/d2rVrywsvvCBpaWnirWbOnCl16tSRkJAQiY6OlsTExHzLL126VBo2bGjKN27cWD7//HPxJTdyvXPmzJEHHnhAqlatahZ9L77e6+Prf1+XxYsXm3+rPXv2FL/kwCeMGzfOmT59ujNy5EgnLCysQMdMnjzZlF2xYoXz7bffOo8++qhTt25d58KFC463i4mJcZo2beps27bN+fLLL5369es7Tz75ZL7H/Pa3v3VatWrlbN++3Tl48KAzadIkJzAw0Nm5c6fjj9e7detWJzQ01ImLi3P27Nnj7Nu3z1myZIlz8eJFxx+v10X/HXTt2lW/FdRZvny54wtu9Hp3797t/Md//IezcuVK58CBA8769eud3/zmN06vXr0cb7R48WKnXLlyzty5c53vv//eGTJkiFOlShUnJSUl1/JfffWVExQU5EydOtX54YcfnNdee80pW7asuW5fcKPX+9RTTzkzZ850/vGPfzh79+51Bg4caN6bjx8/7vjj9bocPnzYufXWW50HHnjA6dGjh+OPCFE+Zt68eQUKUdnZ2U5kZKTz1ltvubedOXPGCQ4Odv72t7853kzfVPUD8uuvv3Zv+5//+R8nICDAOXHiRJ7HVaxY0Vm4cKHHtvDwcGfOnDmOP15vdHS0+fDxNbbXq/RDSN+Uk5KSfCZE3cz15vTxxx+bD7LLly873qZ169bO888/717PyspyoqKiTMDPzRNPPOF069btmvv5d7/7neMLbvR6r3blyhWncuXKzoIFCxx/vd4rV6449913n/P+++87Tz/9tN+GKJrz/NThw4dN85ZWG+f8HiCthtUmIG+mz0+bPFq2bOnepteh30W4ffv2PI/T5s4lS5aYphD98metRr548aI89NBD4m/Xm5qaavZFRESY665Ro4a0b99etmzZIt7O9u97/vx5eeqpp0yzgjbZ+grb672aNuVpc2CZMt71laeXLl2SHTt2eLzX6LXpel7vNbo9Z3nVpUsXr39vsr3e3O7ly5cvS3h4uPjr9U6cONG8P2nTtT8jRPkpDVBKP1xz0nXXPm+lz0//8eWkHxz6hpPfc//444/NG9Mtt9xiZs793e9+J8uXL5f69euLv13voUOHzOPrr78uQ4YMkfj4eNMHrGPHjl7f78327/vSSy+ZwNijRw/xJbbXm9OpU6dk0qRJMnToUPE2+tyysrJu6L1Gt/vie5Pt9V5t9OjREhUVdU2Q9Jfr3bJli3zwwQemL5i/I0SVoDFjxpgOd/kt+/btE39R1Nc7duxYOXPmjPz973+Xb775RkaOHClPPPGE7N69W/zterWmTWlQHDRokDRv3lz+/Oc/m47Ic+fOFX+73pUrV8qGDRvk7bffltL27zc9PV26desmjRo1MqEZvm3y5Mmmllz/B087afubs2fPmgE+GqCqVasm/s676oVLmVGjRsnAgQPzLVOvXj2rc7uaO1JSUszoPBddb9asmXjz9epz1+aqnK5cuWKa6fJqxjl48KC89957smfPHrn77rvNtqZNm8qXX35pmn9mz54t/nS9rr+pfrDmdNddd8nRo0elJBTl9WqA0r/x1SNTe/XqZUY9bdq0SfzpenN+IMXExEjlypXNh27ZsmXF2+gHZVBQkHlvyUnX87o+3X4j5X39el2mTZtmQpT+j16TJk3EF9zo9R48eFCOHDki3bt3v+Z/+rQGdv/+/XLHHXeI3yjpTlko2o7l06ZNc29LS0vzqY7l33zzjXvbmjVr8u2I+91335lj9NicOnfubEaS+Nv16t9XO3Ze3bG8WbNmTmxsrONv16sdyXXkVs5Fz/HOO+84hw4dcvztel3/Xtu0aeO0b9/eOXfunOPtHY+HDx/u0fFYBwDk17H83/7t3zy2tW3b1qc6lt/I9aopU6aY0bQJCQmOr7mR671w4cI1/1a1U3mHDh3Mz5mZmY4/IUT5iJ9++smMTJowYYJTqVIl87MuZ8+edZdp0KCBs2zZMo8pDnQY6qeffmpCht7IvjTFQfPmzc10BVu2bDHDu3MOCdehwXq9ul9dunTJDBvXobS6TYeFa4DUD6rVq1c7/na96s9//rN5U166dKnz448/mkAVEhJirt0fr/dqvjI6z+Z6NUDpaLXGjRubv6eGSNeio568cQi8/g/a/PnzTWgcOnSoee9JTk42+/v37++MGTPGY4qDMmXKmH+jOuR//PjxPjfFwY1cr74X68jKTz75xONvmfP925+u92r+PDqPEOUj9CbUD42rl40bN7rL6LrWVOWsrRg7dqxTo0YN8w+gY8eOzv79+x1f8Msvv5gPGQ2MGhQGDRrk8Yaj849cff3//Oc/zdw6ERERToUKFZwmTZpcM+WBP12v0v8TvO2228z16v/J6xxE/ny9vhqibvR69TG3f++6aFlv9O677zq1a9c2YUFrLnROLBetTdP3sKunbLjzzjtN+bvvvtsn/mfH9npvv/32XP+WGh59xY3+fUtLiArQ/5R0kyIAAICvYXQeAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAACABUIUAOTijTfekICAAGnTpk2ur8+YMWPM/mbNmsmvv/7KawiUQnx3HgDk4sKFC3LnnXfK8ePH5ZNPPpFevXq598XFxcl//ud/SoMGDWTz5s0SERHBawiUQtREAUAuypcvL2+++ab5+Q9/+INcuXLF/Dxr1iwToOrWrSvr168nQAGlGDVRAJAHx3GkZcuWsnPnTpk9e7ZUqlRJ+vfvL1FRUfLll1+aIOVy4MABmTZtmiQmJsru3bvl1ltvlSNHjvDaAn6MEAUA+di0aZM8/PDDUrVqVTl79qx51Ca8hg0bepT79NNP5fnnn5fWrVvL4cOHTT8pQhTg3whRAHAd7dq1k61bt0rlypVNgNLO5FfLzs6WwMB/9ZB49tlnJT4+nhAF+Dn6RAFAPubNmycJCQnm58zMTAkNDc39zfT/AhSA0oN/9QCQh6VLl8qQIUMkPDxcevfuLZcuXZLRo0fzegEwCFEAkIvPP/9c+vbtKxUrVpQ1a9aYjuUapnS6A23aAwBCFABc5YsvvpDHHntMypQpI6tWrZIWLVpIlSpVzNQGauTIkbxmAAhRAJCTTlHQvXt3ycrKkmXLlsmDDz7o3jd8+HC5/fbbZfv27bJ48WJeOKCUoyYKAP6Pzu/UtWtXOX/+vHz44YcSExPj8doEBwfLpEmTzM+xsbGmozmA0ospDgCgkDHFAVA6lCnpJwAA/kBrr7Qzujp06JBZ107oqlWrVqYZEIB/oSYKAAqBzk6e82tgrp5rauDAgbzOgJ8hRAEAAFigYzkAAAAhCgAAoHhQEwUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAAGCBEAUAACA37n8BmwuyjW9eSggAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"plt.scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "a7c41c4712b66521",
- "outputs": [],
- "execution_count": null
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dcb3b344-f904-46f8-b50a-31bf77567358",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Regardless of the specified domain, the unisolvent nodes are stored in the internal reference domain $[-1, 1]^m$. When the domain is not $[-1, 1]^m$, Minterpy automatically transforms the nodes to the specified domain before passing them to the function, for example, when calling\n",
+ "`grid(func)`.\n",
+ "```"
+ ]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/grid/grid-create-from-points.ipynb b/docs/how-to/grid/grid-create-from-points.ipynb
index 40e8e0cc..c596e93a 100644
--- a/docs/how-to/grid/grid-create-from-points.ipynb
+++ b/docs/how-to/grid/grid-create-from-points.ipynb
@@ -1,26 +1,29 @@
{
"cells": [
{
- "metadata": {},
"cell_type": "markdown",
- "source": "# Create a Grid from an Array of Generating Points",
- "id": "12e4f1e73ba755f"
+ "id": "12e4f1e73ba755f",
+ "metadata": {},
+ "source": [
+ "# Create a Grid from an Array of Generating Points"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": 1,
+ "id": "7937e29866199fe5",
+ "metadata": {},
+ "outputs": [],
"source": [
"import numpy as np\n",
"import minterpy as mp\n",
"import matplotlib.pyplot as plt"
- ],
- "id": "7937e29866199fe5",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "c3a659532557a7fc",
+ "metadata": {},
"source": [
"Minterpy interpolating polynomials (i.e., in the Lagrange or Newton basis)\n",
"lives on a grid that holds the so-called _unisolvent nodes_.\n",
@@ -38,12 +41,12 @@
"\n",
"This guide provides an example on how to construct a `Grid` instance based\n",
"on a given array of generating points using the `from_points()` method."
- ],
- "id": "c3a659532557a7fc"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "d9070443d6eacf2b",
+ "metadata": {},
"source": [
"## About generating points\n",
"\n",
@@ -51,6 +54,7 @@
"a two-dimensional array of floats that must additionally satisfy\n",
"the following conditions:\n",
"\n",
+ "- the values must be in $[-1, 1]^m$, regardless of the domain assigned to the grid.\n",
"- the shape must be $(n + 1, m)$ where $n$ is the maximum polynomial degree\n",
" across all dimensions and $m$ is the spatial dimension. Each column of the\n",
" array is the one-dimensional interpolation points associated with that\n",
@@ -58,12 +62,12 @@
"- each column of the array must have unique values (no repeats).\n",
"- the number of rows of the array must be equal to or larger than the maximum\n",
" exponents of the grid's multi-index set in any dimension."
- ],
- "id": "d9070443d6eacf2b"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "b833565eeda2390f",
+ "metadata": {},
"source": [
"## About `from_points()` factory method\n",
"\n",
@@ -77,14 +81,16 @@
"As explained above, the array of generating points must satisfy\n",
"several conditions for it to be valid.\n",
"\n",
+ "The method also accepts an optional `domain` argument. If not specified the domain defaults to the internal reference domain $[-1, 1]^m$. As noted above, regardless of the specified domain, the generating points must always be in $[-1, 1]^m$.\n",
+ "\n",
"The `from_points()` method is a shortcut to create a grid with a given multi-index set\n",
"and a specific array of generating points."
- ],
- "id": "b833565eeda2390f"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "8944e62814c4e6d7",
+ "metadata": {},
"source": [
"## Example: Two-dimensional interpolation grid\n",
"\n",
@@ -94,22 +100,24 @@
"(defined with respect to $l_p$-degree $1.0$).\n",
"The interpolation grid has Leja-ordered Chebyshev-Lobatto points in the first dimension\n",
"and the Leja points in the second dimension."
- ],
- "id": "8944e62814c4e6d7"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "57b9c7058ebfaf3",
+ "metadata": {},
"source": [
"### Multi-index set\n",
"\n",
"Create an instance of `MultiIndexSet` following the above specification:"
- ],
- "id": "57b9c7058ebfaf3"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "602e91c691ef23ab",
+ "metadata": {},
+ "outputs": [],
"source": [
"exponents = np.array([\n",
" [0, 0],\n",
@@ -122,40 +130,44 @@
" [0, 3],\n",
" [1, 3],\n",
"])"
- ],
- "id": "602e91c691ef23ab",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "mi = mp.MultiIndexSet(exponents, lp_degree=1.0)",
+ "execution_count": null,
"id": "b432150f73a969f1",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "mi = mp.MultiIndexSet(exponents, lp_degree=1.0)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(mi)",
+ "execution_count": null,
"id": "522b98eccba7322c",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(mi)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "b1a31562c4afbd8b",
+ "metadata": {},
"source": [
"### Generating points\n",
"\n",
"The generating points as required by the grid are given as follows:"
- ],
- "id": "b1a31562c4afbd8b"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "340c17de85d0e703",
+ "metadata": {},
+ "outputs": [],
"source": [
"gen_points = np.array([\n",
" [-1., 0. ],\n",
@@ -163,14 +175,12 @@
" [-0.5, 1.0],\n",
" [ 0.5, 0.57735035],\n",
"])"
- ],
- "id": "340c17de85d0e703",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "9c078b5044591939",
+ "metadata": {},
"source": [
"Because the maximum exponents in the multi-indices across all dimensions is $3$,\n",
"the required number of points is $3 + 1 = 4$ per column.\n",
@@ -179,78 +189,96 @@
"In this particular case, each column is based on different one-dimensional\n",
"interpolation points.\n",
"The generating points, however, must have unique values per column. "
- ],
- "id": "9c078b5044591939"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "38b1abd452d4dffe",
+ "metadata": {},
"source": [
"### Grid instance\n",
"\n",
"An instance of `Grid` given the multi-index set and the array of generating points\n",
"can be constructed via `from_points()` method as follows:"
- ],
- "id": "38b1abd452d4dffe"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd = mp.Grid.from_points(mi, gen_points)",
+ "execution_count": null,
"id": "6dbe3419f4ccae9c",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd = mp.Grid.from_points(mi, gen_points)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The grid has the following unisolvent nodes:",
- "id": "5bfd42efddc2c05f"
+ "id": "5bfd42efddc2c05f",
+ "metadata": {},
+ "source": [
+ "The grid has the following unisolvent nodes:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(grd.unisolvent_nodes)",
+ "execution_count": null,
"id": "bed1d9eefa3143d3",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The two-dimensional interpolation grid is plotted below:",
- "id": "cfe55ab8e02b40c7"
+ "id": "cfe55ab8e02b40c7",
+ "metadata": {},
+ "source": [
+ "The two-dimensional interpolation grid is plotted below:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "be3ab3fb62f8f00",
+ "metadata": {},
+ "outputs": [],
"source": [
"plt.scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "be3ab3fb62f8f00",
- "outputs": [],
- "execution_count": null
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19b849f6-af83-4b3d-b498-ad4d08f7c35e",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Regardless of the specified domain, the unisolvent nodes are stored in the internal reference domain $[-1, 1]^m$. When the domain is not $[-1, 1]^m$, Minterpy automatically transforms the nodes to the specified domain before passing them to the function, for example, when calling\n",
+ "`grid(func)`.\n",
+ "```"
+ ]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/grid/grid-create-from-value-set.ipynb b/docs/how-to/grid/grid-create-from-value-set.ipynb
index 69c9dd9f..0af942d0 100644
--- a/docs/how-to/grid/grid-create-from-value-set.ipynb
+++ b/docs/how-to/grid/grid-create-from-value-set.ipynb
@@ -1,26 +1,29 @@
{
"cells": [
{
- "metadata": {},
"cell_type": "markdown",
- "source": "# Create a Grid from an Array of Generating Values",
- "id": "30fda6830c7191f2"
+ "id": "30fda6830c7191f2",
+ "metadata": {},
+ "source": [
+ "# Create a Grid from an Array of Generating Values"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "284f17796016b5b",
+ "metadata": {},
+ "outputs": [],
"source": [
"import numpy as np\n",
"import minterpy as mp\n",
"import matplotlib.pyplot as plt"
- ],
- "id": "284f17796016b5b",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "b6943bbbd9caee2b",
+ "metadata": {},
"source": [
"Minterpy interpolating polynomials (i.e., in the Lagrange or Newton basis)\n",
"lives on a grid that holds the so-called _unisolvent nodes_.\n",
@@ -39,12 +42,12 @@
"This guide provides an example on how to construct a `Grid` instance based\n",
"on a given array of generating values (i.e., one-dimensional interpolation\n",
"points) using the `from_value_set()` method."
- ],
- "id": "b6943bbbd9caee2b"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "799ba4eb511bf60e",
+ "metadata": {},
"source": [
"## About generating values\n",
"\n",
@@ -57,6 +60,7 @@
"a one-dimensional array of floats that must additionally satisfy the following\n",
"conditions:\n",
"\n",
+ "- the values must be in $[-1, 1]$, regardless of the domain assigned to the grid.\n",
"- the length must be $n + 1$ where $n$ is the maximum polynomial degree\n",
" across all dimensions that the grid must support.\n",
"- the values of the array must be unique, that is, it contains no repeats.\n",
@@ -68,12 +72,12 @@
"Generating values may also be provided as a two-dimensional array as long\n",
"as the array only has either one colum or one row.\n",
"```"
- ],
- "id": "799ba4eb511bf60e"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "eedf18386c3eba2f",
+ "metadata": {},
"source": [
"## About the `from_value_set()` factory method\n",
"\n",
@@ -87,17 +91,19 @@
"As explained above, the array of generating values must satisfy\n",
"several conditions for it to be valid.\n",
"\n",
+ "The method also accepts an optional `domain` argument. If not specified the domain defaults to the internal reference domain $[-1, 1]^m$. As noted above, regardless of the specified domain, the generating values must always be in $[-1, 1]$.\n",
+ "\n",
"In many applications, a single set of one-dimensional interpolation points\n",
"is used for different dimensions.\n",
"The `from_value_set()` method is a shortcut to create a grid with\n",
"a specific one-dimensional interpolation points that are replicated\n",
"in every dimension."
- ],
- "id": "eedf18386c3eba2f"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "b5abd137fddfa0e6",
+ "metadata": {},
"source": [
"## Example: Two-dimensional interpolation grid\n",
"\n",
@@ -106,22 +112,24 @@
"$A = \\{ (0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2), (0, 3) \\}$\n",
"(defined with respect to $l_p$-degree $1.0$).\n",
"The interpolation grid has Leja points in every dimension."
- ],
- "id": "b5abd137fddfa0e6"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "63f066280abf5c9e",
+ "metadata": {},
"source": [
"### Multi-index set\n",
"\n",
"Create an instance of `MultiIndexSet` following the above specification:"
- ],
- "id": "63f066280abf5c9e"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "591af982cc982d0e",
+ "metadata": {},
+ "outputs": [],
"source": [
"exponents = np.array([\n",
" [0, 0],\n",
@@ -135,123 +143,145 @@
" [2, 2],\n",
" [0, 3],\n",
"])"
- ],
- "id": "591af982cc982d0e",
- "outputs": [],
- "execution_count": null
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "mi = mp.MultiIndexSet(exponents, lp_degree=1.0)",
+ "execution_count": null,
"id": "7c107f6e46ee336a",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "mi = mp.MultiIndexSet(exponents, lp_degree=1.0)"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(mi)",
+ "execution_count": null,
"id": "eaaa8b707cee089b",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(mi)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "c0ff0c1f9f06f7b2",
+ "metadata": {},
"source": [
"### Generating values\n",
"\n",
"The required generating values as required by the grid are given as follows:"
- ],
- "id": "c0ff0c1f9f06f7b2"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "gen_values = np.array([0, -1, 1, 0.57735035])",
+ "execution_count": null,
"id": "1b946b0c6700c900",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "gen_values = np.array([0, -1, 1, 0.57735035])"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "f6b35e3ac281ca6d",
+ "metadata": {},
"source": [
"Because the maximum exponents in the multi-indices across all dimensions is $3$,\n",
"the required number of generating values is $3 + 1 = 4$."
- ],
- "id": "f6b35e3ac281ca6d"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
+ "id": "def5222540d869d0",
+ "metadata": {},
"source": [
"### Grid instance\n",
"\n",
"An instance of `Grid` given the multi-index set and the array of generating values\n",
"can be constructed via `from_value_set()` method as follows:"
- ],
- "id": "def5222540d869d0"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "grd = mp.Grid.from_value_set(mi, gen_values)",
+ "execution_count": null,
"id": "3a11686f15dd0cc3",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "grd = mp.Grid.from_value_set(mi, gen_values)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The grid has the following unisolvent nodes:",
- "id": "cb165b42ba414cbd"
+ "id": "cb165b42ba414cbd",
+ "metadata": {},
+ "source": [
+ "The grid has the following unisolvent nodes:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
- "source": "print(grd.unisolvent_nodes)",
+ "execution_count": null,
"id": "bd81d11f9e3842d",
+ "metadata": {},
"outputs": [],
- "execution_count": null
+ "source": [
+ "print(grd.unisolvent_nodes)"
+ ]
},
{
- "metadata": {},
"cell_type": "markdown",
- "source": "The two-dimensional interpolation grid is plotted below:",
- "id": "24eb3e4afee3e70b"
+ "id": "24eb3e4afee3e70b",
+ "metadata": {},
+ "source": [
+ "The two-dimensional interpolation grid is plotted below:"
+ ]
},
{
- "metadata": {},
"cell_type": "code",
+ "execution_count": null,
+ "id": "80b67f24678953a9",
+ "metadata": {},
+ "outputs": [],
"source": [
"plt.scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1])\n",
"plt.xlabel(\"$x_1$\", fontsize=16)\n",
"plt.ylabel(\"$x_2$\", fontsize=16);"
- ],
- "id": "80b67f24678953a9",
- "outputs": [],
- "execution_count": null
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9f048e4b-c2b3-4b95-a13c-5dbf01f4c4c6",
+ "metadata": {},
+ "source": [
+ "```{note}\n",
+ "Regardless of the specified domain, the unisolvent nodes are stored in the internal reference domain $[-1, 1]^m$. When the domain is not $[-1, 1]^m$, Minterpy automatically transforms the nodes to the specified domain before passing them to the function, for example, when calling\n",
+ "`grid(func)`.\n",
+ "```"
+ ]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
}
},
"nbformat": 4,
diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst
index 06b32399..6add3633 100644
--- a/docs/how-to/index.rst
+++ b/docs/how-to/index.rst
@@ -1,17 +1,35 @@
-#############
+=============
How-to Guides
-#############
+=============
This page contains a series of guides on how to carry out routine numerical
tasks with polynomials in Minterpy as well as demonstrating important features
of the main components of Minterpy.
+Numerical tasks
+===============
+
+This collection of guides demonstrates how to solve common computational problems using Minterpy. They provide neither
+motivation nor broader context; the guides assume you know what you want to do and focus on how to do it.
+
+.. toctree::
+ :maxdepth: 2
+
+ Integration
+
+Minterpy components
+===================
+
+This collection of guides provides an example-driven exploration of individual objects and core building-blocks of
+Minterpy. These guides are intended for advanced users and developers who want to deepen their understanding of Minterpy
+internals prior to modification or extension.
+
.. toctree::
:maxdepth: 2
Multi-index Set
+ Domain
Grid
- Integration
..
.. todo::
diff --git a/docs/index.rst b/docs/index.rst
index 3351bdfc..9596f1fb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,9 +14,12 @@ Minterpy Documentation
.. image:: /assets/Wordmark-color.png
+|
+
Minterpy is an open-source Python package designed for constructing
-and manipulating multivariate interpolating polynomials with the goal of
-lifting the curse of dimensionality from interpolation tasks.
+and manipulating multivariate interpolating polynomials
+with the goal of addressing the curse of dimensionality
+from interpolation tasks.
.. grid:: auto
:margin: 0
@@ -103,8 +106,9 @@ lifting the curse of dimensionality from interpolation tasks.
|
Minterpy is being continuously extended and improved,
-with new functionalities added to address the bottlenecks
-involving interpolations in various computational tasks.
+with new functionalities added to address the computational bottlenecks
+in accuracy, stability, and performance of multidimensional interpolation
+tasks.
|
diff --git a/src/minterpy/core/ABC/multivariate_polynomial_abstract.py b/src/minterpy/core/ABC/multivariate_polynomial_abstract.py
index a5d2c39e..82fadac6 100644
--- a/src/minterpy/core/ABC/multivariate_polynomial_abstract.py
+++ b/src/minterpy/core/ABC/multivariate_polynomial_abstract.py
@@ -16,24 +16,23 @@
import numpy as np
from copy import copy, deepcopy
+from numpy.typing import ArrayLike
from typing import List, Optional, Tuple, Union
from minterpy.global_settings import ARRAY, SCALAR
+from minterpy.core.domain import Domain
from minterpy.core.grid import Grid
from minterpy.core.multi_index import MultiIndexSet
from minterpy.utils.verification import (
- check_type,
- check_values,
is_real_scalar,
- check_shape,
shape_eval_output,
- verify_domain,
+ standardize_query_points,
+ verify_derivative_order,
verify_poly_coeffs,
- verify_poly_domain,
verify_poly_power,
- verify_query_points,
)
from minterpy.utils.multi_index import find_match_between
+from minterpy.utils.exceptions import DomainMismatchError
__all__ = ["MultivariatePolynomialABC", "MultivariatePolynomialSingleABC"]
@@ -139,24 +138,34 @@ def _eval(
"""
pass
- def __call__(self, xx: np.ndarray, **kwargs) -> np.ndarray:
- """Evaluate the polynomial on a set of query points.
+ # --- Instance methods: Public
+ def eval_on_internal(
+ self,
+ xx: np.ndarray,
+ *,
+ truncate_cols: bool = False,
+ **kwargs,
+ ) -> np.ndarray:
+ """Evaluate polynomial at points in the internal domain.
- The function is called when an instance of a polynomial is called with
- a set of query points, i.e., :math:`p(\mathbf{X})` where
- :math:`\mathbf{X}` is a matrix of values with :math:`k` rows
- and each row is of length :math:`m` (i.e., a point in
- :math:`m`-dimensional space).
+ This method evaluates the polynomial at points assumed to already be
+ in the internal domain used by the polynomial's algorithms. It skips
+ coordinate transformation to avoid unnecessary floating-point error
+ accumulation.
Parameters
----------
xx : :class:`numpy:numpy.ndarray`
- The set of query points to evaluate as a two-dimensional array
- of shape ``(k, m)`` where ``k`` is the number of query points and
- ``m`` is the spatial dimension of the polynomial.
+ Query points in the internal domain. It should be a two-dimensional
+ array of shape ``(k, m)`` where ``k`` is the number of query points
+ and ``m`` is the spatial dimension.
+ truncate_cols : bool, optional
+ If ``True``, then the last columns of ``xx`` are truncated to match
+ the spatial dimension of the polynomial; otherwise all provided
+ columns are attempted to be evaluated. The default is ``False``.
**kwargs
- Additional keyword-only arguments that change the behavior of
- the underlying evaluation (see the concrete implementation).
+ Additional keyword-only arguments passed to the underlying
+ evaluation method (see concrete implementation).
Returns
-------
@@ -171,6 +180,15 @@ def __call__(self, xx: np.ndarray, **kwargs) -> np.ndarray:
``(k, np)`` is returned where ``np`` is the number of
coefficient sets.
+ Notes
+ -----
+ - Use this method when: (1) Points are already in the internal
+ domain (e.g., unisolvent nodes) (2) Avoiding unnecessary coordinate
+ transformations that incur floating-point errors.
+ - Coordinate transformations are not free numerically. While they
+ are affine, round-trip transformations over many interation may
+ accumulate floating-point errors.
+
Notes
-----
- The function calls the concrete implementation of the static method
@@ -181,74 +199,54 @@ def __call__(self, xx: np.ndarray, **kwargs) -> np.ndarray:
_eval
The underlying static method to evaluate the polynomial(s) instance
on a set of query points.
-
- TODO
- ----
- - Possibly built-in rescaling between ``user_domain`` and
- ``internal_domain``. An idea: use sklearn min max scaler
- (``transform()`` and ``inverse_transform()``)
"""
- # Verify query points
- xx = verify_query_points(xx, self.spatial_dimension)
+ # Truncate
+ if truncate_cols:
+ xx = xx[:, :self.spatial_dimension]
- # Evaluate using concrete static method
+ # Evaluate using a concrete static method
yy = self._eval(self, xx, **kwargs)
# Follow the convention of output shape from an evaluation
- return shape_eval_output(yy)
+ yy = shape_eval_output(yy)
- # anything else any polynomial must support
- # TODO mathematical operations? abstract
- # TODO copy operations. abstract
+ return yy
class MultivariatePolynomialSingleABC(MultivariatePolynomialABC):
- """abstract base class for "single instance" multivariate polynomials
+ """Abstract base class for "single instance" multivariate polynomials
- Attributes
+ Parameters
----------
multi_index : MultiIndexSet
- The multi-indices of the multivariate polynomial.
- internal_domain : array_like
- The domain the polynomial is defined on (basically the domain of the unisolvent nodes).
- Either one-dimensional domain (min,max), a stack of domains for each
- domain with shape (spatial_dimension,2).
- user_domain : array_like
- The domain where the polynomial can be evaluated. This will be mapped onto the ``internal_domain``.
- Either one-dimensional domain ``min,max)`` a stack of domains for each
- domain with shape ``(spatial_dimension,2)``.
+ The multi-index set of the multivariate polynomial.
+ coeffs : np.ndarray, optional
+ Coefficients of the polynomial. If ``None``, the polynomial is
+ uninitialized. Either a one-dimensional array of length equal to
+ the number of monomials or a two-dimensional array of shape
+ ``(num_monomials, num_polynomials)`` for multiple polynomials
+ sharing the same multi-index set.
+ grid : Grid, optional
+ The underlying grid on which the (interpolating) polynomial is defined.
+ If not given, the default Grid instance will be constructed.
+ domain : Domain, optional
+ The domain of the underlying grid. If not given, the domain will be
+ derived from the default Grid instance.
Notes
-----
- the grid with the corresponding indices defines the "basis" or polynomial space a polynomial is part of.
- e.g. also the constraints for a Lagrange polynomial, i.e. on which points they must vanish.
- ATTENTION: the grid might be defined on other indices than multi_index! e.g. useful for defining Lagrange coefficients with "extra constraints"
- but all indices from multi_index must be contained in the grid!
- this corresponds to polynomials with just some of the Lagrange polynomials of the basis being "active"
+ - The multi-index set associated with ``grid`` may be different from
+ ``multi_index``. All indices from ``multi_index`` must be contained
+ in ``grid.multi_index``.
"""
-
# __doc__ += __doc_attrs__
_coeffs: Optional[ARRAY] = None
- @staticmethod
- @abc.abstractmethod
- def generate_internal_domain(
- internal_domain, spatial_dimension
- ): # pragma: no cover
- # no docstring here, since it is given in the concrete implementation
- pass
-
- @staticmethod
- @abc.abstractmethod
- def generate_user_domain(user_domain, spatial_dimension): # pragma: no cover
- # no docstring here, since it is given in the concrete implementation
- pass
-
# TODO static methods should not have a parameter "self"
@staticmethod
@abc.abstractmethod
- def _add(poly_1, poly_2): # pragma: no cover
+ def _add(poly_1, poly_2, **kwargs): # pragma: no cover
# no docstring here, since it is given in the concrete implementation
pass
@@ -282,68 +280,12 @@ def _scalar_add(poly, scalar): # pragma: no cover
# no docstring here, since it is given in the concrete implementation
pass
- @staticmethod
- def _gen_grid_default(multi_index):
- """Return the default :class:`Grid` for a given :class:`MultiIndexSet` instance.
-
- For the default values of the Grid class, see :class:`minterpy.Grid`.
-
-
- :param multi_index: An instance of :class:`MultiIndexSet` for which the default :class:`Grid` shall be build
- :type multi_index: MultiIndexSet
- :return: An instance of :class:`Grid` with the default optional parameters.
- :rtype: Grid
- """
- return Grid(multi_index)
-
- @staticmethod
- @abc.abstractmethod
- def _partial_diff(
- poly: MultivariatePolynomialABC,
- dim: int,
- order: int,
- **kwargs,
- ) -> "MultivariatePolynomialSingleABC": # pragma: no cover
- """Abstract method for differentiating poly. on a given dim. and order.
-
- Parameters
- ----------
- poly : MultivariatePolynomialABC
- The instance of polynomial to differentiate.
- dim : int
- Spatial dimension with respect to which the differentiation
- is taken. The dimension starts at 0 (i.e., the first dimension).
- order : int
- Order of partial derivative.
- **kwargs
- Additional keyword-only arguments that change the behavior of
- the underlying differentiation (see the concrete implementation).
-
- Returns
- -------
- MultivariatePolynomialSingleABC
- A new polynomial instance that represents the partial derivative
- of the original polynomial of the given order of derivative with
- respect to the specified dimension.
-
- Notes
- -----
- - The concrete implementation of this static method is called when
- the public method ``partial_diff()`` is called on an instance.
-
- See also
- --------
- partial_diff
- The public method to differentiate the polynomial of a specified
- order of derivative with respect to a given dimension.
- """
- pass
-
@staticmethod
@abc.abstractmethod
def _diff(
poly: MultivariatePolynomialABC,
order: np.ndarray,
+ diff_factor: float,
**kwargs,
) -> "MultivariatePolynomialSingleABC": # pragma: no cover
"""Abstract method for diff. poly. on given orders w.r.t each dim.
@@ -356,6 +298,9 @@ def _diff(
A one-dimensional integer array specifying the orders of derivative
along each dimension. The length of the array must be ``m`` where
``m`` is the spatial dimension of the polynomial.
+ diff_factor : float
+ Differentiation scaling factor taking into account the user
+ domain.
**kwargs
Additional keyword-only arguments that change the behavior of
the underlying differentiation (see the concrete implementation).
@@ -384,21 +329,21 @@ def _diff(
@abc.abstractmethod
def _integrate_over(
poly: "MultivariatePolynomialABC",
- bounds: Optional[np.ndarray],
+ bounds: np.ndarray,
**kwargs,
) -> Union[float, np.ndarray]:
- """Abstract method for definite integration.
+ r"""Abstract method for definite integration.
Parameters
----------
poly : MultivariatePolynomialABC
The instance of polynomial to integrate.
- bounds : Union[List[List[float]], np.ndarray], optional
- The bounds of the integral, an ``(m, 2)`` array where ``m``
+ bounds : :class:`numpy:numpy.ndarray`
+ The limits of the integration, an ``(m, 2)`` array where ``m``
is the number of spatial dimensions. Each row corresponds to
- the bounds in a given dimension.
- If not given, then the canonical bounds :math:`[-1, 1]^m` will
- be used instead.
+ the limits `[lower, upper]` for a given dimension.
+ If ``None``, then the bounds of the internal domain associated
+ with the polynomial are used instead.
**kwargs
Additional keyword-only arguments that change the behavior of
the underlying integration (see the respective concrete
@@ -409,12 +354,15 @@ def _integrate_over(
Union[:py:class:`float`, :class:`numpy:numpy.ndarray`]
The integral value of the polynomial over the given bounds.
If only one polynomial is available, the return value is of
- a :py:class:`float` type.
+ a :py:class:`float` type. For multiple polynomials, an array of
+ values is returned.
Notes
-----
- The concrete implementation of this static method is called when
the public method ``integrate_over()`` is called on an instance.
+ - Bounds are assumed to be in the internal domain (already transformed
+ from the user domain by the public method).
See Also
--------
@@ -429,11 +377,10 @@ def __init__(
self,
multi_index: Union[MultiIndexSet, ARRAY],
coeffs: Optional[ARRAY] = None,
- internal_domain: Optional[ARRAY] = None,
- user_domain: Optional[ARRAY] = None,
grid: Optional[Grid] = None,
+ domain: Optional[Domain] = None,
):
-
+ # Verify and assign multi_index
if multi_index.__class__ is MultiIndexSet:
if len(multi_index) == 0:
raise ValueError("MultiIndexSet must not be empty!")
@@ -442,37 +389,13 @@ def __init__(
# TODO should passing multi indices as ndarray be supported?
self.multi_index = MultiIndexSet(multi_index)
- nr_monomials, spatial_dimension = self.multi_index.exponents.shape
+ # Verify and assign grid (delegate to setter)
self.coeffs = coeffs # calls the setter method and checks the input shape
- if internal_domain is not None:
- check_type(internal_domain, np.ndarray)
- check_values(internal_domain)
- check_shape(internal_domain, shape=(2, spatial_dimension))
- self.internal_domain = self.generate_internal_domain(
- internal_domain, self.multi_index.spatial_dimension
- )
-
- if user_domain is not None: # TODO not better "external domain"?!
- check_type(user_domain, np.ndarray)
- check_values(user_domain)
- check_shape(user_domain, shape=(2, spatial_dimension))
- self.user_domain = self.generate_user_domain(
- user_domain, self.multi_index.spatial_dimension
- )
+ # Verify and assign grid
+ self._grid: Grid = _verify_grid(self.multi_index, grid, domain)
- # TODO make multi_index input optional? otherwise use the indices from grid
- # TODO class method from_grid
- if grid is None:
- grid = self._gen_grid_default(self.multi_index)
- if type(grid) is not Grid:
- raise ValueError(f"unexpected type {type(grid)} of the input grid")
-
- if not grid.multi_index.is_superset(self.multi_index):
- raise ValueError(
- "the multi indices of a polynomial must be a subset of the indices of the grid in use"
- )
- self.grid: Grid = grid
+ # # TODO make multi_index input optional? otherwise use the indices from grid
# weather or not the indices are independent from the grid ("basis")
# TODO this could be enconded by .active_monomials being None
self.indices_are_separate: bool = self.grid.multi_index != self.multi_index
@@ -489,70 +412,112 @@ def from_degree(
cls,
spatial_dimension: int,
poly_degree: int,
- lp_degree: int,
+ lp_degree: float,
coeffs: Optional[ARRAY] = None,
- internal_domain: ARRAY = None,
- user_domain: ARRAY = None,
+ grid: Optional[Grid] = None,
+ domain: Optional[Domain] = None,
):
- """Initialise Polynomial from given coefficients and the default construction for given polynomial degree, spatial dimension and :math:`l_p` degree.
-
- :param spatial_dimension: Dimension of the domain space of the polynomial.
- :type spatial_dimension: int
+ r"""Create a polynomial from polynomial degree specifications.
- :param poly_degree: The degree of the polynomial, i.e. the (integer) supremum of the :math:`l_p` norms of the monomials.
- :type poly_degree: int
+ This factory method constructs a polynomial with a complete multi-index
+ set :math:`\mathcal{M}_{m, n, p}` defined by the spatial dimension
+ :math:`m`, polynomial degree :math:`n`,
+ and :math:`l_p` degree :math:`p`.
- :param lp_degree: The :math:`l_p` degree used to determine the polynomial degree.
- :type lp_degree: int
-
- :param coeffs: coefficients of the polynomial. These shall be 1D for a single polynomial, where the length of the array is the number of monomials given by the ``multi_index``. For a set of similar polynomials (with the same number of monomials) the array can also be 2D, where the first axis refers to the monomials and the second axis refers to the polynomials.
- :type coeffs: np.ndarray
-
- :param internal_domain: the internal domain (factory) where the polynomials are defined on, e.g. :math:`[-1,1]^d` where :math:`d` is the dimension of the domain space. If a ``callable`` is passed, it shall get the dimension of the domain space and returns the ``internal_domain`` as an :class:`np.ndarray`.
- :type internal_domain: np.ndarray or callable
- :param user_domain: the domain window (factory), from which the arguments of a polynomial are transformed to the internal domain. If a ``callable`` is passed, it shall get the dimension of the domain space and returns the ``user_domain`` as an :class:`np.ndarray`.
- :type user_domain: np.ndarray or callable
+ Parameters
+ ----------
+ spatial_dimension : int
+ Spatial dimension of the multi-index set (:math:`m`); the value of
+ ``spatial_dimension`` must be a positive integer (:math:`m > 0`).
+ poly_degree : int
+ Polynomial degree of the multi-index set (:math:`n`); the value of
+ ``poly_degree`` must be a non-negative integer (:math:`n \geq 0`).
+ lp_degree : float
+ :math:`p` of the :math:`l_p`-norm (i.e., the :math:`l_p`-degree)
+ that is used to define the multi-index set. The value of
+ ``lp_degree`` must be a positive float (:math:`p > 0`).
+ coeffs : np.ndarray, optional
+ Coefficients of the polynomial. If ``None``, the polynomial is
+ uninitialized. Either one-dimensional array of length equal to
+ the number of monomials, or two-dimensional array of shape
+ ``(num_monomials, num_polynomials)`` for multiple polynomials
+ sharing the same multi-index set.
+ grid : Grid, optional
+ The underlying grid on which the polynomial is defined.
+ If not given, a default Grid instance will be constructed.
+ domain : Domain, optional
+ The domain of the underlying grid. If not given, the domain will be
+ derived from the Grid instance (either provided or constructed).
+ Returns
+ -------
+ MultivariatePolynomialSingleABC
+ A new polynomial instance with the specified parameters of the
+ complete multi-index set.
"""
- return cls(
- MultiIndexSet.from_degree(spatial_dimension, poly_degree, lp_degree),
- coeffs,
- internal_domain,
- user_domain,
+ mi = MultiIndexSet.from_degree(
+ spatial_dimension,
+ poly_degree,
+ lp_degree,
)
+ return cls(mi, coeffs, grid, domain)
+
@classmethod
def from_poly(
cls,
polynomial: "MultivariatePolynomialSingleABC",
new_coeffs: Optional[ARRAY] = None,
) -> "MultivariatePolynomialSingleABC":
- """constructs a new polynomial instance based on the properties of an input polynomial
+ """Create a new polynomial instance based on an existing polynomial.
- useful for copying polynomials of other types
+ This factory method constructs a new polynomial by copying
+ the structure (multi-index set, grid) from an existing polynomial,
+ optionally with new coefficients.
+ Useful for converting between polynomial types or creating polynomials
+ with the same structure but different coefficients.
+ Parameters
+ ----------
+ polynomial : MultivariatePolynomialSingleABC
+ Input polynomial instance defining the properties to be reused
+ (multi-index set and grid).
+ new_coeffs : np.ndarray, optional
+ The coefficients the new polynomial should have. If ``None``,
+ uses a copy of ``polynomial.coeffs``.
- :param polynomial: input polynomial instance defining the properties to be reused
- :param new_coeffs: the coefficients the new polynomials should have. using `polynomial.coeffs` if `None`
- :return: new polynomial instance with equal properties
+ Returns
+ -------
+ MultivariatePolynomialSingleABC
+ A new polynomial instance with the same structure as the input
+ polynomial.
Notes
-----
- The coefficients can also be assigned later.
+ - The coefficients can also be assigned later if the polynomial is
+ created uninitialized.
+
+ TODO
+ ----
+ - Copying the coefficients of the given polynomial if ``new_coeffs``
+ is ``None`` is not safe because the concrete class that calls this
+ method may be different from the class of the given polynomial.
"""
p = polynomial
if new_coeffs is None: # use the same coefficients
- new_coeffs = p.coeffs
+ new_coeffs = p.coeffs.copy()
- return cls(p.multi_index, new_coeffs, p.internal_domain, p.user_domain, p.grid)
+ return cls(
+ copy(p.multi_index),
+ new_coeffs,
+ copy(p.grid),
+ )
@classmethod
def from_grid(
cls,
grid: Grid,
coeffs: Optional[np.ndarray] = None,
- internal_domain: Optional[np.ndarray] = None,
- user_domain: Optional[np.ndarray] = None,
):
"""Create an instance of polynomial with a `Grid` instance.
@@ -567,10 +532,6 @@ def from_grid(
coefficients of a single polynomial on the same grid.
This parameter is optional, if not specified the polynomial
is considered "uninitialized".
- internal_domain : :class:`numpy:numpy.ndarray`, optional
- The internal domain of the polynomial(s).
- user_domain : :class:`numpy:numpy.ndarray`, optional
- The user domain of the polynomial(s).
Returns
-------
@@ -580,8 +541,6 @@ def from_grid(
return cls(
multi_index=grid.multi_index,
coeffs=coeffs,
- internal_domain=internal_domain,
- user_domain=user_domain,
grid=grid,
)
@@ -632,6 +591,64 @@ def coeffs(self, value: Optional[np.ndarray]) -> None:
expected_num_monomials = self.num_active_monomials
self._coeffs = verify_poly_coeffs(value, expected_num_monomials)
+ @property
+ def grid(self) -> Grid:
+ """The underlying grid of the polynomial(s).
+
+ Returns
+ -------
+ Grid
+ The underlying grid of the polynomial(s). Interpolating polynomials
+ live on a Grid.
+ """
+ return self._grid
+
+ @property
+ def domain(self) -> Domain:
+ """The domain associated with the Grid of the polynomial(s).
+
+ The domain represents the rectangular bounds of the Grid.
+
+ Returns
+ -------
+ Domain
+ The domain of the interpolation grid.
+ """
+ return self._grid.domain
+
+ @property
+ def spatial_dimension(self) -> int:
+ """The spatial dimension of the polynomial(s).
+
+ Returns
+ -------
+ int
+ The spatial dimension of the polynomial(s). This dimension may
+ be less than the spatial dimension of the underlying grid.
+
+ Notes
+ -----
+ - The value is propagated from the underlying ``multi_index``.
+ """
+ return self.multi_index.spatial_dimension
+
+ @property
+ def unisolvent_nodes(self) -> np.ndarray:
+ """Unisolvent nodes on which the interpolating polynomial lives.
+
+ Returns
+ -------
+ np.ndarray
+ A two-dimensional array of shape ``(N, m)`` where
+ ``N`` is the number of the multi-index set exponents and
+ ``m`` is the spatial dimension.
+
+ Notes
+ -----
+ - The value is propagated from the underlying ``grid``.
+ """
+ return self._grid.unisolvent_nodes
+
@property
def num_active_monomials(self) -> int:
"""The number of active monomials of the polynomial(s).
@@ -656,7 +673,8 @@ def __eq__(self, other: "MultivariatePolynomialSingleABC") -> bool:
- both are of the same concrete class, *and*
- the underlying multi-index sets are equal, *and*
- - the underlying grid instances are equal, *and*
+ - the underlying grid instances are equal (including the underlying
+ domains and multi-index sets), *and*
- the coefficients of the polynomials are equal.
Parameters
@@ -672,7 +690,7 @@ def __eq__(self, other: "MultivariatePolynomialSingleABC") -> bool:
``False`` otherwise.
"""
# The instances are of different concrete classes
- if not isinstance(self, type(other)):
+ if self.__class__ != other.__class__:
return False
# The underlying multi-index sets are equal
@@ -757,7 +775,7 @@ def __add__(self, other: Union["MultivariatePolynomialSingleABC", SCALAR]):
----------
other : Union[MultivariatePolynomialSingleABC, SCALAR]
The right operand, either an instance of polynomial (of the same
- concrete class as the right operand) or a real scalar number.
+ concrete class as the left operand) or a real scalar number.
Returns
-------
@@ -775,9 +793,19 @@ def __add__(self, other: Union["MultivariatePolynomialSingleABC", SCALAR]):
return self._scalar_add(self, other)
# Verify the operands before conducting addition
- poly_1, poly_2 = self._verify_operands(other, operation="+ or -")
+ poly_1, poly_2 = self._verify_operands(other)
+
+ # Compute the grid of the polynomial sum
+ grd_add = poly_1.grid | poly_2.grid
+
+ # Compute the multi-index set of the polynomial sum
+ # NOTE: They differ only if one of the polynomials has separate indices
+ if poly_1.indices_are_separate or poly_2.indices_are_separate:
+ mi_add = poly_1.multi_index | poly_2.multi_index
+ else:
+ mi_add = grd_add.multi_index
- return self._add(poly_1, poly_2)
+ return self._add(poly_1, poly_2, multi_index=mi_add, grid=grd_add)
def __sub__(self, other: Union["MultivariatePolynomialSingleABC", SCALAR]):
"""Subtract the polynomial(s) with another poly. or a real scalar.
@@ -852,9 +880,19 @@ def __mul__(self, other: Union["MultivariatePolynomialSingleABC", SCALAR]):
return _scalar_mul(self, other)
# Verify the operands before conducting multiplication
- poly_1, poly_2 = self._verify_operands(other, operation="*")
+ poly_1, poly_2 = self._verify_operands(other)
- return self._mul(poly_1, poly_2)
+ # Compute the grid of the polynomial product
+ grd_mul = poly_1.grid * poly_2.grid
+
+ # Compute the multi-index set of the polynomial product
+ # NOTE: They differ only if one of the polynomials has separate indices
+ if poly_1.indices_are_separate or poly_2.indices_are_separate:
+ mi_mul = poly_1.multi_index * poly_2.multi_index
+ else:
+ mi_mul = grd_mul.multi_index
+
+ return self._mul(poly_1, poly_2, multi_index=mi_mul, grid=grd_mul)
def __truediv__(self, other: SCALAR) -> "MultivariatePolynomialSingleABC":
"""Divide an instance of polynomial with a real scalar number (``/``).
@@ -1042,11 +1080,10 @@ def __copy__(self):
copy.copy
copy operator form the python standard library.
"""
+ # TODO: Use copy for each instance to propagate consistent behavior
return self.__class__(
self.multi_index,
self._coeffs,
- self.internal_domain,
- self.user_domain,
self.grid,
)
@@ -1067,8 +1104,6 @@ def __deepcopy__(self, mem):
return self.__class__(
deepcopy(self.multi_index),
deepcopy(self._coeffs),
- deepcopy(self.internal_domain),
- deepcopy(self.user_domain),
deepcopy(self.grid),
)
@@ -1079,7 +1114,7 @@ def __len__(self) -> int:
Returns
-------
int
- The number of polynomial in the instance. A single instance of
+ The number of polynomials in the instance. A single instance of
polynomial may contain multiple polynomials with different
coefficient values but sharing the same underlying multi-index set
and grid.
@@ -1089,35 +1124,51 @@ def __len__(self) -> int:
return self.coeffs.shape[1]
- @property
- def spatial_dimension(self):
- """Spatial dimension.
+ # --- Special methods: Callable instance
+ def __call__(
+ self,
+ xx: ArrayLike,
+ *,
+ truncate_cols: bool = False,
+ **kwargs,
+ ) -> np.ndarray:
+ r"""Evaluate the polynomial on a set of query points.
- The dimension of space where the polynomial(s) live on.
+ The function is called when an instance of a polynomial is called with
+ a set of query points, i.e., :math:`p(\mathbf{X})` where
+ :math:`\mathbf{X}` is a matrix of values with :math:`k` rows
+ and each row is of length :math:`m` (i.e., a point in
+ :math:`m`-dimensional space).
- :return: Dimension of domain space.
- :rtype: int
+ Parameters
+ ----------
+ xx : array_like
+ Query points to evaluate. Can be a scalar, list, or numpy array.
+ The points will be standardized to a two-dimensional array of
+ shape ``(k, m)`` where ``k`` is the number of query points and
+ ``m`` is the spatial dimension of the polynomial.
+ truncate_cols : bool, optional
+ If ``True``, then the last columns of ``xx`` are truncated to match
+ the spatial dimension of the polynomial; otherwise all provided
+ columns are attempted to be evaluated. The default is ``False``.
+ **kwargs
+ Additional keyword-only arguments that change the behavior of
+ the underlying evaluation (see the concrete implementation).
- Notes
- -----
- This is propagated from the ``multi_index.spatial_dimension``.
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The values of the polynomial evaluated at query points.
"""
- return self.multi_index.spatial_dimension
-
- @property
- def unisolvent_nodes(self):
- """Unisolvent nodes the polynomial(s) is(are) defined on.
-
- For definitions of unisolvent nodes see the mathematical introduction.
+ # Standardize query points
+ dim = self.spatial_dimension
+ xx = standardize_query_points(xx, dim, truncate_cols=truncate_cols)
- :return: Array of unisolvent nodes.
- :rtype: np.ndarray
+ if not self.domain.is_identity:
+ xx = self.domain.map_to_internal(xx)
- Notes
- -----
- This is propagated from from ``self.grid.unisolvent_nodes``.
- """
- return self.grid.unisolvent_nodes
+ # Query points are standardized and truncated
+ return self.eval_on_internal(xx, truncate_cols=False, **kwargs)
# --- Instance methods
def _new_instance_if_necessary(
@@ -1209,57 +1260,34 @@ def add_points(self, exponents: ARRAY) -> "MultivariatePolynomialSingleABC":
def expand_dim(
self,
- target_dimension: Union["MultivariatePolynomialSingleABC", int],
- extra_internal_domain: Optional[np.ndarray] = None,
- extra_user_domain: Optional[np.ndarray] = None,
- ):
+ target_dimension: int,
+ ) -> "MultivariatePolynomialSingleABC":
"""Expand the spatial dimension of the polynomial instance.
Parameters
----------
- target_dimension : Union[int, MultivariatePolynomialSingleABC]
- The new spatial dimension. It must be larger than or equal to
- the current dimension of the polynomial. Alternatively, another
- instance of polynomial that has a higher dimension, a consistent
- underlying `Grid` instance is consistent, and a matching domain
- can also be specified as a target dimension.
- extra_internal_domain : :class:`numpy:numpy.ndarray`, optional
- The additional internal domains for the expanded polynomial.
- This parameter is optional; if not specified, the values are either
- taken from the domain of the higher-dimensional polynomial or
- from the domain of the other dimensions.
- extra_user_domain : :class:`numpy:numpy.ndarray`, optional
- The additional user domains for the expanded polynomial.
- This parameter is optional; if not specified, the values are either
- taken from the domain of the higher-dimensional polynomial or
- from the domain of the other dimensions.
+ target_dimension : int
+ The new spatial dimension. It must be larger than or equal to the
+ current spatial dimension of the polynomial.
Returns
-------
MultivariatePolynomialSingleABC
- A new instance of polynomial whose spatial dimension has been
- expanded to the target.
-
- Raises
- ------
- ValueError
- If the target dimension is an `int`, the exception is raised
- when the user or internal domains cannot be extrapolated to
- a higher dimension. If the target dimension is an instance of
- `MultivariatePolynomialSingleABC`, the exception is raised when
- the user or internal domains do no match.
- In both cases, an exception may also be raised by attempting
- to expand the dimension of the underlying `Grid` or `MultiIndexSet`
- instances.
+ The new instance of polynomial with expanded dimension.
"""
- if isinstance(target_dimension, MultivariatePolynomialSingleABC):
- return _expand_dim_to_target_poly(self, target_dimension)
-
- return _expand_dim_to_target_dim(
- self,
- target_dimension,
- extra_internal_domain,
- extra_user_domain,
+ # Expand the dimension of the multi-index set
+ mi = self.multi_index.expand_dim(target_dimension)
+
+ # Expand the underlying grid if necessary
+ if self.grid.spatial_dimension < target_dimension:
+ grd = self.grid.expand_dim(target_dimension)
+ else:
+ grd = self.grid
+
+ return self.__class__(
+ mi,
+ self._coeffs,
+ grid=grd,
)
def partial_diff(
@@ -1268,7 +1296,7 @@ def partial_diff(
order: int = 1,
**kwargs,
) -> "MultivariatePolynomialSingleABC":
- """Return the partial derivative poly. at the given dim. and order.
+ """Differentiate the polynomial with respect to a given dimension.
Parameters
----------
@@ -1276,7 +1304,7 @@ def partial_diff(
Spatial dimension with respect to which the differentiation
is taken. The dimension starts at 0 (i.e., the first dimension).
order : int
- Order of partial derivative.
+ Order of partial derivative; the default is 1.
**kwargs
Additional keyword-only arguments that change the behavior of
the underlying differentiation (see the respective concrete
@@ -1291,35 +1319,14 @@ def partial_diff(
Notes
-----
- - This method calls the concrete implementation of the abstract
- method ``_partial_diff()`` after input validation.
-
- See Also
- --------
- _partial_diff
- The underlying static method to differentiate the polynomial
- instance of a specified order of derivative and with respect to
- a specified dimension.
+ - This method is a syntactic sugar to `diff()` to differentiate
+ with respect to a particular dimension.
"""
+ # Create an array of the derivative order passable to diff
+ deriv_order = np.zeros(self.spatial_dimension, dtype=int)
+ deriv_order[dim] = order
- # Guard rails for dim
- if not np.issubdtype(type(dim), np.integer):
- raise TypeError(f"dim <{dim}> must be an integer")
-
- if dim < 0 or dim >= self.spatial_dimension:
- raise ValueError(
- f"dim <{dim}> for spatial dimension <{self.spatial_dimension}>"
- f" should be between 0 and {self.spatial_dimension-1}"
- )
-
- # Guard rails for order
- if not np.issubdtype(type(dim), np.integer):
- raise TypeError(f"order <{order}> must be a non-negative integer")
-
- if order < 0:
- raise ValueError(f"order <{order}> must be a non-negative integer")
-
- return self._partial_diff(self, dim, order, **kwargs)
+ return self.diff(deriv_order, **kwargs)
def diff(
self,
@@ -1350,6 +1357,8 @@ def diff(
-----
- This method calls the concrete implementation of the abstract
method ``_diff()`` after input validation.
+ - For zero-order derivative (identity operation), returns a copy
+ of the polynomial.
See Also
--------
@@ -1357,38 +1366,33 @@ def diff(
The underlying static method to differentiate the polynomial
of specified orders of derivative along each dimension.
"""
+ # --- Verify the order of derivatives
+ deriv_order = verify_derivative_order(order, self.spatial_dimension)
- # convert 'order' to numpy 1d array if it isn't already. This allows type checking below.
- order = np.ravel(order)
-
- # Guard rails for order
- if not np.issubdtype(order.dtype.type, np.integer):
- raise TypeError(f"order of derivative <{order}> can only be non-negative integers")
+ # --- Short circuit identity differentiation
+ if np.all(deriv_order == 0):
+ return copy(self)
- if np.any(order < 0):
- raise ValueError(f"order of derivative <{order}> cannot have negative values")
+ # --- Compute the scaling factor
+ diff_factor = self.domain.diff_factor(deriv_order)
- if len(order) != self.spatial_dimension:
- raise ValueError(f"inconsistent number of elements in 'order' <{len(order)}>,"
- f"expected <{self.spatial_dimension}> corresponding to each spatial dimension")
-
- return self._diff(self, order, **kwargs)
+ return self._diff(self, deriv_order, diff_factor, **kwargs)
def integrate_over(
self,
- bounds: Optional[Union[List[List[float]], np.ndarray]] = None,
+ bounds: np.ndarray = None,
**kwargs,
) -> Union[float, np.ndarray]:
- """Compute the definite integral of the polynomial over the bounds.
+ r"""Compute the definite integral of the polynomial over the bounds.
Parameters
----------
- bounds : Union[List[List[float]], np.ndarray], optional
- The bounds of the integral, an ``(m, 2)`` array where ``m``
- is the number of spatial dimensions. Each row corresponds to
- the bounds in a given dimension.
- If not given, then the canonical bounds :math:`[-1, 1]^m` will
- be used instead.
+ bounds : np.ndarray, optional
+ The integral bounds or the limits of integration, an array of
+ shape ``(m, 2)`` where ``m`` is the number of spatial dimensions.
+ Each row corresponds to the limits ``[lower, upper]``
+ for a given dimension. If not given, the integral is computed over
+ the entire domain (i.e., ``self.domain.bounds``).
**kwargs
Additional keyword-only arguments that change the behavior of
the underlying integration (see the respective concrete
@@ -1398,38 +1402,35 @@ def integrate_over(
-------
Union[:py:class:`float`, :class:`numpy:numpy.ndarray`]
The integral value of the polynomial over the given bounds.
- If only one polynomial is available, the return value is of
- a :py:class:`float` type.
+ If only one polynomial is available, a single value of
+ :py:class:`float` type is returned. For multiple polynomials, an
+ array of values is returned.
Raises
------
ValueError
- If the bounds either of inconsistent shape or not in
- the :math:`[-1, 1]^m` domain.
+ If the bounds are of inconsistent shape.
Notes
-----
- - This method calls the concrete implementation of the abstract
- method ``_integrate_over()`` after input validation.
+ - Bounds are specified in the user domain. Internally, they are
+ transformed to the internal domain.
+ - The concrete implementation of the abstract method
+ ``_integrate_over()`` assumes the domain is internal.
+ - Integration outside the domain bounds is allowed but involves
+ extrapolation.
See Also
--------
_integrate_over
The underlying static method to integrate the polynomial instance
over the given bounds.
-
- TODO
- ----
- - The default fixed domain [-1, 1]^M may in the future be relaxed.
- In that case, the domain check below along with the concrete
- implementations for the poly. classes must be updated.
"""
+ # --- Get the integration bounds
num_dim = self.spatial_dimension
if bounds is None:
- # The canonical bounds are [-1, 1]^M
- bounds = np.ones((num_dim, 2))
- bounds[:, 0] *= -1
-
+ # Default: over the entire (user) domain
+ bounds = self.domain.bounds
if isinstance(bounds, list):
bounds = np.atleast_2d(bounds)
@@ -1440,16 +1441,18 @@ def integrate_over(
"The bounds shape is inconsistent! "
f"Given {bounds.shape}, expected {(num_dim, 2)}."
)
- # Domain fit, i.e., in [-1, 1]^M
- if np.any(bounds < -1) or np.any(bounds > 1):
- raise ValueError("Bounds are outside [-1, 1]^M domain!")
- # --- Compute the integrals
- # If the lower and upper bounds are equal, immediately return 0
- if np.any(np.isclose(bounds[:, 0], bounds[:, 1])):
- return 0.0
+ # --- Transform the bounds
+ if not self.domain.is_identity:
+ # bounds is (m, 2), map function expects (N, m) with each row
+ # a point in m-dimensional space. So transpose the bounds,
+ # then transpose the outcome to get the transformed bounds
+ bounds = self.domain.map_to_internal(bounds.T).T
+ # The Jacobian factor
+ int_factor = self.domain.int_factor()
- value = self._integrate_over(self, bounds, **kwargs)
+ # --- Compute the integral
+ value = int_factor * self._integrate_over(self, bounds, **kwargs)
try:
# One-element array (one set of coefficients), just return the item
@@ -1457,130 +1460,10 @@ def integrate_over(
except ValueError:
return value
- # --- Public utility methods
- def has_matching_dimension(
- self,
- other: "MultivariatePolynomialSingleABC",
- ) -> bool:
- """Return ``True`` if the polynomials have matching dimensions.
-
- Parameters
- ----------
- other : MultivariatePolynomialSingleABC
- The second instance of polynomial to compare.
-
- Returns
- -------
- bool
- ``True`` if the two spatial dimensions match, ``False`` otherwise.
- """
- return self.spatial_dimension == other.spatial_dimension
-
- def has_matching_domain(
- self,
- other: "MultivariatePolynomialSingleABC",
- tol: float = 1e-16,
- ) -> bool:
- """Return ``True`` if the polynomials have matching domains.
-
- Parameters
- ----------
- other : MultivariatePolynomialSingleABC
- The second instance of polynomial to compare.
- tol : float, optional
- The tolerance used to check for matching domains.
- Default is 1e-16.
-
- Returns
- -------
- bool
- ``True`` if the two domains match, ``False`` otherwise.
-
- Notes
- -----
- - The method checks both the internal and user domains.
- - If the dimensions of the polynomials do not match, the comparison
- is carried out up to the smallest matching dimension.
- """
- # Get the dimension to deal with unmatching dimension
- dim_1 = self.spatial_dimension
- dim_2 = other.spatial_dimension
- dim = np.min([dim_1, dim_2]) # Check up to the smallest matching dim.
-
- # Check matching internal domain
- internal_domain_1 = self.internal_domain[:, :dim]
- internal_domain_2 = other.internal_domain[:, :dim]
- has_matching_internal_domain = np.less_equal(
- np.abs(internal_domain_1 - internal_domain_2),
- tol,
- )
-
- # Check matching user domain
- user_domain_1 = self.user_domain[:, :dim]
- user_domain_2 = other.user_domain[:, :dim]
- has_matching_user_domain = np.less_equal(
- np.abs(user_domain_1 - user_domain_2),
- tol,
- )
-
- # Checking both domains
- has_matching_domain = np.logical_and(
- has_matching_internal_domain,
- has_matching_user_domain,
- )
-
- return np.all(has_matching_domain)
-
# --- Private utility methods: Not supposed to be called from the outside
- def _match_dims(
- self,
- other: "MultivariatePolynomialSingleABC",
- ) -> Tuple[
- "MultivariatePolynomialSingleABC",
- "MultivariatePolynomialSingleABC",
- ]:
- """Match the dimension of two polynomials.
-
- Parameters
- ----------
- other : MultivariatePolynomialSingleABC
- An instance polynomial whose dimension is to match with the current
- polynomial instance.
-
- Returns
- -------
- Tuple[MultivariatePolynomialSingleABC, MultivariatePolynomialSingleABC]
- The two instances of polynomials whose dimensions have been
- matched.
-
- Raises
- ------
- ValueError
- If the dimension of one of the polynomial instance can't be
- matched due to, for instance, incompatible domain.
-
- Notes
- -----
- - If both polynomials have matching dimension and domains, then
- the function return the two polynomials as they are.
- """
- if self.has_matching_dimension(other) and \
- self.has_matching_domain(other):
- # Dimension and domain match, no need to do anything
- return self, other
-
- # Otherwise expand the lower dimension polynomial to a higher dimension
- if self.spatial_dimension > other.spatial_dimension:
- other_expanded = other.expand_dim(self)
- return self, other_expanded
- else:
- self_expanded = self.expand_dim(other)
- return self_expanded, other
-
def _verify_operands(
self,
other: "MultivariatePolynomialSingleABC",
- operation: str,
) -> Tuple[
"MultivariatePolynomialSingleABC",
"MultivariatePolynomialSingleABC",
@@ -1589,20 +1472,78 @@ def _verify_operands(
# Only supported for polynomials of the same concrete class
if self.__class__ != other.__class__:
raise TypeError(
- f"Unsupported operand type(s) for {operation}: "
+ f"Unsupported operation for "
f"'{self.__class__}' and '{other.__class__}'"
)
# Check if the number of coefficients is consistent
if len(self) != len(other):
raise ValueError(
- "Cannot add polynomials with inconsistent "
+ "Cannot operate on polynomials with inconsistent "
"number of coefficient sets"
)
+ return self, other
+
+
+def _verify_grid(
+ multi_index: MultiIndexSet,
+ grid: Optional[Grid],
+ domain: Optional[Domain]
+) -> Grid:
+ """Verify the given Grid instance.
+
+ Parameters
+ ----------
+ multi_index : MultiIndexSet
+ The multi-index set of the polynomial.
+ grid : Grid, optional
+ Grid to be validated; If ``None``, the default Grid instance will be
+ constructed.
+ domain : Domain, optional
+ Domain to create the grid or validation. If ``grid`` is ``None``,
+ this domain is used to create the grid. If both ``grid``
+ and ``domain`` are provided, they must match.
+
+ Returns
+ -------
+ Grid
+ Validated grid instance.
+
+ Raises
+ ------
+ TypeError
+ If ``grid`` is not a Grid instance.
+ DomainMismatchError
+ If both ``grid`` and ``domain`` are provided, but they don't match.
+ ValueError
+ If ``multi_index`` is not a subset of ``grid.multi_index``.
+ """
+ if grid is not None:
+
+ if not isinstance(grid, Grid):
+ raise TypeError(f"grid must be a Grid instance, got {type(grid)}")
+
+ if domain is not None and domain != grid.domain:
+ raise DomainMismatchError(
+ f"grid domain {grid.domain} does not match "
+ f"the given domain {domain}"
+ )
+
+ # A grid multi-index must be able to support the given polynomial
+ if not grid.multi_index.is_superset(multi_index):
+ raise ValueError(
+ "The given multi-index set must be a subset of "
+ "the indices of the given grid"
+ )
+ else:
- poly_1, poly_2 = self._match_dims(other)
+ if domain is None:
+ # Create a default grid instance
+ grid = Grid(multi_index)
+ else:
+ grid = Grid(multi_index, domain=domain)
- return poly_1, poly_2
+ return grid
def _scalar_mul(
@@ -1699,242 +1640,3 @@ def _scalar_floordiv(
poly_copy.coeffs //= other
return poly_copy
-
-
-def _has_consistent_number_of_polys(
- poly_1: "MultivariatePolynomialSingleABC",
- poly_2: "MultivariatePolynomialSingleABC",
-) -> bool:
- """Check if two polynomials have a consistent number of coefficient sets.
- """
- coeffs_1 = poly_1.coeffs
- coeffs_2 = poly_2.coeffs
-
- ndim_1 = coeffs_1.ndim
- ndim_2 = coeffs_2.ndim
-
- if (ndim_1 == 1) and (ndim_2 == 1):
- return True
-
- has_same_dims = coeffs_1.ndim == coeffs_2.ndim
-
- try:
- has_same_cols = coeffs_1.shape[1] == coeffs_2.shape[1]
- except IndexError:
- return False
-
- return has_same_dims and has_same_cols
-
-
-def _expand_dim_to_target_poly(
- origin_poly: "MultivariatePolynomialSingleABC",
- target_poly: "MultivariatePolynomialSingleABC",
-) -> "MultivariatePolynomialSingleABC":
- """Expand the dimension of the polynomial to the dimension of another.
-
- Parameters
- ----------
- origin_poly : MultivariatePolynomialSingleABC
- The polynomial whose spatial dimension is to be expanded.
- target_poly : MultivariatePolynomialSingleABC
- The polynomial whose spatial dimension is the target dimension.
-
- Returns
- -------
- MultivariatePolynomialSingleABC
- A new instance of polynomial with an expanded dimension.
-
- Notes
- -----
- - The extra internal and user domains of the resulting instance takes
- the values from the target polynomial.
- """
- if not origin_poly.has_matching_domain(target_poly):
- raise ValueError(
- "Polynomial cannot be expanded to the dimension of the target "
- "due to non-matching domain."
- )
-
- # Domains and dimensions match: return a copy
- if origin_poly.has_matching_dimension(target_poly):
- return copy(origin_poly)
-
- # Otherwise: expand the dimension
-
- # Get the dimensions
- origin_dimension = origin_poly.spatial_dimension
- target_dimension = target_poly.spatial_dimension
-
- # Expand the dimension underlying multi-index set to the target dimension
- mi = origin_poly.multi_index.expand_dim(target_dimension)
-
- # Expand the dimension of the underlying grid to the target grid
- grd = origin_poly.grid.expand_dim(target_poly.grid)
-
- # Expand the dimension of the internal domain (use values from the larger)
- origin_internal_domain = origin_poly.internal_domain
- target_internal_domain = target_poly.internal_domain
- internal_domain = np.c_[
- origin_internal_domain,
- target_internal_domain[:, origin_dimension:],
- ]
-
- # Expand the dimension of the user domain (use values from the larger)
- origin_user_domain = origin_poly.user_domain
- target_user_domain = target_poly.user_domain
- user_domain = np.c_[
- origin_user_domain,
- target_user_domain[:, origin_dimension:],
- ]
-
- # NOTE: There is no need to verify the domains again because they are
- # taken from the properties of polynomial instances (already verified)
-
- # Return a new instance
- try:
- # The instance is initialized with coefficients
- return origin_poly.__class__(
- multi_index=mi,
- coeffs=origin_poly.coeffs,
- internal_domain=internal_domain,
- user_domain=user_domain,
- grid=grd,
- )
- except ValueError:
- # The instance has no coefficients
- return origin_poly.__class__(
- multi_index=mi,
- coeffs=None,
- internal_domain=internal_domain,
- user_domain=user_domain,
- grid=grd,
- )
-
-
-def _expand_dim_to_target_dim(
- origin_poly: "MultivariatePolynomialSingleABC",
- target_dimension: int,
- extra_internal_domain: Optional[np.ndarray] = None,
- extra_user_domain: Optional[np.ndarray] = None,
-) -> "MultivariatePolynomialSingleABC":
- """Expand the dimension of the polynomial to the target dimension.
-
- Parameters
- ----------
- origin_poly : MultivariatePolynomialSingleABC
- The polynomial whose spatial dimension is to be expanded.
- target_dimension : int
- The target dimension to which the given polynomial will be expanded.
- extra_internal_domain : :class:`numpy:numpy.ndarray`, optional
- The additional internal domains for the expanded dimensions.
- extra_user_domain : :class:`numpy:numpy.ndarray`, optional
- The additional user domains for the expanded dimensions.
-
- Returns
- -------
- MultivariatePolynomialSingleABC
- A new instance of polynomial with an expanded dimension.
-
- Raises
- ------
- ValueError
- If ``extra_internal_domain`` and ``extra_user_domain`` are both
- ``None`` and the domains of the origin polynomial are not uniform
- such that the domains cannot be extrapolated to higher dimension.
- """
- if origin_poly.spatial_dimension == target_dimension:
- return copy(origin_poly)
-
- # Expand the underlying multi-index set
- mi = origin_poly.multi_index.expand_dim(target_dimension)
-
- # Expand the dimension of the underlying grid
- grd = origin_poly.grid.expand_dim(target_dimension)
-
- # Expand the dimension of the internal domain
- origin_internal_domain = origin_poly.internal_domain
- target_internal_domain = _expand_domain(
- origin_internal_domain,
- target_dimension,
- extra_internal_domain,
- )
-
- # Expand the dimension of the user domain
- origin_user_domain = origin_poly.user_domain
- target_user_domain = _expand_domain(
- origin_user_domain,
- target_dimension,
- extra_user_domain,
- )
-
- # Return a new instance
- try:
- # The instance is initialized with coefficients
- return origin_poly.__class__(
- multi_index=mi,
- coeffs=origin_poly.coeffs,
- internal_domain=target_internal_domain,
- user_domain=target_user_domain,
- grid=grd,
- )
- except ValueError:
- # The instance has no coefficients
- return origin_poly.__class__(
- multi_index=mi,
- coeffs=None,
- internal_domain=target_internal_domain,
- user_domain=target_user_domain,
- grid=grd,
- )
-
-
-def _is_domain_uniform(domain: np.ndarray):
- """Check if a given domain is non-uniform"""
- lb_uniform = np.unique(domain[0, :]).size == 1
- ub_uniform = np.unique(domain[1, :]).size == 1
-
- return lb_uniform and ub_uniform
-
-
-def _expand_domain(
- origin_domain: np.ndarray,
- target_dimension: int,
- extra_domain: Optional[np.ndarray] = None,
-) -> np.ndarray:
- """Append additional polynomial domains to the origin domain column-wise.
-
- Parameters
- ----------
- origin_domain : :class:`numpy:numpy.ndarray`
- The origin polynomial domain to be expanded.
- target_dimension : int
- The target spatial dimension to which the given polynomial
- will be expanded.
- extra_domain : :class:`numpy:numpy.ndarray`, optional
- The additional domain to be added to form the target domain.
- This parameter is optional, if `None` is provided, the domain can only
- be expanded if ``origin_domain`` is uniform.
- """
- # Get the spatial dimension difference
- origin_dimension = origin_domain.shape[1]
- diff_dimension = target_dimension - origin_dimension
-
- # If no extra domain is provided
- if extra_domain is None:
- if _is_domain_uniform(origin_domain):
- extra_domain = np.repeat(
- origin_domain[:, 0][:, np.newaxis],
- repeats=diff_dimension,
- axis=1,
- )
- else:
- raise ValueError(
- "Non-uniform domain cannot be extrapolated "
- "for dimension expansion"
- )
- # Combine the extra domain
- target_domain = np.c_[origin_domain, extra_domain]
- # Verify the resulting domain
- target_domain = verify_poly_domain(target_domain, target_dimension)
-
- return target_domain
diff --git a/src/minterpy/core/__init__.py b/src/minterpy/core/__init__.py
index 3d01adfd..ef4acfd4 100644
--- a/src/minterpy/core/__init__.py
+++ b/src/minterpy/core/__init__.py
@@ -2,27 +2,30 @@
The core sub-package of Minterpy.
The sub-package contains several top domain-specific classes
-(e.g., :py:mod:`.multi_index`)
+(e.g., :py:mod:`.multi_index`, :py:mod:`.grid`)
and the abstract base classes.
-.. warning::
-
- Modifications to this sub-package should only be made with a thorough
- understanding of the overall Minterpy code base.
- We strongly advise holding discussions with the project maintainers
- prior to any modifications.
-
-+-------------------------+--------------------------------------------------------------------------------------+
-| Module / Sub-package | Description |
-+=========================+======================================================================================+
-| :py:mod:`.multi_index` | The set of multi-indices representing the exponents of multidimensional polynomials |
-+-------------------------+--------------------------------------------------------------------------------------+
-| :py:mod:`.grid` | The grid on which interpolating polynomials live |
-+-------------------------+--------------------------------------------------------------------------------------+
-| :py:mod:`.tree` | The data to carry out the multidimensional divided difference scheme (DDS) |
-+-------------------------+--------------------------------------------------------------------------------------+
-| :py:mod:`.ABC` | The core abstract base classes |
-+-------------------------+--------------------------------------------------------------------------------------+
+.. important::
+
+ This sub-package forms the computational core of Minterpy.
+ Its components are tightly coupled, and any significant changes may have
+ far-reaching downstream effects on correctness, accuracy, and performance.
+ We recommend discussing any proposed modifications
+ with the project maintainers before proceeding.
+
++-------------------------+--------------------------------------------------------------------------------------------+
+| Module / Sub-package | Description |
++=========================+============================================================================================+
+| :py:mod:`.multi_index` | The set of multi-indices representing the exponents of multidimensional polynomials |
++-------------------------+--------------------------------------------------------------------------------------------+
+| :py:mod:`.domain` | The user-defined domain and its transformation to and from the internal domain |
++-------------------------+--------------------------------------------------------------------------------------------+
+| :py:mod:`.grid` | The interpolation grid of unisolvent nodes defined by multi-indices and generating points |
++-------------------------+--------------------------------------------------------------------------------------------+
+| :py:mod:`.tree` | The data to carry out the multidimensional divided difference scheme (DDS) |
++-------------------------+--------------------------------------------------------------------------------------------+
+| :py:mod:`.ABC` | The abstract base classes for multivariate polynomial representations |
++-------------------------+--------------------------------------------------------------------------------------------+
"""
__all__ = []
@@ -32,6 +35,11 @@
__all__ += multi_index.__all__
+from . import domain # noqa
+from .domain import * # noqa
+
+__all__ += domain.__all__
+
from . import grid # noqa
from .grid import * # noqa
diff --git a/src/minterpy/core/domain.py b/src/minterpy/core/domain.py
new file mode 100644
index 00000000..d51a9788
--- /dev/null
+++ b/src/minterpy/core/domain.py
@@ -0,0 +1,675 @@
+r"""
+This module contains the implementation of the `Domain` class.
+
+The `Domain` class represents a rectangular domain in :math:`m`-dimensional
+space and provides utilities for transformation between user-defined domains
+and the internal reference domain (currently :math:`[-1, 1]^m`).
+
+Background information
+======================
+
+Internal polynomial representations of Minterpy are defined on an internal
+reference domain (currently :math:`[-1, 1]^m`).
+Users, however, are interested in approximating functions defined on custom
+rectangular domains
+:math:`\Omega = [a_1, b_1] \times \cdots \times [a_i, b_i] \times \cdots \times [a_m, b_m]`
+where :math:`a_i` and :math:`b_i` are the lower and upper bounds of
+dimension :math:`i`, respectively.
+In other words, the domain :math:`\Omega` is a cartesian product of intervals
+:math:`[a_i, b_i]` for :math:`i = 1, \dots, m`.
+
+The transformations between these domains are essential for correct evaluation,
+differentiation, and integration of polynomials approximating functions
+in the user-defined domains.
+
+More detailed background information can be found in
+:doc:`/fundamentals/domain`.
+
+Implementation details
+======================
+
+An instance of the `Domain` class consists of domain bounds
+as a two-dimensional array of shape ``(m, 2)``, where ``m`` is the spatial
+dimension. The first column contains the lower bounds
+and the second column the upper bounds across dimensions.
+Each row is the interval of each dimension.
+The bounds are finite real numbers, and the lower bounds are strictly
+smaller than the upper bounds.
+
+The instance of the class provides:
+
+- Coordinate transformations to and from internal domain
+- Scaling factors for differentiation from chain rule
+- Scaling factors for integration (i.e., the determinant of the Jacobian)
+- Domain validation utilities
+
+The transformations are **separable**: each spatial dimension is transformed
+independently of the other dimensions via an affine map.
+
+How-To Guides
+=============
+
+The relevant section of the :doc:`docs ` contains
+several how-to guides related to instances of the `Domain` class illustrating
+their main usages and features.
+
+----
+
+"""
+import numpy as np
+
+from typing import Optional, Union
+
+from minterpy.global_settings import DEFAULT_DOMAIN, FLOAT_DTYPE
+from minterpy.utils.verification import verify_domain_bounds
+from minterpy.utils.exceptions import DomainMismatchError
+
+__all__ = ["Domain"]
+
+
+DEFAULT_ATOL = 1e-12
+DEFAULT_RTOL = 1e-9
+
+
+class Domain:
+ """A class representing the domain of the polynomials.
+
+ A domain of a polynomial is the set of all input points
+ for which the polynomials are defined.
+ A domain is defined by the lower and upper bounds per dimension.
+ Currently, only domains with finite bounds are supported.
+
+ Parameters
+ ----------
+ bounds : np.ndarray
+ The domain bounds as a 2D array with shape ``(m, 2)``,
+ where ``m`` is the spatial dimension.
+ The first column contains the lower bounds and the second column
+ the upper bounds across dimensions.
+ """
+ _INTERNAL_BOUNDS: np.ndarray = DEFAULT_DOMAIN
+
+ def __init__(self, bounds: np.ndarray):
+
+ # Verify and assign the bounds
+ self._bounds = verify_domain_bounds(bounds)
+
+ # Assign lazily-evaluated properties
+ self._internal_bounds = None
+ self._is_identity = None
+
+ # --- Factory methods
+ @classmethod
+ def uniform(
+ cls,
+ spatial_dimension: int,
+ lower: float,
+ upper: float,
+ ):
+ r"""Create an instance with uniform bounds.
+
+ Creates a hyper-rectangular domain :math:`[a, b]^m` where :math:`a`
+ and :math:`b` are the lower and upper bounds for each dimension,
+ respectively.
+
+ Parameters
+ ----------
+ spatial_dimension : int
+ The number of dimensions for the space.
+ lower : float
+ The lower bound of each dimension.
+ upper : float
+ The upper bound of each dimension.
+
+ Returns
+ -------
+ Domain
+ A new instance of the `Domain` class initialized with uniform
+ bounds, i.e., the same lower and upper bound for all dimensions.
+ """
+ bounds = np.empty((spatial_dimension, 2), dtype=FLOAT_DTYPE)
+ bounds[:, 0] = lower
+ bounds[:, 1] = upper
+
+ return cls(bounds)
+
+ @classmethod
+ def identity(cls, spatial_dimension: int):
+ r"""Create an instance with the default internal bounds.
+
+ The default internal bounds are :math:`[-1, 1]^m`.
+
+ Parameters
+ ----------
+ spatial_dimension : int
+ The number of dimensions for the space.
+
+ Returns
+ -------
+ Domain
+ A new instance of the `Domain` class initialized with the default
+ internal bounds, i.e., :math:`[-1, 1]^m` where :math:`m`
+ is the spatial dimension.
+ """
+ internal_bounds = cls._INTERNAL_BOUNDS[np.newaxis, :]
+ bounds = np.repeat(internal_bounds, spatial_dimension, axis=0)
+
+ return cls(bounds)
+
+ # --- Properties (public)
+ @property
+ def bounds(self) -> np.ndarray:
+ """The domain bounds.
+
+ Return
+ ------
+ np.ndarray
+ The domain bounds as a 2D array with shape ``(m, 2)``,
+ where ``m`` is the spatial dimension.
+ The first column contains the lower bounds and the second column
+ the upper bounds across dimensions.
+ """
+ return self._bounds
+
+ @property
+ def spatial_dimension(self):
+ """Dimension of the domain space.
+
+ Return
+ ------
+ int
+ The dimension of the domain space.
+ """
+ return len(self.bounds)
+
+ @property
+ def lowers(self) -> np.ndarray:
+ """The lower bounds of the domain.
+
+ Returns
+ -------
+ np.ndarray
+ The lower bounds of the domain as a one-dimensional array having
+ shape ``(m, )``.
+ """
+ return self.bounds[:, 0]
+
+ @property
+ def uppers(self) -> np.ndarray:
+ """The upper bounds of the domain.
+
+ Returns
+ -------
+ np.ndarray
+ The upper bounds of the domain as a one-dimensional array having
+ shape ``(m, )``.
+ """
+ return self.bounds[:, 1]
+
+ @property
+ def widths(self) -> np.ndarray:
+ """The widths of the domain.
+
+ Returns
+ -------
+ np.ndarray
+ The difference between the upper and lower bounds of the domain
+ as a one-dimensional array having shape ``(m, )``.
+ """
+ return self.uppers - self.lowers
+
+ @property
+ def is_identity(self) -> bool:
+ """Check whether the domain is (approx) equal to the internal domain.
+
+ Returns
+ -------
+ bool
+ ``True`` if the domain is an identity, ``False`` otherwise.
+
+ Notes
+ -----
+ - This check uses numerical tolerances (``DEFAULT_RTOL`` and
+ ``DEFAULT_ATOL``) for robustness against floating-point errors.
+ - When the domain is an identity, transformations and scaling factors
+ computation can usually be skipped.
+ """
+ if self._is_identity is None:
+ # Get the default tolerances
+ rtol = DEFAULT_RTOL
+ atol = DEFAULT_ATOL
+
+ lb = np.allclose(self.lowers, self._internal_lowers, rtol, atol)
+ ub = np.allclose(self.uppers, self._internal_uppers, rtol, atol)
+
+ self._is_identity = bool(lb and ub)
+
+ return self._is_identity
+
+ @property
+ def is_uniform(self) -> bool:
+ r"""Check whether the domain is uniform.
+
+ A uniform domain has the same lower and upper bounds in all dimensions,
+ i.e., the domain has the form :math:`[a, b]^m` for some :math:`a`
+ and :math:`b`.
+
+ Returns
+ -------
+ bool
+ ``True`` if the domain is uniform, ``False`` otherwise.
+
+ Notes
+ -----
+ - The dimension of a uniform domain can be expanded by extrapolating
+ the bounds of the extra dimension from the bounds of the other
+ dimensions.
+ - A domain of dimension 1 is always uniform by definition.
+ - An identity domain is currently always uniform, but not vice versa.
+ - This check uses numerical tolerances (``DEFAULT_RTOL`` and
+ ``DEFAULT_ATOL``) for robustness against floating-point errors.
+ """
+ # Get the default tolerances
+ rtol = DEFAULT_RTOL
+ atol = DEFAULT_ATOL
+
+ # Lower bound condition
+ lb_c = np.allclose(self.lowers, self.lowers[0], rtol=rtol, atol=atol)
+
+ # Upper bound condition
+ ub_c = np.allclose(self.uppers, self.uppers[0], rtol=rtol, atol=atol)
+
+ return bool(lb_c and ub_c)
+
+ @property
+ def internal_bounds(self) -> np.ndarray:
+ """The bounds of the internal domain.
+
+ Returns
+ -------
+ np.ndarray
+ The bounds of the internal domain as a 2D array with shape
+ ``(m, 2)``, where ``m`` is the spatial dimension.
+ The first column contains the lower bounds and the second column
+ the upper bounds across dimensions.
+ """
+ if self._internal_bounds is None:
+ # Note: Use the default hard-coded internal bounds
+ self._internal_bounds = np.repeat(
+ self._INTERNAL_BOUNDS[np.newaxis, :],
+ self.spatial_dimension,
+ axis=0,
+ )
+
+ return self._internal_bounds
+
+ # --- Properties (private)
+ @property
+ def _internal_lowers(self) -> np.ndarray:
+ """The lower bounds of the internal domain.
+
+ Returns
+ -------
+ np.ndarray
+ The lower bounds of the internal domain as a one-dimensional array
+ having shape ``(m, )``.
+ """
+ return self.internal_bounds[:, 0]
+
+ @property
+ def _internal_uppers(self) -> np.ndarray:
+ """The upper bounds of the internal domain.
+
+ Returns
+ -------
+ np.ndarray
+ The upper bounds of the internal domain as a one-dimensional array
+ having shape ``(m, )``.
+ """
+ return self.internal_bounds[:, 1]
+
+ @property
+ def _internal_widths(self) -> np.ndarray:
+ """The widths of the internal domain.
+
+ Returns
+ -------
+ np.ndarray
+ The difference between the upper and lower bounds of the internal
+ domain as a one-dimensional array having shape ``(m, )``.
+ """
+ return self._internal_uppers - self._internal_lowers
+
+ # --- Instance methods
+ def map_to_internal(
+ self,
+ xx: np.ndarray,
+ validate: bool = False,
+ ) -> np.ndarray:
+ r"""Map input points to the internal domain.
+
+ The default internal domain is :math:`[-1, 1]^m`.
+
+ Parameters
+ ----------
+ xx : :class:`numpy:numpy.ndarray`
+ The input points to be mapped to the internal domain.
+ validate : bool, optional
+ If True, validate that input points are within domain bounds, i.e.,
+ no extrapolation is allowed.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The points in the internal domain, except when extrapolated
+ (with ``validate=False``).
+ """
+ if validate and not np.all(self.contains(xx)):
+ raise ValueError(
+ "Input points are outside of domain bounds. "
+ "Set validate=False to allow extrapolation."
+ )
+
+ ilb = self._internal_lowers
+ iwidths = self._internal_widths
+
+ return ilb + iwidths * (xx - self.lowers) / self.widths
+
+ def map_from_internal(
+ self,
+ xx: np.ndarray,
+ validate: bool = False,
+ ) -> np.ndarray:
+ r"""Map points in the internal domain to the original domain.
+
+ The default internal domain is :math:`[-1, 1]^m`.
+
+ Parameters
+ ----------
+ xx : :class:`numpy:numpy.ndarray`
+ The input points in the internal domain to be mapped
+ to the original domain.
+ validate : bool, optional
+ If True, validate that input points are within domain bounds, i.e.,
+ no extrapolation is allowed.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The points in the original domain.
+ """
+ if validate and not np.all(self.contains(xx, internal=True)):
+ raise ValueError(
+ "Input points are outside of the internal domain. "
+ "Set validate=False to allow extrapolation."
+ )
+
+ ilb = self._internal_lowers
+ iwidths = self._internal_widths
+
+ return self.lowers + (xx - ilb) / iwidths * self.widths
+
+ def int_factor(self) -> float:
+ """Compute the scaling factor for polynomial integration.
+
+ Returns
+ -------
+ float
+ The scaling factor for polynomial integration.
+
+ Notes
+ -----
+ - Here, we assume that the integration is carried out over all
+ dimensions.
+ """
+ if self.is_identity:
+ return 1.0
+
+ return float(np.prod(self.widths / self._internal_widths))
+
+ def diff_factor(self, order: np.ndarray) -> float:
+ """Compute the scaling factor for polynomial differentiation.
+
+ Parameters
+ ----------
+ order : :class:`numpy:numpy.ndarray`
+ A one-dimensional integer array specifying the order of derivatives
+ along each dimension. The length of the array must be ``m`` where
+ ``m`` is the spatial dimension of the polynomial.
+
+ Returns
+ -------
+ float
+ The scaling factor for polynomial differentiation.
+ """
+ if self.is_identity:
+ return 1.0
+
+ if np.all(order == 0):
+ return 1.0
+
+ idx = order > 0
+ iwidths = self._internal_widths
+
+ return float(np.prod((iwidths[idx] / self.widths[idx])**(order[idx])))
+
+ def partial_matching(
+ self,
+ other: "Domain",
+ rtol: Optional[float] = None,
+ atol: Optional[float] = None,
+ ) -> bool:
+ """Compare two instances of domain up to the common dimension.
+
+ This method performs approximate equality checking between two domains
+ using numerical tolerances, comparing only up to the minimum spatial
+ dimension of the two domains.
+ Both user-defined bounds and internal reference domain bounds are
+ compared. This is useful for checking the compatibility of domains
+ of different dimensions before, e.g., merging.
+
+ Parameters
+ ----------
+ other : Domain
+ An instance of :class:`Domain` that is to be compared with
+ the current instance.
+ rtol : float, optional
+ The relative tolerance parameter. If not specified,
+ the module-level default ``DEFAULT_RTOL`` is used.
+ atol : float, optional
+ The absolute tolerance parameter. If not specified,
+ the module-level default ``DEFAULT_ATOL`` is used.
+
+ Returns
+ -------
+ bool
+ ``True`` if the two instances match up to the common dimension,
+ ``False`` otherwise.
+ """
+ # Get the default tolerances
+ rtol = DEFAULT_RTOL if rtol is None else rtol
+ atol = DEFAULT_ATOL if atol is None else atol
+
+ dim = min(self.spatial_dimension, other.spatial_dimension)
+
+ # "User" bounds
+ bounds_match = np.allclose(
+ self.bounds[:dim, :],
+ other.bounds[:dim, :],
+ rtol=rtol,
+ atol=atol,
+ )
+
+ # Check internal bounds (currently identical for all instances,
+ # but enables future generalization of an internal coordinate system)
+ internal_bounds_match = np.allclose(
+ self.internal_bounds[:dim, :],
+ other.internal_bounds[:dim, :],
+ rtol=rtol,
+ atol=atol,
+ )
+
+ return bool(bounds_match and internal_bounds_match)
+
+ def contains(self, xx: np.ndarray, internal: bool = False) -> np.ndarray:
+ """Check whether the input points are contained in the domain.
+
+ Parameters
+ ----------
+ xx : :class:`numpy:numpy.ndarray`
+ A set of values to be checked.
+ internal : bool, optional
+ Check against internal bounds instead of the domain bounds.
+ The default is ``False``.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ A boolean array indicating whether each point is contained
+ in the domain.
+ """
+ if internal:
+ lb = self._internal_lowers
+ ub = self._internal_uppers
+ else:
+ lb = self.lowers
+ ub = self.uppers
+
+ return np.all(lb <= xx, axis=1) & np.all(xx <= ub, axis=1)
+
+ def expand_dim(self, target: Union[int, "Domain"]) -> "Domain":
+ """Expand the dimension of the domain.
+
+ Parameters
+ ----------
+ target : Union[int, Domain]
+ The target dimension to expand to. If an integer, it specifies
+ the new dimension. If a Domain instance, it specifies the domain
+ whose dimension to expand to.
+
+ Returns
+ -------
+ Domain
+ A new instance of the :class:`Domain` class with expanded
+ dimension.
+
+ Raises
+ ------
+ ValueError
+ If the target dimension is smaller than the current dimension or
+ an expansion to a target integer is attempted for a non-identity
+ domain.
+ DomainMismatchError
+ If the target domain does not match the current domain.
+ TypeError
+ If the target is not an instance of Domain or int.
+
+ Notes
+ -----
+ - If the target domain is the current instance, the current instance
+ is returned.
+ """
+ if isinstance(target, int):
+
+ if target < self.spatial_dimension:
+ raise ValueError(
+ f"Target dimension {target} cannot be smaller than "
+ f"the current dimension {self.spatial_dimension}"
+ )
+
+ if not self.is_uniform:
+ raise ValueError(
+ "Non-uniform domain cannot be expanded due to ambiguous "
+ "bounds for the extra dimension."
+ )
+
+ lb = self.lowers[0]
+ ub = self.uppers[0]
+
+ return self.__class__.uniform(target, lb, ub)
+
+ if isinstance(target, Domain):
+
+ if not self.partial_matching(target):
+ raise DomainMismatchError(
+ "Target domain does not match the current domain"
+ )
+
+ # If there's no need to expand, return the current instance
+ if self is target:
+ return self
+
+ if target.spatial_dimension < self.spatial_dimension:
+ raise ValueError(
+ f"Target dimension {target.spatial_dimension} cannot be "
+ "smaller than the current dimension "
+ f"{self.spatial_dimension}"
+ )
+
+ return self.__class__(target.bounds.copy())
+
+ raise TypeError(
+ "Target domain must be an instance of Domain or int, "
+ f"got {type(target)} instead"
+ )
+
+
+ # --- Dunder methods
+ def __eq__(self, other: "Domain") -> bool:
+ """Compare two instances of Domain for exact equality in value.
+
+ Two instances of :class:`Domain` class are considered equal in value if
+ and only if their underlying bounds arrays are exactly equal.
+ In other words, this is a strict comparison without any numerical
+ tolerance.
+
+ Parameters
+ ----------
+ other : Domain
+ An instance of :class:`Domain` that is to be compared with
+ the current instance.
+
+ Returns
+ -------
+ bool
+ ``True`` if the two instances are equal in value,
+ ``False`` otherwise.
+ """
+ if not isinstance(other, Domain):
+ return False
+
+ bounds_eq = np.array_equal(self.bounds, other.bounds)
+ internal_bounds_eq = np.array_equal(
+ self.internal_bounds,
+ other.internal_bounds,
+ )
+
+ return bool(bounds_eq and internal_bounds_eq)
+
+ def __or__(self, other: "Domain") -> "Domain":
+ """Combine two instances of Domain via the ``|`` operator.
+
+ Two instances of Domain may be combined via the ``|`` operator if they
+ are partially matched.
+
+ Parameters
+ ----------
+ other : Domain
+ An instance of :class:`Domain` that is to be combined with
+ the current instance.
+
+ Returns
+ -------
+ Domain
+ A new instance of :class:`Domain` that is the result of the
+ combination of the two instances.
+
+ Raises
+ ------
+ DomainMismatchError
+ If the two domains are not partially matched.
+ """
+ if self.spatial_dimension > other.spatial_dimension:
+ return other.expand_dim(self)
+
+ return self.expand_dim(other)
\ No newline at end of file
diff --git a/src/minterpy/core/grid.py b/src/minterpy/core/grid.py
index b922ad7d..fb485cd0 100644
--- a/src/minterpy/core/grid.py
+++ b/src/minterpy/core/grid.py
@@ -17,6 +17,36 @@
More detailed background information can be found in
:doc:`/fundamentals/interpolation-at-unisolvent-nodes`.
+Generating Points Convention
+-----------------------------
+
+The generating points are stored as a two-dimensional array of shape
+``(n + 1, m)``:
+
+- **Columns** represent spatial dimensions: each column contains the
+ one-dimensional interpolation nodes for that specific dimension
+- **Rows** represent polynomial degrees: row ``i`` contains nodes for
+ polynomial degree ``i`` (from degree 0 to degree ``n``)
+
+**Important**: Currently, generating points must be defined in the normalized
+domain :math:`[-1, 1]^m`. When a custom user domain is specified,
+the Grid handles transformation automatically during function evaluation.
+
+Example structure for a 2D grid with maximum degree 3::
+
+ generating_points = [
+ [x0_dim1, x0_dim2], # Degree 0 nodes
+ [x1_dim1, x1_dim2], # Degree 1 nodes
+ [x2_dim1, x2_dim2], # Degree 2 nodes
+ [x3_dim1, x3_dim2], # Degree 3 nodes
+ ]
+ # All values must be in [-1, 1]
+
+The generating function, when provided, produces this array structure.
+Different generating function (e.g., Chebyshev-Lobatto, equidistant, Leja)
+creates different node distributions within :math:`[-1, 1]`, each with distinct
+approximation properties.
+
How-To Guides
=============
@@ -28,13 +58,14 @@
"""
from copy import copy, deepcopy
-from typing import Callable, Optional, Union
+from typing import Callable, Optional, Union, Tuple
import numpy as np
from minterpy.global_settings import ARRAY, INT_DTYPE
from minterpy.gen_points import GENERATING_FUNCTIONS, gen_points_from_values
+from minterpy.core.domain import Domain
from minterpy.core.multi_index import MultiIndexSet
from minterpy.core.tree import MultiIndexTree
from minterpy.utils.arrays import is_unique
@@ -42,7 +73,6 @@
check_type,
check_values,
check_dimensionality,
- check_domain_fit,
)
__all__ = ["Grid"]
@@ -54,14 +84,6 @@
GEN_FUNCTION = Callable[[int, int], np.ndarray]
-def _gen_unisolvent_nodes(multi_index, generating_points):
- """
- .. todo::
- - document this function but ship it to utils first.
- """
- return np.take_along_axis(generating_points, multi_index.exponents, axis=0)
-
-
# TODO implement comparison operations based on multi index comparison operations and the generating values used
class Grid:
"""A class representing the nodes on which interpolating polynomials live.
@@ -93,6 +115,10 @@ class Grid:
are created from the default generating function. If specified,
then the points must be consistent with any non-``None`` generating
function.
+ domain : Domain, optional
+ The domain of the interpolation grid. This parameter is optional.
+ If not specified, a normalized domain in the hypercube of
+ :math:`[-1, 1]^m` is created.
Notes
-----
@@ -101,6 +127,11 @@ class Grid:
of the multi-index set of polynomial exponents and the spatial dimension
(``m``). Furthermore, it must return an array of shape ``(n + 1, m)``
whose values are unique per column.
+ - Generating points array has shape ``(n + 1, m)`` where columns
+ represent spatial dimensions and rows represent polynomial degrees.
+ All values must be in the normalized domain :math:`[-1, 1]^m`.
+ Different generating functions create different node distributions within
+ this normalized space.
- The multi-index set to construct a :class:`Grid` instance may not be
downward-closed. However, building a :class:`.MultiIndexTree` used
in the transformation between polynomials in the Newton and Lagrange
@@ -114,6 +145,7 @@ def __init__(
multi_index: MultiIndexSet,
generating_function: Optional[Union[GEN_FUNCTION, str]] = None,
generating_points: Optional[np.ndarray] = None,
+ domain: Optional[Domain] = None,
):
# --- Arguments processing
@@ -121,6 +153,12 @@ def __init__(
# Process and assign the multi-index set argument
self._multi_index = _process_multi_index(multi_index)
+ # Process and assign the domain
+ if domain is None:
+ domain = Domain.identity(self.multi_index.spatial_dimension)
+
+ self._domain = _process_domain(domain, multi_index)
+
# If generating_function and points not specified,
# use the default generating function
no_gen_function = generating_function is None
@@ -134,13 +172,15 @@ def __init__(
)
# Assign and verify the generating points argument
+ # Note: Domain is already processed
if no_gen_points:
generating_points = self._create_generating_points()
else:
# Create a copy to avoid accidental changes from the outside
generating_points = generating_points.copy()
- self._generating_points = generating_points
- self._verify_generating_points()
+ self._generating_points = self._verify_generating_points(
+ generating_points
+ )
# --- Post-assignment verifications
@@ -164,6 +204,7 @@ def from_degree(
lp_degree: float,
generating_function: Optional[Union[GEN_FUNCTION, str]] = None,
generating_points: Optional[np.ndarray] = None,
+ domain: Optional[Domain] = None,
):
r"""Create an instance of Grid with a complete multi-index set.
@@ -206,6 +247,10 @@ def from_degree(
the generating points are created from the default generating
function. If specified, then the points must be consistent
with any non-``None`` generating function.
+ domain : Domain, optional
+ The domain of the interpolation grid. This parameter is optional.
+ If not specified, a normalized domain in the hypercube of
+ :math:`[-1, 1]^m` is created.
Returns
-------
@@ -222,13 +267,14 @@ def from_degree(
)
# Create an instance of Grid
- return cls(mi, generating_function, generating_points)
+ return cls(mi, generating_function, generating_points, domain=domain)
@classmethod
def from_function(
cls,
multi_index: MultiIndexSet,
generating_function: Union[GEN_FUNCTION, str],
+ domain: Optional[Domain] = None,
) -> "Grid":
"""Create an instance of Grid with a given generating function.
@@ -246,6 +292,10 @@ def from_function(
and ``m`` is the spatial dimension.
Alternatively, a string as a key to dictionary of built-in
generating functions may be specified.
+ domain : Domain, optional
+ The domain of the interpolation grid. This parameter is optional.
+ If not specified, a normalized domain in the hypercube of
+ :math:`[-1, 1]^m` is created.
Returns
-------
@@ -253,13 +303,18 @@ def from_function(
A new instance of the `Grid` class initialized with the given
generating function.
"""
- return cls(multi_index, generating_function=generating_function)
+ return cls(
+ multi_index,
+ generating_function=generating_function,
+ domain=domain,
+ )
@classmethod
def from_points(
cls,
multi_index: MultiIndexSet,
generating_points: np.ndarray,
+ domain: Optional[Domain] = None,
) -> "Grid":
"""Create an instance of Grid from an array of generating points.
@@ -275,6 +330,10 @@ def from_points(
where ``n`` is the maximum polynomial degree in all dimensions
(i.e., the maximum exponent) and ``m`` is the spatial dimension.
The values in each column must be unique.
+ domain : Domain, optional
+ The domain of the interpolation grid. This parameter is optional.
+ If not specified, a normalized domain in the hypercube of
+ :math:`[-1, 1]^m` is created.
Returns
-------
@@ -282,13 +341,18 @@ def from_points(
A new instance of the `Grid` class initialized
with the given multi-index set and generating points.
"""
- return cls(multi_index, generating_points=generating_points)
+ return cls(
+ multi_index,
+ generating_points=generating_points,
+ domain=domain,
+ )
@classmethod
def from_value_set(
cls,
multi_index: MultiIndexSet,
generating_values: np.ndarray,
+ domain: Optional[Domain] = None,
):
"""Create an instance of Grid from an array of generating values.
@@ -304,6 +368,10 @@ def from_value_set(
a one-dimensional array of floats of length ``(n + 1, )``
where ``n`` is the maximum exponent of the multi-index set.
The values in the array must be unique.
+ domain : Domain, optional
+ The domain of the interpolation grid. This parameter is optional.
+ If not specified, a normalized domain in the hypercube of
+ :math:`[-1, 1]^m` is created.
Returns
-------
@@ -331,7 +399,11 @@ def from_value_set(
spatial_dimension,
)
- return cls(multi_index, generating_points=generating_points)
+ return cls(
+ multi_index,
+ generating_points=generating_points,
+ domain=domain,
+ )
# --- Properties
@property
@@ -383,6 +455,19 @@ def generating_points(self) -> np.ndarray:
"""
return self._generating_points
+ @property
+ def domain(self) -> Domain:
+ """The domain associated with the Grid.
+
+ The domain represents the rectangular bounds of the Grid.
+
+ Returns
+ -------
+ Domain
+ The domain of the interpolation grid.
+ """
+ return self._domain
+
@property
def max_exponent(self) -> int:
"""The maximum exponent of the interpolation grid.
@@ -397,18 +482,26 @@ def max_exponent(self) -> int:
@property
def unisolvent_nodes(self) -> np.ndarray:
- """The array of unisolvent nodes.
+ """The array of unisolvent nodes in the normalized domain.
+
+ The unisolvent nodes are provided in the normalized domain
+ :math:`[-1, 1]^m`.
- For a definition of unisolvent nodes, see
- :doc:`/fundamentals/unisolvence`
- in the docs.
+ For a definition of unisolvent nodes,
+ see :doc:`/fundamentals/unisolvence` in the docs.
Returns
-------
:class:`numpy:numpy.ndarray`
- The unisolvent nodes as a two-dimensional array of floats.
- The shape of the array is ``(N, m)`` where ``N`` is the number of
- elements in the multi-index set and ``m`` is the spatial dimension.
+ The unisolvent nodes in [-1, 1]^m as a two-dimensional array
+ of floats. The shape of the array is ``(N, m)``
+ where ``N`` is the number of elements in the multi-index set
+ and ``m`` is the spatial dimension.
+
+ Notes
+ -----
+ - When evaluating functions via ``grid(func)``,
+ the nodes are automatically transformed to the user-defined domain.
"""
if self._unisolvent_nodes is None: # lazy evaluation
self._unisolvent_nodes = _gen_unisolvent_nodes(
@@ -443,18 +536,6 @@ def tree(self):
self._tree = MultiIndexTree(self)
return self._tree
- @property
- def has_generating_function(self) -> bool:
- """Return ``True`` if the instance has a generating function.
-
- Returns
- -------
- bool
- ``True`` if the instance has a generating function assigned to it,
- and ``False`` otherwise.
- """
- return self.generating_function is not None
-
@property
def is_complete(self) -> bool:
"""Return ``True`` if the instance has a complete multi-index set.
@@ -506,67 +587,143 @@ def add_exponents(self, exponents: np.ndarray) -> "Grid":
return self._new_instance(mi_added)
- def expand_dim(self, target_dimension: Union[int, "Grid"]) -> "Grid":
- """Expand the dimension of the Grid.
+ def expand_dim(self, target: Union[int, "Grid"]) -> "Grid":
+ """Expand the dimension of the Grid to a target dimension or Grid.
+
+ This method creates a new Grid with a higher spatial dimension by
+ expanding both the underlying multi-index set and the domain.
+ The expansion can target either a specific integer dimension
+ or match the dimension of another (compatible) Grid instance.
Parameters
----------
- target_dimension : Union[Grid, int]
- The new spatial dimension. It must be larger than or equal
- to the current dimension of the Grid. Alternatively,
- another instance of Grid whose dimension is higher can also
- be specified as a target dimension.
+ target : Union[Grid, int]
+ The target for for dimension expansion:
+
+ - If int: The new spatial dimension (must be >= current dimension).
+ For non-normalized domains, expansion to an integer is not
+ allowed.
+ - If Grid: Another Grid instance whose dimension to match.
+ The grids must have compatible generating functions or points.
Returns
-------
Grid
- The Grid with expanded dimension.
+ A new Grid instance with expanded dimension, containing:
+
+ - Expanded multi-index set
+ - Expanded domain
+ - Compatible generating function or points
Raises
------
ValueError
- If an instance is expanded to a dimension that cannot be supported
- either by the available generating function or generating points.
- If the target dimension is a `Grid`, the exception is raised
- when there are inconsistencies in either generating function
- or points.
+ If the target dimension is smaller than the current dimension,
+ if expanding a non-normalized domain to an integer dimension,
+ if the generating points cannot accommodate the target dimension,
+ or if the Grid is not compatible with the target Grid.
"""
# Expand the dimension to the target Grid instance
- if isinstance(target_dimension, Grid):
- return _expand_dim_to_target_grid(self, target_dimension)
+ if isinstance(target, Grid):
+ return self._expand_to_grid(target)
- # Expand to the target dimension
- return _expand_dim_to_target_dim(self, target_dimension)
+ return self._expand_to_dimension(target)
- def is_compatible(self, other: "Grid") -> bool:
- """Return ``True`` if the instance is compatible with another.
+ def has_compatible_gen_function(self, other: "Grid") -> bool:
+ """Check if two grids have a compatible generating function.
- Two grids are compatible if they have the same generating function
- (if exists) and the generating points up to a common dimension.
+ Two grids have a compatible generating function if each has
+ a generating function and they are an identical function.
Parameters
----------
other : Grid
- The other instance to check its compatibility with the current
- instance.
+ Another Grid instance to check compatibility with.
Returns
-------
bool
- ``True`` if the current instance is compatible with the given
- instance; ``False`` otherwise.
+ ``True`` if the generating functions are compatible,
+ ``False`` otherwise.
"""
- if _have_gen_functions(self, other):
- # Check if the Grid instances have compatible generating functions
- if not _have_compatible_gen_functions(self, other):
- return False
-
- # Check if the Grid instances have compatible generating points
- if _have_compatible_gen_points(self, other):
- return True
+ if self._has_generating_function and other._has_generating_function:
+ # Check if the grid instances have compatible generating functions
+ if self.generating_function == other.generating_function:
+ return True
return False
+ def has_compatible_gen_points(self, other: "Grid") -> bool:
+ """Check if two grids have compatible generating points.
+
+ Two grids have compatible generating points if their generating point
+ arrays match in the common dimensions (columns) and common degrees
+ (rows). Compatibility is checked by comparing the overlapping subarray.
+
+ Parameters
+ ----------
+ other : Grid
+ Another Grid instance to check compatibility with.
+
+ Returns
+ -------
+ bool
+ ``True`` if the generating points are compatible in their common
+ dimensions and degrees, ``False`` otherwise.
+
+ Notes
+ -----
+ - Compatibility is checked only for the intersection of dimensions
+ and degrees
+ - For example, a grid with 3D generating points can be compatible with
+ a 2D grid if their first 2 dimensions match; or a grid with degree 5
+ can be compatible with degree 3 if the first 4 rows (degrees 0-3)
+ match
+ """
+ # Find common dimensions and degrees to compare
+ common_dim = min(self.spatial_dimension, other.spatial_dimension)
+ common_deg = min(
+ self.generating_points.shape[0],
+ other.generating_points.shape[0]
+ )
+
+ # Extract overlapping subarrays
+ gen_points_self = self.generating_points[:common_deg, :common_dim]
+ gen_points_other = other.generating_points[:common_deg, :common_dim]
+
+ # Check if they match
+ return np.array_equal(gen_points_self, gen_points_other)
+
+ def is_compatible(self, other: "Grid") -> bool:
+ """Check if two instances of Grid are compatible.
+
+ Two instances of Grid are compatible if they have:
+
+ - The same generating function (when both have one), OR
+ - Compatible generating points (matching values in common dimensions,
+ i.e., columns and common degrees, i.e., rows)
+
+
+ Parameters
+ ----------
+ other : Grid
+ Another Grid instance to check compatibility with.
+
+ Returns
+ -------
+ bool
+ ``True`` if the generating data is compatible, ``False`` otherwise.
+
+ Notes
+ -----
+ - This method checks ONLY the compatibility of the underlying
+ generating functions or points of the two instances of Grid.
+ """
+ return (
+ self.has_compatible_gen_function(other) or
+ self.has_compatible_gen_points(other)
+ )
+
def make_complete(self) -> "Grid":
"""Complete the underlying multi-index set of the `Grid` instance.
@@ -615,121 +772,98 @@ def make_downward_closed(self) -> "Grid":
return self._new_instance(mi_downward_closed)
- def merge(self, other: "Grid", multi_index: MultiIndexSet) -> "Grid":
- """Merge two instances of Grid with a new multi-index set.
-
- Parameters
- ----------
- other : Grid
- Another instance of `Grid` to merge with the current instance.
- multi_index : MultiIndexSet
- The multi-index set of the merged instance.
-
- Returns
- -------
- Grid
- The merged instance with the given multi-index set.
-
- Raises
- ------
- ValueError
- If the generating functions of the instances are incompatible
- with each other (if both are specified) or if the generating points
- are incompatible with each other (if an instance is missing
- a generating function).
- """
- if _have_gen_functions(self, other):
- # Check if the Grid instances have compatible generating functions
- if _have_compatible_gen_functions(self, other):
- # The functions are compatible
- gen_fun = self.generating_function
- return self.__class__.from_function(multi_index, gen_fun)
- else:
- raise ValueError(
- "The Grid instance has an incompatible generating function"
- " with the other instance"
- )
-
- # Check if the Grid instances have compatible generating points
- if _have_compatible_gen_points(self, other):
- # Get the largest generating points from the two
- gen_points = _get_larger_gen_points(self, other)
- return self.__class__.from_points(multi_index, gen_points)
-
- # Points are inconsistent
- raise ValueError(
- "The Grid instance has incompatible generating points "
- "with the other instance"
- )
-
# --- Special methods: Copies
# copying
def __copy__(self):
- """Creates of a shallow copy.
+ """Creates a shallow copy of the instance.
- This function is called, if one uses the top-level function ``copy()`` on an instance of this class.
+ This function is called when using the top-level function ``copy()``
+ on an instance of this class.
- :return: The copy of the current instance.
- :rtype: Grid
+ Returns
+ -------
+ Grid
+ A shallow copy of the current instance.
See Also
--------
copy.copy
- copy operator form the python standard library.
+ Copy operator from the Python standard library.
"""
return self.__class__(
self._multi_index,
generating_function=self._generating_function,
generating_points=self._generating_points,
+ domain=self._domain,
)
def __deepcopy__(self, mem):
"""Create of a deepcopy.
- This function is called, if one uses the top-level function
+ This function is called if one uses the top-level function
``deepcopy()`` on an instance of this class.
Returns
-------
Grid
A deepcopy of the current instance where the underlying
- multi-index set and the generating points are deepcopied.
+ multi-index set, the domain, and the generating points
+ are deepcopied.
See Also
--------
copy.deepcopy
copy function from the Python standard library.
"""
- # Create a new instance with deep-copied multi-index set
- multi_index = deepcopy(self._multi_index)
- new_self = self._new_instance(multi_index)
+ # Create a new instance with a deep-copied multi-index set and domain
+ multi_index = deepcopy(self._multi_index, mem)
+ domain = deepcopy(self._domain, mem)
- return new_self
+ return self._new_instance(multi_index, domain)
# --- Dunder method: Callable instance
def __call__(self, fun: Callable, *args, **kwargs) -> np.ndarray:
- """Evaluate the given function on the unisolvent nodes of the grid.
+ """Evaluate a function on the unisolvent nodes in the given domain.
+
+ The function is evaluated at the unisolvent nodes transformed from
+ the canonical domain :math:`[-1, 1]^m` to the user-defined domain.
+ This allows users to define functions in their natural coordinate
+ system without manually handling domain transformations.
Parameters
----------
fun : Callable
- The given function to evaluate. The function must accept as its
- first argument a two-dimensional array and return as its output
- an array of the same length as the input array.
+ The function to evaluate. Must accept as its first argument a
+ two-dimensional array of shape ``(N, m)`` and return an array of
+ length ``N``, where ``N`` is the number of unisolvent nodes
+ and ``m`` is the spatial dimension.
*args
- Additional positional arguments passed to the given function.
+ Additional positional arguments passed to the function.
**kwargs
- Additional keyword arguments passed to the given function.
+ Additional keyword arguments passed to the function.
Returns
-------
:class:`numpy:numpy.ndarray`
- The values of the given function evaluated on the unisolvent nodes
- (i.e., the coefficients of the polynomial in the Lagrange basis).
+ The function values evaluated at the unisolvent nodes in the
+ user domain. These values correspond to the coefficients of
+ the polynomial in the Lagrange basis.
+
+ Notes
+ -----
+ - The unisolvent nodes are automatically transformed from
+ :math:`[-1, 1]^m` to the given domain before evaluation.
+ - If the domain is normalized (:math:`[-1, 1]^m`),
+ there no transformation takes place.
"""
# No need for type checking the argument; rely on Python to raise any
- # exceptions when a problematic 'fun' is called on the nodes.
- return fun(self.unisolvent_nodes, *args, **kwargs)
+ # exceptions when problematic 'fun' is called on the nodes.
+ if self.domain.is_identity:
+ xx = self.unisolvent_nodes
+ else:
+ xx = self.domain.map_from_internal(self.unisolvent_nodes)
+
+ return fun(xx, *args, **kwargs)
# --- Dunder methods: Rich comparison
def __eq__(self, other: "Grid") -> bool:
@@ -739,7 +873,8 @@ def __eq__(self, other: "Grid") -> bool:
- both the underlying multi-index sets are equal, and
- both the generating points are equal, and
- - both the generating functions are equal.
+ - both the generating functions are equal, and
+ - both the underlying domains are equal.
Parameters
----------
@@ -764,6 +899,10 @@ def __eq__(self, other: "Grid") -> bool:
if self.generating_function != other.generating_function:
return False
+ # Domain equality
+ if self.domain != other.domain:
+ return False
+
# Generating points equality
if not np.array_equal(self.generating_points, other.generating_points):
return False
@@ -776,44 +915,111 @@ def __eq__(self, other: "Grid") -> bool:
# --- Dunder methods: Arithmetics
def __mul__(self, other: "Grid") -> "Grid":
- """Multiply two instances of `Grid` via the ``*`` operator.
+ """Multiply two instances of Grid via the ``*`` operator.
+
+ Multiplying two instances of Grid creates a product grid with
+ a multi-index set that is the product of the underlying sets
+ of the two operands, and a domain that is the union of the two
+ underlying domains.
Parameters
----------
- other : `Grid`
- The second operand of the grid multiplication.
+ other : Grid
+ The second Grid operand for multiplication.
Returns
-------
- `Grid`
- The product of two grids; the underlying multi-index set is
- the product of the multi-index sets of the operands.
+ Grid
+ The product grid with:
+
+ - Product of the two multi-index sets
+ - Union of the two domains
+ - Compatible generating function or points
+
+ Notes
+ -----
+ - This operation provides the infrastructure for polynomial
+ multiplication: if polynomial ``p1`` is defined on ``grid1``
+ and ``p2`` on ``grid2``, then their product ``(p1 * p2)``
+ is naturally defined on ``(grid1 * grid2)``.
"""
# Multiply the underlying multi-index sets
mi_product = self.multi_index * other.multi_index
+ domain_union = self.domain | other.domain
- return self.merge(other, mi_product)
+ #return self.merge(other, mi_product)
+ return self._combine_with(other, mi_product, domain_union)
def __or__(self, other: "Grid") -> "Grid":
- """Combine two instances of `Grid` via the ``|`` operator.
+ """Combine two instances of Grid via the ``|`` operator.
+
+ Combining two instances of Grid creates a union grid with a multi-index
+ set that is the union of the underlying sets of the two operands,
+ and a domain that is the union of the two underlying domains.
Parameters
----------
- other : `Grid`
- The second operand of the grid union.
+ other : Grid
+ The second Grid operand for union.
Returns
-------
- `Grid`
- The union of two grids; the underlying multi-index set is
- the union of the multi-index sets of the operands.
+ Grid
+ The union grid with:
+
+ - Union of the two multi-index sets
+ - Union of the two domains
+ - Compatible generating function or points
+
+ Notes
+ -----
+ - This operation provides the infrastructure for polynomial addition:
+ if polynomial ``p1`` is defined on ``grid1`` and ``p2`` on ``grid2``,
+ then their sum ``(p1 + p2)`` is naturally defined
+ on ``(grid1 | grid2)``.
"""
# Add (union) the underlying multi-index sets
mi_union = self.multi_index | other.multi_index
+ domain_union = self.domain | other.domain
- return self.merge(other, mi_union)
+ return self._combine_with(other, mi_union, domain_union)
# --- Private internal methods: not to be called directly from outside
+ def _combine_with(
+ self,
+ other: "Grid",
+ multi_index: MultiIndexSet,
+ domain: Domain,
+ ) -> "Grid":
+ """Combine two compatible grids with specified multi-index and domain.
+
+ This is a low-level helper used by grid operations, e.g., ``__mul__``,
+ ``__or__``. The multi-index set and domain are already computed by the
+ caller; this method picks the compatible generating data and constructs
+ a new instance of Grid.
+
+ Parameters
+ ----------
+ other : Grid
+ The other grid to combine with
+ multi_index : MultiIndexSet
+ The multi-index for the result
+ domain : Domain
+ The domain for the result
+
+ Returns
+ -------
+ Grid
+ New grid constructed with the given multi-index and domain
+ """
+ # Get the generating data
+ gen_fun, gen_points = self._pick_generating_data(other)
+
+ if gen_fun is None:
+ return Grid.from_points(multi_index, gen_points, domain)
+
+ return Grid.from_function(multi_index, gen_fun, domain)
+
def _create_generating_points(self) -> np.ndarray:
"""Construct generating points from the generating function.
@@ -834,39 +1040,158 @@ def _create_generating_points(self) -> np.ndarray:
return generating_function(poly_degree, spatial_dimension)
- def _verify_generating_points(self):
- """Verify if the generating points are valid.
+ def _expand_to_grid(self, target: "Grid") -> "Grid":
+ """Expand the dimension of the Grid to match that of a target Grid.
+
+ This method expands the dimension of the current Grid instance to
+ match the spatial dimension of the target Grid. Both the underlying
+ multi-index set and domain are expanded, and the grids must have
+ compatible generating functions or points.
+
+ Parameters
+ ----------
+ target : Grid
+ The target Grid whose dimension to match. Must have compatible
+ generating functions or points with the current Grid instance.
+
+ Returns
+ -------
+ Grid
+ A new instance of Grid with:
+
+ - Dimension matching the target Grid
+ - Expanded multi-index set
+ - Expanded domain
+ - Compatible generating function or points for both grids
+ """
+ # Expand the domain
+ domain_expanded = self.domain.expand_dim(target.domain)
+
+ # Expand the multi-index set
+ target_dimension = target.spatial_dimension
+ mi_expanded = self.multi_index.expand_dim(target_dimension)
+
+ # Combine with the target Grid and validates compatibility
+ return self._combine_with(target, mi_expanded, domain_expanded)
+
+ def _expand_to_dimension(self, target: int) -> "Grid":
+ """Expand the dimension of the Grid to a target dimension.
+
+ This method expands the dimension of the current Grid instance to
+ the specified dimension (given as integer).
+ The domain must be normalized to allow expansion, as the new dimension
+ is inferred to have the same normalized bounds.
+
+ Parameters
+ ----------
+ target : int
+ The target dimension to expand the Grid to. Must be greater than
+ or equal to the current dimension.
+
+ Returns
+ -------
+ Grid
+ A new instance of Grid with:
+
+ - Dimension equal to the target dimension
+ - Expanded multi-index set
+ - Expanded domain
+ - Same generating function or points as the current instance
+ """
+ # Expand the domain
+ domain_expanded = self.domain.expand_dim(target)
+
+ # Expand the multi-index set
+ mi_expanded = self.multi_index.expand_dim(target)
+
+ # Construct a new instance with the expanded components
+ if self._has_generating_function:
+ return self.__class__.from_function(
+ mi_expanded, self.generating_function, domain_expanded,
+ )
+
+ return self.__class__.from_points(
+ mi_expanded,
+ self.generating_points,
+ domain_expanded,
+ )
+
+ @property
+ def _has_generating_function(self) -> bool:
+ """Return ``True`` if the instance has a generating function.
+
+ Returns
+ -------
+ bool
+ ``True`` if the instance has a generating function assigned to it,
+ and ``False`` otherwise.
+ """
+ return self.generating_function is not None
+
+ def _verify_generating_points(
+ self,
+ generating_points: np.ndarray,
+ ) -> np.ndarray:
+ """Validate generating points.
+
+ Parameters
+ ----------
+ generating_points : np.ndarray
+ The given generating points to validate, a 2D array of
+ shape ``(n + 1, m)`` where ``n`` is the maximum degree of the
+ the grid in any dimension and ``m`` is the maximum spatial
+ dimension of the grid.
+
+ Returns
+ -------
+ np.ndarray
+ Validated generating points.
Raises
------
ValueError
- If the points are not of the correct dimension, contains
- NaN's or inf's, do not fit the standard domain, or the values
- per column are not unique.
+ If the points are not of the correct dimension, contain
+ NaN's or inf's, do not match with the dimension of the grid,
+ the values per column are not unique, or the points are not
+ within the internal domain.
TypeError
If the points are not given in the correct type.
+
+ Notes
+ -----
+ - Generating points may contain more columns (i.e., spatial dimension)
+ than the grid itself (as defined by the dimension of the multi-index
+ set). If there's no generating function, this indicates the maximum
+ dimension the current grid can be expanded to.
"""
- # Check array dimension
- check_dimensionality(self._generating_points, dimensionality=2)
- # No NaN's and inf's
- check_values(self._generating_points)
- # Check domain fit
- check_domain_fit(self._generating_points)
- # Check dimension against spatial dimension of the set
- gen_points_dim = self._generating_points.shape[1]
+ # --- Type and structure checks
+ check_type(generating_points, np.ndarray)
+ check_dimensionality(generating_points, dimensionality=2)
+ check_values(generating_points) # No NaN's and inf's
+
+ # --- Dimension check
+ gen_points_dim = generating_points.shape[1]
if gen_points_dim < self.spatial_dimension:
raise ValueError(
- "Dimension mismatch between the generating points "
- f"({gen_points_dim}) and the multi-index set "
- f"({self.spatial_dimension})"
+ "Dimension mismatch between generating points "
+ f"({gen_points_dim}) and the grid ({self.spatial_dimension})"
)
- # Check the uniqueness of values column-wise
- are_unique = all([is_unique(xx) for xx in self._generating_points.T])
- if not are_unique:
+
+ # --- Uniqueness check (column-wise)
+ if not all(is_unique(col) for col in generating_points.T):
raise ValueError(
"One or more columns of the generating points are not unique"
)
+ # --- Internal domain containment check
+ gen_points_ = generating_points[:, :self.spatial_dimension]
+ if not np.all(self.domain.contains(gen_points_, internal=True)):
+ raise ValueError(
+ "Generating points are not contained in the internal domain"
+ )
+
+ return generating_points
+
def _verify_matching_gen_function_and_points(self):
"""Verify if the generation function and points match.
@@ -920,18 +1245,25 @@ def _verify_grid_max_exponent(self):
f"of {max_exponent_multi_index}"
)
- def _new_instance(self, multi_index: MultiIndexSet) -> "Grid":
- """Construct new grid instance with a new multi-index set.
+ def _new_instance(
+ self,
+ multi_index: MultiIndexSet,
+ domain: Optional[Domain] = None,
+ ) -> "Grid":
+ """Construct a new grid instance with a new multi-index set and domain.
Parameters
----------
multi_index : MultiIndexSet
The multi-index set of the new instance.
+ domain : Domain, optional
+ The domain of the new instance. If not specified, the domain of
+ the current grid will be used.
Returns
-------
Grid
- The new instance of `Grid` with the given multi-index set.
+ A new instance of `Grid` with the given multi-index set.
Notes
-----
@@ -941,19 +1273,83 @@ def _new_instance(self, multi_index: MultiIndexSet) -> "Grid":
the generating function or the generating points, an exception
will be raised.
"""
- if self.has_generating_function:
+ if domain is None:
+ domain = self.domain
+
+ if self._has_generating_function:
return self.__class__.from_function(
multi_index,
- self.generating_function,
+ self._generating_function,
+ domain=domain,
)
return self.__class__.from_points(
multi_index,
- self.generating_points,
+ self._generating_points,
+ domain=domain,
)
+ def _pick_generating_data(
+ self,
+ other: "Grid",
+ ) -> Tuple[Optional[GEN_FUNCTION], Optional[np.ndarray]]:
+ """Pick generating data from two compatible grids.
+
+ This private method validates that the two grids have compatible
+ generating functions or generating points, then selects the appropriate
+ generating data to use for constructing a combined grid.
+ If both grids have compatible generating functions,
+ the generating function; otherwise, the larger set of generating points
+ is used.
+
+ Parameters
+ ----------
+ other : Grid
+ Another instance of Grid to pick compatible generating data from.
+
+ Returns
+ -------
+ Tuple[Optional[GEN_FUNCTION], Optional[np.ndarray]]
+ A tuple containing either (Only one element of the tuple will be
+ non-None):
+
+ - (generating_function, None) if both grids have compatible
+ functions
+ - (None, generating_points) if grids have compatible points.
+
+ Raises
+ ------
+ ValueError
+ If the grids are not compatible.
+
+ Notes
+ -----
+ - This is a low-level helper method used internally by grid combination
+ operations (__mul__, __or__, expand_dim). Compatibility is determined
+ by the is_compatible() method.
+ """
+ if self.has_compatible_gen_function(other):
+ return self.generating_function, None
+
+ if self.has_compatible_gen_points(other):
+ gen_points = _get_larger_gen_points(self, other)
+ return None, gen_points
+
+ raise ValueError(
+ "Cannot pick generating data from incompatible grids. "
+ "Grids must have matching generating functions or compatible "
+ "generating points."
+ )
+
# --- Internal helper functions
+def _gen_unisolvent_nodes(multi_index, generating_points):
+ """
+ .. todo::
+ - document this function but ship it to utils first.
+ """
+ return np.take_along_axis(generating_points, multi_index.exponents, axis=0)
+
def _process_multi_index(multi_index: MultiIndexSet) -> MultiIndexSet:
"""Process the MultiIndexSet given as an argument to Grid constructor.
@@ -985,6 +1381,45 @@ def _process_multi_index(multi_index: MultiIndexSet) -> MultiIndexSet:
return multi_index
+def _process_domain(domain: Domain, multi_index: MultiIndexSet) -> Domain:
+ """Process the Domain given as an argument to Grid constructor.
+
+ Parameters
+ ----------
+ domain : Domain
+ The domain as input argument to the Grid constructor to be
+ processed.
+ multi_index : MultiIndexSet
+ The multi-index set as input argument to the Grid constructor to be
+ processed.
+
+ Returns
+ -------
+ Domain
+ The same instance of :class:`Domain` if processing does not
+ raise any exceptions.
+
+ Raises
+ ------
+ TypeError
+ If the domain argument is not an instance of :class:`Domain`.
+ ValueError
+ If the domain argument does not have the same spatial dimension as
+ the multi-index set.
+ """
+ check_type(domain, Domain)
+
+ # Spatial dimensions must be consistent
+ if domain.spatial_dimension != multi_index.spatial_dimension:
+ raise ValueError(
+ f"Spatial dimension of the domain ({domain.spatial_dimension}) is "
+ "inconsistent with that of the multi-index set "
+ f"({multi_index.spatial_dimension})"
+ )
+
+ return domain
+
+
def _process_generating_function(
generating_function: Optional[Union[GEN_FUNCTION, str]],
) -> Optional[GEN_FUNCTION]:
@@ -1018,139 +1453,6 @@ def _process_generating_function(
)
-def _expand_dim_to_target_dim(
- origin_grid: "Grid",
- target_dimension: int,
-) -> "Grid":
- """Expand the dimension of a given Grid to a target dimension.
-
- Parameters
- ----------
- origin_grid : Grid
- The `Grid` instance whose dimension is to be expanded.
- target_dimension : Grid
- The target dimension; must be equal to or larger than the current
- dimension of the `Grid` instance.
-
- Returns
- -------
- Grid
- The grid with an expanded dimension.
-
- Raises
- ------
- ValueError
- If the target dimension cannot be accommodated by the available
- generating points.
- """
- # Expand the dimension of the multi-index set
- mi_expanded = origin_grid.multi_index.expand_dim(target_dimension)
-
- # Check if a generating function is available
- if origin_grid.has_generating_function:
- return origin_grid.__class__.from_function(
- mi_expanded,
- origin_grid.generating_function,
- )
-
- # Check if the available generating points can accommodate higher dimension
- gen_points_dim = origin_grid.generating_points.shape[1]
- if gen_points_dim >= target_dimension:
- return origin_grid.__class__.from_points(
- mi_expanded,
- origin_grid.generating_points,
- )
-
- raise ValueError(
- f"The available dimension of the generating points ({gen_points_dim} "
- f"can't accommodate target dimension ({target_dimension})"
- )
-
-
-def _expand_dim_to_target_grid(
- origin_grid: "Grid",
- target_grid: "Grid",
-) -> "Grid":
- """Expand the dimension of a given Grid to the dimension of another.
-
- Parameters
- ----------
- origin_grid : Grid
- The grid whose dimension is to be expanded.
- target_grid : Grid
- The grid whose dimension is the base for expansion.
-
- Returns
- -------
- Grid
- The grid with an expanded dimension.
-
- Raises
- ------
- ValueError
- If the generating functions are not compatible (when available) or
- if the generating points are not compatible.
- """
- # Create expanded multi-index set
- target_dim = target_grid.spatial_dimension
- mi_expanded = origin_grid.multi_index.expand_dim(target_dim)
-
- return origin_grid.merge(target_grid, mi_expanded)
-
-
-def _have_gen_functions(*grids) -> bool:
- """Check if a sequence of Grid instances all have generating function."""
- return all(grd.has_generating_function for grd in grids)
-
-
-def _have_compatible_gen_functions(grid_1: "Grid", grid_2: "Grid") -> bool:
- """Check if two grids have compatible generating functions.
-
- Parameters
- ----------
- grid_1 : Grid
- First instance of `Grid` to compare.
- grid_2 : Grid
- Second instance of `Grid` to compare.
- """
- # There is no way in Python to check for equality to what functions do
- return (
- grid_1.has_generating_function
- and grid_2.has_generating_function
- and grid_1.generating_function == grid_2.generating_function
- )
-
-
-def _have_compatible_gen_points(grid_1: "Grid", grid_2: "Grid") -> bool:
- """Check if two grids have compatible generating points.
-
- Parameters
- ----------
- grid_1 : Grid
- First `Grid` instance to compare.
- grid_2 : Grid
- Second `Grid` instance to compare.
-
- Returns
- -------
- bool
- ``True`` if all generating points in the common spatial dimension
- of the two `Grid` instances are equal; ``False`` otherwise.
- """
- dim_1 = grid_1.spatial_dimension
- dim_2 = grid_2.spatial_dimension
- dim = np.min([dim_1, dim_2])
-
- row_1 = grid_1.generating_points.shape[0]
- row_2 = grid_2.generating_points.shape[0]
- row = np.min([row_1, row_2])
-
- gen_points_1 = grid_1.generating_points[:row, :dim]
- gen_points_2 = grid_2.generating_points[:row, :dim]
-
- return np.array_equal(gen_points_1, gen_points_2)
-
-
def _get_larger_gen_points(grid_1: "Grid", grid_2: "Grid") -> np.ndarray:
"""Get the larger array of generating points from two Grid instances.
@@ -1176,4 +1478,4 @@ def _get_larger_gen_points(grid_1: "Grid", grid_2: "Grid") -> np.ndarray:
grids = [grid_1, grid_2]
idx = np.argmax([dim_1, dim_2])
- return grids[idx].generating_points
+ return grids[idx].generating_points
\ No newline at end of file
diff --git a/src/minterpy/core/multi_index.py b/src/minterpy/core/multi_index.py
index 2d6fe441..f09e0bf3 100644
--- a/src/minterpy/core/multi_index.py
+++ b/src/minterpy/core/multi_index.py
@@ -1,4 +1,4 @@
-"""
+r"""
This module contains the implementation of the `MultiIndexSet` class.
The `MultiIndexSet` class represents the multi-index sets of exponents that
@@ -149,7 +149,7 @@ def from_degree(
poly_degree: int,
lp_degree: float = DEFAULT_LP_DEG,
) -> "MultiIndexSet":
- """Create an instance from given spatial dim., poly., and lp-degrees.
+ r"""Create an instance from given spatial dim., poly., and lp-degrees.
Parameters
----------
@@ -1029,7 +1029,11 @@ def __eq__(self, other: "MultiIndexSet") -> bool:
``True`` if the two instances are equal in value, ``False``
otherwise.
"""
- # Check for consistent type
+ # Identity check
+ if self is other:
+ return True
+
+ # Check for a consistent type
if not isinstance(other, MultiIndexSet):
return False
diff --git a/src/minterpy/extras/regression/ordinary_regression.py b/src/minterpy/extras/regression/ordinary_regression.py
index 00f25f6c..a0aef71e 100644
--- a/src/minterpy/extras/regression/ordinary_regression.py
+++ b/src/minterpy/extras/regression/ordinary_regression.py
@@ -27,7 +27,7 @@
eval_newton_monomials,
eval_newton_polynomials,
)
-from minterpy.utils.verification import verify_query_points
+from minterpy.utils.verification import standardize_query_points
from .regression_abc import RegressionABC
__all__ = ["OrdinaryRegression"]
@@ -330,7 +330,7 @@ def fit(
)
# Verify the training points
- xx = verify_query_points(xx, self.multi_index.spatial_dimension)
+ xx = standardize_query_points(xx, self.multi_index.spatial_dimension)
# Get the regression matrix on the data points
self._regression_matrix = self.get_regression_matrix(xx)
diff --git a/src/minterpy/global_settings.py b/src/minterpy/global_settings.py
index 3d0842f7..f70e1249 100644
--- a/src/minterpy/global_settings.py
+++ b/src/minterpy/global_settings.py
@@ -15,7 +15,7 @@
FLOAT_DTYPE = np.float64 # NOTE: numpy.float_ is deprecated in NumPy v2.0
B_DTYPE = np.bool_
-DEFAULT_DOMAIN = np.array([-1, 1])
+DEFAULT_DOMAIN = np.array([-1, 1], dtype=FLOAT_DTYPE)
# --- Numba types. Must match the Numpy dtypes
diff --git a/src/minterpy/interpolation.py b/src/minterpy/interpolation.py
index 21db96f2..ff9f3da3 100644
--- a/src/minterpy/interpolation.py
+++ b/src/minterpy/interpolation.py
@@ -21,206 +21,413 @@
+-------------------+---------------------------------------------------------+
"""
+import attrs
+import numpy as np
-import attr
-
+from numpy.typing import ArrayLike
from typing import Callable, Optional
-from .core import Grid, MultiIndexSet
-from .dds import dds
-from .polynomials import NewtonPolynomial, LagrangePolynomial
-from .transformations import NewtonToCanonical, NewtonToChebyshev
-from .global_settings import DEFAULT_LP_DEG
+from minterpy.core import Grid, MultiIndexSet, Domain
+from minterpy.dds import dds
+from minterpy.polynomials import (
+ NewtonPolynomial,
+ LagrangePolynomial,
+ CanonicalPolynomial,
+ ChebyshevPolynomial,
+)
+from minterpy.transformations import NewtonToCanonical, NewtonToChebyshev
+from minterpy.global_settings import DEFAULT_LP_DEG
__all__ = ["Interpolator", "Interpolant", "interpolate"]
class InterpolationError(Exception):
"""Exception raised if the interpolation went wrong."""
-
pass
-@attr.s(frozen=True, order=False, eq=False)
+@attrs.define(frozen=True, order=False, eq=False)
class Interpolator:
- """The construction class for interpolation.
+ r"""The class for constructing polynomial interpolant.
- Data type which contains all relevant parts for interpolation and caches them.
+ The class contains all the relevant parts for constructing a polynomial
+ interpolant in the Newton basis of a given callable.
+ The instance of this class is a callable; passing a function to it will
+ return an interpolating polynomial.
- Attributes
+ With an instance of this class, one can construct interpolants of the
+ same underlying polynomial for different functions.
+
+ Parameters
----------
- spatial_dimension : dimension of the domain space.
- poly_degree : degree of the interpolation polynomials
- lp_degree : degree of the :math:`l_p` norm used to determine the `poly_degree`.
- multi_index : lexicographically complete multi index set build from `(spatial_dimension, poly_degree, lp_degree)`.
- grid : `Grid` instance build from `multi_index`.
+ spatial_dimension : int
+ The dimension of the interpolator.
+ poly_degree : int
+ The degree of the interpolating polynomial.
+ lp_degree : float
+ The degree :math:`p` of the :math:`l_p`-norm used to define
+ the (multivariate) polynomial degree.
+ bounds : array_like, optional
+ The bounds of the domain space, an array of shape ``(m, 2)``,
+ where ``m`` is the spatial dimension. Each row corresponds to the
+ lower and upper bounds of the domain in the corresponding dimension.
+ If not provided, the bounds are assumed to be :math:`[-1, 1]^m`.
+ Attributes
+ ----------
+ multi_index : MultiIndexSet
+ The multi-index set used to define the interpolating polynomial,
+ a lexicographically complete multi-index set, i.e.,
+ :math:`\mathcal{A}_{m, n, p}`, where :math:`m` is
+ ``spatial_dimension``, :math:`n` is ``poly_degree``,
+ and :math:`p` is the ``lp_degree``.
+ grid : Grid
+ The underlying interpolation grid with the domain defined
+ by ``bounds``.
"""
+ # Constructor parameters
+ _spatial_dimension: int = attrs.field(repr=False)
+ _poly_degree: int = attrs.field(repr=False)
+ _lp_degree: float = attrs.field(repr=False)
+ _bounds: Optional[ArrayLike] = attrs.field(default=None, repr=False)
- spatial_dimension: int = attr.ib()
- poly_degree: int = attr.ib()
- lp_degree: int = attr.ib()
-
- multi_index = attr.ib(init=False, repr=False)
- grid = attr.ib(init=False, repr=False)
+ # --- Core properties (computed based on constructor parameters)
+ multi_index: MultiIndexSet = attrs.field(init=False, repr=False)
+ grid: Grid = attrs.field(init=False, repr=False)
@multi_index.default
- def __multi_index_default(self) -> MultiIndexSet:
+ def _multi_index_default(self) -> MultiIndexSet:
return MultiIndexSet.from_degree(
- self.spatial_dimension, self.poly_degree, self.lp_degree
+ self._spatial_dimension,
+ self._poly_degree,
+ self._lp_degree
)
@grid.default
- def __grid_default(self) -> Grid:
- return Grid(self.multi_index)
+ def _grid_default(self) -> Grid:
+ if self._bounds is not None:
+ domain = Domain(self._bounds)
+ else:
+ domain = None
+ return Grid(self.multi_index, domain=domain)
+
+ # --- Derived properties
+ @property
+ def spatial_dimension(self) -> int:
+ """The dimension of the interpolator."""
+ return self.multi_index.spatial_dimension
- def __call__(self, fct: Callable) -> Optional[NewtonPolynomial]:
- """Interpolate a given function.
+ @property
+ def poly_degree(self) -> int:
+ """The degree of the interpolating polynomial (and multi-index set)."""
+ return self.multi_index.poly_degree
- Builds a `NewtonPolynomial` which interpolates the given `fct`, where the precomuted setting of the current instance is used.
+ @property
+ def lp_degree(self) -> float:
+ """:math:`p` of :math:`l_p`-norm used to define the multi-index set."""
+ return self.multi_index.lp_degree
- :param fct: Function to be interpolated. Needs to be (numpy) universal function which shall be interpolated. If `arr` is an :class:`np.ndarray` with shape ``arr.shape == (N,spatial_dimension)``, the signature needs to be ``fct(arr) -> res``, where ``res`` is an :class:`np.ndarray` with shape ``(N,)``.
- :type fct: Callable
+ @property
+ def domain(self) -> Domain:
+ """The domain of the interpolating polynomial."""
+ return self.grid.domain
+
+ # --- Dunder methods
+ def __repr__(self) -> str:
+ return (
+ f"Interpolator(spatial_dimension={self.spatial_dimension}, "
+ f"poly_degree={self.poly_degree}, "
+ f"lp_degree={self.lp_degree})"
+ )
- :return: Interpolation polynomial in Newton form, which interpolates the function ``fct``, where the used divided difference scheme is build from ``self.multi_index`` and ``self.grid``.
- :rtype: NewtonPolynomial
+ def __call__(self, func: Callable) -> NewtonPolynomial:
+ """Interpolate a given function and return an interpolating polynomial.
+
+ Parameters
+ ----------
+ func : Callable
+ The function to interpolate. It must accept as the first argument
+ a :class:`numpy:numpy.ndarray` with shape ``(k, m)``, where ``k``
+ is the number of evaluation points and ``m`` is the spatial
+ dimension. The function returns a :class:`numpy:numpy.ndarray`
+ with shape ``(k, )`` for scalar outputs and ``(k, d)``
+ for vector outputs, where ``d`` is the output dimension.
+
+ Returns
+ -------
+ NewtonPolynomial
+ An interpolating polynomial of the function ``func`` in the Newton
+ basis. The interpolating polynomial is constructed according
+ to the underlying multi-index set and interpolation grid.
+
+ Raises
+ ------
+ InterpolationError
+ If anything goes wrong with the interpolation.
+ """
+ try:
+ func_values = self.grid(func)
+ except Exception as e:
+ raise InterpolationError(e) from e
+
+ return self.interpolate_values(func_values)
+
+ def interpolate_values(self, func_values: ArrayLike) -> NewtonPolynomial:
+ """Interpolate a given array of values at the unisolvent nodes.
+
+ Parameters
+ ----------
+ func_values : array_like
+ The function values at the unisolvent nodes (i.e., the Lagrange
+ coefficients of the interpolating polynomial).
- :raises InterpolationError: Raised if anything goes wrong with the interpolation.
+ Returns
+ -------
+ NewtonPolynomial
+ An interpolating polynomial of the function ``func`` in the Newton
+ basis. The interpolating polynomial is constructed according
+ to the underlying multi-index set and interpolation grid.
+
+ Raises
+ ------
+ InterpolationError
+ If anything goes wrong with the interpolation.
"""
try:
- fct_values = self.grid(fct)
- # NOTE: Don't use np.squeeze as DDS results may be of shape (1,1)
- interpol_coeffs = dds(fct_values, self.grid.tree).reshape(-1)
+ coeffs = dds(func_values, self.grid.tree)
+ # DDS returns shape (N, d) where N=#points, d=output_dim
+ # For scalar functions (d=1), convert to 1D array of shape (N,)
+ if coeffs.shape[1] == 1:
+ coeffs = coeffs[:, 0] # Most explicit and safe
except Exception as e:
raise InterpolationError(e) from e
- return NewtonPolynomial(self.multi_index, interpol_coeffs)
+ return NewtonPolynomial(self.multi_index, coeffs, grid=self.grid)
-@attr.s(frozen=True, order=False, eq=False)
+@attrs.define(frozen=True, order=False, eq=False)
class Interpolant:
- """Data type representing the result of an interpolation.
+ """A class representing the result of an interpolation of a given function.
- Instances of this class can be used as functions, which interpolate a given function. Users who do not want to learn anything about neither polynomial interpolation nor bases in multivariate polynomial bases may use the instances of this class just as an interpolative representant of their function, which they can evaluate. (Other properties are conceivable too)
+ An instance of this class is a callable that interpolates a given function.
+ It serves as an intermediate layer between function approximation
+ and the corresponding interpolating polynomial representation; it can be
+ used without any in-depth knowledge of the underlying polynomial
+ representation.
- Attributes
+ Parameters
----------
- fct : Function to be interpolated. Needs to be (numpy) universal function which shall be interpolated. If `arr` is an :class:`np.ndarray` with shape ``arr.shape == (N,spatial_dimension)``, the signature needs to be ``fct(arr) -> res``, where ``res`` is an :class:`np.ndarray` with shape ``(N,)``.
- interpolator : Instance of :class:`Interpolator`, which represents the interpolation scheme to be used.
+ func : Callable
+ The function to interpolate. It must accept as the first argument
+ a :class:`numpy:numpy.ndarray` with shape ``(k, m)``, where ``k``
+ is the number of evaluation points and ``m`` is the spatial
+ dimension. The function returns a :class:`numpy:numpy.ndarray`
+ with shape ``(k, )`` for scalar outputs and ``(k, d)``
+ for vector outputs, where ``d`` is the output dimension.
+ interpolator : Interpolator
+ The underlying setting for the interpolation.
"""
+ # --- Constructor parameters
+ func: Callable = attrs.field(repr=False)
+ interpolator: Interpolator = attrs.field(repr=False)
- fct: Callable = attr.ib(repr=False)
- interpolator: Interpolator = attr.ib(repr=False)
- __interpolation_poly: NewtonPolynomial = attr.ib(init=False, repr=False)
-
- @__interpolation_poly.default
- def __interpolation_poly_default(self):
- return self.interpolator(self.fct)
+ # --- Private attribute
+ _func_values: np.ndarray = attrs.field(init=False, repr=False)
+ _interpolation_poly: NewtonPolynomial = attrs.field(init=False, repr=False)
- @classmethod
- def from_degree(cls, fct, spatial_dimension, poly_degree, lp_degree):
- """Custom constructor of an interpolant using dimensionality and degree parameter.
-
- :param fct: Function to be interpolated. Needs to be (numpy) universal function which shall be interpolated. If `arr` is an :class:`np.ndarray` with shape ``arr.shape == (N,spatial_dimension)``, the signature needs to be ``fct(arr) -> res``, where ``res`` is an :class:`np.ndarray` with shape ``(N,)``.
- :type fct: Callable
+ @_func_values.default
+ def _func_values_default(self):
+ return self.interpolator.grid(self.func)
- :param spatial_dimension: dimension of the domain space.
- :type spatial_dimension: int
- :param poly_degree: degree of the interpolation polynomials
- :type poly_degree: int
- :param lp_degree: degree of the :math:`l_p` norm used to determine the `poly_degree`.
- :type lp_degree: int
+ @_interpolation_poly.default
+ def _interpolation_poly_default(self):
+ return self.interpolator.interpolate_values(self._func_values)
- :return: The interpolant of ``fct`` using the default interpolator build from ``(spatial_dimension, poly_degree, lp_degree)``.
- :rtype: Interpolant
+ # --- Factory method
+ @classmethod
+ def from_degree(
+ cls,
+ func: Callable,
+ spatial_dimension: int,
+ poly_degree: int,
+ lp_degree: float,
+ bounds: Optional[ArrayLike] = None,
+ ) -> "Interpolant":
+ r"""Create an interpolant with respect to a complete multi-index set.
+
+ Parameters
+ ----------
+ func : Callable
+ The function to interpolate. It must accept as the first argument
+ a :class:`numpy:numpy.ndarray` with shape ``(k, m)``, where ``k``
+ is the number of evaluation points and ``m`` is the spatial
+ dimension. The function returns a :class:`numpy:numpy.ndarray`
+ with shape ``(k, )`` for scalar outputs and ``(k, d)``
+ for vector outputs, where ``d`` is the output dimension.
+ spatial_dimension : int
+ The dimension of the interpolator.
+ poly_degree : int
+ The degree of the interpolating polynomial.
+ lp_degree : float
+ The degree :math:`p` of the :math:`l_p`-norm used to define
+ the (multivariate) polynomial degree.
+ bounds : array_like, optional
+ The bounds of the domain space, an array of shape ``(m, 2)``,
+ where ``m`` is the spatial dimension. Each row corresponds to the
+ lower and upper bounds of the domain in the corresponding dimension.
+ If not provided, the bounds are assumed to be :math:`[-1, 1]^m`.
+
+ Returns
+ --------
+ Interpolant
+ An instance of interpolant of ``func`` using an interpolating
+ polynomial with respect to complete multi-index set.
+
+ Notes
+ -----
+ - ``spatial_dimension`` (:math:`m`), ``poly_degree`` (:math:`n`),
+ ``lp_degree`` (:math:`p`) are used to construct the lexicographically
+ complete multi-index set :math:`\mathcal{A}_{m, n, p}`.
"""
- return cls(fct, Interpolator(spatial_dimension, poly_degree, lp_degree))
+ return cls(
+ func,
+ Interpolator(spatial_dimension, poly_degree, lp_degree, bounds),
+ )
+ # --- Properties
@property
- def spatial_dimension(self):
- """Dimension of the domain space the interpolation polynomial lives on.
-
- This is the propagated attribute from ``self.interpolator``.
-
- :rtype: int
- """
+ def spatial_dimension(self) -> int:
+ """The dimension of the interpolator."""
return self.interpolator.spatial_dimension
@property
- def poly_degree(self):
- """Degree of the interpolation polynomial.
-
- This is the propagated attribute from ``self.interpolator``.
-
- :rtype: int
- """
+ def poly_degree(self) -> int:
+ """The degree of the interpolating polynomial."""
return self.interpolator.poly_degree
@property
- def lp_degree(self):
- """Degree of the :math:`l_p` norm.
-
- This is the propagated attribute from ``self.interpolator``.
-
- :rtype: int
- """
+ def lp_degree(self) -> float:
+ """:math:`p` of :math:`l_p`-norm used to define the multi-index set."""
return self.interpolator.lp_degree
@property
- def lagrange_coeffs(self):
- """Return the Lagrange coefficients of the interpolating polynomial."""
- return self.interpolator.grid(self.fct)
+ def multi_index(self) -> MultiIndexSet:
+ """The multi-index set defining the interpolating polynomial."""
+ return self.interpolator.multi_index
+
+ # --- Public methods
+ def to_newton(self) -> NewtonPolynomial:
+ """Return the interpolant as a polynomial in the Newton basis.
+
+ Returns
+ -------
+ NewtonPolynomial
+ The interpolating polynomial represented in the Newton basis.
+ """
+ return self._interpolation_poly
- def to_newton(self):
- """Return the interpolant as a polynomial in the Newton basis."""
- return self.__interpolation_poly
+ def to_lagrange(self) -> LagrangePolynomial:
+ """Return the interpolant as a polynomial in the Lagrange basis.
- def to_lagrange(self):
- """Return the interpolant as a polynomial in the Lagrange basis."""
+ Returns
+ -------
+ LagrangePolynomial
+ The interpolating polynomial represented in the Lagrange basis.
+ """
return LagrangePolynomial.from_grid(
self.interpolator.grid,
- self.lagrange_coeffs,
+ self._func_values,
)
- def to_canonical(self):
- """Return the interpolant as a polynomial in the canonical basis."""
- nwt_poly = self.__interpolation_poly
-
- return NewtonToCanonical(nwt_poly)()
-
- def to_chebyshev(self):
- """Return the interpolant as a polynomial in the Chebyshev basis."""
- nwt_poly = self.__interpolation_poly
-
- return NewtonToChebyshev(nwt_poly)()
-
- def __call__(self, pts):
- """Evaulate the interpolant on a given array of points.
-
- :param pts: Array of points, where the shape needs to be ``pts.shape == (N,spatial_dimension)``,
+ def to_canonical(self) -> CanonicalPolynomial:
+ """Return the interpolant as a polynomial in the canonical basis.
+ Returns
+ -------
+ CanonicalPolynomial
+ The interpolating polynomial represented in the canonical
+ (monomial) basis.
"""
- return self.__interpolation_poly(pts)
+ nwt_poly = self._interpolation_poly
+ return NewtonToCanonical(nwt_poly)()
-def interpolate(fct, spatial_dimension, poly_degree, lp_degree=DEFAULT_LP_DEG):
- """Interpolate a given function.
+ def to_chebyshev(self) -> ChebyshevPolynomial:
+ """Return the interpolant as a polynomial in the Chebyshev basis.
- Return an interpolant, which represents the given function on the domain :math:`[-1, 1]^d`, where :math:`d` is the dimension of the domain space.
+ Returns
+ -------
+ ChebyshevPolynomial
+ The interpolating polynomial represented in the Chebyshev basis
+ (of the first kind).
+ """
+ nwt_poly = self._interpolation_poly
+ return NewtonToChebyshev(nwt_poly)()
+ # --- Dunder methods
+ def __call__(self, xx: ArrayLike, **kwargs) -> np.ndarray:
+ """Evaluate the interpolant on a given array of points.
+
+ Parameters
+ ----------
+ xx : array_like
+ Query points to evaluate. Can be a scalar, list, or numpy array.
+ The points will be standardized to a two-dimensional array of
+ shape ``(k, m)`` where ``k`` is the number of query points and
+ ``m`` is the spatial dimension of the polynomial.
+ **kwargs
+ Additional keyword-only arguments that change the behavior of
+ the underlying evaluation (see the concrete implementation of the
+ interpolating polynomial).
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The values of the polynomial evaluated at query points.
+ """
+ return self._interpolation_poly(xx)
- :param fct: Function to be interpolated. Needs to be (numpy) universal function which shall be interpolated. If `arr` is an :class:`np.ndarray` with shape ``arr.shape == (N,spatial_dimension)``, the signature needs to be ``fct(arr) -> res``, where ``res`` is an :class:`np.ndarray` with shape ``(N,)``.
- :type fct: Callable
- :param spatial_dimension: dimension of the domain space.
- :type spatial_dimension: int
- :param poly_degree: degree of the interpolation polynomials
- :type poly_degree: int
- :param lp_degree: degree of the :math:`l_p` norm used to determine the `poly_degree`.
- :type lp_degree: int
+def interpolate(
+ func: Callable,
+ spatial_dimension: int,
+ poly_degree: int,
+ lp_degree: float = DEFAULT_LP_DEG,
+ bounds: Optional[ArrayLike] = None,
+) -> Interpolant:
+ r"""Interpolate a function using a complete multi-index set polynomial.
- :return: The interpolant of ``fct`` using the default interpolator build from ``(spatial_dimension, poly_degree, lp_degree)``.
- :rtype: Interpolant
+ Parameters
+ ----------
+ func : Callable
+ The function to interpolate. It must accept as the first argument
+ a :class:`numpy:numpy.ndarray` with shape ``(k, m)``, where ``k``
+ is the number of evaluation points and ``m`` is the spatial
+ dimension. The function returns a :class:`numpy:numpy.ndarray`
+ with shape ``(k, )`` for scalar outputs and ``(k, d)``
+ for vector outputs, where ``d`` is the output dimension.
+ spatial_dimension : int
+ The dimension of the interpolator.
+ poly_degree : int
+ The degree of the interpolating polynomial.
+ lp_degree : float, optional
+ The degree :math:`p` of the :math:`l_p`-norm used to define
+ the (multivariate) polynomial degree.
+ bounds : array_like, optional
+ The bounds of the domain space, an array of shape ``(m, 2)``,
+ where ``m`` is the spatial dimension. Each row corresponds to the
+ lower and upper bounds of the domain in the corresponding dimension.
+ If not provided, the bounds are assumed to be :math:`[-1, 1]^m`.
+
+ Returns
+ -------
+ Interpolant
+ The interpolant of ``func`` with respect to the lexicographically
+ complete multi-index set :math:`\mathcal{A}_{m, n, p}` where :math:`m`
+ is ``spatial_dimension``, :math:`n` is ``poly_degree``, and :math:`p`
+ is ``lp_degree``.
"""
- return Interpolant.from_degree(fct, spatial_dimension, poly_degree, lp_degree)
+ m, n, p = spatial_dimension, poly_degree, lp_degree
+
+ return Interpolant.from_degree(func, m, n, p, bounds)
diff --git a/src/minterpy/jit_compiled/common.py b/src/minterpy/jit_compiled/common.py
index 18d1c3c4..c42c04cf 100644
--- a/src/minterpy/jit_compiled/common.py
+++ b/src/minterpy/jit_compiled/common.py
@@ -69,7 +69,7 @@ def n_choose_r(n: int, r: int) -> int:
@njit(I_2D(I_1D, UINT32), cache=True)
def combinations_iter(xx: np.ndarray, r: int) -> np.ndarray:
- """Return successive r-length combinations of elements as an array.
+ r"""Return successive r-length combinations of elements as an array.
Parameters
----------
diff --git a/src/minterpy/jit_compiled/multi_index.py b/src/minterpy/jit_compiled/multi_index.py
index 86889f2b..4ff82a3d 100644
--- a/src/minterpy/jit_compiled/multi_index.py
+++ b/src/minterpy/jit_compiled/multi_index.py
@@ -210,7 +210,7 @@ def is_lex_sorted(indices: np.ndarray) -> bool:
@njit(INT(I_2D, I_1D), cache=True)
def search_lex_sorted(indices: np.ndarray, index: np.ndarray) -> int:
- """Find the position of a given entry within an array of multi-indices.
+ r"""Find the position of a given entry within an array of multi-indices.
Parameters
----------
diff --git a/src/minterpy/jit_compiled/newton/eval.py b/src/minterpy/jit_compiled/newton/eval.py
index 28cc86db..efe81656 100644
--- a/src/minterpy/jit_compiled/newton/eval.py
+++ b/src/minterpy/jit_compiled/newton/eval.py
@@ -82,7 +82,7 @@ def eval_newton_monomials_multiple(
monomials_placeholder: np.ndarray,
triangular: bool
) -> None:
- """Evaluate the Newton monomials at multiple query points.
+ r"""Evaluate the Newton monomials at multiple query points.
The following notations are used below:
diff --git a/src/minterpy/polynomials/arithmetic.py b/src/minterpy/polynomials/arithmetic.py
new file mode 100644
index 00000000..cee6441e
--- /dev/null
+++ b/src/minterpy/polynomials/arithmetic.py
@@ -0,0 +1,366 @@
+"""
+Implementations of arithmetic operations on polynomials at coefficients level.
+"""
+import numpy as np
+
+from typing import Callable, Literal, Tuple
+
+from minterpy.core.ABC import MultivariatePolynomialSingleABC
+from minterpy.core.multi_index import MultiIndexSet
+from minterpy.core.grid import Grid
+from minterpy.jit_compiled.canonical import compute_coeffs_poly_prod
+from minterpy.utils.multi_index import find_match_between
+
+
+def select_active_monomials(
+ coeffs: np.ndarray,
+ grid: Grid,
+ active_multi_index: MultiIndexSet,
+) -> np.ndarray:
+ """Get the coefficients that corresponds to the active monomials.
+
+ Parameters
+ ----------
+ coeffs : :class:`numpy:numpy.ndarray`
+ The coefficients of a polynomial associated with the multi-index set
+ of the grid on which the polynomial lives. They are stored in an array
+ whose length is the same as the length of ``grid.multi_index``.
+ grid : Grid
+ The grid on which the polynomial lives.
+ active_multi_index : MultiIndexSet
+ The multi-index set of active monomials; the coefficients will be
+ picked according to this multi-index set.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The coefficients of a polynomial associated with the active monomials
+ as specified by ``multi_index``.
+
+ Notes
+ -----
+ - ``active_multi_index`` must be a subset of ``grid.multi_index``. This
+ precondition is assumed to be fulfilled by construction upstream.
+ """
+ exponents_multi_index = active_multi_index.exponents
+ exponents_grid = grid.multi_index.exponents
+ active_idx = find_match_between(exponents_multi_index, exponents_grid)
+
+ return coeffs[active_idx]
+
+def get_compute_coeffs_add(mode: Literal["monomials", "lagrange"]) -> Callable:
+ """Get the function to compute the coefficients of a polynomial sum.
+
+ Parameters
+ ----------
+ mode : str
+ The mode of the coefficients computation.
+
+ Returns
+ -------
+ Callable
+ The function to compute the coefficients of a polynomial sum.
+ """
+ if mode == "monomials":
+ return _compute_coeffs_poly_add_via_monomials
+
+ return _compute_coeffs_poly_add_via_lagrange
+
+
+def get_compute_coeffs_mul(mode: Literal["monomials", "lagrange"]) -> Callable:
+ """Get the function to compute the coefficients of a polynomial product.
+
+ Parameters
+ ----------
+ mode : str
+ The mode of the coefficients computation.
+
+ Returns
+ -------
+ Callable
+ The function to compute the coefficients of a polynomial product.
+ """
+ if mode == "monomials":
+ return _compute_coeffs_poly_mul_via_monomials
+
+ return _compute_coeffs_poly_mul_via_lagrange
+
+
+def _compute_coeffs_poly_add_via_monomials(
+ poly_1: MultivariatePolynomialSingleABC,
+ poly_2: MultivariatePolynomialSingleABC,
+ multi_index_add: MultiIndexSet,
+) -> np.ndarray:
+ r"""Compute the coefficients of a summed polynomial via the monomials.
+
+ For example, suppose: :math:`A = \{ (0, 0) , (1, 0), (0, 1) \}` with
+ coefficients :math:`c_A = (1.0 , 2.0, 3.0)` is summed with
+ :math:`B = \{ (0, 0), (1, 0), (2, 0) \}` with coefficients
+ :math:`c_B = (1.0, 5.0, 3.0)`. The union/sum multi-index set is
+ :math:`A \times B = \{ (0, 0), (1, 0), (2, 0), (0, 1) \}`.
+
+ The corresponding coefficients of the sum are:
+
+ - :math:`(0, 0)` appears in both operands, so the coefficient
+ is :math:`1.0 + 1.0 = 2.0`
+ - :math:`(1, 0)` appears in both operands, so the coefficient is
+ :math:`2.0 + 5.0 = 7.0`
+ - :math:`(2, 0)` only appears in the second operand, so the coefficient
+ is :math:`3.0`
+ - :math:`(0, 1)` only appears in the first operand, so the coefficient
+ is :math:`3.0`
+
+ or :math:`c_{A | B} = (2.0, 7.0, 3.0, 3.0)`.
+
+ Parameters
+ ----------
+ poly_1 : MultivariatePolynomialSingleABC
+ Left operand of the polynomial-polynomial addition.
+ poly_2 : MultivariatePolynomialSingleABC
+ Right operand of the polynomial-polynomial addition.
+ multi_index_add : MultiIndexSet
+ The multi-index set of the summed polynomial, i.e., the union
+ of the multi-index sets of the two operands.
+
+ Notes
+ -----
+ - ``multi_index_sum`` is assumed to be the result of unionizing
+ ``poly_1.multi_index`` and ``poly_2.multi_index``.
+ - The lengths of ``poly_1`` and ``poly_2`` are assumed to be the same.
+ - The function does not check whether the above assumptions are fulfilled;
+ the caller is responsible to make sure of that. If the assumptions are
+ not fulfilled, the function may not raise any exception but produce
+ the wrong results.
+ """
+ # Shape the coefficients; ensure they have the same dimension
+ coeffs_1, coeffs_2 = _shape_coeffs(poly_1.coeffs, poly_2.coeffs)
+
+ # Get the exponents
+ mi_1 = _match_mi_dim(poly_1.multi_index, multi_index_add)
+ mi_2 = _match_mi_dim(poly_2.multi_index, multi_index_add)
+
+ exponents_1 = mi_1.exponents
+ exponents_2 = mi_2.exponents
+ exponents_sum = multi_index_add.exponents
+
+ # Create the output array
+ num_monomials = len(multi_index_add)
+ num_polynomials = len(poly_1)
+ coeffs_poly_sum = np.zeros((num_monomials, num_polynomials))
+
+ # Get the matching indices
+ idx_1 = find_match_between(exponents_1, exponents_sum)
+ idx_2 = find_match_between(exponents_2, exponents_sum)
+
+ coeffs_poly_sum[idx_1, :] += coeffs_1[:, :]
+ coeffs_poly_sum[idx_2, :] += coeffs_2[:, :]
+
+ return coeffs_poly_sum
+
+
+def _compute_coeffs_poly_add_via_lagrange(
+ poly_1: MultivariatePolynomialSingleABC,
+ poly_2: MultivariatePolynomialSingleABC,
+ grid_add: Grid,
+) -> np.ndarray:
+ """Compute the coefficients of a summed polynomial via Lagrange polynomial.
+
+ Parameters
+ ----------
+ poly_1 : MultivariatePolynomialSingleABC
+ Left operand of the addition/subtraction expression.
+ poly_2 : MultivariatePolynomialSingleABC
+ Right operand of the addition/subtraction expression.
+ grid_add : Grid
+ The Grid associated with the summed polynomial.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The coefficients of the summed polynomial in the Newton basis.
+ """
+ # Compute the values of the operands at the unisolvent nodes
+ # NOTE: The grid may be of higher dimension than one of the polynomials;
+ # evaluation must ignore the extra dimensions
+ nodes_add = grid_add.unisolvent_nodes
+ lag_coeffs_1 = poly_1.eval_on_internal(nodes_add, truncate_cols=True)
+ lag_coeffs_2 = poly_2.eval_on_internal(nodes_add, truncate_cols=True)
+ lag_coeffs_add = lag_coeffs_1 + lag_coeffs_2
+
+ return lag_coeffs_add
+
+
+def _compute_coeffs_poly_mul_via_monomials(
+ poly_1: MultivariatePolynomialSingleABC,
+ poly_2: MultivariatePolynomialSingleABC,
+ multi_index_mul: MultiIndexSet,
+) -> np.ndarray:
+ r"""Compute the coefficients of a product polynomial via the monomials.
+
+ For example, suppose: :math:`A = \{ (0, 0) , (1, 0), (0, 1) \}` with
+ coefficients :math:`c_A = (1.0 , 2.0, 3.0)` is multiplied with
+ :math:`B = \{ (0, 0) , (1, 0) \}` with coefficients
+ :math:`c_B = (1.0 , 5.0)`. The product multi-index set is
+ :math:`A \times B = \{ (0, 0) , (1, 0), (2, 0), (0, 1), (1, 1) \}`.
+
+ The corresponding coefficients of the product are:
+
+ - :math:`(0, 0)` is coming from :math:`(0, 0) + (0, 0)`, the coefficient
+ is :math:`1.0 \times 1.0 = 1.0`
+ - :math:`(1, 0)` is coming from :math:`(0, 0) + (1, 0)` and
+ :math:`(1, 0) + (0, 0)`, the coefficient is
+ :math:`1.0 \times 5.0 + 2.0 \times 1.0 = 7.0`
+ - :math:`(2, 0)` is coming from :math:`(1, 0) + (1, 0)`, the coefficient
+ is :math:`2.0 \times 5.0 = 10.0`
+ - :math:`(0, 1)` is coming from :math:`(0, 1) + (0, 0)`, the coefficient
+ is :math:`3.0 \times 1.0 = 3.0`
+ - :math:`(1, 1)` is coming from :math:`(0, 1) + (1, 0)`, the coefficient
+ is :math:`3.0 \times 5.0 = 15.0`
+
+ or :math:`c_{A \times B} = (1.0, 7.0, 10.0, 3.0, 15.0)`.
+
+ Parameters
+ ----------
+ poly_1 : MultivariatePolynomialSingleABC
+ Left operand of the polynomial-polynomial multiplication expression.
+ poly_2 : MultivariatePolynomialSingleABC
+ Right operand of the polynomial-polynomial multiplication expression.
+ multi_index_mul : MultiIndexSet
+ The multi-index set of the product polynomial, i.e., the product of the
+ multi-index sets of the two operands.
+
+ Notes
+ -----
+ - ``multi_index_mul`` is assumed to be the result of multiplying
+ ``poly_1.multi_index`` and ``poly_2.multi_index``.
+ - The lengths of ``poly_1`` and ``poly_2`` are assumed to be the same.
+ - The function does not check whether the above assumptions are fulfilled;
+ the caller is responsible to make sure of that. If the assumptions are
+ not fulfilled, the function may not raise any exception but produce
+ the wrong results.
+ """
+ # Shape the coefficients; ensure they have the same dimension
+ coeffs_1, coeffs_2 = _shape_coeffs(poly_1.coeffs, poly_2.coeffs)
+
+ # Pre-allocate output array placeholder
+ num_monomials = len(multi_index_mul)
+ num_polys = len(poly_1)
+ coeffs_mul = np.zeros((num_monomials, num_polys))
+
+ # Compute the coefficients (use pre-allocated placeholder as output)
+ # NOTE: Handle separated indices case, i.e., use the provided multi_index
+ # rather than grid.multi_index
+ mi_1 = _match_mi_dim(poly_1.multi_index, multi_index_mul)
+ mi_2 = _match_mi_dim(poly_2.multi_index, multi_index_mul)
+
+ exponents_1 = mi_1.exponents
+ exponents_2 = mi_2.exponents
+ exponents_mul = multi_index_mul.exponents
+ compute_coeffs_poly_prod(
+ exponents_1,
+ coeffs_1,
+ exponents_2,
+ coeffs_2,
+ exponents_mul,
+ coeffs_mul,
+ )
+
+ return coeffs_mul
+
+
+def _compute_coeffs_poly_mul_via_lagrange(
+ poly_1: MultivariatePolynomialSingleABC,
+ poly_2: MultivariatePolynomialSingleABC,
+ grid_mul: Grid,
+) -> np.ndarray:
+ """Compute the coefficients of a product Newton polynomial via Lagrange.
+
+ Parameters
+ ----------
+ poly_1 : NewtonPolynomial
+ Left operand of the multiplication.
+ poly_2 : NewtonPolynomial
+ Right operand of the multiplication.
+ grid_mul : Grid
+ The Grid associated with the product polynomial, i.e., the product of
+ the grids of the two operands.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ The coefficients of the product polynomial in the Newton basis.
+
+ Notes
+ -----
+ - Both polynomials are assumed to have the same spatial dimension and
+ matching domains. These conditions have been made sure upstream.
+ """
+ # Compute the values of the operands at the unisolvent nodes
+ # NOTE: The grid may be of higher dimension than one of the polynomials;
+ # evaluation must ignore the extra dimensions
+ nodes_mul = grid_mul.unisolvent_nodes
+ lag_coeffs_1 = poly_1.eval_on_internal(nodes_mul, truncate_cols=True)
+ lag_coeffs_2 = poly_2.eval_on_internal(nodes_mul, truncate_cols=True)
+ lag_coeffs_prod = lag_coeffs_1 * lag_coeffs_2
+
+ return lag_coeffs_prod
+
+
+def _shape_coeffs(
+ coeffs_1: np.ndarray,
+ coeffs_2: np.ndarray,
+) -> Tuple[np.ndarray, np.ndarray]:
+ """Shape the coefficients of two polynomials to be two-dimensional.
+
+ Parameters
+ ----------
+ coeffs_1 : np.ndarray
+ The coefficients of the first polynomial.
+ coeffs_2 : np.ndarray
+ The coefficients of the second polynomial.
+
+ Returns
+ -------
+ Tuple[np.ndarray, np.ndarray]
+ The coefficients of the two polynomials, shaped to be two-dimensional.
+
+ Notes
+ -----
+ - A polynomial with a single set of coefficients is assumed to be
+ one-dimensional, but certain operations must be performed on
+ two-dimensional arrays. For example, a 1D array of shape ``(N,)``
+ is reshaped to ``(N, 1)`` where ``N`` is the number of coefficients.
+ """
+ if coeffs_1.ndim == 1:
+ coeffs_1 = coeffs_1[:, np.newaxis]
+
+ if coeffs_2.ndim == 1:
+ coeffs_2 = coeffs_2[:, np.newaxis]
+
+ return coeffs_1, coeffs_2
+
+
+def _match_mi_dim(mi_1: MultiIndexSet, mi_2: MultiIndexSet) -> MultiIndexSet:
+ """Expand the first multi-index set to match the dimension of the second.
+
+ Parameters
+ ----------
+ mi_1 : MultiIndexSet
+ The multi-index set to expand.
+ mi_2 : MultiIndexSet
+ The target multi-index set whose dimension to match.
+
+ Returns
+ -------
+ MultiIndexSet
+ ``mi_1`` expanded to match ``mi_2.spatial_dimension``, or ``mi_1``
+ unchanged if dimensions already match.
+
+ Notes
+ -----
+ - Assumes ``mi_1.spatial_dimension <= mi_2.spatial_dimension``.
+ """
+ if mi_1.spatial_dimension < mi_2.spatial_dimension:
+ return mi_1.expand_dim(mi_2.spatial_dimension)
+
+ return mi_1
diff --git a/src/minterpy/polynomials/canonical_polynomial.py b/src/minterpy/polynomials/canonical_polynomial.py
index 57c197f3..930f7fb1 100644
--- a/src/minterpy/polynomials/canonical_polynomial.py
+++ b/src/minterpy/polynomials/canonical_polynomial.py
@@ -8,24 +8,19 @@
from scipy.special import factorial
+from minterpy import MultiIndexSet, Grid
from minterpy.global_settings import INT_DTYPE
from minterpy.core.ABC import MultivariatePolynomialSingleABC
+from minterpy.polynomials.arithmetic import (
+ get_compute_coeffs_add,
+ get_compute_coeffs_mul,
+)
+from minterpy.polynomials.scalar_add import scalar_add_via_monomials
from minterpy.utils.polynomials.canonical import (
eval_polynomials,
integrate_monomials,
)
-from minterpy.utils.polynomials.interface import (
- compute_coeffs_poly_prod_via_monomials,
- compute_coeffs_poly_sum_via_monomials,
- get_grid_and_multi_index_poly_prod,
- get_grid_and_multi_index_poly_sum,
- PolyData,
- scalar_add_via_monomials,
-)
-from minterpy.utils.verification import (
- dummy,
- verify_domain,
-)
+from minterpy.utils.verification import dummy
from minterpy.utils.arrays import make_coeffs_2d
from minterpy.utils.multi_index import find_match_between
from minterpy.jit_compiled.multi_index import all_indices_are_contained
@@ -71,6 +66,8 @@ def eval_canonical(poly: "CanonicalPolynomial", xx: np.ndarray) -> np.ndarray:
def add_canonical(
poly_1: "CanonicalPolynomial",
poly_2: "CanonicalPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "CanonicalPolynomial":
"""Add two polynomial instances in the canonical basis.
@@ -81,9 +78,15 @@ def add_canonical(
Parameters
----------
poly_1 : CanonicalPolynomial
- Left operand of the addition expression.
+ Left operand of the addition.
poly_2 : CanonicalPolynomial
- Right operand of the addition expression.
+ Right operand of the addition.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the union
+ of the multi-index sets of the operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the union of the grids
+ of the two operands.
Returns
-------
@@ -93,22 +96,29 @@ def add_canonical(
Notes
-----
- - This function assumes: both polynomials must be in canonical basis,
- they must be initialized, have the same dimension and their domains
- are matching, and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ - This function assumes the caller has verified: both polynomials are in
+ the canonical basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed
+ with respect to the given ``multi_index``.
"""
- # --- Get the ingredients of the summed polynomial in the Canonical basis
- poly_sum_data = _compute_data_poly_sum(poly_1, poly_2)
+ # Get the function to compute the sum coefficients via monomials
+ compute_coeffs = get_compute_coeffs_add("monomials")
- # --- Return a new instance
- return CanonicalPolynomial(**poly_sum_data._asdict())
+ # Compute the coefficients
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
+
+ # Create and return a new instance of polynomial
+ return CanonicalPolynomial(multi_index, coeffs, grid)
def mul_canonical(
poly_1: "CanonicalPolynomial",
poly_2: "CanonicalPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "CanonicalPolynomial":
"""Multiply two polynomial instances in the canonical basis.
@@ -119,46 +129,43 @@ def mul_canonical(
Parameters
----------
poly_1 : CanonicalPolynomial
- Left operand of the multiplication expression.
+ Left operand of the multiplication.
poly_2 : CanonicalPolynomial
- Right operand of the multiplication expression.
+ Right operand of the multiplication.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the product
+ of the multi-index sets of the two operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the product of the grids
+ of the two operands.
Returns
-------
CanonicalPolynomial
- The product of two polynomials in the canonical basis; a new instance
- of polynomial.
+ The product of the two polynomials in the canonical basis as a new
+ polynomial instance.
Notes
-----
- - This function assumes: both polynomials must be in the canonical basis,
- they must be initialized, have the same dimension and their domains
- are matching, and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ - This function assumes the caller has verified: both polynomials are in
+ the canonical basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed
+ with respect to the given ``multi_index``.
"""
- # --- Get the ingredients of the product polynomial in the canonical basis
- poly_prod_data = _compute_data_poly_prod(poly_1, poly_2)
-
- # --- Return a new instance
- return CanonicalPolynomial(**poly_prod_data._asdict())
+ # Get the function to compute the product coefficients via monomials
+ compute_coeffs = get_compute_coeffs_mul("monomials")
+ # Compute the coefficients
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
-# TODO redundant
-canonical_generate_internal_domain = verify_domain
-canonical_generate_user_domain = verify_domain
+ # Create and return a new instance of polynomial
+ return CanonicalPolynomial(multi_index, coeffs, grid)
-def _canonical_partial_diff(poly: "CanonicalPolynomial", dim: int, order: int) -> "CanonicalPolynomial":
- """ Partial differentiation in Canonical basis.
- """
- spatial_dim = poly.multi_index.spatial_dimension
- deriv_order_along = np.zeros(spatial_dim, dtype=INT_DTYPE)
- deriv_order_along[dim] = order
- return _canonical_diff(poly, deriv_order_along)
-
-
-def _canonical_diff(poly: "CanonicalPolynomial", order: np.ndarray) -> "CanonicalPolynomial":
+def canonical_diff(poly: "CanonicalPolynomial", order: np.ndarray, diff_factor: float) -> "CanonicalPolynomial":
""" Partial differentiation in Canonical basis.
"""
@@ -190,7 +197,7 @@ def _canonical_diff(poly: "CanonicalPolynomial", order: np.ndarray) -> "Canonica
new_coeffs[map_pos] = diff_coeffs
# Squeezing the last dimension to handle single polynomial
- return CanonicalPolynomial.from_poly(poly, new_coeffs.reshape(poly.coeffs.shape))
+ return CanonicalPolynomial.from_poly(poly, diff_factor * new_coeffs.reshape(poly.coeffs.shape))
def _canonical_integrate_over(
@@ -236,125 +243,11 @@ class CanonicalPolynomial(MultivariatePolynomialSingleABC):
_scalar_add = staticmethod(scalar_add_via_monomials)
# Calculus
- _partial_diff = staticmethod(_canonical_partial_diff)
- _diff = staticmethod(_canonical_diff)
+ _diff = staticmethod(canonical_diff)
_integrate_over = staticmethod(_canonical_integrate_over)
- # Domain generation
- generate_internal_domain = staticmethod(canonical_generate_internal_domain)
- generate_user_domain = staticmethod(canonical_generate_user_domain)
-
# --- Internal utility functions
-def _compute_data_poly_sum(
- poly_1: "CanonicalPolynomial",
- poly_2: "CanonicalPolynomial",
-) -> PolyData:
- """Compute the data to create a summed polynomial in the canonical basis.
-
- Addition or subtraction of polynomials in the canonical basis is based
- on the adding (resp. subtracting) the coefficients of the matching
- monomials of the two polynomial operands (i.e., the matching elements of
- the two multi-index sets).
-
- Parameters
- ----------
- poly_1 : CanonicalPolynomial
- Left operand of the addition expression.
- poly_2 : CanonicalPolynomial
- Right operand of the addition expression.
-
- Returns
- -------
- PolyData
- The ingredients to construct a summed polynomial in the canonical
- basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same dimension
- and matching domains.
- """
- # --- Get the grid and multi-index set of the summed polynomial
- grd_sum, mi_sum = get_grid_and_multi_index_poly_sum(poly_1, poly_2)
-
- # --- Process the coefficients
- # NOTE: indices may or may not be separate, use the summed multi-index set
- # instead of the one attached to grid
- coeffs_sum = compute_coeffs_poly_sum_via_monomials(poly_1, poly_2, mi_sum)
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_sum = poly_1.internal_domain
- user_domain_sum = poly_1.user_domain
-
- return PolyData(
- multi_index=mi_sum,
- coeffs=coeffs_sum,
- internal_domain=internal_domain_sum,
- user_domain=user_domain_sum,
- grid=grd_sum,
- )
-
-
-def _compute_data_poly_prod(
- poly_1: "CanonicalPolynomial",
- poly_2: "CanonicalPolynomial",
-) -> PolyData:
- """Compute the data to create a product polynomial in the canonical basis.
-
- Multiplication of polynomials in the canonical basis is based on
- multiplying the coefficients of the matching monomials of the two
- polynomial operands (i.e., the matching elements of the two multi-index
- sets). In the canonical basis, multiplying two monomials results in a
- a monomial of a higher degree (the degree sum of the two monomials).
-
- Parameters
- ----------
- poly_1 : CanonicalPolynomial
- Left operand of the multiplication expression.
- poly_2 : CanonicalPolynomial
- Right operand of the multiplication expression.
-
- Returns
- -------
- PolyData
- The ingredients to construct a product polynomial in the canonical
- basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same dimension
- and matching domains.
- """
- # --- Get the grid and multi-index set of the summed polynomial
- grd_prod, mi_prod = get_grid_and_multi_index_poly_prod(poly_1, poly_2)
-
- # --- Process the coefficients
- # NOTE: indices may or may not be separate, use the summed multi-index set
- # instead of the one attached to grid
- coeffs_prod = compute_coeffs_poly_prod_via_monomials(
- poly_1,
- poly_2,
- mi_prod,
- )
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_prod = poly_1.internal_domain
- user_domain_prod = poly_1.user_domain
-
- return PolyData(
- multi_index=mi_prod,
- coeffs=coeffs_prod,
- internal_domain=internal_domain_prod,
- user_domain=user_domain_prod,
- grid=grd_prod,
- )
-
-
def _compute_quad_weights(
poly: CanonicalPolynomial,
bounds: np.ndarray,
diff --git a/src/minterpy/polynomials/chebyshev_polynomial.py b/src/minterpy/polynomials/chebyshev_polynomial.py
index 1aa49ee9..9e2f2104 100644
--- a/src/minterpy/polynomials/chebyshev_polynomial.py
+++ b/src/minterpy/polynomials/chebyshev_polynomial.py
@@ -15,20 +15,17 @@
from minterpy.core.ABC import MultivariatePolynomialSingleABC
from minterpy.core import Grid, MultiIndexSet
+from minterpy.polynomials.arithmetic import (
+ get_compute_coeffs_add,
+ get_compute_coeffs_mul,
+ select_active_monomials,
+)
+from minterpy.polynomials.scalar_add import scalar_add_via_monomials
from minterpy.utils.polynomials.chebyshev import (
evaluate_monomials,
evaluate_polynomials,
)
-from minterpy.utils.polynomials.interface import (
- compute_coeffs_poly_sum_via_monomials,
- compute_coeffs_poly_prod_via_monomials,
- get_grid_and_multi_index_poly_prod,
- get_grid_and_multi_index_poly_sum,
- PolyData,
- scalar_add_via_monomials,
- select_active_monomials,
-)
-from minterpy.utils.verification import dummy, verify_domain
+from minterpy.utils.verification import dummy
from minterpy.services import is_scalar
@@ -74,45 +71,61 @@ def eval_chebyshev(
def add_chebyshev(
poly_1: "ChebyshevPolynomial",
poly_2: "ChebyshevPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "ChebyshevPolynomial":
"""Add two polynomial instances in the Chebyshev basis.
This is the concrete implementation of ``_add()`` method in the
- ``MultivariatePolynomialSingleABC`` abstract base class specifically for
- polynomials in the Chebyshev basis.
+ ``MultivariatePolynomialSingleABC`` abstract class specifically for
+ polynomial in the Chebyshev basis.
Parameters
----------
poly_1 : ChebyshevPolynomial
- Left operand of the addition expression.
+ Left operand of the addition.
poly_2 : ChebyshevPolynomial
- Right operand of the addition expression.
+ Right operand of the addition.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the union
+ of the multi-index sets of the operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the union of the grids
+ of the two operands.
Returns
-------
ChebyshevPolynomial
- The product of two polynomials in the Chebyshev basis as a new instance
- of polynomial in the Chebyshev basis.
+ The sum of two polynomials in the Chebyshev basis as a new instance
+ of polynomial.
Notes
-----
- - This function assumes: both polynomials must be in the Chebyshev basis,
- they must be initialized (coefficients are not ``None``),
- have the same dimension and their domains are matching,
- and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ - The Chebyshev basis is closed under addition so the coefficients of
+ the resulting polynomial can be summed up via the monomials rule.
+ - This function assumes the caller has verified: both polynomials are in
+ the Chebyshev basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed
+ with respect to the given ``multi_index``.
"""
- # --- Get the ingredients of a summed polynomial in the Chebyshev basis
- poly_data = _compute_data_poly_sum(poly_1, poly_2)
+ # Get the function to compute the sum coefficients via monomials
+ compute_coeffs = get_compute_coeffs_add("monomials")
+
+ # Compute the coefficients
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
- # --- Return a new instance
- return ChebyshevPolynomial(**poly_data._asdict())
+ # Create and return a new instance of polynomial
+ return ChebyshevPolynomial(multi_index, coeffs, grid)
def mul_chebyshev(
poly_1: "ChebyshevPolynomial",
poly_2: "ChebyshevPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "ChebyshevPolynomial":
"""Multiply two polynomial instances in the Chebyshev basis.
@@ -123,30 +136,51 @@ def mul_chebyshev(
Parameters
----------
poly_1 : ChebyshevPolynomial
- Left operand of the multiplication expression.
+ Left operand of the multiplication.
poly_2 : ChebyshevPolynomial
- Right operand of the multiplication expression.
+ Right operand of the multiplication.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the product
+ of the multi-index sets of the operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the product of the grids
+ of the two operands.
Returns
-------
ChebyshevPolynomial
The product of two polynomials in the Chebyshev basis as a new instance
- of polynomial in the Chebyshev basis.
+ of polynomial.
Notes
-----
- - This function assumes: both polynomials must be in the Chebyshev basis,
- they must be initialized (coefficients are not ``None``),
- have the same dimension and their domains are matching,
- and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ - The Chebyshev basis is in general not closed under multiplication,
+ so the coefficients of the resulting polynomial must be computed via
+ the transformation from the Lagrange coefficients.
+ - If one operand has a scalar multi-index, monomials multiplication
+ is used instead.
+ - This function assumes the caller has verified: both polynomials are in
+ the Chebyshev basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed
+ with respect to the given ``multi_index``.
"""
- # --- Get the ingredients of the product polynomial in the Chebyshev basis
- poly_prod_data = _compute_data_poly_prod(poly_1, poly_2)
+ # Handle the case where no transformation is required
+ if is_scalar(poly_1.multi_index) or is_scalar(poly_2.multi_index):
+ # If one of the operands has a scalar multi-index set
+ # (regardless of the grid), compute the coefficients via monomials
+ compute_coeffs = get_compute_coeffs_mul("monomials")
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
+ else:
+ # Compute the coefficients via a transformation from Lagrange coeffs.
+ compute_coeffs = get_compute_coeffs_mul("lagrange")
+ coeffs_lag = compute_coeffs(poly_1, poly_2, grid)
+ coeffs = _transform_lag2cheb(coeffs_lag, multi_index, grid)
- # --- Return a new instance
- return ChebyshevPolynomial(**poly_prod_data._asdict())
+ # Create and return a new instance of polynomial
+ return ChebyshevPolynomial(multi_index, coeffs, grid)
class ChebyshevPolynomial(MultivariatePolynomialSingleABC):
@@ -167,224 +201,59 @@ class ChebyshevPolynomial(MultivariatePolynomialSingleABC):
_scalar_add = staticmethod(scalar_add_via_monomials)
# Calculus
- _partial_diff = staticmethod(dummy) # type: ignore
_diff = staticmethod(dummy) # type: ignore
_integrate_over = staticmethod(dummy) # type: ignore
- # Domain generation
- generate_internal_domain = staticmethod(verify_domain)
- generate_user_domain = staticmethod(verify_domain)
-
# --- Internal utility functions
-def _compute_data_poly_sum(
- poly_1: "ChebyshevPolynomial",
- poly_2: "ChebyshevPolynomial",
-) -> PolyData:
- """Compute the data to create a summed polynomial in the Chebyshev basis.
-
- Addition or subtraction of polynomials in the Chebyshev basis is based
- on adding (resp. subtracting) the coefficients of the matching monomials
- of the two polynomial operands (i.e., the matching elements of the two
- multi-index sets). This procedure is the same as that of the canonical
- polynomial.
-
- Parameters
- ----------
- poly_1 : ChebyshevPolynomial
- Left operand of the addition/subtraction expression.
- poly_2 : ChebyshevPolynomial
- Right operand of the addition/subtraction expression.
-
- Returns
- -------
- PolyData
- The ingredients to construct a summed polynomial in the Chebyshev
- basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same type, the same spatial
- dimension, and matching domains. These conditions have been made sure
- upstream.
- """
- # --- Get the grid and multi-index set of the summed polynomial
- grd_sum, mi_sum = get_grid_and_multi_index_poly_sum(poly_1, poly_2)
-
- # --- Process the coefficients
- # NOTE: indices may or may not be separate, use the summed multi-index set
- # instead of the one attached to grid
- coeffs_sum = compute_coeffs_poly_sum_via_monomials(poly_1, poly_2, mi_sum)
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_sum = poly_1.internal_domain
- user_domain_sum = poly_1.user_domain
-
- return PolyData(
- mi_sum,
- coeffs_sum,
- internal_domain_sum,
- user_domain_sum,
- grd_sum,
- )
-
-
-def _compute_data_poly_prod(
- poly_1: ChebyshevPolynomial,
- poly_2: ChebyshevPolynomial,
-) -> PolyData:
- """Compute the data to create a product polynomial in the Chebyshev basis.
-
- Parameters
- ----------
- poly_1 : ChebyshevPolynomial
- Left operand of the multiplication expression.
- poly_2 : ChebyshevPolynomial
- Right operand of the multiplication expression.
-
- Returns
- -------
- PolyData
- A tuple with all the ingredients to construct a product polynomial
- in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same spatial dimension and
- matching domains. These conditions have been made sure upstream.
- """
- # --- Get the grid and multi-index set of the summed polynomial
- grd_prod, mi_prod = get_grid_and_multi_index_poly_prod(poly_1, poly_2)
-
- # --- Process the coefficients
- # NOTE: indices may or may not be separate, use the summed multi-index set
- # instead of the one attached to grid
- coeffs_prod = _compute_coeffs_poly_prod(poly_1, poly_2, grd_prod, mi_prod)
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_prod = poly_1.internal_domain
- user_domain_prod = poly_1.user_domain
-
- return PolyData(
- multi_index=mi_prod,
- coeffs=coeffs_prod,
- internal_domain=internal_domain_prod,
- user_domain=user_domain_prod,
- grid=grd_prod,
- )
-
-
-def _compute_coeffs_poly_prod(
- poly_1: ChebyshevPolynomial,
- poly_2: ChebyshevPolynomial,
- grid_prod: Grid,
- multi_index_prod: MultiIndexSet,
+def _transform_lag2cheb(
+ coeffs_lag: np.ndarray,
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> np.ndarray:
- """Compute the coefficients of the product polynomial in Chebyshev basis.
+ """Transform Lagrange coefficients to Chebyshev coefficients.
- In general, the coefficients of a product polynomial in the Chebyshev basis
- are obtained by going through the Lagrange basis first.
- Specifically, the Lagrange coefficients are computed by multiplying
- the evaluation results of the Chebyshev polynomial operands on the product
- Grid. Afterward, these coefficients are transformed to the Chebyshev
- coefficients.
-
- This is because the multiplication of two Chebyshev monomial does not
- return the monomial of a higher degree. For instance, the Chebyshev
- monomial of degree :math:`3` is not the result of multiplying Chebyshev
- monomials of degree :math:`1` and :math:`2`.
-
- However, if one of the polynomial operands has a scalar multi-index set
- regardless of the grid, then the coefficients is obtained by multiplying
- the coefficients of the non-scalar polynomial with the coefficient of
- the scalar polynomial.
+ Given polynomial coefficients in the Lagrange basis, compute the equivalent
+ coefficients in the Chebyshev basis by solving a linear system based on
+ evaluating Chebyshev monomials at the unisolvent nodes.
Parameters
----------
- poly_1 : ChebyshevPolynomial
- Left operand of the multiplication expression.
- poly_2 : ChebyshevPolynomial
- Right operand of the multiplication expression.
- grid_prod : Grid
- The grid of the product polynomial.
- multi_index_prod : MultiIndexSet
- The multi-index of the product polynomial.
+ coeffs_lag : np.ndarray
+ Coefficients in the Lagrange basis. Shape is ``(N, K)`` where ``N`` is
+ the number of unisolvent nodes (equals ``len(grid.multi_index)``) and
+ ``K`` is the number of polynomial instances.
+ multi_index : MultiIndexSet
+ The multi-index set of the target polynomial. In case of separated
+ indices cases, it may be a subset of ``grid.multi_index``.
+ grid : Grid
+ The grid on which the Lagrange polynomial lives.
Returns
-------
- :class:`numpy:numpy.ndarray`
- The coefficients of the product between two polynomials.
- """
- # --- Handle the case where no transformation is required
- # If one of the operands has a scalar multi-index set
- if is_scalar(poly_1.multi_index) or is_scalar(poly_2.multi_index):
- return compute_coeffs_poly_prod_via_monomials(
- poly_1,
- poly_2,
- multi_index_prod,
- )
-
- return _compute_coeffs_poly_prod_via_lagrange(
- poly_1,
- poly_2,
- grid_prod,
- multi_index_prod,
- )
-
-
-def _compute_coeffs_poly_prod_via_lagrange(
- poly_1: ChebyshevPolynomial,
- poly_2: ChebyshevPolynomial,
- grid_prod: Grid,
- multi_index_prod: MultiIndexSet,
-) -> np.ndarray:
- """Compute the coefficients of a product Chebyshev polynomial via Lagrange.
-
- Parameters
- ----------
- poly_1 : ChebyshevPolynomial
- Left operand of the multiplication expression.
- poly_2 : ChebyshevPolynomial
- Right operand of the multiplication expression.
- grid_prod : Grid
- The Grid associated with the product polynomial.
- multi_index_prod : MultiIndexSet
- The multi-index set of the product polynomial.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of the product polynomial in the Chebyshev basis.
+ np.ndarray
+ Coefficients in the Chebyshev basis. Shape is ``(N, K)`` where ``N``
+ equals ``len(multi_index)`` and ``K`` is the number of polynomial
+ instances.
Notes
-----
- - Both polynomials are assumed to have the same spatial dimension and
- matching domains. These conditions have been made sure upstream.
+ - The transformation solves the linear system ``A @ c_cheb = c_lag`` where
+ ``A`` is the Chebyshev-to-Lagrange transformation matrix formed by
+ evaluating Chebyshev monomials at the unisolvent nodes.
"""
- # Compute the values of the operands at the unisolvent nodes
- lag_coeffs_1 = grid_prod(poly_1)
- lag_coeffs_2 = grid_prod(poly_2)
- lag_coeffs_prod = lag_coeffs_1 * lag_coeffs_2
-
- # Compute the Chebyshev monomials at the unisolvent nodes
+ # Compute the Chebyshev-to-Lagrange transformation matrix
+ # (Chebyshev monomials evaluated at unisolvent nodes)
cheb2lag = evaluate_monomials(
- grid_prod.unisolvent_nodes,
- grid_prod.multi_index.exponents,
+ grid.unisolvent_nodes,
+ grid.multi_index.exponents,
)
- # Compute the inverse transformation
- cheb_coeffs_prod = np.linalg.solve(cheb2lag, lag_coeffs_prod)
+ # Solve for Chebyshev coefficients: cheb2lag @ cheb_coeffs = lag_coeffs
+ coeffs_cheb = np.linalg.solve(cheb2lag, coeffs_lag)
- # Deal with separate indices, select only w.r.t the active monomials
- if poly_1.indices_are_separate or poly_2.indices_are_separate:
- cheb_coeffs_prod = select_active_monomials(
- cheb_coeffs_prod,
- grid_prod,
- multi_index_prod,
- )
+ # Handle separated indices: select only coefficients for active monomials
+ if multi_index != grid.multi_index:
+ coeffs_cheb = select_active_monomials(coeffs_cheb, grid, multi_index)
- return cheb_coeffs_prod
+ return coeffs_cheb
diff --git a/src/minterpy/polynomials/lagrange_polynomial.py b/src/minterpy/polynomials/lagrange_polynomial.py
index b26b4097..96c1a328 100644
--- a/src/minterpy/polynomials/lagrange_polynomial.py
+++ b/src/minterpy/polynomials/lagrange_polynomial.py
@@ -41,7 +41,7 @@
from minterpy.global_settings import SCALAR
from minterpy.core.ABC import MultivariatePolynomialSingleABC
from minterpy.utils.polynomials.lagrange import integrate_monomials_lagrange
-from minterpy.utils.verification import dummy, verify_domain
+from minterpy.utils.verification import dummy
__all__ = ["LagrangePolynomial"]
@@ -103,11 +103,6 @@ def integrate_over_lagrange(
return quad_weights @ poly.coeffs
-# TODO redundant
-lagrange_generate_internal_domain = verify_domain
-lagrange_generate_user_domain = verify_domain
-
-
class LagrangePolynomial(MultivariatePolynomialSingleABC):
"""Concrete implementation of polynomials in the Lagrange basis.
@@ -146,14 +141,9 @@ class LagrangePolynomial(MultivariatePolynomialSingleABC):
_scalar_add = staticmethod(scalar_add_lagrange) # type: ignore
# Calculus
- _partial_diff = staticmethod(dummy) # type: ignore
_diff = staticmethod(dummy) # type: ignore
_integrate_over = staticmethod(integrate_over_lagrange)
- # Domain generation
- generate_internal_domain = staticmethod(lagrange_generate_internal_domain)
- generate_user_domain = staticmethod(lagrange_generate_user_domain)
-
# --- Internal utility functions
def _compute_quad_weights(
diff --git a/src/minterpy/polynomials/newton_polynomial.py b/src/minterpy/polynomials/newton_polynomial.py
index bcf25590..0cc8b5ff 100644
--- a/src/minterpy/polynomials/newton_polynomial.py
+++ b/src/minterpy/polynomials/newton_polynomial.py
@@ -44,25 +44,22 @@
)
from minterpy.core import Grid, MultiIndexSet
from minterpy.dds import dds
-from minterpy.utils.verification import dummy, verify_domain
+from minterpy.utils.verification import dummy
from minterpy.utils.polynomials.newton import (
eval_newton_polynomials,
deriv_newt_eval as eval_diff_numpy,
integrate_monomials_newton,
)
+from minterpy.polynomials.arithmetic import (
+ get_compute_coeffs_add,
+ get_compute_coeffs_mul,
+ select_active_monomials,
+)
+from minterpy.polynomials.scalar_add import scalar_add_via_monomials
from minterpy.jit_compiled.newton.diff import (
eval_multiple_query as eval_diff_numba,
eval_multiple_query_par as eval_diff_numba_par,
)
-from minterpy.utils.polynomials.interface import (
- compute_coeffs_poly_sum_via_monomials,
- compute_coeffs_poly_prod_via_monomials,
- get_grid_and_multi_index_poly_prod,
- get_grid_and_multi_index_poly_sum,
- PolyData,
- scalar_add_via_monomials,
- select_active_monomials,
-)
from minterpy.services import is_scalar
__all__ = ["NewtonPolynomial"]
@@ -113,8 +110,10 @@ def eval_newton(poly: "NewtonPolynomial", xx: np.ndarray) -> np.ndarray:
def add_newton(
poly_1: "NewtonPolynomial",
poly_2: "NewtonPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "NewtonPolynomial":
- """Add two instances of polynomials in the Newton basis.
+ r"""Add two instances of polynomials in the Newton basis.
This is the concrete implementation of ``_add()`` method in the
``MultivariatePolynomialSingleABC`` abstract class specifically for
@@ -123,75 +122,154 @@ def add_newton(
Parameters
----------
poly_1 : NewtonPolynomial
- Left operand of the addition/subtraction expression.
+ Left operand of the addition.
poly_2 : NewtonPolynomial
- Right operand of the addition/subtraction expression.
+ Right operand of the addition.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the union
+ of the multi-index sets of the operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the union of the grids
+ of the two operands.
Returns
-------
NewtonPolynomial
The sum of two polynomials in the Newton basis as a new instance
- of polynomial in the Newton basis.
+ of polynomial.
Notes
-----
- - This function assumes: both polynomials must be in the Newton basis,
- they must be initialized (coefficients are not ``None``),
- have the same dimension and their domains are matching,
- and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ **Algorithm:**
+
+ The Newton basis is in general not closed under addition because Newton
+ monomials depend on the underlying grid's generating points. When grids
+ differ, the monomials differ, requiring transformation through the Lagrange
+ basis:
+
+ 1. Evaluate both Newton polynomials at the union grid's unisolvent nodes
+ to obtain Lagrange coefficients
+ 2. Sum the Lagrange coefficients
+ 3. Transform back to Newton coefficients on the union grid
+
+ **Special cases (monomials addition used instead):**
+
+ - **Compatible grids**: If both operand grids are compatible with the
+ result grid, their Newton monomials are identical, allowing direct
+ coefficient addition.
+ - **Scalar operand**: If one operand is a constant (multi-index set
+ contains only :math:`(0, \ldots, 0)`), it can be added directly
+ regardless of grid compatibility.
+
+ **Preconditions:**
+
+ - This function assumes the caller has verified: both polynomials are in
+ the Newton basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed with respect
+ to the provided ``multi_index``.
"""
- # --- Get the ingredients of the summed polynomial in the Newton basis
- poly_sum_data = _compute_data_poly_sum(poly_1, poly_2)
+ # Handle the case where no transformation is required
+ if _is_compute_coeffs_poly_add_via_monomials(poly_1, poly_2, grid):
+ compute_coeffs = get_compute_coeffs_add("monomials")
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
+ else:
+ # Compute the coefficients via a transformation from Lagrange coeffs.
+ compute_coeffs = get_compute_coeffs_add("lagrange")
+ coeffs_lag = compute_coeffs(poly_1, poly_2, grid)
+ coeffs = _transform_lag2nwt(coeffs_lag, multi_index, grid)
- # --- Return a new instance
- return NewtonPolynomial(**poly_sum_data._asdict())
+ # Create and return a new instance of polynomial
+ return NewtonPolynomial(multi_index, coeffs, grid)
def mul_newton(
poly_1: "NewtonPolynomial",
poly_2: "NewtonPolynomial",
+ multi_index: MultiIndexSet,
+ grid: Grid,
) -> "NewtonPolynomial":
- """Multiply instances of polynomials in the Newton basis.
+ r"""Multiply instances of polynomials in the Newton basis.
This is the concrete implementation of ``_mul()`` method in the
``MultivariatePolynomialSingleABC`` abstract class specifically for
- handling polynomials in the Newton basis.
+ polynomials in the Newton basis.
Parameters
----------
poly_1 : NewtonPolynomial
- Left operand of the multiplication expression.
+ Left operand of the multiplication.
poly_2 : NewtonPolynomial
- Right operand of the multiplication expression.
+ Right operand of the multiplication.
+ multi_index : MultiIndexSet
+ The multi-index set of the resulting polynomial, i.e., the product
+ of the multi-index sets of the operands.
+ grid : Grid
+ The grid of the resulting polynomial, i.e., the product of the grids
+ of the two operands.
Returns
-------
NewtonPolynomial
The product of two polynomials in the Newton basis as a new instance
- of polynomial in the Newton basis.
+ of polynomial.
Notes
-----
- - This function assumes: both polynomials must be in the Newton basis,
- they must be initialized (coefficients are not ``None``),
- have the same dimension and their domains are matching,
- and the number of polynomials per instance are the same.
- These conditions are not explicitly checked in this function; the caller
- is responsible for the verification.
+ **Algorithm:**
+
+ The Newton basis is in general not closed under multiplication because
+ the product of Newton monomials does not result in a Newton monomial
+ of a higher degree (unlike canonical basis). The coefficients
+ of a polynomial product requires transformation through the Lagrange basis:
+
+ 1. Evaluate both Newton polynomials at the product grid's unisolvent nodes
+ to obtain Lagrange coefficients
+ 2. Multiply the Lagrange coefficients
+ 3. Transform back to Newton coefficients on the product grid
+
+ **Special cases (monomials addition used instead):**
+
+ - **Scalar polynomial with scalar grid**: If one operand is a constant
+ polynomial (multi-index contains only :math:`(0, \ldots, 0)`)
+ and its grid's multi-index is also scalar,
+ direct coefficient multiplication is used.
+ - **Scalar polynomial with compatible non-scalar grid**: If one operand is
+ a constant polynomial but its grid's multi-index is non-scalar, direct
+ coefficient multiplication is used only if the grid is compatible with
+ the product grid.
+
+ **Preconditions:**
+
+ - This function assumes the caller has verified: both polynomials are in
+ the Newton basis, both are initialized, their domains match, and they
+ have the same number of polynomial instances.
+ - The provided ``multi_index`` may differ from ``grid.multi_index`` when
+ the polynomial multi-index set is a subset of the grid multi-index set
+ (i.e., separated indices). The coefficients are computed with respect
+ to the provided ``multi_index``.
"""
- # --- Get the ingredients of the product polynomial in the Newton basis
- poly_prod_data = _compute_data_poly_prod(poly_1, poly_2)
+ # Handle the case where no transformation is required
+ if _is_compute_coeffs_poly_mul_via_monomials(poly_1, poly_2, grid):
+ compute_coeffs = get_compute_coeffs_mul("monomials")
+ coeffs = compute_coeffs(poly_1, poly_2, multi_index)
+ else:
+ # Compute the coefficients via a transformation from Lagrange coeffs.
+ compute_coeffs = get_compute_coeffs_mul("lagrange")
+ coeffs_lag = compute_coeffs(poly_1, poly_2, grid)
+ coeffs = _transform_lag2nwt(coeffs_lag, multi_index, grid)
- # --- Return a new instance
- return NewtonPolynomial(**poly_prod_data._asdict())
+ # Create and return a new instance of polynomial
+ return NewtonPolynomial(multi_index, coeffs, grid)
# --- Calculus
def diff_newton(
poly: "NewtonPolynomial",
order: np.ndarray,
+ diff_factor: float,
*,
backend: str = "numba",
) -> "NewtonPolynomial":
@@ -276,6 +354,7 @@ def diff_newton(
# DDS returns a 2D array, reshaping it according to input coefficient array
nwt_diff_coeffs = dds(lag_diff_coeffs, tree).reshape(poly.coeffs.shape)
+ nwt_diff_coeffs *= diff_factor
return NewtonPolynomial(
coeffs=nwt_diff_coeffs,
@@ -284,69 +363,6 @@ def diff_newton(
)
-def partial_diff_newton(
- poly: "NewtonPolynomial",
- dim: int,
- order: int,
- *,
- backend: str = "numba",
-) -> "NewtonPolynomial":
- """Differentiate polynomial(s) in the Newton basis with respect to a given
- dimension and order of derivative.
-
- This is a wrapper for the partial differentiation function in
- the Newton basis.
-
- Parameters
- ----------
- poly : NewtonPolynomial
- The instance of polynomial in Newton form to differentiate.
- dim : int
- Spatial dimension with respect to which the differentiation
- is taken. The dimension starts at 0 (i.e., the first dimension).
- order : int
- Order of partial derivative.
- backend : str
- Computational backend to carry out the differentiation.
- Supported values are:
-
- - ``"numpy"``: implementation based on NumPy; not performant, only
- applicable for a very small problem size (small degree,
- low dimension).
- - ``"numba"`` (default): implementation based on compiled code with
- the help of Numba; applicable up to moderate problem size.
- - ``"numba-par"``: parallelized (CPU) implementation based on compiled
- code with the help of Numba for relatively large problem sizes.
-
- Returns
- -------
- NewtonPolynomial
- A new instance of `NewtonPolynomial` that represents the partial
- derivative of the original polynomial of the given order of derivative
- with respect to the specified dimension.
-
- Notes
- -----
- - The abstract class is responsible to validate ``dim`` and ``order``; no
- additional validation regarding those two parameters are required here.
-
- See Also
- --------
- NewtonPolynomial.partial_diff
- The public method to differentiate the polynomial instance of
- a specified order of derivative with respect to a given dimension.
- NewtonPolynomial.diff
- The public method to differentiate the polynomial instance of
- the given orders of derivative along each dimension.
- """
- # Create a specification for differentiation
- spatial_dim = poly.multi_index.spatial_dimension
- deriv_order_along = np.zeros(spatial_dim, dtype=int)
- deriv_order_along[dim] = order
-
- return diff_newton(poly, deriv_order_along, backend=backend)
-
-
def integrate_over_newton(
poly: "NewtonPolynomial", bounds: np.ndarray
) -> np.ndarray:
@@ -371,11 +387,6 @@ def integrate_over_newton(
return quad_weights @ poly.coeffs
-# TODO redundant
-generate_internal_domain_newton = verify_domain
-generate_user_domain_newton = verify_domain
-
-
class NewtonPolynomial(MultivariatePolynomialSingleABC):
"""Concrete implementations of polynomials in the Newton basis.
@@ -398,140 +409,12 @@ class NewtonPolynomial(MultivariatePolynomialSingleABC):
_scalar_add = staticmethod(scalar_add_via_monomials)
# Calculus
- _partial_diff = staticmethod(partial_diff_newton)
_diff = staticmethod(diff_newton)
_integrate_over = staticmethod(integrate_over_newton)
- # Domain generation
- generate_internal_domain = staticmethod(generate_internal_domain_newton)
- generate_user_domain = staticmethod(generate_user_domain_newton)
-
# --- Internal utility functions
-def _compute_data_poly_sum(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
-) -> PolyData:
- """Compute the data to create a summed polynomial in the Newton basis.
-
- This function is responsible to prepare the data to construct a new
- instance of polynomial in the Newton basis that is the summed of two Newton
- polynomials, specifically:
-
- - the underlying grid
- - the underlying multi-index set
- - the polynomial coefficients
- - the internal domain
- - the user domain
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the addition/subtraction expression.
- poly_2 : NewtonPolynomial
- Right operand of the addition/subtraction expression.
-
- Returns
- -------
- PolyData
- The ingredients to construct a summed polynomial in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same type, the same spatial
- dimension, and matching domains. These conditions have been made sure
- upstream.
- """
- # --- Get the grid and multi-index set of the summed polynomial
- grd_sum, mi_sum = get_grid_and_multi_index_poly_sum(poly_1, poly_2)
-
- # --- Process the coefficients
- coeffs_sum = _compute_coeffs_poly_sum(poly_1, poly_2, grd_sum, mi_sum)
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_sum = poly_1.internal_domain
- user_domain_sum = poly_1.user_domain
-
- return PolyData(
- multi_index=mi_sum,
- coeffs=coeffs_sum,
- internal_domain=internal_domain_sum,
- user_domain=user_domain_sum,
- grid=grd_sum,
- )
-
-
-def _compute_coeffs_poly_sum(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
- grid_sum: Grid,
- multi_index_sum: MultiIndexSet,
-) -> np.ndarray:
- """Compute the coefficients of a summed polynomial in the Newton basis.
-
- In general, the coefficients of a summed polynomial in the Newton basis
- are obtained by going through the Lagrange basis first.
- Specifically, the Lagrange coefficients are computed by summing up
- the evaluation results of the Newton polynomial operands on the union
- Grid. Afterward, these coefficients are transformed to the Newton
- coefficients.
-
- This is because the Newton monomials depends on the underlying grid
- (specifically, its generating points). The monomials of two Newton
- polynomials are different if the generating points of the two polynomials
- are different.
-
- There are two exceptions such that the coefficients may be obtained without
- going through the Lagrange basis first.
-
- - if the grids of the two polynomial operands are compatible with the
- grid of the summed polynomial. In this particular case, the monomials
- of the two polynomials are the same.
- - if one of the operands is a constant scalar polynomial, whose only a
- single element (:math:`(0, \ldots, 0)`) in the multi-index set of the
- polynomial and the underlying grid (regardless whether this grid
- is compatible with the grid of the summed polynomial).
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the addition/subtraction expression.
- poly_2 : NewtonPolynomial
- Right operand of the addition/subtraction expression.
- grid_sum : Grid
- The Grid associated with the summed polynomial.
- multi_index_sum : MultiIndexSet
- The multi-index set of the summed polynomial.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of the summed polynomial in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same spatial dimension and
- matching domains. These conditions have been made sure upstream.
- """
- # --- Handle the case where no transformation is required
- if _is_compute_coeffs_poly_sum_via_monomials(poly_1, poly_2, grid_sum):
- return compute_coeffs_poly_sum_via_monomials(
- poly_1,
- poly_2,
- multi_index_sum,
- )
-
- return _compute_coeffs_poly_sum_via_lagrange(
- poly_1,
- poly_2,
- grid_sum,
- multi_index_sum,
- )
-
-
-def _is_compute_coeffs_poly_sum_via_monomials(
+def _is_compute_coeffs_poly_add_via_monomials(
poly_1: NewtonPolynomial,
poly_2: NewtonPolynomial,
grid_sum: Grid
@@ -557,183 +440,32 @@ def _is_compute_coeffs_poly_sum_via_monomials(
# If one of the operands is a scalar polynomial
is_scalar_poly = is_scalar(poly_1) or is_scalar(poly_2)
# ...or if the grids are compatible
- is_compatible_grid_1 = poly_1.grid.is_compatible(grid_sum)
- is_compatible_grid_2 = poly_2.grid.is_compatible(grid_sum)
+ is_compatible_grid_1 = poly_1.grid.has_compatible_gen_points(grid_sum)
+ is_compatible_grid_2 = poly_2.grid.has_compatible_gen_points(grid_sum)
is_compatible_grids = is_compatible_grid_1 and is_compatible_grid_2
return is_scalar_poly or is_compatible_grids
-def _compute_coeffs_poly_sum_via_lagrange(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
- grid_sum: Grid,
- multi_index_sum: MultiIndexSet,
-) -> np.ndarray:
- """Compute the coefficients of a summed Newton polynomial via Lagrange.
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the addition/subtraction expression.
- poly_2 : NewtonPolynomial
- Right operand of the addition/subtraction expression.
- grid_sum : Grid
- The Grid associated with the summed polynomial.
- multi_index_sum : MultiIndexSet
- The multi-index set of the summed polynomial.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of the summed polynomial in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same spatial dimension and
- matching domains. These conditions have been made sure upstream.
- """
- # Compute the values of the operands at the unisolvent nodes
- lag_coeffs_1 = grid_sum(poly_1)
- lag_coeffs_2 = grid_sum(poly_2)
- lag_coeffs_sum = lag_coeffs_1 + lag_coeffs_2
-
- # Transform the Lagrange coefficients into Newton coefficients
- nwt_coeffs_sum = _transform_lag2nwt(
- lag_coeffs_sum,
- grid_sum,
- poly_1.indices_are_separate or poly_2.indices_are_separate,
- multi_index_sum,
- )
-
- return nwt_coeffs_sum
-
-
-def _compute_data_poly_prod(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
-) -> PolyData:
- """Compute the data to create a product polynomial in the Newton basis.
-
- This function is responsible to prepare the data to construct a new
- instance of polynomial in the Newton basis that is the product of
- two Newton polynomials, specifically:
-
- - the underlying grid
- - the underlying multi-index set
- - the polynomial coefficients
- - the internal domain
- - the user domain
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the multiplication expression.
- poly_2 : NewtonPolynomial
- Right operand of the multiplication expression.
-
- Returns
- -------
- PolyData
- The ingredients to construct a product polynomial in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same spatial dimension
- and matching domains.
- """
- # --- Get the grid and multi-index set of the product polynomial
- grd_prod, mi_prod = get_grid_and_multi_index_poly_prod(poly_1, poly_2)
-
- # --- Process the coefficients
- coeffs_prod = _compute_coeffs_poly_prod(poly_1, poly_2, grd_prod, mi_prod)
-
- # --- Process the domains
- # NOTE: Because it is assumed that 'poly_1' and 'poly_2' have
- # matching domains, it does not matter which one to use
- internal_domain_prod = poly_1.internal_domain
- user_domain_prod = poly_1.user_domain
-
- return PolyData(
- multi_index=mi_prod,
- coeffs=coeffs_prod,
- internal_domain=internal_domain_prod,
- user_domain=user_domain_prod,
- grid=grd_prod,
- )
-
-
-def _compute_coeffs_poly_prod(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
- grid_prod: Grid,
- multi_index_prod: MultiIndexSet,
-) -> np.ndarray:
- """Compute the coefficients of polynomial product in the Newton basis.
-
- In general, the coefficients of a product polynomial in the Newton basis
- are obtained by going through the Lagrange basis first.
- Specifically, the Lagrange coefficients are computed by multiplying
- the evaluation results of the Newton polynomial operands on the product
- Grid. Afterward, these coefficients are transformed to the Newton
- coefficients.
-
- This is because the Newton monomials depends on the underlying grid
- (specifically, its generating points). The monomials of two Newton
- polynomials are different if the generating points of the two polynomials
- are different. Moreover, a multiplication of two Newton monomial does not,
- in general, return the monomial of a higher degree even for compatible
- monomials (unlike the canonical basis).
-
- There are two exceptions such that the coefficients may be obtained without
- going through the Lagrange basis first because the meaning of the monomials
- are the same.
-
- - if one of the operands is a constant scalar polynomial, whose only a
- single element (:math:`(0, \ldots, 0)`) in the multi-index set of both
- the polynomial and the underlying grid.
- - if one of the operands is a constant scalar polynomial whose underlying
- grid is not scalar but still compatible with the product grid.
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the multiplication expression.
- poly_2 : NewtonPolynomial
- Right operand of the multiplication expression.
- grid_prod : Grid
- The Grid associated with the product polynomial.
- multi_index_prod : MultiIndexSet
- The multi-index set of the product polynomial.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of the product polynomial in the Newton basis.
- """
- # --- Handle case where no transformation is required
- if _is_compute_coeffs_poly_prod_via_monomials(poly_1, poly_2, grid_prod):
- return compute_coeffs_poly_prod_via_monomials(
- poly_1,
- poly_2,
- multi_index_prod,
- )
-
- return _compute_coeffs_poly_prod_via_lagrange(
- poly_1,
- poly_2,
- grid_prod,
- multi_index_prod,
- )
-
-
-def _is_compute_coeffs_poly_prod_via_monomials(
+def _is_compute_coeffs_poly_mul_via_monomials(
poly_1: NewtonPolynomial,
poly_2: NewtonPolynomial,
grid_prod: Grid
) -> bool:
"""Check if the polynomials may be multiplied via the monomials.
+ The multiplication can use the monomial approach when the resulting Newton
+ polynomial lives on the same grid (with the same generating points) as one
+ of the operands. This allows reusing the existing Newton basis without
+ recomputation, which is crucial since changing the degree typically alters
+ the generating points for unnested grids (e.g., Chebyshev-Lobatto).
+
+ Because Newton polynomials are not closed under multiplication (i.e., the
+ product of two Newton basis polynomials is not itself a Newton basis
+ polynomial of higher degree), at least one operand must be a scalar
+ polynomial to ensure that the product can be represented
+ in the same Newton basis as the non-scalar operand.
+
Parameters
----------
poly_1 : NewtonPolynomial
@@ -750,70 +482,34 @@ def _is_compute_coeffs_poly_prod_via_monomials(
one of them has a scalar monomial and both of the underlying grids are
compatible with the given product grid; ``False`` otherwise.
"""
- # Check if one of the operands is a scalar polynomial
+ # Check if either operand is a strictly scalar polynomial
is_scalar_poly = is_scalar(poly_1) or is_scalar(poly_2)
- # Check if one of the monomials of the operand is a scalar
- is_scalar_multi_index_1 = is_scalar(poly_1.multi_index)
- is_scalar_multi_index_2 = is_scalar(poly_2.multi_index)
- is_scalar_multi_index = is_scalar_multi_index_1 or is_scalar_multi_index_2
- # Check if the grids are compatible with the given grid
- is_compatible_grid_1 = poly_1.grid.is_compatible(grid_prod)
- is_compatible_grid_2 = poly_2.grid.is_compatible(grid_prod)
- is_compatible_grids = is_compatible_grid_1 and is_compatible_grid_2
- return is_scalar_poly or (is_scalar_multi_index and is_compatible_grids)
+ # Check grid compatibility based on scalar multi_index
+ poly_1_scalar_idx = is_scalar(poly_1.multi_index)
+ poly_2_scalar_idx = is_scalar(poly_2.multi_index)
+ if poly_1_scalar_idx and poly_2_scalar_idx:
+ # Both have scalar multi_index: always compatible
+ grid_compatible = True
+ elif poly_1_scalar_idx:
+ # Only poly_1 has scalar multi_index: check poly_2's grid
+ grid_compatible = poly_2.grid.has_compatible_gen_points(grid_prod)
+ elif poly_2_scalar_idx:
+ # Only poly_2 has scalar multi_index: check poly_1's grid
+ grid_compatible = poly_1.grid.has_compatible_gen_points(grid_prod)
+ else:
+ # Neither has scalar multi_index: not compatible
+ grid_compatible = False
-def _compute_coeffs_poly_prod_via_lagrange(
- poly_1: NewtonPolynomial,
- poly_2: NewtonPolynomial,
- grid_prod: Grid,
- multi_index_prod: MultiIndexSet,
-) -> np.ndarray:
- """Compute the coefficients of a product Newton polynomial via Lagrange.
-
- Parameters
- ----------
- poly_1 : NewtonPolynomial
- Left operand of the multiplication expression.
- poly_2 : NewtonPolynomial
- Right operand of the multiplication expression.
- grid_product : Grid
- The Grid associated with the product polynomial.
- multi_index_product : MultiIndexSet
- The multi-index set of the product polynomial.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of the product polynomial in the Newton basis.
-
- Notes
- -----
- - Both polynomials are assumed to have the same spatial dimension and
- matching domains. These conditions have been made sure upstream.
- """
- # Compute the values of the operands at the unisolvent nodes
- lag_coeffs_1 = grid_prod(poly_1)
- lag_coeffs_2 = grid_prod(poly_2)
- lag_coeffs_prod = lag_coeffs_1 * lag_coeffs_2
-
- # Transform the Lagrange coefficients into Newton coefficients
- nwt_coeffs_prod = _transform_lag2nwt(
- lag_coeffs_prod,
- grid_prod,
- poly_1.indices_are_separate or poly_2.indices_are_separate,
- multi_index_prod,
- )
-
- return nwt_coeffs_prod
+ # Compatible if either operand is scalar OR grids are compatible
+ return is_scalar_poly or grid_compatible
def _transform_lag2nwt(
- lag_coeffs: np.ndarray,
- grid: Grid,
- indices_are_separate: bool,
+ coeffs_lag: np.ndarray,
multi_index: MultiIndexSet,
+ grid: Grid,
) -> np.ndarray:
"""Transform the (active) Lagrange coefficients to the Newton coefficients.
@@ -823,9 +519,6 @@ def _transform_lag2nwt(
The coefficients of the polynomial in the Lagrange basis.
grid : Grid
The underlying interpolation grid of the polynomial.
- indices_are_separate : bool
- A flag that indicates whether the multi-index set of the grid
- and the given multi-index set are not the same.
multi_index : MultiIndexSet
The multi-index set of the polynomial
@@ -842,13 +535,13 @@ def _transform_lag2nwt(
by better organization and/or using interface functions.
"""
# Transform the Lagrange coefficients into Newton coefficients
- nwt_coeffs = dds(lag_coeffs, grid.tree)
+ coeffs_nwt = dds(coeffs_lag, grid.tree)
# Deal with separate indices, select only w.r.t the active monomials
- if indices_are_separate:
- nwt_coeffs = select_active_monomials(nwt_coeffs, grid, multi_index)
+ if multi_index != grid.multi_index:
+ coeffs_nwt = select_active_monomials(coeffs_nwt, grid, multi_index)
- return nwt_coeffs
+ return coeffs_nwt
def _compute_quad_weights(
diff --git a/src/minterpy/polynomials/scalar_add.py b/src/minterpy/polynomials/scalar_add.py
new file mode 100644
index 00000000..41e7d782
--- /dev/null
+++ b/src/minterpy/polynomials/scalar_add.py
@@ -0,0 +1,105 @@
+"""
+Scalar addition for polynomials.
+
+This module implements scalar addition for polynomial bases where adding
+a constant affects only the coefficient of the (0, ..., 0) monomial
+(Newton, Canonical, Chebyshev). It excludes Lagrange polynomials, where
+scalar addition affects all coefficients uniformly.
+"""
+import numpy as np
+
+from minterpy.global_settings import SCALAR
+from minterpy.core.ABC import MultivariatePolynomialSingleABC
+from minterpy.core import Grid, MultiIndexSet
+
+
+def scalar_add_via_monomials(
+ poly: MultivariatePolynomialSingleABC,
+ scalar: SCALAR,
+) -> MultivariatePolynomialSingleABC:
+ r"""Add an instance of polynomial with a real scalar based on the monomial.
+
+ Monomial-based scalar addition add the scalar to the polynomial coefficient
+ that corresponds to the multi-index set element of :math:`(0, \ldots, 0)`
+ (if exists). If the element does not exist, meaning that the polynomial
+ does not have a constant term, then the multi-index set is extended.
+
+ Parameters
+ ----------
+ poly : MultivariatePolynomialSingleABC
+ A polynomial instance to be added with a scalar.
+ scalar : SCALAR
+ The real scalar number to be added to the polynomial instance.
+
+ Returns
+ -------
+ MultivariatePolynomialSingleABC
+ The summed polynomial; the polynomial is a new instance.
+
+ Notes
+ -----
+ - Currently ``NewtonPolynomial``, ``CanonicalPolynomial``, and
+ ``ChebyshevPolynomial`` follow monomial-based scalar addition, while
+ ``LagrangePolynomial`` does not. For the latter, because the coefficients
+ are function values, adding scalar will add to all coefficients.
+ """
+ # Create a constant polynomial
+ poly_scalar = _create_scalar_poly(poly, scalar)
+
+ # Rely on the `__add__()` method implemented upstream
+ return poly + poly_scalar
+
+
+def _create_scalar_poly(
+ poly: MultivariatePolynomialSingleABC,
+ scalar: SCALAR,
+) -> MultivariatePolynomialSingleABC:
+ """Create a constant scalar polynomial from a given polynomial.
+
+ Parameters
+ ----------
+ poly : MultivariatePolynomialSingleABC
+ An instance of polynomial from which a constant polynomial will be
+ created.
+ scalar : SCALAR
+ Real numbers for the coefficient value of the constant polynomial.
+
+ Returns
+ -------
+ MultivariatePolynomialSingleABC
+ A polynomial of the same instance as ``poly`` having the same grid and
+ domains but with a single element multi-index set. If the grid does
+ not include the element (0, ..., 0) then the grid will be extended.
+
+ Notes
+ -----
+ - Assumes ``poly`` is initialized (``len(poly) >= 1``). The caller
+ verifies this precondition.
+ """
+ # Create a single-element multi-index set of (0, ..., 0)
+ dim = poly.spatial_dimension
+ lp_degree = poly.multi_index.lp_degree
+ mi_0 = MultiIndexSet.from_degree(dim, poly_degree=0, lp_degree=lp_degree)
+
+ # Create a Grid containing the constant term
+ # The input polynomial's grid may not include (0, ..., 0) if its
+ # multi-index set is non-downward-closed, so we construct a minimal
+ # grid containing only this element.
+ grd_0 = Grid(
+ mi_0,
+ poly.grid.generating_function,
+ poly.grid.generating_points,
+ domain=poly.grid.domain,
+ )
+
+ # Create the coefficient
+ if len(poly) == 1:
+ coeffs_0 = np.array([scalar], dtype=poly.coeffs.dtype)
+ else:
+ coeffs_0 = scalar * np.ones(
+ shape=(1, len(poly)),
+ dtype=poly.coeffs.dtype,
+ )
+
+ # Return a polynomial instance of the same class as input
+ return poly.__class__(multi_index=mi_0, coeffs=coeffs_0, grid=grd_0)
diff --git a/src/minterpy/services.py b/src/minterpy/services.py
index 1610e288..4519493a 100644
--- a/src/minterpy/services.py
+++ b/src/minterpy/services.py
@@ -20,7 +20,7 @@
def is_scalar(
obj: Union["MultivariatePolynomialSingleABC", Grid, MultiIndexSet],
) -> bool:
- """Check if a Minterpy object is a scalar.
+ r"""Check if a Minterpy object is a scalar.
This check applies to both polynomial and grid objects.
A scalar multidimensional polynomial (resp. grid) consists of a single
diff --git a/src/minterpy/utils/exceptions.py b/src/minterpy/utils/exceptions.py
new file mode 100644
index 00000000..4bf1025a
--- /dev/null
+++ b/src/minterpy/utils/exceptions.py
@@ -0,0 +1,16 @@
+"""
+This module contains custom exceptions used across Minterpy.
+"""
+
+class InvalidDomainBoundsError(ValueError):
+ """Raised when domain bounds are invalid."""
+ pass
+
+
+class InvalidDerivativeOrderError(ValueError):
+ """Raised when order of derivatives specification is invalid."""
+ pass
+
+
+class DomainMismatchError(ValueError):
+ """Raised when two instances of Domains do not match."""
\ No newline at end of file
diff --git a/src/minterpy/utils/polynomials/interface.py b/src/minterpy/utils/polynomials/interface.py
deleted file mode 100644
index 2009cac0..00000000
--- a/src/minterpy/utils/polynomials/interface.py
+++ /dev/null
@@ -1,392 +0,0 @@
-"""
-This module contains functions that bridge between the upper layer of
-abstraction (``NewtonPolynomial``, ``LagrangePolynomial``, etc.) to the
-lower layer of abstraction (numerical routines that operates on arrays) that
-typically resides in the ``minterpy.utils`` or ``minterpy.jit_compiled``.
-
-The idea behind this module is to minimize the detail of computations
-inside the concrete polynomial modules.
-"""
-import numpy as np
-
-from typing import NamedTuple, Tuple, Union
-
-from minterpy.global_settings import SCALAR
-from minterpy.core.ABC import MultivariatePolynomialSingleABC
-from minterpy.core import Grid, MultiIndexSet
-from minterpy.utils.multi_index import find_match_between
-from minterpy.jit_compiled.canonical import compute_coeffs_poly_prod
-
-
-class PolyData(NamedTuple):
- """Container for complete inputs to create a polynomial in any basis."""
- multi_index: MultiIndexSet
- coeffs: np.ndarray
- internal_domain: np.ndarray
- user_domain: np.ndarray
- grid: Grid
-
-
-def shape_coeffs(
- poly_1: MultivariatePolynomialSingleABC,
- poly_2: MultivariatePolynomialSingleABC,
-) -> Tuple[np.ndarray, np.ndarray]:
- """Shape the polynomial coefficients before carrying out binary operations.
-
- Parameters
- ----------
- poly_1 : MultivariatePolynomialSingleABC
- The first operand in a binary polynomial expression.
- poly_2 : MultivariatePolynomialSingleABC
- The second operand in a binary polynomial expression.
-
- Returns
- -------
- Tuple[:class:`numpy:numpy.ndarray`, :class:`numpy:numpy.ndarray`]
- A tuple of polynomial coefficients, the first and second operands,
- respectively. Both are two-dimensional arrays with the length of
- the polynomials as the number of columns.
-
- Notes
- -----
- - Relevant binary expressions include subtraction, addition,
- and multiplication with polynomials as both operands.
- """
- assert len(poly_1) == len(poly_2)
-
- num_poly = len(poly_1)
- if num_poly > 1:
- return poly_1.coeffs, poly_2.coeffs
-
- coeffs_1 = poly_1.coeffs[:, np.newaxis]
- coeffs_2 = poly_2.coeffs[:, np.newaxis]
-
- return coeffs_1, coeffs_2
-
-
-def get_grid_and_multi_index_poly_sum(
- poly_1: MultivariatePolynomialSingleABC,
- poly_2: MultivariatePolynomialSingleABC,
-) -> Tuple[Grid, MultiIndexSet]:
- """Get the grid and multi-index set of a summed polynomial.
-
- Parameters
- ----------
- poly_1 : MultivariatePolynomialSingleABC
- The first operand in the addition expression.
- poly_2 : MultivariatePolynomialSingleABC
- The second operand in the addition expression.
-
- Returns
- -------
- Tuple[Grid, MultiIndexSet]
- The instances of `Grid` and `MultiIndexSet` of the summed polynomial.
- """
- # --- Compute the union of the grid instances
- grd_sum = poly_1.grid | poly_2.grid
-
- # --- Compute union of the multi-index sets if they are separate
- if poly_1.indices_are_separate or poly_2.indices_are_separate:
- mi_sum = poly_1.multi_index | poly_2.multi_index
- else:
- # Otherwise use the one attached to the grid instance
- mi_sum = grd_sum.multi_index
-
- return grd_sum, mi_sum
-
-
-def get_grid_and_multi_index_poly_prod(
- poly_1: MultivariatePolynomialSingleABC,
- poly_2: MultivariatePolynomialSingleABC,
-) -> Tuple[Grid, MultiIndexSet]:
- """Get the grid and multi-index set of a product polynomial.
-
- Parameters
- ----------
- poly_1 : MultivariatePolynomialSingleABC
- The first operand in the addition expression.
- poly_2 : MultivariatePolynomialSingleABC
- The second operand in the addition expression.
-
- Returns
- -------
- Tuple[Grid, MultiIndexSet]
- The instances of `Grid` and `MultiIndexSet` of the product polynomial.
- """
- # --- Compute the union of the grid instances
- grd_prod = poly_1.grid * poly_2.grid
-
- # --- Compute union of the multi-index sets if they are separate
- if poly_1.indices_are_separate or poly_2.indices_are_separate:
- mi_prod = poly_1.multi_index * poly_2.multi_index
- else:
- # Otherwise use the one attached to the grid instance
- mi_prod = grd_prod.multi_index
-
- return grd_prod, mi_prod
-
-
-def select_active_monomials(
- coeffs: np.ndarray,
- grid: Grid,
- active_multi_index: MultiIndexSet,
-) -> np.ndarray:
- """Get the coefficients that corresponds to the active monomials.
-
- Parameters
- ----------
- coeffs : :class:`numpy:numpy.ndarray`
- The coefficients of a polynomial associated with the multi-index set
- of the grid on which the polynomial lives. They are stored in an array
- whose length is the same as the length of ``grid.multi_index``.
- grid : Grid
- The grid on which the polynomial lives.
- active_multi_index : MultiIndexSet
- The multi-index set of active monomials; the coefficients will be
- picked according to this multi-index set.
-
- Returns
- -------
- :class:`numpy:numpy.ndarray`
- The coefficients of a polynomial associated with the active monomials
- as specified by ``multi_index``.
-
- Notes
- -----
- - ``active_multi_index`` must be a subset of ``grid.multi_index``.
- """
- exponents_multi_index = active_multi_index.exponents
- exponents_grid = grid.multi_index.exponents
- active_idx = find_match_between(exponents_multi_index, exponents_grid)
-
- return coeffs[active_idx]
-
-
-def scalar_add_via_monomials(
- poly: MultivariatePolynomialSingleABC,
- scalar: SCALAR,
-) -> MultivariatePolynomialSingleABC:
- """Add an instance of polynomial with a real scalar based on the monomial.
-
- Monomial-based scalar addition add the scalar to the polynomial coefficient
- that corresponds to the multi-index set element of :math:`(0, \ldots, 0)`
- (if exists). If the element does not exist, meaning that the polynomial
- does not have a constant term, then the multi-index set is extended.
-
- Parameters
- ----------
- poly : MultivariatePolynomialSingleABC
- A polynomial instance to be added with a scalar.
- scalar : SCALAR
- The real scalar number to be added to the polynomial instance.
-
- Returns
- -------
- MultivariatePolynomialSingleABC
- The summed polynomial; the polynomial is a new instance.
-
- Notes
- -----
- - Currently ``NewtonPolynomial``, ``CanonicalPolynomial``, and
- ``ChebyshevPolynomial`` follow monomial-based scalar addition, while
- ``LagrangePolynomial`` does not.
- """
- # Create a constant polynomial
- poly_scalar = _create_scalar_poly(poly, scalar)
-
- # Rely on the `__add__()` method implemented upstream
- return poly + poly_scalar
-
-
-def compute_coeffs_poly_sum_via_monomials(
- poly_1: MultivariatePolynomialSingleABC,
- poly_2: MultivariatePolynomialSingleABC,
- multi_index_sum: MultiIndexSet,
-) -> np.ndarray:
- r"""Compute the coefficients of a summed polynomial via the monomials.
-
- For example, suppose: :math:`A = \{ (0, 0) , (1, 0), (0, 1) \}` with
- coefficients :math:`c_A = (1.0 , 2.0, 3.0)` is summed with
- :math:`B = \{ (0, 0), (1, 0), (2, 0) \}` with coefficients
- :math:`c_B = (1.0, 5.0, 3.0)`. The union/sum multi-index set is
- :math:`A \times B = \{ (0, 0), (1, 0), (2, 0), (0, 1) \}`.
-
- The corresponding coefficients of the sum are:
-
- - :math:`(0, 0)` appears in both operands, so the coefficient
- is :math:`1.0 + 1.0 = 2.0`
- - :math:`(1, 0)` appears in both operands, so the coefficient is
- :math:`2.0 + 5.0 = 7.0`
- - :math:`(2, 0)` only appears in the second operand, so the coefficient
- is :math:`3.0`
- - :math:`(0, 1)` only appears in the first operand, so the coefficient
- is :math:`3.0`
-
- or :math:`c_{A | B} = (2.0, 7.0, 3.0, 3.0)`.
-
- Parameters
- ----------
- poly_1 : MultivariatePolynomialSingleABC
- Left operand of the polynomial-polynomial addition expression.
- poly_2 : MultivariatePolynomialSingleABC
- Right operand of the polynomial-polynomial addition expression.
- multi_index_sum : MultiIndexSet
- The multi-index set of the summed polynomial.
-
- Notes
- -----
- - ``multi_index_sum`` is assumed to be the result of unionizing
- ``poly_1.multi_index`` and ``poly_2.multi_index``.
- - The lengths of ``poly_1`` and ``poly_2`` are assumed to be the same.
- - The function does not check whether the above assumptions are fulfilled;
- the caller is responsible to make sure of that. If the assumptions are
- not fulfilled, the function may not raise any exception but produce
- the wrong results.
- """
- # Shape the coefficients; ensure they have the same dimension
- coeffs_1, coeffs_2 = shape_coeffs(poly_1, poly_2)
-
- # Get the exponents
- exponents_1 = poly_1.multi_index.exponents
- exponents_2 = poly_2.multi_index.exponents
- exponents_sum = multi_index_sum.exponents
-
- # Create the output array
- num_monomials = len(multi_index_sum)
- num_polynomials = len(poly_1)
- coeffs_poly_sum = np.zeros((num_monomials, num_polynomials))
-
- # Get the matching indices
- idx_1 = find_match_between(exponents_1, exponents_sum)
- idx_2 = find_match_between(exponents_2, exponents_sum)
-
- coeffs_poly_sum[idx_1, :] += coeffs_1[:, :]
- coeffs_poly_sum[idx_2, :] += coeffs_2[:, :]
-
- return coeffs_poly_sum
-
-
-def compute_coeffs_poly_prod_via_monomials(
- poly_1: MultivariatePolynomialSingleABC,
- poly_2: MultivariatePolynomialSingleABC,
- multi_index_prod: MultiIndexSet,
-) -> np.ndarray:
- r"""Compute the coefficients of a product polynomial via the monomials.
-
- For example, suppose: :math:`A = \{ (0, 0) , (1, 0), (0, 1) \}` with
- coefficients :math:`c_A = (1.0 , 2.0, 3.0)` is multiplied with
- :math:`B = \{ (0, 0) , (1, 0) \}` with coefficients
- :math:`c_B = (1.0 , 5.0)`. The product multi-index set is
- :math:`A \times B = \{ (0, 0) , (1, 0), (2, 0), (0, 1), (1, 1) \}`.
-
- The corresponding coefficients of the product are:
-
- - :math:`(0, 0)` is coming from :math:`(0, 0) + (0, 0)`, the coefficient
- is :math:`1.0 \times 1.0 = 1.0`
- - :math:`(1, 0)` is coming from :math:`(0, 0) + (1, 0)` and
- :math:`(1, 0) + (0, 0)`, the coefficient is
- :math:`1.0 \times 5.0 + 2.0 \times 1.0 = 7.0`
- - :math:`(2, 0)` is coming from :math:`(1, 0) + (1, 0)`, the coefficient
- is :math:`2.0 \times 5.0 = 10.0`
- - :math:`(0, 1)` is coming from :math:`(0, 1) + (0, 0)`, the coefficient
- is :math:`3.0 \times 1.0 = 3.0`
- - :math:`(1, 1)` is coming from :math:`(0, 1) + (1, 0)`, the coefficient
- is :math:`3.0 \times 5.0 = 15.0`
-
- or :math:`c_{A \times B} = (1.0, 7.0, 10.0, 3.0, 15.0)`.
-
- Parameters
- ----------
- poly_1 : MultivariatePolynomialSingleABC
- Left operand of the polynomial-polynomial multiplication expression.
- poly_2 : MultivariatePolynomialSingleABC
- Right operand of the polynomial-polynomial multiplication expression.
- multi_index_prod : MultiIndexSet
- The multi-index set of the product polynomial.
-
- Notes
- -----
- - ``multi_index_prod`` is assumed to be the result of multiplying
- ``poly_1.multi_index`` and ``poly_2.multi_index``.
- - The lengths of ``poly_1`` and ``poly_2`` are assumed to be the same.
- - The function does not check whether the above assumptions are fulfilled;
- the caller is responsible to make sure of that. If the assumptions are
- not fulfilled, the function may not raise any exception but produce
- the wrong results.
- """
- # Shape the coefficients; ensure they have the same dimension
- coeffs_1, coeffs_2 = shape_coeffs(poly_1, poly_2)
-
- # Pre-allocate output array placeholder
- num_monomials = len(multi_index_prod)
- num_polys = len(poly_1)
- coeffs_prod = np.zeros((num_monomials, num_polys))
-
- # Compute the coefficients (use pre-allocated placeholder as output)
- # NOTE: indices may or may not be separate,
- # use the multi-index instead of the one attached to grid
- exponents_1 = poly_1.multi_index.exponents
- exponents_2 = poly_2.multi_index.exponents
- exponents_prod = multi_index_prod.exponents
- compute_coeffs_poly_prod(
- exponents_1,
- coeffs_1,
- exponents_2,
- coeffs_2,
- exponents_prod,
- coeffs_prod,
- )
-
- return coeffs_prod
-
-
-def _create_scalar_poly(
- poly: MultivariatePolynomialSingleABC,
- scalar: Union[SCALAR, np.ndarray],
-) -> MultivariatePolynomialSingleABC:
- """Create a constant scalar polynomial from a given polynomial.
-
- Parameters
- ----------
- poly : MultivariatePolynomialSingleABC
- An instance of polynomial from which a constant polynomial will be
- created.
- scalar : Union[SCALAR, np.ndarray]
- Real numbers for the coefficient value of the constant polynomial.
- Multiple real numbers (as an array) indicates multiple set of
- coefficients.
-
- Returns
- -------
- MultivariatePolynomialSingleABC
- A polynomial of the same instance as ``poly`` having the same grid and
- domains but with a single element multi-index set. If the grid does
- not include the element (0, ..., 0) then the grid will be extended.
- """
- # Create a single-element multi-index set of (0, ..., 0)
- dim = poly.spatial_dimension
- lp_degree = poly.multi_index.lp_degree
- mi = MultiIndexSet.from_degree(dim, poly_degree=0, lp_degree=lp_degree)
-
- # Create a Grid
- # The grid of the polynomial may not include the multi-index set element
- # (0, ..., 0) (i.e., it's non-downward-closed) so create a new one.
- grd = Grid(mi, poly.grid.generating_function, poly.grid.generating_points)
-
- # Create the coefficient
- if len(poly) == 1:
- coeffs = np.array([scalar])
- else:
- coeffs = scalar * np.ones(
- shape=(1, len(poly)),
- dtype=poly.coeffs.dtype,
- )
-
- # Return a polynomial instance of the same class as input
- return poly.__class__(
- multi_index=mi,
- coeffs=coeffs,
- internal_domain=poly.internal_domain,
- user_domain=poly.user_domain,
- grid=grd,
- )
diff --git a/src/minterpy/utils/verification.py b/src/minterpy/utils/verification.py
index 3aaf0297..ce26b620 100644
--- a/src/minterpy/utils/verification.py
+++ b/src/minterpy/utils/verification.py
@@ -6,34 +6,18 @@
import numpy as np
from _warnings import warn
+from numpy.typing import ArrayLike
-from minterpy.global_settings import DEBUG, DEFAULT_DOMAIN, FLOAT_DTYPE, INT_DTYPE
-
-
-def verify_domain(domain, spatial_dimension):
- """Building and verification of domains.
-
- This function builds a suitable domain as the cartesian product of a one-
- dimensional domain, or verifies the domain shape, of a multivariate domain is
- passed. If None is passed, the default domain is build from [-1,1].
-
- :param domain: Either one-dimensional domain ``(min,max)``, or a stack of domains for each domain with shape ``(spatial_dimension,2)``. If :class:`None` is passed, the ``DEFAULT_DOMAIN`` is repeated for each spatial dimentsion.
- :type domain: array_like, None
- :param spatial_dimension: Dimentsion of the domain space.
- :type spatial_dimension: int
-
- :return verified_domain: Stack of domains for each dimension with shape ``(spatial_dimension,2)``.
- :rtype: np.ndarray
- :raise ValueError: If no domain with the expected shape can be constructed from the input.
-
- """
- if domain is None:
- domain = np.repeat(DEFAULT_DOMAIN[:, np.newaxis], spatial_dimension, axis=1)
- domain = np.require(domain, dtype=FLOAT_DTYPE)
- if domain.ndim == 1:
- domain = np.repeat(domain[:, np.newaxis], spatial_dimension, axis=1)
- check_shape(domain, shape=(2, spatial_dimension))
- return domain
+from minterpy.global_settings import (
+ DEBUG,
+ DEFAULT_DOMAIN,
+ FLOAT_DTYPE,
+ INT_DTYPE,
+)
+from minterpy.utils.exceptions import (
+ InvalidDomainBoundsError,
+ InvalidDerivativeOrderError,
+)
def check_type(obj: Any, expected_type: Type[Any]):
@@ -246,58 +230,6 @@ def check_values(xx: Union[int, float, np.ndarray], **kwargs):
)
-DOMAIN_WARN_MSG2 = "the grid points must fit the interpolation domain [-1;1]^m."
-DOMAIN_WARN_MSG = (
- "this may lead to unexpected behaviour, "
- "e.g. rank deficiencies in the regression matrices, etc. ."
-)
-
-
-def check_domain_fit(points: np.ndarray):
- """Checks weather a given array of points is properly formatted and spans the standard domain :math:`[-1,1]^m`.
-
- .. todo::
- - maybe remove the warnings.
- - generalise to custom ``internal_domain``
-
- :param points: array to be checked. Here ``m`` is the dimenstion of the domain and ``k`` is the number of points.
- :type points: np.ndarray, shape = (m, k)
- :raises ValueError: if the grid points do not fit into the domain :math:`[-1;1]^m`.
- :raises ValueError: if less than one point is passed.
-
- """
- # check first if the sample points are valid
- check_type(points, np.ndarray)
- check_values(points)
- # check weather the points lie outside of the domain
- sample_max = np.max(points, axis=1)
- if not np.allclose(np.maximum(sample_max, 1.0), 1.0):
- raise ValueError(DOMAIN_WARN_MSG2 + f"violated max: {sample_max}")
- sample_min = np.min(points, axis=1)
- if not np.allclose(np.minimum(sample_min, -1.0), -1.0):
- raise ValueError(DOMAIN_WARN_MSG2 + f"violated min: {sample_min}")
- check_dimensionality(points, dimensionality=2)
- nr_of_points, m = points.shape
- if nr_of_points == 0:
- raise ValueError("at least one point must be given")
- if nr_of_points == 1:
- return # one point cannot span the domain
- if DEBUG:
- # check weather the points span the hole domain
- max_grid_val = np.max(sample_max)
- if not np.isclose(max_grid_val, 1.0):
- warn(
- f"the highest encountered value in the given points is {max_grid_val} (expected 1.0). "
- + DOMAIN_WARN_MSG
- )
- min_grid_val = np.min(sample_min)
- if not np.isclose(min_grid_val, -1.0):
- warn(
- f"the smallest encountered value in the given points is {min_grid_val} (expected -1.0). "
- + DOMAIN_WARN_MSG
- )
-
-
def is_real_scalar(x: Union[int, float, np.integer, np.floating]) -> bool:
"""Check if a given value is a real scalar number.
@@ -628,90 +560,30 @@ def verify_poly_coeffs(coeffs: np.ndarray, num_monomials: int) -> np.ndarray:
return coeffs
-def verify_poly_domain(
- domain: np.ndarray,
+def standardize_query_points(
+ xx: np.ndarray,
spatial_dimension: int,
+ truncate_cols: bool = False,
) -> np.ndarray:
- r"""Verify that the given polynomial domain is valid.
-
- Examples
- --------
- >>> verify_poly_domain(np.array([[1], [2]]), 1) # integer array
- array([[1.],
- [2.]])
- >>> verify_poly_domain(np.array([[1, 2], [2, 3]]), 2)
- array([[1., 2.],
- [2., 3.]])
- >>> verify_poly_domain([3, 2], 1) # doctest: +NORMALIZE_WHITESPACE
- Traceback (most recent call last):
- ...
- ValueError: The upper bounds must be strictly larger than the lower
- bounds. Invalid values in the polynomial domain!
- """
- try:
- # The domain must be a NumPy ndarray
- domain = np.atleast_2d(np.array(domain)).astype(np.float64)
- if domain.shape[0] == 1:
- # Column array
- domain = domain.T
-
- # The dimension of the array must be two-dimensional
- check_dimensionality(domain, dimensionality=2)
-
- # The values must not contain inf
- check_values(domain, nan=False, inf=True, zero=True, negative=True)
-
- # The length must be two (lower and upper bounds)
- if domain.shape[0] != 2:
- raise ValueError(
- f"The domain is defined by {domain.shape[0]} numbers "
- "instead of by 2 (lower and upper bounds)."
- )
-
- # The number of columns must be the same as the dimension
- if domain.shape[1] != spatial_dimension:
- raise ValueError(
- f"The dimension of the domain ({domain.shape[1]}) does not "
- f"match the required dimension ({spatial_dimension})."
- )
-
- # The lower bounds must be smaller than the upper bounds
- if np.any(domain[1, :] - domain[0, :] <= 0):
- raise ValueError(
- "The upper bounds must be strictly larger than "
- "the lower bounds."
- )
-
- except TypeError as err:
- custom_message = "Invalid type for polynomial domain!"
- err.args = _add_custom_exception_message(err.args, custom_message)
- raise err
-
- except ValueError as err:
- custom_message = "Invalid values in the polynomial domain!"
- err.args = _add_custom_exception_message(err.args, custom_message)
- raise err
-
- return domain
-
-
-def verify_query_points(xx: np.ndarray, spatial_dimension: int) -> np.ndarray:
r"""Verify if the values of the query points for evaluation are valid.
Parameters
----------
xx : :class:`numpy:numpy.ndarray`
A one- or two-dimensional array of query points at which a polynomial
- is evaluated. The length of the array is ``N``, i.e., the number
+ is evaluated. The length of the array is ``k``, i.e., the number
of query points.
spatial_dimension : int
The spatial dimension of the polynomial (``m``).
The shape of the query points array must be consistent with this.
+ truncate_cols : bool, optional
+ If ``True``, truncate columns to match ``spatial_dimension`` after
+ conversion to array. Default is ``False``.
Returns
-------
:class:`numpy:numpy.ndarray`
- A two-dimensional array of ``numpy.float64`` with a length of ``N``
+ A two-dimensional array of ``numpy.float64`` with a length of ``k``
and a number of columns of ``m``. If the dtype of the array is not of
`numpy.float64`, the function does a type conversion if possible.
@@ -725,29 +597,31 @@ def verify_query_points(xx: np.ndarray, spatial_dimension: int) -> np.ndarray:
Examples
--------
- >>> verify_query_points(1, 1) # a scalar integer
+ >>> standardize_query_points(1, 1) # a scalar integer
array([[1.]])
- >>> verify_query_points([3., 4., 5.], 1) # a list
+ >>> standardize_query_points([3., 4., 5.], 1) # a list
array([[3.],
[4.],
[5.]])
- >>> verify_query_points([[3, 4, 5]], 3) # a list of lists of integers
+ >>> standardize_query_points([[3, 4, 5]], 3) # a list of lists of integers
array([[3., 4., 5.]])
- >>> verify_query_points(np.array([1., 2., 3.]), 1) # 1 dimension
+ >>> standardize_query_points(np.array([1., 2., 3.]), 1) # 1 dimension
array([[1.],
[2.],
[3.]])
- >>> verify_query_points(np.array([[1., 2.], [3., 4.]]), 2) # 2 dimensions
+ >>> standardize_query_points(np.array([[1., 2.], [3., 4.]]), 2) # 2 dims
array([[1., 2.],
[3., 4.]])
- >>> verify_query_points(np.array([1, 2, 3]), 1) # integer
+ >>> standardize_query_points(np.array([1, 2, 3]), 1) # integer
array([[1.],
[2.],
[3.]])
- >>> verify_query_points(np.array(["a", "b"]), 1)
+ >>> standardize_query_points(np.array([[1, 2, 3]]), 2, True) # Truncate
+ array([[1., 2.]])
+ >>> standardize_query_points(np.array(["a", "b"]), 1) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
- ValueError: could not convert string to float: 'a' Invalid values in query points array!
+ ValueError: could not convert string to float: ...
Notes
-----
@@ -764,8 +638,10 @@ def verify_query_points(xx: np.ndarray, spatial_dimension: int) -> np.ndarray:
# Check spatial dimension
if xx.ndim == 1:
+ xx = xx[:spatial_dimension] if truncate_cols else xx
dim = 1
else:
+ xx = xx[:, :spatial_dimension] if truncate_cols else xx
dim = xx.shape[1]
dim_is_consistent = dim == spatial_dimension
if not dim_is_consistent:
@@ -859,6 +735,193 @@ def verify_poly_power(power: int) -> int:
return power
+def verify_domain_bounds(bounds: ArrayLike) -> np.ndarray:
+ r"""Verify that the given bounds for a domain are valid.
+
+ Valid domain bounds must satisfy the following conditions:
+
+ - Convertible to a 2D numeric array
+ - Shape of ``(m, 2)`` where ``m >= 1`` (dimension is positive)
+ - Finite values (no NaN or inf)
+ - Non-empty
+ - Upper bounds strictly greater than lower bounds for all dimensions
+
+ Parameters
+ ----------
+ bounds : array_like
+ Domain bounds as either a 1D array ``[lower, upper]`` for a single
+ dimension, or a 2D array of shape ``(m, 2)`` for ``m`` dimensions.
+ Each row specifies ``[lower, upper]`` for one dimension.
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ Validated domain bounds as a 2D array of shape ``(m, 2)`` with
+ dtype ``FLOAT_DTYPE``.
+
+ Raises
+ ------
+ InvalidDomainBoundsError
+ If the bounds violate any validation condition (see above).
+
+ Examples
+ --------
+ >>> verify_domain_bounds(np.array([-1, 1])) # 1D array, single dimension
+ array([[-1., 1.]])
+ >>> verify_domain_bounds(np.array([[1, 2]])) # Integer array (auto-converted)
+ array([[1., 2.]])
+ >>> verify_domain_bounds(np.array([[1., 2.], [2., 3.]])) # 2D domain
+ array([[1., 2.],
+ [2., 3.]])
+ >>> verify_domain_bounds([3, 2]) # doctest: +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ minterpy.utils.exceptions.InvalidDomainBoundsError: Upper bounds must be
+ strictly greater than lower bounds.
+ Invalid dimensions: [0]; Invalid bounds: [[3.0, 2.0]]
+ """
+ # Convert to a two-dimensional NumPy array of FLOAT_DTYPE
+ try:
+ bounds = np.atleast_2d(np.array(bounds, dtype=FLOAT_DTYPE))
+ except (TypeError, ValueError) as err:
+ raise InvalidDomainBoundsError(
+ f"Domain bounds must be array-like of numeric values, "
+ f"got {type(bounds).__name__} instead"
+ ) from err
+
+ # Validate dimensionality
+ if bounds.ndim != 2:
+ raise InvalidDomainBoundsError(
+ f"Domain bounds must be 2D after conversion, "
+ f"got {bounds.ndim}D array instead"
+ )
+
+ # Validate non-empty array
+ if bounds.size == 0:
+ raise InvalidDomainBoundsError("Domain bounds cannot be empty")
+
+ # Validate shape
+ if bounds.shape[1] != 2:
+ raise InvalidDomainBoundsError(
+ f"Bounds must have 2 columns [lower, upper], "
+ f"got {bounds.shape[1]} columns instead"
+ )
+
+ # Validate finite values (no Inf and NaN allowed)
+ if not np.isfinite(bounds).all():
+ raise InvalidDomainBoundsError(
+ "Bounds must be finite (no NaN or inf values)"
+ )
+
+ # Validate lower < upper
+ widths = bounds[:, 1] - bounds[:, 0]
+ if np.any(widths <= 0):
+ invalid_dims = np.where(widths <= 0)[0]
+ raise InvalidDomainBoundsError(
+ f"Upper bounds must be strictly greater than lower bounds. "
+ f"Invalid dimensions: {invalid_dims.tolist()}; "
+ f"Invalid bounds: {bounds[invalid_dims].tolist()}"
+ )
+
+ return bounds
+
+
+def verify_derivative_order(
+ order: ArrayLike,
+ spatial_dimension: int,
+) -> np.ndarray:
+ r"""Verify that the given order of derivatives are valid.
+
+ Valid orders of derivatives must satisfy the following conditions:
+
+ - Convertible to a 1D numeric array
+ - The length must match the spatial dimension (``m``)
+ - Integer values (whole numbers only, round floats like 2.0 are accepted
+ and converted)
+ - Non-negative (all values are >=0, no NaN, no Inf)
+
+ Parameters
+ ----------
+ order : array_like
+ Order of derivative for each dimension. An order must be specified
+ for each spatial dimension. Non-integer values are accepted only
+ if they represent whole numbers.
+ spatial_dimension : int
+ Expected number of dimensions (must be positive).
+
+ Returns
+ -------
+ :class:`numpy:numpy.ndarray`
+ Validated order of derivatives as a 1D array of shape ``(m,)`` with
+ dtype ``INT_DTYPE``.
+
+ Raises
+ ------
+ InvalidDerivativeOrderError
+ If the specification violate any validation condition (see above).
+
+ Examples
+ --------
+ >>> verify_derivative_order(1, 1) # Scalar, 1D
+ array([1])
+ >>> verify_derivative_order([1, 0, 1], 3) # List as order, 3D
+ array([1, 0, 1])
+ >>> verify_derivative_order(np.array([1, 0, 2, 0]), 4) # 4D
+ array([1, 0, 2, 0])
+ >>> verify_derivative_order([3, 2, 0], 2) # doctest: +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ minterpy.utils.exceptions.InvalidDerivativeOrderError: Order length 3 does
+ not match spatial dimension 2. Order of derivative for each dimension must
+ be specified.
+ >>> verify_derivative_order([3.2, 2.0], 2) # doctest: +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ minterpy.utils.exceptions.InvalidDerivativeOrderError: Order of derivatives
+ must be whole numbers. Invalid values: [3.2]
+ >>> verify_derivative_order([0, -1], 2) # doctest: +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ minterpy.utils.exceptions.InvalidDerivativeOrderError: Order of
+ derivatives must be non-negative. Negative values at dimensions: [1];
+ Invalid values: [-1]
+ """
+ # Normalize input to 1D NumPy array
+ order = np.ravel(order)
+
+ # Validate length matches spatial dimension
+ if len(order) != spatial_dimension:
+ raise InvalidDerivativeOrderError(
+ f"Order length {len(order)} does not match spatial dimension "
+ f"{spatial_dimension}. Order of derivative for each dimension "
+ f"must be specified."
+ )
+
+ # Validate integer values (accept round floats)
+ round_mask = np.mod(order, 1) == 0
+ if not np.all(round_mask):
+ non_round_mask = ~round_mask
+ invalid_values = order[non_round_mask]
+ raise InvalidDerivativeOrderError(
+ f"Order of derivatives must be whole numbers. "
+ f"Invalid values: {invalid_values.tolist()}"
+ )
+
+ # Validate non-negative values
+ if np.any(order < 0):
+ negative_dims = np.where(order < 0)[0]
+ invalid_values = order[negative_dims]
+ raise InvalidDerivativeOrderError(
+ f"Order of derivatives must be non-negative. "
+ f"Negative values at dimensions: {negative_dims.tolist()}; "
+ f"Invalid values: {invalid_values.tolist()}"
+ )
+
+ # Convert to integer dtype
+ order = order.astype(INT_DTYPE)
+
+ return order
+
def _add_custom_exception_message(
exception_args: Tuple[str, ...],
diff --git a/tests/conftest.py b/tests/conftest.py
index 30202c1a..de903f2a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,6 +12,7 @@
from minterpy import (
MultiIndexSet,
+ Domain,
Grid,
LagrangePolynomial,
NewtonPolynomial,
@@ -69,11 +70,11 @@
# Primary parameters to create complete multi-index sets, grids, & polynomials
SPATIAL_DIMENSIONS = [1, 3]
-POLY_DEGREES = [0, 1, 4] # NOTE: Include test for poly_degree 0 (Issue #27)
+POLY_DEGREES = [0, 1, 3] # NOTE: Include test for poly_degree 0 (Issue #27)
LP_DEGREES = [0.5, 1.0, 2.0, np.inf]
# Number of coefficient sets in a single polynomial instance
-NUM_POLYS = [1, 2, 5]
+NUM_POLYS = [1, 4]
# asserts that a call runs as expected
@@ -292,6 +293,34 @@ def num_polynomials(request):
"""Fixture for the number of polynomials."""
return request.param
+# Fixture for Domain types
+DOMAIN_TYPES = ["default", "random"]
+
+
+def _id_domain_type(domain_type):
+ return f"dom={domain_type:<7}"
+
+
+@pytest.fixture(params=DOMAIN_TYPES, ids=_id_domain_type)
+def domain_type(request):
+ return request.param
+
+
+# Fixture for Domain instance
+@pytest.fixture
+def domain(SpatialDimension, domain_type):
+ # Create a default domain
+ if domain_type == "default":
+ return Domain.identity(SpatialDimension)
+
+ # Create a custom domain
+ lb = np.random.uniform(0, 5, size=SpatialDimension)
+ # Ensure ub > lb
+ ub = lb + np.random.uniform(5, 10, size=SpatialDimension)
+
+ return Domain(np.c_[lb, ub])
+
+
# fixtures for number of similar polynomials
nr_similar_polynomials = [None, 1, 2]
@@ -796,7 +825,7 @@ def build_rnd_points(nr_points, spatial_dimension, nr_poly=None, seed=None):
def build_random_newton_polynom(
- dim: int, deg: int, lp: int, n_poly=1, seed=None
+ dim: int, deg: int, lp: int, domain=None, n_poly=1, seed=None
) -> NewtonPolynomial:
"""Build a random Newton polynomial.
@@ -824,7 +853,7 @@ def build_random_newton_polynom(
else:
rnd_coeffs = np.random.uniform(-1, 1, size=(len(mi), n_poly))
- return NewtonPolynomial(mi, rnd_coeffs)
+ return NewtonPolynomial(mi, rnd_coeffs, domain=domain)
def build_random_multi_index():
diff --git a/tests/test_domain.py b/tests/test_domain.py
new file mode 100644
index 00000000..69610893
--- /dev/null
+++ b/tests/test_domain.py
@@ -0,0 +1,669 @@
+import numpy as np
+import pytest
+
+from minterpy import Domain
+from minterpy.global_settings import DEFAULT_DOMAIN
+from minterpy.utils.exceptions import (
+ DomainMismatchError,
+ InvalidDomainBoundsError,
+)
+
+INVALID_BOUNDS = [
+ {"type": "empty", "bounds": np.array([])},
+ {"type": "nan", "bounds": np.array([[1, np.nan], [2, 3]])},
+ {"type": "inf", "bounds": np.array([[1, 3], [2, np.inf]])},
+ {"type": "too_few", "bounds": np.array([1])},
+ {"type": "too_many", "bounds": np.array([1, 2, 3])},
+ {"type": "wrong_shape", "bounds": np.array([[1], [2]])},
+ {"type": "lower>upper", "bounds": np.array([3, 1])},
+ {"type": "lower==upper", "bounds": np.array([[1, 1], [2, 2], [3, 3]])},
+]
+
+
+# Invalid values (`val`)
+def _id_invalid_bounds(invalid_bounds):
+ return f"val={invalid_bounds['type']:>12}"
+
+
+@pytest.fixture(params=INVALID_BOUNDS, ids=_id_invalid_bounds)
+def invalid_bounds(request):
+ """Return invalid bounds fixture."""
+ return request.param["bounds"]
+
+
+@pytest.fixture
+def random_bounds_valid(SpatialDimension):
+ """Generate random valid bounds for testing."""
+ bounds = np.random.uniform(low=0, high=5, size=(SpatialDimension, 2))
+ bounds[:, 0] = -1 * bounds[:, 0]
+
+ return bounds
+
+
+@pytest.fixture
+def random_bounds_pair(SpatialDimension):
+ """Generate a pair of random valid bounds for testing."""
+ bounds_1 = np.random.uniform(low=0, high=5, size=(SpatialDimension, 2))
+ bounds_1[:, 0] = -1 * bounds_1[:, 0]
+
+ bounds_2 = np.random.uniform(low=0, high=5, size=(SpatialDimension, 2))
+ bounds_2[:, 0] = -1 * bounds_2[:, 0]
+
+ return bounds_1, bounds_2
+
+
+def generate_values_inbounds(random_bounds):
+ """Generate random valid values for testing."""
+ dim = len(random_bounds)
+ lb, ub = random_bounds[:, 0], random_bounds[:, 1]
+ bound_widths = ub - lb
+
+ return lb + np.random.rand(10, dim) * bound_widths
+
+
+def generate_values_outbounds(random_bounds):
+ """Generate random out of bounds values for testing."""
+ dim = len(random_bounds)
+ xx = np.empty((10, dim))
+ lb, ub = random_bounds[:, 0], random_bounds[:, 1]
+
+ idx = np.random.randint(0, 10)
+ for j in range(dim):
+ for i in range(idx):
+ xx[i, j] = lb[j] - np.random.rand()
+ for i in range(10 - idx):
+ xx[idx+i, j] = ub[j] + np.random.rand()
+
+ return xx
+
+class TestInit:
+ """All tests related to the default construction of Domain."""
+
+ def test_one_dimension(self):
+ """Test that a 1D domain is correctly initialized."""
+ bounds = np.array([-5, 5])
+ domain = Domain(bounds)
+
+ # Assertions
+ assert domain.spatial_dimension == 1
+ assert np.all(bounds == domain.bounds)
+
+ def test_invalid_bounds_smaller_upper(self, invalid_bounds):
+ """Test raising an exception for any invalid bounds."""
+
+ with pytest.raises(InvalidDomainBoundsError):
+ _ = Domain(invalid_bounds)
+
+
+class TestFactoryMethods:
+ """All tests related to the construction via factory methods."""
+
+ def test_uniform_domain(self, SpatialDimension):
+ """Test that a uniform domain is correctly initialized."""
+ upper_bound = np.random.uniform(0, 5)
+ lower_bound = -1 * upper_bound
+
+ domain = Domain.uniform(SpatialDimension, lower_bound, upper_bound)
+
+ # Assertions
+ assert domain.spatial_dimension == SpatialDimension
+ assert np.all(domain.bounds[:, 0] == lower_bound)
+ assert np.all(domain.bounds[:, 1] == upper_bound)
+
+ def test_normalized_domain(self, SpatialDimension):
+ """Test that a normalized domain is correctly initialized."""
+ domain = Domain.identity(SpatialDimension)
+
+ # Assertions
+ assert domain.spatial_dimension == SpatialDimension
+ assert np.all(domain.bounds[:, 0] == -1.)
+ assert np.all(domain.bounds[:, 1] == 1.)
+
+
+class TestProperties:
+ """All tests related to the properties of Domain."""
+
+ def test_spatial_dimension(self, random_bounds_valid):
+ """Test the spatial dimension property."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Assertion
+ assert my_dom.spatial_dimension == random_bounds_valid.shape[0]
+
+ def test_bounds(self, random_bounds_valid):
+ """Test the bounds property."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Assertions
+ assert np.all(my_dom.bounds == random_bounds_valid)
+
+ def test_lower_bounds(self, random_bounds_valid):
+ """Test the lower bounds property."""
+ dom = Domain(random_bounds_valid)
+
+ # Assertion
+ assert np.all(dom.lowers == random_bounds_valid[:, 0])
+
+ def test_upper_bounds(self, random_bounds_valid):
+ """Test the upper bounds property."""
+ dom = Domain(random_bounds_valid)
+
+ # Assertion
+ assert np.all(dom.uppers == random_bounds_valid[:, 1])
+
+ def test_domain_widths(self, random_bounds_valid):
+ """Test the domain widths property."""
+ dom = Domain(random_bounds_valid)
+
+ # Assertion
+ diff_bounds = random_bounds_valid[:, 1] - random_bounds_valid[:, 0]
+ assert np.all(dom.widths == diff_bounds)
+
+ def test_normalized(self, SpatialDimension):
+ """Test the normalized property for normalized domain."""
+ my_dom_1 = Domain.identity(SpatialDimension)
+ my_dom_2 = Domain.uniform(SpatialDimension, -1, 1)
+ bounds = np.ones((SpatialDimension, 2))
+ bounds[:, 0] = -1
+ my_dom_3 = Domain(bounds)
+
+ # Assertions
+ assert my_dom_1.is_identity
+ assert my_dom_2.is_identity
+ assert my_dom_3.is_identity
+ # Normalized domain is always uniform.
+ assert my_dom_1.is_uniform
+ assert my_dom_2.is_uniform
+ assert my_dom_3.is_uniform
+
+ def test_not_normalized(self, random_bounds_valid):
+ """"Test the normalized property for non-normalized domain."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Assertion
+ assert not my_dom.is_identity
+
+ def test_is_uniform(self, SpatialDimension):
+ """Test the uniform property for uniform domain."""
+ # Create upper and lower bounds for uniform domain
+ upper_bound = np.random.uniform(0, 5)
+ lower_bound = -1 * upper_bound
+
+ # Create domain
+ domain = Domain.uniform(SpatialDimension, lower_bound, upper_bound)
+
+ # Assertions
+ assert domain.is_uniform
+ assert not domain.is_identity # In general not normalized.
+
+ def test_is_not_uniform(self, random_bounds_valid):
+ """Test the uniform property for non-uniform domain."""
+ domain = Domain(random_bounds_valid)
+
+ # Assertion
+ if domain.spatial_dimension == 1:
+ pytest.skip("1D domain is always uniform.")
+
+ assert not domain.is_uniform
+
+class TestMapValues:
+ """All tests related to the mapping of values."""
+
+ def test_to_normalized_edges(self, random_bounds_valid):
+ """"Test the mapping of the edges to the normalized domain."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate values at the edge of the domain
+ xx = np.c_[dom.lowers, dom.uppers].T
+ yy = dom.map_to_internal(xx)
+
+ # Assertions
+ assert np.allclose(yy[0], -1.0)
+ assert np.allclose(yy[1], 1.0)
+
+ def test_from_normalized_edges(self, random_bounds_valid):
+ """"Test the mapping of the edges from the normalized domain."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate values at the edge of the normalized domain
+ dim = dom.spatial_dimension
+ xx = np.c_[-1 * np.ones(dim), np.ones(dim)].T
+ yy = dom.map_from_internal(xx)
+
+ # Assertions
+ assert np.allclose(yy[0], dom.lowers)
+ assert np.allclose(yy[1], dom.uppers)
+
+ def test_to_normalized_extrapolation(self, random_bounds_valid):
+ """Test that extrapolation to normalized domain is correctly handled."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate out of bounds values in the original domain
+ xx = generate_values_outbounds(random_bounds_valid)
+ yy = dom.map_to_internal(xx)
+
+ # Assertion
+ default_lb, default_ub = DEFAULT_DOMAIN
+ assert np.any(yy <= default_lb) or np.any(yy >= default_ub)
+
+ def test_from_normalized_extrapolation(self, random_bounds_valid):
+ """Test that extrapolation from normalized domain is correctly handled.
+ """
+ dom = Domain(random_bounds_valid)
+
+ # Generate out of bounds values in the normalized domain
+ bounds = np.ones((dom.spatial_dimension, 2))
+ bounds[:, 0] = -1
+ xx = generate_values_outbounds(bounds)
+ yy = dom.map_from_internal(xx)
+
+ # Assertion
+ lb, ub = dom.lowers, dom.uppers
+ assert np.any(yy <= lb) or np.any(yy >= ub)
+
+ def test_to_normalized_valid(self, random_bounds_valid):
+ """Test transformation to the normalized domain with valid values."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate random valid input values
+ xx = generate_values_inbounds(random_bounds_valid)
+ # All the same for valid input values
+ yy_1 = dom.map_to_internal(xx)
+ yy_2 = dom.map_to_internal(xx, validate=True)
+ yy_3 = dom.map_to_internal(xx, validate=False)
+
+ # Assertion
+ default_lb, default_ub = DEFAULT_DOMAIN
+ assert np.all(yy_1 >= default_lb) and np.all(yy_1 <= default_ub)
+ assert np.all(yy_2 >= default_lb) and np.all(yy_2 <= default_ub)
+ assert np.all(yy_3 >= default_lb) and np.all(yy_3 <= default_ub)
+
+ def test_to_normalized_invalid(self, random_bounds_valid):
+ """Test transformation to the normalized domain with invalid values."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate random invalid input values
+ xx = generate_values_outbounds(random_bounds_valid)
+
+ with pytest.raises(ValueError):
+ _ = dom.map_to_internal(xx, validate=True)
+
+ def test_from_internal_valid(self, random_bounds_valid):
+ """Test transformation from the internal domain with valid values."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate random valid input values in the normalized domain
+ xx = -1 + 2 * np.random.rand(10, dom.spatial_dimension)
+ # All the same for valid input values
+ yy_1 = dom.map_from_internal(xx)
+ yy_2 = dom.map_from_internal(xx, validate=True)
+ yy_3 = dom.map_from_internal(xx, validate=False)
+
+ # Assertion
+ lb, ub = dom.lowers, dom.uppers
+ assert np.all(yy_1 >= lb) and np.all(yy_1 <= ub)
+ assert np.all(yy_2 >= lb) and np.all(yy_2 <= ub)
+ assert np.all(yy_3 >= lb) and np.all(yy_3 <= ub)
+
+ def test_from_normalized_validation_invalid(self, random_bounds_valid):
+ """Test transformation from the normalized domain with invalid values.
+ """
+ dom = Domain(random_bounds_valid)
+
+ # Generate random invalid input values in the normalized domain
+ xx = -10 + 3 * np.random.rand(10, dom.spatial_dimension)
+
+ # Assertion
+ with pytest.raises(ValueError):
+ _ = dom.map_from_internal(xx, validate=True)
+
+ def test_from_and_to(self, random_bounds_valid):
+ """Test that values are correctly mapped from and to normalized."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate random valid input values in the normalized domain
+ xx_1 = -1 + 2 * np.random.rand(10, dom.spatial_dimension)
+ yy = dom.map_from_internal(xx_1)
+ xx_2 = dom.map_to_internal(yy)
+
+ # Assertions
+ assert np.allclose(xx_1, xx_2)
+
+ def test_to_and_from(self, random_bounds_valid):
+ """Test that values are correctly mapped to and from normalized."""
+ dom = Domain(random_bounds_valid)
+
+ # Generate random valid input values in the original domain
+ xx_1 = generate_values_inbounds(random_bounds_valid)
+ yy = dom.map_to_internal(xx_1)
+ xx_2 = dom.map_from_internal(yy)
+
+ # Assertions
+ assert np.allclose(xx_1, xx_2)
+
+
+class TestScalingFactor:
+ """All tests related to the scaling factor."""
+
+ def test_integration(self, random_bounds_valid):
+ """Test the integration scaling factor."""
+ dom = Domain(random_bounds_valid)
+ diff_bounds = random_bounds_valid[:, 1] - random_bounds_valid[:, 0]
+
+ assert np.isclose(
+ dom.int_factor(),
+ np.prod(diff_bounds) / 2**dom.spatial_dimension,
+ )
+
+ def test_integration_normalized(self, SpatialDimension):
+ """Test the integration scaling factor for normalized domain."""
+ dom = Domain.identity(SpatialDimension)
+
+ assert np.isclose(dom.int_factor(), 1.)
+
+ def test_diff_zero_order(self, random_bounds_valid):
+ """Test the differentiation scaling factor for 0th-order derivative."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Assertions
+ expected = 1.0 # Always 1.0
+ dim = my_dom.spatial_dimension
+ assert np.isclose(
+ my_dom.diff_factor(np.zeros(dim, dtype=int)),
+ expected,
+ )
+
+ def test_diff_identity(self, SpatialDimension):
+ """Test the differentiation scaling factor in the identity domain."""
+ my_dom = Domain.identity(SpatialDimension)
+
+ # Assertion
+ expected = 1.0 # Always 1.0
+ order = np.random.randint(0, 5, size=(SpatialDimension,))
+ assert np.isclose(my_dom.diff_factor(order), expected)
+
+ def test_diff_first_order(self, SpatialDimension):
+ """Test the differentiation scaling factor for 1st-order derivative."""
+ dom = Domain.uniform(SpatialDimension, 0, 10)
+ order = np.ones(dom.spatial_dimension, dtype=int)
+
+ diff_factor = dom.diff_factor(order)
+ iwidth = dom.internal_bounds[0, 1] - dom.internal_bounds[0, 0]
+ width = dom.widths[0]
+
+ assert np.isclose(diff_factor, (iwidth/width)**SpatialDimension)
+
+
+class TestEquality:
+ """All tests related to equality check."""
+
+ def test_equal(self, random_bounds_valid):
+ """Test strict equality of values between two instances."""
+ my_dom_1 = Domain(random_bounds_valid)
+ my_dom_2 = Domain(random_bounds_valid)
+
+ # Assertions
+ assert my_dom_1 is not my_dom_2
+ assert my_dom_2 is not my_dom_1
+ assert my_dom_1 == my_dom_2
+ assert my_dom_2 == my_dom_1
+
+ def test_not_equal_bounds(self, random_bounds_pair):
+ """Test strict inequality of bounds between two instances."""
+ bounds_1, bounds_2 = random_bounds_pair
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions
+ assert my_dom_1 is not my_dom_2
+ assert my_dom_2 is not my_dom_1
+ assert my_dom_1 != my_dom_2
+ assert my_dom_2 != my_dom_1
+
+ def test_not_equal_dimensions(self, random_bounds_valid):
+ """Test strict inequality of dimensions between two instances."""
+ bounds_1 = random_bounds_valid
+ bounds_2 = np.vstack([bounds_1, bounds_1[-1]])
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions
+ assert my_dom_1 is not my_dom_2
+ assert my_dom_2 is not my_dom_1
+ assert my_dom_1 != my_dom_2
+ assert my_dom_2 != my_dom_1
+
+ @pytest.mark.parametrize("invalid_type", [None, 1, "string"])
+ def test_not_equal_type(self, random_bounds_valid, invalid_type):
+ """Test equality check with invalid types."""
+
+ dom = Domain(random_bounds_valid)
+
+ # Assertion
+ assert dom != invalid_type
+
+
+class TestPartialMatching:
+ """All tests related to partial matching."""
+
+ def test_partially_matched(self, random_bounds_valid):
+ """Test partially matched domain."""
+ bounds_1 = random_bounds_valid
+ bounds_2 = np.vstack([bounds_1, bounds_1[-1]])
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions (symmetric)
+ assert my_dom_1.partial_matching(my_dom_2)
+ assert my_dom_2.partial_matching(my_dom_1)
+
+ def test_no_partially_matched(self, random_bounds_pair):
+ """Test no partially matched domain."""
+ bounds_1, bounds_2 = random_bounds_pair
+
+ my_dom_1 = Domain(bounds_1)
+ if len(bounds_2) > 1:
+ my_dom_2 = Domain(bounds_2[:-1])
+ else:
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions (symmetric)
+ assert not my_dom_1.partial_matching(my_dom_2)
+ assert not my_dom_2.partial_matching(my_dom_1)
+
+
+class TestContains:
+ """All tests related to the contains check."""
+
+ def test_contained(self, random_bounds_valid):
+ """Test containment of sample inside the bounds."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Generate random valid input values in the original domain
+ xx = generate_values_inbounds(random_bounds_valid)
+
+ # Assertion
+ assert np.all(my_dom.contains(xx))
+
+ def test_not_contained(self, random_bounds_valid):
+ """Test containment of sample outside the bounds."""
+ my_dom = Domain(random_bounds_valid)
+
+ # Generate input values outside the original bound
+ xx = generate_values_outbounds(random_bounds_valid)
+
+ # Assertion
+ assert not np.any(my_dom.contains(xx))
+
+
+class TestUnion:
+ """All tests related to the union operation."""
+
+ def test_identical(self, random_bounds_valid):
+ """Test union of identical instances (edge case)."""
+ dom_1 = Domain(random_bounds_valid)
+
+ dom_2 = dom_1 | dom_1
+
+ # Assertions
+ assert dom_1 == dom_2
+ assert dom_1 is dom_2 # Union of identical instances is itself
+
+ def test_equal(self, random_bounds_valid):
+ """Test union of equal instances."""
+ dom_1 = Domain(random_bounds_valid)
+ dom_2 = Domain(random_bounds_valid)
+
+ dom_3 = dom_1 | dom_2
+ dom_4 = dom_2 | dom_1
+
+ # Assertions
+ assert dom_1 == dom_2
+ print(np.array_equal(dom_2.bounds, dom_3.bounds))
+ print(np.array_equal(dom_2.internal_bounds, dom_3.internal_bounds))
+ assert dom_2 == dom_3
+ assert dom_3 == dom_4
+ assert dom_3 is not dom_4
+
+ def test_expansion(self, random_bounds_valid):
+ """Test union of an instance with a higher dimension."""
+ bounds_1 = random_bounds_valid
+ bounds_2 = np.vstack([bounds_1, bounds_1[-1]])
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+ my_dom_3 = my_dom_1 | my_dom_2
+ my_dom_4 = my_dom_2 | my_dom_1
+
+ # Assertions
+ assert my_dom_2 == my_dom_3
+ assert my_dom_2 == my_dom_4
+
+ def test_chaining(self, random_bounds_valid):
+ """Test union of three instances."""
+ bounds_1 = random_bounds_valid
+ bounds_2 = np.vstack([bounds_1, bounds_1[-1]])
+ bounds_3 = np.vstack([bounds_2, bounds_2[-1]])
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+ my_dom_3 = Domain(bounds_3)
+
+ # Assertions
+ assert my_dom_3 == my_dom_1 | my_dom_2 | my_dom_3
+ assert my_dom_3 == my_dom_2 | my_dom_3 | my_dom_1
+ assert my_dom_3 == my_dom_3 | my_dom_2 | my_dom_1
+
+ def test_invalid(self, random_bounds_pair):
+ """Test union of instances with mismatching dimensions."""
+ bounds_1, bounds_2 = random_bounds_pair
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ with pytest.raises(DomainMismatchError):
+ _ = my_dom_1 | my_dom_2
+
+
+class TestExpandDim:
+ """All tests related to the expansion of the domain."""
+
+ def test_to_target_int_same(self, SpatialDimension):
+ """Test expanding the dimension to the same target dimension."""
+ dom_1 = Domain.identity(SpatialDimension)
+ dom_2 = dom_1.expand_dim(SpatialDimension)
+
+ # Assertion
+ assert dom_1 == dom_2
+ assert dom_1 is not dom_2
+
+ def test_to_target_int_normalized(self, SpatialDimension):
+ """Test expanding the dimension to an integer target dimension."""
+ my_dom_1 = Domain.identity(SpatialDimension)
+
+ my_dom_2 = my_dom_1.expand_dim(SpatialDimension + 1)
+
+ # Assertions
+ assert my_dom_1 is not my_dom_2
+ assert my_dom_1 != my_dom_2
+ assert my_dom_2 == Domain.identity(SpatialDimension + 1)
+
+ def test_to_target_int_contraction(self, SpatialDimension):
+ """Test contracting the dimension to an integer target dimension."""
+ my_dom = Domain.identity(SpatialDimension)
+
+ with pytest.raises(ValueError):
+ _ = my_dom.expand_dim(SpatialDimension - 1)
+
+ def test_to_target_int_nonuniform(self, random_bounds_valid):
+ """Test expanding the dimension to an integer target dimension."""
+ my_dom = Domain(random_bounds_valid)
+
+ if my_dom.spatial_dimension == 1:
+ pytest.skip("1D domain can always be expanded.")
+
+ with pytest.raises(ValueError):
+ _ = my_dom.expand_dim(my_dom.spatial_dimension + 1)
+
+ def test_to_target_domain_identical(self, random_bounds_valid):
+ """Test expanding the dimension to the same domain."""
+ dom = Domain(random_bounds_valid)
+
+ # Assertions
+ assert dom.expand_dim(dom) is dom # Expanding to itself is identity
+ assert dom.expand_dim(dom) == dom
+
+ def test_to_target_domain_equal(self, random_bounds_valid):
+ """Test expanding the dimension to a domain with the same bounds."""
+ my_dom_1 = Domain(random_bounds_valid)
+ my_dom_2 = Domain(random_bounds_valid)
+
+ # Assertions
+ assert my_dom_1.expand_dim(my_dom_2) is not my_dom_1
+ assert my_dom_2.expand_dim(my_dom_1) is not my_dom_2
+ assert my_dom_1.expand_dim(my_dom_2) == my_dom_1
+ assert my_dom_2.expand_dim(my_dom_1) == my_dom_2
+
+ def test_to_target_domain_expansion(self, random_bounds_valid):
+ """Test expanding the dimension to a domain with a higher dimension."""
+ bounds_1 = random_bounds_valid
+ bounds_2 = np.vstack([bounds_1, bounds_1[-1]])
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions
+ assert my_dom_1.expand_dim(my_dom_2) is not my_dom_1
+ assert my_dom_1.expand_dim(my_dom_2) is not my_dom_2
+ assert my_dom_1.expand_dim(my_dom_2) == my_dom_2
+
+ def test_to_target_domain_contraction(self, random_bounds_valid):
+ """Test expanding the dimension to a domain with a lower dimension."""
+ bounds_1 = random_bounds_valid
+ bounds_1 = np.vstack([bounds_1, bounds_1[-1]])
+ bounds_2 = random_bounds_valid
+
+
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ # Assertions
+ with pytest.raises(ValueError):
+ _ = my_dom_1.expand_dim(my_dom_2)
+
+ def test_to_target_domain_mismatch(self, random_bounds_pair):
+ """Test that an exception is raised for mismatching dimensions."""
+ bounds_1, bounds_2 = random_bounds_pair
+ my_dom_1 = Domain(bounds_1)
+ my_dom_2 = Domain(bounds_2)
+
+ with pytest.raises(DomainMismatchError):
+ _ = my_dom_1.expand_dim(my_dom_2)
+
+ def test_to_target_domain_invalid_type(self, random_bounds_valid):
+ """Test that an exception is raised for invalid target domain."""
+ my_dom = Domain(random_bounds_valid)
+
+ with pytest.raises(TypeError):
+ _ = my_dom.expand_dim([1, 2, 3])
diff --git a/tests/test_grid.py b/tests/test_grid.py
index 2131a890..a15e27c3 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -1,3 +1,4 @@
+import copy
import numpy as np
import pytest
@@ -7,8 +8,10 @@
gen_chebychev_2nd_order_leja_ordered,
gen_points_from_values,
)
+from minterpy.core.domain import Domain
from minterpy.core.grid import DEFAULT_FUN
from minterpy.utils.multi_index import get_exponent_matrix
+from minterpy.utils.exceptions import DomainMismatchError
from conftest import create_mi_pair_distinct
@@ -26,6 +29,22 @@ def _fun_multi_out(xx: np.ndarray):
return xx # xx is assumed to be multi-dimensional
+@pytest.fixture
+def normalized_domain(SpatialDimension) -> Domain:
+ """Normalized domain for testing."""
+ return Domain.identity(SpatialDimension)
+
+@pytest.fixture
+def random_unif_domain(SpatialDimension) -> Domain:
+ """Create a random uniform domain for testing"""
+ # Create a custom domain
+ lb = np.random.randint(0, 10)
+ ub = lb + np.random.randint(1, 10)
+ domain = Domain.uniform(SpatialDimension, lb, ub)
+
+ return domain
+
+
class TestInit:
"""All tests related to the default constructor of Grid."""
@@ -104,6 +123,35 @@ def _gen_fun(poly_degree, spatial_dimension):
generating_points=gen_points,
)
+ def test_with_default_domain(self, multi_index_mnp, normalized_domain):
+ """Constructing a Grid with a default domain."""
+ # Get the complete multi-index set
+ mi = multi_index_mnp
+
+ # Construct a grid with defaults
+ grd = Grid(mi)
+
+ # Assertions
+ assert grd.domain == normalized_domain
+ assert grd.domain.spatial_dimension == grd.spatial_dimension
+
+ def test_with_custom_domain(self, multi_index_mnp, random_unif_domain):
+ """Test constructing a Grid with a custom domain."""
+
+ # Create a Grid instance with the custom domain
+ grd = Grid(multi_index_mnp, domain=random_unif_domain)
+
+ # Assertions
+ assert grd.domain == random_unif_domain
+ assert grd.domain.spatial_dimension == grd.spatial_dimension
+
+ def test_with_invalid_domain(self, multi_index_mnp):
+ """Test constructing a Grid with an invalid domain."""
+
+ dim = multi_index_mnp.spatial_dimension + 1
+ with pytest.raises(ValueError):
+ _ = Grid(multi_index_mnp, domain=Domain.identity(dim))
+
class TestInitGenPoints:
"""Tests construction with generating points."""
@@ -291,6 +339,31 @@ def test_from_degree_with_gen_function_and_points(
assert grd_1 == grd_2
assert grd_2 == grd_1
+ def test_from_degree_with_domain(
+ self,
+ SpatialDimension,
+ PolyDegree,
+ LpDegree,
+ ):
+ """Test the `from_degree()` method with domain."""
+ # Create a complete multi-index set
+ mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
+ # Create a normalized domain
+ domain = Domain.identity(SpatialDimension)
+
+ # Create a grid
+ grd_1 = Grid(mi, domain=domain)
+ grd_2 = Grid.from_degree(
+ SpatialDimension,
+ PolyDegree,
+ LpDegree,
+ domain=domain,
+ )
+
+ # Assertions
+ assert grd_1 == grd_2
+ assert grd_2 == grd_1
+
def test_from_gen_function(self, multi_index_mnp):
"""Test the `from_function()` method."""
# Get the complete multi-index set
@@ -320,6 +393,22 @@ def test_from_gen_function_with_str(self, multi_index_mnp):
assert grd_1 == grd_2
assert grd_2 == grd_1
+ def test_from_gen_function_with_domain(self, multi_index_mnp):
+ """Test the `from_function()` method with domain."""
+ # Get the complete multi-index set
+ mi = multi_index_mnp
+
+ # Create a uniform domain
+ domain = Domain.uniform(mi.spatial_dimension, 0, 1)
+
+ # Create instances of Grid
+ grd_1 = Grid(mi, generating_function=DEFAULT_FUN, domain=domain)
+ grd_2 = Grid.from_function(mi, DEFAULT_FUN, domain=domain)
+
+ # Assertion
+ assert grd_1 == grd_2
+ assert grd_2 == grd_1
+
def test_from_gen_function_invalid_non_unique(self, multi_index_mnp):
"""Test the `from_function()` method."""
# Get the complete multi-index set
@@ -352,6 +441,26 @@ def test_from_gen_points(self, multi_index_mnp):
assert grd_1 == grd_2
assert grd_2 == grd_1
+ def test_from_gen_points_with_domain(self, multi_index_mnp):
+ """Test the `from_points()` method with domain."""
+ # Get the complete multi-index set
+ mi = multi_index_mnp
+
+ # Create an array of generating points
+ gen_function = GENERATING_FUNCTIONS[DEFAULT_FUN]
+ gen_points = gen_function(mi.max_exponent, mi.spatial_dimension)
+
+ # Create a uniform domain
+ domain = Domain.uniform(mi.spatial_dimension, 0, 1)
+
+ # Create instances of Grid
+ grd_1 = Grid(mi, generating_points=gen_points, domain=domain)
+ grd_2 = Grid.from_points(mi, gen_points, domain=domain)
+
+ # Assertions
+ assert grd_1 == grd_2
+ assert grd_2 == grd_1
+
def test_from_gen_points_invalid_wrong_shape(self, multi_index_mnp):
"""Test invalid call to the `from_points()` due to dimension mismatch.
"""
@@ -403,6 +512,27 @@ def test_from_value_set(self, multi_index_mnp):
assert grd_1 == grd_2
assert grd_2 == grd_1
+ def test_from_value_set_with_domain(self, multi_index_mnp):
+ """Test the `from_value_set()` method with domain."""
+ # Get the complete multi-index set
+ mi = multi_index_mnp
+
+ # Create an array of generating values (the default 1d generating
+ # function) and the corresponding generating points
+ gen_values = gen_chebychev_2nd_order_leja_ordered(mi.max_exponent)
+ gen_points = gen_points_from_values(gen_values, mi.spatial_dimension)
+
+ # Create a uniform domain
+ domain = Domain.uniform(mi.spatial_dimension, 0, 1)
+
+ # Create instances of Grid
+ grd_1 = Grid(mi, generating_points=gen_points, domain=domain)
+ grd_2 = Grid.from_value_set(mi, gen_values, domain=domain)
+
+ # Assertions
+ assert grd_1 == grd_2
+ assert grd_2 == grd_1
+
def test_from_value_set_invalid_wrong_shape(self, multi_index_mnp):
"""Test invalid call to `from_value_set()` due to wrong shape."""
# Get the complete multi-index set
@@ -433,6 +563,26 @@ def test_from_value_set_invalid_non_unique(self, multi_index_mnp):
Grid.from_value_set(mi, gen_values)
+class TestDomainProperty:
+ """All tests related to the domain property of the Grid instance."""
+
+ def test_read_only(self, multi_index_mnp):
+ """Test the property is read-only."""
+ # Create a Grid instance
+ grd = Grid(multi_index_mnp)
+
+ # Assertion
+ with pytest.raises(AttributeError):
+ grd.domain = np.arange(10)
+
+ def test_always_exist(self, multi_index_mnp):
+ """Test the property is read-only."""
+ # Create a Grid instance
+ grd = Grid(multi_index_mnp)
+
+ # Assertion
+ assert grd.domain is not None
+
class TestUnisolventNodes:
"""All tests related to the unisolvent nodes property."""
def test_unisolvent_nodes(self, SpatialDimension, PolyDegree, LpDegree):
@@ -491,6 +641,21 @@ def test_call_one_dim_output(self, multi_index_mnp):
assert len(lag_coeffs_2) == len(mi)
assert np.array_equal(lag_coeffs_1, lag_coeffs_2)
+ def test_call_custom_domain(self, multi_index_mnp, random_unif_domain):
+ """Test calling on a valid callable with custom domain."""
+ # Create a Grid
+ grd = Grid(multi_index_mnp, domain=random_unif_domain)
+
+ # Call the Grid instance
+ yy_1 = grd(_fun_one_out, sum=True)
+ yy_2 = _fun_one_out(
+ random_unif_domain.map_from_internal(grd.unisolvent_nodes),
+ sum=True,
+ )
+
+ # Assertions
+ assert np.array_equal(yy_1, yy_2)
+
@pytest.mark.parametrize("invalid_function", [1, 2.0, "3.5"])
def test_call_invalid_function(self, multi_index_mnp, invalid_function):
"""Test calling on an invalid function."""
@@ -538,6 +703,7 @@ def test_target_dim_with_gen_fun(self, multi_index_mnp):
assert grd_expanded != grd
assert grd_expanded.spatial_dimension == new_dim
assert grd_expanded.multi_index == mi.expand_dim(new_dim)
+ assert grd_expanded.domain == grd.domain.expand_dim(new_dim)
def test_target_dim_same_dim_with_gen_points(self, multi_index_mnp):
"""Test expanding to the same dimension with generating points."""
@@ -574,6 +740,7 @@ def test_target_dim_with_gen_points_valid(self, multi_index_mnp):
# Assertion
assert grd_expanded.spatial_dimension == target_dim
+ assert grd_expanded.domain == grd.domain.expand_dim(target_dim)
def test_target_dim_with_gen_points_invalid(self, multi_index_mnp):
"""Test expanding the dimension with invalid generating points."""
@@ -634,6 +801,7 @@ def test_target_grid_with_gen_points_valid(self, multi_index_mnp):
expanded_dim = expanded_grid.spatial_dimension
target_dim = target_grid.spatial_dimension
assert expanded_dim == target_dim
+ assert expanded_grid.domain == target_grid.domain
def test_target_grid_with_larger_origin_gen_points(self, multi_index_mnp):
"""Test expanding the dimension to the dimension of a target grid
@@ -668,6 +836,7 @@ def test_target_grid_with_larger_origin_gen_points(self, multi_index_mnp):
assert np.all(
expanded_grid.generating_points == origin_grid.generating_points
)
+ assert expanded_grid.domain == target_grid.domain
def test_target_grid_with_gen_points_invalid(self, multi_index_mnp):
"""Test expanding the dimension to the dimension of a target grid
@@ -717,6 +886,7 @@ def test_target_grid_with_gen_fun_valid(self, multi_index_mnp):
assert np.all(
expanded_grid.generating_points == target_grid.generating_points
)
+ assert expanded_grid.domain == target_grid.domain
def test_target_grid_with_gen_fun_invalid(self, multi_index_mnp):
"""Test expanding the dimension to the dimension of a target grid
@@ -729,7 +899,7 @@ def test_target_grid_with_gen_fun_invalid(self, multi_index_mnp):
# Get the ingredients for a Grid
origin_gen_fun = GENERATING_FUNCTIONS[DEFAULT_FUN]
- target_gen_fun = lambda x, y: origin_gen_fun(x, y)
+ target_gen_fun = lambda x, y: 0.99 * origin_gen_fun(x, y)
# Create instances of Grid
origin_grid = Grid.from_function(origin_mi, origin_gen_fun)
@@ -743,14 +913,23 @@ def test_target_grid_with_gen_fun_invalid(self, multi_index_mnp):
class TestEquality:
"""All tests related to equality check of Grid instances."""
- def test_equal(self, SpatialDimension, PolyDegree, LpDegree):
+ def test_equal_multi_index(self, multi_index_mnp):
"""Test equality of two Grid instances."""
- # Create a common multi-index set
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
+ # Create two Grid instances equal in value
+ grd_1 = Grid(multi_index_mnp)
+ grd_2 = Grid(multi_index_mnp)
+
+ # Assertions
+ assert grd_1 is not grd_2 # Not identical instances
+ assert grd_1 == grd_2 # but equal in value
+ assert grd_2 == grd_1 # symmetric property
+
+ def test_equal_domain(self, multi_index_mnp, random_unif_domain):
+ """Test equality of two Grid instances with custom but equal domain."""
# Create two Grid instances equal in value
- grd_1 = Grid(mi)
- grd_2 = Grid(mi)
+ grd_1 = Grid(multi_index_mnp, domain=random_unif_domain)
+ grd_2 = Grid(multi_index_mnp, domain=random_unif_domain)
# Assertions
assert grd_1 is not grd_2 # Not identical instances
@@ -779,8 +958,9 @@ def test_unequal_gen_points(self, SpatialDimension, PolyDegree, LpDegree):
mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
# Create two Grid instances with different generating points
- # Chebyshev points
- grd_1 = Grid(mi)
+ # Chebyshev points (the default)
+ grd_ = Grid(mi)
+ grd_1 = Grid.from_points(mi, grd_.generating_points)
# Equidistant points
grd_2 = Grid.from_value_set(
mi,
@@ -792,6 +972,23 @@ def test_unequal_gen_points(self, SpatialDimension, PolyDegree, LpDegree):
assert grd_1 != grd_2 # Not equal in values
assert grd_2 != grd_1 # symmetric property
+ def test_unequal_domain(self, multi_index_mnp):
+ """Test inequality of two Grid instances due to different domains.
+ """
+ # Create two different domains
+ dim = multi_index_mnp.spatial_dimension
+ domain_1 = Domain.uniform(dim, 0, 1)
+ domain_2 = Domain.identity(dim)
+
+ # Create two Grid instances
+ grd_1 = Grid(multi_index_mnp, domain=domain_1)
+ grd_2 = Grid(multi_index_mnp, domain=domain_2)
+
+ # Assertions
+ assert grd_1 is not grd_2 # Not identical instances
+ assert grd_1 != grd_2 # Not equal in values
+ assert grd_2 != grd_1 # symmetric property
+
def test_inequality_inconsistent_type(self):
"""Test inequality check with inconsistent types."""
# Create a multi-index set
@@ -824,6 +1021,7 @@ def test_square(self, SpatialDimension, PolyDegree, LpDegree):
# Assertions
assert grd_prod.multi_index == mi * mi
assert len(grd_prod.unisolvent_nodes) == len(mi * mi)
+ assert grd_prod.domain == grd.domain | grd.domain
def test_with_gen_points(self):
"""Test the multiplication of instances having only gen. points."""
@@ -848,6 +1046,7 @@ def test_with_gen_points(self):
assert grd_prod.multi_index == mi_1 * mi_2
assert len(grd_prod.unisolvent_nodes) == len(mi_1 * mi_2)
assert np.all(grd_prod.generating_points == gen_points_2)
+ assert grd_prod.domain == grd_1.domain | grd_2.domain
@pytest.mark.parametrize("invalid_value", [1.0, 2, "123", np.array([1])])
def test_invalid(self, multi_index_mnp, invalid_value):
@@ -862,6 +1061,20 @@ def test_invalid(self, multi_index_mnp, invalid_value):
with pytest.raises(AttributeError):
grd * invalid_value
+ def test_invalid_domain(self, multi_index_mnp):
+ """Test taking the product of two instances with different domains."""
+ # Create two different domains
+ dim = multi_index_mnp.spatial_dimension
+ domain_1 = Domain.uniform(dim, 0, 1)
+ domain_2 = Domain.identity(dim)
+
+ # Create two Grid instances
+ grd_1 = Grid(multi_index_mnp, domain=domain_1)
+ grd_2 = Grid(multi_index_mnp, domain=domain_2)
+
+ # Assertion
+ with pytest.raises(DomainMismatchError):
+ _ = grd_1 * grd_2
class TestUnion:
"""All tests related to taking the union of `Grid` instances."""
@@ -903,6 +1116,7 @@ def test_with_gen_points(self):
assert grd_prod.multi_index == mi_1 | mi_2
assert len(grd_prod.unisolvent_nodes) == len(mi_1 | mi_2)
assert np.all(grd_prod.generating_points == gen_points_2)
+ assert grd_prod.domain == grd_1.domain | grd_2.domain
@pytest.mark.parametrize("invalid_value", [1.0, 2, "123", np.array([1])])
def test_invalid(self, multi_index_mnp, invalid_value):
@@ -915,8 +1129,22 @@ def test_invalid(self, multi_index_mnp, invalid_value):
# Assertion
with pytest.raises(AttributeError):
- grd | invalid_value
+ _ = grd | invalid_value
+
+ def test_invalid_domain(self, multi_index_mnp):
+ """Test taking the union of two instances with different domains."""
+ # Create two different domains
+ dim = multi_index_mnp.spatial_dimension
+ domain_1 = Domain.uniform(dim, 0, 1)
+ domain_2 = Domain.identity(dim)
+
+ # Create two Grid instances
+ grd_1 = Grid(multi_index_mnp, domain=domain_1)
+ grd_2 = Grid(multi_index_mnp, domain=domain_2)
+ # Assertion
+ with pytest.raises(DomainMismatchError):
+ _ = grd_1 | grd_2
class TestAddExponents:
"""All tests related to the method to add a set of exponents."""
@@ -934,6 +1162,7 @@ def test_identical(self, multi_index_mnp):
# Assertion
assert grd == grd_added
assert grd_added == grd
+ assert grd.domain == grd_added.domain # Domain is preserved
def test_too_large_exponent(self, multi_index_mnp):
"""Test adding an exponent that cannot be supported by the grid."""
@@ -983,6 +1212,7 @@ def test_set_diff(self, multi_index_mnp):
# Assertion
assert grd_1_added == grd_2
assert grd_2 == grd_1_added
+ assert grd_2.domain == grd_1_added.domain # Domain is preserved
class TestMakeComplete:
@@ -1003,6 +1233,7 @@ def test_already_complete(self, multi_index_mnp):
assert grd_complete == grd
assert grd == grd_complete
assert grd is not grd_complete
+ assert grd.domain == grd_complete.domain # Domain is preserved
def test_incomplete(self, multi_index_incomplete):
"""Test making an incomplete grid complete."""
@@ -1018,6 +1249,7 @@ def test_incomplete(self, multi_index_incomplete):
# Assertions
assert not grd.is_complete
assert grd_complete.is_complete
+ assert grd.domain == grd_complete.domain # Domain is preserved
class TestMakeDownwardClosed:
@@ -1038,6 +1270,7 @@ def test_already_downward_closed(self, multi_index_mnp):
assert grd_downward_closed == grd
assert grd == grd_downward_closed
assert grd is not grd_downward_closed
+ assert grd.domain == grd_downward_closed.domain # Domain is preserved
def test_non_downward_closed(self, multi_index_non_downward_closed):
"""Test making a non-downward-closed grid downward-closed."""
@@ -1053,6 +1286,7 @@ def test_non_downward_closed(self, multi_index_non_downward_closed):
# Assertions
assert not grd.is_downward_closed
assert grd_downward_closed.is_downward_closed
+ assert grd.domain == grd_downward_closed.domain # Domain is preserved
class TestIsCompatible:
@@ -1120,7 +1354,7 @@ def _custom_gen_function(poly_degree, spatial_dimension):
if xx.ndim == 1:
xx = xx[:, np.newaxis]
generating_points = np.tile(xx, (1, spatial_dimension))
- generating_points[:, ::2] *= -1
+ generating_points[:, ::2] *= -0.9
return generating_points
@@ -1131,3 +1365,33 @@ def _custom_gen_function(poly_degree, spatial_dimension):
# Assertion
assert not grd_1.is_compatible(grd_2)
assert not grd_2.is_compatible(grd_1) # Commutativity must hold
+
+
+class TestCopy:
+ """All tests related to copy behavior of Grid instances."""
+
+ def test_shallow(self, multi_index_mnp, random_unif_domain):
+ """Test the behavior of shallow copy."""
+ grd = Grid(multi_index_mnp, domain=random_unif_domain)
+
+ grd_copy = copy.copy(grd)
+
+ # Assertions
+ assert grd is not grd_copy
+ assert grd.multi_index is grd_copy.multi_index
+ assert grd.domain is grd_copy.domain
+ assert grd.generating_function is grd_copy.generating_function
+ assert grd.generating_points is not grd_copy.generating_points
+
+ def test_deep(self, multi_index_mnp, random_unif_domain):
+ """Test the behavior of deep copy."""
+ grd = Grid(multi_index_mnp, domain=random_unif_domain)
+
+ grd_copy = copy.deepcopy(grd)
+
+ # Assertions
+ assert grd is not grd_copy
+ assert grd.multi_index is not grd_copy.multi_index
+ assert grd.domain is not grd_copy.domain
+ assert grd.generating_function is grd_copy.generating_function
+ assert grd.generating_points is not grd_copy.generating_points
diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py
index 35a71a7f..32eb46c0 100644
--- a/tests/test_interpolation.py
+++ b/tests/test_interpolation.py
@@ -6,173 +6,259 @@
import numpy as np
import pytest
-from conftest import (
- LpDegree,
- NrPoints,
- PolyDegree,
- SpatialDimension,
- assert_call,
- assert_grid_equal,
- assert_interpolant_almost_equal,
- assert_multi_index_equal,
- assert_polynomial_almost_equal,
- build_random_newton_polynom,
- build_rnd_points,
-)
-from numpy.testing import assert_, assert_almost_equal
+
+from conftest import build_random_newton_polynom
import minterpy as mp
-from minterpy import Interpolant, Interpolator, interpolate
-
-# test construction
-
-
-def test_init_interpolator(SpatialDimension, PolyDegree, LpDegree):
- assert_call(Interpolator, SpatialDimension, PolyDegree, LpDegree)
- interpolator = Interpolator(SpatialDimension, PolyDegree, LpDegree)
- groundtruth_multi_index = mp.MultiIndexSet.from_degree(
- SpatialDimension, PolyDegree, LpDegree
- )
- groundtruth_grid = mp.Grid(groundtruth_multi_index)
- assert_multi_index_equal(interpolator.multi_index, groundtruth_multi_index)
- assert_grid_equal(interpolator.grid, groundtruth_grid)
-
-
-def test_init_interpolant(SpatialDimension, PolyDegree, LpDegree):
- assert_call(
- Interpolant,
- lambda x: x[:, 0],
- Interpolator(SpatialDimension, PolyDegree, LpDegree),
- )
- assert_call(
- Interpolant.from_degree,
- lambda x: x[:, 0],
- SpatialDimension,
- PolyDegree,
- LpDegree,
- )
- interpolant_default = Interpolant(
- lambda x: x[:, 0], Interpolator(SpatialDimension, PolyDegree, LpDegree)
- )
- interpolant_from_degree = Interpolant.from_degree(
- lambda x: x[:, 0], SpatialDimension, PolyDegree, LpDegree
- )
- assert_interpolant_almost_equal(interpolant_default, interpolant_from_degree)
-
-
-def test_call_interpolate(SpatialDimension, PolyDegree, LpDegree):
- assert_call(interpolate, lambda x: x[:, 0], SpatialDimension, PolyDegree, LpDegree)
- interpolant = interpolate(lambda x: x[:, 0], SpatialDimension, PolyDegree, LpDegree)
- assert_(isinstance(interpolant, Interpolant))
-
-
-# test if the interpolator can interpolate
-def test_interpolator(SpatialDimension, PolyDegree, LpDegree):
- groundtruth_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
- interpolator = Interpolator(SpatialDimension, PolyDegree, LpDegree)
- res_from_newton_poly = interpolator(groundtruth_poly)
- res_from_canonical_poly = interpolator(mp.NewtonToCanonical(groundtruth_poly)())
- assert_polynomial_almost_equal(res_from_newton_poly, groundtruth_poly)
- assert_polynomial_almost_equal(res_from_canonical_poly, groundtruth_poly)
-
-
-# test if the interpolant interpolates
-def test_interpolant(NrPoints, SpatialDimension, PolyDegree, LpDegree):
- rnd_points = build_rnd_points(NrPoints, SpatialDimension)
- groundtruth_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
- groundtruth = groundtruth_poly(rnd_points)
- interpolant = Interpolant.from_degree(
- groundtruth_poly, SpatialDimension, PolyDegree, LpDegree
- )
- res = interpolant(rnd_points)
- assert_almost_equal(res, groundtruth)
-
-
-# test if the interpolate does what it promisses
-def test_interpolate(NrPoints, SpatialDimension, PolyDegree, LpDegree):
- rnd_points = build_rnd_points(NrPoints, SpatialDimension)
- groundtruth_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
- groundtruth = groundtruth_poly(rnd_points)
- interpolant = interpolate(groundtruth_poly, SpatialDimension, PolyDegree, LpDegree)
- res = interpolant(rnd_points)
- assert_almost_equal(res, groundtruth)
-
-
-def _fun(xx: np.ndarray) -> np.ndarray:
- """Dummy function for testing interpolant."""
- return np.sum(xx, axis=1)
+from minterpy import (
+ Interpolant,
+ Interpolator,
+ interpolate,
+ Domain,
+ Grid,
+ NewtonPolynomial,
+ LagrangePolynomial,
+ ChebyshevPolynomial,
+ CanonicalPolynomial,
+)
+#######################
+# Internal functions #
+#######################
-class TestPoly:
- """All tests related to the accessing the polynomial of an interpolant."""
+def func(xx):
+ """Dummy function for testing interpolant."""
+ return np.repeat(np.sum(xx, axis=1)[:, np.newaxis], repeats=5, axis=1)
+
+##########
+# Tests #
+##########
+
+class TestInterpolator:
+ """All tests related to the Interpolator class."""
+
+ def test_init(self, multi_index_mnp, domain):
+ """Test the initialization of an Interpolator instance."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+ grd = mp.Grid(mi, domain=domain)
+
+ # Create an instance of Interpolator
+ interpolator = Interpolator(m, n, p, bounds)
+
+ # Assertions
+ assert interpolator.multi_index == mi
+ assert interpolator.grid == grd
+ assert interpolator.domain == domain
+ assert interpolator.spatial_dimension == m
+ assert interpolator.poly_degree == n
+ assert interpolator.lp_degree == p
+
+ def test_init_default_bound(self, multi_index_mnp):
+ """Test the initialization of an instance with default bounds."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+
+ # Create an instance of Interpolator
+ interpolator = Interpolator(m, n, p)
+
+ # Create a default Domain
+ domain = Domain.identity(m)
+
+ # Assertions
+ assert interpolator.domain == domain
+
+ def test_call(self, multi_index_mnp, domain):
+ """Test the evaluation of an interpolator instance."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+ grd = mp.Grid(mi, domain=domain)
+
+ # Create an instance of Interpolator
+ interpolator = Interpolator(m, n, p, bounds)
+
+ # Interpolate the function
+ interpol_1 = interpolator(func)
+
+ # Create a reference interpolant
+ interpol_2 = NewtonPolynomial(mi, interpol_1.coeffs, grid=grd)
+
+ # Assertion
+ assert interpol_1 == interpol_2
+
+ def test_identity(self, multi_index_mnp, domain):
+ """Test interpolating a Newton polynomial in the Newton basis."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
+ # Create a groundtruth polynomial
+ groundtruth_poly = build_random_newton_polynom(m, n, p, domain)
+
+ # Create an interpolator and interpolate the groundtruth polynomial
+ interpolator = Interpolator(m, n, p, bounds)
+ interpolant_poly = interpolator(groundtruth_poly)
+
+ # Assertions
+ assert isinstance(interpolant_poly, type(groundtruth_poly))
+ assert interpolant_poly.multi_index == groundtruth_poly.multi_index
+ assert interpolant_poly.grid == groundtruth_poly.grid
+ assert np.allclose(interpolant_poly.coeffs, groundtruth_poly.coeffs)
+
+
+class TestInterpolant:
+ """All tests related to the Interpolant class."""
+
+ def test_init(self, multi_index_mnp, domain):
+ """Test default construction of an Interpolant instance."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
+ # Create interpolant instances
+ interpolant = Interpolant.from_degree(func, m, n, p, bounds)
+
+ # Assertions
+ assert interpolant.spatial_dimension == m
+ assert interpolant.poly_degree == n
+ assert interpolant.lp_degree == p
+ assert interpolant.multi_index == mi
+
+ def test_poly(self, multi_index_mnp, domain):
+ """Test construction of the underlying interpolating polynomial."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
+ # Create an interpolator
+ interpolator = Interpolator(m, n, p, bounds)
+
+ # Create interpolant instances
+ interpolant_1 = Interpolant(func, interpolator)
+ interpolant_2 = Interpolant.from_degree(func, m, n, p, bounds)
+
+ # Assertions
+ assert interpolant_1.to_newton() == interpolator(func)
+ assert interpolant_2.to_newton() == interpolator(func)
+
+ def test_call(self, multi_index_mnp, domain):
+ """Test calling an interpolant instance."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
+ # Create a reference polynomial
+ reference_poly = build_random_newton_polynom(m, n, p, domain)
+
+ # Interpolate the groundtruth polynomial
+ interpolant = Interpolant.from_degree(reference_poly, m, n, p, bounds)
+
+ # Create a set of random test points
+ lb, ub = domain.lowers, domain.uppers
+ xx_test = lb + (ub - lb) * np.random.rand(100, m)
+
+ # Assertion
+ assert np.allclose(interpolant(xx_test), reference_poly(xx_test))
+
+ def test_to_newton(self, multi_index_mnp, domain):
+ """Test getting the interpolating polynomial in the Newton basis."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
- def test_to_newton(self, SpatialDimension, PolyDegree, LpDegree):
- """Test obtaining the interpolating polynomial in the Newton basis."""
# Interpolate a function
- interpol = interpolate(_fun, SpatialDimension, PolyDegree, LpDegree)
- poly_1 = interpol.to_newton()
+ interpolant = interpolate(func, m, n, p, bounds)
+ poly_1 = interpolant.to_newton()
- # Create a reference
- grd = mp.Grid.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = grd(_fun)
- # 'interpolate()' use DDS (don't use LagrangeToNewton in the test
- # as the results won't be identical, very close to, but not identical)
- nwt_coeffs = mp.dds.dds(lag_coeffs, grd.tree)
- poly_2 = mp.NewtonPolynomial.from_grid(grd, nwt_coeffs)
+ # Create a reference polynomial
+ poly_2 = NewtonPolynomial(mi, poly_1.coeffs, domain=domain)
+ # Assertion
assert poly_1 == poly_2
- def test_to_lagrange(self, SpatialDimension, PolyDegree, LpDegree):
- """Test obtaining the interpolating polynomial in the Newton basis."""
+ def test_to_lagrange(self, multi_index_mnp, domain):
+ """Test getting the interpolating polynomial in the Lagrange basis."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
# Interpolate a function
- interpol = interpolate(_fun, SpatialDimension, PolyDegree, LpDegree)
- poly_1 = interpol.to_lagrange()
+ interpolant = interpolate(func, m, n, p, bounds)
+ poly_1 = interpolant.to_lagrange()
- # Create a reference
- grd = mp.Grid.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = grd(_fun)
- poly_2 = mp.LagrangePolynomial.from_grid(grd, lag_coeffs)
+ # Create a reference polynomial
+ grd = Grid(mi, domain=domain)
+ coeffs = grd(func)
+ poly_2 = LagrangePolynomial(mi, coeffs, grid=grd)
+ # Assertion
assert poly_1 == poly_2
- def test_to_canonical(self, SpatialDimension, PolyDegree, LpDegree):
- """Test obtaining the interpolating polynomial in the Newton basis."""
+ def test_to_canonical(self, multi_index_mnp, domain):
+ """Test getting the interpolating polynomial in the canonical basis."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
# Interpolate a function
- interpol = interpolate(_fun, SpatialDimension, PolyDegree, LpDegree)
- poly_1 = interpol.to_canonical()
-
- # Create a reference
- grd = mp.Grid.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = grd(_fun)
- lag_coeffs = grd(_fun)
- # 'interpolate()' use DDS (don't use LagrangeToNewton in the test
- # as the results won't be identical, very close to, but not identical)
- nwt_coeffs = mp.dds.dds(lag_coeffs, grd.tree)
- nwt_poly = mp.NewtonPolynomial.from_grid(grd, nwt_coeffs)
- poly_2 = mp.NewtonToCanonical(nwt_poly)()
+ interpolant = interpolate(func, m, n, p, bounds)
+ poly_1 = interpolant.to_canonical()
+ # Create a reference polynomial
+ coeffs = poly_1.coeffs
+ poly_2 = CanonicalPolynomial(mi, coeffs, domain=domain)
+
+ # Assertion
assert poly_1 == poly_2
- def test_to_chebyshev(self, SpatialDimension, PolyDegree, LpDegree):
- """Test obtaining the interpolating polynomial in the Newton basis."""
+ def test_to_chebyshev(self, multi_index_mnp, domain):
+ """Test getting the interpolating polynomial in the Chebyshev basis."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
# Interpolate a function
- interpol = interpolate(_fun, SpatialDimension, PolyDegree, LpDegree)
- poly_1 = interpol.to_chebyshev()
-
- # Create a reference
- grd = mp.Grid.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = grd(_fun)
- lag_coeffs = grd(_fun)
- # 'interpolate()' use DDS (don't use LagrangeToNewton in the test
- # as the results won't be identical, very close to, but not identical)
- nwt_coeffs = mp.dds.dds(lag_coeffs, grd.tree)
- nwt_poly = mp.NewtonPolynomial.from_grid(grd, nwt_coeffs)
- poly_2 = mp.NewtonToChebyshev(nwt_poly)()
+ interpolant = interpolate(func, m, n, p, bounds)
+ poly_1 = interpolant.to_chebyshev()
+ # Create a reference polynomial
+ coeffs = poly_1.coeffs
+ poly_2 = ChebyshevPolynomial(mi, coeffs, domain=domain)
+
+ # Assertion
assert poly_1 == poly_2
+
+class TestInterpolate:
+ """All tests related to the interpolate function."""
+
+ def test_call(self, multi_index_mnp, domain):
+ """Test calling the function."""
+ # Fetch the relevant parameters for construction
+ mi = multi_index_mnp
+ m, n, p = mi.spatial_dimension, mi.poly_degree, mi.lp_degree
+ bounds = domain.bounds
+
+ # Create an interpolant instance
+ interpolant_1 = Interpolant.from_degree(func, m, n, p, bounds)
+ interpolant_2 = interpolate(func, m, n, p, bounds)
+
+ # Create a set of random test points
+ lb, ub = domain.lowers, domain.uppers
+ xx_test = lb + (ub - lb) * np.random.rand(100, m)
+
+ # Assertion (must be identical)
+ assert isinstance(interpolant_2, Interpolant)
+ assert np.allclose(interpolant_1(xx_test), interpolant_2(xx_test))
diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py
index add2dcd6..ec0cc534 100644
--- a/tests/test_polynomial.py
+++ b/tests/test_polynomial.py
@@ -20,10 +20,13 @@
POLY_CLASSES,
)
from minterpy import (
+ Domain,
Grid,
LagrangePolynomial,
MultiIndexSet,
)
+from minterpy.global_settings import DEFAULT_DOMAIN
+from minterpy.utils.exceptions import DomainMismatchError
class TestInitialization:
@@ -61,7 +64,7 @@ def test_with_grid(
assert_call(poly_class_all, multi_index_mnp, grid=grd)
assert_call(poly_class_all, multi_index_mnp, coeffs, grid=grd)
- def test_with_invalid_grid(
+ def test_with_grid_invalid_smaller(
self,
poly_class_all,
SpatialDimension,
@@ -86,6 +89,38 @@ def test_with_invalid_grid(
with pytest.raises(ValueError):
poly_class_all(mi, coeffs, grid=grd)
+ @pytest.mark.parametrize("invalid_type", ["a", 1, [1, 2, 3]])
+ def test_with_grid_invalid_type(
+ self,
+ poly_class_all,
+ multi_index_mnp,
+ invalid_type,
+ ):
+ """Test initialization with an invalid grid type."""
+ with pytest.raises(TypeError):
+ _ = poly_class_all(multi_index_mnp, grid=invalid_type)
+
+ def test_with_domain_valid(self, poly_class_all, multi_index_mnp):
+ """Test initialization with a domain."""
+ # Create a non-default domain
+ dim = multi_index_mnp.spatial_dimension
+ domain = Domain.uniform(dim, 0, 10)
+ poly = poly_class_all(multi_index_mnp, domain=domain)
+
+ # Assertions
+ assert poly.domain == domain
+
+ def test_with_domain_invalid(self, poly_class_all, multi_index_mnp):
+ """Test initialization with a non-matching domain."""
+ # Create a non-default domain
+ dim = multi_index_mnp.spatial_dimension
+ domain = Domain.uniform(dim, 0, 10)
+
+ # Create a polynomial instance with the default grid
+ grid = Grid(multi_index_mnp)
+ with pytest.raises(DomainMismatchError):
+ _ = poly_class_all(multi_index_mnp, grid=grid, domain=domain)
+
@pytest.mark.parametrize("spatial_dimension", [0, 1, 5])
def test_empty_set(self, spatial_dimension, poly_class_all, LpDegree):
"""Test initialization with an empty multi-index set."""
@@ -99,6 +134,18 @@ def test_empty_set(self, spatial_dimension, poly_class_all, LpDegree):
class TestFrom:
"""All tests related to the different factory methods."""
+ def test_from_degree(self, poly_class_all, multi_index_mnp):
+ """Test creating an instance from the parameters of a complete set.
+ """
+ # Create a polynomial instance
+ m = multi_index_mnp.spatial_dimension
+ n = multi_index_mnp.poly_degree
+ p = multi_index_mnp.lp_degree
+ poly = poly_class_all.from_degree(m, n, p)
+
+ # Assertions
+ assert poly.multi_index == multi_index_mnp
+
def test_from_grid_uninit(self, poly_class_all, grid_mnp):
"""Test creating an uninitialized polynomial from a Grid instance."""
# Create a polynomial instance
@@ -261,98 +308,20 @@ def test_target_dim_higher_dim(self, rand_poly_mnp_all):
assert poly_2.multi_index == poly_1.multi_index.expand_dim(new_dim)
assert poly_2.grid == poly_1.grid.expand_dim(new_dim)
- def test_target_dim_new_domains(self, rand_poly_mnp_all):
- """Test dimension expansion of a polynomial with specified domains."""
- # Get the random polynomial
- poly_1 = rand_poly_mnp_all
-
- # Get the current and the new dimension
- dim = poly_1.spatial_dimension
- new_dim = dim + 2
-
- # Define valid additional domains
- new_domains = np.array([[-2, -2], [2, 2]])
-
- # Expand the dimension
- poly_2 = poly_1.expand_dim(
- new_dim,
- extra_internal_domain=new_domains,
- extra_user_domain=new_domains,
- )
-
- # Assertions
- assert poly_1 != poly_2
- assert poly_2.spatial_dimension == new_dim
- assert poly_2.multi_index == poly_1.multi_index.expand_dim(new_dim)
- assert poly_2.grid == poly_1.grid.expand_dim(new_dim)
- assert np.array_equal(poly_2.user_domain[:, dim:], new_domains)
- assert np.array_equal(poly_2.internal_domain[:, dim:], new_domains)
-
- def test_target_dim_non_uniform_domain(self, poly_mnp_non_unif_domain):
+ def test_expand_dim_non_unif_domain(self, poly_class_all, multi_index_mnp):
"""Test dimension expansion in which the domain cannot be extrapolated.
"""
- origin_dim = poly_mnp_non_unif_domain.spatial_dimension
- target_dim = origin_dim + 1
-
- # Expansion of polynomials w/ a non-uniform domain raises an exception
- with pytest.raises(ValueError):
- poly_mnp_non_unif_domain.expand_dim(target_dim)
-
- def test_target_poly_same_dim(self, rand_poly_mnp_all):
- """Test dimension expansion of a polynomial to the dimension of
- another polynomial having the same dimension.
- """
- # Get the random polynomial
- poly_1 = rand_poly_mnp_all
-
- # Expand the dimension
- poly_2 = poly_1.expand_dim(poly_1)
-
- # Assertions
- assert poly_1 == poly_2
- assert poly_2 == poly_1
-
- def test_target_poly_higher_dim(self, poly_mnp_pair_diff_dim):
- """Test dimension expansion of a polynomial to the dimension of another
- polynomial having a higher dimension.
- """
- # Get the polynomial instances
- poly_1, poly_2 = poly_mnp_pair_diff_dim
- # The first polynomial must have smaller dimension
- if poly_1.spatial_dimension > poly_2.spatial_dimension:
- poly_1, poly_2 = poly_2, poly_1
+ dim = multi_index_mnp.spatial_dimension
+ lb = np.random.choice(np.arange(0, 5), size=dim, replace=False)
+ ub = np.random.choice(np.arange(5, 10), size=dim, replace=False)
+ domain = Domain(np.c_[lb, ub])
+ poly = poly_class_all(multi_index_mnp, domain=domain)
- # Expand the dimension
- poly_1_exp = poly_1.expand_dim(poly_2)
+ if domain.spatial_dimension == 1:
+ pytest.skip("Dimension 1 can always be expanded.")
- # Assertions
- assert poly_1_exp.has_matching_dimension(poly_2)
- assert poly_1_exp.has_matching_domain(poly_2)
-
- def test_target_poly_contraction(self, poly_mnp_pair_diff_dim):
- """Test dimension expansion of a polynomial to the dimension of another
- polynomial having a smaller dimension; this should raise an exception.
- """
- # Get the polynomial instances
- poly_1, poly_2 = poly_mnp_pair_diff_dim
- # The first polynomial must have larger dimension
- if poly_1.spatial_dimension < poly_2.spatial_dimension:
- poly_1, poly_2 = poly_2, poly_1
-
- # Expand (contract) the dimension
with pytest.raises(ValueError):
- poly_1.expand_dim(poly_2)
-
- def test_target_poly_incompatible_domain(self, poly_mnp_pair_diff_domain):
- """Test dimension expansion of a polynomial to the dimension of another
- polynomial with incompatible internal domain.
- """
- # Get the polynomial instances
- poly_1, poly_2 = poly_mnp_pair_diff_domain
-
- # Expanding the dimension to a polynomial with incompatible domain
- with pytest.raises(ValueError):
- poly_1.expand_dim(poly_2)
+ poly.expand_dim(dim + 1)
class TestEquality:
@@ -491,6 +460,27 @@ def test_different_poly(self, SpatialDimension, PolyDegree, LpDegree):
assert poly_1 != poly_2
assert poly_2 != poly_1
+ def test_different_domain(self, poly_class_all, multi_index_mnp):
+ """Test inequality due to different domain."""
+ # Generate a set of random coefficients
+ coeffs = np.random.rand(len(multi_index_mnp))
+
+ # Create two domains
+ dom_1 = Domain.uniform(multi_index_mnp.spatial_dimension, 0, 1)
+ dom_2 = Domain.identity(multi_index_mnp.spatial_dimension)
+
+ # Create two polynomials
+ poly_1 = poly_class_all(multi_index_mnp, coeffs, domain=dom_1)
+ poly_2 = poly_class_all(multi_index_mnp, coeffs, domain=dom_2)
+
+ # Assertions
+ assert poly_1 != poly_2
+ assert poly_2 != poly_1
+ assert poly_1.multi_index == poly_2.multi_index
+ assert poly_2.multi_index == poly_1.multi_index
+ assert poly_1.domain != poly_2.domain
+ assert poly_2.domain != poly_1.domain
+
class TestEvaluation:
"""All tests related to the evaluation of a polynomial instance."""
@@ -557,6 +547,42 @@ def test_multiple_coeffs(
# Due to identical coefficients, results are identical
assert np.allclose(yy_test[:, i], yy_test[:, 0])
+ def test_different_domains(
+ self,
+ poly_class_no_lag,
+ SpatialDimension,
+ PolyDegree,
+ LpDegree,
+ num_polynomials,
+ ):
+ """Test that domain affects input transformation, not internal poly."""
+ m, n, p = SpatialDimension, PolyDegree, LpDegree
+ mi = MultiIndexSet.from_degree(m, n, p)
+ coeffs = np.arange(len(mi), dtype=float)
+ coeffs = np.repeat(coeffs[:, None], num_polynomials, axis=1)
+
+ # Create a polynomial instance with the default internal domain
+ poly_1 = poly_class_no_lag(mi, coeffs)
+
+ # Create the same polynomial with custom domain
+ lb = np.random.uniform(0, 5, size=m)
+ ub = lb + np.random.uniform(5, 10, size=m) # Ensure ub > lb
+ domain = Domain(np.c_[lb, ub])
+ poly_2 = poly_class_no_lag(mi, coeffs, domain=domain)
+
+ # Test points in the internal domain
+ lb, ub = DEFAULT_DOMAIN
+ xx_test_1 = lb + (ub - lb) * np.random.rand(100, m)
+
+ # Corresponding points in the custom domain
+ xx_test_2 = poly_2.domain.map_from_internal(xx_test_1)
+
+ # Assertion
+ yy_1 = poly_1(xx_test_1)
+ yy_2 = poly_2(xx_test_2)
+
+ assert np.allclose(yy_1, yy_2)
+
class TestNegation:
"""All tests related to the negation of a polynomial instance."""
@@ -625,83 +651,6 @@ def test_pos_polys(self, rand_poly_mnp_all):
assert poly == (+poly) # Equality in value
-class TestHasMatchingDomain:
- """All tests related to method to check if polynomial domains match."""
- def test_sanity(self, rand_poly_mnp_all):
- """Test if a polynomial has a matching domain with itself."""
- # Get a random polynomial instance
- poly = rand_poly_mnp_all
-
- # Assertion
- assert poly.has_matching_domain(poly)
-
- def test_same_dim(self, rand_poly_mnp_all):
- """Test if poly. has a matching domain with another of the same dim."""
- # Get a random polynomial instance
- poly_1 = rand_poly_mnp_all
- poly_2 = copy.copy(rand_poly_mnp_all)
-
- # Assertions
- assert poly_1.has_matching_domain(poly_2)
- assert poly_2.has_matching_domain(poly_1)
-
- def test_diff_dim(self, rand_poly_mnp_all_pair):
- """Test if two default polynomials have a matching domain.
-
- Notes
- -----
- - All currently supported Minterpy polynomials have the same default
- domain. This may change in the future.
- """
- # Get the two random polynomials
- poly_1, poly_2 = rand_poly_mnp_all_pair
-
- # Assertion
- assert poly_1.has_matching_domain(poly_2)
- assert poly_2.has_matching_domain(poly_1)
-
- def test_user_domain(self, poly_class_all, multi_index_mnp):
- """Test the case when two polynomials have different user domains."""
- # Get the complete multi-index set
- mi = multi_index_mnp
-
- # Create a polynomial instance
- m = mi.spatial_dimension
- user_domain_1 = np.ones((2, m))
- user_domain_1[0, :] *= -2
- user_domain_1[1, :] *= 2
- poly_1 = poly_class_all(mi, user_domain=user_domain_1)
- user_domain_2 = np.ones((2, m))
- user_domain_2[0, :] *= -0.5
- user_domain_2[1, :] *= 0.5
- poly_2 = poly_class_all(mi, user_domain=user_domain_2)
-
- # Assertion
- assert not poly_1.has_matching_domain(poly_2)
- assert not poly_2.has_matching_domain(poly_1) # Must be symmetric
-
- def test_internal_domain(self, poly_class_all, multi_index_mnp):
- """Test the case when two polynomials have a different internal domain.
- """
- # Get the complete multi-index set
- mi = multi_index_mnp
-
- # Create a polynomial instance
- m = mi.spatial_dimension
- internal_domain_1 = np.ones((2, m))
- internal_domain_1[0, :] *= -2
- internal_domain_1[1, :] *= 2
- poly_1 = poly_class_all(mi, internal_domain=internal_domain_1)
- internal_domain_2 = np.ones((2, m))
- internal_domain_2[0, :] *= -0.5
- internal_domain_2[1, :] *= 0.5
- poly_2 = poly_class_all(mi, internal_domain=internal_domain_2)
-
- # Assertion
- assert not poly_1.has_matching_domain(poly_2)
- assert not poly_2.has_matching_domain(poly_1) # Must be symmetric
-
-
class TestMultiplicationScalar:
"""All tests related to the multiplication of a polynomial with scalars."""
def test_mul_identity(self, rand_poly_mnp_all):
@@ -817,8 +766,7 @@ def test_inconsistent_num_polys(
print(poly_1 * poly_2)
def test_non_matching_domain(self, poly_class_all, multi_index_mnp):
- """Multiplication of polynomials with a non-matching domain raises
- and exception.
+ """Test that poly multiplication raises error for non-matching domains.
"""
# Get the complete multi-index set
mi = multi_index_mnp
@@ -826,19 +774,15 @@ def test_non_matching_domain(self, poly_class_all, multi_index_mnp):
# Create a random set of coefficients
coeffs = np.random.rand(len(mi))
- # Create a polynomial instance
- domain_1 = np.ones((2, mi.spatial_dimension))
- domain_1[0, :] *= -2
- domain_1[1, :] *= 2
- poly_1 = poly_class_all(mi, coeffs, user_domain=domain_1)
- domain_2 = np.ones((2, mi.spatial_dimension))
- domain_2[0, :] *= -0.5
- domain_2[1, :] *= 0.5
- poly_2 = poly_class_all(mi, coeffs, user_domain=domain_2)
+ # Create polynomial instances with different domains
+ dom_1 = Domain.uniform(mi.spatial_dimension, lower=0, upper=1)
+ poly_1 = poly_class_all(mi, coeffs, domain=dom_1)
+ dom_2 = Domain.uniform(mi.spatial_dimension, lower=0, upper=0.5)
+ poly_2 = poly_class_all(mi, coeffs, domain=dom_2)
# Perform multiplication
- with pytest.raises(ValueError):
- print(poly_1 * poly_2)
+ with pytest.raises(DomainMismatchError):
+ _ = poly_1 * poly_2
@pytest.mark.parametrize(
"invalid_value",
@@ -899,7 +843,7 @@ def test_scalar_poly_same_dim(self, rand_poly_mnp_no_lag):
assert poly_prod_2 == poly * scalar
def test_scalar_poly_diff_dim(self, rand_poly_mnp_no_lag):
- """Test multiplication with a scalar polynomial of the higher dim.
+ r"""Test multiplication with a scalar polynomial of the higher dim.
Multiplication with a constant scalar polynomial of a higher dimension
should produce a polynomial multiplied by the scalar value with
@@ -941,47 +885,76 @@ def test_scalar_poly_separate_indices(
):
"""Test multiplication with a scalar polynomial with separate indices.
- Some scalar polynomial with separate indices are defined such that
- the grid remains compatible with the product grid.
- For instance, the first two Leja-ordered Chebyshev-Lobatto points
- are the same regardless the degree of the sequence.
- Multiplication with such a scalar polynomial should be allowed without
- any transformation.
- This is an edge case, especially for Newton polynomial.
+ When a non-scalar polynomial is multiplied by a scalar polynomial,
+ the multiplication can proceed without basis transformation if the
+ non-scalar polynomial's generating points are compatible with
+ (i.e., contained within) the product grid's generating points.
+
+ For Leja-ordered Chebyshev-Lobatto points, the first two points
+ remain invariant regardless of the sequence degree. Therefore, if
+ a non-scalar polynomial of degree 0 or 1 has generating points that
+ match the first two points of the product grid (even when the product
+ grid is of higher degree), the product polynomial inherits the same
+ Newton basis and the coefficients are simply scaled
+ by the scalar value.
+
+ Without this compatibility (even for a degree 1 polynomial),
+ if the generating points don't match, the polynomial's representation
+ must be recomputed in the new basis via the divided difference scheme,
+ which this test verifies is avoided when points are compatible.
+
+ This is an edge case particularly relevant for Newton polynomials,
+ where changing the degree typically alters the generating points
+ for unnested grids.
Notes
-----
- Instances of `LagrangePolynomial` are excluded from this test as
it does not support polynomial-polynomial multiplication.
+ - For fully nested grids, this test would work
+ with higher polynomial degrees as all generating points are nested.
+ - A scalar polynomial can have separate indices (i.e., live on a
+ higher-degree grid than necessary for representing a constant).
"""
- # Create an instance of polynomial
+ # Create a polynomial of degree 0 or 1
m = SpatialDimension
p = LpDegree
poly = poly_class_no_lag.from_degree(m, poly_degree, p)
poly.coeffs = np.random.rand(len(poly.multi_index), num_polynomials)
- # Create a scalar polynomial (of the same dimension)
+ # Create a scalar polynomial on a compatible grid
+ # The scalar has multi_index with all-zero exponents (degree 0)
exponents = np.zeros((1, m), dtype=np.int_)
mi = MultiIndexSet(exponents, p)
- # Create a Grid based of Leja-ordered Chebyshev-Lobatto points
- n = 1 # NOTE: the first two points are always the same
- grd = Grid.from_degree(m, 1, p)
- # Generate a random scalar
- scalar = np.random.rand(1)[0]
- # Repeat the scalar column-wise to match the length of the polynomial
+
+ # Use Leja-ordered Chebyshev-Lobatto grid with degree 5
+ # Key: The first two points (degrees 0-1) of this grid match those
+ # in the poly's grid, making them compatible
+ n = 5
+ grd = Grid.from_degree(m, n, p)
+
+ # Create scalar coefficient matching the number of polynomials
+ scalar = 3 * np.random.rand(1)[0]
coeffs = np.repeat(scalar, len(poly))[np.newaxis, :]
poly_scalar = poly.__class__(mi, coeffs, grid=grd)
- # Multiplication
+ # Multiply: scalar has scalar multi_index and compatible grid,
+ # so multiplication proceeds via monomials without transformation
poly_prod_1 = poly * poly_scalar
poly_prod_2 = poly_scalar * poly
- # Assertions
+ # Verify the products maintain separate indices & correct coefficients
assert poly_prod_1.indices_are_separate
assert poly_prod_2.indices_are_separate
assert np.all(poly_prod_1.coeffs == poly.coeffs * scalar)
assert np.all(poly_prod_2.coeffs == poly.coeffs * scalar)
+ # Verify numerical correctness by evaluation
+ xx_test = -1 + 2 * np.random.rand(10, SpatialDimension)
+ yy_test_1 = poly_prod_1(xx_test)
+ yy_test_2 = poly(xx_test) * scalar
+ assert np.allclose(yy_test_1, yy_test_2)
+
def test_inplace(self, rand_poly_mnp_no_lag):
"""Augmented multiplication of polynomials raises an exception.
@@ -1353,8 +1326,7 @@ def test_non_matching_domain(
PolyDegree,
LpDegree,
):
- """Addition and subtraction of polynomials with a non-matching domain
- raises an exception.
+ """Test addition and subtraction with non-matching domain raises error.
"""
# Create a MultiIndexSet
mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
@@ -1362,23 +1334,19 @@ def test_non_matching_domain(
# Create a random set of coefficients
coeffs = np.random.rand(len(mi))
- # Create a polynomial instance
- domain_1 = np.ones((2, SpatialDimension))
- domain_1[0, :] *= -2
- domain_1[1, :] *= 2
- poly_1 = poly_class_all(mi, coeffs, user_domain=domain_1)
- domain_2 = np.ones((2, SpatialDimension))
- domain_2[0, :] *= -0.5
- domain_2[1, :] *= 0.5
- poly_2 = poly_class_all(mi, coeffs, user_domain=domain_2)
+ # Create polynomial instances with different domains
+ dom_1 = Domain.uniform(SpatialDimension, lower=-2, upper=2)
+ poly_1 = poly_class_all(mi, coeffs, domain=dom_1)
+ dom_2 = Domain.uniform(SpatialDimension, lower=0, upper=1)
+ poly_2 = poly_class_all(mi, coeffs, domain=dom_2)
# Assertions
- with pytest.raises(ValueError):
+ with pytest.raises(DomainMismatchError):
# Addition
- print(poly_1 + poly_2)
- with pytest.raises(ValueError):
+ _ = poly_1 + poly_2
+ with pytest.raises(DomainMismatchError):
# Subtraction
- print(poly_1 - poly_2)
+ _ = poly_1 - poly_2
class TestAdditionScalar:
@@ -1539,7 +1507,7 @@ def test_self_thrice(self, rand_poly_mnp_no_lag):
assert poly_sum == 4 * poly
def test_scalar_poly_same_dim(self, rand_poly_mnp_no_lag):
- """Test adding a scalar polynomial of the same dimension.
+ r"""Test adding a scalar polynomial of the same dimension.
Notes
-----
@@ -1566,7 +1534,7 @@ def test_scalar_poly_same_dim(self, rand_poly_mnp_no_lag):
assert poly_sum == poly + scalar
def test_scalar_poly_diff_dim(self, rand_poly_mnp_no_lag):
- """Test adding a scalar polynomial of the higher dimension
+ r"""Test adding a scalar polynomial of the higher dimension
Notes
-----
@@ -1861,7 +1829,7 @@ def test_self_thrice(self, rand_poly_mnp_no_lag):
assert poly_sub == -2 * poly
def test_scalar_poly_same_dim(self, rand_poly_mnp_no_lag):
- """Test subtracting a scalar polynomial of the same dimension.
+ r"""Test subtracting a scalar polynomial of the same dimension.
Notes
-----
@@ -1888,7 +1856,7 @@ def test_scalar_poly_same_dim(self, rand_poly_mnp_no_lag):
assert poly_sum == poly - scalar
def test_scalar_poly_diff_dim(self, rand_poly_mnp_no_lag):
- """Test subtracting a scalar polynomial of the higher dimension
+ r"""Test subtracting a scalar polynomial of the higher dimension
Notes
-----
@@ -2056,21 +2024,17 @@ def test_non_matching_domain(
# Create a random set of coefficients
coeffs = np.random.rand(len(mi))
- # Create a polynomial instance with different domains
- domain_1 = np.ones((2, SpatialDimension))
- domain_1[0, :] *= -2
- domain_1[1, :] *= 2
- poly_1 = poly_class_all(mi, coeffs, user_domain=domain_1)
- domain_2 = np.ones((2, SpatialDimension))
- domain_2[0, :] *= -0.5
- domain_2[1, :] *= 0.5
- poly_2 = poly_class_all(mi, coeffs, user_domain=domain_2)
+ # Create polynomial instances with different domains
+ dom_1 = Domain.uniform(SpatialDimension, lower=0, upper=1)
+ poly_1 = poly_class_all(mi, coeffs, domain=dom_1)
+ dom_2 = Domain.uniform(SpatialDimension, lower=0, upper=2)
+ poly_2 = poly_class_all(mi, coeffs, domain=dom_2)
# Assertions
- with pytest.raises(ValueError):
+ with pytest.raises(DomainMismatchError):
# Addition
poly_1 += poly_2
- with pytest.raises(ValueError):
+ with pytest.raises(DomainMismatchError):
# Subtraction
poly_1 -= poly_2
diff --git a/tests/test_polynomial_canonical.py b/tests/test_polynomial_canonical.py
index f10d21c0..bbf7ed61 100644
--- a/tests/test_polynomial_canonical.py
+++ b/tests/test_polynomial_canonical.py
@@ -113,267 +113,3 @@ def test_sub_different_poly():
groundtruth = polys[0].__class__(groundtruth_multi_index, groundtruth_coeffs)
assert_polynomial_almost_equal(res, groundtruth)
-
-def test_partial_diff():
-
- # ATTENTION: the exponent vectors of all derivatives have to be included already!
- exponents = np.array([[0, 0, 0],
- [0, 1, 0],
- [0, 0, 1],
- [0, 1, 1],
- [0, 0, 2]])
- coeffs = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
- assert exponents.shape == (5, 3)
- assert coeffs.shape == (5,)
-
- mi = MultiIndexSet(exponents, lp_degree=1.0)
- can_poly = CanonicalPolynomial(mi, coeffs)
-
- groundtruth_coeffs_dx = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
- groundtruth_coeffs_dy = np.array([2.0, 0.0, 4.0, 0.0, 0.0])
- groundtruth_coeffs_dz = np.array([3.0, 4.0, 10.0, 0.0, 0.0])
-
- can_poly_dx = can_poly.partial_diff(0)
- coeffs_dx = can_poly_dx.coeffs
- assert np.allclose(coeffs_dx, groundtruth_coeffs_dx)
-
- can_poly_dy = can_poly.partial_diff(1)
- coeffs_dy = can_poly_dy.coeffs
- assert np.allclose(coeffs_dy, groundtruth_coeffs_dy)
-
- can_poly_dz = can_poly.partial_diff(2)
- coeffs_dz = can_poly_dz.coeffs
- assert np.allclose(coeffs_dz, groundtruth_coeffs_dz)
-
-
-def test_diff():
-
- # ATTENTION: the exponent vectors of all derivatives have to be included already!
- exponents = np.array([[0, 0, 0],
- [0, 1, 0],
- [0, 0, 1],
- [0, 1, 1],
- [0, 0, 2]])
- coeffs = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
- assert exponents.shape == (5, 3)
- assert coeffs.shape == (5,)
-
- mi = MultiIndexSet(exponents, lp_degree=1.0)
- can_poly = CanonicalPolynomial(mi, coeffs)
-
- # Testing zeroth order derivatives
- can_poly_zero_deriv = can_poly.diff([0,0,0])
- coeffs_zero_deriv = can_poly_zero_deriv.coeffs
- assert np.allclose(coeffs_zero_deriv, coeffs)
-
- groundtruth_coeffs_dyz = np.array([4.0, 0.0, 0.0, 0.0, 0.0])
- groundtruth_coeffs_dz2 = np.array([10.0, 0.0, 0.0, 0.0, 0.0])
- groundtruth_coeffs_dyz2 = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
-
- can_poly_dyz = can_poly.diff([0,1,1])
- coeffs_dyz = can_poly_dyz.coeffs
- assert np.allclose(coeffs_dyz, groundtruth_coeffs_dyz)
-
- can_poly_dz2 = can_poly.diff([0,0,2])
- coeffs_dz2 = can_poly_dz2.coeffs
- assert np.allclose(coeffs_dz2, groundtruth_coeffs_dz2)
-
- can_poly_dyz2 = can_poly.diff([0,1,2])
- coeffs_dyz2 = can_poly_dyz2.coeffs
- assert np.allclose(coeffs_dyz2, groundtruth_coeffs_dyz2)
-
-
-def test_partial_diff_multiple_poly():
-
- # ATTENTION: the exponent vectors of all derivatives have to be included already!
- exponents = np.array([[0, 0, 0],
- [0, 1, 0],
- [0, 0, 1],
- [0, 1, 1],
- [0, 0, 2]])
- coeffs = np.array([[3.0, 3.0, 3.0, 3.0, 3.0],
- [5.0, 4.0, 3.0, 2.0, 1.0]]).T
-
- assert exponents.shape == (5, 3)
- assert coeffs.shape == (5,2)
-
- mi = MultiIndexSet(exponents, lp_degree=1.0)
- can_poly = CanonicalPolynomial(mi, coeffs)
-
- groundtruth_coeffs_dx = np.array([[0.0, 0.0, 0.0, 0.0, 0.0],
- [0.0, 0.0, 0.0, 0.0, 0.0]]).T
- groundtruth_coeffs_dy = np.array([[3.0, 0.0, 3.0, 0.0, 0.0],
- [4.0, 0.0, 2.0, 0.0, 0.0]]).T
- groundtruth_coeffs_dz = np.array([[3.0, 3.0, 6.0, 0.0, 0.0],
- [3.0, 2.0, 2.0, 0.0, 0.0]]).T
-
- can_poly_dx = can_poly.partial_diff(0)
- coeffs_dx = can_poly_dx.coeffs
- assert np.allclose(coeffs_dx, groundtruth_coeffs_dx)
-
- can_poly_dy = can_poly.partial_diff(1)
- coeffs_dy = can_poly_dy.coeffs
- assert np.allclose(coeffs_dy, groundtruth_coeffs_dy)
-
- can_poly_dz = can_poly.partial_diff(2)
- coeffs_dz = can_poly_dz.coeffs
- assert np.allclose(coeffs_dz, groundtruth_coeffs_dz)
-
-
-def test_integrate_over_bounds_invalid_shape(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid shape."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi))
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = np.random.rand(SpatialDimension + 3, 2)
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- can_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_invalid_domain(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid domain."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi))
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = 2 * np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- can_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_equal(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with equal bounds (should be zero)."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi))
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Create bounds (one of them has lb == ub)
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
- idx = np.random.choice(SpatialDimension)
- bounds[idx, 0] = bounds[idx, 1]
-
- # Compute the integral
- ref = 0.0
- value = can_poly.integrate_over(bounds)
-
- # Assertion
- assert np.isclose(ref, value)
-
-
-def test_integrate_over_bounds_flipped(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with flipped bounds."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi))
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Compute the integral
- value_1 = can_poly.integrate_over()
-
- # Flip bounds
- bounds = np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
- bounds[:, [0, 1]] = bounds[:, [1, 0]]
-
- # Compute the integral with flipped bounds
- value_2 = can_poly.integrate_over(bounds)
-
- if np.mod(SpatialDimension, 2) == 1:
- # Odd spatial dimension flips the sign
- assert np.isclose(value_1, -1 * value_2)
- else:
- assert np.isclose(value_1, value_2)
-
-
-def test_integrate_over(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration in different basis (sanity check)."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi))
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Transform to other polynomial bases
- nwt_poly = CanonicalToNewton(can_poly)()
- lag_poly = CanonicalToLagrange(can_poly)()
-
- # Compute the integral
- # NOTE: Canonical integration won't work in high degree
- value_can = can_poly.integrate_over()
- value_nwt = nwt_poly.integrate_over()
- value_lag = lag_poly.integrate_over()
-
- # Assertions
- assert np.isclose(value_can, value_nwt)
- assert np.isclose(value_can, value_lag)
-
- # Create bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- value_can = can_poly.integrate_over(bounds)
- value_nwt = nwt_poly.integrate_over(bounds)
- value_lag = lag_poly.integrate_over(bounds)
-
- # Assertions
- assert np.isclose(value_can, value_nwt)
- assert np.isclose(value_can, value_lag)
-
-
-def test_integrate_over_multiple_polynomials(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration in different basis (sanity check)."""
- # Create a set of Canonical polynomials
- num_polys = 6
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- can_coeffs = np.random.rand(len(mi), num_polys)
- can_poly = CanonicalPolynomial(mi, can_coeffs)
-
- # Transform to other polynomial bases
- nwt_poly = CanonicalToNewton(can_poly)()
- lag_poly = CanonicalToLagrange(can_poly)()
-
- # Compute the integral
- # NOTE: Canonical integration won't work in high degree
- value_can = can_poly.integrate_over()
- value_nwt = nwt_poly.integrate_over()
- value_lag = lag_poly.integrate_over()
-
- # Assertions
- assert np.allclose(value_can, value_nwt)
- assert np.allclose(value_can, value_lag)
-
- # Create bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- value_can = can_poly.integrate_over(bounds)
- value_nwt = nwt_poly.integrate_over(bounds)
- value_lag = lag_poly.integrate_over(bounds)
-
- # Assertions
- assert np.allclose(value_can, value_nwt)
- assert np.allclose(value_can, value_lag)
diff --git a/tests/test_polynomial_differentiation.py b/tests/test_polynomial_differentiation.py
new file mode 100644
index 00000000..401543e1
--- /dev/null
+++ b/tests/test_polynomial_differentiation.py
@@ -0,0 +1,601 @@
+"""
+Test suite for polynomial differentiation functionality.
+
+This module verifies differentiation operations for all differentiable
+polynomial bases. Tests are focused around "obvious" mathematical properties
+rather than validation against external reference data:
+
+- Linearity: Differentiation distributes over addition and scalar
+ multiplication
+- Product rule: Differentiation of products follows the Leibniz rule
+- Basis invariance: Differentiation commutes with basis transformations
+- Domain awareness: Automatic chain rule scaling for custom domains
+- Theorems: Schwarz's theorem (commutativity of mixed partials), identity
+ operation, successive differentiation
+
+The tests employ random polynomials across various domains, spatial dimensions,
+and order of derivatives.
+"""
+import numpy as np
+import pytest
+
+from numpy.testing import assert_almost_equal
+
+from minterpy import (
+ Domain,
+ CanonicalPolynomial,
+ NewtonPolynomial,
+ get_transformation,
+)
+from minterpy.utils.multi_index import find_match_between
+
+##################################
+# Module variables and fixtures #
+##################################
+
+# Transformation between bases
+TARGET_POLYS = {
+ NewtonPolynomial: CanonicalPolynomial,
+ CanonicalPolynomial: NewtonPolynomial,
+}
+
+# --- Differentiable polynomial classes
+DIFFERENTIABLE_POLYS = [NewtonPolynomial, CanonicalPolynomial]
+
+
+def _id_poly_class(poly_class):
+ return f"{poly_class.__name__:>19}"
+
+
+@pytest.fixture(params=DIFFERENTIABLE_POLYS, ids=_id_poly_class)
+def differentiable_poly(request):
+ return request.param
+
+
+# --- Random integrable polynomial instance
+@pytest.fixture
+def rand_poly(
+ differentiable_poly,
+ multi_index_mnp,
+ domain,
+ num_polynomials,
+):
+ """Create a random differentiable polynomial as a fixture."""
+ # Create random coefficients
+ if num_polynomials > 1:
+ coeffs = np.random.rand(len(multi_index_mnp), num_polynomials)
+ else:
+ coeffs = np.random.rand(len(multi_index_mnp))
+
+ # Create an instance of polynomial
+ poly = differentiable_poly(multi_index_mnp, coeffs, domain=domain)
+
+ return poly
+
+
+# --- Differentiation backends (for Newton polynomial)
+DIFF_BACKENDS = ["numpy", "numba", "numba-par"]
+
+
+# --- Order of derivatives
+deriv_order_types = [
+ "single_1st", # [1, 0, 0, ...], [0, 1, 0, ...], etc.
+ "single_2nd", # [2, 0, 0, ...], [0, 2, 0, ...], etc.
+ "mixed_1st", # [1, 1, 0, ...], [0, 1, 0, 1, ...], etc.
+ "mixed_2nd", # [1, 0, 2, ...], [0, 2, 1, 0, ...], etc.
+]
+
+def _id_deriv_order_type(deriv_order_type):
+ return f"deriv_order={deriv_order_type:>10}"
+
+
+@pytest.fixture(params=deriv_order_types, ids=_id_deriv_order_type)
+def deriv_order_type(request):
+ """The order of derivatives type as a test fixture."""
+ return request.param
+
+
+@pytest.fixture()
+def deriv_order(deriv_order_type, multi_index_mnp):
+ """The order of derivatives as a test fixture.
+
+ Notes
+ -----
+ - The available order derivative depends on the spatial dimension and
+ the degree of the polynomial.
+ """
+ # Get the dimension and degree
+ m = multi_index_mnp.spatial_dimension
+ n = multi_index_mnp.poly_degree
+
+ deriv_order = np.zeros(m, dtype=int)
+ if deriv_order_type == "single_1st":
+ # Differentiate once with respect to a random dimension
+ idx = np.random.choice(m)
+ deriv_order[idx] = 1
+ elif deriv_order_type == "single_2nd":
+ if n < 2:
+ pytest.skip(f"{deriv_order_type} requires n >= 2")
+ # Differentiate twice with respect to a random dimension
+ idx = np.random.choice(m)
+ deriv_order[idx] = 2
+ elif deriv_order_type == "mixed_1st":
+ if m < 2 or n < 2:
+ pytest.skip(f"{deriv_order_type} requires m >= 2 and n >= 2")
+ # Differentiate twice with respect to two random dimensions
+ idxs = np.random.choice(m, 2, replace=False)
+ deriv_order[idxs[0]] = 1
+ deriv_order[idxs[1]] = 1
+ elif deriv_order_type == "mixed_2nd":
+ if m < 2 or n < 3:
+ pytest.skip(f"{deriv_order_type} requires m >= 2 and n >= 3")
+ # Differentiate thrice with respect to two random dimensions
+ idxs = np.random.choice(m, 2, replace=False)
+ deriv_order[idxs[0]] = 1
+ deriv_order[idxs[1]] = 2
+
+ return deriv_order
+
+#######################
+# Internal functions #
+#######################
+
+def assert_polynomial_almost_equal(poly_1, poly_2):
+ """Assert that two polynomials are almost equal in value."""
+ try:
+ assert isinstance(poly_1, type(poly_2))
+ assert poly_1.multi_index == poly_2.multi_index
+ assert poly_1.grid == poly_2.grid
+ assert_almost_equal(poly_1.coeffs, poly_2.coeffs)
+ except AssertionError as a:
+ raise AssertionError(
+ f"The two instances of {poly_1.__class__.__name__} "
+ f"are not almost equal:\n\n {a}"
+ )
+
+
+def diff_can_coeffs(
+ coeffs: np.ndarray,
+ exponents: np.ndarray,
+ deriv_order: np.ndarray,
+ diff_factor: float,
+):
+ """Compute differentiated canonical polynomial coefficients via power rule.
+
+ This is an independent implementation of canonical polynomial
+ differentiation for verification purposes. Applies the power rule directly
+ to each monomial term.
+
+ Parameters
+ ----------
+ coeffs : np.ndarray
+ The coefficients of the original polynomial.
+ exponents : np.ndarray
+ The multi-index exponents of the original polynomial as an array
+ of shape ``(N, m)``, where ``N`` is the number of monomials and ``m``
+ is the number of spatial dimensions.
+ deriv_order : np.ndarray
+ The order of derivatives as an array of shape ``(m,)``, where ``m`` is
+ the number of spatial dimensions. Each element of the array specifies
+ the order of differentiation along each spatial dimension.
+ diff_factor : float
+ The domain scaling factor.
+
+ Returns
+ -------
+ np.ndarray
+ The coefficients of the differentiated polynomial.
+ """
+ # Get the non-negative exponents after differentiation
+ nn_exponents = exponents[~np.any(exponents - deriv_order < 0, axis=1)]
+
+ # Find correspondence between original and differentiated polynomials
+ diff_exponents = nn_exponents - deriv_order
+ indices_target = find_match_between(diff_exponents, exponents)
+ indices_origin = find_match_between(nn_exponents, exponents)
+
+ # Compute the coefficients
+ coeffs_ = np.zeros(coeffs.shape)
+ for i, (idx_1, idx_2) in enumerate(zip(indices_target, indices_origin)):
+ # Compute the factor due to monomial exponents being knocked down
+ factor = 1.0
+ for j, deriv_order_dim in enumerate(deriv_order):
+ for k in range(deriv_order_dim):
+ factor *= nn_exponents[i, j] - k
+ coeffs_[idx_1] = factor * coeffs[idx_2]
+
+ return diff_factor * coeffs_
+
+##########
+# Tests #
+##########
+
+def test_domain_scaling_factor(rand_poly, deriv_order):
+ """Test the scaling factor of a differentiable polynomial (forward).
+
+ Internal representation of polynomial does not change with respect to the
+ user domain. The result between differentiation of polynomials in the user
+ domain and in the default internal domain only differs by a scaling factor
+ (which depends on the order of derivatives).
+ """
+ poly_1 = rand_poly
+ # Create another instance with the default internal domain
+ poly_2 = rand_poly.__class__(
+ poly_1.multi_index,
+ poly_1.coeffs,
+ )
+
+ # Differentiate the polynomial
+ diff_poly_1 = poly_1.diff(deriv_order)
+ diff_poly_2 = poly_2.diff(deriv_order)
+
+ diff_factor = poly_1.domain.diff_factor(deriv_order)
+
+ # Assertions
+ # NOTE: Domains are by construction not equal, instances are not "close"!
+ assert isinstance(diff_poly_1, type(diff_poly_2))
+ assert diff_poly_1.multi_index == diff_poly_2.multi_index
+ # Forward: user domain -> factor * internal domain
+ assert np.allclose(diff_poly_1.coeffs, diff_factor * diff_poly_2.coeffs)
+ assert np.allclose(diff_factor * diff_poly_2.coeffs, diff_poly_1.coeffs)
+ # Backward: internal domain -> user domain / factor
+ assert np.allclose(diff_poly_1.coeffs / diff_factor, diff_poly_2.coeffs)
+ assert np.allclose(diff_poly_2.coeffs, diff_poly_1.coeffs / diff_factor)
+
+
+def test_linearity_mul_add(rand_poly, deriv_order):
+ """Test linearity of differentiation (scalar multiplication and addition).
+
+ diff(a * poly_1 + b * poly_2) = a * diff(poly_1) + b * diff(poly_2)
+ """
+ poly_1 = rand_poly
+ # Create another instance with different coefficients
+ if len(poly_1) > 1:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index), len(poly_1))
+ else:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index))
+ poly_2 = poly_1.__class__.from_poly(poly_1, coeffs_2)
+
+ # Generate random scalar factors
+ a = np.random.uniform(1, 5)
+ b = np.random.uniform(1, 5)
+
+ # Differentiate the polynomials
+ diff_poly_1 = (a * poly_1 + b * poly_2).diff(deriv_order)
+ diff_poly_2 = a * poly_1.diff(deriv_order) + b * poly_2.diff(deriv_order)
+
+ # Assertion
+ assert_polynomial_almost_equal(diff_poly_1, diff_poly_2)
+
+
+def test_linearity_div_sub(rand_poly, deriv_order):
+ """Test linearity of differentiation (scalar division and subtraction).
+
+ diff(poly_1 / a - poly_2 / b) = diff(poly_1) / a - diff(poly_2) / b
+ """
+ poly_1 = rand_poly
+ # Create another instance with different coefficients
+ if len(poly_1) > 1:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index), len(poly_1))
+ else:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index))
+ poly_2 = poly_1.__class__.from_poly(poly_1, coeffs_2)
+
+ # Generate random scalar factors
+ a = np.random.uniform(1, 5)
+ b = np.random.uniform(1, 5)
+
+ # Differentiate the polynomials
+ diff_poly_1 = (poly_1 / a - poly_2 / b).diff(deriv_order)
+ diff_poly_2 = poly_1.diff(deriv_order) / a - poly_2.diff(deriv_order) / b
+
+ # Assertion
+ assert_polynomial_almost_equal(diff_poly_1, diff_poly_2)
+
+
+def test_product(rand_poly):
+ """Test the product rule of differentiation.
+
+ For first order differentiation:
+
+ diff(poly_1 * poly_2) = poly_1 * diff(poly_2) + poly_2 * diff(poly_1)
+ """
+ poly_1 = rand_poly
+ # Create another instance with different coefficients
+ if len(poly_1) > 1:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index), len(poly_1))
+ else:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index))
+ poly_2 = poly_1.__class__.from_poly(poly_1, coeffs_2)
+
+ # Product rule: diff(f*g) = f*diff(g) + g*diff(f)
+ # Applied only for first order differentiation
+ m = poly_1.spatial_dimension
+ dim = np.random.choice(m)
+ lhs = (poly_1 * poly_2).partial_diff(dim)
+ rhs = poly_2 * poly_1.partial_diff(dim) + poly_1 * poly_2.partial_diff(dim)
+
+ # Assertion
+ assert_polynomial_almost_equal(lhs, rhs)
+
+
+def test_constant_annihilation(
+ differentiable_poly,
+ multi_index_mnp,
+ domain,
+ num_polynomials,
+ deriv_order,
+):
+ """Test differentiation of constant polynomial results in zero."""
+ # Create a random constant polynomial (the first index is the constant)
+ if num_polynomials > 1:
+ coeffs = np.zeros((len(multi_index_mnp), num_polynomials))
+ coeffs[0, :] = 1.0
+ else:
+ coeffs = np.zeros(len(multi_index_mnp))
+ coeffs[0] = 1.0
+ poly = differentiable_poly(multi_index_mnp, coeffs, domain=domain)
+
+ # Differentiate the polynomial
+ diff_poly = poly.diff(deriv_order)
+
+ # Assertions: All coefficients are zero
+ assert np.allclose(diff_poly.coeffs, 0)
+
+
+def test_identity(rand_poly):
+ """Test zero-order derivative is the identity operation.
+
+ diff(poly, [0, 0, ...]) = poly (i.e., polynomial equal in value)
+ """
+ poly = rand_poly
+
+ # Zero-order derivative
+ deriv_order = np.zeros(poly.spatial_dimension, dtype=int)
+ diff_poly = poly.diff(deriv_order)
+
+ # Assertions
+ # Polynomials are (strictly) equal in value
+ assert poly == diff_poly
+ # Polynomials are different object
+ assert diff_poly is not poly
+
+
+def test_partial_diff(rand_poly):
+ """Test that partial_diff(dim, order) equals diff([0,...,order,...,0]).
+
+ `partial_diff()` is a syntactic sugar for single dimension differentiation
+ using `diff()`.
+ """
+
+ dim = np.random.choice(rand_poly.spatial_dimension)
+ n = rand_poly.multi_index.poly_degree
+ if n == 0:
+ pytest.skip("Partial differentiation requires polynomial degree > 0")
+
+ order = np.random.randint(low=1, high=n+1)
+ # Create an equivalent order of derivatives array
+ deriv_order = np.zeros(rand_poly.spatial_dimension, dtype=int)
+ deriv_order[dim] = order
+
+ # Differentiate the polynomial
+ diff_poly_1 = rand_poly.partial_diff(dim, order)
+ diff_poly_2 = rand_poly.diff(deriv_order)
+
+ # Assertion: strict equality
+ assert diff_poly_1 == diff_poly_2
+
+
+def test_order_higher_than_degree(rand_poly):
+ """Test that diff with order > degree results in a zero polynomial."""
+ poly = rand_poly
+
+ # Differentiate the polynomial with order > degree
+ dim = np.random.choice(poly.spatial_dimension)
+ order = poly.multi_index.poly_degree + 1
+
+ diff_poly = poly.partial_diff(dim, order)
+
+ # Assertions
+ assert np.allclose(diff_poly.coeffs, 0)
+
+
+def test_basis_invariance_eval(rand_poly, deriv_order):
+ """Test that the evaluation of differentiated poly is basis invariant.
+
+ A polynomial differentiated in one basis should evaluate identically to
+ the same polynomial transformed to another basis and then differentiated.
+ They represent the same mathematical object in different bases.
+
+ Notes
+ -----
+ - This is a "weak form" of closeness between two polynomials.
+ The underlying coefficients may not be strictly speaking "close" but
+ as long as the evaluation over the domain is close, then the polynomials
+ are close.
+ """
+ poly_origin = rand_poly
+
+ # Transform to a different basis
+ target_basis = TARGET_POLYS[type(poly_origin)]
+ poly_target = get_transformation(poly_origin, target_basis)()
+
+ # Differentiate the polynomials
+ diff_poly_origin = poly_origin.diff(deriv_order)
+ diff_poly_target = poly_target.diff(deriv_order)
+
+ # Generate test points
+ dom = poly_origin.grid.domain
+ m = dom.spatial_dimension
+ lb, ub = dom.lowers, dom.uppers
+ xx_test = np.random.uniform(lb, ub, size=(1000, m))
+
+ # Evaluate the polynomials
+ yy_origin = diff_poly_origin(xx_test)
+ yy_target = diff_poly_target(xx_test)
+
+ # Assertion
+ assert np.allclose(yy_origin, yy_target)
+
+
+def test_basis_invariance_poly(rand_poly, deriv_order):
+ """Test that differentiated polynomials are basis invariant.
+
+ A polynomial differentiated in one basis should be equal or close to the
+ same polynomial transformed to another basis, differentiated in that basis,
+ then transformed back.
+
+ Notes
+ -----
+ - This assumes a "strong form" of closeness between two polynomials where
+ the coefficients must all be close.
+ - This test may fail at sufficiently high polynomial degrees or spatial
+ dimensions due to accumulated numerical errors in basis transformations,
+ even when differentiation is correctly implemented.
+ """
+ poly_origin = rand_poly
+
+ # Transform to a different basis
+ target_basis = TARGET_POLYS[type(poly_origin)]
+ poly_target = get_transformation(poly_origin, target_basis)()
+
+ # Differentiate the origin polynomial
+ diff_poly_origin_1 = poly_origin.diff(deriv_order)
+
+ # Differentiate the target and transform back to origin basis
+ diff_poly_target = poly_target.diff(deriv_order)
+ diff_poly_origin_2 = get_transformation(
+ diff_poly_target,
+ type(poly_origin),
+ )()
+
+ # Assertion
+ assert_polynomial_almost_equal(diff_poly_origin_1, diff_poly_origin_2)
+ assert_polynomial_almost_equal(diff_poly_origin_2, diff_poly_origin_1)
+
+
+def test_schwarz(rand_poly):
+ """Test the Schwarz theorem: order of differentiation does not matter.
+
+ Schwarz's theorem states that the order of differentiation does not matter
+ for polynomials, i.e., mixed partial derivatives commute.
+ """
+ poly = rand_poly
+ m = poly.spatial_dimension
+ if poly.spatial_dimension < 2:
+ pytest.skip("Schwarz theorem requires dimension >= 2")
+
+ # Select two random dimensions
+ dim_i, dim_j = np.random.choice(m, size=2, replace=False)
+
+ # Differentiate the polynomial
+ diff_poly_1 = poly.partial_diff(dim_i).partial_diff(dim_j)
+ diff_poly_2 = poly.partial_diff(dim_j).partial_diff(dim_i)
+
+ # Assertion
+ assert_polynomial_almost_equal(diff_poly_1, diff_poly_2)
+
+
+def test_successive(rand_poly):
+ """Test the successive differentiation equals simultaneous differentiation.
+
+ Applying `partial_diff()` once with respect to each dimension successively
+ should be equivalent to calling `diff()` with order of derivatives
+ [1, 1, ..., 1].
+ """
+ poly = rand_poly
+ m = poly.spatial_dimension
+
+ if poly.multi_index.poly_degree < 1:
+ pytest.skip("Test requires polynomial degree >= 1")
+
+ # Successive differentiation with randomly selected dimensions
+ dims = np.random.choice(m, size=m, replace=False)
+ diff_poly_1 = poly.partial_diff(dims[0])
+ for dim in dims[1:]:
+ diff_poly_1 = diff_poly_1.partial_diff(dim)
+ # Simultaneous differentiation
+ deriv_order = np.ones(m, dtype=int)
+ diff_poly_2 = poly.diff(deriv_order)
+
+ # Assertion
+ assert_polynomial_almost_equal(diff_poly_1, diff_poly_2)
+
+
+def test_newton_backend(rand_poly, deriv_order):
+ """Test the different differentiation backend for Newton polynomial.
+
+ All backends (e.g., numpy, numba, numba-par) should produce close results.
+ """
+ poly = rand_poly
+ if not isinstance(poly, NewtonPolynomial):
+ pytest.skip("Different backend is only defined for Newton polynomials")
+
+ poly_diffs = []
+ for backend in DIFF_BACKENDS:
+ poly_diffs.append(poly.diff(deriv_order, backend=backend))
+
+ # Assertions
+ assert_polynomial_almost_equal(poly_diffs[0], poly_diffs[1])
+ assert_polynomial_almost_equal(poly_diffs[1], poly_diffs[2])
+ assert_polynomial_almost_equal(poly_diffs[2], poly_diffs[0])
+
+
+def test_newton_backend_invalid(rand_poly, deriv_order):
+ """Test invalid backend for Newton polynomial should raise an exception."""
+ if not isinstance(rand_poly, NewtonPolynomial):
+ pytest.skip("Newton backend is only defined for Newton polynomials")
+
+ with pytest.raises(NotImplementedError, match="invalid"):
+ _ = rand_poly.diff(deriv_order, backend="invalid")
+
+
+def test_partial_diff_canonical(rand_poly):
+ """Test the partial differentiation of a canonical polynomial.
+
+ Notes
+ -----
+ - Because the differentiation of a polynomial in the canonical basis
+ follows a simple(r) rule, the results can be compared with an alternative
+ method.
+ """
+ poly = rand_poly
+ if not isinstance(poly, CanonicalPolynomial):
+ pytest.skip("Test is specific to CanonicalPolynomial")
+
+ # (Partial) Differentiate the polynomial once
+ m = poly.spatial_dimension
+ for dim in range(m):
+ deriv_order = np.zeros(m, dtype=int)
+ deriv_order[dim] = 1
+ diff_poly = poly.partial_diff(dim)
+
+ coeffs, exps = poly.coeffs, poly.multi_index.exponents
+ diff_factor = poly.domain.diff_factor(deriv_order)
+ coeffs_ref = diff_can_coeffs(coeffs, exps, deriv_order, diff_factor)
+
+ # Assertion
+ assert np.allclose(coeffs_ref, diff_poly.coeffs)
+
+
+def test_diff_canonical(rand_poly, deriv_order):
+ """Test the differentiation of a canonical polynomial.
+
+ Notes
+ -----
+ - Because the differentiation of a polynomial in the canonical basis
+ follows a simple(r) rule, the results can be compared with an alternative
+ method.
+ """
+ poly = rand_poly
+ if not isinstance(poly, CanonicalPolynomial):
+ pytest.skip("Test is specific to CanonicalPolynomial")
+
+ # Differentiate the polynomial
+ diff_poly = poly.diff(deriv_order)
+ coeffs, exps = poly.coeffs, poly.multi_index.exponents
+ diff_factor = poly.domain.diff_factor(deriv_order)
+ coeffs_ref = diff_can_coeffs(coeffs, exps, deriv_order, diff_factor)
+
+ # Assertion
+ assert np.allclose(coeffs_ref, diff_poly.coeffs)
diff --git a/tests/test_polynomial_integration.py b/tests/test_polynomial_integration.py
new file mode 100644
index 00000000..ffc39acd
--- /dev/null
+++ b/tests/test_polynomial_integration.py
@@ -0,0 +1,275 @@
+"""
+Test suite for polynomial integration functionality across different bases.
+
+This module verifies the `integrate_over()` method for all "integrable"
+polynomial representations. Tests cover integration over custom domains,
+subdivisions, bound validations, linearity properties,
+and invariance under basis transformations.
+"""
+import numpy as np
+import pytest
+
+from itertools import product
+
+from minterpy import (
+ Domain,
+ LagrangePolynomial,
+ NewtonPolynomial,
+ CanonicalPolynomial,
+ get_transformation,
+)
+
+##################################
+# Module variables and fixtures #
+##################################
+
+# Transformation between bases
+TARGET_POLYS = {
+ LagrangePolynomial: [NewtonPolynomial, CanonicalPolynomial],
+ NewtonPolynomial: [LagrangePolynomial, CanonicalPolynomial],
+ CanonicalPolynomial: [LagrangePolynomial, NewtonPolynomial],
+}
+
+
+# Integrable polynomial classes
+INTEGRABLE_POLYS = [LagrangePolynomial, NewtonPolynomial, CanonicalPolynomial]
+
+
+def _id_poly_class(poly_class):
+ return f"{poly_class.__name__:>19}"
+
+
+@pytest.fixture(params=INTEGRABLE_POLYS, ids=_id_poly_class)
+def integrable_poly(request):
+ return request.param
+
+
+# Random integrable polynomial instance
+@pytest.fixture
+def rand_integrable_poly(
+ integrable_poly,
+ multi_index_mnp,
+ domain,
+ num_polynomials,
+):
+ """Create a random integrable polynomial as a fixture."""
+ # Create random coefficients
+ if num_polynomials > 1:
+ coeffs = np.random.rand(len(multi_index_mnp), num_polynomials)
+ else:
+ coeffs = np.random.rand(len(multi_index_mnp))
+
+ # Create an instance of polynomial
+ poly = integrable_poly(multi_index_mnp, coeffs, domain=domain)
+
+ return poly
+
+
+def test_domain_scaling_factor(rand_integrable_poly):
+ """Test the integration of polynomial over its domain.
+
+ Notes
+ -----
+ - Minterpy polynomials are internally defined in the internal domain.
+ The difference between integrating Minterpy polynomials with a custom
+ domain and the ones with the default domain is just a factor (Jacobian).
+ """
+ poly_1 = rand_integrable_poly
+ # Create another instance with the default internal domain
+ poly_2 = rand_integrable_poly.__class__(
+ poly_1.multi_index,
+ poly_1.coeffs,
+ )
+
+ # Compute the integral over the whole domain
+ int_1 = poly_1.integrate_over()
+ int_2 = poly_2.integrate_over()
+
+ # Assertions (commutative check)
+ assert np.allclose(poly_1.domain.int_factor() * int_2, int_1)
+ assert np.allclose(int_1, poly_1.domain.int_factor() * int_2)
+
+
+def test_sum_over_subdivided_domain(rand_integrable_poly):
+ """Test the sum of polynomial integration over subdomains."""
+ poly = rand_integrable_poly
+
+ # Divide the domain at a mid-point
+ mid_point = np.mean(poly.domain.bounds, axis=1)
+ interval_1 = np.stack([poly.domain.lowers, mid_point], axis=1)
+ interval_2 = np.stack([mid_point, poly.domain.uppers], axis=1)
+ # Divide the domains (in higher dimension -> hyper-rectangles)
+ subdomains = [
+ np.array(combi) for combi in product(*zip(interval_1, interval_2))
+ ]
+
+ # Compute the integral over the whole domain
+ int_1 = poly.integrate_over()
+
+ # Compute the integral over each subdomain
+ int_2 = 0.0
+ for subdomain in subdomains:
+ int_2 += poly.integrate_over(subdomain)
+
+ # Assertions (commutative check)
+ assert np.allclose(int_1, int_2)
+ assert np.allclose(int_2, int_1)
+
+
+def test_bounds_equal(rand_integrable_poly):
+ """Test that integral is zero when at least one integral bound collapses.
+ """
+ poly = rand_integrable_poly
+
+ # Create bounds (one of them has lb == ub)
+ dim = poly.domain.spatial_dimension
+ bounds = poly.domain.bounds.copy()
+ idx = np.random.choice(dim)
+ bounds[idx, 0] = bounds[idx, 1]
+
+ # Compute integral
+ int_over = poly.integrate_over(bounds)
+
+ # Assertion
+ assert np.allclose(int_over, 0.0)
+
+
+def test_bounds_flipped(rand_integrable_poly):
+ """Test that integral flip sign when one of the bounds is flipped."""
+ poly = rand_integrable_poly
+
+ # Compute the integral
+ int_over_1 = poly.integrate_over()
+
+ # Flip one of the bounds
+ dim = poly.domain.spatial_dimension
+ bounds = poly.domain.bounds.copy()
+ idx = np.random.choice(dim)
+ bounds[idx, [0, 1]] = bounds[idx, [1, 0]]
+
+ # Compute the integral with flipped bounds
+ int_over_2 = poly.integrate_over(bounds)
+
+ # Assertion
+ assert np.allclose(int_over_1, -1 * int_over_2)
+
+
+def test_bounds_invalid_shape(rand_integrable_poly):
+ """Test polynomial integration with bounds of invalid shape."""
+ poly = rand_integrable_poly
+
+ # Create bounds (invalid shape)
+ dim = poly.domain.spatial_dimension
+ bounds = np.random.rand(dim + 3, 2) # Dimension mismatch
+
+ with pytest.raises(ValueError):
+ _ = poly.integrate_over(bounds)
+
+
+def test_list_as_bounds(rand_integrable_poly):
+ """Test integrate over with bounds specified with lists.
+
+ Notes
+ -----
+ - The method accepts bounds as lists (or any array-like structure),
+ not just NumPy arrays, as they are internally converted using
+ ``np.atleast_2d()``.
+ """
+ poly = rand_integrable_poly
+
+ # Compute the integral
+ value_1 = poly.integrate_over()
+
+ # Create the bounds as a list
+ bounds = [list(_) for _ in poly.domain.bounds]
+
+ # Compute the integral with the bounds as a list
+ value_2 = poly.integrate_over(bounds)
+
+ # Assertion (equality should be exact)
+ assert np.all(value_1 == value_2)
+
+
+def test_linearity_mul_add(rand_integrable_poly):
+ """Test linearity of integration from scalar multiplication and addition.
+
+ Integ(a * poly_1 + b * poly_2) = a * integ(poly_1) + b * integ(poly_2)
+ """
+ if isinstance(rand_integrable_poly, LagrangePolynomial):
+ pytest.skip(
+ "Polynomial Arithmetic operations with Lagrange polynomials "
+ "are not supported"
+ )
+
+ poly_1 = rand_integrable_poly
+ # Create another instance with different coefficients
+ if len(poly_1) > 1:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index), len(poly_1))
+ else:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index))
+ poly_2 = poly_1.__class__.from_poly(poly_1, coeffs_2)
+
+ # Generate random scalar factors
+ a = np.random.uniform(-5, 5)
+ b = np.random.uniform(-5, 5)
+
+ int_1 = (a * poly_1 + b * poly_2).integrate_over()
+ int_2 = a * poly_1.integrate_over() + b * poly_2.integrate_over()
+
+ # Assertions (commutative check)
+ assert np.allclose(int_1, int_2)
+ assert np.allclose(int_2, int_1)
+
+
+def test_linearity_div_sub(rand_integrable_poly):
+ """Test linearity of integration from scalar division and subtraction.
+
+ Integ(poly_1 / a + poly_2 / b) = integ(poly_1) / a + integ(poly_2) / b
+ """
+ if isinstance(rand_integrable_poly, LagrangePolynomial):
+ pytest.skip(
+ "Polynomial Arithmetic operations with Lagrange polynomials "
+ "are not supported"
+ )
+
+ poly_1 = rand_integrable_poly
+ # Create another instance with different coefficients
+ if len(poly_1) > 1:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index), len(poly_1))
+ else:
+ coeffs_2 = np.random.rand(len(poly_1.multi_index))
+ poly_2 = poly_1.__class__.from_poly(poly_1, coeffs_2)
+
+ # Generate random scalar factors
+ a = np.random.uniform(-5, 5)
+ b = np.random.uniform(-5, 5)
+
+ int_1 = (poly_1 / a - poly_2 / b).integrate_over()
+ int_2 = poly_1.integrate_over() / a - poly_2.integrate_over() / b
+
+ # Assertions (commutative check)
+ assert np.allclose(int_1, int_2)
+ assert np.allclose(int_2, int_1)
+
+
+def test_bases_transformation_invariance(rand_integrable_poly):
+ """Test the integral of polynomial in different bases.
+
+ Polynomials represented in different bases should have the same integral.
+ In other words, polynomial integration is invariant under basis
+ transformation.
+ """
+ poly = rand_integrable_poly
+ int_origin = poly.integrate_over()
+
+ # Convert to a different basis
+ int_targets = []
+ target_bases = TARGET_POLYS[poly.__class__]
+ for target_basis in target_bases:
+ poly_target = get_transformation(poly, target_basis)()
+ int_targets.append(poly_target.integrate_over())
+
+ # Assertions
+ for int_target in int_targets:
+ assert np.allclose(int_origin, int_target)
+ assert np.allclose(int_target, int_origin)
diff --git a/tests/test_polynomial_lagrange.py b/tests/test_polynomial_lagrange.py
index b54c87f5..0c4e42bb 100644
--- a/tests/test_polynomial_lagrange.py
+++ b/tests/test_polynomial_lagrange.py
@@ -140,262 +140,3 @@ def test_exponentiation(rand_poly_mnp_lag):
with pytest.raises(NotImplementedError):
_ = poly**3
-
-
-def test_integrate_over_bounds_invalid_shape(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid shape."""
- # Create a Lagrange polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = CanonicalPolynomial(mi, lag_coeffs)
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = np.random.rand(SpatialDimension + 3, 2)
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- lag_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_invalid_domain(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid domain."""
- # Create a Lagrange polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = CanonicalPolynomial(mi, lag_coeffs)
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = 2 * np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- lag_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_equal(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with equal bounds (should be zero)."""
- # Create a Lagrange polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = LagrangePolynomial(mi, lag_coeffs)
-
- # Create bounds (one of them has lb == ub)
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
- idx = np.random.choice(SpatialDimension)
- bounds[idx, 0] = bounds[idx, 1]
-
- # Compute the integral
- ref = 0.0
- value = lag_poly.integrate_over(bounds)
-
- # Assertion
- assert isinstance(value, float)
- assert np.isclose(ref, value)
-
-
-def test_integrate_over_bounds_flipped(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with flipped bounds."""
- # Create a Lagrange polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = CanonicalPolynomial(mi, lag_coeffs)
-
- # Compute the integral
- value_1 = lag_poly.integrate_over()
-
- # Flip bounds
- bounds = np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
- bounds[:, [0, 1]] = bounds[:, [1, 0]]
-
- # Compute the integral with flipped bounds
- value_2 = lag_poly.integrate_over(bounds)
-
- if np.mod(SpatialDimension, 2) == 1:
- # Odd spatial dimension flips the sign
- assert np.isclose(value_1, -1 * value_2)
- else:
- assert np.isclose(value_1, value_2)
-
-
-def test_integrate_over_list_as_bounds(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test integrate over with bounds specified with lists."""
- # Create a Lagrange polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = CanonicalPolynomial(mi, lag_coeffs)
-
- # Compute the integral
- value_1 = lag_poly.integrate_over()
-
- # Flip bounds
- bounds = [[-1, 1] for _ in range(SpatialDimension)]
-
- # Compute the integral with flipped bounds
- value_2 = lag_poly.integrate_over(bounds)
-
- # Assertion
- assert np.isclose(value_1, value_2)
-
-
-def test_integrate_over(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration in different basis (sanity check)."""
- # Create a Canonical polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- lag_coeffs = np.random.rand(len(mi))
- lag_poly = LagrangePolynomial(mi, lag_coeffs)
-
- # Transform to other polynomial bases
- nwt_poly = LagrangeToNewton(lag_poly)()
- can_poly = LagrangeToCanonical(lag_poly)()
-
- # Compute the integral
- value_lag = lag_poly.integrate_over()
- value_nwt = nwt_poly.integrate_over()
- # NOTE: Canonical integration won't work in high degree
- value_can = can_poly.integrate_over()
-
- # Assertions
- assert np.isclose(value_lag, value_nwt)
- assert np.isclose(value_lag, value_can)
-
- # Create bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- value_lag = lag_poly.integrate_over(bounds)
- value_nwt = nwt_poly.integrate_over(bounds)
- value_can = can_poly.integrate_over(bounds)
-
- # Assertions
- assert np.isclose(value_lag, value_nwt)
- assert np.isclose(value_lag, value_can)
-
-
-def test_integrate_over_sum_function(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration for a simple sum function."""
- # Create a Lagrange interpolating polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- grd = Grid(mi)
- lag_coeffs = np.sum(grd.unisolvent_nodes, axis=1)
- lag_poly = LagrangePolynomial(mi, lag_coeffs)
-
- # With the default bounds
- if PolyDegree > 0:
- ref = 0.0
- value = lag_poly.integrate_over()
-
- # Assertion
- assert np.isclose(ref, value)
-
- # With non-symmetric bounds (non-cancelling)
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- if PolyDegree > 0:
- # Rhe reference from analytical results for non-cancelling bounds
- ref = 0.0
- for i in range(SpatialDimension):
- ref += (
- np.diff(bounds[i] ** 2)
- * np.prod(np.diff(np.delete(bounds, i, axis=0)))
- )
- ref *= 0.5
- value = lag_poly.integrate_over(bounds)
-
- # Assertion
- assert np.isclose(ref, value)
-
-
-def test_integrate_over_product_function(
- SpatialDimension, LpDegree,
-):
- """Test polynomial integration for a simple product function."""
- fun = lambda xx: np.prod(xx, axis=1)
-
- # Create a Lagrange interpolating polynomial
- exp = np.ones((1, SpatialDimension), dtype=int)
- exp_completed = make_complete(exp, LpDegree)
- mi = MultiIndexSet(exp_completed, LpDegree)
- grd = Grid(mi)
- lag_coeffs = fun(grd.unisolvent_nodes)
- lag_poly = LagrangePolynomial(mi, lag_coeffs)
-
- # --- With the default bounds
-
- # Compute the integral without bounds
- ref = 0.0
- value = lag_poly.integrate_over()
-
- # Assertion
- assert np.isclose(ref, value)
-
- # --- With non-symmetric bounds (non-cancelling)
-
- # Set up bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- ref = 0.5**SpatialDimension * np.prod(np.diff(bounds**2))
- value = lag_poly.integrate_over(bounds)
-
- # Assertion
- assert np.isclose(ref, value)
-
-
-def test_integrate_over_multiple_polynomials(
- SpatialDimension, LpDegree,
-):
- """Test integration with multiple polynomials."""
- num_polys = 6
- factors = np.arange(1, num_polys + 1)[np.newaxis, :]
- fun = lambda xx: np.prod(xx, axis=1)[:, np.newaxis] * factors
-
- # Create a Lagrange interpolating polynomial
- exp = np.ones((1, SpatialDimension), dtype=int)
- exp_completed = make_complete(exp, LpDegree)
- mi = MultiIndexSet(exp_completed, LpDegree)
- grd = Grid(mi)
- lag_coeffs = fun(grd.unisolvent_nodes)
- lag_poly = LagrangePolynomial(mi, lag_coeffs)
-
- # --- With the default bounds
-
- # Compute the integral without bounds
- ref = 0.0
- value = lag_poly.integrate_over()
-
- # Assertion
- assert len(value) == num_polys
- assert np.allclose(ref, value)
-
- # --- With non-symmetric bounds (non-cancelling)
-
- # Set up bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- ref = factors * 0.5 ** SpatialDimension * np.prod(np.diff(bounds ** 2))
- value = lag_poly.integrate_over(bounds)
-
- # Assertion
- assert len(value) == num_polys
- assert np.allclose(ref, value)
diff --git a/tests/test_polynomial_newton.py b/tests/test_polynomial_newton.py
index f05fa1c8..cf51c3d2 100644
--- a/tests/test_polynomial_newton.py
+++ b/tests/test_polynomial_newton.py
@@ -3,26 +3,13 @@
The subclassing is not tested here, see tesing module `test_polynomial.py`
"""
-import numpy as np
import pytest
-from conftest import (
- assert_polynomial_almost_equal,
- build_rnd_coeffs,
- build_rnd_points,
- build_random_newton_polynom,
-)
+
+from conftest import build_rnd_coeffs, build_rnd_points
from numpy.testing import assert_almost_equal
-from minterpy.global_settings import INT_DTYPE
from minterpy.utils.polynomials.newton import eval_newton_polynomials
-from minterpy import Grid, MultiIndexSet
-
-from minterpy import (
- NewtonPolynomial,
- NewtonToCanonical,
- CanonicalToNewton,
- NewtonToLagrange,
-)
+from minterpy import Grid, NewtonPolynomial, NewtonToCanonical
@pytest.fixture(params=["numpy", "numba", "numba-par"])
@@ -75,249 +62,3 @@ def test_eval_batch(multi_index_mnp, num_polynomials, BatchSizes):
# Assert
assert_almost_equal(yy_newton, yy_canonical)
-
-
-class TestDiff:
- """All tests related to the differentiation of polys. in the Newton basis.
- """
-
- def test_zero_derivative(
- self,
- SpatialDimension,
- PolyDegree,
- LpDegree,
- num_polynomials,
- diff_backend,
- ):
- """Test taking the 0th-order derivative of polynomials."""
- # Create a random Newton polynomial
- newton_poly = build_random_newton_polynom(
- SpatialDimension,
- PolyDegree,
- LpDegree,
- num_polynomials,
- )
-
- # A derivative of order zero along all dimensions should be equivalent
- # to the same polynomial
- orders = np.zeros(SpatialDimension, dtype=INT_DTYPE)
- zero_order_diff_newt = newton_poly.diff(orders)
-
- # Assertion
- assert_polynomial_almost_equal(zero_order_diff_newt, newton_poly)
-
- def test_vs_canonical(
- self,
- SpatialDimension,
- PolyDegree,
- LpDegree,
- num_polynomials,
- diff_backend,
- ):
- """Test comparing the gradient with that computed in canonical basis.
- """
- # Create a random Newton polynomial
- newton_poly = build_random_newton_polynom(
- SpatialDimension,
- PolyDegree,
- LpDegree,
- num_polynomials,
- )
-
- # Transform to the canonical basis
- trafo_n2c = NewtonToCanonical(newton_poly)
- canon_poly = trafo_n2c()
-
- # Differentiate in the canonical basis and transform back
- diff_order = np.ones(SpatialDimension, dtype=INT_DTYPE)
- can_diff_poly = canon_poly.diff(diff_order)
- trafo_c2n = CanonicalToNewton(can_diff_poly)
- newt_can_diff_poly = trafo_c2n()
-
- # Differentiate the original polynomial
- newt_diff_poly = newton_poly.diff(diff_order, backend=diff_backend)
-
- # Assertion
- assert_polynomial_almost_equal(newt_can_diff_poly, newt_diff_poly)
-
- def test_partial_diff(
- self,
- SpatialDimension,
- PolyDegree,
- LpDegree,
- diff_backend,
- ):
- """Test taking the partial derivative of polynomials."""
- # Create a random Newton polynomial
- newton_poly = build_random_newton_polynom(
- SpatialDimension,
- PolyDegree,
- LpDegree,
- )
-
- # Check partial derivative on each dimension by comparing it
- # with the partial derivative in the canonical basis
- for dim in range(SpatialDimension):
- # Transform to the canonical basis
- trafo_n2c = NewtonToCanonical(newton_poly)
- canon_poly = trafo_n2c()
- # ...differentiate
- can_diff_poly = canon_poly.partial_diff(dim)
- # ...and transform back
- trafo_c2n = CanonicalToNewton(can_diff_poly)
- newt_can_diff_poly = trafo_c2n()
-
- # Differentiate the original polynomial
- newt_diff_poly = newton_poly.partial_diff(
- dim,
- backend=diff_backend,
- )
-
- # Assertion
- assert_polynomial_almost_equal(newt_can_diff_poly, newt_diff_poly)
-
- def test_unsupported_backend(
- self,
- SpatialDimension,
- PolyDegree,
- LpDegree,
- num_polynomials,
- diff_backend,
- ):
- """Test unsupported backend to differentiate Newton polynomials."""
- # Create a Newton polynomial
- mi = MultiIndexSet.from_degree(SpatialDimension, PolyDegree, LpDegree)
- nwt_coeffs = np.random.rand(len(mi))
- nwt_poly = NewtonPolynomial(mi, nwt_coeffs)
-
- # Attempt to differentiate with a non-supported back-end
- unsupported_backend = "numdumb"
- with pytest.raises(NotImplementedError):
- nwt_poly.partial_diff(0, backend=unsupported_backend)
-
- with pytest.raises(NotImplementedError):
- nwt_poly.diff(
- order=np.ones(SpatialDimension, dtype=np.int_),
- backend=unsupported_backend,
- )
-
-
-def test_integrate_over_bounds_invalid_shape(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid shape."""
- # Create a Newton polynomial
- nwt_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = np.random.rand(SpatialDimension + 3, 2)
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- nwt_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_invalid_domain(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with bounds of invalid domain."""
- # Create a Newton polynomial
- nwt_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
-
- # Create bounds (outside the canonical domain of [-1, 1]^M)
- bounds = 2 * np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
-
- with pytest.raises(ValueError):
- nwt_poly.integrate_over(bounds)
-
-
-def test_integrate_over_bounds_equal(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with equal bounds (should be zero)."""
- # Create a Newton polynomial
- nwt_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
-
- # Create bounds (one of them has lb == ub)
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
- idx = np.random.choice(SpatialDimension)
- bounds[idx, 0] = bounds[idx, 1]
-
- # Compute the integral
- ref = 0.0
- value = nwt_poly.integrate_over(bounds)
-
- # Assertion
- assert np.isclose(ref, value)
-
-
-def test_integrate_over_bounds_flipped(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration with specified and valid bounds."""
- # Create a Newton polynomial
- nwt_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
-
- # Compute the integral
- value_1 = nwt_poly.integrate_over()
-
- # Flip bounds
- bounds = np.ones((SpatialDimension, 2))
- bounds[:, 0] *= -1
- bounds[:, [0, 1]] = bounds[:, [1, 0]]
-
- # Compute the integral with flipped bounds
- value_2 = nwt_poly.integrate_over(bounds)
-
- if np.mod(SpatialDimension, 2) == 1:
- # Odd spatial dimension flips the sign
- assert np.isclose(value_1, -1 * value_2)
- else:
- assert np.isclose(value_1, value_2)
-
-
-def test_integrate_over(
- SpatialDimension, PolyDegree, LpDegree
-):
- """Test polynomial integration in different basis (sanity check)."""
- # Create a Canonical polynomial
- nwt_poly = build_random_newton_polynom(
- SpatialDimension, PolyDegree, LpDegree
- )
-
- # Transform to other polynomial bases
- lag_poly = NewtonToLagrange(nwt_poly)()
- can_poly = NewtonToCanonical(nwt_poly)()
-
- # Compute the integral
- value_nwt = nwt_poly.integrate_over()
- value_lag = lag_poly.integrate_over()
- # NOTE: Canonical integration won't work in high degree
- value_can = can_poly.integrate_over()
-
- # Assertions
- assert np.isclose(value_nwt, value_lag)
- assert np.isclose(value_nwt, value_can)
-
- # Create bounds
- bounds = np.random.rand(SpatialDimension, 2)
- bounds[:, 0] *= -1
-
- # Compute the integral with bounds
- value_lag = lag_poly.integrate_over(bounds)
- value_nwt = nwt_poly.integrate_over(bounds)
- value_can = can_poly.integrate_over(bounds)
-
- # Assertions
- assert np.isclose(value_nwt, value_lag)
- assert np.isclose(value_nwt, value_can)