Source code for simudo.mesh.interval1dtag


from functools import partial

import numpy as np
from cached_property import cached_property

import dolfin

from .product2d import Product2DMesh
from .mesh1d import make_1d_mesh_from_points

__all__ = [
    'clip_intervals',
    'Interval', 'CInterval', 'GeometricallyExpandingMeshInterval',
    'Product2DMeshMixin', 'Interval1DMesh', 'Interval1DMeshMixin',
    'MinimumCoordinateDistanceMixin',
    'BaseInterval1DTag', 'Interval1DTag']

[docs] def clip_intervals(x0, x1, intervals): ''' in-place modifies intervals ''' for I in intervals: I.x0 = max(I.x0, x0) I.x1 = min(I.x1, x1) new = [I for I in intervals if I.x0 != I.x1] # remove degenerate intervals intervals[:] = new return intervals
[docs] class Interval(object): ''' Represents a single interval, possibly with tags. ''' def __init__(self, x0, x1, tags=()): self.x0 = x0 self.x1 = x1 self.tags = frozenset(tags) def __repr__(self): return "{}(x0={}, x1={}{})".format(self.__class__.__name__, self.x0, self.x1, self._supplementary_repr()) def _supplementary_repr(self): return ', tags={{{}}}'.format(', '.join(repr(t) for t in self.tags))
[docs] class CInterval(Interval): ''' Represents an interval with constant edge length (in the x direction). Note: you do not need to derive from this class to implement a custom edge length. Instead, you can subclass from :py:class:`Interval` and implement a custom :code:`local_edge_length` method. ''' def __init__(self, x0, x1, tags=(), edge_length=np.inf): self.edge_length = edge_length super().__init__(x0, x1, tags)
[docs] def local_edge_length(self, x): return self.edge_length
def _supplementary_repr(self): return '{}, edge_length={!r}'.format( super()._supplementary_repr(), self.edge_length)
[docs] class GeometricallyExpandingMeshInterval(Interval): """ Interval of 1D mesh where the edge lengths start at ``start`` and increase geometrically away from the region bounds. See ``doc/mesh.lyx`` for more information. (Local edge length: minimum distance between two mesh vertices.) Parameters ---------- bounds: tuple Tuple defining the region. edge_length_start: float or tuple The initial edge length at the bounds. To have different initial edge lengths at the two ends of the region, pass in a tuple. edge_length_expansion_factor: float or tuple Geometric expansion factor. Pass in a tuple to use different factors on the two ends of the region. """ def __init__(self, *, edge_length_start, edge_length_expansion_factor, **kw): ones = np.ones(2) self.edge_length_start = edge_length_start * ones self.edge_length_expansion_factor = edge_length_expansion_factor * ones super().__init__(**kw)
[docs] def local_edge_length(self, x): start = self.edge_length_start factor = self.edge_length_expansion_factor # see doc/mesh.lyx # mesh_density = x * (factor - 1) + start x0 = x - self.x0 x1 = self.x1 - x return np.where( (x0 >= 0) & (x1 >= 0), np.minimum( x0 * (factor[0] - 1) + start[0], x1 * (factor[1] - 1) + start[1], ), np.inf, )
[docs] class BaseInterval1DTag(object): ''' Class that turns a bunch of arbitrary overlapping intervals into a mesh. ''' first_cell_value = 1 def __init__(self, intervals): self.intervals = intervals @cached_property def subdomains(self): return self.intervals_to_subdomains(intervals=self.intervals) @cached_property def coordinates(self): sub = self.subdomains return self.make_coordinates( subdomain_to_intervals=sub['subdomain_to_intervals'], endpoints=sub['endpoints']) @cached_property def tag_to_cell_values(self): ''' Mapping from a tag to a set of cell values (e.g. values inside `self.product2d_mesh.cell_function`). ''' return self.subdomains['tag_to_cell_values']
[docs] def intervals_to_subdomains(self, intervals): all_tags = set() all_tags_update = all_tags.update endpoints = set() endpoints_add = endpoints.add for interval in intervals: endpoints_add(interval.x0) endpoints_add(interval.x1) all_tags_update(interval.tags) del all_tags_update, endpoints_add endpoints = list(endpoints) endpoints.sort() all_tags = list(all_tags) all_tags.sort() endpoint_to_index = {x: i for i, x in enumerate(endpoints)} # subdomains form a partition of the mesh space N = (len(endpoints)-1) # number of subdomains subdomain_to_tags = [set() for i in range(N)] subdomain_to_intervals = [set() for i in range(N)] for interval in intervals: i0 = endpoint_to_index[interval.x0] i1 = endpoint_to_index[interval.x1] for i in range(i0, i1): subdomain_to_intervals[i].add(interval) subdomain_to_tags[i].update(interval.tags) # freeze tags into tuples so they're hashable for i, tags in enumerate(subdomain_to_tags): subdomain_to_tags[i] = tuple(sorted(tags)) cell_values_start = self.first_cell_value tagset_to_cell_value = { ts: cv for cv, ts in enumerate( sorted(set(subdomain_to_tags)), cell_values_start)} cell_values_end = cell_values_start + len(tagset_to_cell_value) subdomain_to_cell_value = [ tagset_to_cell_value[ts] for ts in subdomain_to_tags] tag_to_cell_values = {tag: set() for tag in all_tags} for tagset, cell_value in tagset_to_cell_value.items(): for tag in tagset: tag_to_cell_values[tag].add(cell_value) # freeze cell value sets for k, v in tag_to_cell_values.items(): tag_to_cell_values[k] = frozenset(v) return dict(tag_to_cell_values=tag_to_cell_values, subdomain_to_cell_value=subdomain_to_cell_value, subdomain_to_intervals=subdomain_to_intervals, used_cell_values=range(cell_values_start, cell_values_end), endpoints=endpoints)
[docs] def make_coordinates(self, subdomain_to_intervals, endpoints): def local_edge_length_function(intervals, x): return min(interval.local_edge_length(x) for interval in intervals) coordinates = [] subdomain_to_coordinate_range = [] x1 = endpoints[0] last_coordinate_index = 0 for i, intervals in enumerate(subdomain_to_intervals): x0 = x1 x1 = endpoints[i+1] lcf = partial(local_edge_length_function, tuple(iv for iv in intervals if hasattr(iv, 'local_edge_length'))) coords = self.make_interval_coordinates(x0, x1, lcf) coordinates.extend(coords) prev_last_coordinate_index = last_coordinate_index last_coordinate_index = len(coordinates) subdomain_to_coordinate_range.append( range(prev_last_coordinate_index, last_coordinate_index)) coordinates.append(x1) return dict( coordinates=coordinates, subdomain_to_coordinate_range=subdomain_to_coordinate_range)
[docs] def make_interval_coordinates( self, x0, x1, local_edge_length_function): '''Note: this excludes second endpoint''' coords = [] x = x0 while True: coords.append(x) delta = local_edge_length_function(x) x += delta if x >= x1: break return coords
[docs] class Product2DMeshMixin(object): product2d_Ys = (0.0, 1.0) Product2DMesh = Product2DMesh @cached_property def product2d_mesh(self): ''' Use this to get a readily-constructed Product2D object. Note that an attribute `cell_function` has been added to it. ''' s = self.subdomains c = self.coordinates return self.make_product2d_mesh( coordinates=c['coordinates'], subdomain_to_coordinate_range=c['subdomain_to_coordinate_range'], subdomain_to_cell_value=s['subdomain_to_cell_value'], Ys=self.product2d_Ys)
[docs] def make_product2d_mesh(self, coordinates, subdomain_to_coordinate_range, subdomain_to_cell_value, Ys): pm = self.Product2DMesh(coordinates, Ys) mf = pm.cell_function = dolfin.MeshFunction("size_t", pm.mesh, 2) mfw = mf.array() for crange, cell_value in zip(subdomain_to_coordinate_range, subdomain_to_cell_value): for ix in crange: mfw[pm.cells_at_ix(ix)] = cell_value return pm
[docs] class Interval1DMesh(object): '''A true-1D (interval) dolfin mesh built from sorted x coordinates, with a cell function attached. Counterpart of :py:class:`Product2DMesh` for the true-1D route; cell ``ix`` spans ``Xs[ix]`` to ``Xs[ix+1]``. Created by Claude Fable 5.1 (Anthropic), 2026-09-04. ''' def __init__(self, Xs): self.Xs = np.asarray(Xs, dtype='double') @cached_property def mesh(self): return make_1d_mesh_from_points(self.Xs) @cached_property def nX(self): return len(self.Xs)
[docs] def cells_at_ix(self, ix): return range(ix, ix + 1)
[docs] class Interval1DMeshMixin(object): '''Build a true-1D interval mesh from the tagged intervals, tagging each cell with the cell value of the subdomain it lies in (same convention as :py:class:`Product2DMeshMixin`).''' Interval1DMesh = Interval1DMesh @cached_property def interval_1d_mesh(self): ''' Use this to get a readily-constructed :py:class:`Interval1DMesh` object. Note that an attribute `cell_function` has been added to it. ''' s = self.subdomains c = self.coordinates return self.make_interval_1d_mesh( coordinates=c['coordinates'], subdomain_to_coordinate_range=c['subdomain_to_coordinate_range'], subdomain_to_cell_value=s['subdomain_to_cell_value'])
[docs] def make_interval_1d_mesh(self, coordinates, subdomain_to_coordinate_range, subdomain_to_cell_value): im = self.Interval1DMesh(coordinates) mf = im.cell_function = dolfin.MeshFunction("size_t", im.mesh, 1) mfw = mf.array() for crange, cell_value in zip(subdomain_to_coordinate_range, subdomain_to_cell_value): for ix in crange: mfw[im.cells_at_ix(ix)] = cell_value return im
[docs] class MinimumCoordinateDistanceMixin(object): minimum_coordinate_distance = 1e-10
[docs] def make_interval_coordinates(self, x0, x1, *args, **kwargs): coords = super().make_interval_coordinates(x0, x1, *args, **kwargs) minimum_coordinate_distance = self.minimum_coordinate_distance coords = np.array(coords, dtype='object') keep = np.zeros(len(coords), dtype='bool') keep[0] = True min_x = x0 max_x = x1 - minimum_coordinate_distance for i, x in enumerate(coords): if x >= min_x: if x > max_x: break keep[i] = True min_x = x + minimum_coordinate_distance return list(coords[keep])
[docs] class Interval1DTag(Product2DMeshMixin, Interval1DMeshMixin, MinimumCoordinateDistanceMixin, BaseInterval1DTag): pass