# coding: utf-8
from __future__ import annotations
import numpy as np
import h5py
import yaml
import os
from collections import abc, OrderedDict
from typing import Union as TypeUnion, Optional, List, Dict, Iterable, TYPE_CHECKING
# Union clashes with core.basic.Union
from sympy import Integer
from sympy.core.singleton import Singleton
from sympy.core import Basic, symbols
from sympy.core.containers import Tuple
from sympy.tensor import IndexedBase, Indexed
from sympy.core import Add, Mul, Pow
from sympy.core.expr import AtomicExpr
from sympde.old_sympy_utilities import is_sequence, with_metaclass
from sympde.core.basic import CalculusFunction
from .basic import BasicDomain, InteriorDomain, Boundary, Union, Connectivity
from .basic import Interval, Interface, CornerBoundary, CornerInterface
from .basic import ProductDomain
# TODO fix circular dependency between domain and mapping
if TYPE_CHECKING:
from sympde.topology.mapping import Mapping
# TODO add pdim
iterable_types = (tuple, list, Tuple, Union)
#==============================================================================
[docs]
class Domain(BasicDomain):
"""
Represents an undefined domain.
A domain is defined by at least one interior domain and possible boundaries.
A domain without a boundary is either infinite or periodic.
A domain can also be constructed from a connectivity, in which case, only the
name and connectivity need to be passed.
"""
def __new__(cls, name : str, *,
interiors : TypeUnion[Iterable[InteriorDomain], InteriorDomain, None] = None,
boundaries : TypeUnion[Iterable[Boundary], Boundary, None] = None,
dim : Optional[int] = None,
connectivity : Optional[Connectivity] = None,
mapping : Optional[Mapping] = None,
logical_domain : Optional[Domain] = None):
"""
Interiors or connectivity must be given. When the mapping is given
then logical_domain must be specified as well.
Parameters
----------
name : str
Name of the domain
interiors : Iterable[InteriorDomain], InteriorDomain or None, optional
Interior domains
boundaries : Iterable[Boundary], Boundary or None, optional
The boundaries of the domain
dim : int, optional
Dimension of the space of the domain
connectivity : Connectivity or None, optional
Connectivity object with the interfaces of the domain
mapping : Mapping or None, optional
Maps the logical domain to the physical domain
logical_domain : Domain or None, optional
Logical domain that is mapped to the physical domain
"""
# ...
if not isinstance(name, str):
raise TypeError('> name must be a string')
# ...
# ...
if ( ( interiors is None ) and ( connectivity is None ) and ( dim is None) ):
raise ValueError('> either interiors or connectivity must be given')
# ...
# ...
if not( interiors is None ):
if not isinstance( interiors, (*iterable_types, InteriorDomain)):
raise TypeError('> Expecting an iterable or a InteriorDomain')
if isinstance( interiors, InteriorDomain ):
interiors = [interiors]
else:
new_interiors = []
for i in interiors:
if isinstance(i , iterable_types):
new_interiors += list(i)
else:
new_interiors.append(i)
interiors = new_interiors
if not all([isinstance(i, InteriorDomain) for i in interiors]):
raise TypeError('> all interiors must be of type InteriorDomain')
interiors = Tuple(*interiors)
# ...
if not( boundaries is None ):
if not isinstance( boundaries, (*iterable_types, Boundary)):
raise TypeError('> Expecting an iterable or a Boundary')
if isinstance( boundaries, Boundary ):
boundaries = [boundaries]
else:
if not all([isinstance(i, Boundary) for i in boundaries]):
raise TypeError('> all boundaries must be of type Boundary')
else:
boundaries = []
boundaries = Tuple(*boundaries)
if not( connectivity is None ):
if not isinstance( connectivity, Connectivity ):
raise TypeError('> Expecting a Connectivity')
# TODO check that patches appearing in connectivity are in interiors
else:
connectivity = Connectivity()
# ...
if interiors is None and dim:
interiors = [InteriorDomain(name, dim=dim)]
if len(interiors) == 0 and dim is None:
raise TypeError('No interior domain found')
elif len(interiors) == 1:
dtype = interiors[0].dtype
dim = interiors[0].dim
interiors = Union(*interiors)
else:
dim = interiors[0].dim
interiors = Union(*interiors)
dtype = [i.dtype for i in interiors]
assert mapping is None and logical_domain is None or \
mapping is not None and logical_domain is not None
# ...
boundaries = Union(*boundaries)
obj = Basic.__new__(cls, name, interiors, boundaries, mapping)
obj._connectivity = connectivity
obj._corners = None
obj._dtype = dtype
obj._dim = dim
obj._logical_domain = logical_domain
return obj
@property
def name(self) -> str:
return self.args[0]
@property
def interior(self) -> TypeUnion[Union, InteriorDomain]:
"""Either a Union object containing the interiors or just the interior
domain if there is only one"""
return self.args[1]
@property
def boundary(self) -> TypeUnion[Union, Boundary]:
"""Either a Union object containing the boundaries or just a boundary
if there is only one"""
return self.args[2]
@property
def mapping(self) -> Optional[Mapping]:
"""The mapping that maps the logical domain to the physical domain"""
return self.args[3]
@property
def subdomains(self) -> tuple:
"""returns subdomains as tuple of Domains"""
if isinstance( self.interior, iterable_types):
subs = self.interior
else:
subs = [self.interior]
return tuple(subs)
@property
def mappings(self) -> OrderedDict:
return OrderedDict([(P.logical_domain, P.mapping)
for P in self.subdomains])
@property
def logical_domain(self) -> Domain:
"""The domain is the image of the logical_domain under the mapping"""
return self._logical_domain
@property
def connectivity(self) -> Connectivity:
"""Contains information about the interfaces"""
return self._connectivity
@property
def dim(self) -> int:
"""Dimension of the space"""
return self._dim
@property
def dtype(self) -> dict:
"""Dictionary containing information about domain"""
return self._dtype
@property
def interfaces(self) -> TypeUnion[Union, Interface, None]:
"""
Union of the interfaces
The Union constructor is applied to the interfaces. If there is only
one interface it returns the interface object and None if there is no
interface.
"""
return self.connectivity.interfaces
@property
def corners(self):
corners = getattr(self,'_corners', None)
if corners is None:
corners = self.get_shared_corners()
self._corners = corners
return corners
def __len__(self):
if isinstance(self.interior, InteriorDomain):
return 1
elif isinstance(self.interior, Union):
return len(self.interior)
@property
def interior_names(self) -> List[str]:
if isinstance(self.interior, InteriorDomain):
return [self.interior.name]
elif isinstance(self.interior, Union):
return [i.name for i in self.interior.args]
[docs]
def set_interfaces(self, *interfaces):
for i in interfaces:
self.connectivity[i.name] = i
def _sympystr(self, printer):
sstr = printer.doprint
return '{}'.format(sstr(self.name))
[docs]
def get_boundary(self, axis, ext):
"""
Return the domain boundary at the given extremity of the required axis.
Parameters
----------
axis : int | None
Index of the coordinate (0 <= axis < ndim) which has constant value at the boundary.
In 1D passing `axis=None` is accepted, in which case it is interpreted as 0.
ext : {-1, +1}
Extremity identifier:
* If -1, the boundary is at the minimum value of $x_{axis}$
* If +1, the boundary is at the maximum value of $x_{axis}$
Returns
-------
Boundary (from sympde.topology.basic)
The domain boundary of interest.
"""
if axis is None:
assert(self.interior.dim == 1)
axis = 0
if isinstance(self.boundary, Union):
x = [i for i in self.boundary.args if i.ext == ext and i.axis == axis]
if len(x) == 0:
raise ValueError(f'> could not find boundary with axis {axis} and ext {ext}')
return x[0]
elif isinstance(self.boundary, Boundary):
if self.boundary.axis == axis and self.boundary.ext == ext:
return self.boundary
raise ValueError(f'> could not find boundary with axis {axis} and ext {ext}')
[docs]
def get_interface(self, domain1, domain2):
interfaces = []
for i in self.interface:
if i.plus in [domain1, domain2]:
if i.minus in [domain1, domain2]:
interfaces.append(i)
if interfaces:
return Union(*interfaces)
raise ValueError('> could not find the interface of {} and {}'.format(domain1, domain2))
[docs]
def get_interior(self, name):
"""return interior by name."""
if isinstance(self.interior, Union):
x = [i for i in self.interior.args if i.name == name]
if len(x) == 0:
raise ValueError('> could not find interior {}'.format(name))
return x[0]
elif isinstance(self.interior, InteriorDomain):
if self.interior.name == name:
return self.interior
else:
return None
[docs]
def todict(self):
name = str(self.name)
dim = str(self.dim)
interior = self.interior.todict()
boundary = self.boundary.todict()
connectivity = self.connectivity.todict()
dtype = self.dtype
if dtype is None:
dtype = 'None'
d = {'name': name,
'dim': dim,
'dtype': dtype,
'interior': interior,
'boundary': boundary,
'connectivity': connectivity}
return dict(sorted(d.items()))
[docs]
def export( self, filename ):
yml = self.todict()
# Dump metadata to string in YAML file format
geo = yaml.dump( data = yml,
sort_keys = None)
# Create HDF5 file (in parallel mode if MPI communicator size > 1)
h5 = h5py.File( filename, mode='w' )
# Write geometry metadata as fixed-length array of ASCII characters
h5['topology.yml'] = np.array( geo, dtype='S' )
# Close HDF5 file
h5.close()
[docs]
@classmethod
def from_file(cls, filename):
"""
Read the "topology.yml" portion of an HDF5 geometry file and create a (mapped)
multipatch domain using the information therein.
Parameters
----------
filename : str
Name of the HDF5 geometry file to be read.
Returns
-------
Domain
Multipatch domain.
"""
# ... check extension of the file
_, ext = os.path.splitext(filename)
if ext != '.h5':
raise ValueError('> Only h5 files are supported')
# ...
from sympde.topology.mapping import Mapping
h5 = h5py.File( filename, mode='r' )
yml = yaml.load( h5['topology.yml'][()], Loader=yaml.SafeLoader )
domain_name = yml['name']
dim = int(yml['dim'])
dtype = yml['dtype']
d_interior = yml['interior']
d_boundary = yml['boundary']
d_connectivity = yml['connectivity']
if dtype == 'None':
dtype = None
assert dtype is not None
assert all(dtype)
if isinstance(d_interior, dict):
d_interior = [d_interior]
dtype = [dtype]
constructors = [globals()[dt['type']] for dt in dtype]
interiors = [cs(i['name'], **dt['parameters']) for cs,i,dt in zip(constructors, d_interior, dtype)]
mappings = [Mapping(I['mapping'], dim=dim) if I.get('mapping', "None") != "None" else None for I in d_interior]
domains = [mapping(i) if mapping else i for i,mapping in zip(interiors, mappings)]
patch_index = {I.name:ind for ind,I in enumerate(interiors)}
boundaries = []
for bd in d_boundary:
name = bd['patch']
axis = bd['axis']
ext = bd['ext']
i = patch_index[name]
bd = domains[i].get_boundary(axis=int(axis), ext=int(ext))
boundaries.append(bd)
connectivity = []
for _,(minus, plus, ornt) in d_connectivity.items():
minus_name = minus['patch']
minus_axis = int(minus['axis'])
minus_ext = int(minus['ext'])
minus_patch_i = patch_index[minus_name]
plus_name = plus['patch']
plus_axis = int(plus['axis'])
plus_ext = int(plus['ext'])
plus_patch_i = patch_index[plus_name]
interface = ((minus_patch_i, minus_axis, minus_ext),
( plus_patch_i, plus_axis, plus_ext), ornt)
connectivity.append(interface)
if len(domains) == 1:
return domains[0]
return Domain.join(domains, connectivity, domain_name)
[docs]
@classmethod
def join(cls, patches, connectivity, name):
"""Create a multipatch domain by joining patches in 2D or 3D.
Parameters
----------
patches : sequence of Domain
Atomic patches in the joined domain.
connectivity : sequence of tuple
Interface descriptions of the form `(minus, plus, orientation)`.
Each side is `(patch, axis, ext)`, where `patch` is a patch
object or its index in `patches` and `ext` is `-1` or `1`.
A 2D orientation is `-1` or `1`. A 3D orientation is a tuple
of three values, each equal to `-1` or `1`.
name : str
Name of the domain.
Returns
-------
Domain
Multipatch domain.
Notes
-----
The orientations are specified in the same manner as in GeoPDES, see e.g.
<https://github.com/rafavzqz/geopdes/blob/master/geopdes/doc/geo_specs_mp_v21.txt#L193-L237>
and
T. Dokken, E. Quak, V. Skytt. Requirements from Isogeometric Analysis for changes in product design ontologies, 2010.
Example
-------
.. code-block:: python
# list of patches (mapped domains)
Omega_0 = F0(A)
Omega_1 = F1(A)
Omega_2 = F2(A)
Omega_3 = F3(A)
patches = [Omega_0, Omega_1, Omega_2, Omega_3]
# integers representing the axes
axis_0 = 0
axis_1 = 1
axis_2 = 2
# integers representing the extremities: left (-1) or right (+1)
ext_0 = -1
ext_1 = +1
# A connectivity list in 2D
connectivity = [((Omega_0, axis_0, ext_0), (Omega_1, axis_0, ext_1), 1),
((Omega_1, axis_1, ext_0), (Omega_3, axis_1, ext_1), -1),
((Omega_0, axis_1, ext_0), (Omega_2, axis_1, ext_1), 1),
((Omega_2, axis_0, ext_0), (Omega_3, axis_0, ext_1), -1)]
# alternative option (passing interface patches by their indices in the patches list):
connectivity = [((0, axis_0, ext_0), (1, axis_0, ext_1), 1),
((1, axis_1, ext_0), (3, axis_1, ext_1), -1),
((0, axis_1, ext_0), (2, axis_1, ext_1), 1),
((2, axis_0, ext_0), (3, axis_0, ext_1), -1)]
# A connectivity list in 3D
connectivity = [((Omega_0, axis_0, ext_1), (Omega_1, axis_0, ext_0), ( 1, 1, 1)),
((Omega_0, axis_1, ext_1), (Omega_2, axis_1, ext_0), ( 1, -1, 1)),
((Omega_1, axis_1, ext_1), (Omega_3, axis_1, ext_0), (-1, 1, -1)),
((Omega_2, axis_0, ext_1), (Omega_3, axis_0, ext_0), (-1, 1, 1))]
# alternative option (passing interface patches by their indices in the patches list):
connectivity = [((0, axis_0, ext_1), (1, axis_0, ext_0), ( 1, 1, 1)),
((0, axis_1, ext_1), (2, axis_1, ext_0), ( 1, -1, 1)),
((1, axis_1, ext_1), (3, axis_1, ext_0), (-1, 1, -1)),
((2, axis_0, ext_1), (3, axis_0, ext_0), (-1, 1, 1))]
# the multi-patch domain
Omega = Domain.join(patches=patches, connectivity=connectivity, name='Omega')
"""
assert isinstance(patches, (tuple, list))
assert isinstance(connectivity, (tuple, list))
assert isinstance(name, str)
if len(patches) == 1:
# single patch domain: return the patch
assert len(connectivity) == 0
return patches[0]
assert all(p.dim==patches[0].dim for p in patches)
ldim = int(patches[0].dim)
patch_given_by_indices = (len(connectivity) > 0 and isinstance(connectivity[0][0][0], int))
from sympde.topology.mapping import MultiPatchMapping
# ... connectivity
interfaces = {}
boundaries = []
for cn in connectivity:
if patch_given_by_indices:
patch_minus = patches[cn[0][0]]
patch_plus = patches[cn[1][0]]
else:
patch_minus = cn[0][0]
patch_plus = cn[1][0]
bnd_minus = patch_minus.get_boundary(axis=cn[0][1], ext=cn[0][2])
bnd_plus = patch_plus.get_boundary(axis=cn[1][1], ext=cn[1][2])
# Check that orientation is provided in the correct format depending on the dimension
ornt = cn[2] if len(cn) == 3 else None
if ldim == 1:
assert ornt is None, 'ornt is not needed for 1D interfaces'
elif ldim == 2:
assert ornt in (-1, 1), 'ornt must be either -1 or 1 for 2D interfaces'
elif ldim == 3:
ornt = tuple(ornt)
assert len(ornt) == 3, 'ornt must be a tuple of length 3 for 3D interfaces'
assert all(o in (-1, 1) for o in ornt), 'each element of ornt must be either -1 or 1 for 3D interfaces'
else:
raise ValueError(f'Unsupported dimension: {ldim}')
# Create a new Interface object using the join method of the Boundary objects
interface = bnd_minus.join(bnd_plus, ornt=ornt)
if interface.name in interfaces:
interface = bnd_plus.join(bnd_minus, ornt=ornt)
interfaces[interface.name] = interface
boundaries.append(bnd_minus)
boundaries.append(bnd_plus)
connectivity = Connectivity()
for k,v in interfaces.items():
connectivity[k] = v
# ... boundary
boundaries = Union(*[b for p in patches for b in p.boundary]).complement(Union(*boundaries))
if boundaries is None:
boundaries = ()
else :
boundaries = boundaries.as_tuple()
# ... interiors
interiors = Union(*[p.interior for p in patches])
if all(e.mapping for e in interiors):
logical_interiors = Union(*[e.logical_domain for e in interiors])
logical_boundaries = [e.logical_domain for e in boundaries]
logical_connectivity = Connectivity()
for k,v in connectivity.items():
logical_connectivity[v.logical_domain.name] = v.logical_domain
mapping = MultiPatchMapping({e.logical_domain: e.mapping for e in interiors})
logical_domain = Domain(name,
interiors=logical_interiors,
boundaries=logical_boundaries,
connectivity=logical_connectivity)
else:
mapping = None
logical_domain = None
# ...
return Domain(name,
interiors=interiors,
boundaries=boundaries,
connectivity=connectivity,
mapping=mapping,
logical_domain=logical_domain)
[docs]
def get_shared_corners(self):
""" Compute the corners shared by multiple patches in 2D """
interfaces = self.interfaces
interfaces = (interfaces,) if isinstance(interfaces, Interface) else interfaces
directions = {i.plus:i.ornt for i in interfaces}
directions.update({i.minus:i.ornt for i in interfaces})
boundaries = {i.minus:i.plus for i in interfaces}
boundaries.update({value:key for key, value in boundaries.items()})
not_treated_corners = set([tuple(set((b, n))) for b in boundaries for n in b.adjacent_boundaries])
grouped_corners = []
while not_treated_corners:
corner = not_treated_corners.pop()
grouped_corners.append([corner])
if not ( corner[0] in boundaries and corner[1] in boundaries):
while corner[1] in boundaries:
bd1 = boundaries[corner[1]]
bd2 = bd1.domain.get_boundary(axis=corner[0].axis, ext=corner[0].ext)
corner = (bd1, bd2.rotate(directions[bd1]))
grouped_corners[-1].append(corner)
corner = grouped_corners[-1][0]
while corner[0] in boundaries:
bd2 = boundaries[corner[0]]
bd1 = bd2.domain.get_boundary(axis=corner[1].axis, ext=corner[1].ext)
corner = (bd1.rotate(directions[bd2]), bd2)
grouped_corners[-1].insert(0, corner)
else:
while corner[1] in boundaries:
bd1 = boundaries[corner[1]]
bd2 = bd1.domain.get_boundary(axis=corner[0].axis, ext=corner[0].ext)
corner = (bd1, bd2.rotate(directions[bd1]))
if corner == grouped_corners[-1][0]:
break
grouped_corners[-1].append(corner)
else:
corner = grouped_corners[-1][0]
while corner[0] in boundaries:
bd2 = boundaries[corner[0]]
bd1 = bd2.domain.get_boundary(axis=corner[1].axis, ext=corner[1].ext)
corner = (bd1.rotate(directions[bd2]), bd2)
grouped_corners[-1].insert(0, corner)
grouped_corners[-1] = tuple(tuple(set(c)) for c in grouped_corners[-1])
not_treated_corners = not_treated_corners.difference(grouped_corners[-1])
grouped_corners = set(tuple(grouped_corners))
grouped_corners = Union(*[CornerInterface(*[CornerBoundary(*e) for e in cs]) for cs in grouped_corners])
return grouped_corners
[docs]
def get_subdomain(self, names):
"""
Returns an individual patch or a Union of patches of a multipatch domain.
Parameters
----------
names : tuple of str or str
Names of the patches to join.
If a string is given, the corresponding patch will be returned.
If a tuple of strings is given, the Union of the corresponding subdomains will be returned.
Notes
-----
The subdomain is returned as it was before being joined, which means that its boundary includes the
boundaries that are part of an interface in the multipatch domain.
"""
if names == ():
return None
if isinstance(names, str):
names = (names,)
assert names[0] in self.interior_names
elif isinstance(names, tuple):
assert all(isinstance(name, str) for name in names)
assert len(set(names)) == len(names)
assert all(name in self.interior_names or name == self.name for name in names)
# Check trivial case of single patch domain
if isinstance(self.interior, InteriorDomain):
assert names[0] == self.interior.name
return self
# If all patches are joined we get the full domain
# Same if the full domain is part of the union
if len(names) == len(self.interior_names) or self.name in names:
return self
# Build dictionary of interiors accessed by names
interior_dict = {i.name: i for i in self.interior.as_tuple()}
# Build dictionary of boundaries
if self.boundary is not None:
if isinstance(self.boundary, Union):
boundary_dict = {(b.domain.name, b.axis, b.ext): b for b in self.boundary.as_tuple()}
else:
b = self.boundary
boundary_dict = {(b.domain.name, b.axis, b.ext): b}
else:
boundary_dict = {}
# Build dictionary of interfaces
if self.interfaces is not None:
if isinstance(self.interfaces, Union):
interfaces_dict = {(i.minus.domain.name, i.plus.domain.name): i for i in self.interfaces.as_tuple()}
elif isinstance(self.interfaces, Interface):
i = self.interfaces
interfaces_dict = {(i.minus.domain.name, i.plus.domain.name): i}
else:
interfaces_dict = {}
interfaces = []
for name in names:
if name == self.name:
return self
interior = interior_dict[name]
boundaries = [boundary_dict.get((name, axis, ext)) for axis in range(self.dim) for ext in [-1, 1]]
boundaries = [b for b in boundaries if b is not None]
# Extract boundaries and interfaces from interfaces_dict
for other_name in self.interior_names:
if other_name != name:
i_minus = interfaces_dict.pop((name, other_name), None)
i_plus = interfaces_dict.pop((other_name, name), None)
if other_name not in names:
if i_minus is not None:
boundaries.append(i_minus.minus)
if i_plus is not None:
boundaries.append(i_plus.plus)
else:
if i_plus is not None:
interfaces.append((i_plus.name, i_plus))
if i_minus is not None:
interfaces.append((i_minus.name, i_minus))
# Create domain with name, interior and boundaries
new_domain = Domain(name=name, interiors=interior, boundaries=boundaries,
mapping=interior.mapping, logical_domain=interior.logical_domain)
try:
previous_domain = Domain.join([previous_domain,new_domain], [], name=f"{previous_domain.name}|{new_domain.name}")
except NameError:
previous_domain = new_domain
# Add interfaces
joined_domain = previous_domain
for k,v in interfaces:
joined_domain.connectivity[k] = v
return joined_domain
#==============================================================================
[docs]
class PeriodicDomain(BasicDomain):
def __init__(self, domain, periods):
assert isinstance(domain, Domain)
self._domain = domain
self._periods = tuple(periods)
boundary_dict = domain.boundary.todict()
names = []
for bd in boundary_dict:
if periods[int(bd['axis'])] == True:
names += [bd['name']]
boundary = [bd for bd in domain.boundary.args if bd.name not in names]
if len(boundary)>1:
self._boundary = Union(*boundary)
else:
self._boundary = None
@property
def domain(self):
return self._domain
@property
def periods(self):
return self._periods
@property
def boundary(self):
return self._boundary
@property
def dim(self):
return self.domain.dim
@property
def coordinates(self):
return self.domain.coordinates
def __hash__(self):
return hash((self._domain, self._periods))
#==============================================================================
[docs]
class NCubeInterior(InteriorDomain):
def __new__(cls, name, dim=None, dtype=None, min_coords=None, max_coords=None,
mapping=None, logical_domain=None):
obj = InteriorDomain.__new__(cls, name, dim=dim, dtype=dtype,
mapping=mapping, logical_domain=logical_domain)
obj._min_coords = min_coords
obj._max_coords = max_coords
boundaries = []
i = 1
for axis in range(dim):
for ext in [-1, 1]:
bnd_name = r'\Gamma_{}'.format(i)
bd_logical_domain = logical_domain
if bd_logical_domain:
bd_logical_domain = bd_logical_domain.get_boundary(axis=axis, ext=ext)
Gamma = Boundary(bnd_name, obj, axis=axis, ext=ext, mapping=mapping, logical_domain=bd_logical_domain)
boundaries += [Gamma]
i += 1
obj._boundary = Union(*boundaries)
return obj
@property
def min_coords(self):
return self._min_coords
@property
def max_coords(self):
return self._max_coords
@property
def boundary(self):
return self._boundary
def __hash__(self):
return hash((self.args, self.min_coords, self.max_coords))
[docs]
def get_boundary(self, axis=None, ext=None):
"""return boundary by (axis, ext)."""
# ...
assert(not( ext is None ))
if axis is None:
assert(self.dim == 1)
axis = 0
if isinstance(self.boundary, Union):
x = [i for i in self.boundary.args if i.ext == ext and i.axis==axis]
if x:return x[0]
raise ValueError('> could not find boundary with axis {} and ext {}'.format(axis, ext))
#==============================================================================
# Ncube's properties (in addition to Domain's properties):
# . min_coords (default value is tuple of zeros)
# . max_coords (default value is tuple of ones)
#
[docs]
class NCube(Domain):
def __new__(cls, name, dim, min_coords, max_coords):
assert isinstance(name, str)
assert isinstance(dim, (int, Integer))
assert isinstance(min_coords, iterable_types[:-1])
assert isinstance(max_coords, iterable_types[:-1])
if not name:
raise ValueError("Name must be provided")
if dim < 1:
raise ValueError("Number of dimensions must be at least 1")
if not (dim == len(min_coords) == len(max_coords)):
raise ValueError("Input arguments must have 'dim' components")
if not all(xmin < xmax for xmin, xmax in zip(min_coords, max_coords)):
raise ValueError("Min coordinates must be smaller than max")
coord_names = 'x1:{}'.format(dim + 1)
coordinates = symbols(coord_names, real=True)
# Choose which type to use:
# a) if dim <= 3, use Line, Square or Cube;
# b) if dim <= 4, use a generic 'NCube' type.
#
# Moreover, store all initialization parameters in a 'dtype' dictionary.
# This dictionary will be written to file when exporting the geometry,
# and it must contain all information necessary for building a new object
# by calling the appropriate constructor:
#
# cls = globals()[dtype['type']]
# domain = cls(name, **dtype['parameters'])
#
min_coords = tuple(float(i) for i in min_coords)
max_coords = tuple(float(i) for i in max_coords)
if dim == 1:
cls = Line
dtype = {'type': 'Line',
'parameters': {'bounds': [min_coords[0], max_coords[0]]}}
elif dim == 2:
cls = Square
dtype = {'type': 'Square',
'parameters': {'bounds1': [min_coords[0], max_coords[0]],
'bounds2': [min_coords[1], max_coords[1]]}}
elif dim == 3:
cls = Cube
dtype = {'type': 'Cube',
'parameters': {'bounds1': [min_coords[0], max_coords[0]],
'bounds2': [min_coords[1], max_coords[1]],
'bounds3': [min_coords[2], max_coords[2]]}}
else:
dtype = {'type': 'NCube',
'parameters': {'dim' : dim,
'min_coords': [*min_coords],
'max_coords': [*max_coords]}}
interior = NCubeInterior(name, dim=dim, dtype=dtype, min_coords=tuple(min_coords), max_coords=tuple(max_coords))
# Create instance of given type
obj = super().__new__(cls, name, interiors=[interior], boundaries=interior.boundary)
# Store attributes in object
obj._coordinates = tuple(coordinates)
# Return object
return obj
[docs]
@classmethod
def from_file(cls, filename):
msg = "Class method 'from_file' must be called on 'Domain' base class"
raise TypeError(msg)
@property
def min_coords(self):
return self.interior.min_coords
@property
def max_coords(self):
return self.interior.max_coords
#==============================================================================
[docs]
class Line(NCube):
def __new__(cls, name='Line', bounds=(0, 1)):
dim = 1
min_coords = (bounds[0],)
max_coords = (bounds[1],)
return super().__new__(cls, name, dim, min_coords, max_coords)
@property
def bounds(self):
return (self.min_coords[0], self.max_coords[0])
#==============================================================================
[docs]
class Square(NCube):
def __new__(cls, name='Square', bounds1=(0, 1), bounds2=(0, 1)):
dim = 2
min_coords = (bounds1[0], bounds2[0])
max_coords = (bounds1[1], bounds2[1])
return super().__new__(cls, name, dim, min_coords, max_coords)
@property
def bounds1(self):
return (self.min_coords[0], self.max_coords[0])
@property
def bounds2(self):
return (self.min_coords[1], self.max_coords[1])
#==============================================================================
[docs]
class Cube(NCube):
def __new__(cls, name='Cube', bounds1=(0, 1), bounds2=(0, 1), bounds3=(0, 1)):
dim = 3
min_coords = (bounds1[0], bounds2[0], bounds3[0])
max_coords = (bounds1[1], bounds2[1], bounds3[1])
return super().__new__(cls, name, dim, min_coords, max_coords)
@property
def bounds1(self):
return (self.min_coords[0], self.max_coords[0])
@property
def bounds2(self):
return (self.min_coords[1], self.max_coords[1])
@property
def bounds3(self):
return (self.min_coords[2], self.max_coords[2])
#==============================================================================
[docs]
class BoundaryVector(IndexedBase):
is_commutative = False
[docs]
class NormalVector(BoundaryVector):
pass
[docs]
class MinusNormalVector(NormalVector):
pass
[docs]
class PlusNormalVector(NormalVector):
pass
[docs]
class TangentVector(BoundaryVector):
pass
#==============================================================================
[docs]
class ElementDomain(with_metaclass(Singleton, Basic)):
pass
#==============================================================================
[docs]
class BasicArea(AtomicExpr):
def __new__(cls, domain):
if not isinstance(domain, (BasicDomain, ElementDomain)):
raise TypeError('expecting a BasicDomain or ElementDomain')
return Basic.__new__(cls, domain)
@property
def domain(self):
return self.args[0]
[docs]
class DomainArea(BasicArea):
pass
[docs]
class ElementArea(BasicArea):
pass
#==============================================================================
[docs]
class BasicGeometryOperator(CalculusFunction):
def __getitem__(self, indices, **kw_args):
if is_sequence(indices):
# Special case needed because M[*my_tuple] is a syntax error.
return Indexed(self, *indices, **kw_args)
else:
return Indexed(self, indices, **kw_args)
#==============================================================================
[docs]
class Area(BasicGeometryOperator):
def __new__(cls, *args, **options):
# (Try to) sympify args first
if options.pop('evaluate', True):
r = cls.eval(*args)
else:
r = None
if r is None:
return Basic.__new__(cls, *args, **options)
else:
return r
[docs]
@classmethod
def eval(cls, *args):
"""."""
if not args:
return
if not len(args) == 1:
raise ValueError('Expecting one argument')
expr = args[0]
if isinstance(expr, Union):
return Add(*[cls.eval(a) for a in expr.args])
elif isinstance(expr, ElementDomain):
return ElementArea(expr)
# elif isinstance(expr, InteriorDomain):
# return DomainArea(expr)
return cls(expr, evaluate=False)
#==============================================================================
[docs]
def split(domain, value):
if domain.dtype['type'] == 'Line':
assert(isinstance(value, (int, float)))
# TODO assert value <- bounds
bounds = domain.interior.bounds
# ... left
bounds = (bounds[0], value)
I_left = Line(name='{name}_l'.format(name=domain.name),
bounds=bounds)
# ...
# ... right
bounds = (value, bounds[1])
I_right = Line(name='{name}_r'.format(name=domain.name),
bounds=bounds)
# ...
# ... interiors
interiors = [I_left.interior, I_right.interior]
# ...
# ... external boundaries
bnd_left = [b for b in I_left.boundary.as_tuple() if b.ext == -1]
bnd_left = bnd_left[0]
bnd_right = [b for b in I_right.boundary.as_tuple() if b.ext == 1]
bnd_right = bnd_right[0]
boundaries = [bnd_left, bnd_right]
# ...
# ... connectivity: internal interfaces
int_left = [b for b in I_left.boundary.as_tuple() if b.ext == 1]
int_left = int_left[0]
int_right = [b for b in I_right.boundary.as_tuple() if b.ext == -1]
int_right = int_right[0]
connectivity = Connectivity()
connectivity['I'] = (int_left, int_right)
# ...
return Domain(domain.name,
interiors=interiors,
boundaries=boundaries,
connectivity=connectivity)
else:
raise NotImplementedError('TODO')