Source code for ddg.conversion.arrays

import numpy as np

import ddg.conversion.nets.geometry.quadrics
import ddg.datastructures.halfedge.get as get
import ddg.datastructures.nets as nets
import ddg.datastructures.nets.conversion
from ddg._error_messages import should_never_happen
from ddg.conversion.nets.geometry.spheres import sphere_to_smooth_net


[docs]def from_discrete_net(net, /): """Compute all points and faces of a discrete net. Parameters ---------- net : ddg.datastructures.nets.net.DiscreteNet A net with `net.domain.bounded == True`. Returns ------- tuple of numpy.ndarray and numpy.ndarray The first array is of shape `(n, d)` and comprises of the `n` points in R^d of the net. The second array is of shape `(m, k)` and comprises of the `m` faces of `net.domain.face_data`. Note that `k` isn't necessarily equal to 3. See Also -------- ddg.conversion.arrays.from_discrete_curve """ if not net.domain.bounded: raise ValueError(f"{net.domain.bounded = } must be True") return np.array(list(net[net.domain])), np.array(list(net.domain.face_data))
[docs]def from_discrete_curve(curve, /): """Compute all points of a discrete curve. Parameters ---------- curve : ddg.datastructures.nets.net.DiscreteCurve A curve with `curve.domain.bounded == True`. Returns ------- tuple of numpy.ndarray and bool The array is of shape `(n, d)` and comprises of the `n` points in R^d of the curve. The bool is equal to `curve.domain.periodic`. See Also -------- ddg.conversion.arrays.from_discrete_net """ if not curve.domain.bounded: raise ValueError(f"{curve.domain.bounded = } must be True") return np.array(list(curve[curve.domain])), curve.domain.periodic
[docs]def from_empty_net(_, /): """Convert an empty net to an empty mesh. Parameters ---------- _ : ddg.datastructures.nets.net.EmptyNet Returns ------- tuple of () and () """ return (), ()
[docs]def from_point_net(net, /): """Convert a point net to a mesh of one point and no faces. Parameters ---------- net : ddg.datastructures.nets.net.PointNet Returns ------- tuple of numpy.ndarray of shape ((1, n)) and () The array contains the value of `net()`, which is a point in R^n. """ return np.array(net())[None, :], ()
def _from_net(net): if isinstance(net, nets.DiscreteCurve): return from_discrete_curve(net) elif isinstance(net, nets.DiscreteNet): return from_discrete_net(net) elif isinstance(net, nets.EmptyNet): return from_empty_net(net) elif isinstance(net, nets.PointNet): return from_point_net(net) else: raise Exception(should_never_happen)
[docs]def from_net_collection(net_collection, /): """Convert a net collection to a list of curves and meshes. Parameters ---------- nets : ddg.datastructures.nets.net.NetCollection Every net in the collection has to be a DiscreteCurve or DiscreteNet with bounded domain or a PointNet or an EmptyNet. Returns ------- list of curves and meshes A curve is a pair of points and periodicity. A mesh is a pair of points and faces. """ # Problem 1 (if we want to return *one* object): We may get multiple curves # *and* meshes, but the representation as points and periodicity can only # handle connected curves. # # Problem 2 (if we want to return *one* object): We may get curves *and* # meshes. If that happens, we could represent the curves as meshes and # merge everything into one mesh. Is that reasonable? # # Problem 3: This function could easily be generalised to operate on # sequences or iterables. Should we do that? def convertible(net): if isinstance(net, nets.DiscreteNet): if net.domain.bounded: return net else: return "The net is a DiscreteNet, but its domain isn't bounded" elif isinstance(net, (nets.EmptyNet, nets.PointNet)): return net else: return ( f"The net's type is {type(net)}, so it is neither a DiscreteCurve " "or DiscreteNet with a bounded domain, an EmptyNet or a PointNet" ) nets_or_errors = list(map(convertible, net_collection.nets)) bad = [ (i, error) for i, error in enumerate(nets_or_errors) if isinstance(error, str) ] if bad: raise ValueError( "Some nets in the collection can't be converted to arrays:\n" + "\n".join(f"Net {i}: {error}" for i, error in bad) ) else: good = list(filter(lambda n: isinstance(n, nets.Net), nets_or_errors)) return list(map(_from_net, good))
def _he_verts(surface, coordinate_attribute): return np.array([getattr(v, coordinate_attribute) for v in surface.verts]) def _isolated_he_edges(surface): edges = [ e for e in get.single_edges(surface) if e.face is None and e.opp.face is None ] return np.array([(e.head.index, e.pre.head.index) for e in edges]) def _he_faces(surface): return [tuple(v.index for v in get.get_vertices(f)) for f in surface.faces]
[docs]def from_half_edge_surface(surface, coordinate_attribute="co", /): """Get the points, isolated edges and faces of a half edge surface. Parameters ---------- surface : ddg.datastructures.halfedge.Surface coordinate_attribute : str (default="co") Returns ------- tuple of numpy.ndarray, numpy.ndarray and list The first array is of shape (n, d), where n is the number of points lying in R^d. The second array is of shape (k, 2), where k is the number of edges which don't belong to a face. The list contains tuples of integers. Each tuple corresponds to a face and each integer is the index of a vertex of that face. """ return ( _he_verts(surface, coordinate_attribute), _isolated_he_edges(surface), _he_faces(surface), )
[docs]def from_1d_subspace(line, length, /): """Sample a 1-dimensional subspace. Parameters ---------- line : ddg.geometry.subspaces.Subspace A subspace with `line.dimension == 1`. length : float Must be greater than or equal to 0.0. Returns ------- (a, b), False `a` and `b` are of type numpy.ndarray. `b - a` has length `length` and `b - a / 2` is the closest point of the line to the origin. Raises ------ ValueError If `line.dimension != 1` or `length < 0.0`. """ if line.dimension != 1: raise ValueError(f"{line.dimension = } must be 1.") if length < 0: raise ValueError(f"{length = } must be non-negative.") match line.affine_point_and_directions: case p, [d]: d_ = length / 2 * d return (p - d_, p + d_), False case _: raise Exception(should_never_happen)
[docs]def from_2d_subspace(plane, side_length_1, side_length_2, /): """Convert a 2-dimensional subspace to a mesh. The mesh is given as two triangles:: d c ┌─────┐ ┐ │ /│ │ │ / │ │ │ x │ │ side_length_2 │ / │ │ │/ │ │ └─────┘ ┘ a b └─────┘ side_length_1 such that `x` is the closest point of the subspace to the origin. Parameters ---------- plane : ddg.geometry.subspaces.Subspace A subspace with `subspace.dimension == 2`. side_length_1 : float Must be greater than or equal to 0.0. side_length_2 : float Must be greater than or equal to 0.0. Returns ------- points, faces : tuple of numpy.ndarray and tuple The first entry points has shape (4, 3) and comprises of a, b, c, d. The second entry faces comprises of two tuples of 3 integers encoding the two triangles. Raises ------ ValueError If `plane.dimension != 2` or `side_length_1 < 0.0` or `side_length_2 < 0.0`. """ if plane.dimension != 2: raise ValueError(f"{plane.dimension = } must be 2.") if side_length_1 < 0 or side_length_2 < 0: raise ValueError( f"{side_length_1 = } and {side_length_2 = } must be non-negative." ) match plane.affine_point_and_directions: case p, [u, v]: mu = side_length_1 / 2 * u mv = side_length_2 / 2 * v a = p - mu - mv b = p + mu - mv c = p + mu + mv d = p - mu + mv points = np.row_stack((a, b, c, d)) faces = (0, 1, 2), (0, 2, 3) return points, faces case _: raise Exception(should_never_happen)
[docs]def from_quadric(quadric, /, *num_step): """Convert a quadric to a curve or a mesh or a list thereof. Every convertible quadric has a parametrization, which is sampled. These parametrizations are implementation details, but the following guarantees hold: - every parametrization is defined on a box I_1 x ... x I_d, where d is the dimension of the quadric and I_1, ..., I_d are possibly unbounded intervals - the k-th `num_step` argument determines how I_k is sampled - the first entry `num` of the k-th `num_step` determines the number of samples of I_k - if I_k is unbounded, the second entry `step` of the k-th `num_step` determines the step size between the samples of I_k Consequently, - increasing the `num` arguments increases the number of points and additionally the number of faces if the quadric is 2-dimensional. - if I_k isn't bounded, decreasing the respective `step` results in a finer curve or mesh. Parameters ---------- quadric : ddg.geometry.quadrics.Quadric *num_step : tuple of an int and a float The length of `num_step` must be 0 if `quadric.dimension` is -1 or 0 and otherwise must be equal to `quadric.dimension`. Returns ------- curve or mesh or a list of curves and meshes A curve is an tuple of points of type numpy.ndarray and the periodicity of type bool. A mesh is an tuple of points of type numpy.ndarray and the faces of type numpy.ndarray. Raises ------ ValueError If the quadric is a (possibly empty) set of points and `num_step` is given. """ # We intentionally don't allow for other ways to sample the # parametrization, because the choice of parametrization is # arbitrary and therefore an implementation detail. For example, it # doesn't make sense to let the user specify samples in the domain # such as anchor values. # # The parametrization isn't even always obvious, e.g. the paraboloid # may be parametrized by # (u, v) -> (u, v, u^2 + v^2) # and also as a surface of revolution and a circle can be # parametrized in two directions. So if the user wants more control, # they should just directly sample a parametrization of their # choice. # TODO: Rewrite this using vectorised parametrization and domains # once they are available. net_or_collection = ddg.conversion.nets.geometry.quadrics.quadric_to_smooth_net( quadric ) if quadric.dimension in {-1, 0} and num_step: raise ValueError( "Since quadric.dimension is -1 or 0, no num_step may be given." ) elif quadric.dimension == 1: ((num, step),) = num_step # sampling is not allowed to be [(step, num, "c")], otherwise # sample_interval raises a ValueError for some reason. sampling = (step, num, "c") else: sampling = [(step, num, "c") for num, step in num_step] if isinstance(net_or_collection, nets.SmoothNet): discrete_net = nets.conversion.sample_smooth_net(net_or_collection, sampling) return _from_net(discrete_net) elif isinstance(net_or_collection, (nets.EmptyNet, nets.PointNet)): return _from_net(net_or_collection) elif isinstance(net_or_collection, nets.NetCollection): discrete_net_collection = nets.conversion.sample_smooth_net( net_or_collection, sampling ) return list(map(_from_net, discrete_net_collection)) else: raise Exception(should_never_happen)
[docs]def from_sphere(sphere, num, /): """Convert a sphere to a curve or a mesh. Circles are converted to curves and 2-dimensional spheres are converted to meshes. Parameters ---------- sphere : ddg.geometry.spheres.Sphere num : int Increasing this number increases the number of samples in each direction of the sphere parametrization. Returns ------- curve or mesh A curve is an tuple of points of type numpy.ndarray and the periodicity of type bool. A mesh is an tuple of points of type numpy.ndarray and the faces of type numpy.ndarray. """ smooth_net = sphere_to_smooth_net(sphere) discrete_net = ddg.sample_smooth_net(smooth_net, (num, "t")) if isinstance(discrete_net, nets.DiscreteCurve): return from_discrete_curve(discrete_net) else: return from_discrete_net(discrete_net)
[docs]def from_indexed_face_set(ifs, coordinate_key="co", /): """Get the points, and faces of an indexed face set. Parameters ---------- ifs : ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet coordinate_attribute : str (default="co") Returns ------- tuple of numpy.ndarray and list """ # TODO: Decide if this function is even necessary. # It shouldn't be necessary. After all, it duplicates the most basic # functionality that one would expect from an indexed face set. return ifs.vertex_attributes[coordinate_key], ifs.face_list()