diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0da951fbf..d00ec3946 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,24 +12,36 @@ env: official_container_repository: ghcr.io/evalf/nutils jobs: build-python-package: - name: Build Python package - runs-on: ubuntu-20.04 + name: 'Build Python package on ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - {os: ubuntu-latest} + - {os: macos-latest, build-args: --no-sdist} + - {os: windows-latest, build-args: --no-sdist} steps: - name: Checkout uses: actions/checkout@v2 - - name: Install build dependencies - run: python3 -m pip install setuptools wheel - - name: Build package - run: | - # To make the wheels reproducible, set the timestamp of the (files in - # the) generated wheels to the date of the commit. - export SOURCE_DATE_EPOCH=`git show -s --format=%ct` - python3 setup.py sdist bdist_wheel + - uses: actions/setup-python@v2 + if: matrix.os == 'windows-latest' + with: + python-version: '3.10' + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + default: true + - name: Build wheels + uses: messense/maturin-action@v1 + with: + args: --release ${{ matrix.build-args }} - name: Upload package artifacts uses: actions/upload-artifact@v2 with: name: python-package - path: dist/ + path: target/wheels if-no-files-found: error test: needs: build-python-package @@ -49,7 +61,6 @@ jobs: - {name: "mkl matrix parallel", os: ubuntu-latest, python-version: "3.10", matrix-backend: mkl, nprocs: 2} - {name: "parallel", os: ubuntu-latest, python-version: "3.10", matrix-backend: numpy, nprocs: 2} - {name: "numpy 1.17", os: ubuntu-latest, python-version: "3.7", matrix-backend: numpy, nprocs: 1, numpy-version: ==1.17} - - {name: "tensorial", os: ubuntu-latest, python-version: "3.10", matrix-backend: numpy, nprocs: 1, tensorial: test} fail-fast: false env: NUTILS_MATRIX: ${{ matrix.matrix-backend }} @@ -67,7 +78,7 @@ jobs: - name: Move nutils directory run: mv nutils _nutils - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Download Python package artifact @@ -86,7 +97,7 @@ jobs: python -um pip install --upgrade --upgrade-strategy eager wheel python -um pip install --upgrade --upgrade-strategy eager coverage treelog stringly meshio numpy$_numpy_version # Install Nutils from `dist` dir created in job `build-python-package`. - python -um pip install --no-index --find-links ./dist nutils + python -um pip install --no-index --find-links ./dist --only-binary :all: nutils - name: Install Scipy if: ${{ matrix.matrix-backend == 'scipy' }} run: python -um pip install --upgrade --upgrade-strategy eager scipy @@ -134,21 +145,38 @@ jobs: python -um pip install --upgrade --upgrade-strategy eager wheel python -um pip install --upgrade --upgrade-strategy eager treelog stringly matplotlib scipy pillow numpy git+https://github.com/evalf/nutils-SI.git # Install Nutils from `dist` dir created in job `build-python-package`. - python -um pip install --no-index --find-links ./dist nutils + python -um pip install --no-index --find-links ./dist --only-binary :all: nutils - name: Test run: python -um unittest discover -b -q -t . -s examples test-sphinx: name: Test building docs + needs: build-python-package runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - - name: Install dependencies + - name: Move nutils directory + run: mv nutils _nutils + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Download Python package artifact + uses: actions/download-artifact@v2 + with: + name: python-package + path: dist/ + - name: Install Nutils and dependencies + id: install + env: + _numpy_version: ${{ matrix.numpy-version }} run: | - python3 -um pip install setuptools wheel - python3 -um pip install --upgrade --upgrade-strategy eager .[docs] + python -um pip install --upgrade --upgrade-strategy eager wheel + python -um pip install --upgrade --upgrade-strategy eager Sphinx treelog stringly meshio numpy scipy matplotlib + # Install Nutils from `dist` dir created in job `build-python-package`. + python -um pip install --no-index --find-links ./dist --only-binary :all: nutils - name: Build docs - run: python3 setup.py build_sphinx --nitpicky --warning-is-error --keep-going + run: python3 -m sphinx -n -W --keep-going docs build/sphinx build-and-test-container-image: name: Build container image needs: build-python-package diff --git a/.gitignore b/.gitignore index cc88697bc..ed544328f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ __pycache__/ /dist/ /nutils.egg-info/ /.eggs/ +/target +Cargo.lock +/nutils/_rust.so diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..cde71ec79 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nutils" +version = "0.1.0" +authors = ["Evalf "] +edition = "2021" + +[lib] +name = "nutils" +crate-type = ["cdylib"] + +[package.metadata.maturin] +name = "nutils._rust" + +[dependencies] +approx = "0.5" +num = "0.4" +pyo3 = { version = "0.16", features = ["extension-module", "abi3-py37"] } +numpy = "0.16" diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..792d7efdb --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +develop: build + cp --reflink=auto target/debug/libnutils.so nutils/_rust.so + +develop-release: build-release + cp --reflink=auto target/release/libnutils.so nutils/_rust.so + +build: + cargo build + +build-release: + cargo build --release + +bench: + cargo +nightly bench --features bench + +bench-python-compare: + python3 -m pytest benches/ --benchmark-compare --benchmark-group-by=name + +test-rust: + cargo test + +docs-python: develop-release + python3 -m sphinx -n -W --keep-going -E -D html_theme=sphinx_rtd_theme docs build/sphinx/html + +.PHONY: build diff --git a/examples/adaptivity.py b/examples/adaptivity.py index 750c23956..a41573f43 100644 --- a/examples/adaptivity.py +++ b/examples/adaptivity.py @@ -39,12 +39,13 @@ def main(etype: str, btype: str, degree: int, nrefine: int): x, y = geom - .5 exact = (x**2 + y**2)**(1/3) * numpy.cos(numpy.arctan2(y+x, y-x) * (2/3)) - domain = domain.trim(exact-1e-15, maxrefine=0) + #domain = domain.select(exact, ischeme='gauss0') linreg = util.linear_regressor() for irefine in treelog.iter.fraction('level', range(nrefine+1)): if irefine: + basis = domain.basis(btype, degree=degree) refdom = domain.refined refbasis = refdom.basis(btype, degree=degree) ns.add_field('vref', refbasis) @@ -52,7 +53,11 @@ def main(etype: str, btype: str, degree: int, nrefine: int): res -= refdom.boundary.integral('vref ∇_k(u) n_k dS' @ ns, degree=degree*2) indicator = res.derivative('vref').eval(**args) supp = refbasis.get_support(indicator**2 > numpy.mean(indicator**2)) - domain = domain.refined_by(refdom.transforms[supp]) + if hasattr(domain, 'coord_system'): + domain = domain.refined_by(refdom.coord_system.trans_to(domain.coord_system).apply_indices(supp)) + else: + domain = domain.refined_by(refdom.transforms[supp]) + ns = Namespace() ns.x = geom @@ -61,7 +66,9 @@ def main(etype: str, btype: str, degree: int, nrefine: int): ns.uexact = exact ns.du = 'u - uexact' - sqr = domain.boundary['trimmed'].integral('u^2 dS' @ ns, degree=degree*2) + #sqr = domain.boundary['trimmed'].integral('u^2 dS' @ ns, degree=degree*2) + sqr = domain.boundary['right,bottom'].integral('u^2 dS' @ ns, degree=degree*2) + #sqr = domain.boundary.select(x - y, 'gauss0').integral('u^2 dS' @ ns, degree=degree*2) cons = solver.optimize('u,', sqr, droptol=1e-15) sqr = domain.boundary.integral('du^2 dS' @ ns, degree=7) diff --git a/examples/coil.py b/examples/coil.py index 0cd6b586d..a50bf0b94 100644 --- a/examples/coil.py +++ b/examples/coil.py @@ -65,7 +65,7 @@ def main(nelems: int = 50, degree: int = 3, freq: float = 0., nturns: int = 1, r # domain is mapped from [0,1] to [0,inf) using an arctanh transform. Finally, # a Neumann boundary condition is used at z=0 to obtain symmetry in z=0. - RZ, ns.rz0 = mesh.rectilinear([numpy.linspace(0, 1, nelems)]*2, space='RZ') + RZ, ns.rz0 = mesh.rectilinear([numpy.linspace(0, 1, nelems)]*2, spaces=('R', 'Z')) REV, ns.θ = mesh.line([-numpy.pi, numpy.pi], bnames=['start', 'end'], space='Θ') REV0 = REV.refined[:1].boundary['end'].sample('bezier', 2) ns.rz = numpy.arctanh(ns.rz0) * 2 * rcoil diff --git a/nutils/debug_flags.py b/nutils/debug_flags.py index cd6f51eb9..0bd95fd14 100644 --- a/nutils/debug_flags.py +++ b/nutils/debug_flags.py @@ -7,6 +7,7 @@ sparse = _env.pop('sparse', _all or __debug__) # check sparse chunks in evaluable lower = _env.pop('lower', _all or __debug__) # check lowered shape, dtype in function evalf = _env.pop('evalf', _all) # check evaluated arrays in evaluable +hierarchical = _env.pop('hierarchical', _all) # check hierarchical topologies if _env: warnings.warn('unused debug flags: {}'.format(', '.join(_env))) diff --git a/nutils/element.py b/nutils/element.py index a4a951e55..59435cf91 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -8,7 +8,8 @@ system, and provide pointsets for purposes of integration and sampling. """ -from . import util, numeric, cache, transform, warnings, types, points +from . import util, numeric, cache, transform, warnings, types, points, _rust +from ._rust import CoordSystem import numpy import re import math @@ -353,6 +354,15 @@ def edge_transforms(self): assert self.ndims > 0 return tuple(transform.SimplexEdge(self.ndims, i) for i in range(self.ndims+1)) + def uniform_edges_coord_system(self, coord_system: CoordSystem, offset: int) -> CoordSystem: + if self.ndims == 1: + s = _rust.Simplex.line + elif self.ndims == 2: + s = _rust.Simplex.triangle + else: + raise NotImplementedError + return coord_system.edges(s, offset) + @property def child_refs(self): return tuple([self] * (2**self.ndims)) @@ -361,6 +371,15 @@ def child_refs(self): def child_transforms(self): return tuple(transform.SimplexChild(self.ndims, ichild) for ichild in range(2**self.ndims)) + def uniform_children_coord_system(self, coord_system: CoordSystem, offset: int) -> CoordSystem: + if self.ndims == 1: + s = _rust.Simplex.line + elif self.ndims == 2: + s = nutils._rust.Simplex.triangle + else: + raise NotImplementedError + return coord_system.children(s, offset) + @property def ribbons(self): return tuple(((iedge1, iedge2), (iedge2+1, iedge1)) for iedge1 in range(self.ndims+1) for iedge2 in range(iedge1, self.ndims)) diff --git a/nutils/elementseq.py b/nutils/elementseq.py index ffd49b3e9..4a88cdef0 100644 --- a/nutils/elementseq.py +++ b/nutils/elementseq.py @@ -3,6 +3,7 @@ from . import types, numeric, util from .element import Reference from .pointsseq import PointsSequence +from ._rust import CoordSystem from typing import Tuple, Sequence, Iterable, Iterator, Optional, Union, overload import abc import itertools @@ -122,10 +123,7 @@ def __getitem__(self, index): if numeric.isint(index): return self.get(index) elif isinstance(index, slice): - index = range(len(self))[index] - if index == range(len(self)): - return self - return self.take(numpy.arange(index.start, index.stop, index.step)) + return self.slice(index) elif numeric.isintarray(index): return self.take(index) elif numeric.isboolarray(index): @@ -175,6 +173,25 @@ def get(self, index: int) -> Reference: raise NotImplementedError + def slice(self, __s: slice) -> 'References': + '''Return a slice of this sequence. + + Parameters + ---------- + s : :class:`slice` + The slice. + + Returns + ------- + :class:`References` + The slice. + ''' + + start, stop, step = __s.indices(len(self)) + if start == 0 and stop == len(self) and step == 1: + return self + return self.take(numpy.arange(start, stop, step)) + def take(self, indices: numpy.ndarray) -> 'References': '''Return a selection of this sequence. @@ -295,6 +312,9 @@ def children(self) -> 'References': return _Derived(self, 'child_refs', self.ndims) + def children_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + raise NotImplementedError + @property def edges(self) -> 'References': '''Return the sequence of edge references. @@ -309,6 +329,9 @@ def edges(self) -> 'References': return _Derived(self, 'edge_refs', self.ndims-1) + def edges_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + raise NotImplementedError + @property def isuniform(self) -> 'bool': '''``True`` if all reference in this sequence are equal.''' @@ -411,10 +434,16 @@ def product(self, other: References) -> References: def children(self) -> References: return References.from_iter(self.item.child_refs, self.ndims).repeat(len(self)) + def children_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + return self.item.uniform_children_coord_system(coord_system, 0) + @property def edges(self) -> References: return References.from_iter(self.item.edge_refs, self.ndims-1).repeat(len(self)) + def edges_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + return self.item.uniform_edges_coord_system(coord_system, 0) + @property def isuniform(self) -> bool: return True @@ -562,10 +591,20 @@ def compress(self, mask: numpy.ndarray) -> References: def children(self) -> References: return self.sequence1.children.chain(self.sequence2.children) + def children_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + coord_system1 = self.sequence1.children_coord_system(coord_system.slice(slice(0, len(self.sequence1)))) + coord_system2 = self.sequence2.children_coord_system(coord_system.slice(slice(len(self.sequence1), None))) + return coord_system1.concat(coord_system2) + @property def edges(self) -> References: return self.sequence1.edges.chain(self.sequence2.edges) + def edges_coord_system(self, coord_system: CoordSystem) -> CoordSystem: + coord_system1 = self.sequence1.edges_coord_system(coord_system.slice(slice(0, len(self.sequence1)))) + coord_system2 = self.sequence2.edges_coord_system(coord_system.slice(slice(len(self.sequence1), None))) + return coord_system1.concat(coord_system2) + def getpoints(self, ischeme: str, degree: int) -> PointsSequence: return self.sequence1.getpoints(ischeme, degree).chain(self.sequence2.getpoints(ischeme, degree)) diff --git a/nutils/evaluable.py b/nutils/evaluable.py index d82ce2631..1cbfc3ddb 100644 --- a/nutils/evaluable.py +++ b/nutils/evaluable.py @@ -990,7 +990,7 @@ def _unaligned(self): _inflations = () def _derivative(self, var, seen): - if self.dtype in (bool, int) or var not in self.dependencies: + if self.dtype in (bool, int) or var not in self.arguments: return Zeros(self.shape + var.shape, dtype=self.dtype) raise NotImplementedError('derivative not defined for {}'.format(self.__class__.__name__)) @@ -1832,13 +1832,13 @@ def _inflations(self): continue parts2 = func2_inflations[axis] dofmaps = set(parts1) | set(parts2) - if (len(parts1) < len(dofmaps) and len(parts2) < len(dofmaps) # neither set is a subset of the other; total may be dense - and self.shape[axis].isconstant and all(dofmap.isconstant for dofmap in dofmaps)): - mask = numpy.zeros(int(self.shape[axis]), dtype=bool) - for dofmap in dofmaps: - mask[dofmap.eval()] = True - if mask.all(): # axis adds up to dense - continue + #if (len(parts1) < len(dofmaps) and len(parts2) < len(dofmaps) # neither set is a subset of the other; total may be dense + # and self.shape[axis].isconstant and all(dofmap.isconstant for dofmap in dofmaps)): + # mask = numpy.zeros(int(self.shape[axis]), dtype=bool) + # for dofmap in dofmaps: + # mask[dofmap.eval()] = True + # if mask.all(): # axis adds up to dense + # continue inflations.append((axis, types.frozendict((dofmap, util.sum(parts[dofmap] for parts in (parts1, parts2) if dofmap in parts)) for dofmap in dofmaps))) return tuple(inflations) @@ -2300,6 +2300,15 @@ class FloorDivide(Pointwise): __slots__ = () evalf = numpy.floor_divide + def _intbounds_impl(self): + dl, du = self.args[1]._intbounds + if dl != du or not isinstance(dl, int) or dl <= 0: + return super()._intbounds_impl() + nl, nu = self.args[0]._intbounds + l = nl // dl if isinstance(nl, int) else nl + u = nu // dl if isinstance(nu, int) else nu + return l, u + class Absolute(Pointwise): __slots__ = () @@ -3878,6 +3887,73 @@ def _intbounds_impl(self): return 0, upper_length - 1 +class TransformCoords(Array): + + def __init__(self, trans, index: Array, coords: Array): + if index.dtype != int or index.ndim != 0: + raise ValueError('argument `index` must be a scalar, integer `nutils.evaluable.Array`') + imin, imax = index._intbounds + if imin < 0 or imax >= trans.from_len: + raise ValueError('argument `index` is out of bounds') + if coords.dtype != float: + raise ValueError('argument `coords` must be a real-valued array with at least one axis') + self._trans = trans + self._index = index + self._coords = coords + super().__init__(args=[index, coords], shape=(*coords.shape[:-1], trans.to_dim), dtype=float) + + def evalf(self, index, coords): + return self._trans.apply(index.__index__(), coords)[1] + + def _derivative(self, var, seen): + linear = TransformBasis(self._trans, self._index)[:,:self._trans.from_dim] + dcoords = derivative(self._coords, var, seen) + return einsum('ij,AjB->AiB', linear, dcoords, A=self._coords.ndim - 1, B=var.ndim) + + def _simplified(self): + if self._trans.is_index_map: + return self._coords + + +class TransformIndex(Array): + + def __init__(self, trans, index: Array): + if index.dtype != int or index.ndim != 0: + raise ValueError('argument `index` must be a scalar, integer `nutils.evaluable.Array`') + imin, imax = index._intbounds + if imin < 0 or imax >= trans.from_len: + raise ValueError('argument `index` is out of bounds') + self._trans = trans + self._index = index + super().__init__(args=[index], shape=(), dtype=int) + + def evalf(self, index): + return numpy.array(self._trans.apply_index(index.__index__())) + + def _intbounds_impl(self): + return 0, self._trans.to_len - 1 + + def _simplified(self): + if self._trans.is_identity: + return self._index + + +class TransformBasis(Array): + + def __init__(self, trans, index: Array): + if index.dtype != int or index.ndim != 0: + raise ValueError('argument `index` must be a scalar, integer `nutils.evaluable.Array`') + self._trans = trans + super().__init__(args=[index], shape=(trans.to_dim, trans.to_dim), dtype=float) + + def evalf(self, index): + return self._trans.basis(index.__index__()) + + def _simplified(self): + if self._trans.basis_is_constant: + return asarray(self._trans.basis(0)) + + class _LoopIndex(Argument): __slots__ = 'length' @@ -4360,6 +4436,7 @@ def ln(x): def divmod(x, y): + raise ValueError div = FloorDivide(*_numpy_align(x, y)) mod = x - div * y return div, mod diff --git a/nutils/function.py b/nutils/function.py index 45dd42c5e..da077eb65 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -6,8 +6,7 @@ from typing import Tuple, Union, Type, Callable, Sequence, Any, Optional, Iterator, Iterable, Dict, Mapping, List, FrozenSet, NamedTuple from . import evaluable, numeric, util, types, warnings, debug_flags, sparse -from .transform import EvaluableTransformChain -from .transformseq import Transforms +from ._rust import CoordSystem import builtins import numpy import functools @@ -19,6 +18,66 @@ DType = Type[Union[bool, int, float, complex]] _dtypes = bool, int, float, complex +class Bound: + + def __init__(self, reference: Dict[str, CoordSystem], coord_systems: Tuple[CoordSystem, ...], index: evaluable.Array, coords: Optional[evaluable.Array]): + if not isinstance(reference, dict) or not all(isinstance(key, str) and isinstance(val, CoordSystem) for key, val in reference.items()): + raise ValueError('argument `reference`: expected a `dict` or `str` to `CoordSystem`s') + elif not reference: + raise ValueError('argument `reference` must not be empty') + + if not isinstance(coord_systems, tuple) or not all(isinstance(c, CoordSystem) for c in coord_systems): + raise ValueError('argument `coord_systems`: expected a `tuple` of `CoordSystem`s') + if len(coord_systems) == 0: + raise ValueError('argument `coord_systems` must not be empty') + n = len(coord_systems[0]) + dim = coord_systems[0].dim + if not all(len(c) == n and c.dim == dim for c in coord_systems[1:]): + raise ValueError('argument `coord_systems`: the dimensions vary') + + if index.dtype != int or index.ndim != 0: + raise ValueError('argument `index` must be a scalar, integer `nutils.evaluable.Array`') + + self.reference = reference + self.spaces = tuple(reference) + self.coord_systems = coord_systems + self.index = index + + if coords is not None: + if not isinstance(coords, evaluable.Array): + raise ValueError('argument `coords` must be an `nutils.evaluable.Array`') + if coords.ndim == 0: + raise ValueError('argument `coords` must have at least one axis') + if not evaluable.equalindex(coords.shape[-1], dim): + raise ValueError('argument `coords`: the last axis does not match the dimension of the coordinate system') + deriv = evaluable.Diagonalize(evaluable.ones(coords.shape)) + coords = evaluable.WithDerivative(coords, self._tip_derivative_target, deriv) + self.coords = coords + + def without_points(self) -> 'Bound': + return Bound(self.reference, self.coord_systems, self.index, None) + + def opposite(self) -> 'Bound': + if len(self.coord_systems) > 1: + return Bound(self.reference, self.coord_systems[::-1], self.index, self.coords) + else: + return self + + def into_lower_args(self) -> 'LowerArgs': + if self.coords is not None: + points_shape = self.coords.shape[:-1] + else: + points_shape = () + return LowerArgs(points_shape, (self,)) + + @property + def dim(self): + return self.coord_systems[0].dim + + @property + def _tip_derivative_target(self): + return evaluable.IdentifierDerivativeTarget(('tip', self.spaces), (self.dim,)) + class LowerArgs(NamedTuple): '''Arguments for Lowerable.lower @@ -27,35 +86,132 @@ class LowerArgs(NamedTuple): points_shape : :class:`tuple` of scalar, integer :class:`nutils.evaluable.Array` The shape of the leading points axes that are to be added to the lowered :class:`nutils.evaluable.Array`. - transform_chains : mapping of :class:`str` to :class:`nutils.transform.EvaluableTransformChain` pairs - coordinates : mapping of :class:`str` to :class:`nutils.evaluable.Array` objects - The coordinates at which the function will be evaluated. + bounds : :class:`tuple` of :class:`Bound` s + A tuple of bounds of one or more spaces to a coordinate systems and + optionally an opposite coordinate system. ''' points_shape: Tuple[evaluable.Array, ...] - transform_chains: Mapping[str, Tuple[EvaluableTransformChain, EvaluableTransformChain]] - coordinates: Mapping[str, evaluable.Array] + bounds: Tuple[Bound, ...] def consistent(self): return all( - evaluable.equalshape(coords.shape[:-1], self.points_shape) - and space in self.transform_chains - for space, coords in self.coordinates.items()) - - @classmethod - def for_space(cls, space: str, transform_chains: Tuple[EvaluableTransformChain, EvaluableTransformChain], coordinates: evaluable.Array) -> 'LowerArgs': - return cls(coordinates.shape[:-1], {space: transform_chains}, {space: coordinates}) + evaluable.equalshape(bound.coords.shape[:-1], self.points_shape) + for bound in self.bounds + if bound.coords is not None) def __or__(self, other: 'LowerArgs') -> 'LowerArgs': - duplicates = set(self.transform_chains) & set(other.transform_chains) + duplicates = frozenset(self.spaces) & frozenset(other.spaces) if duplicates: raise ValueError(f'Nested integrals or samples in the same space: {", ".join(sorted(duplicates))}.') - transform_chains = self.transform_chains.copy() - transform_chains.update(other.transform_chains) - coordinates = {space: evaluable.Transpose.to_end(evaluable.appendaxes(coords, other.points_shape), coords.ndim - 1) for space, coords in self.coordinates.items()} - coordinates.update({space: evaluable.prependaxes(coords, self.points_shape) for space, coords in other.coordinates.items()}) + bounds = [] + for bound in self.bounds: + coords = evaluable.Transpose.to_end(evaluable.appendaxes(bound.coords, other.points_shape), bound.coords.ndim - 1) + bounds.append(Bound(bound.reference, bound.coord_systems, bound.index, coords)) + for bound in other.bounds: + coords = evaluable.prependaxes(bound.coords, self.points_shape) + bounds.append(Bound(bound.reference, bound.coord_systems, bound.index, coords)) points_shape = self.points_shape + other.points_shape - return LowerArgs(points_shape, transform_chains, coordinates) + return LowerArgs(points_shape, bounds) + + @property + def reference(self) -> Dict[str, CoordSystem]: + reference = {} + for bound in self.bounds: + reference.update(bound.reference) + return reference + + @property + def spaces(self) -> FrozenSet[str]: + return frozenset(space for bound in self.bounds for space in bound.spaces) + + def without_points(self) -> 'LowerArgs': + return LowerArgs((), tuple(bound.without_points() for bound in self.bounds)) + + def opposite(self) -> 'LowerArgs': + return LowerArgs(self.points_shape, tuple(bound.opposite() for bound in self.bounds)) + + def index_coords_at(self, spaces: Tuple[str, ...], coord_system: CoordSystem) -> Tuple[evaluable.Array, Optional[evaluable.Array]]: + if not isinstance(spaces, tuple) or not all(isinstance(space, str) for space in spaces): + raise ValueError('arguent `spaces`: expected a `tuple` of `str`') + missing = frozenset(spaces) - self.spaces + if missing: + raise ValueError('missing spaces: {}'.format(', '.join(sorted(missing)))) + if not isinstance(coord_system, CoordSystem): + raise ValueError('argument `coord_system`: expected an `CoordSystem`') + + orig_len = len(coord_system) + orig_dim = coord_system.dim + + # filter bounds that have at least one space of interest and order to match `spaces` as good as possible + bounds = tuple(bound for bound in self.bounds if not set(bound.spaces).isdisjoint(spaces)) + bounds = tuple(sorted(bounds, key=lambda bound: builtins.min(spaces.index(space) for space in bound.spaces if space in spaces))) + + # extend `coord_system` + bound_reference = {space: ref for bound in bounds for space, ref in bound.reference.items()} + bound_spaces = tuple(bound_reference) + for i in range(len(bound_spaces) - len(spaces) + 1): + if bound_spaces[i:i + len(spaces)] == spaces: + break + else: + raise ValueError('Cannot create a transformation for a non-contiguous subset of bound spaces.') + stride_before = 1 + dim_before = 0 + for space in reversed(bound_spaces[:i]): + ref = bound_reference[space] + coord_system = ref * coord_system + stride_before *= len(ref) + dim_before += ref.dim + stride_after = 1 + dim_after = 0 + for space in bound_spaces[i + len(spaces):]: + ref = bound_reference[space] + coord_system *= ref + stride_after *= len(ref) + dim_after += ref.dim + assert orig_len * stride_before * stride_after == len(coord_system) + assert orig_dim + dim_before + dim_after == coord_system.dim + + bound_coord_system = util.product(bound.coord_systems[0] for bound in bounds) + #if coord_system == bound_coord_system: + # trans = None + #else: + trans = bound_coord_system.trans_to(coord_system) + + bound_index = bounds[0].index + for bound in bounds[1:]: + bound_index = bound_index * len(bound.coord_systems[0]) + bound.index + index = evaluable.TransformIndex(trans, bound_index) + if stride_after != 1: + index = evaluable.FloorDivide(index, stride_after) + if stride_before != 1: + index = evaluable.Mod(index, orig_len) + + if any(bound.coords is None for bound in bounds): + return index, None + + bound_coords = evaluable.concatenate([bound.coords for bound in bounds], axis=-1) + coords = evaluable.TransformCoords(trans, bound_index, bound_coords) + coords = coords[..., dim_before:dim_before + orig_dim] + + bound_reference = util.product(coord_system for bound in bounds for coord_system in bound.reference.values()) + ref_trans = coord_system.trans_to(bound_reference) + L = evaluable.TransformBasis(ref_trans, index)[:,:coord_system.dim] + if bound_reference.dim == coord_system.dim: + Linv = evaluable.inverse(L) + else: + LTL = evaluable.einsum('ki,kj->ij', L, L) + Linv = evaluable.einsum('ik,jk->ij', evaluable.inverse(LTL), L) + Linv = evaluable.prependaxes(Linv, self.points_shape) + Linv = Linv[..., dim_before:dim_before + orig_dim,:] + offset = 0 + for bound in bounds: + for space, reference in bound.reference.items(): + target = _root_derivative_target(space, reference.dim) + coords = evaluable.WithDerivative(coords, target, Linv[...,offset:offset+reference.dim]) + offset += reference.dim + + return index, coords class Lowerable(Protocol): @@ -104,7 +260,7 @@ def _cache_lower(self, args: LowerArgs) -> evaluable.Array: cached_args, cached_result = getattr(self, '_ArrayMeta__cached_lower', (None, None)) if cached_args == args: return cached_result - missing_spaces = self.spaces - set(args.transform_chains) + missing_spaces = self.spaces - set(space for bound in args.bounds for space in bound.spaces) if missing_spaces: raise ValueError('Cannot lower {} because the following spaces are unspecified: {}.'.format(self, missing_spaces)) result = self._ArrayMeta__cache_lower_orig(args) @@ -214,7 +370,7 @@ def lower(self, args: LowerArgs) -> evaluable.Array: @util.cached_property def as_evaluable_array(self) -> evaluable.Array: - return self.lower(LowerArgs((), {}, {})) + return self.lower(LowerArgs((), ())) def __index__(self): if self.arguments or self.spaces: @@ -677,6 +833,7 @@ def lower(self, args: LowerArgs) -> evaluable.Array: evalargs = tuple(arg.lower(args) if isinstance(arg, Array) else evaluable.EvaluableConstant(arg) for arg in self._args) # type: Tuple[Union[evaluable.Array, evaluable.EvaluableConstant], ...] add_points_shape = tuple(map(evaluable.asarray, self.shape[:self._npointwise])) points_shape = args.points_shape + add_points_shape + # FIXME coordinates = {space: evaluable.Transpose.to_end(evaluable.appendaxes(coords, add_points_shape), coords.ndim-1) for space, coords in args.coordinates.items()} return _CustomEvaluable(type(self).__name__, self.evalf, self.partial_derivative, evalargs, self.shape[self._npointwise:], self.dtype, self.spaces, types.frozendict(self.arguments), LowerArgs(points_shape, types.frozendict(args.transform_chains), types.frozendict(coordinates))) @@ -805,7 +962,7 @@ def __init__(self, __arg: Array) -> None: self.arguments = __arg.arguments def lower(self, args: LowerArgs) -> evaluable.Array: - return self._arg.lower(LowerArgs((), args.transform_chains, {})) + return self._arg.lower(args.without_points()) class _Wrapper(Array): @@ -898,7 +1055,7 @@ def __init__(self, arg: Array, replacements: Dict[str, Array]) -> None: def lower(self, args: LowerArgs) -> evaluable.Array: arg = self._arg.lower(args) - replacements = {name: _WithoutPoints(value).lower(args) for name, value in self._replacements.items()} + replacements = {name: value.lower(args.without_points()) for name, value in self._replacements.items()} return evaluable.replace_arguments(arg, replacements) @@ -943,59 +1100,32 @@ def __init__(self, arg: Array, space: str) -> None: super().__init__(arg.shape, arg.dtype, arg.spaces, arg.arguments) def lower(self, args: LowerArgs) -> evaluable.Array: - oppargs = LowerArgs(args.points_shape, dict(args.transform_chains), args.coordinates) - oppargs.transform_chains[self._space] = args.transform_chains[self._space][::-1] - return self._arg.lower(oppargs) + return self._arg.lower(args.opposite()) -class _RootCoords(Array): - - def __init__(self, space: str, ndims: int) -> None: - self._space = space - super().__init__((ndims,), float, frozenset({space}), {}) - - def lower(self, args: LowerArgs) -> evaluable.Array: - inv_linear = evaluable.diagonalize(evaluable.ones(self.shape)) - inv_linear = evaluable.prependaxes(inv_linear, args.points_shape) - tip_coords = args.coordinates[self._space] - tip_coords = evaluable.WithDerivative(tip_coords, _tip_derivative_target(self._space, tip_coords.shape[-1]), evaluable.Diagonalize(evaluable.ones(tip_coords.shape))) - coords = args.transform_chains[self._space][0].apply(tip_coords) - return evaluable.WithDerivative(coords, _root_derivative_target(self._space, self.shape[0]), inv_linear) +class _IndexAtCoordSystem(Array): - -class _TransformsIndex(Array): - - def __init__(self, space: str, transforms: Transforms) -> None: - self._space = space - self._transforms = transforms - super().__init__((), int, frozenset({space}), {}) + def __init__(self, spaces: Tuple[str, ...], coord_system: CoordSystem) -> None: + self._spaces = spaces + self._coord_system = coord_system + super().__init__((), int, frozenset(spaces), {}) def lower(self, args: LowerArgs) -> evaluable.Array: - index, tail = args.transform_chains[self._space][0].index_with_tail_in(self._transforms) + index, coords = args.index_coords_at(self._spaces, self._coord_system) return evaluable.prependaxes(index, args.points_shape) -class _TransformsCoords(Array): +class _CoordsAtCoordSystem(Array): - def __init__(self, space: str, transforms: Transforms) -> None: - self._space = space - self._transforms = transforms - super().__init__((transforms.fromdims,), float, frozenset({space}), {}) + def __init__(self, spaces: Tuple[str, ...], coord_system: CoordSystem) -> None: + self._spaces = spaces + self._coord_system = coord_system + super().__init__((coord_system.dim,), float, frozenset(spaces), {}) def lower(self, args: LowerArgs) -> evaluable.Array: - index, tail = args.transform_chains[self._space][0].index_with_tail_in(self._transforms) - head = self._transforms.get_evaluable(index) - L = head.linear - if self._transforms.todims > self._transforms.fromdims: - LTL = evaluable.einsum('ki,kj->ij', L, L) - Linv = evaluable.einsum('ik,jk->ij', evaluable.inverse(LTL), L) - else: - Linv = evaluable.inverse(L) - Linv = evaluable.prependaxes(Linv, args.points_shape) - tip_coords = args.coordinates[self._space] - tip_coords = evaluable.WithDerivative(tip_coords, _tip_derivative_target(self._space, tip_coords.shape[-1]), evaluable.Diagonalize(evaluable.ones(tip_coords.shape))) - coords = tail.apply(tip_coords) - return evaluable.WithDerivative(coords, _root_derivative_target(self._space, self._transforms.todims), Linv) + index, coords = args.index_coords_at(self._spaces, self._coord_system) + assert coords is not None + return coords class _Derivative(Array): @@ -1013,10 +1143,6 @@ def lower(self, args: LowerArgs) -> evaluable.Array: return evaluable.derivative(arg, self._eval_var) -def _tip_derivative_target(space: str, dim: int) -> evaluable.DerivativeTargetBase: - return evaluable.IdentifierDerivativeTarget((space, 'tip'), (dim,)) - - def _root_derivative_target(space: str, dim: int) -> evaluable.DerivativeTargetBase: return evaluable.IdentifierDerivativeTarget((space, 'root'), (dim,)) @@ -1036,10 +1162,11 @@ def __init__(self, func: Array, geom: Array) -> None: def lower(self, args: LowerArgs) -> evaluable.Array: func = self._func.lower(args) geom = self._geom.lower(args) - ref_dim = builtins.sum(args.transform_chains[space][0].todims for space in self._geom.spaces) - if self._geom.shape[-1] != ref_dim: - raise Exception('cannot invert {}x{} jacobian'.format(self._geom.shape[-1], ref_dim)) - refs = tuple(_root_derivative_target(space, chain.todims) for space, (chain, opposite) in args.transform_chains.items() if space in self._geom.spaces) + ref_dims = {space: coord_system.dim for space, coord_system in args.reference.items() if space in self._geom.spaces} + sum_ref_dims = builtins.sum(ref_dims.values()) + if self._geom.shape[-1] != sum_ref_dims: + raise Exception('cannot invert {}x{} jacobian'.format(self._geom.shape[-1], sum_ref_dims)) + refs = tuple(_root_derivative_target(space, dim) for space, dim in ref_dims.items()) dfunc_dref = evaluable.concatenate([evaluable.derivative(func, ref) for ref in refs], axis=-1) dgeom_dref = evaluable.concatenate([evaluable.derivative(geom, ref) for ref in refs], axis=-1) dref_dgeom = evaluable.inverse(dgeom_dref) @@ -1062,10 +1189,11 @@ def __init__(self, func: Array, geom: Array) -> None: def lower(self, args: LowerArgs) -> evaluable.Array: func = self._func.lower(args) geom = self._geom.lower(args) - ref_dim = builtins.sum(args.transform_chains[space][0].fromdims for space in self._geom.spaces) - if self._geom.shape[-1] != ref_dim + 1: - raise ValueError('expected a {}d geometry but got a {}d geometry'.format(ref_dim + 1, self._geom.shape[-1])) - refs = tuple((_root_derivative_target if chain.todims == chain.fromdims else _tip_derivative_target)(space, chain.fromdims) for space, (chain, opposite) in args.transform_chains.items() if space in self._geom.spaces) + ref_dims = {space: coord_system.dim for bound in args.bounds for space, coord_system in bound.reference.items() if space in self._geom.spaces} + sum_ref_dims = builtins.sum(ref_dims.values()) + if self._geom.shape[-1] != sum_ref_dims + 1: + raise ValueError('expected a {}d geometry but got a {}d geometry'.format(sum_ref_dims + 1, self._geom.shape[-1])) + refs = tuple(_root_derivative_target(space, dim) for space, dim in ref_dims.items()) dfunc_dref = evaluable.concatenate([evaluable.derivative(func, ref) for ref in refs], axis=-1) dgeom_dref = evaluable.concatenate([evaluable.derivative(geom, ref) for ref in refs], axis=-1) dref_dgeom = evaluable.einsum('Ajk,Aik->Aij', dgeom_dref, evaluable.inverse(evaluable.grammium(dgeom_dref))) @@ -1090,15 +1218,22 @@ def __init__(self, geom: Array, tip_dim: Optional[int] = None) -> None: def lower(self, args: LowerArgs) -> evaluable.Array: geom = self._geom.lower(args) - tip_dim = builtins.sum(args.transform_chains[space][0].fromdims for space in self._geom.spaces) + + # filter bounds that have at least one space of interest + bounds = tuple(bound for bound in args.bounds if not set(bound.spaces).isdisjoint(self._geom.spaces)) + + bound_spaces = frozenset(space for bound in bounds for space in bound.reference) + if bound_spaces != self._geom.spaces: + raise NotImplementedError('jacobian subset bound spaces') + + tip_dim = builtins.sum(bound.dim for bound in bounds) if self._tip_dim is not None and self._tip_dim != tip_dim: raise ValueError('Expected a tip dimension of {} but got {}.'.format(self._tip_dim, tip_dim)) if self._geom.shape[-1] < tip_dim: raise ValueError('the dimension of the geometry cannot be lower than the dimension of the tip coords') if not self._geom.spaces: return evaluable.ones(geom.shape[:-1]) - tips = [_tip_derivative_target(space, chain.fromdims) for space, (chain, opposite) in args.transform_chains.items() if space in self._geom.spaces] - J = evaluable.concatenate([evaluable.derivative(geom, tip) for tip in tips], axis=-1) + J = evaluable.concatenate([evaluable.derivative(geom, bound._tip_derivative_target) for bound in bounds], axis=-1) return evaluable.sqrt_abs_det_gram(J) @@ -1111,8 +1246,16 @@ def __init__(self, geom: Array) -> None: def lower(self, args: LowerArgs) -> evaluable.Array: geom = self._geom.lower(args) - spaces_dim = builtins.sum(args.transform_chains[space][0].todims for space in self._geom.spaces) - normal_dim = spaces_dim - builtins.sum(args.transform_chains[space][0].fromdims for space in self._geom.spaces) + + # filter bounds that have at least one space of interest + bounds = tuple(bound for bound in args.bounds if not set(bound.spaces).isdisjoint(self._geom.spaces)) + + bound_spaces = frozenset(space for bound in bounds for space in bound.reference) + if bound_spaces != self._geom.spaces: + raise NotImplementedError('normal subset bound spaces') + + spaces_dim = builtins.sum(reference.dim for bound in bounds for reference in bound.reference.values()) + normal_dim = spaces_dim - builtins.sum(bound.dim for bound in bounds) if self._geom.shape[-1] != spaces_dim: raise ValueError('The dimension of geometry must equal the sum of the dimensions of the given spaces.') if normal_dim == 0: @@ -1121,18 +1264,18 @@ def lower(self, args: LowerArgs) -> evaluable.Array: raise ValueError('Cannot unambiguously compute the normal because the dimension of the normal space is larger than one.') tangents = [] normal = None - for space, (chain, opposite) in args.transform_chains.items(): - if space not in self._geom.spaces: - continue - rgrad = evaluable.derivative(geom, _root_derivative_target(space, chain.todims)) - if chain.todims == chain.fromdims: - # `chain.basis` is `eye(chain.todims)` + for bound in bounds: + rgrad = evaluable.concatenate([evaluable.derivative(geom, _root_derivative_target(space, reference.dim)) for space, reference in bound.reference.items()], axis=-1) + if bound.dim == builtins.sum(reference.dim for reference in bound.reference.values()): + # `trans.basis` is `eye(chain.todims)` tangents.append(rgrad) else: - assert normal is None and chain.todims == chain.fromdims + 1 - basis = evaluable.einsum('Aij,jk->Aik', rgrad, chain.basis) - tangents.append(basis[..., :chain.fromdims]) - normal = basis[..., chain.fromdims:] + reference = util.product(bound.reference.values()) + trans = bound.coord_systems[0].trans_to(reference) + assert normal is None and trans.to_dim == trans.from_dim + 1 + basis = evaluable.einsum('Aij,jk->Aik', rgrad, evaluable.TransformBasis(trans, bound.index)) + tangents.append(basis[..., :trans.from_dim]) + normal = basis[..., trans.from_dim:] assert normal is not None return evaluable.Normal(evaluable.concatenate((*tangents, normal), axis=-1)) @@ -1145,6 +1288,7 @@ def __init__(self, rgrad: Array) -> None: super().__init__(rgrad.shape[:-1], float, rgrad.spaces, rgrad.arguments) def lower(self, args: LowerArgs) -> evaluable.Array: + raise NotImplemented rgrad = self._rgrad.lower(args) if self._rgrad.shape[-2] == 2: normal = evaluable.stack([rgrad[..., 1, 0], -rgrad[..., 0, 0]], axis=-1) @@ -3293,17 +3437,12 @@ def isarray(__arg: Any) -> bool: return isinstance(__arg, Array) -def rootcoords(space: str, __dim: int) -> Array: - 'Return the root coordinates.' - return _RootCoords(space, __dim) - +def transforms_index(coord_system: CoordSystem, *spaces: str) -> Array: + return _IndexAtCoordSystem(spaces, coord_system) -def transforms_index(space: str, transforms: Transforms) -> Array: - return _TransformsIndex(space, transforms) - -def transforms_coords(space: str, transforms: Transforms) -> Array: - return _TransformsCoords(space, transforms) +def transforms_coords(coord_system: CoordSystem, *spaces: str) -> Array: + return _CoordsAtCoordSystem(spaces, coord_system) def Elemwise(__data: Sequence[numpy.ndarray], __index: IntoArray, dtype: DType) -> Array: @@ -3866,8 +4005,8 @@ def get_support(self, dof: Union[int, numpy.ndarray]) -> numpy.ndarray: def f_dofs_coeffs(self, index: evaluable.Array) -> Tuple[evaluable.Array, evaluable.Array]: indices = [] for n in reversed(self._transforms_shape[1:]): - index, ielem = evaluable.divmod(index, n) - indices.append(ielem) + indices.append(evaluable.mod(index, n)) + index = evaluable.FloorDivide(index, n) indices.append(index) indices.reverse() ranges = [evaluable.Range(evaluable.get(lengths_i, 0, index_i)) + evaluable.get(offsets_i, 0, index_i) @@ -3915,6 +4054,53 @@ def f_dofs_coeffs(self, index: evaluable.Array) -> Tuple[evaluable.Array, evalua return dofs, p_coeffs +class ProductBasis(Basis): + '''The product of two bases. + + Parameters + ---------- + basis1 : :class:`Basis` + basis2 : :class:`Basis` + ''' + + def __init__(self, basis1: Basis, basis2: Basis): + self._basis1 = basis1 + self._basis2 = basis2 + super().__init__( + basis1.ndofs * basis2.ndofs, + basis1.nelems * basis2.nelems, + basis1.index * basis2.ndofs + basis2.index, + numpy.concatenate([basis1.coords, basis2.coords], axis=0)) + + def lower(self, args: LowerArgs) -> evaluable.Array: + basis1 = self._basis1.lower(args) + basis2 = self._basis2.lower(args) + return evaluable.ravel(evaluable.insertaxis(basis1, -1, basis2.shape[-1]) * evaluable.insertaxis(basis2, -2, basis1.shape[-1]), -1) + + #@_int_or_vec_ielem + #def get_dofs(self, ielem: Union[int, numpy.ndarray]) -> numpy.ndarray: + # ielem1, ielem2 = builtins.divmod(ielem, self._basis2.nelems) + # dofs1 = self._basis1.get_dofs(ielem1) + # dofs2 = self._basis2.get_dofs(ielem2) + # return numpy.ravel(dofs1[:,None] * self._basis2.ndofs + dofs2[None,:]) + + @_int_or_vec_dof + def get_support(self, dof: Union[int, numpy.ndarray]) -> numpy.ndarray: + dof1, dof2 = builtins.divmod(dof, self._basis2.ndofs) + ielems1 = self._basis1.get_support(dof1) + ielems2 = self._basis2.get_support(dof2) + return numpy.ravel(ielems1[:,None] * self._basis2.nelems + ielems2[None,:]) + + def f_dofs_coeffs(self, index: evaluable.Array) -> Tuple[evaluable.Array, evaluable.Array]: + index1 = evaluable.FloorDivide(index, self._basis2.nelems) + index2 = evaluable.mod(index, self._basis2.nelems) + dofs1, coeffs1 = self._basis1.f_dofs_coeffs(index1) + dofs2, coeffs2 = self._basis2.f_dofs_coeffs(index2) + dofs = evaluable.ravel(evaluable.insertaxis(dofs1, 1, dofs2.shape[0]) * self._basis2.ndofs + evaluable.insertaxis(dofs2, 0, dofs1.shape[0]), 0) + coeffs = evaluable.PolyOuterProduct(coeffs1, coeffs2) + return dofs, coeffs + + def Namespace(*args, **kwargs): from .expression_v1 import Namespace return Namespace(*args, **kwargs) diff --git a/nutils/mesh.py b/nutils/mesh.py index 10d501d2a..0b6dcfb42 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -10,6 +10,7 @@ from .elementseq import References from .transform import TransformItem from .topology import Topology +from ._rust import CoordSystem from typing import Optional, Sequence, Tuple, Union import numpy import os @@ -19,70 +20,32 @@ import treelog as log import io import contextlib +from collections import OrderedDict _ = numpy.newaxis # MESH GENERATORS -@log.withcontext -def rectilinear(richshape: Sequence[Union[int, Sequence[float]]], periodic: Sequence[int] = (), name: Optional[str] = None, space: str = 'X', root: Optional[TransformItem] = None) -> Tuple[Topology, function.Array]: - 'rectilinear mesh' - - verts = [numpy.arange(v + 1) if numeric.isint(v) else v for v in richshape] - shape = [len(v) - 1 for v in verts] - ndims = len(shape) - - if name is not None: - warnings.deprecation('Argument `name` is deprecated; use `root` with a `TransformItem` instead.') - if root is not None: - raise ValueError('Arguments `name` and `root` cannot be used simultaneously.') - root = transform.Index(hash(name)) - elif root is None: - root = transform.Index(ndims, 0) - - axes = [transformseq.DimAxis(i=0, j=n, mod=n if idim in periodic else 0, isperiodic=idim in periodic) for idim, n in enumerate(shape)] - topo = topology.StructuredTopology(space, root, axes) - - funcsp = topo.basis('spline', degree=1, periodic=()) - coords = numeric.meshgrid(*verts).reshape(ndims, -1) - geom = (funcsp * coords).sum(-1) - - return topo, geom - - -_oldrectilinear = rectilinear # reference for internal unittests - - -def line(nodes: Union[int, Sequence[float]], periodic: bool = False, bnames: Optional[Sequence[Tuple[str, str]]] = None, *, name: Optional[str] = None, space: str = 'X', root: Optional[TransformItem] = None) -> Tuple[Topology, function.Array]: - if name is not None: - warnings.deprecation('Argument `name` is deprecated; use `root` with a `transform.transformitem` instead.') - if root is not None: - raise ValueError('Arguments `name` and `root` cannot be used simultaneously.') - root = transform.Index(hash(name)) - elif root is None: - root = transform.Index(1, 0) +def line(nodes: Union[int, Sequence[float]], periodic: bool = False, bnames: Optional[Tuple[str, str]] = None, *, space: str = 'X') -> Tuple[Topology, function.Array]: if isinstance(nodes, int): nodes = numpy.arange(nodes + 1) - domain = topology.StructuredLine(space, root, 0, len(nodes) - 1, periodic=periodic, bnames=bnames) - geom = domain.basis('std', degree=1, periodic=[]).dot(nodes) + domain = Topology.line(space, len(nodes) - 1, periodic=periodic, bnames=bnames) + geom = domain.basis('std', degree=1, periodic=()) @ nodes return domain, geom -def newrectilinear(nodes: Sequence[Union[int, Sequence[float]]], periodic: Optional[Sequence[int]] = None, name: Optional[str] = None, bnames=[['left', 'right'], ['bottom', 'top'], ['front', 'back']], spaces: Optional[Sequence[str]] = None, root: Optional[TransformItem] = None) -> Tuple[Topology, function.Array]: +def rectilinear(nodes: Sequence[Union[int, Sequence[float]]], periodic: Optional[Sequence[int]] = None, bnames=[['left', 'right'], ['bottom', 'top'], ['front', 'back']], spaces: Optional[Sequence[str]] = None) -> Tuple[Topology, function.Array]: if periodic is None: periodic = [] if not spaces: spaces = 'XYZ' if len(nodes) <= 3 else map('R{}'.format, range(len(nodes))) else: assert len(spaces) == len(nodes) - domains, geoms = zip(*(line(nodesi, i in periodic, bnamesi, name=name, space=spacei, root=root) for i, (nodesi, bnamesi, spacei) in enumerate(zip(nodes, tuple(bnames)+(None,)*len(nodes), spaces)))) + domains, geoms = zip(*(line(nodesi, i in periodic, bnamesi, space=spacei) for i, (nodesi, bnamesi, spacei) in enumerate(zip(nodes, tuple(bnames)+(None,)*len(nodes), spaces)))) return util.product(domains), numpy.stack(geoms) -if os.environ.get('NUTILS_TENSORIAL'): - def rectilinear(richshape: Sequence[Union[int, Sequence[float]]], periodic: Sequence[int] = (), name: Optional[str] = None, space: str = 'X', root: Optional[TransformItem] = None) -> Tuple[Topology, function.Array]: - spaces = tuple(space+str(i) for i in range(len(richshape))) - return newrectilinear(richshape, periodic, name=name, spaces=spaces, root=root) +newrectilinear = rectilinear @log.withcontext @@ -637,10 +600,8 @@ def unitsquare(nelems, etype): The geometry function. ''' - space = 'X' - if etype == 'square': - topo, geom = rectilinear([nelems, nelems], space=space) + topo, geom = rectilinear([nelems, nelems]) return topo, geom / nelems elif etype in ('triangle', 'mixed'): @@ -651,13 +612,14 @@ def unitsquare(nelems, etype): v = numpy.arange(nelems+1, dtype=float) coords = numeric.meshgrid(v, v).reshape(2, -1).T transforms = transformseq.IndexTransforms(2, len(simplices)) - topo = topology.SimplexTopology(space, simplices, transforms, transforms) + topo = Topology.simplex('X', simplices) if etype == 'mixed': references = list(topo.references) square = element.getsimplex(1)**2 connectivity = list(topo.connectivity) isquares = [i * nelems + j for i in range(nelems) for j in range(nelems) if i % 2 == j % 3] + nsquares = len(isquares) dofs = list(simplices) for n in sorted(isquares, reverse=True): i, j = divmod(n, nelems) @@ -665,9 +627,26 @@ def unitsquare(nelems, etype): connectivity[n*2:(n+1)*2] = numpy.concatenate(connectivity[n*2:(n+1)*2])[[3, 2, 4, 1] if i % 2 == j % 2 else [3, 2, 0, 5]], connectivity = [c-numpy.greater(c, n*2) for c in connectivity] dofs[n*2:(n+1)*2] = numpy.unique([*dofs[n*2], *dofs[n*2+1]]), + reorder = numpy.argsort([len(c) == 4 for c in connectivity]) + renumber = numpy.concatenate([numpy.argsort(reorder), [-1]]) + connectivity = tuple(types.frozenarray(numpy.take(renumber, connectivity[i]), copy=False) for i in reorder) + ntriangles = len(connectivity) - nsquares + assert all(len(c) == 3 for c in connectivity[:ntriangles]) + assert all(len(c) == 4 for c in connectivity[ntriangles:]) coords = coords[numpy.argsort(numpy.unique(numpy.concatenate(dofs), return_index=True)[1])] - transforms = transformseq.IndexTransforms(2, len(connectivity)) - topo = topology.ConnectedTopology(space, References.from_iter(references, 2), transforms, transforms, connectivity) + coord_system = CoordSystem(2, len(connectivity)) + ref_coord_system = OrderedDict(X=coord_system) + triangles = Topology( + ref_coord_system, + References.uniform(element.getsimplex(2), ntriangles), + coord_system.slice(slice(0, ntriangles)), + coord_system.slice(slice(0, ntriangles))) + squares = Topology( + ref_coord_system, + References.uniform(element.getsimplex(1)**2, nsquares), + coord_system.slice(slice(ntriangles, None)), + coord_system.slice(slice(ntriangles, None))) + topo = topology._WithConnectivity(Topology.disjoint_union(triangles, squares), connectivity) geom = (topo.basis('std', degree=1) * coords.T).sum(-1) x, y = topo.boundary.sample('_centroid', None).eval(geom).T diff --git a/nutils/sample.py b/nutils/sample.py index f3f3dfeb7..2b625d0cf 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -17,9 +17,9 @@ from . import types, points, util, function, evaluable, parallel, numeric, matrix, sparse, warnings from .pointsseq import PointsSequence -from .transformseq import Transforms -from .transform import EvaluableTransformChain -from typing import Iterable, Mapping, Optional, Sequence, Tuple, Union +from .types import frozendict +from ._rust import CoordSystem +from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union import numpy import numbers import collections.abc @@ -27,10 +27,6 @@ import treelog as log import abc -_PointsShape = Tuple[evaluable.Array, ...] -_TransformChainsMap = Mapping[str, Tuple[EvaluableTransformChain, EvaluableTransformChain]] -_CoordinatesMap = Mapping[str, evaluable.Array] - def argdict(arguments) -> Mapping[str, numpy.ndarray]: if len(arguments) == 1 and 'arguments' in arguments and isinstance(arguments['arguments'], collections.abc.Mapping): @@ -59,14 +55,15 @@ class Sample(types.Singleton): __slots__ = 'spaces', 'ndims', 'nelems', 'npoints' @staticmethod - def new(space: str, transforms: Iterable[Transforms], points: PointsSequence, index: Optional[Union[numpy.ndarray, Sequence[numpy.ndarray]]] = None) -> 'Sample': + def new(reference: Dict[str, CoordSystem], coord_systems: Iterable[CoordSystem], points: PointsSequence, index: Optional[Union[numpy.ndarray, Sequence[numpy.ndarray]]] = None) -> 'Sample': '''Create a new :class:`Sample`. Parameters ---------- - transforms : :class:`tuple` or transformation chains - List of transformation chains leading to local coordinate systems that - contain points. + reference : :class:`dict` of :class:`str` to :class:`~nutils._rust.CoordSystem` + Reference coordinate systems. + coord_systems : :class:`tuple` of :class:`~nutils._rust.CoordSystem` + List of coordinate systems. points : :class:`~nutils.pointsseq.PointsSequence` Points sequence. index : integer array or :class:`tuple` of integer arrays, optional @@ -74,7 +71,7 @@ def new(space: str, transforms: Iterable[Transforms], points: PointsSequence, in If absent the indices will be strictly increasing. ''' - sample = _DefaultIndex(space, tuple(transforms), points) + sample = _DefaultIndex(frozendict(reference), tuple(coord_systems), points) if index is not None: if isinstance(index, (tuple, list)): assert all(ind.shape == (pnt.npoints,) for ind, pnt in zip(index, points)) @@ -83,8 +80,8 @@ def new(space: str, transforms: Iterable[Transforms], points: PointsSequence, in return sample @staticmethod - def empty(spaces: Tuple[str, ...], ndims: int) -> 'Sample': - return _Empty(spaces, ndims) + def empty(reference: Dict[str, CoordSystem], ndims: int) -> 'Sample': + return _Empty(reference, ndims) def __init__(self, spaces: Tuple[str, ...], ndims: int, nelems: int, npoints: int) -> None: ''' @@ -371,41 +368,29 @@ def zip(*samples: 'Sample') -> 'Sample': class _TransformChainsSample(Sample): - __slots__ = 'space', 'transforms', 'points' - - def __init__(self, space: str, transforms: Tuple[Transforms, ...], points: PointsSequence) -> None: - ''' - parameters - ---------- - space : ::class:`str` - The name of the space on which this sample is defined. - transforms : :class:`tuple` or transformation chains - List of transformation chains leading to local coordinate systems that - contain points. - points : :class:`~nutils.pointsseq.PointsSequence` - Points sequence. - ''' + __slots__ = 'reference', 'coord_systems', 'points' - assert len(transforms) >= 1 - assert all(len(t) == len(points) for t in transforms) - self.space = space - self.transforms = transforms + def __init__(self, reference, coord_systems: Tuple[CoordSystem, ...], points: PointsSequence) -> None: + assert len(coord_systems) >= 1 + assert all(len(t) == len(points) for t in coord_systems) + self.reference = reference + self.coord_systems = coord_systems self.points = points - super().__init__((space,), transforms[0].fromdims, len(points), points.npoints) + super().__init__(tuple(reference), coord_systems[0].dim, len(points), points.npoints) def get_evaluable_weights(self, __ielem: evaluable.Array) -> evaluable.Array: return self.points.get_evaluable_weights(__ielem) def get_lower_args(self, __ielem: evaluable.Array) -> function.LowerArgs: - return function.LowerArgs.for_space(self.space, tuple(t.get_evaluable(__ielem) for t in (self.transforms*2)[:2]), self.points.get_evaluable_coords(__ielem)) + return function.Bound(dict(self.reference), self.coord_systems, __ielem, self.points.get_evaluable_coords(__ielem)).into_lower_args() def basis(self) -> function.Array: return _Basis(self) def subset(self, mask: numpy.ndarray) -> Sample: selection = types.frozenarray([ielem for ielem in range(self.nelems) if mask[self.getindex(ielem)].any()]) - transforms = tuple(transform[selection] for transform in self.transforms) - return Sample.new(self.space, transforms, self.points.take(selection)) + coord_systems = tuple(transform[selection] for transform in self.coord_systems) + return Sample.new(self.reference, coord_systems, self.points.take(selection)) def get_element_tri(self, ielem: int) -> numpy.ndarray: if not 0 <= ielem < self.nelems: @@ -457,7 +442,7 @@ def __init__(self, parent: Sample, index: numpy.ndarray) -> None: assert index.shape == (parent.npoints,) self._parent = parent self._index = index - super().__init__(parent.space, parent.transforms, parent.points) + super().__init__(parent.reference, parent.coord_systems, parent.points) def getindex(self, ielem: int) -> numpy.ndarray: return numpy.take(self._index, self._parent.getindex(ielem)) @@ -493,11 +478,11 @@ def get_lower_args(self, __ielem: evaluable.Array) -> function.LowerArgs: raise SkipTest('`{}` does not implement `Sample.get_lower_args`'.format(type(self).__qualname__)) @property - def transforms(self) -> Tuple[Transforms, ...]: - raise SkipTest('`{}` does not implement `Sample.transforms`'.format(type(self).__qualname__)) + def coord_systems(self) -> Tuple[CoordSystem, ...]: + raise SkipTest('`{}` does not implement `Sample.coord_systems`'.format(type(self).__qualname__)) @property - def points(self) -> Tuple[Transforms, ...]: + def points(self) -> Tuple[CoordSystem, ...]: raise SkipTest('`{}` does not implement `Sample.points`'.format(type(self).__qualname__)) def basis(self) -> function.Array: @@ -522,7 +507,7 @@ def get_evaluable_weights(self, __ielem: evaluable.Array) -> evaluable.Array: return evaluable.Zeros((0,) * len(self.spaces), dtype=float) def get_lower_args(self, __ielem: evaluable.Array) -> function.LowerArgs: - return function.LowerArgs((), {}, {}) + return function.LowerArgs((), ()) def get_element_tri(self, ielem: int) -> numpy.ndarray: raise IndexError('index out of range') @@ -607,19 +592,22 @@ def getindex(self, __ielem: int) -> numpy.ndarray: return (index1[:, None] * self._sample2.npoints + index2[None, :]).ravel() def get_evaluable_indices(self, __ielem: evaluable.Array) -> evaluable.Array: - ielem1, ielem2 = evaluable.divmod(__ielem, self._sample2.nelems) + ielem1 = evaluable.FloorDivide(__ielem, self._sample2.nelems) + ielem2 = evaluable.mod(__ielem, self._sample2.nelems) index1 = self._sample1.get_evaluable_indices(ielem1) index2 = self._sample2.get_evaluable_indices(ielem2) return evaluable.appendaxes(index1 * self._sample2.npoints, index2.shape) + evaluable.prependaxes(index2, index1.shape) def get_evaluable_weights(self, __ielem: evaluable.Array) -> evaluable.Array: - ielem1, ielem2 = evaluable.divmod(__ielem, self._sample2.nelems) + ielem1 = evaluable.FloorDivide(__ielem, self._sample2.nelems) + ielem2 = evaluable.mod(__ielem, self._sample2.nelems) weights1 = self._sample1.get_evaluable_weights(ielem1) weights2 = self._sample2.get_evaluable_weights(ielem2) return evaluable.einsum('A,B->AB', weights1, weights2) def get_lower_args(self, __ielem: evaluable.Array) -> function.LowerArgs: - ielem1, ielem2 = evaluable.divmod(__ielem, self._sample2.nelems) + ielem1 = evaluable.FloorDivide(__ielem, self._sample2.nelems) + ielem2 = evaluable.mod(__ielem, self._sample2.nelems) return self._sample1.get_lower_args(ielem1) | self._sample2.get_lower_args(ielem2) def get_element_tri(self, ielem: int) -> numpy.ndarray: @@ -719,14 +707,14 @@ def _getslice(self, array, ielem): def get_lower_args(self, __ielem: evaluable.Array) -> function.LowerArgs: points_shape = evaluable.Take(self._sizes, __ielem), - coordinates = {} - transform_chains = {} + bounds = [] for samplei, ielemsi, ilocalsi in zip(self._samples, self._ielems, self._ilocals): argsi = samplei.get_lower_args(evaluable.Take(ielemsi, __ielem)) slicei = self._getslice(ilocalsi, __ielem) - transform_chains.update(argsi.transform_chains) - coordinates.update({space: evaluable._take(coords, slicei, axis=0) for space, coords in argsi.coordinates.items()}) - return function.LowerArgs(points_shape, transform_chains, coordinates) + for bound in argsi.bounds: + coords = evaluable._take(bound.coords, slicei, axis=0) + bounds.append(function.Bound(bound.spaces, bound.coord_systems, bound.index, coords)) + return function.LowerArgs(points_shape, bounds) def get_evaluable_indices(self, ielem): return self._getslice(self._indices, ielem) @@ -913,7 +901,7 @@ def __init__(self, sample: _TransformChainsSample) -> None: super().__init__(shape=(sample.npoints,), dtype=float, spaces=frozenset({sample.space}), arguments={}) def lower(self, args: function.LowerArgs) -> evaluable.Array: - aligned_space_coords = args.coordinates[self._sample.space] + aligned_space_coords = args.get_coords(self._sample.space) assert aligned_space_coords.ndim == len(args.points_shape) + 1 space_coords, where = evaluable.unalign(aligned_space_coords) # Reinsert the coordinate axis, the last axis of `aligned_space_coords`, or @@ -925,9 +913,8 @@ def lower(self, args: function.LowerArgs) -> evaluable.Array: space_coords = evaluable.Transpose(space_coords, numpy.argsort(where)) where = tuple(sorted(where)) - chain = args.transform_chains[self._sample.space][0] - index, tail = chain.index_with_tail_in(self._sample.transforms[0]) - coords = tail.apply(space_coords) + # FIXME + index, coords = args.relative_index_coords(self._sample.spaces, self._sample.coord_systems[0]) expect = self._sample.points.get_evaluable_coords(index) sampled = evaluable.Sampled(coords, expect) indices = self._sample.get_evaluable_indices(index) diff --git a/nutils/topology.py b/nutils/topology.py index 396a8c0da..e1ce015d3 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -14,14 +14,18 @@ :mod:`nutils.element` iterators. """ -from . import element, function, evaluable, util, parallel, numeric, cache, transform, transformseq, warnings, matrix, types, points, sparse +from . import debug_flags, element, function, evaluable, util, parallel, numeric, cache, transform, warnings, matrix, types, points, sparse from .sample import Sample +from .element import Reference from .elementseq import References from .pointsseq import PointsSequence -from typing import Any, FrozenSet, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Union +from ._rust import CoordSystem, Simplex +from typing import Any, Dict, FrozenSet, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Union +import typing import numpy import functools import collections.abc +from collections import OrderedDict import itertools import functools import operator @@ -42,10 +46,7 @@ class Topology(types.Singleton): Parameters ---------- - spaces : :class:`tuple` of :class:`str` - The unique, ordered list of spaces on which this topology is defined. - space_dims : :class:`tuple` of :class:`int` - The dimension of each space in :attr:`spaces`. + ref_coord_system : :class:`tuple` of :class:`str` and :class:`~nutils._rust.CoordSystem` pairs references : :class:`nutils.elementseq.References` The references. @@ -55,24 +56,22 @@ class Topology(types.Singleton): The unique, ordered list of spaces on which this topology is defined. space_dims : :class:`tuple` of :class:`int` The dimension of each space in :attr:`spaces`. + ref_coord_system : :class:`dict` of :class:`str` to :class:`~nutils._rust.CoordSystem` references : :class:`nutils.elementseq.References` The references. ndims : :class:`int` The dimension of this topology. ''' - __slots__ = 'spaces', 'space_dims', 'references', 'ndims' + __slots__ = 'ref_coord_system', 'spaces', 'space_dims', 'references', 'coord_system', 'opposite', 'ndims' @staticmethod - def empty(spaces: Iterable[str], space_dims: Iterable[int], ndims: int) -> 'Topology': + def empty(ref_coord_system: Tuple[Tuple[str, CoordSystem], ...], ndims: int) -> 'Topology': '''Return an empty topology. Parameters ---------- - spaces : :class:`tuple` of :class:`str` - The unique, ordered list of spaces on which the empty topology is defined. - space_dims : :class:`tuple` of :class:`int` - The dimension of each space in :attr:`spaces`. + ref_coord_system : :class:`tuple` of :class:`str` and :class:`~nutils._rust.CoordSystem` pairs ndims : :class:`int` The dimension of the empty topology. @@ -86,7 +85,7 @@ def empty(spaces: Iterable[str], space_dims: Iterable[int], ndims: int) -> 'Topo :meth:`empty_like` : create an empty topology with spaces and dimension copied from another topology ''' - return _Empty(tuple(spaces), tuple(space_dims), ndims) + return _Empty(ref_coord_system, ndims) def empty_like(self) -> 'Topology': '''Return an empty topology with the same spaces and dimensions as this topology. @@ -101,7 +100,7 @@ def empty_like(self) -> 'Topology': :meth:`empty_like` : create an empty topology with custom spaces and dimension ''' - return Topology.empty(self.spaces, self.space_dims, self.ndims) + return Topology.empty(self.ref_coord_system, self.ndims) def disjoint_union(*topos: 'Topology') -> 'Topology': '''Return the union of the given disjoint topologies. @@ -130,10 +129,47 @@ def disjoint_union(*topos: 'Topology') -> 'Topology': else: return empty - def __init__(self, spaces: Tuple[str, ...], space_dims: Tuple[int, ...], references: References) -> None: - self.spaces = spaces - self.space_dims = space_dims + @staticmethod + def line(space: str, nelems: int, bnames: Optional[Iterable[str]] = None, periodic: bool = False) -> 'Topology': + if not isinstance(space, str): + raise ValueError('argument `space`: expected a `str`') + if not isinstance(nelems, int) or nelems < 0: + raise ValueError('argument `nelems`: expected a non-negative `int`') + if bnames is None: + bnames = f'{space}-left', f'{space}-right' + else: + bnames = tuple(bnames) + if len(bnames) != 2 or not all(isinstance(name, str) for name in bnames): + raise ValueError('argument `bnames`: expected an iterable of two `str`') + coord_system = CoordSystem(1, nelems) + ref_coord_system = (space, coord_system), + return _Line(ref_coord_system, coord_system, coord_system, bnames, bool(periodic)) + + @staticmethod + def simplex(space: str, simplices: numpy.array) -> 'Topology': + if not isinstance(space, str): + raise ValueError('argument `space`: expected a `str`') + + simplices = numpy.asarray(simplices) + keep = numpy.zeros(simplices.max()+1, dtype=bool) + keep[simplices.flat] = True + simplices = types.arraydata(simplices if keep.all() else (numpy.cumsum(keep)-1)[simplices]) + + coord_system = CoordSystem(simplices.shape[1] - 1, simplices.shape[0]) + ref_coord_system = (space, coord_system), + return _SimplexTopology(ref_coord_system, simplices, coord_system, coord_system) + + def __init__(self, ref_coord_system: Tuple[Tuple[str, CoordSystem], ...], references: References, coord_system: CoordSystem, opposite: CoordSystem): + if not isinstance(ref_coord_system, tuple) or not all(len(item) == 2 and isinstance(item[0], str) and isinstance(item[1], CoordSystem) for item in ref_coord_system): + raise ValueError('argument `ref_coord_system`: expected a `tuple` of `str` and `CoordSystem` pairs') + if not (references.ndims == coord_system.dim == opposite.dim and len(references) == len(coord_system) == len(opposite)): + raise ValueError('`references`, `coord_system` and `opposite` have different dimensions') + self.ref_coord_system = ref_coord_system + self.spaces = tuple(space for space, _ in ref_coord_system) + self.space_dims = tuple(ref.dim for _, ref in ref_coord_system) self.references = references + self.coord_system = coord_system + self.opposite = opposite self.ndims = references.ndims super().__init__() @@ -191,6 +227,8 @@ def take(self, __indices: Union[numpy.ndarray, Sequence[int]]) -> 'Topology': indices = numpy.unique(indices.astype(int, casting='same_kind')) if indices[0] < 0 or indices[-1] >= len(self): raise IndexError('element index out of range') + if len(indices) == len(self): + return self return self.take_unchecked(indices) def take_unchecked(self, __indices: numpy.ndarray) -> 'Topology': @@ -282,42 +320,44 @@ def __getitem__(self, item: Any) -> 'Topology': raise KeyError(item) return topo + def __invert__(self): + return OppositeTopology(self) + def __mul__(self, other: Any) -> 'Topology': if isinstance(other, Topology): - return _Mul(self, other) + left = self._disjoint_topos + right = other._disjoint_topos + empty = Topology.empty(self.ref_coord_system + other.ref_coord_system, self.ndims + other.ndims) + return Topology.disjoint_union(empty, *(_Mul(l, r) for l in left for r in right if len(l) and len(r))) else: return NotImplemented def __and__(self, other: Any) -> 'Topology': if not isinstance(other, Topology): return NotImplemented - elif self.spaces != other.spaces or self.space_dims != other.space_dims or self.ndims != other.ndims: - raise ValueError('The topologies must have the same spaces and dimensions.') + elif self.ref_coord_system != other.ref_coord_system: + raise ValueError('The topologies must have the same (order of) spaces and reference coordinate system.') elif not self or not other: return self.empty_like() else: - return NotImplemented + raise NotImplementedError __rand__ = __and__ def __or__(self, other: Any) -> 'Topology': if not isinstance(other, Topology): return NotImplemented - elif self.spaces != other.spaces or self.space_dims != other.space_dims or self.ndims != other.ndims: - raise ValueError('The topologies must have the same spaces and dimensions.') + elif self.ref_coord_system != other.ref_coord_system: + raise ValueError('The topologies must have the same (order of) spaces and dimensions.') elif not self: return other elif not other: return self else: - return NotImplemented + return UnionTopology((self, other)) __ror__ = __or__ - @property - def border_transforms(self) -> transformseq.Transforms: - raise NotImplementedError - @property def refine_iter(self) -> 'Topology': topo = self @@ -325,22 +365,27 @@ def refine_iter(self) -> 'Topology': yield topo topo = topo.refined + @property + def _index_coords(self): + index = function.transforms_index(self.coord_system, *self.spaces) + coords = function.transforms_coords(self.coord_system, *self.spaces) + return index, coords + @property def f_index(self) -> function.Array: '''The evaluable index of the element in this topology.''' - raise NotImplementedError + return self._index_coords[0] @property def f_coords(self) -> function.Array: '''The evaluable element local coordinates.''' - raise NotImplementedError + return self._index_coords[1] def basis(self, name: str, *args, **kwargs) -> function.Basis: - ''' - Create a basis. - ''' + '''Create a basis.''' + if self.ndims == 0: return function.PlainBasis([[1]], [[0]], 1, self.f_index, self.f_coords) split = name.split('-', 1) @@ -351,10 +396,85 @@ def basis(self, name: str, *args, **kwargs) -> function.Basis: f = getattr(self, 'basis_' + name) return f(*args, **kwargs) + def basis_discont(self, degree: int) -> function.Basis: + 'discontinuous shape functions' + + assert numeric.isint(degree) and degree >= 0 + if self.references.isuniform: + coeffs = [self.references[0].get_poly_coeffs('bernstein', degree=degree)]*len(self.references) + else: + coeffs = [ref.get_poly_coeffs('bernstein', degree=degree) for ref in self.references] + return function.DiscontBasis(coeffs, self.f_index, self.f_coords) + + def _basis_c0_structured(self, name, degree): + 'C^0-continuous shape functions with lagrange stucture' + + assert numeric.isint(degree) and degree >= 0 + + connectivity = self.connectivity + + if degree == 0: + raise ValueError('Cannot build a C^0-continuous basis of degree 0. Use basis \'discont\' instead.') + + coeffs = [ref.get_poly_coeffs(name, degree=degree) for ref in self.references] + offsets = numpy.cumsum([0] + [len(c) for c in coeffs]) + dofmap = numpy.repeat(-1, offsets[-1]) + for ielem, ioppelems in enumerate(connectivity): + for iedge, jelem in enumerate(ioppelems): # loop over element neighbors and merge dofs + if jelem < ielem: + continue # either there is no neighbor along iedge or situation will be inspected from the other side + jedge = util.index(connectivity[jelem], ielem) + idofs = offsets[ielem] + self.references[ielem].get_edge_dofs(degree, iedge) + jdofs = offsets[jelem] + self.references[jelem].get_edge_dofs(degree, jedge) + for idof, jdof in zip(idofs, jdofs): + while dofmap[idof] != -1: + idof = dofmap[idof] + while dofmap[jdof] != -1: + jdof = dofmap[jdof] + if idof != jdof: + dofmap[max(idof, jdof)] = min(idof, jdof) # create left-looking pointer + # assign dof numbers left-to-right + ndofs = 0 + for i, n in enumerate(dofmap): + if n == -1: + dofmap[i] = ndofs + ndofs += 1 + else: + dofmap[i] = dofmap[n] + + elem_slices = map(slice, offsets[:-1], offsets[1:]) + dofs = tuple(types.frozenarray(dofmap[s]) for s in elem_slices) + return function.PlainBasis(coeffs, dofs, ndofs, self.f_index, self.f_coords) + + def basis_lagrange(self, degree): + 'lagrange shape functions' + + return self._basis_c0_structured('lagrange', degree) + + def basis_bernstein(self, degree): + 'bernstein shape functions' + + return self._basis_c0_structured('bernstein', degree) + + basis_std = basis_bernstein + + def basis_spline(self, degree): + 'spline basis functions' + + if degree == 1: + return self.basis('std', degree) + else: + raise NotImplementedError + def sample(self, ischeme: str, degree: int) -> Sample: 'Create sample.' - raise NotImplementedError + points = PointsSequence.from_iter((ischeme(reference, degree) for reference in self.references), self.ndims) if callable(ischeme) \ + else self.references.getpoints(ischeme, degree) + coord_systems = self.coord_system, + if len(self.coord_system) == 0 or self.opposite != self.coord_system: + coord_systems += self.opposite, + return Sample.new(dict(self.ref_coord_system), coord_systems, points) @util.single_or_multiple def integrate_elementwise(self, funcs: Iterable[function.Array], *, degree: int, asfunction: bool = False, ischeme: str = 'gauss', arguments: Optional[_ArgDict] = None) -> Union[List[numpy.ndarray], List[function.Array]]: @@ -482,7 +602,7 @@ def project(self, fun: function.Array, onto: function.Array, geometry: function. def refined_by(self, refine: Iterable[int]) -> 'Topology': 'create refined space by refining dofs in existing one' - raise NotImplementedError + return HierarchicalTopology(self, (types.arraydata(numpy.arange(len(self), dtype=int)),)).refined_by(refine) @property def refined(self) -> 'Topology': @@ -594,7 +714,10 @@ def refine_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': doing. ''' - raise NotImplementedError + if self.ndims == 0: + return self + else: + return RefinedTopology(self) def refine_spaces_count(self, count: Mapping[str, int]) -> 'Topology': '''Return the topology with the given spaces refined the given amount times. @@ -632,16 +755,66 @@ def trim(self, levelset: function.Array, maxrefine: int, ndivisions: int = 8, na raise NotImplementedError + if arguments is None: + arguments = {} + + refs = [] + if leveltopo is None: + ielem_arg = evaluable.Argument('_trim_index', (), dtype=int) + coordinates = self.references.getpoints('vertex', maxrefine).get_evaluable_coords(ielem_arg) + levelset = levelset.lower(function.LowerArgs.for_space(self.space, (self.transforms, self.opposites), ielem_arg, coordinates)).optimized_for_numpy + with log.iter.percentage('trimming', range(len(self)), self.references) as items: + for ielem, ref in items: + levels = levelset.eval(_trim_index=ielem, **arguments) + refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) + else: + log.info('collecting leveltopo elements') + coordinates = evaluable.Points(evaluable.NPoints(), self.ndims) + ielem = evaluable.Argument('_leveltopo_ielem', (), int) + levelset = levelset.lower(function.LowerArgs.for_space(self.space, (leveltopo.transforms, leveltopo.opposites), ielem, coordinates)).optimized_for_numpy + bins = [set() for ielem in range(len(self))] + for trans in leveltopo.transforms: + ielem, tail = self.transforms.index_with_tail(trans) + bins[ielem].add(tail) + fcache = cache.WrapperCache() + with log.iter.percentage('trimming', self.references, self.transforms, bins) as items: + for ref, trans, ctransforms in items: + levels = numpy.empty(ref.nvertices_by_level(maxrefine)) + cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine)) + # confirm cover and greedily optimize order + mask = numpy.ones(len(levels), dtype=bool) + while mask.any(): + imax = numpy.argmax([mask[indices].sum() for tail, points, indices in cover]) + tail, points, indices = cover.pop(imax) + ielem = leveltopo.transforms.index(trans + tail) + levels[indices] = levelset.eval(_leveltopo_ielem=ielem, _points=points, **arguments) + mask[indices] = False + refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) + log.debug('cache', fcache.stats) + return SubsetTopology(self, refs, newboundary=name) + def subset(self, topo: 'Topology', newboundary: Optional[Union[str, 'Topology']] = None, strict: bool = False) -> 'Topology': 'intersection' raise NotImplementedError + refs = [ref.empty for ref in self.references] + for ref, trans in zip(topo.references, topo.transforms): + try: + ielem = self.transforms.index(trans) + except ValueError: + assert not strict, 'elements do not form a strict subset' + else: + subref = self.references[ielem] & ref + if strict: + assert subref == ref, 'elements do not form a strict subset' + refs[ielem] = subref + if not any(refs): + return self.empty_like() + return SubsetTopology(self, refs, newboundary) + def withgroups(self, vgroups: Mapping[str, Union[str, 'Topology']] = {}, bgroups: Mapping[str, Union[str, 'Topology']] = {}, igroups: Mapping[str, Union[str, 'Topology']] = {}, pgroups: Mapping[str, Union[str, 'Topology']] = {}) -> 'Topology': - if all(isinstance(v, str) for g in (vgroups, bgroups, igroups) for v in g.values()) and not pgroups: - return _WithGroupAliases(self, types.frozendict(vgroups), types.frozendict(bgroups), types.frozendict(igroups)) - else: - raise NotImplementedError + return _WithGroupsTopology(self, types.frozendict(vgroups), types.frozendict(bgroups), types.frozendict(igroups), types.frozendict(pgroups)) if vgroups or bgroups or igroups or pgroups else self def withsubdomain(self, **kwargs: 'Topology') -> 'Topology': return self.withgroups(vgroups=kwargs) @@ -672,10 +845,28 @@ def check_boundary(self, geometry: function.Array, elemwise: bool = False, ische if numpy.greater(abs(volumes - volume), tol).any(): print('divergence check failed: {} != {}'.format(volumes, volume)) - def indicator(self, subtopo: Union[str, 'Topology']) -> 'Topology': - '''Create an indicator function for a subtopology.''' + def indicator(self, subtopo): + if isinstance(subtopo, str): + subtopo = self.get_groups(*subtopo.split(',')) + missing = frozenset(subtopo.spaces) - frozenset(self.spaces) + if missing: + raise ValueError('The following spaces of the sub topology are not present in the super topology: {}'.format(','.join(missing))) + + sub_coord_system = subtopo.coord_system + for i in range(len(self.spaces) - len(subtopo.spaces) + 1): + if self.spaces[i:i + len(subtopo.spaces)] == subtopo.spaces: + break + else: + raise ValueError('Cannot create an indicator for a sub topology defined on a non-contiguous subset of spaces of the super topology.') + for ispace in reversed(range(i)): + sub_coord_system = self.ref_coord_system[ispace][1] * sub_coord_system + for ispace in range(i + len(subtopo.spaces), len(self.spaces)): + sub_coord_system *= self.ref_coord_system[ispace][1] - raise NotImplementedError + trans = sub_coord_system.trans_to(self.coord_system) + values = numpy.zeros([len(self)], dtype=int) + values[numpy.unique(trans.apply_indices(numpy.arange(len(subtopo))))] = 1 + return function.get(values, 0, self.f_index) def select(self, indicator: function.Array, ischeme: str = 'bezier2', **kwargs: numpy.ndarray) -> 'Topology': # Select elements where `indicator` is strict positive at any of the @@ -685,7 +876,7 @@ def select(self, indicator: function.Array, ischeme: str = 'bezier2', **kwargs: sample = self.sample(*element.parse_legacy_ischeme(ischeme)) isactive, ielem = sample.eval([indicator > 0, self.f_index], **kwargs) selected = types.frozenarray(numpy.unique(ielem[isactive])) - return self[selected] + return self.take(selected) def locate(self, geom, coords, *, tol=0, eps=0, maxiter=0, arguments=None, weights=None, maxdist=None, ischeme=None, scale=None, skip_missing=False) -> Sample: '''Create a sample based on physical coordinates. @@ -745,7 +936,89 @@ def locate(self, geom, coords, *, tol=0, eps=0, maxiter=0, arguments=None, weigh located : :class:`nutils.sample.Sample` ''' - raise NotImplementedError + if ischeme is not None: + warnings.deprecation('the ischeme argument is deprecated and will be removed in future') + if scale is not None: + warnings.deprecation('the scale argument is deprecated and will be removed in future') + if max(tol, eps) <= 0: + raise ValueError('locate requires either tol or eps to be strictly positive') + coords = numpy.asarray(coords, dtype=float) + if geom.ndim == 0: + geom = geom[_] + coords = coords[..., _] + if not geom.shape == coords.shape[1:] == (self.ndims,): + raise ValueError('invalid geometry or point shape for {}D topology'.format(self.ndims)) + arguments = dict(arguments or ()) + centroids = self.sample('_centroid', None).eval(geom, **arguments) + assert len(centroids) == len(self) + ielems = parallel.shempty(len(coords), dtype=int) + points = parallel.shempty((len(coords), len(geom)), dtype=float) + _ielem = evaluable.InRange(evaluable.Argument('_locate_ielem', shape=(), dtype=int), len(self)) + _point = evaluable.Argument('_locate_point', shape=(self.ndims,)) + lower_args = function.Bound(dict(self.ref_coord_system), (self.coord_system, self.opposite), _ielem, _point).into_lower_args() + egeom = geom.lower(lower_args) + xJ = evaluable.Tuple((egeom, evaluable.derivative(egeom, _point))).simplified + if skip_missing: + if weights is not None: + raise ValueError('weights and skip_missing are mutually exclusive') + missing = parallel.shzeros(len(coords), dtype=bool) + with parallel.ctxrange('locating', len(coords)) as ipoints: + for ipoint in ipoints: + xt = coords[ipoint] # target + dist = numpy.linalg.norm(centroids - xt, axis=1) + for ielem in numpy.argsort(dist) if maxdist is None \ + else sorted((dist < maxdist).nonzero()[0], key=dist.__getitem__): + ref = self.references[ielem] + arguments['_locate_ielem'] = ielem + arguments['_locate_point'] = p = numpy.array(ref.centroid) + ex = ep = numpy.inf + iiter = 0 + while ex > tol and ep > eps: # newton loop + if iiter > maxiter > 0: + break # maximum number of iterations reached + iiter += 1 + xp, Jp = xJ.eval(**arguments) + dx = xt - xp + ex0 = ex + ex = numpy.linalg.norm(dx) + if ex >= ex0: + break # newton is diverging + try: + dp = numpy.linalg.solve(Jp, dx) + except numpy.linalg.LinAlgError: + break # jacobian is singular + ep = numpy.linalg.norm(dp) + p += dp # NOTE: modifies arguments['_locate_point'] in place + else: + if ref.inside(p, max(eps, ep)): + ielems[ipoint] = ielem + points[ipoint] = p + break + else: + if skip_missing: + missing[ipoint] = True + else: + raise LocateError('failed to locate point: {}'.format(xt)) + if skip_missing: + ielems = ielems[~missing] + points = points[~missing] + return self._sample(ielems, points, weights) + + def _sample(self, ielems, coords, weights=None): + index = numpy.argsort(ielems, kind='stable') + sorted_ielems = ielems[index] + offsets = [0, *(sorted_ielems[:-1] != sorted_ielems[1:]).nonzero()[0]+1, len(index)] + + unique_ielems = sorted_ielems[offsets[:-1]] + coord_systems = self.coord_system.take(unique_ielems), + if len(self.coord_system) == 0 or self.opposite != self.coord_system: + coord_systems += self.opposite.take(unique_ielems), + + slices = [index[n:m] for n, m in zip(offsets[:-1], offsets[1:])] + points_ = PointsSequence.from_iter([points.CoordsPoints(coords[s]) for s in slices] if weights is None + else [points.CoordsWeightsPoints(coords[s], weights[s]) for s in slices], self.ndims) + + return Sample.new(dict(self.ref_coord_system), coord_systems, points_, index) @property def boundary(self) -> 'Topology': @@ -805,21 +1078,48 @@ def boundary_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': absolutely sure what you are doing. ''' - raise NotImplementedError - - @property - def interfaces(self) -> 'Topology': - return self.interfaces_spaces(self.spaces) - - def interfaces_spaces(self, __spaces: Iterable[str]) -> 'Topology': - '''Return the interfaces in the given spaces. - - Parameters - ---------- - spaces : iterable of :class:`str` - Nonstrict subset of :attr:`spaces`. Duplicates are silently ignored. + connectivity = self.connectivity - Returns + if __spaces != frozenset(self.spaces): + return ValueError('Cannot create the boundary for a subset of spaces.') + references = [] + selection = [] + iglobaledgeiter = itertools.count() + refs_touched = False + for ielem, (ioppelems, elemref) in enumerate(zip(connectivity, self.references)): + for edgeref, ioppelem, iglobaledge in zip(elemref.edge_refs, ioppelems, iglobaledgeiter): + if edgeref: + if ioppelem == -1: + references.append(edgeref) + selection.append(iglobaledge) + else: + ioppedge = util.index(connectivity[ioppelem], ielem) + ref = edgeref - self.references[ioppelem].edge_refs[ioppedge] + if ref: + references.append(ref) + selection.append(iglobaledge) + refs_touched = True + selection = types.frozenarray(selection, dtype=int) + if refs_touched: + references = References.from_iter(references, self.ndims-1) + else: + references = self.references.edges[selection] + coord_system = self.references.edges_coord_system(self.coord_system).take(selection) + return Topology(self.space, references, coord_system, coord_system) + + @property + def interfaces(self) -> 'Topology': + return self.interfaces_spaces(self.spaces) + + def interfaces_spaces(self, __spaces: Iterable[str]) -> 'Topology': + '''Return the interfaces in the given spaces. + + Parameters + ---------- + spaces : iterable of :class:`str` + Nonstrict subset of :attr:`spaces`. Duplicates are silently ignored. + + Returns ------- :class:`Topology` The interfaces in the given spaces. @@ -860,153 +1160,160 @@ def interfaces_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': absolutely sure what you are doing. ''' - raise NotImplementedError + connectivity = self.connectivity - def basis_discont(self, degree: int) -> function.Basis: - 'discontinuous shape functions' + if __spaces != frozenset(self.spaces): + return ValueError('Cannot create the boundary for a subset of spaces.') - assert numeric.isint(degree) and degree >= 0 + references = [] + selection = [] + oppselection = [] + iglobaledgeiter = itertools.count() + refs_touched = False + edges = self.transforms.edges(self.references) if self.references.isuniform: - coeffs = [self.references[0].get_poly_coeffs('bernstein', degree=degree)]*len(self.references) + _nedges = self.references[0].nedges + offset = lambda ielem: ielem * _nedges else: - coeffs = [ref.get_poly_coeffs('bernstein', degree=degree) for ref in self.references] - return function.DiscontBasis(coeffs, self.f_index, self.f_coords) - - -if os.environ.get('NUTILS_TENSORIAL', None) == 'test': # pragma: nocover - - from unittest import SkipTest - - class _TensorialTopology(Topology): + offset = numpy.cumsum([0]+list(ref.nedges for ref in self.references)).__getitem__ + for ielem, (ioppelems, elemref, elemtrans) in enumerate(zip(connectivity, self.references, self.transforms)): + for (edgetrans, edgeref), ioppelem, iglobaledge in zip(elemref.edges, ioppelems, iglobaledgeiter): + if edgeref and -1 < ioppelem < ielem: + ioppedge = util.index(connectivity[ioppelem], ielem) + oppedgetrans, oppedgeref = self.references[ioppelem].edges[ioppedge] + ref = oppedgeref and edgeref & oppedgeref + if ref: + references.append(ref) + selection.append(iglobaledge) + oppselection.append(offset(ioppelem)+ioppedge) + if ref != edgeref: + refs_touched = True + selection = types.frozenarray(selection, dtype=int) + oppselection = types.frozenarray(oppselection, dtype=int) + if refs_touched: + references = References.from_iter(references, self.ndims-1) + else: + references = self.references.edges[selection] + return TransformChainsTopology(self.space, references, edges[selection], edges[oppselection]) - def __and__(self, other: Any) -> Topology: - result = super().__and__(other) - if type(self) == type(other) and result is NotImplemented: - raise SkipTest('`{}` does not implement `Topology.__and__`'.format(type(self).__qualname__)) - return result - def __rand__(self, other: Any) -> Topology: - result = super().__and__(other) - if result is NotImplemented: - raise SkipTest('`{}` does not implement `Topology.__and__`'.format(type(self).__qualname__)) - return result + @property + def _disjoint_topos(self): + return self, - def __sub__(self, other: Any) -> Topology: - if type(self) == type(other): - raise SkipTest('`{}` does not implement `Topology.__sub__`'.format(type(self).__qualname__)) - else: - return NotImplemented - def __rsub__(self, other: Any) -> Topology: - if isinstance(other, Topology): - raise SkipTest('`{}` does not implement `Topology.__sub__`'.format(type(self).__qualname__)) - else: - return NotImplemented +class _Empty(Topology): - @property - def space(self) -> str: - raise SkipTest('`{}` does not implement `Topology.space`'.format(type(self).__qualname__)) + def __init__(self, ref_coord_system: Tuple[Tuple[str, CoordSystem], ...], ndims: int) -> None: + super().__init__(ref_coord_system, References.empty(ndims), CoordSystem(ndims, 0), CoordSystem(ndims, 0)) - @property - def transforms(self) -> transformseq.Transforms: - raise SkipTest('`{}` does not implement `Topology.transforms`'.format(type(self).__qualname__)) + def __invert__(self) -> Topology: + return self - @property - def opposites(self) -> transformseq.Transforms: - raise SkipTest('`{}` does not implement `Topology.opposites`'.format(type(self).__qualname__)) + @property + def connectivity(self) -> Sequence[Sequence[int]]: + return tuple() - @property - def border_transforms(self) -> transformseq.Transforms: - raise SkipTest('`{}` does not implement `Topology.border_transforms`'.format(type(self).__qualname__)) + def indicator(self, subtopo: Union[str, Topology]) -> Topology: + return function.zeros((), int) - @property - def f_index(self) -> function.Array: - raise SkipTest('`{}` does not implement `Topology.f_index`'.format(type(self).__qualname__)) + def refine_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: + return self - @property - def f_coords(self) -> function.Array: - raise SkipTest('`{}` does not implement `Topology.f_coords`'.format(type(self).__qualname__)) + def boundary_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: + return _Empty(self.ref_coord_system, self.ndims - 1) - def refined_by(self, refine: Iterable[int]) -> Topology: - raise SkipTest('`{}` does not implement `Topology.refined_by`'.format(type(self).__qualname__)) + def interfaces_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: + return _Empty(self.ref_coord_system, self.ndims - 1) - def trim(self, levelset: function.Array, maxrefine: int, ndivisions: int = 8, name: str = 'trimmed', leveltopo: Optional[Topology] = None, *, arguments: Optional[_ArgDict] = None) -> Topology: - raise SkipTest('`{}` does not implement `Topology.trim`'.format(type(self).__qualname__)) + def basis_std(self, degree: int, *args, **kwargs) -> function.Array: + return function.zeros((0,)) - def subset(self, topo: Topology, newboundary: Optional[Union[str, Topology]] = None, strict: bool = False) -> Topology: - raise SkipTest('`{}` does not implement `Topology.subset`'.format(type(self).__qualname__)) + basis_spline = basis_std - def withgroups(self, vgroups: Mapping[str, Union[str, Topology]] = {}, bgroups: Mapping[str, Union[str, Topology]] = {}, igroups: Mapping[str, Union[str, Topology]] = {}, pgroups: Mapping[str, Union[str, Topology]] = {}) -> Topology: - try: - return super().withgroups(vgroups, bgroups, igroups, pgroups) - except NotImplementedError: - raise SkipTest('`{}` does not implement `Topology.withgroups`'.format(type(self).__qualname__)) + def sample(self, ischeme: str, degree: int) -> Sample: + return Sample.empty(self.ref_coord_system, self.ndims) - def indicator(self, subtopo: Union[str, Topology]) -> Topology: - raise SkipTest('`{}` does not implement `Topology.indicator`'.format(type(self).__qualname__)) - def locate(self, geom, coords, *, tol=0, eps=0, maxiter=0, arguments=None, weights=None, maxdist=None, ischeme=None, scale=None, skip_missing=False) -> Sample: - raise SkipTest('`{}` does not implement `Topology.locate`'.format(type(self).__qualname__)) +class _WithName(Topology): -else: - _TensorialTopology = Topology + def __init__(self, topo: Topology, name: str): + self.topo = topo + self.name = name + super().__init__(topo.ref_coord_system, topo.references, topo.coord_system, topo.opposite) + def get_groups(self, *groups: str) -> Topology: + if self.name in groups: + return self.topo + else: + return self.topo.get_groups(*groups) -class _EmptyUnlowerable(function.Array): + def take_unchecked(self, __indices: numpy.ndarray) -> Topology: + return _WithName(self.topo.take_unchecked(__indices), self.name) - def lower(self, args: function.LowerArgs) -> evaluable.Array: - raise ValueError('cannot lower') + def slice_unchecked(self, __s: slice, __idim: int) -> 'Topology': + return _WithName(self.topo.slice_unchecked(__s, __idim), self.name) + def __invert__(self): + return _WithName(~self.topo, self.name) -class _Empty(_TensorialTopology): + @property + def f_index(self) -> function.Array: + return self.topo.f_index - def __init__(self, spaces: Tuple[str, ...], space_dims: Tuple[int, ...], ndims: int) -> None: - super().__init__(spaces, space_dims, References.empty(ndims)) + @property + def f_coords(self) -> function.Array: + return self.topo.f_coords - def __invert__(self) -> Topology: - return self + def basis(self, *args, **kwargs) -> function.Basis: + return self.topo.basis(*args, **kwargs) - @property - def connectivity(self) -> Sequence[Sequence[int]]: - return tuple() + def sample(self, ischeme: str, degree: int) -> Sample: + return self.topo.sample(ischeme, degree) - def indicator(self, subtopo: Union[str, Topology]) -> Topology: - return function.zeros((), int) + def refine_spaces_unchecked(self, __spaces: FrozenSet[str]) -> Topology: + return _WithName(self.topo.refine_spaces_unchecked(__spaces), self.name) - @property - def f_index(self) -> function.Array: - return _EmptyUnlowerable((), int, self.spaces, {}) + def trim(self, *args, **kwargs) -> Topology: + return _WithName(self.topo.trim(*args, **kwargs), self.name) - @property - def f_coords(self) -> function.Array: - return _EmptyUnlowerable((self.ndims,), float, self.spaces, {}) + def subset(self, *args, **kwargs) -> Topology: + return _WithName(self.topo.subset(*args, **kwargs), self.name) - def refine_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return self + def indicator(self, subtopo): + if isinstance(subtopo, str) and self.name in subtopo.split(','): + return function.ones(()) + else: + return self.topo.indicator(subtopo) - def boundary_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return _Empty(self.spaces, self.space_dims, self.ndims - 1) + def select(self, *args, **kwargs) -> Topology: + return _WithName(self.topo.select(*args, **kwargs), self.name) - def interfaces_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return _Empty(self.spaces, self.space_dims, self.ndims - 1) + def locate(self, *args, **kwargs): + return self.topo.locate(*args, **kwargs) - def basis_std(self, degree: int, *args, **kwargs) -> function.Array: - return function.zeros((0,)) + def boundary_spaces_unchecked(self, __spaces: FrozenSet[str]) -> Topology: + return self.topo.boundary_spaces_unchecked(__spaces) - basis_spline = basis_std + def interfaces_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': + return self.topo.interfaces_spaces_unchecked(__spaces) - def sample(self, ischeme: str, degree: int) -> Sample: - return Sample.empty(self.spaces, self.ndims) + @property + def _disjoint_topos(self): + return tuple(_WithName(part, self.name) for part in self.topo._disjoint_topos) -class _DisjointUnion(_TensorialTopology): +class _DisjointUnion(Topology): def __init__(self, topo1: Topology, topo2: Topology) -> None: - if topo1.spaces != topo2.spaces or topo1.space_dims != topo2.space_dims or topo1.ndims != topo2.ndims: - raise ValueError('The topologies must have the same spaces and dimensions.') + if topo1.ref_coord_system != topo2.ref_coord_system: + raise ValueError('The topologies must have the same (order of) spaces and reference coordinate system.') self.topo1 = topo1 self.topo2 = topo2 - super().__init__(topo1.spaces, topo1.space_dims, topo1.references + topo2.references) + references = topo1.references + topo2.references + coord_system = topo1.coord_system.concat(topo2.coord_system) + opposite = topo1.opposite.concat(topo2.opposite) + super().__init__(topo1.ref_coord_system, references, coord_system, opposite) def __invert__(self) -> Topology: return Topology.disjoint_union(~self.topo1, ~self.topo2) @@ -1014,8 +1321,8 @@ def __invert__(self) -> Topology: def __and__(self, other: Any) -> Topology: if not isinstance(other, Topology): return NotImplemented - elif self.spaces != other.spaces or self.space_dims != other.space_dims or self.ndims != other.ndims: - raise ValueError('The topologies must have the same spaces and dimensions.') + elif self.ref_coord_system != other.ref_coord_system: + raise ValueError('The topologies must have the same (order of) spaces and reference coordinate system.') else: return Topology.disjoint_union(self.topo1 & other, self.topo2 & other) @@ -1054,6 +1361,7 @@ def sample(self, ischeme: str, degree: int) -> Sample: def trim(self, levelset: function.Array, maxrefine: int, ndivisions: int = 8, name: str = 'trimmed', leveltopo: Optional[Topology] = None, *, arguments: Optional[_ArgDict] = None) -> Topology: if leveltopo is not None: + # TODO return super().trim(levelset, maxrefine, ndivisions, name, leveltopo, arguments=arguments) else: topo1 = self.topo1.trim(levelset, maxrefine, ndivisions, name, arguments=arguments) @@ -1065,15 +1373,23 @@ def select(self, indicator: function.Array, ischeme: str = 'bezier2', **kwargs: topo2 = self.topo2.select(indicator, ischeme, **kwargs) return Topology.disjoint_union(topo1, topo2) + @property + def _disjoint_topos(self): + return self.topo1._disjoint_topos + self.topo2._disjoint_topos + -class _Mul(_TensorialTopology): +class _Mul(Topology): def __init__(self, topo1: Topology, topo2: Topology) -> None: if not set(topo1.spaces).isdisjoint(topo2.spaces): raise ValueError('Cannot multiply two topologies (partially) defined on the same spaces.') self.topo1 = topo1 self.topo2 = topo2 - super().__init__(topo1.spaces + topo2.spaces, topo1.space_dims + topo2.space_dims, topo1.references * topo2.references) + ref_coord_system = topo1.ref_coord_system + topo2.ref_coord_system + references = topo1.references * topo2.references + coord_system = topo1.coord_system * topo2.coord_system + opposite = topo1.opposite * topo2.opposite + super().__init__(ref_coord_system, references, coord_system, opposite) def __invert__(self) -> Topology: return ~self.topo1 * ~self.topo2 @@ -1113,17 +1429,15 @@ def slice_unchecked(self, indices: slice, idim: int) -> Topology: def indicator(self, subtopo: Union[str, Topology]) -> Topology: if isinstance(subtopo, str): - groups = subtopo.split(',') - hassub1 = bool(self.topo1.get_groups(*groups)) - hassub2 = bool(self.topo2.get_groups(*groups)) - if hassub1 and hassub2: - raise NotImplementedError - elif hassub1: - return self.topo1.indicator(subtopo) - elif hassub2: - return self.topo2.indicator(subtopo) - else: - return function.zeros((), int) + subtopo = self.get_groups(*subtopo.split(',')) + missing = frozenset(subtopo.spaces) - frozenset(self.spaces) + if missing: + raise ValueError('The following spaces of the sub topology are not present in the super topology: {}'.format(','.join(missing))) + + if frozenset(subtopo.spaces) <= frozenset(self.topo1.spaces): + return self.topo1.indicator(subtopo) + elif frozenset(subtopo.spaces) <= frozenset(self.topo2.spaces): + return self.topo2.indicator(subtopo) else: return super().indicator(subtopo) @@ -1196,13 +1510,13 @@ def basis(self, name: str, degree: Union[int, Sequence[int]], **kwargs) -> funct basis1 = self.topo1.basis(name, **kwargs1) basis2 = self.topo2.basis(name, **kwargs2) assert basis1.ndim == basis2.ndim == 1 - return numpy.ravel(basis1[:,None] * basis2[None,:]) + return function.ProductBasis(basis1, basis2) def sample(self, ischeme: str, degree: int) -> Sample: return self.topo1.sample(ischeme, degree) * self.topo2.sample(ischeme, degree) -class _Take(_TensorialTopology): +class _Take(Topology): def __init__(self, parent: Topology, indices: types.arraydata) -> None: self.parent = parent @@ -1210,513 +1524,124 @@ def __init__(self, parent: Topology, indices: types.arraydata) -> None: assert indices.ndim == 1 and indices.size assert numpy.greater(indices[1:], indices[:-1]).all() assert 0 <= indices[0] and indices[-1] < len(self.parent) - super().__init__(parent.spaces, parent.space_dims, parent.references.take(self.indices)) - - def sample(self, ischeme: str, degree: int) -> Sample: - return self.parent.sample(ischeme, degree).take_elements(self.indices) - - -class _WithGroupAliases(_TensorialTopology): - - def __init__(self, parent: Topology, vgroups: Mapping[str, str] = {}, bgroups: Mapping[str, str] = {}, igroups: Mapping[str, str] = {}) -> None: - self.parent = parent - self.vgroups = vgroups - self.bgroups = bgroups - self.igroups = igroups - super().__init__(parent.spaces, parent.space_dims, parent.references) - - def _rewrite_groups(self, groups: Iterable[str]) -> Iterator[str]: - for group in groups: - if group in self.vgroups: - yield from self.vgroups[group].split(',') - else: - yield group - - def get_groups(self, *groups: str) -> Topology: - return self.parent.get_groups(*self._rewrite_groups(groups)) - - def take_unchecked(self, indices: numpy.ndarray) -> Topology: - # NOTE: the groups are gone after take - return self.parent.take_unchecked(indices) - - def slice_unchecked(self, indices: slice, idim: int) -> Topology: - # NOTE: the groups are gone after take - return self.parent.slice_unchecked(indices, idim) - - @property - def f_index(self) -> function.Array: - return self.parent.f_index - - @property - def f_coords(self) -> function.Array: - return self.parent.f_coords - - @property - def connectivity(self) -> Sequence[Sequence[int]]: - return self.parent.connectivity - - def basis(self, name: str, *args, **kwargs) -> function.Basis: - return self.parent.basis(name, *args, **kwargs) - - def sample(self, ischeme: str, degree: int) -> Sample: - return self.parent.sample(ischeme, degree) - - def refine_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return _WithGroupAliases(self.parent.refine_spaces(spaces), self.vgroups, self.bgroups, self.igroups) - - def indicator(self, subtopo: Union[str, Topology]) -> Topology: - if isinstance(subtopo, str): - return self.parent.indicator(','.join(self._rewrite_groups(subtopo.split(',')))) - else: - return super().indicator(subtopo) - - def locate(self, geom, coords, *, tol=0, eps=0, maxiter=0, arguments=None, weights=None, maxdist=None, ischeme=None, scale=None, skip_missing=False) -> Sample: - return self.parent.locate(geom, coords, tol=tol, eps=eps, maxiter=maxiter, arguments=arguments, weights=weights, maxdist=maxdist, ischeme=ischeme, scale=scale, skip_missing=skip_missing) - - def boundary_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return _WithGroupAliases(self.parent.boundary_spaces_unchecked(spaces), self.bgroups, types.frozendict({}), types.frozendict({})) - - def interfaces_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: - return _WithGroupAliases(self.parent.interfaces_spaces_unchecked(spaces), self.igroups, types.frozendict({}), types.frozendict({})) - - -class TransformChainsTopology(Topology): - 'base class for topologies with transform chains' - - __slots__ = 'space', 'transforms', 'opposites' - __cache__ = 'border_transforms', 'boundary', 'interfaces' - - @types.apply_annotations - def __init__(self, space: _strictspace, references: types.strict[References], transforms: transformseq.stricttransforms, opposites: transformseq.stricttransforms): - assert transforms.todims == opposites.todims - assert references.ndims == opposites.fromdims == transforms.fromdims - assert len(references) == len(transforms) == len(opposites) - self.space = space - self.transforms = transforms - self.opposites = opposites - super().__init__((space,), (transforms.todims,), references) - - def empty_like(self) -> 'TransformChainsTopology': - return EmptyTopology(self.space, self.transforms.todims, self.ndims) - - def get_groups(self, *groups): - return self.empty_like() - - def take_unchecked(self, indices: numpy.ndarray) -> 'TransformChainsTopology': - indices = types.frozenarray(indices, dtype=int) - return TransformChainsTopology(self.space, self.references.take(indices), self.transforms[indices], self.opposites[indices]) - - def __invert__(self): - return OppositeTopology(self) - - def __or__(self, other): - if not isinstance(other, TransformChainsTopology) or other.space != self.space and other.ndims != self.ndims: - return super().__or__(other) - return other if not self \ - else self if not other \ - else NotImplemented if isinstance(other, UnionTopology) \ - else UnionTopology((self, other)) - - __ror__ = lambda self, other: self.__or__(other) - - def __and__(self, other): - if not isinstance(other, TransformChainsTopology) or other.space != self.space: - return super().__and__(other) - keep_self = numpy.array(list(map(other.transforms.contains_with_tail, self.transforms)), dtype=bool) - if keep_self.all(): - return self - keep_other = numpy.array(list(map(self.transforms.contains_with_tail, other.transforms)), dtype=bool) - if keep_other.all(): - return other - ind_self = types.frozenarray(keep_self.nonzero()[0], copy=False) - ind_other = types.frozenarray([i for i, trans in enumerate(other.transforms) if keep_other[i] and not self.transforms.contains(trans)], dtype=int) - # The last condition is to avoid duplicate elements. Note that we could - # have reused the result of an earlier lookup to avoid a new (using index - # instead of contains) but we choose to trade some speed for simplicity. - references = self.references.take(ind_self).chain(other.references.take(ind_other)) - transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], self.transforms.todims, self.ndims) - opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], self.transforms.todims, self.ndims) - return TransformChainsTopology(self.space, references, transforms, opposites) - - __rand__ = lambda self, other: self.__and__(other) - - def __add__(self, other): - return self | other - - def __sub__(self, other): - assert isinstance(other, TransformChainsTopology) and other.space == self.space and other.ndims == self.ndims - return other.__rsub__(self) - - def __rsub__(self, other): - assert isinstance(other, TransformChainsTopology) and other.space == self.space and other.ndims == self.ndims - return other - other.subset(self, newboundary=getattr(self, 'boundary', None)) - - @property - def border_transforms(self): - indices = set() - for btrans in self.boundary.transforms: - try: - ielem, tail = self.transforms.index_with_tail(btrans) - except ValueError: - pass - else: - indices.add(ielem) - return self.transforms[numpy.array(sorted(indices), dtype=int)] - - @property - def _index_coords(self): - index = function.transforms_index(self.space, self.transforms) - coords = function.transforms_coords(self.space, self.transforms) - return index, coords - - @property - def f_index(self): - return self._index_coords[0] - - @property - def f_coords(self): - return self._index_coords[1] - - def sample(self, ischeme, degree): - 'Create sample.' - - points = PointsSequence.from_iter((ischeme(reference, degree) for reference in self.references), self.ndims) if callable(ischeme) \ - else self.references.getpoints(ischeme, degree) - transforms = self.transforms, - if len(self.transforms) == 0 or self.opposites != self.transforms: - transforms += self.opposites, - return Sample.new(self.space, transforms, points) - - def refined_by(self, refine): - return HierarchicalTopology(self, [numpy.arange(len(self))]).refined_by(refine) + references = parent.references.take(self.indices) + coord_system = parent.coord_system.take(self.indices) + opposite = parent.opposite.take(self.indices) + super().__init__(parent.ref_coord_system, references, coord_system, opposite) @property - def refined(self): - return RefinedTopology(self) - - def refine_spaces_unchecked(self, spaces: Iterable[str]) -> 'TransformChainsTopology': - # Since every `TransformChainsTopology` has exactly one space, we implement - # `refine_spaces` here for all subclasses and return `self.refined` if the - # space of this topology is in the given `spaces`. Subclasses can redefine - # the `refined` property. - if not spaces: - return self - return self.refined - - def refine(self, n): - if numpy.iterable(n): - assert len(n) == self.ndims - assert all(ni == n[0] for ni in n) - n = n[0] - return self if n <= 0 else self.refined.refine(n-1) - - def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None): - if arguments is None: - arguments = {} - - refs = [] - if leveltopo is None: - ielem_arg = evaluable.Argument('_trim_index', (), dtype=int) - coordinates = self.references.getpoints('vertex', maxrefine).get_evaluable_coords(ielem_arg) - transform_chains = self.transforms.get_evaluable(ielem_arg), self.opposites.get_evaluable(ielem_arg) - levelset = levelset.lower(function.LowerArgs.for_space(self.space, transform_chains, coordinates)).optimized_for_numpy - with log.iter.percentage('trimming', range(len(self)), self.references) as items: - for ielem, ref in items: - levels = levelset.eval(_trim_index=ielem, **arguments) - refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) - else: - log.info('collecting leveltopo elements') - coordinates = evaluable.Points(evaluable.NPoints(), self.ndims) - transform_chain = transform.EvaluableTransformChain.from_argument('trans', self.transforms.todims, self.transforms.fromdims) - levelset = levelset.lower(function.LowerArgs.for_space(self.space, (transform_chain, transform_chain), coordinates)).optimized_for_numpy - bins = [set() for ielem in range(len(self))] - for trans in leveltopo.transforms: - ielem, tail = self.transforms.index_with_tail(trans) - bins[ielem].add(tail) - fcache = cache.WrapperCache() - with log.iter.percentage('trimming', self.references, self.transforms, bins) as items: - for ref, trans, ctransforms in items: - levels = numpy.empty(ref.nvertices_by_level(maxrefine)) - cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine)) - # confirm cover and greedily optimize order - mask = numpy.ones(len(levels), dtype=bool) - while mask.any(): - imax = numpy.argmax([mask[indices].sum() for tail, points, indices in cover]) - tail, points, indices = cover.pop(imax) - levels[indices] = levelset.eval(trans=trans + tail, _points=points, **arguments) - mask[indices] = False - refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) - log.debug('cache', fcache.stats) - return SubsetTopology(self, refs, newboundary=name) - - def subset(self, topo, newboundary=None, strict=False): - refs = [ref.empty for ref in self.references] - for ref, trans in zip(topo.references, topo.transforms): - try: - ielem = self.transforms.index(trans) - except ValueError: - assert not strict, 'elements do not form a strict subset' - else: - subref = self.references[ielem] & ref - if strict: - assert subref == ref, 'elements do not form a strict subset' - refs[ielem] = subref - if not any(refs): - return EmptyTopology(self.space, self.transforms.todims, self.ndims) - return SubsetTopology(self, refs, newboundary) - - def withgroups(self, vgroups={}, bgroups={}, igroups={}, pgroups={}): - return WithGroupsTopology(self, vgroups, bgroups, igroups, pgroups) if vgroups or bgroups or igroups or pgroups else self - - def indicator(self, subtopo): - if isinstance(subtopo, str): - subtopo = self[subtopo] - values = numpy.zeros([len(self)], dtype=int) - values[numpy.fromiter(map(self.transforms.index, subtopo.transforms), dtype=int)] = 1 - return function.get(values, 0, self.f_index) - - @log.withcontext - def locate(self, geom, coords, *, tol=0, eps=0, maxiter=0, arguments=None, weights=None, maxdist=None, ischeme=None, scale=None, skip_missing=False): - if ischeme is not None: - warnings.deprecation('the ischeme argument is deprecated and will be removed in future') - if scale is not None: - warnings.deprecation('the scale argument is deprecated and will be removed in future') - if max(tol, eps) <= 0: - raise ValueError('locate requires either tol or eps to be strictly positive') - coords = numpy.asarray(coords, dtype=float) - if geom.ndim == 0: - geom = geom[_] - coords = coords[..., _] - if not geom.shape == coords.shape[1:] == (self.ndims,): - raise ValueError('invalid geometry or point shape for {}D topology'.format(self.ndims)) - arguments = dict(arguments or ()) - centroids = self.sample('_centroid', None).eval(geom, **arguments) - assert len(centroids) == len(self) - ielems = parallel.shempty(len(coords), dtype=int) - points = parallel.shempty((len(coords), len(geom)), dtype=float) - _ielem = evaluable.Argument('_locate_ielem', shape=(), dtype=int) - _point = evaluable.Argument('_locate_point', shape=(self.ndims,)) - transform_chains = self.transforms.get_evaluable(_ielem), self.opposites.get_evaluable(_ielem) - egeom = geom.lower(function.LowerArgs.for_space(self.space, transform_chains, _point)) - xJ = evaluable.Tuple((egeom, evaluable.derivative(egeom, _point))).simplified - if skip_missing: - if weights is not None: - raise ValueError('weights and skip_missing are mutually exclusive') - missing = parallel.shzeros(len(coords), dtype=bool) - with parallel.ctxrange('locating', len(coords)) as ipoints: - for ipoint in ipoints: - xt = coords[ipoint] # target - dist = numpy.linalg.norm(centroids - xt, axis=1) - for ielem in numpy.argsort(dist) if maxdist is None \ - else sorted((dist < maxdist).nonzero()[0], key=dist.__getitem__): - ref = self.references[ielem] - arguments['_locate_ielem'] = ielem - arguments['_locate_point'] = p = numpy.array(ref.centroid) - ex = ep = numpy.inf - iiter = 0 - while ex > tol and ep > eps: # newton loop - if iiter > maxiter > 0: - break # maximum number of iterations reached - iiter += 1 - xp, Jp = xJ.eval(**arguments) - dx = xt - xp - ex0 = ex - ex = numpy.linalg.norm(dx) - if ex >= ex0: - break # newton is diverging - try: - dp = numpy.linalg.solve(Jp, dx) - except numpy.linalg.LinAlgError: - break # jacobian is singular - ep = numpy.linalg.norm(dp) - p += dp # NOTE: modifies arguments['_locate_point'] in place - else: - if ref.inside(p, max(eps, ep)): - ielems[ipoint] = ielem - points[ipoint] = p - break - else: - if skip_missing: - missing[ipoint] = True - else: - raise LocateError('failed to locate point: {}'.format(xt)) - if skip_missing: - ielems = ielems[~missing] - points = points[~missing] - return self._sample(ielems, points, weights) + def connectivity(self): + parent = self.parent.connectivity + renumber = numpy.full((len(parent) + 1,), -1) + renumber[self.indices] = numpy.arange(len(self.indices)) + return tuple(types.frozenarray(numpy.take(renumber, parent[i]), copy=False) for i in self.indices) + + def _take_subtopo(self, subtopo: Topology) -> Topology: + if subtopo: + trans = subtopo.coord_system.trans_to(self.parent.coord_system) + indices = trans.unapply_indices(self.indices) + subtopo = subtopo.take(indices) + return subtopo - def _sample(self, ielems, coords, weights=None): - index = numpy.argsort(ielems, kind='stable') - sorted_ielems = ielems[index] - offsets = [0, *(sorted_ielems[:-1] != sorted_ielems[1:]).nonzero()[0]+1, len(index)] + def get_groups(self, *groups: str) -> Topology: + return self._take_subtopo(self.parent.get_groups(*groups)) - unique_ielems = sorted_ielems[offsets[:-1]] - transforms = self.transforms[unique_ielems], - if len(self.transforms) == 0 or self.opposites != self.transforms: - transforms += self.opposites[unique_ielems], + def take_unchecked(self, __indices: numpy.ndarray) -> Topology: + return self.topo.take_unchecked(numpy.take(self.indices, __indices)) - slices = [index[n:m] for n, m in zip(offsets[:-1], offsets[1:])] - points_ = PointsSequence.from_iter([points.CoordsPoints(coords[s]) for s in slices] if weights is None - else [points.CoordsWeightsPoints(coords[s], weights[s]) for s in slices], self.ndims) + def __invert__(self): + return (~self.parent).take(self.indices) - return Sample.new(self.space, transforms, points_, index) + def sample(self, ischeme: str, degree: int) -> Sample: + return self.parent.sample(ischeme, degree).take_elements(self.indices) - def boundary_spaces_unchecked(self, spaces: FrozenSet[str]) -> 'TransformChainsTopology': - return self.boundary + def refine_spaces_unchecked(self, __spaces: FrozenSet[str]) -> Topology: + return self._take_subtopo(self.parent.refine_spaces_unchecked(__spaces)) - @property - @log.withcontext - def boundary(self): - references = [] - selection = [] - iglobaledgeiter = itertools.count() - refs_touched = False - for ielem, (ioppelems, elemref, elemtrans) in enumerate(zip(self.connectivity, self.references, self.transforms)): - for (edgetrans, edgeref), ioppelem, iglobaledge in zip(elemref.edges, ioppelems, iglobaledgeiter): - if edgeref: - if ioppelem == -1: - references.append(edgeref) - selection.append(iglobaledge) - else: - ioppedge = util.index(self.connectivity[ioppelem], ielem) - ref = edgeref - self.references[ioppelem].edge_refs[ioppedge] - if ref: - references.append(ref) - selection.append(iglobaledge) - refs_touched = True - selection = types.frozenarray(selection, dtype=int) - if refs_touched: - references = References.from_iter(references, self.ndims-1) - else: - references = self.references.edges[selection] - transforms = self.transforms.edges(self.references)[selection] - return TransformChainsTopology(self.space, references, transforms, transforms) + def basis(self, name, *args, **kwargs): + basis = self.parent.basis(name, *args, **kwargs) + return function.PrunedBasis(basis, self.indices, self.f_index, self.f_coords) - def interfaces_spaces_unchecked(self, spaces: FrozenSet[str]) -> 'TransformChainsTopology': - return self.interfaces - @property - @log.withcontext - def interfaces(self): - references = [] - selection = [] - oppselection = [] - iglobaledgeiter = itertools.count() - refs_touched = False - edges = self.transforms.edges(self.references) - if self.references.isuniform: - _nedges = self.references[0].nedges - offset = lambda ielem: ielem * _nedges - else: - offset = numpy.cumsum([0]+list(ref.nedges for ref in self.references)).__getitem__ - for ielem, (ioppelems, elemref, elemtrans) in enumerate(zip(self.connectivity, self.references, self.transforms)): - for (edgetrans, edgeref), ioppelem, iglobaledge in zip(elemref.edges, ioppelems, iglobaledgeiter): - if edgeref and -1 < ioppelem < ielem: - ioppedge = util.index(self.connectivity[ioppelem], ielem) - oppedgetrans, oppedgeref = self.references[ioppelem].edges[ioppedge] - ref = oppedgeref and edgeref & oppedgeref - if ref: - references.append(ref) - selection.append(iglobaledge) - oppselection.append(offset(ioppelem)+ioppedge) - if ref != edgeref: - refs_touched = True - selection = types.frozenarray(selection, dtype=int) - oppselection = types.frozenarray(oppselection, dtype=int) - if refs_touched: - references = References.from_iter(references, self.ndims-1) - else: - references = self.references.edges[selection] - return TransformChainsTopology(self.space, references, edges[selection], edges[oppselection]) +class _WithConnectivity(Topology): - def basis_spline(self, degree): - assert degree == 1 - return self.basis('std', degree) + def __init__(self, topo: Topology, connectivity): + self.topo = topo + self.connectivity = connectivity + super().__init__(topo.ref_coord_system, topo.references, topo.coord_system, topo.opposite) - def _basis_c0_structured(self, name, degree): - 'C^0-continuous shape functions with lagrange stucture' + def get_groups(self, *groups: str) -> Topology: + return self.topo.get_groups(*groups) - assert numeric.isint(degree) and degree >= 0 + def take_unchecked(self, __indices: numpy.ndarray) -> Topology: + return self.topo.take_unchecked(__indices) - if degree == 0: - raise ValueError('Cannot build a C^0-continuous basis of degree 0. Use basis \'discont\' instead.') + def slice_unchecked(self, __s: slice, __idim: int) -> Topology: + return self.topo.slice_unchecked(__s, __idim) - coeffs = [ref.get_poly_coeffs(name, degree=degree) for ref in self.references] - offsets = numpy.cumsum([0] + [len(c) for c in coeffs]) - dofmap = numpy.repeat(-1, offsets[-1]) - for ielem, ioppelems in enumerate(self.connectivity): - for iedge, jelem in enumerate(ioppelems): # loop over element neighbors and merge dofs - if jelem < ielem: - continue # either there is no neighbor along iedge or situation will be inspected from the other side - jedge = util.index(self.connectivity[jelem], ielem) - idofs = offsets[ielem] + self.references[ielem].get_edge_dofs(degree, iedge) - jdofs = offsets[jelem] + self.references[jelem].get_edge_dofs(degree, jedge) - for idof, jdof in zip(idofs, jdofs): - while dofmap[idof] != -1: - idof = dofmap[idof] - while dofmap[jdof] != -1: - jdof = dofmap[jdof] - if idof != jdof: - dofmap[max(idof, jdof)] = min(idof, jdof) # create left-looking pointer - # assign dof numbers left-to-right - ndofs = 0 - for i, n in enumerate(dofmap): - if n == -1: - dofmap[i] = ndofs - ndofs += 1 - else: - dofmap[i] = dofmap[n] + def __invert__(self): + return _WithConnectivity(~self.topo, self.connectivity) - elem_slices = map(slice, offsets[:-1], offsets[1:]) - dofs = tuple(types.frozenarray(dofmap[s]) for s in elem_slices) - return function.PlainBasis(coeffs, dofs, ndofs, self.f_index, self.f_coords) + @property + def f_index(self) -> function.Array: + return self.topo.f_index - def basis_lagrange(self, degree): - 'lagrange shape functions' - return self._basis_c0_structured('lagrange', degree) + @property + def f_coords(self) -> function.Array: + return self.topo.f_coords - def basis_bernstein(self, degree): - 'bernstein shape functions' - return self._basis_c0_structured('bernstein', degree) + def sample(self, ischeme: str, degree: int) -> Sample: + return self.topo.sample(ischeme, degree) - basis_std = basis_bernstein + def refine_spaces_unchecked(self, __spaces: FrozenSet[str]) -> Topology: + # TODO: add connectivity + return self.topo.refine_spaces_unchecked(__spaces) + + def trim(self, *args, **kwargs) -> Topology: + return self.topo.trim(*args, **kwargs) + + def subset(self, *args, **kwargs) -> Topology: + # TODO: add connectivity + return self.topo.subset(*args, **kwargs) + def indicator(self, subtopo): + return self.topo.indicator(subtopo) + + def select(self, *args, **kwargs) -> Topology: + return self.topo.select(*args, **kwargs) -stricttopology = types.strict[Topology] + def locate(self, *args, **kwargs): + return self.topo.locate(*args, **kwargs) class LocateError(Exception): pass -class WithGroupsTopology(TransformChainsTopology): +class _WithGroupsTopology(Topology): 'item topology' __slots__ = 'basetopo', 'vgroups', 'bgroups', 'igroups', 'pgroups' __cache__ = 'refined', - @types.apply_annotations - def __init__(self, basetopo: stricttopology, vgroups: types.frozendict = {}, bgroups: types.frozendict = {}, igroups: types.frozendict = {}, pgroups: types.frozendict = {}): + def __init__(self, basetopo: Topology, vgroups: Optional[Dict[str, Union[str, Topology]]] = None, bgroups: Optional[Dict[str, Union[str, Topology]]] = None, igroups: Optional[Dict[str, Union[str, Topology]]] = None, pgroups: Optional[Dict[str, Union[str, Topology]]] = None): assert vgroups or bgroups or igroups or pgroups self.basetopo = basetopo - self.vgroups = vgroups - self.bgroups = bgroups - self.igroups = igroups - self.pgroups = pgroups - super().__init__(basetopo.space, basetopo.references, basetopo.transforms, basetopo.opposites) - assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, TransformChainsTopology) and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) - - def __len__(self): - return len(self.basetopo) + self.vgroups = vgroups or {} + self.bgroups = bgroups or {} + self.igroups = igroups or {} + self.pgroups = pgroups or {} + super().__init__(basetopo.ref_coord_system, basetopo.references, basetopo.coord_system, basetopo.opposite) + assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, Topology) and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: topos = [] basegroups = [] for group in groups: if group in self.vgroups: item = self.vgroups[group] - assert isinstance(item, (TransformChainsTopology, str)) - if isinstance(item, TransformChainsTopology): + assert isinstance(item, (Topology, str)) + if isinstance(item, Topology): topos.append(item) else: basegroups.extend(item.split(',')) @@ -1726,20 +1651,12 @@ def get_groups(self, *groups: str) -> TransformChainsTopology: topos.append(self.basetopo.get_groups(*basegroups)) return functools.reduce(operator.or_, topos, self.empty_like()) - def take_unchecked(self, __indices: numpy.ndarray) -> TransformChainsTopology: + def take_unchecked(self, __indices: numpy.ndarray) -> Topology: return self.basetopo.take_unchecked(__indices) - def slice_unchecked(self, __s: slice, __idim: int) -> TransformChainsTopology: + def slice_unchecked(self, __s: slice, __idim: int) -> Topology: return self.basetopo.slice_unchecked(__s, __idim) - @property - def border_transforms(self): - return self.basetopo.border_transforms - - @property - def connectivity(self): - return self.basetopo.connectivity - @property def boundary(self): return self.basetopo.boundary.withgroups(self.bgroups) @@ -1749,7 +1666,8 @@ def interfaces(self): baseitopo = self.basetopo.interfaces igroups = self.igroups.copy() for name, topo in self.igroups.items(): - if isinstance(topo, TransformChainsTopology): + if isinstance(topo, Topology): + raise NotImplementedError # last minute orientation fix s = [] for transs in zip(topo.transforms, topo.opposites): @@ -1770,7 +1688,7 @@ def points(self): ptopos = [] pnames = [] topo = self - while isinstance(topo, WithGroupsTopology): + while isinstance(topo, _WithGroupsTopology): for pname, ptopo in topo.pgroups.items(): if pname not in pnames: pnames.append(pname) @@ -1783,404 +1701,204 @@ def basis(self, name, *args, **kwargs): @property def refined(self): - groups = [{name: topo.refined if isinstance(topo, TransformChainsTopology) else topo for name, topo in groups.items()} for groups in (self.vgroups, self.bgroups, self.igroups, self.pgroups)] + groups = [{name: topo.refined if isinstance(topo, Topology) else topo for name, topo in groups.items()} for groups in (self.vgroups, self.bgroups, self.igroups, self.pgroups)] return self.basetopo.refined.withgroups(*groups) def locate(self, geom, coords, **kwargs): return self.basetopo.locate(geom, coords, **kwargs) + def sample(self, *args, **kwargs): + return self.basetopo.sample(*args, **kwargs) -class OppositeTopology(TransformChainsTopology): + +class OppositeTopology(Topology): 'opposite topology' __slots__ = 'basetopo', def __init__(self, basetopo): self.basetopo = basetopo - super().__init__(basetopo.space, basetopo.references, basetopo.opposites, basetopo.transforms) + super().__init__(basetopo.ref_coord_system, basetopo.references, basetopo.opposite, basetopo.coord_system) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: return ~(self.basetopo.get_groups(*groups)) - def take_unchecked(self, __indices: numpy.ndarray) -> TransformChainsTopology: + def take_unchecked(self, __indices: numpy.ndarray) -> Topology: return ~(self.basetopo.take_unchecked(__indices)) - def slice_unchecked(self, __s: slice, __idim: int) -> TransformChainsTopology: + def slice_unchecked(self, __s: slice, __idim: int) -> Topology: return ~(self.basetopo.slice_unchecked(__s, __idim)) - def __len__(self): - return len(self.basetopo) - def __invert__(self): return self.basetopo -class EmptyTopology(TransformChainsTopology): - 'empty topology' - - __slots__ = () - - @types.apply_annotations - def __init__(self, space: _strictspace, todims: types.strictint, fromdims: types.strictint): - super().__init__(space, References.empty(fromdims), transformseq.EmptyTransforms(todims, fromdims), transformseq.EmptyTransforms(todims, fromdims)) - - def __or__(self, other): - if self.space != other.space or self.ndims != other.ndims: - return NotImplemented - return other - - def __rsub__(self, other): - return other - - -def StructuredLine(space, root: transform.stricttransformitem, i: types.strictint, j: types.strictint, periodic: bool = False, bnames: types.tuple[types.strictstr] = None): - if bnames is None: - bnames = '_structured_line_dummy_boundary_left', '_structured_line_dummy_boundary_right' - return StructuredTopology(space, root, axes=(transformseq.DimAxis(i, j, j if periodic else 0, periodic),), nrefine=0, bnames=(bnames,)) - - -class StructuredTopology(TransformChainsTopology): - 'structured topology' +class _Line(Topology): + 'structured line topology' - __slots__ = 'root', 'axes', 'nrefine', 'shape', '_bnames', '_asaffine_geom', '_asaffine_retval' + __slots__ = '_bnames', '_periodic', '_asaffine_geom', '_asaffine_retval' __cache__ = 'connectivity', 'boundary', 'interfaces' - @types.apply_annotations - def __init__(self, space, root: transform.stricttransformitem, axes: types.tuple[types.strict[transformseq.Axis]], nrefine: types.strictint = 0, bnames: types.tuple[types.tuple[types.strictstr]] = (('left', 'right'), ('bottom', 'top'), ('front', 'back'))): + def __init__(self, ref_coord_system: Tuple[Tuple[str, CoordSystem], ...], coord_system: CoordSystem, opposite: CoordSystem, bnames: Tuple[str, str], periodic: bool): 'constructor' - assert all(len(bname) == 2 for bname in bnames) - - self.root = root - self.axes = axes - self.nrefine = nrefine - self.shape = tuple(axis.j - axis.i for axis in self.axes if axis.isdim) self._bnames = bnames + self._periodic = periodic + references = References.uniform(element.getsimplex(1), len(coord_system)) + super().__init__(ref_coord_system, references, coord_system, opposite) - references = References.uniform(util.product(element.getsimplex(1 if axis.isdim else 0) for axis in self.axes), len(self)) - transforms = transformseq.StructuredTransforms(self.root, self.axes, self.nrefine) - nbounds = len(self.axes) - len(self.shape) - if nbounds == 0: - opposites = transforms + @property + def connectivity(self): + connectivity = numpy.stack([numpy.arange(1, len(self) + 1), numpy.arange(-1, len(self) - 1)], axis=1) + if len(self) == 0: + pass + elif self._periodic: + connectivity[0, 1] = len(self) - 1 + connectivity[-1, 0] = 0 else: - axes = [axis.opposite(nbounds-1) for axis in self.axes] - opposites = transformseq.StructuredTransforms(self.root, axes, self.nrefine) - - super().__init__(space, references, transforms, opposites) + connectivity[0, 1] = -1 + connectivity[-1, 0] = -1 + return types.frozenarray(connectivity, copy=False) def __repr__(self): - return '{}<{}>'.format(type(self).__qualname__, 'x'.join(str(axis.j-axis.i)+('p' if axis.isperiodic else '') for axis in self.axes if axis.isdim)) - - def __len__(self): - return numpy.prod(self.shape, dtype=int) + return '{}<{}{}>'.format(type(self).__qualname__, len(self), 'p' if self._periodic else '') - def slice_unchecked(self, indices: slice, idim: int) -> TransformChainsTopology: + def slice_unchecked(self, indices: slice, idim: int) -> Topology: if indices == slice(None): return self - axes = [] - for axis in self.axes: - if axis.isdim: - if idim == 0: - axis = axis.getitem(indices) - idim -= 1 - axes.append(axis) - return StructuredTopology(self.space, self.root, axes, self.nrefine, bnames=self._bnames) + return _Line( + self.ref_coord_system, + self.coord_system.slice(indices), + self.opposite.slice(indices), + self._bnames, + False) @property def periodic(self): - dimaxes = (axis for axis in self.axes if axis.isdim) - return tuple(idim for idim, axis in enumerate(dimaxes) if axis.isdim and axis.isperiodic) - - @property - def connectivity(self): - connectivity = numpy.empty(self.shape+(self.ndims, 2), dtype=int) - connectivity[...] = -1 - ielems = numpy.arange(len(self)).reshape(self.shape) - for idim in range(self.ndims): - s = (slice(None),)*idim - s1 = s + (slice(1, None),) - s2 = s + (slice(0, -1),) - connectivity[s2+(..., idim, 0)] = ielems[s1] - connectivity[s1+(..., idim, 1)] = ielems[s2] - if idim in self.periodic: - connectivity[s+(-1, ..., idim, 0)] = ielems[s+(0,)] - connectivity[s+(0, ..., idim, 1)] = ielems[s+(-1,)] - return types.frozenarray(connectivity.reshape(len(self), self.ndims*2), copy=False) + return (0,) if self._periodic else () @property def boundary(self): - 'boundary' + references = References.uniform(element.getsimplex(0), 1) + coord_system_left = self.coord_system.edges(Simplex.line, 0).take([1]) + coord_system_right = self.coord_system.edges(Simplex.line, 0).take([2 * len(self) - 2]) + left = _WithName(Topology(self.ref_coord_system, references, coord_system_left, coord_system_left), self._bnames[0]) + right = _WithName(Topology(self.ref_coord_system, references, coord_system_right, coord_system_right), self._bnames[1]) + return Topology.disjoint_union(left, right) - nbounds = len(self.axes) - self.ndims - btopos = [StructuredTopology(self.space, root=self.root, axes=self.axes[:idim] + (bndaxis,) + self.axes[idim+1:], nrefine=self.nrefine, bnames=self._bnames) - for idim, axis in enumerate(self.axes) - for bndaxis in axis.boundaries(nbounds)] - if not btopos: - return EmptyTopology(self.space, self.transforms.todims, self.ndims-1) - bnames = [bname for bnames, axis in zip(self._bnames, self.axes) if axis.isdim and not axis.isperiodic for bname in bnames] - return DisjointUnionTopology(btopos, bnames) + def boundary_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology: + if self._periodic: + return Topology.empty(self.ref_coord_system, 0) + return self.boundary @property def interfaces(self): 'interfaces' - assert self.ndims > 0, 'zero-D topology has no interfaces' - itopos = [] - nbounds = len(self.axes) - self.ndims - for idim, axis in enumerate(self.axes): - if not axis.isdim: - continue - axes = (*self.axes[:idim], axis.intaxis(nbounds, side=True), *self.axes[idim+1:]) - oppaxes = (*self.axes[:idim], axis.intaxis(nbounds, side=False), *self.axes[idim+1:]) - itransforms = transformseq.StructuredTransforms(self.root, axes, self.nrefine) - iopposites = transformseq.StructuredTransforms(self.root, oppaxes, self.nrefine) - ireferences = References.uniform(util.product(element.getsimplex(1 if a.isdim else 0) for a in axes), len(itransforms)) - itopos.append(TransformChainsTopology(self.space, ireferences, itransforms, iopposites)) - assert len(itopos) == self.ndims - return DisjointUnionTopology(itopos, names=['dir{}'.format(idim) for idim in range(self.ndims)]) - - def _basis_spline(self, degree, knotvalues=None, knotmultiplicities=None, continuity=-1, periodic=None): - 'spline with structure information' - - if periodic is None: - periodic = self.periodic - - if numeric.isint(degree): - degree = [degree]*self.ndims - - assert len(degree) == self.ndims - - if knotvalues is None or isinstance(knotvalues[0], (int, float)): - knotvalues = [knotvalues] * self.ndims - else: - assert len(knotvalues) == self.ndims - - if knotmultiplicities is None or isinstance(knotmultiplicities[0], int): - knotmultiplicities = [knotmultiplicities] * self.ndims - else: - assert len(knotmultiplicities) == self.ndims - - if not numpy.iterable(continuity): - continuity = [continuity] * self.ndims - else: - assert len(continuity) == self.ndims - - vertex_structure = numpy.array(0) - stdelems = [] - dofshape = [] - slices = [] - cache = {} - for idim in range(self.ndims): - p = degree[idim] - n = self.shape[idim] - isperiodic = idim in periodic - - c = continuity[idim] - if c < 0: - c += p - assert -1 <= c < p - - k = knotvalues[idim] - if k is None: # Defaults to uniform spacing - k = numpy.arange(n+1) - else: - k = numpy.array(k) - while len(k) < n+1: - k_ = numpy.empty(len(k)*2-1) - k_[::2] = k - k_[1::2] = (k[:-1] + k[1:]) / 2 - k = k_ - assert len(k) == n+1, 'knot values do not match the topology size' - - m = knotmultiplicities[idim] - if m is None: # Defaults to open spline without internal repetitions - m = numpy.repeat(p-c, n+1) - if not isperiodic: - m[0] = m[-1] = p+1 - else: - m = numpy.array(m) - assert min(m) > 0 and max(m) <= p+1, 'incorrect multiplicity encountered' - while len(m) < n+1: - m_ = numpy.empty(len(m)*2-1, dtype=int) - m_[::2] = m - m_[1::2] = p-c - m = m_ - assert len(m) == n+1, 'knot multiplicity do not match the topology size' - - if not isperiodic: - nd = sum(m)-p-1 - npre = p+1-m[0] # Number of knots to be appended to front - npost = p+1-m[-1] # Number of knots to be appended to rear - m[0] = m[-1] = p+1 - else: - assert m[0] == m[-1], 'Periodic spline multiplicity expected' - assert m[0] < p+1, 'Endpoint multiplicity for periodic spline should be p or smaller' - - nd = sum(m[:-1]) - npre = npost = 0 - k = numpy.concatenate([k[-p-1:-1]+k[0]-k[-1], k, k[1:1+p]-k[0]+k[-1]]) - m = numpy.concatenate([m[-p-1:-1], m, m[1:1+p]]) - - km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) - assert len(km) == sum(m) - assert nd > 0, 'No basis functions defined. Knot vector too short.' - - stdelems_i = [] - slices_i = [] - offsets = numpy.cumsum(m[:-1])-p - if isperiodic: - offsets = offsets[p:-p] - offset0 = offsets[0]+npre - - for offset in offsets: - start = max(offset0-offset, 0) # Zero unless prepending influence - stop = p+1-max(offset-offsets[-1]+npost, 0) # Zero unless appending influence - slices_i.append(slice(offset-offset0+start, offset-offset0+stop)) - lknots = km[offset:offset+2*p] - km[offset] # Copy operation required - if p: # Normalize for optimized caching - lknots /= lknots[-1] - key = (tuple(numeric.round(lknots*numpy.iinfo(numpy.int32).max)), p) - try: - coeffs = cache[key] - except KeyError: - coeffs = cache[key] = self._localsplinebasis(lknots) - stdelems_i.append(coeffs[start:stop]) - stdelems.append(stdelems_i) - - numbers = numpy.arange(nd) - if isperiodic: - numbers = numpy.concatenate([numbers, numbers[:p]]) - vertex_structure = vertex_structure[..., _]*nd+numbers - dofshape.append(nd) - slices.append(slices_i) - - # Cache effectivity - log.debug('Local knot vector cache effectivity: {}'.format(100*(1.-len(cache)/float(sum(self.shape))))) - - # deduplicate stdelems and compute tensorial products `unique` with indices `index` - # such that unique[index[i,j]] == poly_outer_product(stdelems[0][i], stdelems[1][j]) - index = numpy.array(0) - for stdelems_i in stdelems: - unique_i, index_i = util.unique(stdelems_i, key=types.arraydata) - unique = unique_i if not index.ndim \ - else [numeric.poly_outer_product(a, b) for a in unique for b in unique_i] - index = index[..., _] * len(unique_i) + index_i - - coeffs = [unique[i] for i in index.flat] - dofmap = [types.frozenarray(vertex_structure[S].ravel(), copy=False) for S in itertools.product(*slices)] - return coeffs, dofmap, dofshape + raise NotImplementedError def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicities=None, continuity=-1, periodic=None): 'spline basis' - if removedofs is None or isinstance(removedofs[0], int): - removedofs = [removedofs] * self.ndims - else: - assert len(removedofs) == self.ndims + if removedofs is not None and not isinstance(removedofs[0], int): + removedofs, = removedofs if periodic is None: - periodic = self.periodic - - if numeric.isint(degree): - degree = [degree]*self.ndims + periodic = self._periodic + else: + periodic = 0 in periodic - assert len(degree) == self.ndims + if not numeric.isint(degree): + degree, = degree - if knotvalues is None or isinstance(knotvalues[0], (int, float)): - knotvalues = [knotvalues] * self.ndims - else: - assert len(knotvalues) == self.ndims + if knotvalues is not None and not isinstance(knotvalues[0], (int, float)): + knotvalues, = knotvalues - if knotmultiplicities is None or isinstance(knotmultiplicities[0], int): - knotmultiplicities = [knotmultiplicities] * self.ndims - else: - assert len(knotmultiplicities) == self.ndims + if knotmultiplicities is not None and not isinstance(knotmultiplicities[0], int): + knotmultiplicities, = knotmultiplicities - if not numpy.iterable(continuity): - continuity = [continuity] * self.ndims - else: - assert len(continuity) == self.ndims + if numpy.iterable(continuity): + continuity, = continuity start_dofs = [] stop_dofs = [] dofshape = [] coeffs = [] cache = {} - for idim in range(self.ndims): - p = degree[idim] - n = self.shape[idim] - c = continuity[idim] - if c < 0: - c += p - assert -1 <= c < p + p = degree + n = len(self) - k = knotvalues[idim] - if k is None: - k = numpy.arange(n+1) # default to uniform spacing - else: - k = numpy.array(k) - while len(k) < n+1: - k_ = numpy.empty(len(k)*2-1) - k_[::2] = k - k_[1::2] = (k[:-1] + k[1:]) / 2 - k = k_ - assert len(k) == n+1, 'knot values do not match the topology size' - - m = knotmultiplicities[idim] - if m is None: - m = numpy.repeat(p-c, n+1) # default to open spline without internal repetitions - else: - m = numpy.array(m) - assert min(m) > 0 and max(m) <= p+1, 'incorrect multiplicity encountered' - while len(m) < n+1: - m_ = numpy.empty(len(m)*2-1, dtype=int) - m_[::2] = m - m_[1::2] = p-c - m = m_ - assert len(m) == n+1, 'knot multiplicity do not match the topology size' - - if idim in periodic and not m[0] == m[n] == p+1: # if m[0] == m[n] == p+1 the spline is discontinuous at the boundary - assert m[0] == m[n], 'periodic spline multiplicity expected' - dk = k[n] - k[0] - m = m[:n] - k = k[:n] - nd = m.sum() - while m[n:].sum() < p - m[0] + 2: - k = numpy.concatenate([k, k+dk]) - m = numpy.concatenate([m, m]) - dk *= 2 - km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) - if p > m[0]: - km = numpy.concatenate([km[-p+m[0]:] - dk, km]) - else: - m[0] = m[-1] = p - nd = m[:n].sum()+1 - km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) - - offsets = numpy.cumsum(m[:n]) - m[0] - start_dofs.append(offsets) - stop_dofs.append(offsets+p+1) - dofshape.append(nd) - - coeffs_i = [] - for offset in offsets: - lknots = km[offset:offset+2*p] - key = tuple(numeric.round((lknots[1:-1]-lknots[0])/(lknots[-1]-lknots[0])*numpy.iinfo(numpy.int32).max)) if lknots.size else (), p - try: - local_coeffs = cache[key] - except KeyError: - local_coeffs = cache[key] = self._localsplinebasis(lknots) - coeffs_i.append(local_coeffs) - coeffs.append(tuple(coeffs_i)) - - transforms_shape = tuple(axis.j-axis.i for axis in self.axes if axis.isdim) - func = function.StructuredBasis(coeffs, start_dofs, stop_dofs, dofshape, transforms_shape, self.f_index, self.f_coords) - if not any(removedofs): + c = continuity + if c < 0: + c += p + assert -1 <= c < p + + k = knotvalues + if k is None: + k = numpy.arange(n+1) # default to uniform spacing + else: + k = numpy.array(k) + while len(k) < n+1: + k_ = numpy.empty(len(k)*2-1) + k_[::2] = k + k_[1::2] = (k[:-1] + k[1:]) / 2 + k = k_ + assert len(k) == n+1, 'knot values do not match the topology size' + + m = knotmultiplicities + if m is None: + m = numpy.repeat(p-c, n+1) # default to open spline without internal repetitions + else: + m = numpy.array(m) + assert min(m) > 0 and max(m) <= p+1, 'incorrect multiplicity encountered' + while len(m) < n+1: + m_ = numpy.empty(len(m)*2-1, dtype=int) + m_[::2] = m + m_[1::2] = p-c + m = m_ + assert len(m) == n+1, 'knot multiplicity do not match the topology size' + + if periodic and not m[0] == m[n] == p+1: # if m[0] == m[n] == p+1 the spline is discontinuous at the boundary + assert m[0] == m[n], 'periodic spline multiplicity expected' + dk = k[n] - k[0] + m = m[:n] + k = k[:n] + nd = m.sum() + while m[n:].sum() < p - m[0] + 2: + k = numpy.concatenate([k, k+dk]) + m = numpy.concatenate([m, m]) + dk *= 2 + km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) + if p > m[0]: + km = numpy.concatenate([km[-p+m[0]:] - dk, km]) + else: + m[0] = m[-1] = p + nd = m[:n].sum()+1 + km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) + + offsets = numpy.cumsum(m[:n]) - m[0] + start_dofs.append(offsets) + stop_dofs.append(offsets+p+1) + dofshape.append(nd) + + coeffs_i = [] + for offset in offsets: + lknots = km[offset:offset+2*p] + key = tuple(numeric.round((lknots[1:-1]-lknots[0])/(lknots[-1]-lknots[0])*numpy.iinfo(numpy.int32).max)) if lknots.size else (), p + try: + local_coeffs = cache[key] + except KeyError: + local_coeffs = cache[key] = self._localsplinebasis(lknots) + coeffs_i.append(local_coeffs) + coeffs.append(tuple(coeffs_i)) + + func = function.StructuredBasis(coeffs, start_dofs, stop_dofs, dofshape, (n,), self.f_index, self.f_coords) + if not removedofs: return func mask = numpy.ones((), dtype=bool) - for idofs, ndofs in zip(removedofs, dofshape): + for idofs, ndofs in zip([removedofs], dofshape): mask = mask[..., _].repeat(ndofs, axis=-1) if idofs: mask[..., [numeric.normdim(ndofs, idof) for idof in idofs]] = False @@ -2232,14 +1950,18 @@ def basis_legendre(self, degree: int): raise NotImplementedError('legendre is only implemented for 1D topologies') return function.LegendreBasis(degree, len(self), self.f_index, self.f_coords) - @property - def refined(self): - 'refine non-uniformly' - - axes = [axis.refined for axis in self.axes] - return StructuredTopology(self.space, self.root, axes, self.nrefine+1, bnames=self._bnames) + def refine_spaces_unchecked(self, spaces: FrozenSet[str]): + if not spaces: + return self + return _Line( + self.ref_coord_system, + self.coord_system.children(Simplex.line, 0), + self.opposite.children(Simplex.line, 0), + self._bnames, + self._periodic) def locate(self, geom, coords, *, tol=0, eps=0, weights=None, skip_missing=False, arguments=None, **kwargs): + raise NotImplementedError coords = numpy.asarray(coords, dtype=float) if geom.ndim == 0: geom = geom[_] @@ -2290,28 +2012,11 @@ def _locate(self, geom0, scale, coords, *, eps=0, weights=None, skip_missing=Fal points = points[~missing] return self._sample(ielems, points, weights) - def __str__(self): - 'string representation' - - return '{}({})'.format(self.__class__.__name__, 'x'.join(str(n) for n in self.shape)) - - -class ConnectedTopology(TransformChainsTopology): - 'unstructured topology with connectivity' - - __slots__ = 'connectivity', - @types.apply_annotations - def __init__(self, space, references: types.strict[References], transforms: transformseq.stricttransforms, opposites: transformseq.stricttransforms, connectivity: types.tuple[types.arraydata]): - assert len(connectivity) == len(references) and all(c.shape[0] == e.nedges for c, e in zip(connectivity, references)) - self.connectivity = tuple(map(numpy.asarray, connectivity)) - super().__init__(space, references, transforms, opposites) - - -class SimplexTopology(TransformChainsTopology): +class _SimplexTopology(Topology): 'simpex topology' - __slots__ = 'simplices', 'references', 'transforms', 'opposites' + __slots__ = 'simplices' __cache__ = 'connectivity' def _renumber(simplices): @@ -2320,26 +2025,26 @@ def _renumber(simplices): keep[simplices.flat] = True return types.arraydata(simplices if keep.all() else (numpy.cumsum(keep)-1)[simplices]) - @types.apply_annotations - def __init__(self, space, simplices: _renumber, transforms: transformseq.stricttransforms, opposites: transformseq.stricttransforms): - assert simplices.shape == (len(transforms), transforms.fromdims+1) + def __init__(self, ref_coord_system: Tuple[Tuple[str, CoordSystem], ...], simplices, coord_system: CoordSystem, opposite: CoordSystem): + assert simplices.shape == (len(coord_system), coord_system.dim + 1) self.simplices = numpy.asarray(simplices) assert numpy.greater(self.simplices[:, 1:], self.simplices[:, :-1]).all(), 'nodes should be sorted' assert not numpy.equal(self.simplices[:, 1:], self.simplices[:, :-1]).all(), 'duplicate nodes' - references = References.uniform(element.getsimplex(transforms.fromdims), len(transforms)) - super().__init__(space, references, transforms, opposites) + references = References.uniform(element.getsimplex(coord_system.dim), len(coord_system)) + super().__init__(ref_coord_system, references, coord_system, opposite) @property def connectivity(self): - nverts = self.ndims + 1 - edge_vertices = numpy.arange(nverts).repeat(self.ndims).reshape(self.ndims, nverts)[:, ::-1].T # nverts x ndims - simplices_edges = self.simplices.take(edge_vertices, axis=1) # nelems x nverts x ndims - elems, edges = divmod(numpy.lexsort(simplices_edges.reshape(-1, self.ndims).T), nverts) + ndims = self.references.ndims + nverts = ndims + 1 + edge_vertices = numpy.arange(nverts).repeat(ndims).reshape(ndims, nverts)[:, ::-1].T # nverts x ndims + simplices_edges = numpy.take(self.simplices, edge_vertices, axis=1) # nelems x nverts x ndims + elems, edges = divmod(numpy.lexsort(simplices_edges.reshape(-1, ndims).T), nverts) sorted_simplices_edges = simplices_edges[elems, edges] # (nelems x nverts) x ndims; matching edges are now adjacent i, = numpy.equal(sorted_simplices_edges[1:], sorted_simplices_edges[:-1]).all(axis=1).nonzero() j = i + 1 assert numpy.greater(i[1:], j[:-1]).all(), 'single edge is shared by three or more simplices' - connectivity = numpy.full((len(self.simplices), self.ndims+1), fill_value=-1, dtype=int) + connectivity = numpy.full((len(self.simplices), ndims+1), fill_value=-1, dtype=int) connectivity[elems[i], edges[i]] = elems[j] connectivity[elems[j], edges[j]] = elems[i] return types.frozenarray(connectivity, copy=False) @@ -2366,13 +2071,12 @@ def basis_bubble(self): return function.PlainBasis([coeffs] * len(self), nmap, ndofs, self.f_index, self.f_coords) -class UnionTopology(TransformChainsTopology): +class UnionTopology(Topology): 'grouped topology' __slots__ = '_topos', '_names', 'references', 'transforms', 'opposites' - @types.apply_annotations - def __init__(self, topos: types.tuple[stricttopology], names: types.tuple[types.strictstr] = ()): + def __init__(self, topos: Tuple[Topology, ...], names: Tuple[str, ...] = ()): self._topos = topos self._names = tuple(names)[:len(self._topos)] assert len(set(self._names)) == len(self._names), 'duplicate name' @@ -2415,12 +2119,12 @@ def __init__(self, topos: types.tuple[stricttopology], names: types.tuple[types. transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), topos[0].transforms.todims, ndims), transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), topos[0].transforms.todims, ndims)) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: topos = (topo if name in groups else topo.get_groups(*groups) for topo, name in itertools.zip_longest(self._topos, self._names)) return functools.reduce(operator.or_, filter(None, topos), self.empty_like()) def __or__(self, other): - if not isinstance(other, TransformChainsTopology): + if not isinstance(other, Topology): return super().__or__(other) if not isinstance(other, UnionTopology): return UnionTopology(self._topos + (other,), self._names) @@ -2431,13 +2135,12 @@ def refined(self): return UnionTopology([topo.refined for topo in self._topos], self._names) -class DisjointUnionTopology(TransformChainsTopology): +class DisjointUnionTopology(Topology): 'grouped topology' __slots__ = '_topos', '_names' - @types.apply_annotations - def __init__(self, topos: types.tuple[stricttopology], names: types.tuple[types.strictstr] = ()): + def __init__(self, topos: Tuple[Topology, ...], names: Tuple[str, ...] = ()): self._topos = topos self._names = tuple(names)[:len(self._topos)] assert len(set(self._names)) == len(self._names), 'duplicate name' @@ -2451,7 +2154,7 @@ def __init__(self, topos: types.tuple[stricttopology], names: types.tuple[types. transformseq.chain((topo.transforms for topo in self._topos), topos[0].transforms.todims, ndims), transformseq.chain((topo.opposites for topo in self._topos), topos[0].transforms.todims, ndims)) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: topos = (topo if name in groups else topo.get_groups(*groups) for topo, name in itertools.zip_longest(self._topos, self._names)) topos = tuple(filter(None, topos)) if len(topos) == 0: @@ -2466,16 +2169,15 @@ def refined(self): return DisjointUnionTopology([topo.refined for topo in self._topos], self._names) -class SubsetTopology(TransformChainsTopology): +class SubsetTopology(Topology): 'trimmed' __slots__ = 'refs', 'basetopo', 'newboundary', '_indices' __cache__ = 'connectivity', 'boundary', 'interfaces', 'refined' - @types.apply_annotations - def __init__(self, basetopo: stricttopology, refs: types.tuple[element.strictreference], newboundary=None): + def __init__(self, basetopo: Topology, refs: Tuple[Reference, ...], newboundary=None): if newboundary is not None: - assert isinstance(newboundary, str) or isinstance(newboundary, TransformChainsTopology) and newboundary.ndims == basetopo.ndims-1 + assert isinstance(newboundary, str) or isinstance(newboundary, Topology) and newboundary.ndims == basetopo.ndims-1 assert len(refs) == len(basetopo) self.refs = refs self.basetopo = basetopo @@ -2487,13 +2189,13 @@ def __init__(self, basetopo: stricttopology, refs: types.tuple[element.strictref opposites = self.basetopo.opposites[self._indices] super().__init__(basetopo.space, references, transforms, opposites) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: return self.basetopo.get_groups(*groups).subset(self, strict=False) def __rsub__(self, other): if self.basetopo == other: refs = [baseref - ref for baseref, ref in zip(self.basetopo.references, self.refs)] - return SubsetTopology(self.basetopo, refs, ~self.newboundary if isinstance(self.newboundary, TransformChainsTopology) else self.newboundary) + return SubsetTopology(self.basetopo, refs, ~self.newboundary if isinstance(self.newboundary, Topology) else self.newboundary) return super().__rsub__(other) def __or__(self, other): @@ -2516,7 +2218,7 @@ def refined(self): indices = types.frozenarray(numpy.array([i for i, ref in enumerate(child_refs) if ref], dtype=int), copy=False) refined_transforms = self.transforms.refined(self.references)[indices] self_refined = TransformChainsTopology(self.space, child_refs[indices], refined_transforms, refined_transforms) - return self.basetopo.refined.subset(self_refined, self.newboundary.refined if isinstance(self.newboundary, TransformChainsTopology) else self.newboundary, strict=True) + return self.basetopo.refined.subset(self_refined, self.newboundary.refined if isinstance(self.newboundary, Topology) else self.newboundary, strict=True) @property def boundary(self): @@ -2559,7 +2261,7 @@ def boundary(self): trimmedtransforms.append(elemtrans+(edgetrans,)) trimmedopposites.append(elemtrans+(edgetrans.flipped,)) origboundary = SubsetTopology(baseboundary, brefs) - if isinstance(self.newboundary, TransformChainsTopology): + if isinstance(self.newboundary, Topology): trimmedbrefs = [ref.empty for ref in self.newboundary.references] for ref, trans in zip(trimmedreferences, trimmedtransforms): trimmedbrefs[self.newboundary.transforms.index(trans)] = ref @@ -2600,14 +2302,13 @@ def locate(self, geom, coords, *, eps=0, **kwargs): return sample -class RefinedTopology(TransformChainsTopology): +class RefinedTopology(Topology): 'refinement' __slots__ = 'basetopo', __cache__ = 'boundary', 'connectivity' - @types.apply_annotations - def __init__(self, basetopo: stricttopology): + def __init__(self, basetopo: Topology): self.basetopo = basetopo super().__init__( self.basetopo.space, @@ -2615,7 +2316,7 @@ def __init__(self, basetopo: stricttopology): self.basetopo.transforms.refined(self.basetopo.references), self.basetopo.opposites.refined(self.basetopo.references)) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: return self.basetopo.get_groups(*groups).refined @property @@ -2639,14 +2340,36 @@ def connectivity(self): return tuple(types.frozenarray(c, copy=False) for c in connectivity) -class HierarchicalTopology(TransformChainsTopology): +def _transform_poly(trans, index, coeffs): + assert coeffs.ndim == trans.from_dim + 1 + degree = coeffs.shape[1] - 1 + assert all(n == degree+1 for n in coeffs.shape[2:]) + + eye = numpy.eye(trans.from_dim, dtype=int) + # construct polynomials for affine transforms of individual dimensions + polys = numpy.zeros((trans.from_dim,)+(2,)*trans.from_dim) + polys[(slice(None),)+(0,)*trans.from_dim] = trans.apply(index, numpy.zeros(trans.from_dim, float))[1] + for idim, e in enumerate(eye): + polys[(slice(None),)+tuple(e)] = trans.basis(index)[:, idim] + # reduces polynomials to smallest nonzero power + polys = [poly[tuple(slice(None if p else 1) for p in poly[tuple(eye)])] for poly in polys] + # construct transform poly by transforming all monomials separately and summing + M = numpy.zeros((degree+1,)*(2*trans.from_dim), dtype=float) + for powers in numpy.ndindex(*[degree+1]*trans.from_dim): + if sum(powers) <= degree: + M_power = functools.reduce(numeric.poly_mul, [numeric.poly_pow(poly, power) for poly, power in zip(polys, powers)]) + M[tuple(slice(n) for n in M_power.shape)+powers] += M_power + + return types.frozenarray(numpy.einsum('jk,ik', M.reshape([(degree+1)**trans.from_dim]*2), coeffs.reshape(coeffs.shape[0], -1)).reshape(coeffs.shape), copy=False) + + +class HierarchicalTopology(Topology): 'collection of nested topology elments' __slots__ = 'basetopo', 'levels', '_indices_per_level', '_offsets' __cache__ = 'refined', 'boundary', 'interfaces' - @types.apply_annotations - def __init__(self, basetopo: stricttopology, indices_per_level: types.tuple[types.arraydata]): + def __init__(self, basetopo: Topology, indices_per_level: types.tuple[types.arraydata]): 'constructor' assert all(ind.dtype == int for ind in indices_per_level) @@ -2658,22 +2381,35 @@ def __init__(self, basetopo: stricttopology, indices_per_level: types.tuple[type level = None levels = [] references = References.empty(basetopo.ndims) - transforms = [] - opposites = [] + coord_system = [] + opposite = [] for indices in self._indices_per_level: level = self.basetopo if level is None else level.refined levels.append(level) if len(indices): references = references.chain(level.references.take(indices)) - transforms.append(level.transforms[indices]) - opposites.append(level.opposites[indices]) + coord_system.append(level.coord_system.take(indices)) + opposite.append(level.opposite.take(indices)) + coord_system = functools.reduce(lambda acc, item: acc.concat(item), coord_system) + opposite = functools.reduce(lambda acc, item: acc.concat(item), opposite) self.levels = tuple(levels) - super().__init__(basetopo.space, references, transformseq.chain(transforms, basetopo.transforms.todims, basetopo.ndims), transformseq.chain(opposites, basetopo.transforms.todims, basetopo.ndims)) + # debug + if debug_flags.hierarchical: + covered = self._indices_per_level[0] + for prev_level, level, indices in zip(self.levels, self.levels[1:], self._indices_per_level[1:]): + trans = level.coord_system.trans_to(prev_level.coord_system) + prev_covered = numpy.unique(numpy.array(trans.unapply_indices(covered), dtype=int)) + covered = numpy.union1d(prev_covered, indices) + assert len(covered) == len(prev_covered) + len(indices) + assert len(covered) == len(self.levels[-1]) + + super().__init__(basetopo.ref_coord_system, references, coord_system, opposite) def __and__(self, other): if not isinstance(other, HierarchicalTopology) or self.basetopo != other.basetopo: return super().__and__(other) + raise NotImplementedError indices_per_level = [] levels = max(self.levels, other.levels, key=len) for level, self_indices, other_indices in itertools.zip_longest(levels, self._indices_per_level, other._indices_per_level, fillvalue=()): @@ -2683,21 +2419,19 @@ def __and__(self, other): for index in indices: # keep common elements or elements which are finer than conterpart keep[index] = mask[index] or topo.transforms.contains_with_tail(level.transforms[index]) indices, = keep.nonzero() - indices_per_level.append(indices) - return HierarchicalTopology(self.basetopo, indices_per_level) - - def _rebase(self, newbasetopo: Topology) -> 'HierarchicalTopology': - itemindices_per_level = [] - for baseindices, baselevel, itemlevel in zip(self._indices_per_level, self.basetopo.refine_iter, newbasetopo.refine_iter): - itemindices = [] - itemindex = itemlevel.transforms.index - for basetrans in map(baselevel.transforms.__getitem__, baseindices): - try: - itemindices.append(itemindex(basetrans)) - except ValueError: - pass - itemindices_per_level.append(numpy.unique(numpy.array(itemindices, dtype=int))) - return HierarchicalTopology(newbasetopo, itemindices_per_level) + indices_per_level.append(types.arraydata(indices)) + return HierarchicalTopology(self.basetopo, tuple(indices_per_level)) + + def _rebase(self, new_base_topo: Topology) -> 'HierarchicalTopology': + new_level = new_base_topo + new_indices_per_level = [] + for old_indices, old_level, new_level in zip(self._indices_per_level, self.levels, new_base_topo.refine_iter): + trans = new_level.coord_system.trans_to(old_level.coord_system) + assert trans.is_index_map + new_indices = numpy.array(trans.unapply_indices(old_indices), dtype=int) + new_indices.sort() + new_indices_per_level.append(types.arraydata(new_indices)) + return HierarchicalTopology(new_base_topo, tuple(new_indices_per_level)) def slice_unchecked(self, __s: slice, __idim: int) -> 'HierarchicalTopology': return self._rebase(self.basetopo.slice_unchecked(__s, __idim)) @@ -2706,66 +2440,58 @@ def get_groups(self, *groups: str) -> 'HierarchicalTopology': return self._rebase(self.basetopo.get_groups(*groups)) def refined_by(self, refine): - refine = tuple(refine) - if not all(map(numeric.isint, refine)): - refine = tuple(self.transforms.index_with_tail(item)[0] for item in refine) - refine = numpy.unique(numpy.array(refine, dtype=int)) + refine = numpy.unique(numpy.asarray(refine, dtype=int)) splits = numpy.searchsorted(refine, self._offsets, side='left') indices_per_level = list(map(list, self._indices_per_level))+[[]] fine = self.basetopo for ilevel, (start, stop) in enumerate(zip(splits[:-1], splits[1:])): coarse, fine = fine, fine.refined coarse_indices = tuple(map(indices_per_level[ilevel].pop, reversed(refine[start:stop]-self._offsets[ilevel]))) - coarse_transforms = map(coarse.transforms.__getitem__, coarse_indices) - coarse_references = map(coarse.references.__getitem__, coarse_indices) - fine_transforms = (trans+(ctrans,) for trans, ref in zip(coarse_transforms, coarse_references) for ctrans, cref in ref.children if cref) - indices_per_level[ilevel+1].extend(map(fine.transforms.index, fine_transforms)) + fine_indices = fine.coord_system.trans_to(coarse.coord_system).unapply_indices(coarse_indices) + indices_per_level[ilevel+1].extend(fine_indices) if not indices_per_level[-1]: indices_per_level.pop(-1) - return HierarchicalTopology(self.basetopo, ([numpy.unique(numpy.array(i, dtype=int)) for i in indices_per_level])) + return HierarchicalTopology(self.basetopo, tuple(types.arraydata(numpy.unique(numpy.array(i, dtype=int))) for i in indices_per_level)) @property def refined(self): - refined_indices_per_level = [numpy.array([], dtype=int)] + refined_indices_per_level = [types.arraydata(numpy.array([], dtype=int))] fine = self.basetopo for coarse_indices in self._indices_per_level: coarse, fine = fine, fine.refined - coarse_transforms = map(coarse.transforms.__getitem__, coarse_indices) - coarse_references = map(coarse.references.__getitem__, coarse_indices) - fine_transforms = (trans+(ctrans,) for trans, ref in zip(coarse_transforms, coarse_references) for ctrans, cref in ref.children if cref) - refined_indices_per_level.append(numpy.unique(numpy.fromiter(map(fine.transforms.index, fine_transforms), dtype=int))) - return HierarchicalTopology(self.basetopo, refined_indices_per_level) + fine_indices = fine.coord_system.trans_to(coarse.coord_system).unapply_indices(coarse_indices) + refined_indices_per_level.append(types.arraydata(numpy.unique(numpy.array(fine_indices, dtype=int)))) + return HierarchicalTopology(self.basetopo, tuple(refined_indices_per_level)) + + def refine_spaces_unchecked(self, __spaces: FrozenSet[str]) -> Topology: + if __spaces == frozenset(self.spaces): + return self.refined + else: + return super().refined_spaces_unchecked(__spaces) @property @log.withcontext def boundary(self): - 'boundary elements' - basebtopo = self.basetopo.boundary bindices_per_level = [] for indices, level, blevel in zip(self._indices_per_level, self.basetopo.refine_iter, basebtopo.refine_iter): - bindex = blevel.transforms.index - bindices = [] - for index in indices: - for etrans, eref in level.references[index].edges: - if eref: - trans = level.transforms[index]+(etrans,) - try: - bindices.append(bindex(trans)) - except ValueError: - pass + bindices = blevel.coord_system.trans_to(level.coord_system).unapply_indices(indices) bindices = numpy.array(bindices, dtype=int) if len(bindices) > 1: bindices.sort() assert not numpy.equal(bindices[1:], bindices[:-1]).any() - bindices_per_level.append(bindices) - return HierarchicalTopology(basebtopo, bindices_per_level) + bindices_per_level.append(types.arraydata(bindices)) + return HierarchicalTopology(basebtopo, tuple(bindices_per_level)) + + def boundary_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': + if __spaces == frozenset(self.spaces): + return self.boundary + else: + return super().boundary_spaces_unchecked(__spaces) @property @log.withcontext def interfaces(self): - 'interfaces' - hreferences = References.empty(self.ndims-1) htransforms = [] hopposites = [] @@ -2791,6 +2517,12 @@ def interfaces(self): hopposites.append(level.interfaces.opposites[selection]) return TransformChainsTopology(self.space, hreferences, transformseq.chain(htransforms, self.transforms.todims, self.ndims-1), transformseq.chain(hopposites, self.transforms.todims, self.ndims-1)) + def interfaces_spaces_unchecked(self, __spaces: FrozenSet[str]) -> 'Topology': + if __spaces != frozenset(self.spaces): + return self.interfaces + else: + return super().boundary_spaces_unchecked(__spaces) + @log.withcontext def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): '''Create hierarchical basis. @@ -2846,18 +2578,24 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): ubases = [] ubasis_active = [] ubasis_passive = [] - prev_transforms = None + prev_coord_system = None prev_ielems = [] map_indices = [] + level_trans = [] with log.iter.fraction('level', self.levels[::-1], self._indices_per_level[::-1]) as items: for topo, touchielems_i in items: - topo_index_with_tail = topo.transforms.index_with_tail - mapped_prev_ielems = [topo_index_with_tail(prev_transforms[j])[0] for j in prev_ielems] - map_indices.insert(0, dict(zip(prev_ielems, mapped_prev_ielems))) - nontouchielems_i = numpy.unique(numpy.array(mapped_prev_ielems, dtype=int)) - prev_ielems = ielems_i = numpy.unique(numpy.concatenate([numpy.asarray(touchielems_i, dtype=int), nontouchielems_i], axis=0)) - prev_transforms = topo.transforms + if prev_coord_system is not None: + trans = prev_coord_system.trans_to(topo.coord_system) + level_trans.insert(0, trans) + mapped_prev_ielems = trans.apply_indices(prev_ielems) + map_indices.insert(0, dict(zip(prev_ielems, mapped_prev_ielems))) + nontouchielems_i = numpy.unique(numpy.array(mapped_prev_ielems, dtype=int)) + prev_ielems = ielems_i = numpy.unique(numpy.concatenate([numpy.asarray(touchielems_i, dtype=int), nontouchielems_i], axis=0)) + else: + map_indices.insert(0, {}) + prev_ielems = ielems_i = numpy.asarray(touchielems_i, dtype=int) + prev_coord_system = topo.coord_system basis_i = topo.basis(name, *args, **kwargs) assert isinstance(basis_i, function.Basis) @@ -2881,8 +2619,6 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): for ilevel, (level, indices) in enumerate(zip(self.levels, self._indices_per_level)): for ilocal in indices: - hbasis_trans = transform.canonical(level.transforms[ilocal]) - tail = hbasis_trans[len(hbasis_trans)-ilevel:] trans_dofs = [] trans_coeffs = [] @@ -2894,6 +2630,9 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): if not truncated: # classical hierarchical basis for h, ilocal in enumerate(local_indices): # loop from coarse to fine + if trans_coeffs: + trans_coeffs = [_transform_poly(level_trans[h - 1], ilocal, c) for c in trans_coeffs] + mydofs = ubases[h].get_dofs(ilocal) imyactive = numeric.sorted_index(ubasis_active[h], mydofs, missing=-1) @@ -2903,17 +2642,18 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): mypoly = ubases[h].get_coefficients(ilocal) trans_coeffs.append(mypoly[myactive]) - if h < len(tail): - trans_coeffs = [tail[h].transform_poly(c) for c in trans_coeffs] - else: # truncated hierarchical basis + prev_ilocal = None + for h, ilocal in reversed(tuple(enumerate(local_indices))): # loop from fine to coarse mydofs = ubases[h].get_dofs(ilocal) mypoly = ubases[h].get_coefficients(ilocal) - truncpoly = mypoly if h == len(tail) \ - else numpy.tensordot(numpy.tensordot(tail[h].transform_poly(mypoly), project[..., mypassive], self.ndims), truncpoly[mypassive], 1) + if prev_ilocal is None: + truncpoly = mypoly + else: + truncpoly = numpy.tensordot(numpy.tensordot(_transform_poly(level_trans[h], prev_ilocal, mypoly), project[..., mypassive], self.ndims), truncpoly[mypassive], 1) imyactive = numeric.sorted_index(ubasis_active[h], mydofs, missing=-1) myactive = numpy.greater_equal(imyactive, 0) & numpy.greater(abs(truncpoly), truncation_tolerance).any(axis=tuple(range(1, truncpoly.ndim))) @@ -2933,6 +2673,8 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): project = (V.T[:, :len(S)] / S).dot(U.T).reshape(mypoly.shape[1:]+mypoly.shape[:1]) projectcache[id(mypoly)] = project, mypoly # NOTE: mypoly serves to keep array alive + prev_ilocal = ilocal + # add the dofs and coefficients to the hierarchical basis hbasis_dofs.append(numpy.concatenate(trans_dofs)) hbasis_coeffs.append(numeric.poly_concatenate(*trans_coeffs)) @@ -2961,15 +2703,14 @@ class Patch(types.Singleton): __slots__ = 'topo', 'verts', 'boundaries' - @types.apply_annotations - def __init__(self, topo: stricttopology, verts: types.arraydata, boundaries: types.tuple[types.strict[PatchBoundary]]): + def __init__(self, topo: Topology, verts: types.arraydata, boundaries: types.tuple[types.strict[PatchBoundary]]): super().__init__() self.topo = topo self.verts = numpy.asarray(verts) self.boundaries = boundaries -class MultipatchTopology(TransformChainsTopology): +class MultipatchTopology(Topology): 'multipatch topology' __slots__ = 'patches', @@ -3043,7 +2784,7 @@ def _patchinterfaces(self): if len(data) > 1 }) - def get_groups(self, *groups: str) -> TransformChainsTopology: + def get_groups(self, *groups: str) -> Topology: topos = (patch.topo if 'patch{}'.format(i) in groups else patch.topo.get_groups(*groups) for i, patch in enumerate(self.patches)) topos = tuple(filter(None, topos)) if len(topos) == 0: @@ -3179,7 +2920,7 @@ def boundary(self): subtopos.append(patch.topo.boundary[name]) subnames.append('patch{}-{}'.format(i, name)) if len(subtopos) == 0: - return EmptyTopology(self.space, self.transforms.todims, self.ndims-1) + return Topology.empty(self.ref_coord_system, self.ndims-1) else: return DisjointUnionTopology(subtopos, subnames) @@ -3192,7 +2933,7 @@ def interfaces(self): patch via ``'intrapatch'``. ''' - intrapatchtopo = EmptyTopology(self.space, self.transforms.todims, self.ndims-1) if not self.patches else \ + intrapatchtopo = Topology.empty(self.ref_coord_system, self.ndims-1) if not self.patches else \ DisjointUnionTopology(patch.topo.interfaces for patch in self.patches) btopos = [] @@ -3253,4 +2994,4 @@ def refined(self): return MultipatchTopology(Patch(patch.topo.refined, patch.verts, patch.boundaries) for patch in self.patches) -# vim:sw=2:sts=2:et +# vim:sw=4:sts=4:et diff --git a/nutils/transform.py b/nutils/transform.py index d4c8d17f3..928b479fc 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -3,8 +3,7 @@ """ from typing import Tuple, Dict -from . import cache, numeric, util, types, evaluable -from .evaluable import Evaluable, Array +from . import cache, numeric, util, types import numpy import collections import itertools @@ -471,244 +470,4 @@ class Point(Matrix): def __init__(self, offset: types.arraydata): super().__init__(numpy.zeros((offset.shape[0], 0)), offset) -# EVALUABLE TRANSFORM CHAIN - - -class EvaluableTransformChain(Evaluable): - '''The :class:`~nutils.evaluable.Evaluable` equivalent of a transform chain. - - Attributes - ---------- - todims : :class:`int` - The to dimension of the transform chain. - fromdims : :class:`int` - The from dimension of the transform chain. - ''' - - __slots__ = 'todims', 'fromdims' - - @staticmethod - def empty(__dim: int) -> 'EvaluableTransformChain': - '''Return an empty evaluable transform chain with the given dimension. - - Parameters - ---------- - dim : :class:`int` - The to and from dimensions of the empty transform chain. - - Returns - ------- - :class:`EvaluableTransformChain` - The empty evaluable transform chain. - ''' - - return _EmptyTransformChain(__dim) - - @staticmethod - def from_argument(name: str, todims: int, fromdims: int) -> 'EvaluableTransformChain': - '''Return an evaluable transform chain that evaluates to the given argument. - - Parameters - ---------- - name : :class:`str` - The name of the argument. - todims : :class:`int` - The to dimension of the transform chain. - fromdims: :class:`int` - The from dimension of the transform chain. - - Returns - ------- - :class:`EvaluableTransformChain` - The transform chain that evaluates to the given argument. - ''' - - return _TransformChainArgument(name, todims, fromdims) - - def __init__(self, args: Tuple[Evaluable, ...], todims: int, fromdims: int) -> None: - if fromdims > todims: - raise ValueError('The dimension of the tip cannot be larger than the dimension of the root.') - self.todims = todims - self.fromdims = fromdims - super().__init__(args) - - @property - def linear(self) -> Array: - ':class:`nutils.evaluable.Array`: The linear transformation matrix of the entire transform chain. Shape ``(todims,fromdims)``.' - - return _Linear(self) - - @property - def basis(self) -> Array: - ':class:`nutils.evaluable.Array`: A basis for the root coordinate system such that the first :attr:`fromdims` vectors span the tangent space. Shape ``(todims,todims)``.' - - if self.fromdims == self.todims: - return evaluable.diagonalize(evaluable.ones((self.todims,))) - else: - return _Basis(self) - - def apply(self, __coords: Array) -> Array: - '''Apply this transform chain to the last axis given coordinates. - - Parameters - ---------- - coords : :class:`nutils.evaluable.Array` - The coordinates to transform with shape ``(...,fromdims)``. - - Returns - ------- - :class:`nutils.evaluable.Array` - The transformed coordinates with shape ``(...,todims)``. - ''' - - return _Apply(self, __coords) - - def index_with_tail_in(self, __sequence: 'Transforms') -> Tuple[Array, 'EvaluableTransformChain']: - '''Return the evaluable index of this transform chain in the given sequence. - - Parameters - ---------- - sequence : :class:`nutils.transformseq.Transforms` - The sequence of transform chains. - - Returns - ------- - :class:`nutils.evaluable.Array` - The index of this transform chain in the given sequence. - :class:`EvaluableTransformChain` - The tail. - - See also - -------- - :meth:`nutils.transformseq.Transforms.index_with_tail` : the unevaluable version of this method - ''' - - index_tail = _EvaluableIndexWithTail(__sequence, self) - index = evaluable.ArrayFromTuple(index_tail, 0, (), int, _lower=0, _upper=len(__sequence) - 1) - tails = _EvaluableTransformChainFromTuple(index_tail, 1, __sequence.fromdims, self.fromdims) - return index, tails - - -class _Linear(Array): - - __slots__ = '_fromdims' - - def __init__(self, chain: EvaluableTransformChain) -> None: - self._fromdims = chain.fromdims - super().__init__(args=(chain,), shape=(chain.todims, chain.fromdims), dtype=float) - - def evalf(self, chain: TransformChain) -> numpy.ndarray: - return functools.reduce(lambda r, i: i @ r, (item.linear for item in reversed(chain)), numpy.eye(self._fromdims)) - - def _derivative(self, var: evaluable.DerivativeTargetBase, seen: Dict[Evaluable, Evaluable]) -> Array: - return evaluable.zeros(self.shape + var.shape, dtype=float) - - -class _Basis(Array): - - __slots__ = '_todims', '_fromdims' - - def __init__(self, chain: EvaluableTransformChain) -> None: - self._todims = chain.todims - self._fromdims = chain.fromdims - super().__init__(args=(chain,), shape=(chain.todims, chain.todims), dtype=float) - - def evalf(self, chain: TransformChain) -> numpy.ndarray: - linear = numpy.eye(self._fromdims) - for item in reversed(chain): - linear = item.linear @ linear - assert item.fromdims <= item.todims <= item.fromdims + 1 - if item.todims == item.fromdims + 1: - linear = numpy.concatenate([linear, item.ext[:, _]], axis=1) - assert linear.shape == (self._todims, self._todims) - return linear - - def _derivative(self, var: evaluable.DerivativeTargetBase, seen: Dict[Evaluable, Evaluable]) -> Array: - return evaluable.zeros(self.shape + var.shape, dtype=float) - - -class _Apply(Array): - - __slots__ = '_chain', '_coords' - - def __init__(self, chain: EvaluableTransformChain, coords: Array) -> None: - if coords.ndim == 0: - raise ValueError('expected a coords array with at least one axis but got {}'.format(coords)) - if not evaluable.equalindex(chain.fromdims, coords.shape[-1]): - raise ValueError('the last axis of coords does not match the from dimension of the transform chain') - self._chain = chain - self._coords = coords - super().__init__(args=(chain, coords), shape=(*coords.shape[:-1], chain.todims), dtype=float) - - def evalf(self, chain: TransformChain, coords: numpy.ndarray) -> numpy.ndarray: - return apply(chain, coords) - - def _derivative(self, var: evaluable.DerivativeTargetBase, seen: Dict[Evaluable, Evaluable]) -> Array: - axis = self._coords.ndim - 1 - linear = evaluable.appendaxes(evaluable.prependaxes(self._chain.linear, self._coords.shape[:-1]), var.shape) - dcoords = evaluable.insertaxis(evaluable.derivative(self._coords, var, seen), axis, linear.shape[axis]) - return evaluable.dot(linear, dcoords, axis+1) - - -class _EmptyTransformChain(EvaluableTransformChain): - - __slots__ = () - - def __init__(self, dim: int) -> None: - super().__init__((), dim, dim) - - def evalf(self) -> TransformChain: - return () - - def apply(self, points: Array) -> Array: - return points - - @property - def linear(self): - return evaluable.diagonalize(evaluable.ones((self.todims,))) - - -class _TransformChainArgument(EvaluableTransformChain): - - __slots__ = '_name' - - def __init__(self, name: str, todims: int, fromdims: int) -> None: - self._name = name - super().__init__((evaluable.EVALARGS,), todims, fromdims) - - def evalf(self, evalargs) -> TransformChain: - chain = evalargs[self._name] - assert isinstance(chain, tuple) and all(isinstance(item, TransformItem) for item in chain) - assert not chain or chain[0].todims == self.todims and chain[-1].fromdims == self.fromdims - return chain - - @property - def arguments(self): - return frozenset({self}) - - -class _EvaluableIndexWithTail(evaluable.Evaluable): - - __slots__ = '_sequence' - - def __init__(self, sequence: 'Transforms', chain: EvaluableTransformChain) -> None: - self._sequence = sequence - super().__init__((chain,)) - - def evalf(self, chain: TransformChain) -> Tuple[numpy.ndarray, TransformChain]: - index, tails = self._sequence.index_with_tail(chain) - return numpy.array(index), tails - - -class _EvaluableTransformChainFromTuple(EvaluableTransformChain): - - __slots__ = '_index' - - def __init__(self, items: evaluable.Evaluable, index: int, todims: int, fromdims: int) -> None: - self._index = index - super().__init__((items,), todims, fromdims) - - def evalf(self, items: tuple) -> TransformChain: - return items[self._index] - # vim:sw=2:sts=2:et diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 2ffcc559b..0e1f6fbaa 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -1,9 +1,9 @@ """The transformseq module.""" from typing import Tuple -from . import types, numeric, util, transform, element, evaluable +from . import types, numeric, util, transform, element from .elementseq import References -from .transform import TransformChain, EvaluableTransformChain +from .transform import TransformChain import abc import itertools import operator @@ -305,22 +305,6 @@ def unchain(self): yield self - def get_evaluable(self, index: evaluable.Array) -> EvaluableTransformChain: - '''Return the evaluable transform chain at the given index. - - Parameters - ---------- - index : a scalar, integer :class:`nutils.evaluable.Array` - The index of the transform chain to return. - - Returns - ------- - :class:`nutils.transform.EvaluableTransformChain` - The evaluable transform chain at the given ``index``. - ''' - - return _EvaluableTransformChainFromSequence(self, index) - stricttransforms = types.strict[Transforms] @@ -433,9 +417,6 @@ def __getitem__(self, index): return super().__getitem__(index) return transform.Index(self.fromdims, self._offset + numeric.normdim(self._length, index.__index__())), - def get_evaluable(self, index: evaluable.Array) -> EvaluableTransformChain: - return _EvaluableIndexChain(self.fromdims, self._offset + evaluable.InRange(index, self._length)) - def __len__(self): return self._length @@ -624,9 +605,6 @@ def index_with_tail(self, trans): return flatindex, tail - def get_evaluable(self, index: evaluable.Array) -> EvaluableTransformChain: - return _EvaluableTransformChainFromStructured(self, index) - class MaskedTransforms(Transforms): '''An order preserving subset of another :class:`Transforms` object. @@ -945,110 +923,4 @@ def chain(items, todims, fromdims): else: return ChainedTransforms(unchained) - -class _EvaluableTransformChainFromSequence(EvaluableTransformChain): - - __slots__ = '_sequence', '_index' - - def __init__(self, sequence: Transforms, index: evaluable.Array) -> None: - self._sequence = sequence - self._index = index - super().__init__((index,), sequence.todims, sequence.fromdims) - - def evalf(self, index: numpy.ndarray) -> TransformChain: - return self._sequence[index.__index__()] - - def index_with_tail_in(self, __sequence) -> Tuple[evaluable.Array, EvaluableTransformChain]: - if __sequence == self._sequence: - tails = EvaluableTransformChain.empty(self._sequence.todims) - return self._index, tails - else: - return super().index_with_tail_in(__sequence) - - -class _EvaluableIndexChain(EvaluableTransformChain): - - __slots__ = '_ndim' - - def __init__(self, ndim: int, index: evaluable.Array) -> None: - self._ndim = ndim - super().__init__((index,), ndim, ndim) - - def evalf(self, index: numpy.ndarray) -> TransformChain: - return transform.Index(self._ndim, index.__index__()), - - def apply(self, points: evaluable.Array) -> evaluable.Array: - return points - - @property - def linear(self) -> evaluable.Array: - return evaluable.diagonalize(evaluable.ones((self.todims,))) - - -class _EvaluableTransformChainFromStructured(EvaluableTransformChain): - - __slots__ = '_sequence', '_index' - - def __init__(self, sequence: StructuredTransforms, index: evaluable.Array) -> None: - self._sequence = sequence - self._index = index - super().__init__((index,), sequence.todims, sequence.fromdims) - - def evalf(self, index: numpy.ndarray) -> TransformChain: - return self._sequence[index.__index__()] - - def apply(self, points: evaluable.Array) -> evaluable.Array: - if len(self._sequence._axes) != 1: - return super().apply(points) - desired = super().apply(points) - axis = self._sequence._axes[0] - # axis.map - index = self._index + axis.i - if axis.mod: - index %= axis.mod - # edge - if axis.isdim: - assert evaluable.equalindex(points.shape[-1], 1) - else: - assert evaluable.equalindex(points.shape[-1], 0) - points = evaluable.appendaxes(float(axis.side), (*points.shape[:-1], 1)) - # children - for i in range(self._sequence._nrefine): - index, ichild = evaluable.divmod(index, 2) - points = .5 * (points + evaluable.appendaxes(ichild, points.shape)) - # shift - return points + evaluable.appendaxes(index, points.shape) - - @property - def linear(self) -> evaluable.Array: - if not len(self._sequence): - return super().linear - chain = self._sequence[0] - linear = numpy.eye(self.fromdims) - for item in reversed(chain): - linear = item.linear @ linear - assert linear.shape == (self.todims, self.fromdims) - return evaluable.asarray(linear) - - @property - def basis(self) -> evaluable.Array: - if not len(self._sequence) or self.fromdims == self.todims: - return super().basis - chain = self._sequence[0] - basis = numpy.eye(self.fromdims) - for item in reversed(chain): - basis = item.linear @ basis - assert item.fromdims <= item.todims <= item.fromdims + 1 - if item.todims == item.fromdims + 1: - basis = numpy.concatenate([basis, item.ext[:, None]], axis=1) - assert basis.shape == (self.todims, self.todims) - return evaluable.asarray(basis) - - def index_with_tail_in(self, __sequence) -> Tuple[evaluable.Array, EvaluableTransformChain]: - if __sequence == self._sequence: - tails = EvaluableTransformChain.empty(self._sequence.todims) - return self._index, tails - else: - return super().index_with_tail_in(__sequence) - # vim:sw=2:sts=2:et diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..ca2cfb811 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "nutils" +dependencies = ["numpy>=1.17", "treelog>=1.0b5", "stringly"] + +[build-system] +requires = ["maturin>=0.12,<0.13"] +build-backend = "maturin" diff --git a/setup.py b/setup.py deleted file mode 100644 index af6ecdaf6..000000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -import re -import os -from setuptools import setup - -long_description = """ -Nutils is a Free and Open Source Python programming library for Finite Element -Method computations, developed by `Evalf Computing `_ and -distributed under the permissive MIT license. Key features are a readable, math -centric syntax, an object oriented design, strict separation of topology and -geometry, and high level function manipulations with support for automatic -differentiation. - -Nutils provides the tools required to construct a typical simulation workflow -in just a few lines of Python code, while at the same time leaving full -flexibility to build novel workflows or interact with third party tools. With -native support for Isogeometric Analysis (IGA), the Finite Cell method (FCM), -multi-physics, mixed methods, and hierarchical refinement, Nutils is at the -forefront of numerical discretization science. Efficient under-the-hood -vectorization and built-in parallellisation provide for an effortless -transition from academic research projects to full scale, real world -applications. -""" - -with open(os.path.join('nutils', '__init__.py')) as f: - version = next(filter(None, map(re.compile("^version = '([a-zA-Z0-9.]+)'$").match, f))).group(1) - -setup( - name='nutils', - version=version, - description='Numerical Utilities for Finite Element Analysis', - author='Evalf', - author_email='info@nutils.org', - url='http://nutils.org', - download_url='https://github.com/nutils/nutils/releases', - packages=['nutils', 'nutils.matrix'], - long_description=long_description, - license='MIT', - python_requires='>=3.7', - install_requires=['numpy>=1.17', 'treelog>=1.0b5', 'stringly'], - extras_require=dict( - docs=['Sphinx>=1.8'], - matrix_scipy=['scipy>=0.13'], - matrix_mkl=['mkl'], - export_mpl=['matplotlib>=1.3', 'pillow>2.6'], - import_gmsh=['meshio'], - ), - command_options=dict( - test=dict(test_loader=('setup.py', 'unittest:TestLoader')), - ), -) diff --git a/src/finite_f64.rs b/src/finite_f64.rs new file mode 100644 index 000000000..d95784b04 --- /dev/null +++ b/src/finite_f64.rs @@ -0,0 +1,30 @@ +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(transparent)] +pub struct FiniteF64(pub f64); + +impl Eq for FiniteF64 {} + +impl PartialOrd for FiniteF64 { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl Ord for FiniteF64 { + fn cmp(&self, other: &Self) -> Ordering { + if let Some(ord) = self.0.partial_cmp(&other.0) { + ord + } else { + panic!("not finite"); + } + } +} + +impl Hash for FiniteF64 { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..495d019af --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,366 @@ +pub mod finite_f64; +pub mod map; +pub mod simplex; +mod util; + +use map::relative::RelativeTo; +use map::coord_system::CoordSystem; +use map::Map; +use numpy::{IntoPyArray, IxDyn, PyArray, PyArrayDyn, PyReadonlyArrayDyn, PyReadonlyArray2, PyArray2}; +use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::class::basic::CompareOp; +use pyo3::prelude::*; +use pyo3::types::{PySlice, PySliceIndices}; +use simplex::Simplex; +use std::iter; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +impl From for PyErr { + fn from(err: map::Error) -> PyErr { + PyValueError::new_err(err.to_string()) + } +} + +#[pymodule] +#[allow(non_snake_case)] +fn _rust(py: Python, m: &PyModule) -> PyResult<()> { + let sys_modules = py.import("sys")?.getattr("modules")?; + + #[pyclass(name = "Simplex", module = "nutils._rust")] + #[derive(Debug, Clone)] + struct PySimplex(Simplex); + + #[pymethods] + impl PySimplex { + #[classattr] + pub fn line() -> Self { + Simplex::Line.into() + } + #[classattr] + pub fn triangle() -> Self { + Simplex::Triangle.into() + } + #[getter] + pub fn dim(&self) -> usize { + self.0.dim() + } + #[getter] + pub fn edge_dim(&self) -> usize { + self.0.edge_dim() + } + #[getter] + pub fn edge_simplex(&self) -> Option { + self.0.edge_simplex().map(|simplex| simplex.into()) + } + #[getter] + pub fn nchildren(&self) -> usize { + self.0.nchildren() + } + #[getter] + pub fn nedges(&self) -> usize { + self.0.nedges() + } + pub fn __richcmp__<'py>( + &self, + py: Python<'py>, + other: &'py PyAny, + op: CompareOp, + ) -> PyObject { + if let Ok(other) = PySimplex::extract(other) { + match op { + CompareOp::Eq => (self.0 == other.0).into_py(py), + CompareOp::Ne => (self.0 != other.0).into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } else { + match op { + CompareOp::Eq => false.into_py(py), + CompareOp::Ne => true.into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } + } + pub fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + } + + impl From for PySimplex { + fn from(simplex: Simplex) -> PySimplex { + PySimplex(simplex) + } + } + + impl From for Simplex { + fn from(pysimplex: PySimplex) -> Simplex { + pysimplex.0 + } + } + + impl From<&PySimplex> for Simplex { + fn from(pysimplex: &PySimplex) -> Simplex { + pysimplex.0 + } + } + + m.add_class::()?; + + fn apply_map_from_numpy<'py>( + py: Python<'py>, + map: &impl Map, + index: usize, + coords: PyReadonlyArrayDyn, + ) -> PyResult<(usize, &'py PyArrayDyn)> { + if coords.ndim() == 0 { + return Err(PyValueError::new_err( + "the `coords` argument must have at least one dimension", + )); + } + if coords.shape()[coords.ndim() - 1] != map.dim_in() { + return Err(PyValueError::new_err(format!( + "the last axis of the `coords` argument should have dimension {}", + map.dim_in() + ))); + } + let mut result: Vec = coords + .as_array() + .rows() + .into_iter() + .flat_map(|row| { + row.into_iter() + .cloned() + .chain(iter::repeat(0.0).take(map.delta_dim())) + }) + .collect(); + let index = map.apply_inplace(index, &mut result, map.dim_out(), 0)?; + let result = PyArray::from_vec(py, result); + let shape: Vec = coords + .shape() + .iter() + .take(coords.ndim() - 1) + .cloned() + .chain(iter::once(map.dim_out())) + .collect(); + let result = result.reshape(&shape[..])?; + Ok((index, result)) + } + + #[pyclass(name = "CoordSystem", module = "nutils._rust")] + #[derive(Debug, Clone)] + struct PyCoordSystem(CoordSystem); + + #[pymethods] + impl PyCoordSystem { + #[new] + pub fn new(dim: usize, len: usize) -> Self { + CoordSystem::new(dim, len).into() + } + pub fn __repr__(&self) -> String { + format!("{:?}", self.0) + } + pub fn __richcmp__<'py>( + &self, + py: Python<'py>, + other: &'py PyAny, + op: CompareOp, + ) -> PyObject { + if let Ok(other) = PyCoordSystem::extract(other) { + match op { + CompareOp::Eq => (self.0 == other.0).into_py(py), + CompareOp::Ne => (self.0 != other.0).into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } else { + match op { + CompareOp::Eq => false.into_py(py), + CompareOp::Ne => true.into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } + } + pub fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + pub fn __len__(&self) -> usize { + self.0.len() + } + #[getter] + pub fn dim(&self) -> usize { + self.0.dim() + } + pub fn __mul__(&self, rhs: &PyCoordSystem) -> Self { + Self(&self.0 * &rhs.0) + } + pub fn concat(&self, other: &PyCoordSystem) -> PyResult { + Ok(Self(self.0.concat(&other.0)?)) + } + pub fn slice(&self, s: &PySlice) -> PyResult { + let len: isize = self.0.len().try_into()?; + let PySliceIndices { start, stop, step, slicelength } = s.indices(len.try_into()?)?; + if start == 0 && stop == len && step == 1 { + Ok(self.clone()) + } else if step == 1 { + Ok(Self(self.0.slice(start.try_into()?, slicelength.try_into()?)?)) + } else { + let indices: Vec = (0..slicelength).map(|i| (start + i * step) as usize).collect(); + Ok(Self(self.0.take(&indices)?)) + } + } + pub fn take(&self, indices: Vec) -> PyResult { + Ok(Self(self.0.take(&indices)?)) + } + pub fn children(&self, simplex: &PySimplex, offset: usize) -> PyResult { + Ok(Self(self.0.children(simplex.into(), offset)?)) + } + pub fn edges(&self, simplex: &PySimplex, offset: usize) -> PyResult { + Ok(Self(self.0.edges(simplex.into(), offset)?)) + } + pub fn uniform_points( + &self, + points: PyReadonlyArray2, + offset: usize, + ) -> PyResult { + let point_dim = points.shape()[1]; + let points: Vec = points.as_array().iter().cloned().collect(); + Ok(Self(self.0.uniform_points(points, point_dim, offset)?)) + } + pub fn trans_to(&self, target: &Self) -> PyResult { + self.0 + .relative_to(&target.0) + .map(|rel| PyCoordTrans(rel)) + .ok_or(PyValueError::new_err("cannot make relative")) + } + } + + impl From for PyCoordSystem { + fn from(transforms: CoordSystem) -> PyCoordSystem { + PyCoordSystem(transforms) + } + } + + m.add_class::()?; + + #[pyclass(name = "CoordTrans", module = "nutils._rust")] + #[derive(Debug, Clone)] + struct PyCoordTrans(>::Output); + + #[pymethods] + impl PyCoordTrans { + pub fn __repr__(&self) -> String { + format!("{:?}", self.0) + } + pub fn __richcmp__<'py>( + &self, + py: Python<'py>, + other: &'py PyAny, + op: CompareOp, + ) -> PyObject { + if let Ok(other) = PyCoordTrans::extract(other) { + match op { + CompareOp::Eq => (self.0 == other.0).into_py(py), + CompareOp::Ne => (self.0 != other.0).into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } else { + match op { + CompareOp::Eq => false.into_py(py), + CompareOp::Ne => true.into_py(py), + CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => { + py.NotImplemented() + } + } + } + } + pub fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + #[getter] + pub fn from_len(&self) -> usize { + self.0.len_in() + } + #[getter] + pub fn to_len(&self) -> usize { + self.0.len_out() + } + #[getter] + pub fn from_dim(&self) -> usize { + self.0.dim_in() + } + #[getter] + pub fn to_dim(&self) -> usize { + self.0.dim_out() + } + pub fn apply_index(&self, index: usize) -> PyResult { + self.0 + .apply_index(index) + .ok_or(PyIndexError::new_err("index out of range")) + } + pub fn apply_indices(&self, indices: Vec) -> PyResult> { + self.0 + .apply_indices(&indices) + .ok_or(PyIndexError::new_err("index out of range")) + } + pub fn apply<'py>( + &self, + py: Python<'py>, + index: usize, + coords: PyReadonlyArrayDyn, + ) -> PyResult<(usize, &'py PyArrayDyn)> { + apply_map_from_numpy(py, &self.0, index, coords) + } + pub fn unapply_indices(&self, indices: Vec) -> PyResult> { + self.0 + .unapply_indices(&indices) + .map(|mut indices| { + indices.sort(); + indices + }) + .ok_or(PyValueError::new_err("index out of range")) + } + pub fn basis<'py>(&self, py: Python<'py>, index: usize) -> PyResult<&'py PyArray2> { + if index >= self.0.len_in() { + return Err(PyIndexError::new_err("index out of range")); + } + let mut basis: Vec = iter::repeat(0.0).take(self.0.dim_out() * self.0.dim_out()).collect(); + for i in 0..self.0.dim_in() { + basis[i * self.0.dim_out() + i] = 1.0; + } + let mut dim_in = self.0.dim_in(); + self.0.update_basis(index, &mut basis[..], self.0.dim_out(), &mut dim_in, 0); + PyArray::from_vec(py, basis).reshape([self.0.dim_out(), self.0.dim_out()]) + } + #[getter] + pub fn is_identity(&self) -> bool { + self.0.is_identity() + } + #[getter] + pub fn is_index_map(&self) -> bool { + self.0.is_index_map() + } + #[getter] + pub fn basis_is_constant(&self) -> bool { + self.0.basis_is_constant() + } + } + + m.add_class::()?; + + Ok(()) +} diff --git a/src/map/coord_system.rs b/src/map/coord_system.rs new file mode 100644 index 000000000..eb243c0e4 --- /dev/null +++ b/src/map/coord_system.rs @@ -0,0 +1,343 @@ +use super::ops::UniformConcat; +use super::primitive::{ + AllPrimitiveDecompositions, Primitive, PrimitiveDecompositionIter, UnboundedMap, WithBounds, +}; +use super::relative::RelativeTo; +use super::{AddOffset, Error, Map, UnapplyIndicesData}; +use crate::simplex::Simplex; +use crate::util::{ReplaceNthIter, SkipNthIter}; +use std::iter; +use std::ops::Mul; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct UniformCoordSystem(WithBounds>); + +impl UniformCoordSystem { + pub fn new(dim: usize, len: usize) -> Self { + Self(WithBounds::new_unchecked(Vec::new(), dim, len)) + } + pub fn clone_and_push(&self, primitive: Primitive) -> Result { + if self.0.dim_in() < primitive.dim_out() { + return Err(Error::DimensionMismatch); + } + if self.0.len_in() % primitive.mod_out() != 0 { + return Err(Error::LengthMismatch); + } + let dim_in = self.0.dim_in() - primitive.delta_dim(); + let len_in = self.0.len_in() / primitive.mod_out() * primitive.mod_in(); + let map = self + .0 + .unbounded() + .iter() + .cloned() + .chain(iter::once(primitive)) + .collect(); + Ok(Self(WithBounds::new_unchecked(map, dim_in, len_in))) + } + fn mul(&self, rhs: &Self) -> Self { + let offset = self.0.dim_in(); + let map = iter::once(Primitive::new_transpose(rhs.0.len_out(), self.0.len_out())) + .chain(self.0.unbounded().iter().cloned()) + .chain(iter::once(Primitive::new_transpose( + self.0.len_in(), + rhs.0.len_out(), + ))) + .chain(rhs.0.unbounded().iter().map(|item| { + let mut item = item.clone(); + item.add_offset(offset); + item + })) + .collect(); + Self(WithBounds::new_unchecked( + map, + self.0.dim_in() + rhs.0.dim_in(), + self.0.len_in() * rhs.0.len_in(), + )) + } +} + +//impl Deref for UniformCoordSystem { +// type Target = WithBounds>; +// +// fn deref(&self) -> Self::Target { +// self.map +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.map.$fn($($arg),*) + } + }; +} + +impl Map for UniformCoordSystem { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} + dispatch! {fn basis_is_constant(&self) -> bool} +} + +impl AllPrimitiveDecompositions for UniformCoordSystem { + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { + Box::new( + self.0 + .all_primitive_decompositions() + .map(|(prim, map)| (prim, Self(map))), + ) + } +} + +impl RelativeTo for UniformCoordSystem { + type Output = > as RelativeTo>>>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.0.relative_to(&target.0) + } +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct CoordSystem(UniformConcat); + +impl CoordSystem { + pub fn new(dim: usize, len: usize) -> Self { + let identity = UniformCoordSystem::new(dim, len); + Self(UniformConcat::new_unchecked(vec![identity], dim, 0, len)) + } + fn mul(&self, rhs: &Self) -> Self { + let products = self + .0 + .iter() + .flat_map(|lhs| rhs.0.iter().map(move |rhs| lhs * rhs)) + .collect(); + CoordSystem(UniformConcat::new_unchecked( + products, + self.dim_in() + rhs.dim_in(), + self.delta_dim() + rhs.delta_dim(), + self.len_out() * rhs.len_out(), + )) + } + pub fn len(&self) -> usize { + self.len_in() + } + pub fn dim(&self) -> usize { + self.dim_in() + } + pub fn concat(&self, other: &Self) -> Result { + let maps = self.0.iter().chain(other.0.iter()).cloned().collect(); + Ok(Self(UniformConcat::new( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + )?)) + } + pub fn slice(&self, mut start: usize, mut len: usize) -> Result { + if start + len > self.0.len_in() { + return Err(Error::IndexOutOfRange); + } + let mut maps = Vec::new(); + for map in self.0.iter() { + if len == 0 { + break; + } else if start >= map.len_in() { + start -= map.len_in(); + continue; + } else if start == 0 && len >= map.len_in() { + maps.push(map.clone()); + len -= map.len_in(); + } else { + let primitive = Primitive::new_slice(start, len, map.len_in()); + maps.push(map.clone_and_push(primitive).unwrap()); + if start + len <= map.len_in() { + break; + } + len -= map.len_in() - start; + start = 0; + } + } + Ok(Self(UniformConcat::new_unchecked( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + ))) + } + pub fn take(&self, indices: &[usize]) -> Result { + if !indices.windows(2).all(|pair| pair[0] < pair[1]) { + return Err(Error::IndicesNotStrictIncreasing); + } + if let Some(last) = indices.last() { + if *last >= self.0.len_in() { + return Err(Error::IndexOutOfRange); + } + } + let mut maps = Vec::new(); + let mut offset = 0; + let mut start = 0; + for map in self.0.iter() { + let stop = start + indices[start..].partition_point(|i| *i < offset + map.len_in()); + if stop > start { + let map_indices: Vec<_> = + indices[start..stop].iter().map(|i| *i - offset).collect(); + start = stop; + let primitive = Primitive::new_take(map_indices, map.len_in()); + maps.push(map.clone_and_push(primitive).unwrap()); + } + offset += map.len_in(); + } + assert_eq!(start, indices.len()); + Ok(Self(UniformConcat::new_unchecked( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + ))) + } + fn clone_and_push(&self, primitive: Primitive) -> Result { + if self.0.dim_in() < primitive.dim_out() { + return Err(Error::DimensionMismatch); + } + if self.0.len_in() % primitive.mod_out() != 0 { + return Err(Error::LengthMismatch); + } + let maps = self + .0 + .iter() + .map(|map| map.clone_and_push(primitive.clone()).unwrap()); + Ok(Self(UniformConcat::new_unchecked( + maps.collect(), + self.dim_in() - primitive.delta_dim(), + self.delta_dim() + primitive.delta_dim(), + self.len_out(), + ))) + } + pub fn children(&self, simplex: Simplex, offset: usize) -> Result { + self.clone_and_push(Primitive::new_children(simplex).with_offset(offset)) + } + pub fn edges(&self, simplex: Simplex, offset: usize) -> Result { + self.clone_and_push(Primitive::new_edges(simplex).with_offset(offset)) + } + pub fn uniform_points( + &self, + points: impl Into>, + point_dim: usize, + offset: usize, + ) -> Result { + self.clone_and_push(Primitive::new_uniform_points(points, point_dim).with_offset(offset)) + } +} + +//impl Deref for CoordSystem { +// type Target = OptionReorder>, Vec>; +// +// fn deref(&self) -> Self::Target { +// self.0 +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; +} + +impl Map for CoordSystem { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} + dispatch! {fn basis_is_constant(&self) -> bool} +} + +impl RelativeTo for CoordSystem { + type Output = + as RelativeTo>>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.0.relative_to(&target.0) + } +} + +impl Mul for &UniformCoordSystem { + type Output = UniformCoordSystem; + + fn mul(self, rhs: Self) -> UniformCoordSystem { + UniformCoordSystem::mul(self, rhs) + } +} + +impl Mul for UniformCoordSystem { + type Output = UniformCoordSystem; + + fn mul(self, rhs: Self) -> UniformCoordSystem { + UniformCoordSystem::mul(&self, &rhs) + } +} + +impl Mul for &CoordSystem { + type Output = CoordSystem; + + fn mul(self, rhs: Self) -> CoordSystem { + CoordSystem::mul(self, rhs) + } +} + +impl Mul for CoordSystem { + type Output = CoordSystem; + + fn mul(self, rhs: Self) -> CoordSystem { + CoordSystem::mul(&self, &rhs) + } +} diff --git a/src/map/mod.rs b/src/map/mod.rs new file mode 100644 index 000000000..9ebccf987 --- /dev/null +++ b/src/map/mod.rs @@ -0,0 +1,188 @@ +pub mod ops; +pub mod primitive; +pub mod relative; +pub mod coord_system; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + Empty, + DimensionMismatch, + LengthMismatch, + DimensionZeroHasNoEdges, + StrideSmallerThanOutputDimension, + IndexOutOfRange, + IndicesNotStrictIncreasing, +} + +impl std::error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Empty => write!(f, "The input array is empty."), + Self::DimensionMismatch => write!(f, "The dimensions of the maps differ."), + Self::LengthMismatch => write!(f, "The lengths of the maps differ."), + Self::DimensionZeroHasNoEdges => write!(f, "Dimension zero has no edges."), + Self::StrideSmallerThanOutputDimension => write!( + f, + "The stride of the `coords` argument is smaller than the output dimension." + ), + Self::IndexOutOfRange => write!(f, "The index is out of range."), + Self::IndicesNotStrictIncreasing => { + write!(f, "The indices are not strict monotonic increasing.") + } + } + } +} + +/// An interface for an index and coordinate map. +pub trait Map: std::fmt::Debug { + /// Returns the exclusive upper bound of the indices in the codomain. + fn len_out(&self) -> usize; + /// Returns the exclusive upper bound of the indices of the domain. + fn len_in(&self) -> usize; + /// Returns the dimension of the coordinates of the codimain. + fn dim_out(&self) -> usize { + self.dim_in() + self.delta_dim() + } + /// Returns the dimension of the coordinates of the dimain. + fn dim_in(&self) -> usize; + /// Returns the dimension difference of the coordinates in the codomain and the domain. + fn delta_dim(&self) -> usize; + /// Apply the given index and coordinate, the latter in-place, without + /// checking whether the index is inside the domain and the coordinates + /// have at least dimension [`Self::dim_out()`]. + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize; + /// Applies the given index and coordinate, the latter in-place. The + /// coordinates must have a dimension not smaller than + /// [`Self::dim_out()`]. Returns `None` if the index is outside the domain. + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> Result { + if index >= self.len_in() { + Err(Error::IndexOutOfRange) + } else if offset + self.dim_out() > stride { + Err(Error::StrideSmallerThanOutputDimension) + } else { + Ok(self.apply_inplace_unchecked(index, coords, stride, offset)) + } + } + /// Applies the given index without checking that the index is inside the + /// domain. + fn apply_index_unchecked(&self, index: usize) -> usize; + /// Apply the given index. Returns `None` if the index is outside the + /// domain, + fn apply_index(&self, index: usize) -> Option { + if index < self.len_in() { + Some(self.apply_index_unchecked(index)) + } else { + None + } + } + /// Applies the given indices in-place without checking that the indices are + /// inside the domain. + fn apply_indices_inplace_unchecked(&self, indices: &mut [usize]) { + for index in indices.iter_mut() { + *index = self.apply_index_unchecked(*index); + } + } + /// Applies the given indices in-place. Returns `None` if any of the + /// indices is outside the domain. + fn apply_indices(&self, indices: &[usize]) -> Option> { + if indices.iter().all(|index| *index < self.len_in()) { + let mut indices = indices.to_vec(); + self.apply_indices_inplace_unchecked(&mut indices); + Some(indices) + } else { + None + } + } + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec; + fn unapply_indices(&self, indices: &[T]) -> Option> { + if indices.iter().all(|index| index.get() < self.len_out()) { + Some(self.unapply_indices_unchecked(indices)) + } else { + None + } + } + /// Returns true if this map is the identity map. + fn is_identity(&self) -> bool; + /// Returns true if this map returns coordinates unaltered. + fn is_index_map(&self) -> bool; + // TODO: return Result, add index, dimension checks + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize; + fn basis_is_constant(&self) -> bool; +} + +pub trait AddOffset { + fn add_offset(&mut self, offset: usize); +} + +pub trait UnapplyIndicesData: Clone + std::fmt::Debug { + fn get(&self) -> usize; + fn set(&self, index: usize) -> Self; +} + +impl UnapplyIndicesData for usize { + #[inline] + fn get(&self) -> usize { + *self + } + #[inline] + fn set(&self, index: usize) -> Self { + index + } +} + +#[macro_export] +macro_rules! assert_map_apply { + ($item:expr, $inidx:expr, $incoords:expr, $outidx:expr, $outcoords:expr) => {{ + use std::borrow::Borrow; + let item = $item.borrow(); + let incoords = $incoords; + let outcoords = $outcoords; + assert_eq!(incoords.len(), outcoords.len(), "incoords outcoords"); + let stride; + let mut work: Vec<_>; + if incoords.len() == 0 { + stride = item.dim_out(); + work = Vec::with_capacity(0); + } else { + stride = outcoords[0].len(); + work = iter::repeat(-1.0).take(outcoords.len() * stride).collect(); + for (work, incoord) in iter::zip(work.chunks_mut(stride), incoords.iter()) { + work[..incoord.len()].copy_from_slice(incoord); + } + } + assert_eq!( + item.apply_inplace($inidx, &mut work, stride, 0), + Ok($outidx), + "apply_inplace", + ); + assert_eq!(item.apply_index($inidx), Some($outidx), "apply_index"); + for (actual, desired) in iter::zip(work.chunks(stride), outcoords.iter()) { + assert_abs_diff_eq!(actual[..], desired[..]); + } + }}; + ($item:expr, $inidx:expr, $outidx:expr) => {{ + use std::borrow::Borrow; + let item = $item.borrow(); + let mut work = Vec::with_capacity(0); + assert_eq!( + item.apply_inplace($inidx, &mut work, item.dim_out(), 0) + .unwrap(), + $outidx + ); + assert_eq!(item.apply_index($inidx), Some($outidx)); + }}; +} diff --git a/src/map/ops.rs b/src/map/ops.rs new file mode 100644 index 000000000..ccf0010dc --- /dev/null +++ b/src/map/ops.rs @@ -0,0 +1,918 @@ +use super::{Error, Map, UnapplyIndicesData}; +use num::Integer as _; +use std::ops::Deref; + +/// The composition of two maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct BinaryComposition(Outer, Inner); + +impl BinaryComposition { + /// Returns the composition of two maps. + /// + /// The input dimension and length of the first map must equal the output + /// dimension and length of the second map. + /// + /// Returns an [`Error`] if the dimensions and lengths don't match. + pub fn new(outer: Outer, inner: Inner) -> Result { + if inner.dim_out() != outer.dim_in() { + Err(Error::DimensionMismatch) + } else if inner.len_out() != outer.len_in() { + Err(Error::LengthMismatch) + } else { + Ok(Self(outer, inner)) + } + } + pub fn new_unchecked(outer: Outer, inner: Inner) -> Self { + Self::new(outer, inner).unwrap() + } + /// Returns the outer map of the composition. + pub fn outer(&self) -> &Outer { + &self.0 + } + /// Returns the inner map of the composition. + pub fn inner(&self) -> &Inner { + &self.1 + } +} + +impl Map for BinaryComposition { + #[inline] + fn dim_in(&self) -> usize { + self.1.dim_in() + } + #[inline] + fn delta_dim(&self) -> usize { + self.0.delta_dim() + self.1.delta_dim() + } + #[inline] + fn len_in(&self) -> usize { + self.1.len_in() + } + #[inline] + fn len_out(&self) -> usize { + self.0.len_out() + } + #[inline] + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + let index = self + .1 + .apply_inplace_unchecked(index, coords, stride, offset); + self.0 + .apply_inplace_unchecked(index, coords, stride, offset) + } + #[inline] + fn apply_index_unchecked(&self, index: usize) -> usize { + self.0 + .apply_index_unchecked(self.1.apply_index_unchecked(index)) + } + #[inline] + fn apply_indices_inplace_unchecked(&self, indices: &mut [usize]) { + self.1.apply_indices_inplace_unchecked(indices); + self.0.apply_indices_inplace_unchecked(indices); + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + self.1 + .unapply_indices_unchecked(&self.0.unapply_indices_unchecked(indices)) + } + #[inline] + fn is_identity(&self) -> bool { + self.0.is_identity() && self.1.is_identity() + } + #[inline] + fn is_index_map(&self) -> bool { + self.0.is_index_map() && self.1.is_index_map() + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + let index = self.1.update_basis(index, basis, dim_out, dim_in, offset); + self.0.update_basis(index, basis, dim_out, dim_in, offset) + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.0.basis_is_constant() && self.1.basis_is_constant() + } +} + +/// The composition of an unempty sequence of maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct UniformComposition>(Array) +where + M: Map, + Array: Deref; + +impl UniformComposition +where + M: Map, + Array: Deref, +{ + /// Returns the composition of an unempty sequence of maps. + /// + /// For every consecutive pair of maps in the sequence the input dimension + /// and length of the first map must equal the output dimension and length + /// of the second map. + /// + /// Returns an [`Error`] if the dimensions and lengths don't match or the + /// sequence is empty. + pub fn new(array: Array) -> Result { + let mut iter = array.iter().rev(); + if let Some(map) = iter.next() { + let mut dim_out = map.dim_out(); + let mut len_out = map.len_out(); + for map in iter { + if map.dim_in() != dim_out { + return Err(Error::DimensionMismatch); + } else if map.len_in() != len_out { + return Err(Error::LengthMismatch); + } + dim_out = map.dim_out(); + len_out = map.len_out(); + } + Ok(Self(array)) + } else { + Err(Error::Empty) + } + } + pub fn new_unchecked(array: Array) -> Self { + Self(array) + } + pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, M> { + self.0.iter() + } +} + +impl Map for UniformComposition +where + M: Map, + Array: Deref + std::fmt::Debug, +{ + #[inline] + fn dim_in(&self) -> usize { + self.0.last().unwrap().dim_in() + } + #[inline] + fn dim_out(&self) -> usize { + self.0.first().unwrap().dim_out() + } + #[inline] + fn delta_dim(&self) -> usize { + self.dim_out() - self.dim_in() + } + #[inline] + fn len_in(&self) -> usize { + self.0.last().unwrap().len_in() + } + #[inline] + fn len_out(&self) -> usize { + self.0.first().unwrap().len_out() + } + #[inline] + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.iter().rev().fold(index, |index, map| { + map.apply_inplace_unchecked(index, coords, stride, offset) + }) + } + #[inline] + fn apply_index_unchecked(&self, index: usize) -> usize { + self.iter() + .rev() + .fold(index, |index, map| map.apply_index_unchecked(index)) + } + #[inline] + fn apply_indices_inplace_unchecked(&self, indices: &mut [usize]) { + self.iter() + .rev() + .for_each(|map| map.apply_indices_inplace_unchecked(indices)); + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + let mut iter = self.iter(); + let indices = iter.next().unwrap().unapply_indices_unchecked(indices); + iter.fold(indices, |indices, map| { + map.unapply_indices_unchecked(&indices) + }) + } + #[inline] + fn is_identity(&self) -> bool { + self.iter().all(|map| map.is_identity()) + } + #[inline] + fn is_index_map(&self) -> bool { + self.iter().all(|map| map.is_index_map()) + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.iter().rev().fold(index, |index, map| map.update_basis(index, basis, dim_out, dim_in, offset)) + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.iter().all(|map| map.basis_is_constant()) + } +} + +/// The concatenation of two maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct BinaryConcat(M0, M1); + +impl BinaryConcat { + /// Returns the concatenation of two maps. + /// + /// The two maps must have the same input and output dimensions and the + /// same output length. The maps must not overlap. + /// + /// Returns an [`Error`] if the dimensions and lengths don't match. + pub fn new(map0: M0, map1: M1) -> Result { + if map0.dim_in() != map1.dim_in() || map0.dim_out() != map1.dim_out() { + Err(Error::DimensionMismatch) + } else if map0.len_out() != map1.len_out() { + Err(Error::LengthMismatch) + } else { + Ok(Self(map0, map1)) + } + } + pub fn new_unchecked(map0: M0, map1: M1) -> Self { + Self(map0, map1) + } + /// Returns the first map of the concatenation. + pub fn first(&self) -> &M0 { + &self.0 + } + /// Returns the second map of the concatenation. + pub fn second(&self) -> &M1 { + &self.1 + } +} + +impl Map for BinaryConcat { + #[inline] + fn dim_in(&self) -> usize { + self.0.dim_in() + } + #[inline] + fn dim_out(&self) -> usize { + self.0.dim_out() + } + #[inline] + fn delta_dim(&self) -> usize { + self.0.delta_dim() + } + #[inline] + fn len_in(&self) -> usize { + self.0.len_in() + self.1.len_in() + } + #[inline] + fn len_out(&self) -> usize { + self.0.len_out() + } + #[inline] + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + if index < self.0.len_in() { + self.0 + .apply_inplace_unchecked(index, coords, stride, offset) + } else { + self.1 + .apply_inplace_unchecked(index - self.0.len_in(), coords, stride, offset) + } + } + #[inline] + fn apply_index_unchecked(&self, index: usize) -> usize { + if index < self.0.len_in() { + self.0.apply_index_unchecked(index) + } else { + self.1.apply_index_unchecked(index - self.0.len_in()) + } + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + let mut result = self.0.unapply_indices_unchecked(indices); + result.extend( + self.1 + .unapply_indices_unchecked(indices) + .into_iter() + .map(|i| i.set(i.get() + self.0.len_in())), + ); + result + } + #[inline] + fn is_identity(&self) -> bool { + false + } + #[inline] + fn is_index_map(&self) -> bool { + self.0.is_index_map() && self.1.is_index_map() + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + if index < self.0.len_in() { + self.0.update_basis(index, basis, dim_out, dim_in, offset) + } else { + self.1.update_basis(index - self.0.len_in(), basis, dim_out, dim_in, offset) + } + } + #[inline] + fn basis_is_constant(&self) -> bool { + false + } +} + +/// The concatenation of an unempty sequence of maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct UniformConcat> +where + M: Map, + Array: Deref, +{ + maps: Array, + dim_in: usize, + delta_dim: usize, + len_out: usize, + len_in: usize, +} + +impl UniformConcat +where + M: Map, + Array: Deref, +{ + /// Returns the concatenation of an unempty sequence of maps. + /// + /// The maps must not overlap. + /// + /// Returns an [`Error`] if the dimensions and lengths don't match. + pub fn new( + maps: Array, + dim_in: usize, + delta_dim: usize, + len_out: usize, + ) -> Result { + let mut len_in = 0; + for map in maps.iter() { + len_in += map.len_in(); + if map.dim_in() != dim_in || map.delta_dim() != delta_dim { + return Err(Error::DimensionMismatch); + } else if map.len_out() != len_out { + return Err(Error::LengthMismatch); + } + } + Ok(Self { + maps, + dim_in, + delta_dim, + len_out, + len_in, + }) + } + pub fn new_unchecked(maps: Array, dim_out: usize, dim_in: usize, len_out: usize) -> Self { + Self::new(maps, dim_out, dim_in, len_out).unwrap() + } + pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, M> { + self.maps.iter() + } +} + +impl Map for UniformConcat +where + M: Map, + Array: Deref + std::fmt::Debug, +{ + #[inline] + fn dim_in(&self) -> usize { + self.dim_in + } + #[inline] + fn delta_dim(&self) -> usize { + self.delta_dim + } + #[inline] + fn len_in(&self) -> usize { + self.len_in + } + #[inline] + fn len_out(&self) -> usize { + self.len_out + } + #[inline] + fn apply_inplace_unchecked( + &self, + mut index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + for map in self.iter() { + if index < map.len_in() { + return map.apply_inplace_unchecked(index, coords, stride, offset); + } + index -= map.len_in(); + } + unreachable! {} + } + #[inline] + fn apply_index_unchecked(&self, mut index: usize) -> usize { + for map in self.iter() { + if index < map.len_in() { + return map.apply_index_unchecked(index); + } + index -= map.len_in(); + } + unreachable! {} + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + let mut iter = self.iter(); + if let Some(map) = iter.next() { + let mut result = map.unapply_indices_unchecked(indices); + let mut offset = map.len_in(); + for map in iter { + result.extend( + map.unapply_indices_unchecked(indices) + .into_iter() + .map(|i| i.set(i.get() + offset)), + ); + offset += map.len_in(); + } + result + } else { + Vec::new() + } + } + #[inline] + fn is_identity(&self) -> bool { + self.maps.len() == 1 && self.maps[0].is_identity() + } + #[inline] + fn is_index_map(&self) -> bool { + self.iter().all(|item| item.is_index_map()) + } + #[inline] + fn update_basis(&self, mut index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + for map in self.iter() { + if index < map.len_in() { + return map.update_basis(index, basis, dim_out, dim_in, offset) + } + index -= map.len_in(); + } + unreachable! {} + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.maps.len() == 1 + } +} + +/// The product of two maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct BinaryProduct(M0, M1); + +impl BinaryProduct { + /// Returns the product of two maps. + pub fn new(map0: M0, map1: M1) -> Self { + Self(map0, map1) + } + /// Returns the first term of the product. + pub fn first(&self) -> &M0 { + &self.0 + } + /// Returns the second term of the product. + pub fn second(&self) -> &M1 { + &self.1 + } +} + +impl Map for BinaryProduct { + #[inline] + fn dim_in(&self) -> usize { + self.0.dim_in() + self.1.dim_in() + } + #[inline] + fn dim_out(&self) -> usize { + self.0.dim_out() + self.0.dim_out() + } + #[inline] + fn delta_dim(&self) -> usize { + self.0.delta_dim() + self.1.delta_dim() + } + #[inline] + fn len_in(&self) -> usize { + self.0.len_in() * self.1.len_in() + } + #[inline] + fn len_out(&self) -> usize { + self.0.len_out() * self.1.len_out() + } + #[inline] + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + let (index0, index1) = index.div_rem(&self.1.len_in()); + let index0 = self + .0 + .apply_inplace_unchecked(index0, coords, stride, offset); + let index1 = + self.1 + .apply_inplace_unchecked(index1, coords, stride, offset + self.0.dim_out()); + index0 * self.1.len_out() + index1 + } + #[inline] + fn apply_index_unchecked(&self, index: usize) -> usize { + let (index0, index1) = index.div_rem(&self.1.len_in()); + let index0 = self.0.apply_index_unchecked(index0); + let index1 = self.1.apply_index_unchecked(index1); + index0 * self.1.len_out() + index1 + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + // TODO: collect unique indices per map, unapply and merge + let idx: Vec<_> = indices + .iter() + .enumerate() + .map(|(i, j)| { + let (j, k) = j.get().div_rem(&self.1.len_out()); + UnapplyBinaryProduct(i, k, j) + }) + .collect(); + let mut idx = self.0.unapply_indices_unchecked(&idx); + idx.iter_mut() + .for_each(|UnapplyBinaryProduct(_, ref mut j, ref mut k)| std::mem::swap(j, k)); + let idx = self.1.unapply_indices_unchecked(&idx); + let idx = idx + .into_iter() + .map(|UnapplyBinaryProduct(i, j, k)| indices[i].set(j * self.1.len_out() + k)) + .collect(); + idx + } + #[inline] + fn is_identity(&self) -> bool { + self.0.is_identity() && self.1.is_identity() + } + #[inline] + fn is_index_map(&self) -> bool { + self.0.is_index_map() && self.1.is_index_map() + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + unimplemented!{} + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.0.basis_is_constant() && self.1.basis_is_constant() + } +} + +#[derive(Debug, Clone)] +struct UnapplyBinaryProduct(usize, usize, usize); + +impl UnapplyIndicesData for UnapplyBinaryProduct { + fn get(&self) -> usize { + self.2 + } + fn set(&self, index: usize) -> Self { + Self(self.0, self.1, index) + } +} + +/// The product of a sequence of maps. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct UniformProduct>(Array) +where + M: Map, + Array: Deref; + +impl UniformProduct +where + M: Map, + Array: Deref, +{ + /// Returns the product of a sequence of maps. + pub fn new(array: Array) -> Self { + Self(array) + } + /// Returns an iterator of the terms of the product. + pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, M> { + self.0.iter() + } + pub fn strides_out(&self) -> Vec { + let mut strides = Vec::with_capacity(self.0.len()); + let mut stride = 1; + for map in self.iter().rev() { + strides.push(stride); + stride *= map.len_out(); + } + strides.reverse(); + strides + } + pub fn offsets_out(&self) -> Vec { + let mut offsets = Vec::with_capacity(self.0.len()); + let mut offset = 0; + for map in self.iter() { + offsets.push(offset); + offset += map.dim_out(); + } + offsets + } +} + +//impl Map for UniformProduct +//where +// M: Map, +// Array: Deref, +//{ +// #[inline] +// fn dim_in(&self) -> usize { +// self.iter().map(|map| map.dim_in()).sum() +// } +// #[inline] +// fn dim_out(&self) -> usize { +// self.iter().map(|map| map.dim_out()).sum() +// } +// #[inline] +// fn delta_dim(&self) -> usize { +// self.iter().map(|map| map.delta_dim()).sum() +// } +// #[inline] +// fn len_in(&self) -> usize { +// self.iter().map(|map| map.len_in()).product() +// } +// #[inline] +// fn len_out(&self) -> usize { +// self.iter().map(|map| map.len_out()).product() +// } +// #[inline] +// fn apply_inplace_unchecked( +// &self, +// index: usize, +// coords: &mut [f64], +// stride: usize, +// mut offset: usize, +// ) -> usize { +// let mut iout = 0; +// for (map, iin) in self.iter().zip(self.unravel_index(index)) { +// iout = iout * map.len_out() + self.apply_inplace_unchecked(iin, coords, stride, offset); +// offset += map.dim_out(); +// } +// out_index +// } +// #[inline] +// fn apply_index_unchecked(&self, mut index: usize) -> usize { +// let mut out_index = 0; +// for (map, iin) in self.iter().zip(self.unravel_index(index)) { +// iout = iout * map.len_out() + self.apply_index_unchecked(iin); +// } +// iout +// } +// #[inline] +// fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { +// let indices: Vec<_> = indices +// .iter() +// .map(|k| { +// let (i, j) = k.get().div_rem(&self.0.len_in()); +// UnapplyBinaryProduct(i, j, k.clone()) +// }) +// .collect(); +// +// +// let mut iter = self.iter(); +// let map = iter.next().unwrap(); +// let mut result = map.unapply_indices_unchecked(indices); +// let mut offset = map.len_in(); +// for map in iter { +// result.extend( +// map.unapply_indices_unchecked(indices) +// .into_iter() +// .map(|i| i.set(i.get() + offset)), +// ); +// offset += map.len_in(); +// } +// result +// } +// #[inline] +// fn is_identity(&self) -> bool { +// false +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* $(,)? + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + if $self.0.deref().len() == 1 { + $self.0.deref()[0].$fn($($arg),*) + } else { + BinaryProduct( + UniformProduct(&$self.0.deref()[..1]), + UniformProduct(&$self.0.deref()[1..]), + ).$fn($($arg),*) + } + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + if $self.0.deref().len() == 1 { + $self.0.deref_mut()[0].$fn($($arg),*) + } else { + BinaryProduct( + UniformProduct(&$self.0.deref_mut()[..1]), + UniformProduct(&$self.0.deref_mut()[1..]), + ).$fn($($arg),*) + } + } + }; +} + +impl Map for UniformProduct +where + M: Map, + Array: Deref + std::fmt::Debug, +{ + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize} + dispatch! {fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} + dispatch! {fn basis_is_constant(&self) -> bool} +} + +impl FromIterator for UniformProduct> { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + UniformProduct::new(iter.into_iter().collect()) + } +} + +// pub struct OptionReorder(map, Option); + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_map_apply; + use crate::prim_comp; + use approx::assert_abs_diff_eq; + use std::iter; + + #[test] + fn uniform_composition1() { + let map = UniformComposition::new(vec![prim_comp![Point * 1]]).unwrap(); + assert_eq!(map.len_in(), 1); + assert_eq!(map.len_out(), 1); + assert_eq!(map.dim_in(), 0); + assert_eq!(map.delta_dim(), 0); + assert_eq!(map.apply_index(0), Some(0)); + assert_eq!(map.apply_index(1), None); + } + + #[test] + fn uniform_composition2() { + let map = UniformComposition::new(vec![ + prim_comp![Point*12 <- Transpose(4, 3)], + prim_comp![Point*12 <- Take([1, 0], 3)], + ]) + .unwrap(); + assert_eq!(map.len_in(), 8); + assert_eq!(map.len_out(), 12); + assert_eq!(map.dim_in(), 0); + assert_eq!(map.delta_dim(), 0); + assert_eq!(map.apply_index(0), Some(4)); + assert_eq!(map.apply_index(1), Some(0)); + assert_eq!(map.apply_index(2), Some(5)); + assert_eq!(map.apply_index(3), Some(1)); + assert_eq!(map.apply_index(4), Some(6)); + assert_eq!(map.apply_index(5), Some(2)); + assert_eq!(map.apply_index(6), Some(7)); + assert_eq!(map.apply_index(7), Some(3)); + assert_eq!(map.apply_index(8), None); + } + + #[test] + fn uniform_composition3() { + let map = UniformComposition::new(vec![ + prim_comp![Line*1 <- Children], + prim_comp![Line*2 <- Children], + prim_comp![Line*4 <- Edges], + ]) + .unwrap(); + assert_eq!(map.len_in(), 8); + assert_eq!(map.len_out(), 1); + assert_eq!(map.dim_in(), 0); + assert_eq!(map.delta_dim(), 1); + assert_map_apply!(map, 0, [[]], 0, [[0.25]]); + assert_map_apply!(map, 1, [[]], 0, [[0.00]]); + assert_map_apply!(map, 2, [[]], 0, [[0.50]]); + assert_map_apply!(map, 3, [[]], 0, [[0.25]]); + assert_map_apply!(map, 4, [[]], 0, [[0.75]]); + assert_map_apply!(map, 5, [[]], 0, [[0.50]]); + assert_map_apply!(map, 6, [[]], 0, [[1.00]]); + assert_map_apply!(map, 7, [[]], 0, [[0.75]]); + assert_eq!(map.apply_index(8), None); + assert_eq!( + map.unapply_indices(&[0]), + Some(vec![0, 1, 2, 3, 4, 5, 6, 7]) + ); + } + + #[test] + fn uniform_concat() { + let map = UniformConcat::new( + vec![ + prim_comp![Line*3 <- Take([0], 3)], + prim_comp![Line*3 <- Take([1], 3) <- Children], + prim_comp![Line*3 <- Take([2], 3) <- Children <- Children], + ], + 1, + 0, + 3, + ) + .unwrap(); + assert_eq!(map.len_out(), 3); + assert_eq!(map.len_in(), 7); + assert_eq!(map.dim_in(), 1); + assert_eq!(map.dim_out(), 1); + assert_map_apply!(map, 0, [[0.0], [1.0]], 0, [[0.00], [1.00]]); + assert_map_apply!(map, 1, [[0.0], [1.0]], 1, [[0.00], [0.50]]); + assert_map_apply!(map, 2, [[0.0], [1.0]], 1, [[0.50], [1.00]]); + assert_map_apply!(map, 3, [[0.0], [1.0]], 2, [[0.00], [0.25]]); + assert_map_apply!(map, 4, [[0.0], [1.0]], 2, [[0.25], [0.50]]); + assert_map_apply!(map, 5, [[0.0], [1.0]], 2, [[0.50], [0.75]]); + assert_map_apply!(map, 6, [[0.0], [1.0]], 2, [[0.75], [1.00]]); + assert_eq!(map.apply_index(7), None); + assert_eq!(map.unapply_indices(&[0, 2]), Some(vec![0, 3, 4, 5, 6])); + } + + #[test] + fn uniform_product1() { + let map = UniformProduct::new(vec![prim_comp![Line*2 <- Edges], prim_comp![Line * 3]]); + assert_eq!(map.len_out(), 6); + assert_eq!(map.len_in(), 12); + assert_eq!(map.dim_in(), 1); + assert_eq!(map.dim_out(), 2); + assert_map_apply!(map, 0, [[0.2], [0.3]], 0, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 1, [[0.2], [0.3]], 1, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 2, [[0.2], [0.3]], 2, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 3, [[0.2], [0.3]], 0, [[0.0, 0.2], [0.0, 0.3]]); + assert_map_apply!(map, 4, [[0.2], [0.3]], 1, [[0.0, 0.2], [0.0, 0.3]]); + assert_map_apply!(map, 5, [[0.2], [0.3]], 2, [[0.0, 0.2], [0.0, 0.3]]); + assert_map_apply!(map, 6, [[0.2], [0.3]], 3, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 7, [[0.2], [0.3]], 4, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 8, [[0.2], [0.3]], 5, [[1.0, 0.2], [1.0, 0.3]]); + assert_map_apply!(map, 9, [[0.2], [0.3]], 3, [[0.0, 0.2], [0.0, 0.3]]); + assert_map_apply!(map, 10, [[0.2], [0.3]], 4, [[0.0, 0.2], [0.0, 0.3]]); + assert_map_apply!(map, 11, [[0.2], [0.3]], 5, [[0.0, 0.2], [0.0, 0.3]]); + assert_eq!(map.apply_index(12), None); + assert_eq!(map.unapply_indices(&[0]), Some(vec![0, 3])); + assert_eq!(map.unapply_indices(&[1]), Some(vec![1, 4])); + assert_eq!(map.unapply_indices(&[2]), Some(vec![2, 5])); + assert_eq!(map.unapply_indices(&[3]), Some(vec![6, 9])); + assert_eq!(map.unapply_indices(&[4]), Some(vec![7, 10])); + assert_eq!(map.unapply_indices(&[5]), Some(vec![8, 11])); + } +} diff --git a/src/map/primitive.rs b/src/map/primitive.rs new file mode 100644 index 000000000..5406f6977 --- /dev/null +++ b/src/map/primitive.rs @@ -0,0 +1,1564 @@ +use super::{AddOffset, Error, Map, UnapplyIndicesData}; +use crate::finite_f64::FiniteF64; +use crate::simplex::Simplex; +use num::Integer as _; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +/// An interface for an unbounded coordinate and index map. +pub trait UnboundedMap: std::fmt::Debug { + /// Minimum dimension of the input coordinate. If the dimension of the input + /// coordinate of [`UnboundedMap::apply_inplace()`] is larger than the minimum, then + /// the map of the surplus is the identity map. + fn dim_in(&self) -> usize; + /// Minimum dimension of the output coordinate. + fn dim_out(&self) -> usize { + self.dim_in() + self.delta_dim() + } + /// Difference in dimension of the output and input coordinate. + fn delta_dim(&self) -> usize; + /// Modulus of the input index. The map repeats itself at index `mod_in` + /// and the output index is incremented with `in_index / mod_in * mod_out`. + fn mod_in(&self) -> usize; + /// Modulus if the output index. + fn mod_out(&self) -> usize; + fn apply_mod_out_to_in(&self, n: usize) -> Option { + let (i, rem) = n.div_rem(&self.mod_out()); + (rem == 0).then(|| i * self.mod_in()) + } + fn apply_mod_in_to_out(&self, n: usize) -> Option { + let (i, rem) = n.div_rem(&self.mod_in()); + (rem == 0).then(|| i * self.mod_out()) + } + /// Apply the given index and coordinate, the latter in-place. + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize; + /// Apply the index. + fn apply_index(&self, index: usize) -> usize; + /// Apply a sequence of indices in-place. + fn apply_indices_inplace(&self, indices: &mut [usize]) { + for index in indices.iter_mut() { + *index = self.apply_index(*index); + } + } + /// Unapply a sequence of indices. + fn unapply_indices(&self, indices: &[T]) -> Vec; + /// Returns true if this is the identity map. + fn is_identity(&self) -> bool { + self.mod_in() == 1 && self.mod_out() == 1 && self.dim_out() == 0 + } + /// Returns true if this map manipulates indices only. + fn is_index_map(&self) -> bool { + self.dim_out() == 0 + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize; + fn basis_is_constant(&self) -> bool; +} + +fn coords_iter_mut( + flat: &mut [f64], + stride: usize, + offset: usize, + dim_out: usize, + dim_in: usize, +) -> impl Iterator { + flat.chunks_mut(stride).map(move |coord| { + let coord = &mut coord[offset..]; + let delta = dim_out - dim_in; + if delta != 0 { + coord.copy_within(..coord.len() - delta, delta); + } + &mut coord[..dim_out] + }) +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Identity; + +impl UnboundedMap for Identity { + #[inline] + fn dim_in(&self) -> usize { + 0 + } + #[inline] + fn delta_dim(&self) -> usize { + 0 + } + #[inline] + fn mod_in(&self) -> usize { + 1 + } + #[inline] + fn mod_out(&self) -> usize { + 1 + } + #[inline] + fn apply_inplace( + &self, + index: usize, + _coords: &mut [f64], + _stride: usize, + _offset: usize, + ) -> usize { + index + } + #[inline] + fn apply_index(&self, index: usize) -> usize { + index + } + #[inline] + fn apply_indices_inplace(&self, _indices: &mut [usize]) {} + #[inline] + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices.to_vec() + } + #[inline] + fn is_identity(&self) -> bool { + true + } + #[inline] + fn update_basis(&self, index: usize, _basis: &mut [f64], _dim_out: usize, _dim_in: &mut usize, _offset: usize) -> usize { + index + } + #[inline] + fn basis_is_constant(&self) -> bool { + true + } +} + +impl AddOffset for Identity { + fn add_offset(&mut self, _offset: usize) {} +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Offset(pub M, pub usize); + +impl UnboundedMap for Offset { + #[inline] + fn dim_in(&self) -> usize { + if self.0.dim_out() > 0 { + self.0.dim_in() + self.1 + } else { + 0 + } + } + #[inline] + fn delta_dim(&self) -> usize { + self.0.delta_dim() + } + #[inline] + fn mod_in(&self) -> usize { + self.0.mod_in() + } + #[inline] + fn mod_out(&self) -> usize { + self.0.mod_out() + } + #[inline] + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.0.apply_inplace(index, coords, stride, offset + self.1) + } + #[inline] + fn apply_index(&self, index: usize) -> usize { + self.0.apply_index(index) + } + #[inline] + fn apply_indices_inplace(&self, indices: &mut [usize]) { + self.0.apply_indices_inplace(indices); + } + #[inline] + fn unapply_indices(&self, indices: &[T]) -> Vec { + self.0.unapply_indices(indices) + } + #[inline] + fn is_identity(&self) -> bool { + self.0.is_identity() + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.0.update_basis(index, basis, dim_out, dim_in, offset + self.1) + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.0.basis_is_constant() + } +} + +impl AddOffset for Offset { + fn add_offset(&mut self, offset: usize) { + self.1 += offset; + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Transpose(usize, usize); + +impl Transpose { + #[inline] + pub const fn new(len1: usize, len2: usize) -> Self { + Self(len1, len2) + } + #[inline] + pub fn reverse(&mut self) { + std::mem::swap(&mut self.0, &mut self.1); + } +} + +impl UnboundedMap for Transpose { + fn dim_in(&self) -> usize { + 0 + } + fn delta_dim(&self) -> usize { + 0 + } + fn mod_in(&self) -> usize { + if self.0 != 1 && self.1 != 1 { + self.0 * self.1 + } else { + 1 + } + } + fn mod_out(&self) -> usize { + self.mod_in() + } + fn apply_inplace( + &self, + index: usize, + _coords: &mut [f64], + _stride: usize, + _offset: usize, + ) -> usize { + self.apply_index(index) + } + fn apply_index(&self, index: usize) -> usize { + let (j, k) = index.div_rem(&self.1); + let (i, j) = j.div_rem(&self.0); + (i * self.1 + k) * self.0 + j + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .map(|index| { + let (j, k) = index.get().div_rem(&self.0); + let (i, j) = j.div_rem(&self.1); + index.set((i * self.0 + k) * self.1 + j) + }) + .collect() + } + #[inline] + fn update_basis(&self, index: usize, _basis: &mut [f64], _dim_out: usize, _dim_in: &mut usize, _offset: usize) -> usize { + self.apply_index(index) + } + #[inline] + fn basis_is_constant(&self) -> bool { + true + } +} + +impl AddOffset for Transpose { + fn add_offset(&mut self, _offset: usize) {} +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Take { + indices: Arc<[usize]>, + nindices: usize, + len: usize, +} + +impl Take { + pub fn new(indices: impl Into>, len: usize) -> Self { + // TODO: return err if indices.is_empty() + let indices = indices.into(); + assert!(!indices.is_empty()); + let nindices = indices.len(); + Take { + indices, + nindices, + len, + } + } + pub fn get_indices(&self) -> Arc<[usize]> { + self.indices.clone() + } +} + +impl UnboundedMap for Take { + fn dim_in(&self) -> usize { + 0 + } + fn delta_dim(&self) -> usize { + 0 + } + fn mod_in(&self) -> usize { + self.nindices + } + fn mod_out(&self) -> usize { + self.len + } + fn apply_inplace( + &self, + index: usize, + _coords: &mut [f64], + _stride: usize, + _offset: usize, + ) -> usize { + self.apply_index(index) + } + fn apply_index(&self, index: usize) -> usize { + self.indices[index % self.nindices] + index / self.nindices * self.len + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .filter_map(|index| { + let (j, iout) = index.get().div_rem(&self.len); + let offset = j * self.nindices; + self.indices + .iter() + .position(|i| *i == iout) + .map(|iin| index.set(offset + iin)) + }) + .collect() + } + #[inline] + fn update_basis(&self, index: usize, _basis: &mut [f64], _dim_out: usize, _dim_in: &mut usize, _offset: usize) -> usize { + self.apply_index(index) + } + #[inline] + fn basis_is_constant(&self) -> bool { + true + } +} + +impl AddOffset for Take { + fn add_offset(&mut self, _offset: usize) {} +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Slice { + start: usize, + len_in: usize, + len_out: usize, +} + +impl Slice { + pub fn new(start: usize, len_in: usize, len_out: usize) -> Self { + assert!(len_in > 0); + assert!(len_out >= start + len_in); + Slice { + start, + len_in, + len_out, + } + } +} + +impl UnboundedMap for Slice { + fn dim_in(&self) -> usize { + 0 + } + fn delta_dim(&self) -> usize { + 0 + } + fn mod_in(&self) -> usize { + self.len_in + } + fn mod_out(&self) -> usize { + self.len_out + } + fn apply_inplace( + &self, + index: usize, + _coords: &mut [f64], + _stride: usize, + _offset: usize, + ) -> usize { + self.apply_index(index) + } + fn apply_index(&self, index: usize) -> usize { + self.start + index % self.len_in + index / self.len_in * self.len_out + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .filter_map(|index| { + let (j, i) = index.get().div_rem(&self.len_out); + (self.start..self.start + self.len_in) + .contains(&i) + .then(|| index.set(i - self.start + j * self.len_in)) + }) + .collect() + } + #[inline] + fn is_identity(&self) -> bool { + self.start == 0 && self.len_in == self.len_out + } + #[inline] + fn update_basis(&self, index: usize, _basis: &mut [f64], _dim_out: usize, _dim_in: &mut usize, _offset: usize) -> usize { + self.apply_index(index) + } + #[inline] + fn basis_is_constant(&self) -> bool { + true + } +} + +impl AddOffset for Slice { + fn add_offset(&mut self, _offset: usize) {} +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Children(Simplex); + +impl Children { + pub fn new(simplex: Simplex) -> Self { + Self(simplex) + } +} + +impl UnboundedMap for Children { + fn dim_in(&self) -> usize { + self.0.dim() + } + fn delta_dim(&self) -> usize { + 0 + } + fn mod_in(&self) -> usize { + self.0.nchildren() + } + fn mod_out(&self) -> usize { + 1 + } + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.0.apply_child(index, coords, stride, offset) + } + fn apply_index(&self, index: usize) -> usize { + self.0.apply_child_index(index) + } + fn apply_indices_inplace(&self, indices: &mut [usize]) { + self.0.apply_child_indices_inplace(indices) + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .flat_map(|i| (0..self.mod_in()).map(move |j| i.set(i.get() * self.mod_in() + j))) + .collect() + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.0.update_child_basis(index, basis, dim_out, dim_in, offset) + } + #[inline] + fn basis_is_constant(&self) -> bool { + match self.0 { + Simplex::Line => true, + _ => false, + } + } +} + +impl From for Children { + fn from(simplex: Simplex) -> Children { + Children::new(simplex) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Edges(pub Simplex); + +impl Edges { + pub fn new(simplex: Simplex) -> Self { + Self(simplex) + } +} + +impl UnboundedMap for Edges { + fn dim_in(&self) -> usize { + self.0.edge_dim() + } + fn delta_dim(&self) -> usize { + 1 + } + fn mod_in(&self) -> usize { + self.0.nedges() + } + fn mod_out(&self) -> usize { + 1 + } + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.0.apply_edge(index, coords, stride, offset) + } + fn apply_index(&self, index: usize) -> usize { + self.0.apply_edge_index(index) + } + fn apply_indices_inplace(&self, indices: &mut [usize]) { + self.0.apply_edge_indices_inplace(indices) + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .flat_map(|i| (0..self.mod_in()).map(move |j| i.set(i.get() * self.mod_in() + j))) + .collect() + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.0.update_edge_basis(index, basis, dim_out, dim_in, offset) + } + #[inline] + fn basis_is_constant(&self) -> bool { + false + } +} + +impl From for Edges { + fn from(simplex: Simplex) -> Edges { + Edges::new(simplex) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UniformPoints { + points: Arc<[FiniteF64]>, + npoints: usize, + point_dim: usize, +} + +impl UniformPoints { + pub fn new(points: impl Into>, point_dim: usize) -> Self { + let points: Arc<[FiniteF64]> = unsafe { std::mem::transmute(points.into()) }; + assert_eq!(points.len() % point_dim, 0); + assert_ne!(point_dim, 0); + let npoints = points.len() / point_dim; + UniformPoints { + points, + npoints, + point_dim, + } + } +} + +impl UnboundedMap for UniformPoints { + fn dim_in(&self) -> usize { + 0 + } + fn delta_dim(&self) -> usize { + self.point_dim + } + fn mod_in(&self) -> usize { + self.npoints + } + fn mod_out(&self) -> usize { + 1 + } + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + let points: &[f64] = unsafe { std::mem::transmute(&self.points[..]) }; + let point = &points[(index % self.npoints) * self.point_dim..][..self.point_dim]; + for coord in coords_iter_mut(coords, stride, offset, self.point_dim, 0) { + coord.copy_from_slice(point); + } + index / self.npoints + } + fn apply_index(&self, index: usize) -> usize { + index / self.npoints + } + fn apply_indices_inplace(&self, indices: &mut [usize]) { + indices.iter_mut().for_each(|i| *i /= self.npoints) + } + fn unapply_indices(&self, indices: &[T]) -> Vec { + indices + .iter() + .flat_map(|i| (0..self.mod_in()).map(move |j| i.set(i.get() * self.mod_in() + j))) + .collect() + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + assert!(offset <= *dim_in); + assert!(*dim_in + self.point_dim < dim_out); + // Shift rows `offset..dim_in` `self.point_dim` rows down. + for i in (offset..*dim_in).into_iter().rev() { + for j in 0..*dim_in { + basis[(i + self.point_dim) * dim_out + j] = basis[i * dim_out + j]; + } + } + for i in offset..offset + self.point_dim { + for j in 0..*dim_in { + basis[i * self.point_dim + j] = 0.0; + } + } + // Append identity columns. + for i in 0..*dim_in + self.point_dim { + for j in 0..self.point_dim { + basis[i * dim_out + j] = 0.0; + } + } + for i in 0..self.point_dim { + basis[(i + offset) * dim_out + *dim_in + i] = 1.0; + } + *dim_in += self.point_dim; + index / self.npoints + } + #[inline] + fn basis_is_constant(&self) -> bool { + true + } +} + +/// An enum of primitive maps. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Primitive { + Transpose(Transpose), + Take(Take), + Slice(Slice), + Children(Offset), + Edges(Offset), + UniformPoints(Offset), +} + +impl Primitive { + #[inline] + pub fn new_transpose(len1: usize, len2: usize) -> Self { + Transpose::new(len1, len2).into() + } + #[inline] + pub fn new_take(indices: impl Into>, len: usize) -> Self { + Take::new(indices, len).into() + } + #[inline] + pub fn new_slice(start: usize, len_in: usize, len_out: usize) -> Self { + Slice::new(start, len_in, len_out).into() + } + #[inline] + pub fn new_children(simplex: Simplex) -> Self { + Children::new(simplex).into() + } + #[inline] + pub fn new_edges(simplex: Simplex) -> Self { + Edges::new(simplex).into() + } + #[inline] + pub fn new_uniform_points(points: impl Into>, point_dim: usize) -> Self { + UniformPoints::new(points, point_dim).into() + } + #[inline] + fn offset_mut(&mut self) -> Option<&mut usize> { + match self { + Self::Children(Offset(_, ref mut offset)) => Some(offset), + Self::Edges(Offset(_, ref mut offset)) => Some(offset), + Self::UniformPoints(Offset(_, ref mut offset)) => Some(offset), + _ => None, + } + } + pub fn with_offset(mut self, offset: usize) -> Self { + if let Some(self_offset) = self.offset_mut() { + *self_offset = offset + } + self + } + #[inline] + const fn is_transpose(&self) -> bool { + matches!(self, Self::Transpose(_)) + } + pub fn as_transpose(&self) -> Option<&Transpose> { + match self { + Self::Transpose(transpose) => Some(transpose), + _ => None, + } + } + pub fn as_transpose_mut(&mut self) -> Option<&mut Transpose> { + match self { + Self::Transpose(ref mut transpose) => Some(transpose), + _ => None, + } + } +} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + (@match $self:ident; $fn:ident; $($arg:ident),*) => { + match $self { + Self::Transpose(var) => var.$fn($($arg),*), + Self::Take(var) => var.$fn($($arg),*), + Self::Slice(var) => var.$fn($($arg),*), + Self::Children(var) => var.$fn($($arg),*), + Self::Edges(var) => var.$fn($($arg),*), + Self::UniformPoints(var) => var.$fn($($arg),*), + } + }; +} + +impl UnboundedMap for Primitive { + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn mod_in(&self) -> usize} + dispatch! {fn mod_out(&self) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coords: &mut[f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> usize} + dispatch! {fn apply_indices_inplace(&self, indices: &mut [usize])} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Vec} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} + dispatch! {fn basis_is_constant(&self) -> bool} +} + +impl AddOffset for Primitive { + dispatch! {fn add_offset(&mut self, offset: usize)} +} + +impl std::fmt::Debug for Primitive { + dispatch! {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result} +} + +impl From for Primitive { + fn from(transpose: Transpose) -> Self { + Self::Transpose(transpose) + } +} + +impl From for Primitive { + fn from(take: Take) -> Self { + Self::Take(take) + } +} + +impl From for Primitive { + fn from(slice: Slice) -> Self { + Self::Slice(slice) + } +} + +impl From for Primitive { + fn from(children: Children) -> Self { + Self::Children(Offset(children, 0)) + } +} + +impl From for Primitive { + fn from(edges: Edges) -> Self { + Self::Edges(Offset(edges, 0)) + } +} + +impl From for Primitive { + fn from(uniform_points: UniformPoints) -> Self { + Self::UniformPoints(Offset(uniform_points, 0)) + } +} + +#[inline] +fn comp_dim_out_in(map: &M, dim_out: usize, dim_in: usize) -> (usize, usize) { + let n = map.dim_in().checked_sub(dim_out).unwrap_or(0); + (dim_out + map.delta_dim() + n, dim_in + n) +} + +#[inline] +fn comp_mod_out_in(map: &M, mod_out: usize, mod_in: usize) -> (usize, usize) { + let n = mod_out.lcm(&map.mod_in()); + (n / map.mod_in() * map.mod_out(), mod_in * n / mod_out) +} + +impl UnboundedMap for Array +where + M: UnboundedMap, + Array: Deref + std::fmt::Debug, +{ + #[inline] + fn dim_in(&self) -> usize { + self.iter() + .rev() + .fold((0, 0), |(o, i), map| comp_dim_out_in(map, o, i)) + .1 + } + #[inline] + fn delta_dim(&self) -> usize { + self.iter().map(|map| map.delta_dim()).sum() + } + #[inline] + fn mod_in(&self) -> usize { + self.iter() + .rev() + .fold((1, 1), |(o, i), map| comp_mod_out_in(map, o, i)) + .1 + } + #[inline] + fn mod_out(&self) -> usize { + self.iter() + .rev() + .fold((1, 1), |(o, i), map| comp_mod_out_in(map, o, i)) + .0 + } + #[inline] + fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.iter().rev().fold(index, |index, map| { + map.apply_inplace(index, coords, stride, offset) + }) + } + #[inline] + fn apply_index(&self, index: usize) -> usize { + self.iter() + .rev() + .fold(index, |index, map| map.apply_index(index)) + } + #[inline] + fn apply_indices_inplace(&self, indices: &mut [usize]) { + self.iter() + .rev() + .for_each(|map| map.apply_indices_inplace(indices)); + } + #[inline] + fn unapply_indices(&self, indices: &[T]) -> Vec { + self.iter().fold(indices.to_vec(), |indices, map| { + map.unapply_indices(&indices) + }) + } + #[inline] + fn is_identity(&self) -> bool { + self.iter().all(|map| map.is_identity()) + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.iter().rev().fold(index, |index, map| map.update_basis(index, basis, dim_out, dim_in, offset)) + } + #[inline] + fn basis_is_constant(&self) -> bool { + if self.mod_in() == 1 { + true + } else { + self.iter().all(|map| map.basis_is_constant()) + } + } +} + +impl AddOffset for Array +where + M: UnboundedMap + AddOffset, + Array: Deref + DerefMut, +{ + fn add_offset(&mut self, offset: usize) { + self.iter_mut().for_each(|map| map.add_offset(offset)); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WithBounds { + map: M, + dim_in: usize, + delta_dim: usize, + len_in: usize, + len_out: usize, +} + +impl WithBounds { + pub fn from_input(map: M, dim_in: usize, len_in: usize) -> Result { + if dim_in < map.dim_in() { + Err(Error::DimensionMismatch) + } else if len_in % map.mod_in() != 0 { + Err(Error::LengthMismatch) + } else { + Ok(Self::new_unchecked(map, dim_in, len_in)) + } + } + pub fn from_output(map: M, dim_out: usize, len_out: usize) -> Result { + if dim_out < map.dim_out() { + Err(Error::DimensionMismatch) + } else if len_out % map.mod_out() != 0 { + Err(Error::LengthMismatch) + } else { + let dim_in = dim_out - map.delta_dim(); + let len_in = len_out / map.mod_out() * map.mod_in(); + Ok(Self::new_unchecked(map, dim_in, len_in)) + } + } + pub fn new_unchecked(map: M, dim_in: usize, len_in: usize) -> Self { + let delta_dim = map.delta_dim(); + let len_out = len_in / map.mod_in() * map.mod_out(); + Self { + map, + dim_in, + delta_dim, + len_in, + len_out, + } + } + pub fn unbounded(&self) -> &M { + &self.map + } +} + +impl Map for WithBounds { + #[inline] + fn dim_in(&self) -> usize { + self.dim_in + } + #[inline] + fn delta_dim(&self) -> usize { + self.delta_dim + } + #[inline] + fn len_in(&self) -> usize { + self.len_in + } + #[inline] + fn len_out(&self) -> usize { + self.len_out + } + #[inline] + fn apply_inplace_unchecked( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + self.map.apply_inplace(index, coords, stride, offset) + } + #[inline] + fn apply_index_unchecked(&self, index: usize) -> usize { + self.map.apply_index(index) + } + #[inline] + fn apply_indices_inplace_unchecked(&self, indices: &mut [usize]) { + self.map.apply_indices_inplace(indices) + } + #[inline] + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + self.map.unapply_indices(indices) + } + #[inline] + fn is_identity(&self) -> bool { + self.map.is_identity() + } + #[inline] + fn is_index_map(&self) -> bool { + self.map.is_index_map() + } + #[inline] + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + self.map.update_basis(index, basis, dim_out, dim_in, offset) + } + #[inline] + fn basis_is_constant(&self) -> bool { + self.map.basis_is_constant() + } +} + +impl AddOffset for WithBounds +where + M: UnboundedMap + AddOffset, +{ + fn add_offset(&mut self, offset: usize) { + self.dim_in += offset; + self.map.add_offset(offset); + } +} + +/// An interface for swapping a composition of `Self` with a [`Primitive`]. +pub trait SwapPrimitiveComposition { + type Output; + + /// Returns a [`Primitive`'] and a map such that the composition of those is equivalent to the composition of `self` with `inner`. + fn swap_primitive_composition( + &self, + inner: &Primitive, + stride: usize, + ) -> Option<((Primitive, usize), Self::Output)>; +} + +impl SwapPrimitiveComposition for [Primitive] { + type Output = Vec; + + fn swap_primitive_composition( + &self, + inner: &Primitive, + stride: usize, + ) -> Option<((Primitive, usize), Self::Output)> { + if self.is_empty() { + return Some(((inner.clone(), stride), Vec::new())); + } else if inner.is_transpose() { + return None; + } + let mut target = inner.clone(); + let mut shifted_items: Vec = Vec::new(); + let mut queue: Vec = Vec::new(); + let mut stride_out = stride; + let mut stride_in = stride; + for mut item in self.iter().rev().cloned() { + // Swap matching edges and children at the same offset. + if let Primitive::Edges(Offset(Edges(esimplex), eoffset)) = &item { + if let Primitive::Children(Offset(Children(ref mut csimplex), coffset)) = + &mut target + { + if eoffset == coffset && esimplex.edge_dim() == csimplex.dim() { + if stride_in != 1 && inner.mod_in() != 1 { + shifted_items.push(Primitive::new_transpose(stride_in, inner.mod_in())); + } + shifted_items.append(&mut queue); + if stride_out != 1 && inner.mod_in() != 1 { + shifted_items + .push(Primitive::new_transpose(inner.mod_in(), stride_out)); + } + shifted_items.push(Primitive::new_take( + esimplex.swap_edges_children_map(), + esimplex.nedges() * esimplex.nchildren(), + )); + shifted_items.push(Primitive::Edges(Offset(Edges(*esimplex), *eoffset))); + *csimplex = *esimplex; + stride_in = 1; + stride_out = 1; + continue; + } + } + } + // Update strides. + if inner.mod_in() == 1 && inner.mod_out() == 1 { + } else if inner.mod_out() == 1 { + let n = stride_out.gcd(&item.mod_in()); + stride_out = stride_out / n * item.mod_out(); + stride_in *= item.mod_in() / n; + } else if let Some(Transpose(ref mut m, ref mut n)) = item.as_transpose_mut() { + if stride_out % (*m * *n) == 0 { + } else if stride_out % *n == 0 && (*m * *n) % (stride_out * inner.mod_out()) == 0 { + stride_out /= *n; + *m = *m / inner.mod_out() * inner.mod_in(); + } else if *n % stride_out == 0 && *n % (stride_out * inner.mod_out()) == 0 { + stride_out *= *m; + *n = *n / inner.mod_out() * inner.mod_in(); + } else { + return None; + } + } else if stride_out % item.mod_in() == 0 { + stride_out = stride_out / item.mod_in() * item.mod_out(); + } else { + return None; + } + // Update offsets. + let item_delta_dim = item.delta_dim(); + let target_delta_dim = target.delta_dim(); + let item_dim_in = item.dim_in(); + let target_dim_out = target.dim_out(); + if let (Some(item_offset), Some(target_offset)) = + (item.offset_mut(), target.offset_mut()) + { + if item_dim_in <= *target_offset { + *target_offset += item_delta_dim; + } else if target_dim_out <= *item_offset { + *item_offset -= target_delta_dim; + } else { + return None; + } + } + if !item.is_identity() { + queue.push(item); + } + } + if stride_in != 1 && target.mod_in() != 1 { + shifted_items.push(Primitive::new_transpose(stride_in, target.mod_in())); + } + shifted_items.extend(queue); + if stride_out != 1 && target.mod_in() != 1 { + shifted_items.push(Primitive::new_transpose(target.mod_in(), stride_out)); + } + if target.mod_out() == 1 { + stride_out = 1; + } + shifted_items.reverse(); + Some(((target, stride_out), shifted_items)) + } +} + +impl SwapPrimitiveComposition for Vec { + type Output = Self; + + #[inline] + fn swap_primitive_composition( + &self, + inner: &Primitive, + stride: usize, + ) -> Option<((Primitive, usize), Self::Output)> { + (&self[..]).swap_primitive_composition(inner, stride) + } +} + +impl SwapPrimitiveComposition for WithBounds +where + M: UnboundedMap + SwapPrimitiveComposition, + M::Output: UnboundedMap, +{ + type Output = WithBounds; + + fn swap_primitive_composition( + &self, + inner: &Primitive, + stride: usize, + ) -> Option<((Primitive, usize), Self::Output)> { + self.unbounded() + .swap_primitive_composition(inner, stride) + .map(|(outer, slf)| { + let dim_in = self.dim_in() - inner.delta_dim(); + let len_in = self.len_in() / inner.mod_out() * inner.mod_in(); + (outer, WithBounds::new_unchecked(slf, dim_in, len_in)) + }) + } +} + +/// Return type of [`AllPrimitiveDecompositions::all_primitive_decompositions()`]. +pub type PrimitiveDecompositionIter<'a, T> = Box + 'a>; + +/// An interface for iterating over all possible decompositions into [`Primitive`] and `Self`. +pub trait AllPrimitiveDecompositions: Sized { + /// Return an iterator over all possible decompositions into `Self` and an [`Primitive`]. + /// + /// # Examples + /// + /// ``` + /// use nutils_test::simplex::Simplex::*; + /// use nutils_test::primitive::{Primitive, AllPrimitiveDecompositions as _}; + /// use nutils_test::prim_comp; + /// let map = prim_comp![Triangle*2 <- Edges <- Children]; + /// let mut iter = map.all_primitive_decompositions(); + /// assert_eq!( + /// iter.next(), + /// Some(((Primitive::new_edges(Triangle), 1), prim_comp![Line*6 <- Children]))); + /// assert_eq!( + /// iter.next(), + /// Some(( + /// (Primitive::new_children(Triangle), 1), + /// prim_comp![Triangle*8 <- Edges <- Take([3, 6, 1, 7, 2, 5], 12)], + /// )) + /// ); + /// assert_eq!(iter.next(), None); + /// ``` + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self>; + fn as_transposes(&self) -> Option> + where + Self: Map, + { + let mut transposes = Vec::new(); + if self.is_identity() { + return Some(Vec::new()); + } + let mut next = |m: &Self| { + if let Some(((Primitive::Transpose(transpose), stride), rhs)) = + m.all_primitive_decompositions().next() + { + let len = transpose.mod_out(); + if stride != 1 { + unimplemented! {} + transposes.push(Transpose::new(stride, len)); + } + transposes.push(transpose); + if stride != 1 { + unimplemented! {} + transposes.push(Transpose::new(len, stride)); + } + Some(rhs) + } else { + None + } + }; + let mut rhs = if let Some(rhs) = next(self) { + rhs + } else { + return None; + }; + while !rhs.is_identity() { + rhs = if let Some(rhs) = next(&rhs) { + rhs + } else { + return None; + }; + } + Some(transposes) + } +} + +impl AllPrimitiveDecompositions for Vec { + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { + let mut splits = Vec::new(); + for (i, item) in self.iter().enumerate() { + if let Some((outer, mut inner)) = (&self[..i]).swap_primitive_composition(item, 1) { + inner.extend(self[i + 1..].iter().cloned()); + splits.push((outer, inner)); + } + if let Primitive::Edges(Offset(Edges(Simplex::Line), offset)) = item { + let mut children = Primitive::new_children(Simplex::Line); + children.add_offset(*offset); + if let Some((outer, mut inner)) = + (&self[..i]).swap_primitive_composition(&children, 1) + { + inner.push(item.clone()); + inner.push(Primitive::new_take( + Simplex::Line.swap_edges_children_map(), + Simplex::Line.nedges() * Simplex::Line.nchildren(), + )); + inner.extend(self[i + 1..].iter().cloned()); + splits.push((outer, inner)); + } + } + } + Box::new(splits.into_iter()) + } +} + +impl AllPrimitiveDecompositions for WithBounds +where + M: UnboundedMap + AllPrimitiveDecompositions, +{ + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { + Box::new( + self.unbounded() + .all_primitive_decompositions() + .into_iter() + .map(|(outer, unbounded)| { + ( + outer, + WithBounds::new_unchecked(unbounded, self.dim_in(), self.len_in()), + ) + }), + ) + } +} + +/// Create a bounded composition of primitive maps. +/// +/// # Syntax +/// +/// The arguments of the macro are separated by `<-`, indicating the direction +/// of the map. The first argument is a simplex (`Triangle`, `Line`) or +/// `Point`, multiplied with the output length of the map. The remaining arguments +/// are primitive maps: `Children`, `Edges`, `Transpose(len1, len2)` or `Take(indices, len)`. +/// +/// # Examples +/// +/// ``` +/// use nutils_test::prim_comp; +/// use nutils_test::simplex::Simplex::*; +/// prim_comp![Line*2 <- Children <- Edges]; +/// ``` +#[macro_export] +macro_rules! prim_comp { + (Point*$len_out:literal $($tail:tt)*) => {{ + use $crate::map::primitive::{Primitive, WithBounds}; + #[allow(unused_mut)] + let mut comp: Vec = Vec::new(); + $crate::prim_comp!{@adv comp, Point; $($tail)*} + WithBounds::from_output(comp, 0, $len_out).unwrap() + }}; + ($simplex:tt*$len_out:literal $($tail:tt)*) => {{ + use $crate::map::primitive::{Primitive, WithBounds}; + #[allow(unused_mut)] + let mut comp: Vec = Vec::new(); + $crate::prim_comp!{@adv comp, $simplex; $($tail)*} + let dim_out = $crate::prim_comp!(@dim $simplex); + WithBounds::from_output(comp, dim_out, $len_out).unwrap() + }}; + (@dim Point) => {0}; + (@dim Line) => {1}; + (@dim Triangle) => {2}; + (@adv $comp:ident, $simplex:tt;) => {}; + (@adv $comp:ident, $simplex:tt; <- Children $($tail:tt)*) => {{ + $comp.push(Primitive::new_children($crate::simplex::Simplex::$simplex)); + $crate::prim_comp!{@adv $comp, $simplex; $($tail)*} + }}; + (@adv $comp:ident, Triangle; <- Edges $($tail:tt)*) => {{ + $comp.push(Primitive::new_edges($crate::simplex::Simplex::Triangle)); + $crate::prim_comp!{@adv $comp, Line; $($tail)*} + }}; + (@adv $comp:ident, Line; <- Edges $($tail:tt)*) => {{ + $comp.push(Primitive::new_edges($crate::simplex::Simplex::Line)); + $crate::prim_comp!{@adv $comp, Point; $($tail)*} + }}; + (@adv $comp:ident, $simplex:tt; <- Transpose($len1:expr, $len2:expr) $($tail:tt)*) => {{ + $comp.push(Primitive::new_transpose($len1, $len2)); + $crate::prim_comp!{@adv $comp, $simplex; $($tail)*} + }}; + (@adv $comp:ident, $simplex:tt; <- Take($indices:expr, $len:expr) $($tail:tt)*) => {{ + $comp.push(Primitive::new_take($indices.to_vec(), $len)); + $crate::prim_comp!{@adv $comp, $simplex; $($tail)*} + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_abs_diff_eq; + use std::iter; + use Simplex::*; + + macro_rules! assert_map_apply { + ($item:expr, $inidx:expr, $incoords:expr, $outidx:expr, $outcoords:expr) => {{ + use std::borrow::Borrow; + let item = $item.borrow(); + let incoords = $incoords; + let outcoords = $outcoords; + assert_eq!(incoords.len(), outcoords.len()); + let stride; + let mut work: Vec<_>; + if incoords.len() == 0 { + stride = item.dim_out(); + work = Vec::with_capacity(0); + } else { + stride = outcoords[0].len(); + work = iter::repeat(-1.0).take(outcoords.len() * stride).collect(); + for (work, incoord) in iter::zip(work.chunks_mut(stride), incoords.iter()) { + work[..incoord.len()].copy_from_slice(incoord); + } + } + assert_eq!(item.apply_inplace($inidx, &mut work, stride, 0), $outidx); + for (actual, desired) in iter::zip(work.chunks(stride), outcoords.iter()) { + assert_abs_diff_eq!(actual[..], desired[..]); + } + }}; + ($item:expr, $inidx:expr, $outidx:expr) => {{ + use std::borrow::Borrow; + let item = $item.borrow(); + let mut work = Vec::with_capacity(0); + assert_eq!( + item.apply_inplace($inidx, &mut work, item.dim_out(), 0), + $outidx + ); + }}; + } + + #[test] + fn apply_transpose() { + let item = Primitive::new_transpose(3, 2); + assert_map_apply!(item, 0, 0); + assert_map_apply!(item, 1, 3); + assert_map_apply!(item, 2, 1); + assert_map_apply!(item, 3, 4); + assert_map_apply!(item, 4, 2); + assert_map_apply!(item, 5, 5); + assert_map_apply!(item, 6, 6); + assert_map_apply!(item, 7, 9); + } + + #[test] + fn apply_take() { + let item = Primitive::new_take(vec![4, 1, 2], 5); + assert_map_apply!(item, 0, 4); + assert_map_apply!(item, 1, 1); + assert_map_apply!(item, 2, 2); + assert_map_apply!(item, 3, 9); + assert_map_apply!(item, 4, 6); + assert_map_apply!(item, 5, 7); + } + + #[test] + fn apply_children_line() { + let mut item = Primitive::new_children(Line); + assert_map_apply!(item, 0, [[0.0], [1.0]], 0, [[0.0], [0.5]]); + assert_map_apply!(item, 1, [[0.0], [1.0]], 0, [[0.5], [1.0]]); + assert_map_apply!(item, 2, [[0.0], [1.0]], 1, [[0.0], [0.5]]); + + item.add_offset(1); + assert_map_apply!( + item, + 3, + [[0.2, 0.0], [0.3, 1.0]], + 1, + [[0.2, 0.5], [0.3, 1.0]] + ); + } + + #[test] + fn apply_edges_line() { + let mut item = Primitive::new_edges(Line); + assert_map_apply!(item, 0, [[]], 0, [[1.0]]); + assert_map_apply!(item, 1, [[]], 0, [[0.0]]); + assert_map_apply!(item, 2, [[]], 1, [[1.0]]); + + item.add_offset(1); + assert_map_apply!(item, 0, [[0.2]], 0, [[0.2, 1.0]]); + } + + #[test] + fn apply_uniform_points() { + let mut item = Primitive::new_uniform_points(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2); + assert_map_apply!(item, 0, [[]], 0, [[1.0, 2.0]]); + assert_map_apply!(item, 1, [[]], 0, [[3.0, 4.0]]); + assert_map_apply!(item, 2, [[]], 0, [[5.0, 6.0]]); + assert_map_apply!(item, 3, [[]], 1, [[1.0, 2.0]]); + + item.add_offset(1); + assert_map_apply!(item, 0, [[7.0]], 0, [[7.0, 1.0, 2.0]]); + } + + macro_rules! assert_unapply { + ($item:expr) => {{ + let item = $item; + let nin = 2 * item.mod_in(); + let nout = 2 * item.mod_out(); + assert!(nout > 0); + let mut map: Vec> = (0..nout).map(|_| Vec::new()).collect(); + let mut work = Vec::with_capacity(0); + for i in 0..nin { + map[item.apply_inplace(i, &mut work, item.dim_out(), 0)].push(i); + } + for (j, desired) in map.into_iter().enumerate() { + let mut actual = item.unapply_indices(&[j]); + actual.sort(); + assert_eq!(actual, desired); + } + }}; + } + + #[test] + fn unapply_indices_transpose() { + assert_unapply!(Primitive::new_transpose(3, 2)); + } + + #[test] + fn unapply_indices_take() { + assert_unapply!(Primitive::new_take(vec![4, 1], 5)); + } + + #[test] + fn unapply_indices_children() { + assert_unapply!(Primitive::new_children(Triangle)); + } + + #[test] + fn unapply_indices_edges() { + assert_unapply!(Primitive::new_edges(Triangle)); + } + + #[test] + fn unapply_indices_uniform_points() { + assert_unapply!(Primitive::new_uniform_points( + vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0], + 2 + )); + } + + macro_rules! assert_equiv_maps { + ($a:expr, $b:expr $(, $simplex:ident)*) => {{ + let a: &[Primitive] = &$a; + let b: &[Primitive] = &$b; + let dim_in = 0 $(+ $simplex.dim())*; + let dim_out = a.dim_out(); + println!("a: {a:?}"); + println!("b: {b:?}"); + assert_eq!(a.mod_in(), b.mod_in()); + assert_eq!(a.mod_out(), b.mod_out()); + assert_eq!(a.dim_in(), dim_in); + assert_eq!(b.dim_in(), dim_in); + assert_eq!(a.delta_dim(), b.delta_dim()); + // Build coords: the outer product of the vertices of the given simplices, zero-padded + // to the out dimension. + let coords = iter::once([]); + $( + let coords = coords.flat_map(|coord| { + $simplex + .vertices() + .chunks($simplex.dim()) + .map(move |vert| [&coord, vert].concat()) + }); + )* + let pad: Vec = iter::repeat(0.0).take(a.delta_dim()).collect(); + let coords: Vec = coords.flat_map(|coord| [&coord[..], &pad].concat()).collect(); + // Test if every tip index and coordinate maps to the same out index and coordinate. + for iin in 0..2 * a.mod_in() { + let mut crds_a = coords.clone(); + let mut crds_b = coords.clone(); + let iout_a = a.apply_inplace(iin, &mut crds_a, dim_out, 0); + let iout_b = b.apply_inplace(iin, &mut crds_b, dim_out, 0); + assert_eq!(iout_a, iout_b, "iin={iin}"); + assert_abs_diff_eq!(crds_a[..], crds_b[..]); + } + }}; + } + + macro_rules! assert_swap_primitive_composition { + ($($item:expr),*; $($simplex:ident),*) => {{ + let unshifted = [$(Primitive::from($item),)*]; + let ((litem, lstride), lchain) = (&unshifted[..unshifted.len() - 1]).swap_primitive_composition(&unshifted.last().unwrap(), 1).unwrap(); + let mut shifted: Vec = Vec::new(); + if lstride != 1 { + shifted.push(Primitive::new_transpose(lstride, litem.mod_out())); + } + shifted.push(litem); + shifted.extend(lchain.into_iter()); + assert_equiv_maps!(&shifted[..], &unshifted[..] $(, $simplex)*); + }}; + } + + #[test] + fn swap_primitive_composition() { + assert_swap_primitive_composition!( + Transpose::new(4, 3), Primitive::new_take(vec![0, 1], 3); + ); + assert_swap_primitive_composition!( + Transpose::new(3, 5), Transpose::new(5, 4*3), Primitive::new_take(vec![0, 1], 3); + ); + assert_swap_primitive_composition!( + Transpose::new(5, 4), Transpose::new(5*4, 3), Primitive::new_take(vec![0, 1], 3); + ); + assert_swap_primitive_composition!( + {let mut elem = Primitive::new_children(Line); elem.add_offset(1); elem}, + Primitive::new_children(Line); + Line, Line + ); + assert_swap_primitive_composition!( + Primitive::new_edges(Line), Primitive::new_children(Line); + Line + ); + assert_swap_primitive_composition!( + Primitive::new_children(Line), + {let mut elem = Primitive::new_edges(Line); elem.add_offset(1); elem}; + Line + ); + assert_swap_primitive_composition!( + Primitive::new_edges(Triangle), Primitive::new_children(Line); + Line + ); + } +} diff --git a/src/map/relative.rs b/src/map/relative.rs new file mode 100644 index 000000000..1eb02210a --- /dev/null +++ b/src/map/relative.rs @@ -0,0 +1,607 @@ +use super::ops::{ + BinaryComposition, BinaryProduct, UniformComposition, UniformConcat, UniformProduct, +}; +use super::primitive::{ + AllPrimitiveDecompositions, Identity, Primitive, PrimitiveDecompositionIter, Slice, + SwapPrimitiveComposition, Transpose, UnboundedMap, WithBounds, +}; +use super::{AddOffset, Error, Map, UnapplyIndicesData}; +use crate::util::ReplaceNthIter as _; +use std::collections::BTreeMap; +use std::iter; +use std::sync::Arc; + +//impl AllPrimitiveDecompositions for UniformProduct> +//where +// M: Map + AllPrimitiveDecompositions + Clone, +//{ +// fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { +// Box::new( +// self.iter() +// .enumerate() +// .zip(self.offsets_out()) +// .zip(self.strides_out()) +// .flat_map(move |(((iprod, term), prod_offset), prod_stride)| { +// term.all_primitive_decompositions().into_iter().map( +// move |((mut prim, mut stride), inner)| { +// prim.add_offset(prod_offset); +// if prim.mod_out() == 1 { +// stride = 1; +// } else { +// stride *= prod_stride; +// } +// let product = self.iter().cloned().replace_nth(iprod, inner).collect(); +// ((prim, stride), product) +// }, +// ) +// }), +// ) +// } +//} + +//impl AllPrimitiveDecompositions for BinaryProduct +//where +// M0: Map + AllPrimitiveDecompositions + Clone, +// M1: Map + AllPrimitiveDecompositions + Clone, +//{ +// fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { +// let first = self.first().all_primitive_decompositions().into_iter().map( +// |((prim, mut stride), first)| { +// stride *= self.second().len_out(); +// let product = BinaryProduct::new(first, self.second().clone()); +// ((prim, stride), product) +// }, +// ); +// let second = self +// .second() +// .all_primitive_decompositions() +// .into_iter() +// .map(|((mut prim, stride), second)| { +// prim.add_offset(self.first().dim_out()); +// let product = BinaryProduct::new(self.first().clone(), second); +// ((prim, stride), product) +// }); +// Box::new(first.chain(second)) +// } +//} + +// TODO: UniformComposition? + +impl AllPrimitiveDecompositions for BinaryComposition +where + Outer: Map + AllPrimitiveDecompositions + SwapPrimitiveComposition, + Inner: Map + AllPrimitiveDecompositions + Clone, +{ + fn all_primitive_decompositions<'a>( + &'a self, + ) -> Box + 'a> { + let split_outer = self + .outer() + .all_primitive_decompositions() + .into_iter() + .map(|(prim, outer)| (prim, Self::new(outer, self.inner().clone()).unwrap())); + let split_inner = self + .inner() + .all_primitive_decompositions() + .into_iter() + .filter_map(|((prim, stride), inner)| { + self.outer() + .swap_primitive_composition(&prim, stride) + .map(|(prim, outer)| (prim, Self::new(outer, inner).unwrap())) + }); + Box::new(split_outer.chain(split_inner)) + } +} + +/// Decompose two maps into a common map and two remainders. +/// +/// The decomposition is such that the composition of the common part with the +/// remainder gives a map that is equivalent to the original. +/// +/// # Examples +/// +/// ``` +/// use nutils_test::prim_comp; +/// let map1 = prim_comp![Line*1 <- Children <- Children <- Edges]; +/// let map2 = prim_comp![Line*1 <- Children <- Edges]; +/// let (common, rem1, rem2) = nutils_test::relative::decompose_common(map1, map2); +/// assert_eq!(common, prim_comp![Line*1 <- Children <- Children <- Edges]); +/// assert_eq!(rem1, prim_comp![Point*8]); +/// assert_eq!(rem2, prim_comp![Point*8 <- Take([2, 1], 4)]); +/// ``` +/// +/// ``` +/// use nutils_test::prim_comp; +/// let map1 = prim_comp![Triangle*1 <- Children]; +/// let map2 = prim_comp![Triangle*1 <- Edges <- Children]; +/// let (common, rem1, rem2) = nutils_test::relative::decompose_common(map1, map2); +/// assert_eq!(common, prim_comp![Triangle*1 <- Children]); +/// assert_eq!(rem1, prim_comp![Triangle*4]); +/// assert_eq!(rem2, prim_comp![Triangle*4 <- Edges <- Take([3, 6, 1, 7, 2, 5], 12)]); +/// ``` +pub fn decompose_common(mut map1: M1, mut map2: M2) -> (WithBounds>, M1, M2) +where + M1: Map + AllPrimitiveDecompositions, + M2: Map + AllPrimitiveDecompositions, +{ + // TODO: check output dimensions? and return error if dimensions don't match? + assert_eq!(map1.len_out(), map2.len_out()); + assert_eq!(map1.dim_out(), map2.dim_out()); + let mut common = Vec::new(); + while !map1.is_identity() && !map2.is_identity() { + let mut outers2: BTreeMap<_, _> = map2.all_primitive_decompositions().collect(); + (map1, map2) = if let Some(((outer, stride), map1, map2)) = map1 + .all_primitive_decompositions() + .filter_map(|(key, t1)| outers2.remove(&key).map(|t2| (key, t1, t2))) + .next() + { + if stride != 1 && outer.mod_out() != 1 { + common.push(Primitive::new_transpose(stride, outer.mod_out())); + } + common.push(outer); + (map1, map2) + } else { + break; + }; + assert_eq!(map1.len_out(), map2.len_out()); + assert_eq!(map1.dim_out(), map2.dim_out()); + } + let common = WithBounds::from_input(common, map1.dim_out(), map1.len_out()).unwrap(); + (common, map1, map2) +} + +fn partial_relative_to(source: Source, target: Target) -> PartialRelative +where + Source: Map + AllPrimitiveDecompositions, + Target: Map + AllPrimitiveDecompositions, +{ + let (_, rem, rel) = decompose_common(target, source); + if rem.is_identity() { + PartialRelative::All(rel, None) + } else if let Some(mut transposes) = rem.as_transposes() { + transposes.reverse(); + transposes + .iter_mut() + .for_each(|transpose| transpose.reverse()); + let transposes = WithBounds::new_unchecked(transposes, rem.dim_in(), rem.len_in()); + PartialRelative::All(rel, Some(transposes)) + } else if rem.is_index_map() { + let mut indices: Vec = (0..rem.len_in()).collect(); + rem.apply_indices_inplace_unchecked(&mut indices); + PartialRelative::Some(rel, indices) + } else { + PartialRelative::CannotEstablishRelation + } +} + +enum PartialRelative { + All(M, Option>>), + Some(M, Vec), + CannotEstablishRelation, +} + +/// An interface for determining the relation between two maps. +pub trait RelativeTo: Map + Sized { + type Output: Map; + + /// Return the relative map from `self` to the given target. + fn relative_to(&self, target: &Target) -> Option; + /// Map indices for the target to `self`. + fn unapply_indices_from(&self, target: &Target, indices: &[T]) -> Option> + where + T: UnapplyIndicesData, + { + self.relative_to(target) + .and_then(|rel| rel.unapply_indices(indices)) + } +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Relative { + Identity(WithBounds), + Slice(WithBounds), + Map(M), + TransposedMap(BinaryComposition>, M>), + Composition(UniformComposition), + RelativeToConcat(RelativeToConcat), +} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + (@match $self:ident; $fn:ident; $($arg:ident),*) => { + match $self { + Self::Identity(var) => var.$fn($($arg),*), + Self::Slice(var) => var.$fn($($arg),*), + Self::Map(var) => var.$fn($($arg),*), + Self::TransposedMap(var) => var.$fn($($arg),*), + Self::Composition(var) => var.$fn($($arg),*), + Self::RelativeToConcat(var) => var.$fn($($arg),*), + } + } +} + +impl Map for Relative { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} + dispatch! {fn basis_is_constant(&self) -> bool} +} + +//impl AddOffset for Relative { +// dispatch! {fn add_offset(&mut self, offset: usize)} +//} + +impl RelativeTo for WithBounds> { + type Output = Relative; + + fn relative_to(&self, target: &Self) -> Option { + let (_, rem, rel) = decompose_common(target.clone(), self.clone()); + if rem.is_identity() { + Some(Relative::Map(rel)) + } else if let Some(mut transposes) = rem.as_transposes() { + transposes.reverse(); + transposes + .iter_mut() + .for_each(|transpose| transpose.reverse()); + let transposes = WithBounds::new_unchecked(transposes, rem.dim_in(), rem.len_in()); + Some(Relative::TransposedMap(BinaryComposition::new_unchecked( + transposes, rel, + ))) + } else { + None + } + } +} + +impl RelativeTo for UniformConcat +where + Source: Map + RelativeTo, + Target: Map, +{ + type Output = UniformConcat; + + fn relative_to(&self, target: &Target) -> Option { + self.iter() + .map(|item| item.relative_to(target)) + .collect::>() + .map(|rels| { + UniformConcat::new_unchecked( + rels, + self.dim_in(), + target.dim_in() - self.dim_in(), + target.len_in(), + ) + }) + } +} + +fn pop_common(vecs: &mut [&mut Vec]) -> Option { + let item = vecs.first().and_then(|vec| vec.last()); + if item.is_some() && vecs[1..].iter().all(|vec| vec.last() == item) { + for vec in vecs[1..].iter_mut() { + vec.pop(); + } + vecs[0].pop() + } else { + None + } +} + +#[derive(Debug, Clone)] +struct IndexOutIn(usize, usize); + +impl UnapplyIndicesData for IndexOutIn { + #[inline] + fn get(&self) -> usize { + self.1 + } + #[inline] + fn set(&self, index: usize) -> Self { + Self(self.0, index) + } +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RelativeToConcat { + rels: Vec, + index_map: Arc>, + //common: Vec, + len_out: usize, + len_in: usize, + dim_in: usize, + delta_dim: usize, +} + +impl Map for RelativeToConcat { + fn dim_in(&self) -> usize { + self.dim_in + } + fn delta_dim(&self) -> usize { + self.delta_dim + } + fn len_in(&self) -> usize { + self.len_in + } + fn len_out(&self) -> usize { + self.len_out + } + fn apply_inplace_unchecked( + &self, + index: usize, + coordinates: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + //let index = self + // .common + // .apply_inplace(index, coordinates, stride, offset); + let (iout, iin) = self.index_map[index]; + let n = self.index_map.len(); + self.rels[iin / n].apply_inplace_unchecked(iin % n, coordinates, stride, offset); + iout + } + fn apply_index_unchecked(&self, index: usize) -> usize { + //self.index_map[self.common.apply_index(index)].0 + self.index_map[index].0 + } + fn apply_indices_inplace_unchecked(&self, indices: &mut [usize]) { + //self.common.apply_indices_inplace(indices); + for index in indices.iter_mut() { + *index = self.index_map[*index].0; + } + } + fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec { + // FIXME: VERY EXPENSIVE!!! + let mut in_indices: Vec = Vec::new(); + for index in indices { + in_indices.extend( + self.index_map + .iter() + .enumerate() + .filter_map(|(iin, (iout, _))| (*iout == index.get()).then(|| index.set(iin))), + ); + } + //self.common.unapply_indices(&in_indices) + in_indices + } + fn is_identity(&self) -> bool { + false + } + fn is_index_map(&self) -> bool { + false // TODO + } + fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize { + let (iout, iin) = self.index_map[index]; + let n = self.index_map.len(); + self.rels[iin / n].update_basis(iin % n, basis, dim_out, dim_in, offset); + iout + } + fn basis_is_constant(&self) -> bool { + if self.len_in == 1 { + true + } else { + self.rels.iter().all(|map| map.basis_is_constant()) + } + } +} + +//impl AddOffset for RelativeToConcat { +// fn add_offset(&mut self, offset: usize) { +// self.common.add_offset(offset); +// for rel in self.rels.iter_mut() { +// rel.add_offset(offset); +// } +// self.dim_in += offset; +// } +//} + +impl RelativeTo> for Source +where + Source: Map + AllPrimitiveDecompositions + Clone, + Target: Map + AllPrimitiveDecompositions + Clone, +{ + type Output = Relative; + + fn relative_to(&self, targets: &UniformConcat) -> Option> { + let mut rels_indices = Vec::new(); + let mut offset = 0; + for target in targets.iter().cloned() { + let new_offset = offset + target.len_in(); + match partial_relative_to(self.clone(), target) { + PartialRelative::All(rel, transposes) => { + let slice = Slice::new(offset, rel.len_out(), targets.len_in()); + let slice = + WithBounds::from_input(slice, rel.dim_out(), rel.len_out()).unwrap(); + let slice = Relative::Slice(slice); + let rel = if let Some(transposes) = transposes { + Relative::TransposedMap(BinaryComposition::new_unchecked(transposes, rel)) + } else { + Relative::Map(rel) + }; + return Some(if slice.is_identity() { + rel + } else { + Relative::Composition(UniformComposition::new_unchecked( + vec![slice, rel], + )) + }); + } + PartialRelative::Some(rel, indices) => { + rels_indices.push((rel, offset, indices)); + } + PartialRelative::CannotEstablishRelation => { + //return None; + } + } + offset = new_offset; + } + // Split off common tail. TODO: Only shape increasing items, not take, slice (and transpose?). + let common_len_out = self.len_in(); + //let common = Vec::new(); + //let mut common_len_out = self.len_in(); + //let mut common = Vec::new(); + //{ + // let mut rels: Vec<_> = rels_indices.iter_mut().map(|(rel, _, _)| rel).collect(); + // while let Some(item) = pop_common(&mut rels[..]) { + // common_len_out = common_len_out / item.mod_in() * item.mod_out(); + // common.push(item); + // } + //} + // Build index map. + let mut index_map: Vec> = + iter::repeat(None).take(common_len_out).collect(); + let mut rels = Vec::new(); + for (rel, offset, out_indices) in rels_indices.into_iter() { + let rel_indices: Vec<_> = (offset..offset + out_indices.len()) + .zip(out_indices) + .map(|(i, j)| IndexOutIn(i, j)) + .collect(); + let unapplied = rel.unapply_indices_unchecked(&rel_indices); + if !unapplied.is_empty() { + let rel_offset = rels.len() * common_len_out; + for IndexOutIn(iout, iin) in unapplied { + assert!( + index_map[iin].is_none(), + "target contains duplicate entries" + ); + index_map[iin] = Some((iout, iin + rel_offset)); + } + rels.push(rel); + } + } + index_map + .into_iter() + .collect::>>() + .map(|index_map| { + Relative::RelativeToConcat(RelativeToConcat { + index_map: index_map.into(), + rels, + //common, + delta_dim: targets.dim_in() - self.dim_in(), + dim_in: self.dim_in(), + len_out: targets.len_in(), + len_in: self.len_in(), + }) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prim_comp; + use crate::simplex::Simplex::*; + use approx::assert_abs_diff_eq; + use std::iter; + + macro_rules! assert_equiv_maps { + ($a:expr, $b:expr $(, $simplex:ident)*) => {{ + let a = $a; + let b = $b; + println!("a: {a:?}"); + println!("b: {b:?}"); + // Build coords: the outer product of the vertices of the given simplices, zero-padded + // to the dimension of the root. + let coords = iter::once([]); + let simplex_dim = 0; + $( + let coords = coords.flat_map(|coord| { + $simplex + .vertices() + .chunks($simplex.dim()) + .map(move |vert| [&coord, vert].concat()) + }); + let simplex_dim = simplex_dim + $simplex.dim(); + )* + assert_eq!(simplex_dim, a.dim_in(), "the given simplices don't add up to the input dimension"); + let pad: Vec = iter::repeat(0.0).take(a.delta_dim()).collect(); + let coords: Vec = coords.flat_map(|coord| [&coord[..], &pad].concat()).collect(); + // Test if every input maps to the same output for both `a` and `b`. + for i in 0..2 * a.len_in() { + let mut crds_a = coords.clone(); + let mut crds_b = coords.clone(); + let ja = a.apply_inplace(i, &mut crds_a, a.dim_out(), 0); + let jb = b.apply_inplace(i, &mut crds_b, a.dim_out(), 0); + assert_eq!(ja, jb, "i={i}"); + assert_abs_diff_eq!(crds_a[..], crds_b[..]); + } + }}; + } + + #[test] + fn decompose_common_vec() { + let map1 = prim_comp![Line*2 <- Children <- Children]; + let map2 = prim_comp![Line*2 <- Children <- Take([0, 2], 4)]; + assert_eq!( + decompose_common(map1, map2), + ( + prim_comp![Line*2 <- Children], + prim_comp![Line*4 <- Children], + prim_comp![Line*4 <- Take([0, 2], 4)], + ) + ); + } + + #[test] + fn decompose_common_product() { + let map1 = prim_comp![Line*2 <- Children <- Children]; + let map2 = prim_comp![Line*2 <- Children <- Take([0, 2], 4)]; + let (common, rel1, rel2) = decompose_common(map1.clone(), map2.clone()); + assert!(!common.is_identity()); + assert_equiv_maps!( + BinaryComposition::new(common.clone(), rel1.clone()).unwrap(), + map1.clone(), + Line + ); + assert_equiv_maps!( + BinaryComposition::new(common.clone(), rel2.clone()).unwrap(), + map2.clone(), + Line + ); + //assert_eq!(rel1, BinaryProduct::new(prim_comp![Line*4 <- Children], prim_comp![Line*4 <- Take([0, 2], 4)])); + //assert_eq!(rel2, BinaryProduct::new(prim_comp![Line*4 <- Take([0, 2], 4)], prim_comp![Line*4 <- Children])); + //assert_eq!(common, prim_comp![Line*2]); + // WithBounds { map: [Offset(Children(Line), 1), Transpose(2, 1), Offset(Children(Line), 0)], dim_in: 2, delta_dim: 0, len_in: 16, len_out: 4 } + } + + // #[test] + // fn rel_to_single() { + // let a1 = prim_comp![Line*2 <- Children <- Take([0, 2], 4)]; + // let a2 = prim_comp![Line*2 <- Children <- Take([1, 3], 4) <- Children]; + // let a = BinaryConcat::new(a1, a2).unwrap(); + // let b = prim_comp![Line*2 <- Children <- Children <- Children]; + // assert_equiv_maps!( + // BinaryComposition::new(b.relative_to(&a).unwrap(), a.clone()).unwrap(), + // b, + // Line + // ); + // } +} diff --git a/src/map/tesselation.rs b/src/map/tesselation.rs new file mode 100644 index 000000000..c7803ce10 --- /dev/null +++ b/src/map/tesselation.rs @@ -0,0 +1,456 @@ +use super::ops::UniformConcat; +use super::primitive::{ + AllPrimitiveDecompositions, Primitive, PrimitiveDecompositionIter, WithBounds, +}; +use super::relative::RelativeTo; +use super::{AddOffset, Error, Map, UnapplyIndicesData}; +use crate::simplex::Simplex; +use crate::util::{ReplaceNthIter, SkipNthIter}; +use std::iter; +use std::ops::Mul; + +#[derive(Debug, Clone, PartialEq)] +pub struct UniformTesselation { + shapes: Vec, + map: WithBounds>, +} + +impl UniformTesselation { + pub fn identity(shapes: Vec, len: usize) -> Self { + let dim = shapes.iter().map(|simplex| simplex.dim()).sum(); + let map = WithBounds::new_unchecked(vec![], dim, len); + Self { shapes, map } + } + fn extend(&self, primitives: Primitives, shapes: Vec, len: usize) -> Self + where + Primitives: IntoIterator, + { + let map = self + .map + .unbounded() + .iter() + .cloned() + .chain(primitives) + .collect(); + let dim = shapes.iter().map(|shape| shape.dim()).sum(); + let map = WithBounds::new_unchecked(map, dim, len); + Self { shapes, map } + } + fn offsets(&self) -> impl Iterator + '_ { + self.shapes.iter().scan(0, |offset, shape| { + let item = *offset; + *offset += shape.dim(); + Some(item) + }) + } + pub fn take(&self, indices: &[usize]) -> Self { + assert!(indices.windows(2).all(|pair| pair[0] < pair[1])); + if let Some(last) = indices.last() { + assert!(*last < self.len_in()); + } + if indices.is_empty() { + // TODO: Disallow! Skip this map in the concatenation instead. + Self { + shapes: self.shapes.clone(), + map: WithBounds::new_unchecked(Vec::new(), self.dim_in(), 0), + } + } else { + let primitive = Primitive::new_take(indices, self.len_in()); + let result = self.extend([primitive], self.shapes.clone(), indices.len()); + result + } + } + pub fn children(&self) -> Self { + let primitives = self + .shapes + .iter() + .zip(self.offsets()) + .map(|(shape, offset)| Primitive::new_children(*shape).with_offset(offset)); + let nchildren: usize = self.shapes.iter().map(|shape| shape.nchildren()).product(); + self.extend(primitives, self.shapes.clone(), self.len_in() * nchildren) + } + pub fn edges(&self, ishape: usize) -> Option { + self.shapes.get(ishape).map(|shape| { + let offset = self + .shapes + .iter() + .take(ishape) + .map(|shape| shape.dim()) + .sum(); + let primitive = Primitive::new_edges(*shape).with_offset(offset); + let shapes: Vec<_> = if let Some(edge_shape) = shape.edge_simplex() { + self.shapes + .iter() + .cloned() + .replace_nth(ishape, edge_shape) + .collect() + } else { + self.shapes.iter().cloned().skip_nth(ishape).collect() + }; + self.extend([primitive], shapes, self.len_in() * shape.nedges()) + }) + } + pub fn edges_iter(&self) -> Option + '_> { + if self.shapes.is_empty() { + None + } else { + Some((0..self.shapes.len()).map(|ishape| self.edges(ishape).unwrap())) + } + } + pub fn centroids(&self) -> Self { + let primitives = self + .shapes + .iter() + .map(|shape| Primitive::new_uniform_points(shape.centroid(), shape.dim())); + self.extend(primitives, Vec::new(), self.len_in()) + } + pub fn vertices(&self) -> Self { + let primitives = self + .shapes + .iter() + .map(|shape| Primitive::new_uniform_points(shape.vertices(), shape.dim())); + self.extend(primitives, Vec::new(), self.len_in()) + } +} + +//impl Deref for UniformTesselation { +// type Target = WithBounds>; +// +// fn deref(&self) -> Self::Target { +// self.map +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.map.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.map.$fn($($arg),*) + } + }; +} + +impl Map for UniformTesselation { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} +} + +impl AllPrimitiveDecompositions for UniformTesselation { + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { + Box::new(self.map.all_primitive_decompositions().map(|(prim, map)| { + ( + prim, + Self { + shapes: self.shapes.clone(), + map: map, + }, + ) + })) + } +} + +impl RelativeTo for UniformTesselation { + type Output = > as RelativeTo>>>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.map.relative_to(&target.map) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Tesselation(UniformConcat); + +impl Tesselation { + pub fn identity(shapes: Vec, len: usize) -> Self { + let identity = UniformTesselation::identity(shapes, len); + let dim_in = identity.dim_in(); + let delta_dim = identity.delta_dim(); + let len_out = identity.len_out(); + Self(UniformConcat::new_unchecked( + vec![identity], + dim_in, + delta_dim, + len_out, + )) + } + pub fn len(&self) -> usize { + self.0.len_in() + } + pub fn dim(&self) -> usize { + self.0.dim_in() + } + pub fn product(&self, other: &Self) -> Self { + self * other + } + pub fn concat(&self, other: &Self) -> Result { + let maps = self.0.iter().chain(other.0.iter()).cloned().collect(); + Ok(Self(UniformConcat::new( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + )?)) + } + pub fn take(&self, indices: &[usize]) -> Result { + if !indices.windows(2).all(|pair| pair[0] < pair[1]) { + return Err(Error::IndicesNotStrictIncreasing); + } + if let Some(last) = indices.last() { + if *last >= self.len() { + return Err(Error::IndexOutOfRange); + } + } + let mut maps = Vec::new(); + let mut offset = 0; + let mut start = 0; + for map in self.0.iter() { + let stop = start + indices[start..].partition_point(|i| *i < offset + map.len_in()); + if stop > start { + let map_indices: Vec<_> = + indices[start..stop].iter().map(|i| *i - offset).collect(); + start = stop; + maps.push(map.take(&map_indices)); + } + offset += map.len_in(); + } + assert_eq!(start, indices.len()); + Ok(Self(UniformConcat::new_unchecked( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + ))) + } + pub fn children(&self) -> Self { + let maps = self.0.iter().map(|item| item.children()).collect(); + Self(UniformConcat::new_unchecked( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + )) + } + pub fn edges(&self) -> Result { + if self.dim_in() == 0 { + return Err(Error::DimensionZeroHasNoEdges); + } + let items: Vec<_> = self + .0 + .iter() + .flat_map(|item| item.edges_iter().unwrap()) + .collect(); + Ok(Self(UniformConcat::new( + items, + self.dim_in() - 1, + self.delta_dim() + 1, + self.len_out(), + )?)) + } + pub fn centroids(&self) -> Self { + Self(UniformConcat::new_unchecked( + self.0.iter().map(|item| item.centroids()).collect(), + 0, + self.delta_dim() + self.dim_in(), + self.len_out(), + )) + } + pub fn vertices(&self) -> Self { + Self(UniformConcat::new_unchecked( + self.0.iter().map(|item| item.vertices()).collect(), + 0, + self.delta_dim() + self.dim_in(), + self.len_out(), + )) + } + pub fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + ) -> Result { + self.0.apply_inplace(index, coords, stride, 0) + } + pub fn apply_index(&self, index: usize) -> Option { + self.0.apply_index(index) + } + pub fn unapply_indices(&self, indices: &[T]) -> Option> { + self.0.unapply_indices(indices) + } +} + +//impl Deref for Tesselation { +// type Target = OptionReorder>, Vec>; +// +// fn deref(&self) -> Self::Target { +// self.0 +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; +} + +impl Map for Tesselation { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} +} + +impl RelativeTo for Tesselation { + type Output = as RelativeTo< + UniformConcat, + >>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.0.relative_to(&target.0) + } +} + +impl Mul for &UniformTesselation { + type Output = UniformTesselation; + + fn mul(self, rhs: Self) -> UniformTesselation { + let offset = self.map.dim_in(); + let map = iter::once(Primitive::new_transpose( + rhs.map.len_out(), + self.map.len_out(), + )) + .chain(self.map.unbounded().iter().cloned()) + .chain(iter::once(Primitive::new_transpose( + self.map.len_in(), + rhs.map.len_out(), + ))) + .chain(rhs.map.unbounded().iter().map(|item| { + let mut item = item.clone(); + item.add_offset(offset); + item + })) + .collect(); + let map = WithBounds::new_unchecked( + map, + self.map.dim_in() + rhs.map.dim_in(), + self.map.len_in() * rhs.map.len_in(), + ); + let shapes = self + .shapes + .iter() + .chain(rhs.shapes.iter()) + .cloned() + .collect(); + UniformTesselation { shapes, map } + } +} + +impl Mul for &Tesselation { + type Output = Tesselation; + + fn mul(self, rhs: Self) -> Tesselation { + let products = self + .0 + .iter() + .flat_map(|lhs| rhs.0.iter().map(move |rhs| lhs * rhs)) + .collect(); + Tesselation(UniformConcat::new_unchecked( + products, + self.dim_in() + rhs.dim_in(), + self.delta_dim() + rhs.delta_dim(), + self.len_out() * rhs.len_out(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::simplex::Simplex::*; + use approx::assert_abs_diff_eq; + + #[test] + fn children() { + let tess = Tesselation::identity(vec![Line], 1).children(); + assert_eq!(tess.len(), 2); + let tess = tess.children(); + assert_eq!(tess.len(), 4); + } + + #[test] + fn product() { + let lhs = Tesselation::identity(vec![Line], 1).children(); + let rhs = Tesselation::identity(vec![Line], 1).edges().unwrap(); + let tess = &lhs * &rhs; + let centroids = tess.centroids(); + let stride = 2; + let mut work: Vec<_> = iter::repeat(-1.0).take(stride).collect(); + println!("tess: {tess:?}"); + assert_eq!(centroids.apply_inplace(0, &mut work, stride), Ok(0)); + assert_abs_diff_eq!(work[..], [0.25, 1.0]); + assert_eq!(centroids.apply_inplace(1, &mut work, stride), Ok(0)); + assert_abs_diff_eq!(work[..], [0.25, 0.0]); + assert_eq!(centroids.apply_inplace(2, &mut work, stride), Ok(0)); + assert_abs_diff_eq!(work[..], [0.75, 1.0]); + assert_eq!(centroids.apply_inplace(3, &mut work, stride), Ok(0)); + assert_abs_diff_eq!(work[..], [0.75, 0.0]); + } + + #[test] + fn take() { + let lhs = Tesselation::identity(vec![Line], 1).children(); + let rhs = Tesselation::identity(vec![Line], 1).edges().unwrap(); + let levels: Vec<_> = iter::successors(Some(&lhs * &rhs), |level| Some(level.children())) + .take(3) + .collect(); + let hierarch = levels[1].take(&[0, 1, 2]); + } +} diff --git a/src/map/transforms.rs b/src/map/transforms.rs new file mode 100644 index 000000000..1b74fc61b --- /dev/null +++ b/src/map/transforms.rs @@ -0,0 +1,318 @@ +use super::ops::UniformConcat; +use super::primitive::{ + AllPrimitiveDecompositions, Primitive, PrimitiveDecompositionIter, UnboundedMap, WithBounds, +}; +use super::relative::RelativeTo; +use super::{AddOffset, Error, Map, UnapplyIndicesData}; +use crate::simplex::Simplex; +use crate::util::{ReplaceNthIter, SkipNthIter}; +use std::iter; +use std::ops::Mul; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq)] +pub struct UniformTransforms(WithBounds>); + +impl UniformTransforms { + pub fn identity(dim: usize, len: usize) -> Self { + Self(WithBounds::new_unchecked(Vec::new(), dim, len)) + } + pub fn clone_and_push(&self, primitive: Primitive) -> Result { + if self.0.dim_in() < primitive.dim_out() { + return Err(Error::DimensionMismatch); + } + if self.0.len_in() % primitive.mod_out() != 0 { + return Err(Error::LengthMismatch); + } + let dim_in = self.0.dim_in() - primitive.delta_dim(); + let len_in = self.0.len_in() / primitive.mod_out() * primitive.mod_in(); + let map = self + .0 + .unbounded() + .iter() + .cloned() + .chain(iter::once(primitive)) + .collect(); + Ok(Self(WithBounds::new_unchecked(map, dim_in, len_in))) + } + fn mul(&self, rhs: &Self) -> Self { + let offset = self.0.dim_in(); + let map = iter::once(Primitive::new_transpose(rhs.0.len_out(), self.0.len_out())) + .chain(self.0.unbounded().iter().cloned()) + .chain(iter::once(Primitive::new_transpose( + self.0.len_in(), + rhs.0.len_out(), + ))) + .chain(rhs.0.unbounded().iter().map(|item| { + let mut item = item.clone(); + item.add_offset(offset); + item + })) + .collect(); + Self(WithBounds::new_unchecked( + map, + self.0.dim_in() + rhs.0.dim_in(), + self.0.len_in() * rhs.0.len_in(), + )) + } +} + +//impl Deref for UniformTransforms { +// type Target = WithBounds>; +// +// fn deref(&self) -> Self::Target { +// self.map +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.map.$fn($($arg),*) + } + }; +} + +impl Map for UniformTransforms { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} +} + +impl AllPrimitiveDecompositions for UniformTransforms { + fn all_primitive_decompositions<'a>(&'a self) -> PrimitiveDecompositionIter<'a, Self> { + Box::new( + self.0 + .all_primitive_decompositions() + .map(|(prim, map)| (prim, Self(map))), + ) + } +} + +impl RelativeTo for UniformTransforms { + type Output = > as RelativeTo>>>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.0.relative_to(&target.0) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Transforms(UniformConcat); + +impl Transforms { + pub fn identity(dim: usize, len: usize) -> Self { + let identity = UniformTransforms::identity(dim, len); + Self(UniformConcat::new_unchecked(vec![identity], dim, 0, len)) + } + fn mul(&self, rhs: &Self) -> Self { + let products = self + .0 + .iter() + .flat_map(|lhs| rhs.0.iter().map(move |rhs| lhs * rhs)) + .collect(); + Transforms(UniformConcat::new_unchecked( + products, + self.dim_in() + rhs.dim_in(), + self.delta_dim() + rhs.delta_dim(), + self.len_out() * rhs.len_out(), + )) + } + pub fn concat(&self, other: &Self) -> Result { + let maps = self.0.iter().chain(other.0.iter()).cloned().collect(); + Ok(Self(UniformConcat::new( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + )?)) + } + pub fn take(&self, indices: &[usize]) -> Result { + if !indices.windows(2).all(|pair| pair[0] < pair[1]) { + return Err(Error::IndicesNotStrictIncreasing); + } + if let Some(last) = indices.last() { + if *last >= self.0.len_in() { + return Err(Error::IndexOutOfRange); + } + } + let mut maps = Vec::new(); + let mut offset = 0; + let mut start = 0; + for map in self.0.iter() { + let stop = start + indices[start..].partition_point(|i| *i < offset + map.len_in()); + if stop > start { + let map_indices: Vec<_> = + indices[start..stop].iter().map(|i| *i - offset).collect(); + start = stop; + let primitive = Primitive::new_take(map_indices, map.len_in()); + maps.push(map.clone_and_push(primitive).unwrap()); + } + offset += map.len_in(); + } + assert_eq!(start, indices.len()); + Ok(Self(UniformConcat::new_unchecked( + maps, + self.dim_in(), + self.delta_dim(), + self.len_out(), + ))) + } + fn clone_and_push(&self, primitive: Primitive) -> Result { + if self.0.dim_in() < primitive.dim_out() { + return Err(Error::DimensionMismatch); + } + if self.0.len_in() % primitive.mod_out() != 0 { + return Err(Error::LengthMismatch); + } + let maps = self + .0 + .iter() + .map(|map| map.clone_and_push(primitive.clone()).unwrap()); + Ok(Self(UniformConcat::new_unchecked( + maps.collect(), + self.dim_in() - primitive.delta_dim(), + self.delta_dim() + primitive.delta_dim(), + self.len_out(), + ))) + } + pub fn children(&self, simplex: Simplex, offset: usize) -> Result { + self.clone_and_push(Primitive::new_children(simplex).with_offset(offset)) + } + pub fn edges(&self, simplex: Simplex, offset: usize) -> Result { + self.clone_and_push(Primitive::new_edges(simplex).with_offset(offset)) + } + pub fn uniform_points( + &self, + points: impl Into>, + point_dim: usize, + offset: usize, + ) -> Result { + self.clone_and_push(Primitive::new_uniform_points(points, point_dim).with_offset(offset)) + } + pub fn apply_inplace( + &self, + index: usize, + coords: &mut [f64], + stride: usize, + ) -> Result { + self.0.apply_inplace(index, coords, stride, 0) + } + pub fn apply_index(&self, index: usize) -> Option { + self.0.apply_index(index) + } + pub fn unapply_indices(&self, indices: &[T]) -> Option> { + self.0.unapply_indices(indices) + } +} + +//impl Deref for Transforms { +// type Target = OptionReorder>, Vec>; +// +// fn deref(&self) -> Self::Target { +// self.0 +// } +//} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + $self.0.$fn($($arg),*) + } + }; +} + +impl Map for Transforms { + dispatch! {fn len_out(&self) -> usize} + dispatch! {fn len_in(&self) -> usize} + dispatch! {fn dim_out(&self) -> usize} + dispatch! {fn dim_in(&self) -> usize} + dispatch! {fn delta_dim(&self) -> usize} + dispatch! {fn apply_inplace_unchecked(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> usize} + dispatch! {fn apply_inplace(&self, index: usize, coordinates: &mut [f64], stride: usize, offset: usize) -> Result} + dispatch! {fn apply_index_unchecked(&self, index: usize) -> usize} + dispatch! {fn apply_index(&self, index: usize) -> Option} + dispatch! {fn apply_indices_inplace_unchecked(&self, indices: &mut [usize])} + dispatch! {fn apply_indices(&self, indices: &[usize]) -> Option>} + dispatch! {fn unapply_indices_unchecked(&self, indices: &[T]) -> Vec} + dispatch! {fn unapply_indices(&self, indices: &[T]) -> Option>} + dispatch! {fn is_identity(&self) -> bool} + dispatch! {fn is_index_map(&self) -> bool} + dispatch! {fn update_basis(&self, index: usize, basis: &mut [f64], dim_out: usize, dim_in: &mut usize, offset: usize) -> usize} +} + +impl RelativeTo for Transforms { + type Output = + as RelativeTo>>::Output; + + fn relative_to(&self, target: &Self) -> Option { + self.0.relative_to(&target.0) + } +} + +impl Mul for &UniformTransforms { + type Output = UniformTransforms; + + fn mul(self, rhs: Self) -> UniformTransforms { + UniformTransforms::mul(self, rhs) + } +} + +impl Mul for UniformTransforms { + type Output = UniformTransforms; + + fn mul(self, rhs: Self) -> UniformTransforms { + UniformTransforms::mul(&self, &rhs) + } +} + +impl Mul for &Transforms { + type Output = Transforms; + + fn mul(self, rhs: Self) -> Transforms { + Transforms::mul(self, rhs) + } +} + +impl Mul for Transforms { + type Output = Transforms; + + fn mul(self, rhs: Self) -> Transforms { + Transforms::mul(&self, &rhs) + } +} diff --git a/src/simplex.rs b/src/simplex.rs new file mode 100644 index 000000000..efd69b082 --- /dev/null +++ b/src/simplex.rs @@ -0,0 +1,452 @@ +/// Simplex. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Simplex { + Line, + Triangle, +} + +impl Simplex { + /// Returns the dimension of the simplex. + #[inline] + pub const fn dim(&self) -> usize { + match self { + Self::Line => 1, + Self::Triangle => 2, + } + } + /// Returns the dimension of an edge of the simplex. + #[inline] + pub const fn edge_dim(&self) -> usize { + self.dim() - 1 + } + /// Returns the edge simplex, if any. + pub const fn edge_simplex(&self) -> Option { + match self { + Self::Line => None, + Self::Triangle => Some(Self::Line), + } + } + /// Returns the number of edges of the simplex. + #[inline] + pub const fn nedges(&self) -> usize { + match self { + Self::Line => 2, + Self::Triangle => 3, + } + } + /// Returns the number of children of the simplex. + #[inline] + pub const fn nchildren(&self) -> usize { + match self { + Self::Line => 2, + Self::Triangle => 4, + } + } + + const LINE_SWAP_EDGES_CHILDREN_MAP: [usize; 2] = [2, 1]; + const TRIANGLE_SWAP_EDGES_CHILDREN_MAP: [usize; 6] = [3, 6, 1, 7, 2, 5]; + + /// Returns an array of indices of edges of children corresponding to children of edges. + #[inline] + pub const fn swap_edges_children_map(&self) -> &'static [usize] { + match self { + Self::Line => &Self::LINE_SWAP_EDGES_CHILDREN_MAP, + Self::Triangle => &Self::TRIANGLE_SWAP_EDGES_CHILDREN_MAP, + } + } + + const LINE_CONNECTIVITY: [Option; 4] = [Some(3), None, None, Some(0)]; + const TRIANGLE_CONNECTIVITY: [Option; 12] = [ + Some(11), + None, + None, + None, + Some(10), + None, + None, + None, + Some(9), + Some(8), + Some(4), + Some(0), + ]; + + /// Returns an array of indices of opposite edges of children, or `None` if + /// an edge lies on the boundary of the simplex. + #[inline] + pub const fn connectivity(&self) -> &'static [Option] { + match self { + Self::Line => &Self::LINE_CONNECTIVITY, + Self::Triangle => &Self::TRIANGLE_CONNECTIVITY, + } + } + + const LINE_VERTICES: [f64; 2] = [0.0, 1.0]; + const TRIANGLE_VERTICES: [f64; 6] = [0.0, 0.0, 0.0, 1.0, 1.0, 0.0]; + + /// Returns an array of vertices. + #[inline] + pub const fn vertices(&self) -> &'static [f64] { + match self { + Self::Line => &Self::LINE_VERTICES, + Self::Triangle => &Self::TRIANGLE_VERTICES, + } + } + + pub fn centroid(&self) -> Vec { + let scale = (self.dim() + 1) as f64; + (0..self.dim()) + .map(|j| { + self.vertices() + .iter() + .skip(j) + .step_by(self.dim()) + .sum::() + / scale + }) + .collect() + } + + /// Transform the given child `coordinates` for child `index` to this parent + /// simplex in-place. The returned index is the index of the parent in an + /// infinite, uniform sequence. + pub fn apply_child( + &self, + index: usize, + coordinates: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + match self { + Self::Line => { + for coordinate in coordinates.chunks_mut(stride) { + let coordinate = &mut coordinate[offset..]; + coordinate[0] = 0.5 * (coordinate[0] + (index % 2) as f64); + } + index / 2 + } + Self::Triangle => { + for coordinate in coordinates.chunks_mut(stride) { + let coordinate = &mut coordinate[offset..]; + coordinate[0] *= 0.5; + coordinate[1] *= 0.5; + match index % 4 { + 1 => { + coordinate[0] += 0.5; + } + 2 => { + coordinate[1] += 0.5; + } + 3 => { + coordinate[1] += coordinate[0]; + coordinate[0] = 0.5 - coordinate[0]; + } + _ => {} + } + } + index / 4 + } + } + } + pub const fn apply_child_index(&self, index: usize) -> usize { + match self { + Self::Line => index / 2, + Self::Triangle => index / 4, + } + } + pub fn apply_child_indices_inplace(&self, indices: &mut [usize]) { + match self { + Self::Line => indices.iter_mut().for_each(|i| *i /= 2), + Self::Triangle => indices.iter_mut().for_each(|i| *i /= 4), + } + } + pub fn unapply_child_indices( + &self, + indices: impl Iterator, + ) -> impl Iterator { + let n = self.nchildren(); + indices.flat_map(move |i| (0..n).map(move |j| i * n + j)) + } + + /// Transform the given edge `coordinate` for edge `index` to this parent + /// simplex in-place. The returned index is the index of the parent in an + /// infinite, uniform sequence. + pub fn apply_edge( + &self, + index: usize, + coordinates: &mut [f64], + stride: usize, + offset: usize, + ) -> usize { + match self { + Self::Line => { + for coordinate in coordinates.chunks_mut(stride) { + let coordinate = &mut coordinate[offset..]; + coordinate.copy_within(self.edge_dim()..coordinate.len() - 1, self.dim()); + coordinate[0] = (1 - index % 2) as f64; + } + index / 2 + } + Self::Triangle => { + for coordinate in coordinates.chunks_mut(stride) { + let coordinate = &mut coordinate[offset..]; + coordinate.copy_within(self.edge_dim()..coordinate.len() - 1, self.dim()); + match index % 3 { + 0 => { + coordinate[1] = coordinate[0]; + coordinate[0] = 1.0 - coordinate[0]; + } + 1 => { + coordinate[1] = coordinate[0]; + coordinate[0] = 0.0; + } + _ => { + coordinate[1] = 0.0; + } + } + } + index / 3 + } + } + } + pub const fn apply_edge_index(&self, index: usize) -> usize { + match self { + Self::Line => index / 2, + Self::Triangle => index / 3, + } + } + pub fn apply_edge_indices_inplace(&self, indices: &mut [usize]) { + match self { + Self::Line => indices.iter_mut().for_each(|i| *i /= 2), + Self::Triangle => indices.iter_mut().for_each(|i| *i /= 3), + } + } + pub fn unapply_edge_indices( + &self, + indices: impl Iterator, + ) -> impl Iterator { + let n = self.nedges(); + indices.flat_map(move |i| (0..n).map(move |j| i * n + j)) + } + pub fn update_child_basis( + &self, + index: usize, + basis: &mut [f64], + dim_out: usize, + dim_in: &mut usize, + offset: usize, + ) -> usize { + assert!(offset + self.dim() <= *dim_in); + match self { + Self::Line => { + for i in 0..*dim_in { + basis[i * dim_out + offset] *= 0.5; + } + index / 2 + } + Self::Triangle => unimplemented! {}, + } + } + pub fn update_edge_basis( + &self, + index: usize, + basis: &mut [f64], + dim_out: usize, + dim_in: &mut usize, + offset: usize, + ) -> usize { + assert!(offset + self.edge_dim() <= *dim_in); + // Shift rows `offset..dim_in` one row down. + for i in (offset..*dim_in).into_iter().rev() { + for j in 0..*dim_in { + basis[(i + 1) * dim_out + j] = basis[i * dim_out + j]; + } + } + for j in 0..*dim_in { + basis[offset * dim_out + j] = 0.0; + } + // Zero the normal. + for i in 0..*dim_in + 1 { + basis[i * dim_out + *dim_in] = 0.0; + } + let index = match self { + Self::Line => { + basis[offset * dim_out + *dim_in] = if index % 2 == 0 { 1.0 } else { -1.0 }; + index / 2 + } + Self::Triangle => unimplemented! {}, + }; + *dim_in += 1; + index + } + //fn child_poly_coeffs_inplace(&self, index: usize, coeffs: &mut [f64], strides: &[usize], offset: usize) -> usize { + // match self { + // Self::Line => { + // + // } + // _ => unimplemented!{} + // } + //} +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_abs_diff_eq; + use std::iter; + use Simplex::*; + + macro_rules! assert_child_index_coord { + ($simplex:ident, $inidx:expr, $incoords:expr, $outidx:expr, $outcoords:expr) => {{ + let incoords = $incoords; + let outcoords = $outcoords; + let stride = outcoords[0].len(); + let mut work: Vec<_> = iter::repeat(-1.0).take(outcoords.len() * stride).collect(); + for (work, incoord) in iter::zip(work.chunks_mut(stride), incoords.iter()) { + work[..incoord.len()].copy_from_slice(incoord); + } + assert_eq!($simplex.apply_child($inidx, &mut work, stride, 0), $outidx); + for (actual, desired) in iter::zip(work.chunks(stride), outcoords.iter()) { + assert_abs_diff_eq!(actual[..], desired[..]); + } + }}; + } + + macro_rules! assert_edge_index_coord { + ($simplex:ident, $inidx:expr, $incoords:expr, $outidx:expr, $outcoords:expr) => {{ + let incoords = $incoords; + let outcoords = $outcoords; + let stride = outcoords[0].len(); + let mut work: Vec<_> = iter::repeat(-1.0).take(outcoords.len() * stride).collect(); + for (work, incoord) in iter::zip(work.chunks_mut(stride), incoords.iter()) { + work[..incoord.len()].copy_from_slice(incoord); + } + assert_eq!($simplex.apply_edge($inidx, &mut work, stride, 0), $outidx); + for (actual, desired) in iter::zip(work.chunks(stride), outcoords.iter()) { + assert_abs_diff_eq!(actual[..], desired[..]); + } + }}; + } + + #[test] + fn line_child_coords() { + assert_child_index_coord!(Line, 2, [[0.0], [0.5], [1.0]], 1, [[0.0], [0.25], [0.5]]); + assert_child_index_coord!(Line, 5, [[0.0], [0.5], [1.0]], 2, [[0.5], [0.75], [1.0]]); + } + + #[test] + fn line_edge_coords() { + assert_edge_index_coord!(Line, 2, [[]], 1, [[1.0]]); + assert_edge_index_coord!(Line, 5, [[]], 2, [[0.0]]); + } + + #[test] + fn triangle_child_coords() { + assert_child_index_coord!( + Triangle, + 4, + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], + 1, + [[0.0, 0.0], [0.5, 0.0], [0.0, 0.5]] + ); + assert_child_index_coord!( + Triangle, + 9, + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], + 2, + [[0.5, 0.0], [1.0, 0.0], [0.5, 0.5]] + ); + assert_child_index_coord!( + Triangle, + 14, + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], + 3, + [[0.0, 0.5], [0.5, 0.5], [0.0, 1.0]] + ); + assert_child_index_coord!( + Triangle, + 19, + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], + 4, + [[0.5, 0.0], [0.0, 0.5], [0.5, 0.5]] + ); + } + + #[test] + fn triangle_edge_coords() { + assert_edge_index_coord!(Triangle, 3, [[0.0], [1.0]], 1, [[1.0, 0.0], [0.0, 1.0]]); + assert_edge_index_coord!(Triangle, 7, [[0.0], [1.0]], 2, [[0.0, 0.0], [0.0, 1.0]]); + assert_edge_index_coord!(Triangle, 11, [[0.0], [1.0]], 3, [[0.0, 0.0], [1.0, 0.0]]); + } + + #[test] + fn line_swap_edges_children_map() { + for (i, j) in Line.swap_edges_children_map().iter().cloned().enumerate() { + let mut x = [0.5]; + let mut y = [0.5]; + Line.apply_edge(i, &mut x, 1, 0); + Line.apply_child(Line.apply_edge(j, &mut y, 1, 0), &mut y, 1, 0); + assert_abs_diff_eq!(x[..], y[..]); + } + } + + #[test] + fn triangle_swap_edges_children_map() { + for (i, j) in Triangle + .swap_edges_children_map() + .iter() + .cloned() + .enumerate() + { + let mut x = [0.5, 0.5]; + let mut y = [0.5, 0.5]; + Triangle.apply_edge(Line.apply_child(i, &mut x, 2, 0), &mut x, 2, 0); + Triangle.apply_child(Triangle.apply_edge(j, &mut y, 2, 0), &mut y, 2, 0); + assert_abs_diff_eq!(x[..], y[..]); + } + } + + #[test] + fn line_connectivity() { + for (i, j) in Line.connectivity().iter().cloned().enumerate() { + if let Some(j) = j { + let mut x = [0.5]; + let mut y = [0.5]; + Line.apply_child(Line.apply_edge(i, &mut x, 1, 0), &mut x, 1, 0); + Line.apply_child(Line.apply_edge(j, &mut y, 1, 0), &mut y, 1, 0); + assert_abs_diff_eq!(x[..], y[..]); + } + } + } + + #[test] + fn triangle_connectivity() { + for (i, j) in Triangle.connectivity().iter().cloned().enumerate() { + if let Some(j) = j { + let mut x = [0.5, 0.5]; + let mut y = [0.5, 0.5]; + Triangle.apply_child(Triangle.apply_edge(i, &mut x, 2, 0), &mut x, 2, 0); + Triangle.apply_child(Triangle.apply_edge(j, &mut y, 2, 0), &mut y, 2, 0); + assert_abs_diff_eq!(x[..], y[..]); + } + } + } + + #[test] + fn child_basis() { + let mut basis = vec![1.0, 0.0, 0.0, 1.0]; + let mut dim_in = 2; + assert_eq!(Line.update_child_basis(0, &mut basis, 2, &mut dim_in, 0), 0); + assert_eq!(dim_in, 2); + assert_abs_diff_eq!(basis[..], [0.5, 0.0, 0.0, 1.0]); + } + + #[test] + fn edge_basis() { + let mut basis = vec![1.0, 0.0, 0.0, 0.0]; + let mut dim_in = 1; + assert_eq!(Line.update_edge_basis(1, &mut basis, 2, &mut dim_in, 1), 0); + assert_eq!(dim_in, 2); + assert_abs_diff_eq!(basis[..], [1.0, 0.0, 0.0, -1.0]); + } +} diff --git a/src/topology.rs b/src/topology.rs new file mode 100644 index 000000000..9647c483e --- /dev/null +++ b/src/topology.rs @@ -0,0 +1,544 @@ +use crate::relative::RelativeTo as _; +use crate::simplex::Simplex; +use crate::tesselation::Tesselation; +use crate::{AddOffset, Map, UnapplyIndicesData}; +use std::iter; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; +use std::ops::Mul; + +#[derive(Debug, Clone, PartialEq)] +pub struct Root { + pub name: String, + pub dim: usize, +} + +impl Root { + pub fn new(name: String, dim: usize) -> Self { + Self { name, dim } + } +} + +pub trait TopologyCore: std::fmt::Debug + Clone + Into { + fn tesselation(&self) -> &Tesselation; + fn dim(&self) -> usize { + self.tesselation().dim() + } + fn ntiles(&self) -> usize { + self.tesselation().len() + } + fn refined(&self) -> Topology; + fn map_itiles_to_refined(&self, itiles: &[usize]) -> Vec { + self.refined() + .tesselation() + .unapply_indices_from(self.tesselation(), itiles) + .unwrap() + } + fn boundary(&self) -> Topology; + fn take(&self, itiles: &[usize]) -> Topology { + let mut itiles: Vec = itiles.to_vec(); + itiles.sort_by_key(|&index| index); + Take::new(self.clone().into(), itiles).into() + } + fn refined_by(&self, itiles: &[usize]) -> Topology { + Hierarchical::new(self.clone().into(), vec![(0..self.ntiles()).collect()]) + .refined_by(itiles) + .into() + } + fn centroids(&self) -> Topology { + Point::new(self.tesselation().centroids()).into() + } +} + +#[derive(Clone, PartialEq)] +pub enum Topology { + Point(Rc), + Line(Rc), + Product(Rc), + DisjointUnion(Rc), + Take(Rc), + Hierarchical(Rc), +} + +impl Topology { + fn new_line(len: usize) -> Self { + Line::from_len(len).into() + } + fn disjoin_union(self, other: Self) -> Self { + DisjointUnion::new(self, other).into() + } +} + +macro_rules! dispatch { + ( + $vis:vis fn $fn:ident$(<$genarg:ident: $genpath:path>)?( + &$self:ident $(, $arg:ident: $ty:ty)* + ) $(-> $ret:ty)? + ) => { + #[inline] + $vis fn $fn$(<$genarg: $genpath>)?(&$self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + ($vis:vis fn $fn:ident(&mut $self:ident $(, $arg:ident: $ty:ty)*) $(-> $ret:ty)?) => { + #[inline] + $vis fn $fn(&mut $self $(, $arg: $ty)*) $(-> $ret)? { + dispatch!(@match $self; $fn; $($arg),*) + } + }; + (@match $self:ident; $fn:ident; $($arg:ident),*) => { + match $self { + Self::Point(var) => var.$fn($($arg),*), + Self::Line(var) => var.$fn($($arg),*), + Self::Product(var) => var.$fn($($arg),*), + Self::DisjointUnion(var) => var.$fn($($arg),*), + Self::Take(var) => var.$fn($($arg),*), + Self::Hierarchical(var) => var.$fn($($arg),*), + } + } +} + +impl TopologyCore for Topology { + dispatch! {fn tesselation(&self) -> &Tesselation} + dispatch! {fn dim(&self) -> usize} + dispatch! {fn ntiles(&self) -> usize} + dispatch! {fn refined(&self) -> Topology} + dispatch! {fn map_itiles_to_refined(&self, itiles: &[usize]) -> Vec} + dispatch! {fn boundary(&self) -> Topology} + dispatch! {fn take(&self, itiles: &[usize]) -> Topology} + dispatch! {fn refined_by(&self, itiles: &[usize]) -> Topology} +} + +impl std::fmt::Debug for Topology { + dispatch! {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result} +} + +macro_rules! impl_from_topo { + ($($from:tt),*) => { + $( + impl From<$from> for Topology { + fn from(topo: $from) -> Topology { + Topology::$from(Rc::new(topo)) + } + } + )* + }; +} + +impl_from_topo! {Point, Line, Product, DisjointUnion, Take, Hierarchical} + +impl Mul for Topology { + type Output = Self; + + fn mul(self, rhs: Self) -> Self { + Product::new(self, rhs).into() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Point(Tesselation); + +impl Point { + pub fn new(tesselation: Tesselation) -> Self { + assert_eq!(tesselation.dim(), 0); + Self(tesselation) + } +} + +impl TopologyCore for Point { + fn dim(&self) -> usize { + 0 + } + fn tesselation(&self) -> &Tesselation { + &self.0 + } + fn refined(&self) -> Topology { + self.clone().into() + } + fn boundary(&self) -> Topology { + panic!("the boundary of a Point does not exist"); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Line(Tesselation); + +impl Line { + pub fn new(tesselation: Tesselation) -> Self { + assert_eq!(tesselation.dim(), 1); + Self(tesselation) + } + pub fn from_len(len: usize) -> Self { + Self(Tesselation::identity(vec![Simplex::Line], len)) + } +} + +impl TopologyCore for Line { + fn dim(&self) -> usize { + 1 + } + fn tesselation(&self) -> &Tesselation { + &self.0 + } + fn refined(&self) -> Topology { + Self(self.tesselation().children()).into() + } + fn boundary(&self) -> Topology { + Point(self.tesselation().edges().unwrap().take(&[1, 2 * self.ntiles() - 2])).into() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DisjointUnion { + topos: [Topology; 2], + tesselation: Tesselation, +} + +impl DisjointUnion { + pub fn new(topo0: Topology, topo1: Topology) -> Self { + // TODO: assert common roots + let tesselation = topo0.tesselation().concat(topo1.tesselation()).unwrap(); + Self { + topos: [topo0, topo1], + tesselation, + } + } +} + +impl TopologyCore for DisjointUnion { + fn tesselation(&self) -> &Tesselation { + &self.tesselation + } + fn refined(&self) -> Topology { + Self::new(self.topos[0].refined(), self.topos[1].refined()).into() + } + fn boundary(&self) -> Topology { + Self::new(self.topos[0].boundary(), self.topos[1].boundary()).into() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Product { + topos: [Topology; 2], + tesselation: Tesselation, +} + +impl Product { + pub fn new(topo0: Topology, topo1: Topology) -> Self { + let tesselation = topo0.tesselation() * topo1.tesselation(); + // TODO: assert no common roots + Self { + topos: [topo0, topo1], + tesselation, + } + } +} + +impl TopologyCore for Product { + fn tesselation(&self) -> &Tesselation { + &self.tesselation + } + fn refined(&self) -> Topology { + Self::new(self.topos[0].refined(), self.topos[1].refined()).into() + } + fn boundary(&self) -> Topology { + DisjointUnion::new( + Product::new(self.topos[0].clone(), self.topos[1].boundary()).into(), + Product::new(self.topos[0].boundary(), self.topos[1].clone()).into(), + ).into() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Take { + topo: Topology, + itiles: Vec, + tesselation: Tesselation, +} + +impl Take { + pub fn new(topo: Topology, itiles: Vec) -> Self { + // TODO: requires sorted itiles? + let tesselation = topo.tesselation().take(&itiles); + Self { + topo, + itiles, + tesselation, + } + } +} + +impl TopologyCore for Take { + fn tesselation(&self) -> &Tesselation { + &self.tesselation + } + fn refined(&self) -> Topology { + let refined = self.topo.refined(); + let mut itiles = refined + .tesselation() + .unapply_indices_from(self.topo.tesselation(), &self.itiles) + .unwrap(); + itiles.sort_by_key(|&index| index); + Take::new(refined, itiles).into() + } + fn boundary(&self) -> Topology { + unimplemented! {} + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Hierarchical { + base: Topology, + itiles: Vec>, + tesselation: Tesselation, +} + +fn refine_iter(base: Topology) -> impl Iterator { + iter::successors(Some(base), |topo| Some(topo.refined())) +} + +impl Hierarchical { + pub fn new(base: Topology, itiles: Vec>) -> Self { + let tesselation = Tesselation::concat_iter( + itiles + .iter() + .zip(refine_iter(base.clone())) + .map(|(itiles, level)| level.tesselation().take(itiles)) + ).unwrap(); + assert_eq!(itiles.iter().map(|item| item.len()).sum::(), tesselation.len()); + Self { + base, + itiles, + tesselation, + } + } + fn levels(&self) -> impl Iterator { + refine_iter(self.base.clone()) + } + fn itiles_levels(&self) -> impl Iterator, Topology)> { + self.itiles.iter().zip(self.levels()) + } +} + +impl TopologyCore for Hierarchical { + fn tesselation(&self) -> &Tesselation { + &self.tesselation + } + fn refined(&self) -> Topology { + let itiles = self + .itiles_levels() + .map(|(itiles, level)| level.map_itiles_to_refined(itiles)) + .collect(); + Hierarchical::new(self.base.refined(), itiles).into() + } + fn refined_by(&self, itiles: &[usize]) -> Topology { + let mut global_itiles = itiles.to_vec(); + global_itiles.sort_by_key(|&index| index); + let mut global_itiles = global_itiles.into_iter().peekable(); + let mut offset = 0; + let mut queue = Vec::new(); + let mut refined_itiles = Vec::new(); + for (level_itiles, level) in self.itiles_levels() { + let mut refined_level_itiles: Vec = queue.drain(..).collect(); + for (i, itile) in level_itiles.iter().cloned().enumerate() { + if global_itiles.next_if(|&j| i + offset == j).is_some() { + queue.push(itile); + } else { + refined_level_itiles.push(itile); + } + } + refined_level_itiles.sort_by_key(|&index| index); + refined_itiles.push(refined_level_itiles); + queue = level.map_itiles_to_refined(&queue); + offset += level_itiles.len(); + } + if !queue.is_empty() { + refined_itiles.push(queue); + } + Hierarchical::new(self.base.clone(), refined_itiles).into() + } + fn boundary(&self) -> Topology { + let base_boundary = self.base.boundary(); + let itiles = self + .itiles_levels() + .zip(refine_iter(base_boundary.clone())) + .map(|((itiles, level), blevel)| { + let mut itiles = blevel + .tesselation() + .unapply_indices_from(level.tesselation(), itiles) + .unwrap(); + itiles.sort_by_key(|&index| index); + itiles + }) + .collect(); + Hierarchical::new(base_boundary, itiles).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_abs_diff_eq; + + macro_rules! assert_centroids { + ($topo:expr, $geom:expr, $desired:expr $(, $simplex:ident)*) => {{ + let topo = $topo; + let geom = $geom; + let tesselation = topo.tesselation(); + let desired = $desired; + let dim_out = tesselation.dim_out(); + let centroid_in = iter::once([]); + let simplex_dim = 0; + $( + let centroid_in = centroid_in.map(|coord| [&coord as &[f64], &Simplex::$simplex.centroid()].concat()); + let simplex_dim = simplex_dim + Simplex::$simplex.dim(); + )* + assert_eq!(simplex_dim, topo.dim(), "the given simplices don't add up to the dimension of the topology"); + let pad: Vec = iter::repeat(0.0).take(dim_out - topo.dim()).collect(); + let centroid_in: Vec = centroid_in.flat_map(|coord| [&coord[..], &pad].concat()).collect(); + + assert_eq!(desired.len(), topo.ntiles(), "invalid len of desired centroids"); + + for (i, desired) in desired.into_iter().enumerate() { + println!("i = {i}"); + let mut actual = centroid_in.clone(); + let iroot = tesselation.apply_inplace(i, &mut actual, dim_out).unwrap(); + geom(iroot, &mut actual); + assert_abs_diff_eq!(actual[..], desired[..]); + } + }}; + } + + #[test] + fn test1() { + let x0 = Line::from_len(2); + let geom = |i: usize, c: &mut [f64]| c[0] += i as f64; + assert_centroids!(&x0, geom, [[0.5], [1.5]], Line); + let x1 = x0.refined_by(&[1]); + assert_centroids!(&x1, geom, [[0.5], [1.25], [1.75]], Line); + let x2 = x1.refined_by(&[1]); + println!("{:?}", x2.tesselation()); + assert_centroids!(&x2, geom, [[0.5], [1.75], [1.125], [1.375]], Line); + + let x0b = x0.boundary(); + assert_centroids!(&x0b, geom, [[0.0], [2.0]]); + } + + #[test] + fn test2() { + let x = Topology::new_line(2); + let y = Topology::new_line(2); + let geom = |i: usize, c: &mut [f64]| { + c[0] += (i / 2) as f64; + c[1] += (i % 2) as f64; + }; + let xy: Topology = x.clone() * y.clone(); + assert_centroids!( + &xy, + geom, + [[0.5, 0.5], [0.5, 1.5], [1.5, 0.5], [1.5, 1.5]], + Line, + Line + ); + assert_centroids!( + &xy.boundary(), + geom, + [ + [0.5, 0.0], + [0.5, 2.0], + [1.5, 0.0], + [1.5, 2.0], + [0.0, 0.5], + [0.0, 1.5], + [2.0, 0.5], + [2.0, 1.5] + ], + Line + ); + } + + #[test] + fn hierarchical() { + let x = Topology::new_line(2); + let y = Topology::new_line(2); + let geom = |i: usize, c: &mut [f64]| { + c[0] += (i / 2) as f64; + c[1] += (i % 2) as f64; + }; + let xy0 = x * y; + assert_centroids!( + &xy0, + geom, + [[0.5, 0.5], [0.5, 1.5], [1.5, 0.5], [1.5, 1.5]], + Line, + Line + ); + assert_centroids!( + xy0.boundary(), + geom, + [ + [0.5, 0.0], + [0.5, 2.0], + [1.5, 0.0], + [1.5, 2.0], + [0.0, 0.5], + [0.0, 1.5], + [2.0, 0.5], + [2.0, 1.5] + ], + Line + ); + let xy1 = xy0.refined_by(&[2]); + assert_centroids!( + &xy1, + geom, + [ + [0.5, 0.5], + [0.5, 1.5], + [1.5, 1.5], + [1.25, 0.25], + [1.25, 0.75], + [1.75, 0.25], + [1.75, 0.75] + ], + Line, + Line + ); + assert_centroids!( + xy1.boundary(), + geom, + [ + [0.5, 0.0], + [0.5, 2.0], + [1.5, 2.0], + [0.0, 0.5], + [0.0, 1.5], + [2.0, 1.5], + [1.25, 0.0], + [1.75, 0.0], + [2.0, 0.25], + [2.0, 0.75], + ], + Line + ); + let xy2 = xy1.refined_by(&[3]); + assert_centroids!( + &xy2, + geom, + [ + [0.5, 0.5], + [0.5, 1.5], + [1.5, 1.5], + [1.25, 0.75], + [1.75, 0.25], + [1.75, 0.75], + [1.125, 0.125], + [1.125, 0.375], + [1.375, 0.125], + [1.375, 0.375] + ], + Line, + Line + ); + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 000000000..40c339643 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,72 @@ +pub trait ReplaceNthIter: Iterator + Sized { + /// Replaces the nth item of the iterator with the given item. + fn replace_nth(self, index: usize, item: Self::Item) -> ReplaceNth; +} + +impl ReplaceNthIter for Iter { + fn replace_nth(self, index: usize, item: Iter::Item) -> ReplaceNth { + ReplaceNth(self, 0, index, item) + } +} + +pub struct ReplaceNth(Iter, usize, usize, Iter::Item); + +impl Iterator for ReplaceNth { + type Item = Iter::Item; + + fn next(&mut self) -> Option { + self.0.next().map(|mut value| { + if self.1 == self.2 { + std::mem::swap(&mut self.3, &mut value); + } + self.1 += 1; + value + }) + } +} + +pub trait SkipNthIter: Iterator + Sized { + /// Skips the nth item of the iterator. + fn skip_nth(self, index: usize) -> SkipNth; +} + +impl SkipNthIter for Iter { + fn skip_nth(self, index: usize) -> SkipNth { + SkipNth(self, 0, index) + } +} + +pub struct SkipNth(Iter, usize, usize); + +impl Iterator for SkipNth { + type Item = Iter::Item; + + fn next(&mut self) -> Option { + if let Some(value) = self.0.next() { + let next = if self.1 == self.2 { + self.0.next() + } else { + Some(value) + }; + self.1 += 1; + next + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn replace_nth() { + let mut a = ['a', 'b', 'c', 'd'].into_iter().replace_nth(1, 'd'); + assert_eq!(a.next(), Some('a')); + assert_eq!(a.next(), Some('d')); // replaced + assert_eq!(a.next(), Some('c')); + assert_eq!(a.next(), Some('d')); + assert_eq!(a.next(), None); + } +} diff --git a/tests/test_function.py b/tests/test_function.py index 539a84a12..762d7f8dd 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -397,14 +397,11 @@ class Custom(TestCase): def assertEvalAlmostEqual(self, factual, fdesired, **args): with self.subTest('0d-points'): self.assertAllAlmostEqual(factual.as_evaluable_array.eval(**args), fdesired.as_evaluable_array.eval(**args)) - transform_chains = dict(test=(transform.EvaluableTransformChain.from_argument('test', 2, 2),)*2) with self.subTest('1d-points'): - coords = evaluable.Zeros((5, 2), float) - lower_args = function.LowerArgs(coords.shape[:-1], transform_chains, dict(test=coords)) + lower_args = function.LowerArgs((evaluable.asarray(5),), {}, {}) self.assertAllAlmostEqual(factual.lower(lower_args).eval(**args), fdesired.lower(lower_args).eval(**args)) with self.subTest('2d-points'): - coords = evaluable.Zeros((5, 6, 2), float) - lower_args = function.LowerArgs(coords.shape[:-1], transform_chains, dict(test=coords)) + lower_args = function.LowerArgs((evaluable.asarray(5), evaluable.asarray(6)), {}, {}) self.assertAllAlmostEqual(factual.lower(lower_args).eval(**args), fdesired.lower(lower_args).eval(**args)) def assertMultipy(self, leftval, rightval): @@ -640,6 +637,7 @@ def setUp(self): numpy.random.seed(0) self.f = basis.dot(numpy.random.uniform(size=len(basis))) sample = self.domain.sample('gauss', 2) + print(sample.eval(self.f)) self.f_sampled = sample.asfunction(sample.eval(self.f)) def test_isarray(self): @@ -1154,7 +1152,8 @@ def test_lower(self): ref = element.PointReference() if self.basis.coords.shape[0] == 0 else element.LineReference()**self.basis.coords.shape[0] points = ref.getpoints('bezier', 4) coordinates = evaluable.Constant(points.coords) - lowered = self.basis.lower(function.LowerArgs(coordinates.shape[:-1], dict(X=(self.checktransforms.get_evaluable(evaluable.Argument('ielem', (), int)),)*2), dict(X=coordinates))) + lowerargs = function.LowerArgs.for_space('X', (self.checktransforms,), evaluable.Argument('ielem', (), int), coordinates) + lowered = self.basis.lower(lowerargs) with _builtin_warnings.catch_warnings(): _builtin_warnings.simplefilter('ignore', category=evaluable.ExpensiveEvaluationWarning) for ielem in range(self.checknelems): diff --git a/tests/test_sample.py b/tests/test_sample.py index 395249b75..7dfbcf1eb 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -64,7 +64,8 @@ def test_get_lower_args(self): self.assertEqual(actual_shape, desired_shape) offset = 0 for space, desired_chain, desired_point in zip(self.desired_spaces, desired_chains, desired_points): - self.assertEqual(args.transform_chains[space][0].eval(ielem=ielem), desired_chain) + (chain, *_), index = args.transform_chains[space] + self.assertEqual(chain[index.eval(ielem=ielem).__index__()], desired_chain) desired_coords = desired_point.coords desired_coords = numpy.lib.stride_tricks.as_strided(desired_coords, shape=(*desired_shape, desired_point.ndims,), strides=(0,)*offset+desired_coords.strides[:-1]+(0,)*(len(args.points_shape)-offset-desired_coords.ndim+1)+desired_coords.strides[-1:]) actual_coords = args.coordinates[space].eval(ielem=ielem) @@ -128,7 +129,8 @@ def test_take_elements_single(self): self.assertEqual(take.ndims, self.desired_ndims) args = take.get_lower_args(evaluable.Argument('ielem', (), int)) for space, desired_chain in zip(self.desired_spaces, self.desired_transform_chains[ielem]): - self.assertEqual(args.transform_chains[space][0].eval(ielem=0), desired_chain) + (chain, *_), index = args.transform_chains[space] + self.assertEqual(chain[index.eval(ielem=0).__index__()], desired_chain) def test_take_elements_empty(self): take = self.sample.take_elements(numpy.array([], int)) diff --git a/tests/test_topology.py b/tests/test_topology.py index 847234204..a8da8fbf1 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -643,8 +643,9 @@ def assertConnectivity(self, domain, geom): bmask = numpy.zeros(len(boundary), dtype=int) imask = numpy.zeros(len(interfaces), dtype=int) coordinates = evaluable.Points(evaluable.NPoints(), boundary.ndims) - transform_chain = transform.EvaluableTransformChain.from_argument('trans', domain.transforms.todims, boundary.ndims) - lowered_geom = geom.lower(function.LowerArgs.for_space(domain.space, (transform_chain, transform_chain), coordinates)).simplified + edges = domain.transforms.edges(domain.references) + iedge = evaluable.Argument('_iedge', (), int) + lowered_geom = geom.lower(function.LowerArgs.for_space(domain.space, (edges,), iedge, coordinates)).simplified for ielem, ioppelems in enumerate(domain.connectivity): for iedge, ioppelem in enumerate(ioppelems): etrans, eref = domain.references[ielem].edges[iedge] @@ -666,8 +667,8 @@ def assertConnectivity(self, domain, geom): imask[index] += 1 self.assertEqual(eref, opperef) points = eref.getpoints('gauss', 2) - a0 = lowered_geom.eval(trans=trans, _points=points) - a1 = lowered_geom.eval(trans=opptrans, _points=points) + a0 = lowered_geom.eval(_iedge=edges.index(trans), _points=points) + a1 = lowered_geom.eval(_iedge=edges.index(opptrans), _points=points) numpy.testing.assert_array_almost_equal(a0, a1) self.assertTrue(numpy.equal(bmask, 1).all()) self.assertTrue(numpy.equal(imask, 2).all()) diff --git a/tests/test_transform.py b/tests/test_transform.py index 6e05ce6fd..e5dce1082 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -98,79 +98,3 @@ def setUp(self): del TestTransform, TestInvertible, TestUpdim - - -class EvaluableTransformChainArgument(TestCase): - - def test_evalf(self): - chain = transform.SimplexEdge(2, 0), - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertEqual(echain.eval(chain=chain), chain) - - def test_todims(self): - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertEqual(echain.todims, 2) - - def test_fromdims(self): - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertEqual(echain.fromdims, 1) - - def test_linear(self): - chain = transform.SimplexEdge(2, 0), - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertAllAlmostEqual(echain.linear.eval(chain=chain), numpy.array([[-1.], [1.]])) - - def test_linear_derivative(self): - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertTrue(evaluable.iszero(evaluable.derivative(echain.linear, evaluable.Argument('test', ())).simplified)) - - def test_basis(self): - chain = transform.SimplexEdge(2, 0), - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertAllAlmostEqual(echain.basis.eval(chain=chain), numpy.array([[-1., 1.], [1., 1.]])) - - def test_basis_derivative(self): - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - self.assertTrue(evaluable.iszero(evaluable.derivative(echain.basis, evaluable.Argument('test', ())).simplified)) - - def test_apply(self): - chain = transform.SimplexEdge(2, 0), - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - ecoords = evaluable.Argument('coords', (5, echain.fromdims), float) - coords = numpy.linspace(0, 1, 5*echain.fromdims).reshape(5, echain.fromdims) - self.assertAllAlmostEqual(echain.apply(ecoords).eval(chain=chain, coords=coords), transform.apply(chain, coords)) - - def test_apply_derivative(self): - chain = transform.SimplexEdge(2, 0), - echain = transform.EvaluableTransformChain.from_argument('chain', 2, 1) - ecoords = evaluable.Argument('coords', (5, echain.fromdims), float) - coords = numpy.linspace(0, 1, 5*echain.fromdims).reshape(5, echain.fromdims) - actual = evaluable.derivative(echain.apply(ecoords), ecoords).eval(chain=chain) - desired = numpy.einsum('jk,iklm->ijlm', numpy.array([[-1.], [1.]]), numpy.eye(5*echain.fromdims).reshape(5, echain.fromdims, 5, echain.fromdims)) - self.assertAllAlmostEqual(actual, desired) - - -class EmptyEvaluableTransformChain(TestCase): - - def setUp(self): - super().setUp() - self.chain = transform.EvaluableTransformChain.empty(2) - - def test_evalf(self): - self.assertEqual(self.chain.evalf(), ()) - - def test_todims(self): - self.assertEqual(self.chain.todims, 2) - - def test_fromdims(self): - self.assertEqual(self.chain.fromdims, 2) - - def test_linear(self): - self.assertAllAlmostEqual(self.chain.linear.eval(), numpy.diag([1, 1])) - - def test_basis(self): - self.assertAllAlmostEqual(self.chain.basis.eval(), numpy.diag([1, 1])) - - def test_apply(self): - coords = numpy.array([1., 2.]) - self.assertAllAlmostEqual(self.chain.apply(evaluable.Argument('coords', (2,))).eval(coords=coords), coords) diff --git a/tests/test_transformseq.py b/tests/test_transformseq.py index 2266ee549..18d1dfa63 100644 --- a/tests/test_transformseq.py +++ b/tests/test_transformseq.py @@ -144,30 +144,6 @@ def test_refined(self): for i, trans in enumerate(ctransforms): self.assertEqual(refined.index(trans), i) - def test_get_evaluable(self): - eindex = nutils.evaluable.InRange(nutils.evaluable.Argument('index', (), int), len(self.check)) - echain = self.seq.get_evaluable(eindex) - for index, chain in enumerate(self.check): - self.assertEqual(echain.eval(index=index), chain) - - def test_index_with_tail_in(self): - assert len(self.check) == len(self.checkrefs) - echain = nutils.transform.EvaluableTransformChain.from_argument('chain', self.checktodims, self.checkfromdims) - eindex, etail = echain.index_with_tail_in(self.seq) - for i, (trans, ref) in enumerate(zip(self.check, self.checkrefs)): - self.assertEqual(int(eindex.eval(chain=trans)), i) - self.assertEqual(etail.eval(chain=trans), ()) - for ctrans in ref.child_transforms: - self.assertEqual(self.seq.index_with_tail(trans+(ctrans,)), (i, (ctrans,))) - if self.checkfromdims > 0: - echain = nutils.transform.EvaluableTransformChain.from_argument('chain', self.checktodims, self.checkfromdims-1) - eindex, etail = echain.index_with_tail_in(self.seq) - for i, (trans, ref) in enumerate(zip(self.check, self.checkrefs)): - for etrans in ref.edge_transforms: - for shuffle in lambda t: t, nutils.transform.canonical: - self.assertEqual(int(eindex.eval(chain=shuffle(trans+(etrans,)))), i) - self.assertEqual(etail.eval(chain=shuffle(trans+(etrans,))), (etrans,)) - class Edges: