.. _nets: Nets ==== Nets are one of our data structures. They represent maps from subdomains of :math:`\mathbb{R}^m` or :math:`\mathbb{Z}^m` into another space :math:`X`, e.g. parameterized curves or surfaces. In most cases we will consider maps to :math:`\mathbb{R}^3.` There are different types of nets that all inherit from the :py:class:`~ddg.datastructures.nets.net.Net` class, for example - :py:class:`~ddg.datastructures.nets.net.EmptyNet` - :py:class:`~ddg.datastructures.nets.net.PointNet` smooth nets as - :py:class:`~ddg.datastructures.nets.net.SmoothNet` - :py:class:`~ddg.datastructures.nets.net.SmoothCurve` or their discrete analogs - :py:class:`~ddg.datastructures.nets.net.DiscreteNet` - :py:class:`~ddg.datastructures.nets.net.DiscreteCurve` Further there is the :py:class:`~ddg.datastructures.nets.net.NetCollection` class that can handle collections of multible nets. .. contents:: Table of contents :local: :backlinks: none Smooth Nets ----------- Creating a Smooth Net ^^^^^^^^^^^^^^^^^^^^^ After :ref:`creating a domain `, we can begin constructing a net. For this we only need to define the desired function and pass both, the function and the domain, to the class :py:class:`~ddg.datastructures.nets.net.SmoothNet`. .. doctest:: >>> import ddg >>> import numpy as np >>> domain = ddg.nets.SmoothDomain([[-4, 4]]) >>> def net_fct(t): ... return np.array([t, 2 * t, 0]) ... >>> net = ddg.SmoothNet(net_fct, domain) >>> net(0) array([0, 0, 0]) >>> net(2) array([2, 4, 0]) .. image:: 01_SmoothNet.png :align: center :scale: 20 % .. note:: As opposed to a :py:class:`~ddg.datastructures.nets.net.DiscreteNet`, a :py:class:`~ddg.datastructures.nets.net.SmoothNet` does not store values that have been computed once. The function of a :py:class:`~ddg.datastructures.nets.net.SmoothNet` is evaluated anew each time the net is called with some point from the domain. A short hand to create a domain on the fly you can use the following convention: .. doctest:: >>> import ddg >>> import numpy as np >>> def net_fct(t): ... return np.array([t, 2 * t, 0]) ... >>> net = ddg.SmoothNet(net_fct, [[-4, 4]]) >>> net(0) array([0, 0, 0]) >>> net(2) array([2, 4, 0]) Note that similar to :py:class:`~ddg.datastructures.nets.domain.SmoothDomain`, :py:class:`~ddg.datastructures.nets.net.SmoothNet` expects a list of intervals when the domain is passed in this way. Similar to :py:class:`~ddg.datastructures.nets.domain.SmoothInterval` there is the class :py:class:`~ddg.datastructures.nets.net.SmoothCurve` which lifts this restriction. Further Examples ^^^^^^^^^^^^^^^^ Example: Paraboloid """"""""""""""""""" .. doctest:: >>> import ddg >>> domain = ddg.nets.SmoothDomain([[-4, 4], [-4, 4]]) >>> def paraboloid_fct(x, y): ... return (x, y, x**2 + y**2) ... >>> paraboloid_net = ddg.SmoothNet(paraboloid_fct, domain) .. image:: 02_Paraboloid.png :align: center :scale: 20 % Example: Helix """""""""""""" .. doctest:: >>> import ddg >>> import numpy as np >>> def helix_fct(t): ... return (np.cos(t), np.sin(t), t) ... >>> helix_net = ddg.SmoothNet(helix_fct, [[-2 * np.pi, 2 * np.pi]]) Example: Torus """""""""""""" .. doctest:: >>> import ddg >>> import numpy as np >>> def torus_fct(u, v): ... a = 0.2 ... b = 1 ... x1 = (a * np.cos(u) + b) * np.cos(v) ... x2 = (a * np.cos(u) + b) * np.sin(v) ... x3 = a * np.sin(u) ... return (x1, x2, x3) ... >>> torus_net = ddg.SmoothNet( ... torus_fct, [[0, 2 * np.pi, True], [0, 2 * np.pi, True]] ... ) .. image:: 04_Torus.png :align: center :scale: 20 % Discrete Nets ------------- When we want to visualize our net in Blender, we have to make sure that we pass discrete information to it. For this we use :py:class:`~ddg.datastructures.nets.net.DiscreteNet`. All in all, the process of creating a discrete net is not any different from creating a smooth one - we could just replace most instances of "Smooth" with "Discrete" - but we have to make sure that our domain is a subset of :math:`\mathbb{Z}^n`. Though it is still possible to evaluate the net at any given "real" point, only the integer points of the domain will be used further, e.g. for the visualization in Blender. .. _creating_a_discrete_net: Creating a Discrete Net ^^^^^^^^^^^^^^^^^^^^^^^ The process of creating a discrete net mirrors the smooth case, but we have to take care of mapping our discrete domain to not only valid, but also "reasonable" real points to achieve our desired result. Similar the smooth case the :py:class:`~ddg.datastructures.nets.domain.DiscreteDomain` can be created on the fly as in the following where we will create a discrete net of a circle with 10 points in the domain. (One can also :ref:`explicitly create discrete domains `.) .. doctest:: >>> import ddg >>> import numpy as np >>> def discrete_circle_fct(n): ... t = 2 * np.pi / 10 * n ... return np.array([np.cos(t), np.sin(t), 0]) ... >>> discrete_circle = ddg.DiscreteNet(discrete_circle_fct, [[0, 9, True]]) >>> discrete_circle(0) array([1., 0., 0.]) .. image:: 05_DiscreteCircle.png :align: center :scale: 20 % You might have noticed that we never reach the value :math:`t=2\pi` in the function above. This is by design as the net is periodic. .. warning:: As opposed to a :py:class:`~ddg.datastructures.nets.net.SmoothNet`, a :py:class:`~ddg.datastructures.nets.net.DiscreteNet` saves every value after it has been computed once. If you call a :py:class:`~ddg.datastructures.nets.net.DiscreteNet` with the same point from the domain a second time the saved value is returned. Further Examples ^^^^^^^^^^^^^^^^ The following examples will show you how you could create discrete nets corresponding to the examples given for smooth nets. Example: Paraboloid """"""""""""""""""" .. doctest:: >>> import ddg >>> def discrete_paraboloid_fct(x, y): ... x /= 2.5 ... y /= 2.5 ... return (x, y, x**2 + y**2) ... >>> discrete_paraboloid = ddg.DiscreteNet(discrete_paraboloid_fct, [[-10, 10]] * 2) .. image:: 06_DiscreteParaboloid.png :align: center :scale: 20 % Example: Helix """""""""""""" .. doctest:: >>> import ddg >>> import numpy as np >>> def discrete_helix_fct(t): ... t = -2 * np.pi + t ... return (np.cos(t), np.sin(t), t) ... >>> discrete_helix = ddg.DiscreteNet(discrete_helix_fct, [[0, 11]]) Example: Torus """""""""""""" .. doctest:: >>> import ddg >>> import numpy as np >>> def discrete_torus_fct(u, v): ... a = 0.2 ... b = 1 ... s = 2 * np.pi / 10 ... u = u * s ... v = v * s ... x1 = (a * np.cos(u) + b) * np.cos(v) ... x2 = (a * np.cos(u) + b) * np.sin(v) ... x3 = a * np.sin(u) ... return (x1, x2, x3) ... >>> discrete_torus = ddg.DiscreteNet( ... discrete_torus_fct, [[0, 9, True], [0, 9, True]] ... ) .. image:: 07_DiscreteTorus.png :align: center :scale: 20 % The process of converting smooth nets to discrete nets in this fashion is rather tedious. We will go over a different way to achieve this conversion later. Net Collections --------------- Sometimes an object can not be represented by a single net, but we want to consider multiple nets as one. as one. For this we have the :py:class:`~ddg.datastructures.nets.net.Netcollection` class. Net collections are exactly what the name describes: a collection of nets. .. doctest:: >>> import ddg >>> import numpy as np >>> def line_fct_1(t): ... return np.array([t, t, 0]) ... >>> def line_fct_2(t): ... return np.array([t, -t, 0]) ... >>> line1 = ddg.SmoothNet(line_fct_1, [[-4, 4]]) >>> line2 = ddg.SmoothNet(line_fct_2, [[-4, 4]]) >>> crossing_lines = ddg.NetCollection([line1, line2]) .. image:: 08_CrossingLinesCollection.png :align: center :scale: 20 % In the :py:mod:`~ddg.datastructures.nets.utils` module of nets we can find the definition of a :py:func:`~ddg.datastructures.nets.utils.applicable_to_netcollection` decorator. Any function decorated by it will always be able to be applied to both nets and net collections. Example: Double Helix ^^^^^^^^^^^^^^^^^^^^^ .. doctest:: >>> import ddg >>> import numpy as np >>> def helix_fct_1(t): ... return (np.cos(t), np.sin(t), t) ... >>> def helix_fct_2(t): ... return (np.cos(t + np.pi), np.sin(t + np.pi), t) ... >>> helix_net_1 = ddg.SmoothNet(helix_fct_1, [[-2 * np.pi, 2 * np.pi]]) >>> helix_net_2 = ddg.SmoothNet(helix_fct_2, [[-2 * np.pi, 2 * np.pi]]) >>> double_helix = ddg.NetCollection([helix_net_1, helix_net_2]) Example: Two-sheeted Hyperboloid ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. doctest:: >>> import ddg >>> from ddg.datastructures.nets.utils import compose >>> import numpy as np >>> def first_sheet_fct(u, v): ... return (np.sinh(u) * np.cos(v), np.sinh(u) * np.sin(v), np.cosh(u)) ... >>> def second_sheet_fct(u, v): ... return (np.sinh(u) * np.cos(v), np.sinh(u) * np.sin(v), -np.cosh(u)) ... >>> first_sheet = ddg.SmoothNet( ... first_sheet_fct, [[0, np.inf], [0, 2 * np.pi, True]] ... ) >>> second_sheet = ddg.SmoothNet( ... second_sheet_fct, [[0, np.inf], [0, 2 * np.pi, True]] ... ) >>> two_sheeted_hyperboloid = ddg.NetCollection([first_sheet, second_sheet]) >>> two_sheeted_hyperboloid = compose(np.diag([1, 2, 1]), two_sheeted_hyperboloid) .. image:: 10_TwoSheetedHyperboloidCollection.png :align: center :scale: 20 % Function Composition for Nets ----------------------------- We can compose nets with callables and numpy arrays using :py:func:`~ddg.datastructures.nets.utils.compose`. If `f` is a callable or a numpy array and `n` is a net, then `compose(f, n)` is a net which maps `x` to `f(n(x))` and has the same domain as `n`. Passing a numpy array ``a`` to :py:func:`~ddg.datastructures.nets.utils.compose` is equivalent to passing ``lambda x: a @ x``. .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet( ... lambda t: np.array([t, t**2, t**3]), [[-np.inf, np.inf]] ... ) >>> net(2) array([2, 4, 8]) >>> net = compose(np.diag([0.5, 0.5, 0.5]), net) >>> net(2) array([1., 2., 4.]) .. image:: 11_Transform.png :align: center :scale: 20 % Some common compositions such as :py:func:`~ddg.datastructures.nets.utils.embed`, :py:func:`~ddg.datastructures.nets.utils.homogenize` and :py:func:`~ddg.datastructures.nets.utils.dehomogenize` are included in the :py:mod:`ddg.datastructures.nets.utils` module. .. doctest:: >>> import ddg >>> import numpy as np >>> circle = ddg.SmoothNet( ... lambda t: np.array([np.cos(t), np.sin(t)]), [[0, np.pi, True]] ... ) >>> circle(0) array([1., 0.]) >>> embedded_circle = ddg.nets.utils.embed(circle) >>> embedded_circle(0) array([1., 0., 0.]) >>> homogenized_circle = ddg.nets.utils.homogenize(circle) >>> homogenized_circle(0) array([1., 0., 1.]) >>> dehomogenized_homogenized_circle = ddg.nets.utils.dehomogenize( ... homogenized_circle ... ) >>> dehomogenized_homogenized_circle(0) array([1., 0.]) Note that the second parameter `n` of `compose` must be a net. If it isn't a net, there is no need to retain a domain and we can write a new function .. doctest:: >>> def f(t): ... return circle(2 * t) ... >>> np.allclose(circle(np.pi), [-1, 0]) True >>> np.allclose(f(np.pi), [1, 0]) True Example: Torus Inverted in a Unit Sphere ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. doctest:: >>> import ddg >>> import numpy as np >>> def torus_fct(u, v): ... a = 0.2 ... b = 1 ... x1 = (a * np.cos(u) + b) * np.cos(v) ... x2 = (a * np.cos(u) + b) * np.sin(v) ... x3 = a * np.sin(u) ... return np.array((x1, x2, x3)) ... >>> torus_net = ddg.SmoothNet( ... torus_fct, [[0, 2 * np.pi, True], [0, 2 * np.pi, True]] ... ) >>> translation = lambda x: np.array([x[0] + 0.2, x[1], x[2]]) >>> inversion = lambda x: x / sum([i**2 for i in x]) >>> torus_net = compose(translation, torus_net) >>> torus_net = compose(inversion, torus_net) .. image:: 12_Inversion.png :align: center :scale: 20 % Modifying Nets -------------- Another way of working with nets is the to use our utility functions on them. While domain utility functions will create, when you pass them an actual domain, they will replace the domain of a given net instead. .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet(lambda t: t, [[-np.inf, np.inf]]) >>> net.domain.intervals [[-inf, inf]] >>> new_net = ddg.nets.utils.bound_domain(net, 10) >>> new_net is net True >>> new_net.domain.intervals [[-5.0, 5.0]] Example: Surface of Revolution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. doctest:: >>> import ddg >>> import numpy as np >>> hyperbola1 = lambda u: np.array([np.sinh(u), np.cosh(u)]) >>> hyperbola2 = lambda u: np.array([np.sinh(u), -np.cosh(u)]) >>> hyperbola1 = ddg.SmoothNet(hyperbola1, [[0, np.inf]]) >>> hyperbola2 = ddg.SmoothNet(hyperbola2, [[0, np.inf]]) >>> hyperbola1 = ddg.nets.utils.surface_of_revolution(hyperbola1) >>> hyperbola2 = ddg.nets.utils.surface_of_revolution(hyperbola2) >>> two_sheeted_hyperboloid = ddg.NetCollection([hyperbola1, hyperbola2]) .. image:: 13_TwoSheetedHyperboloid.png :align: center :scale: 20 % More Utility Functions ^^^^^^^^^^^^^^^^^^^^^^ The :py:mod:`~ddg.datastructures.nets.utils` module of nets contains a collection of utility functions which can be applied to nets. An example is :py:func:`~ddg.datastructures.nets.utils.coordinate_lines` , which returns a :py:class:`~ddg.datastructures.nets.net.NetCollection` of curves describing the coordinate lines of the discrete net. .. _sampling_a_smooth_net: Sampling -------- **Sampling** a smooth set means you choose finitely (or countably) many points to obtain a discrete set. This is important for **visualizing smooth nets**. Since they have uncountably many points, they must be converted to discrete nets first. For that, our library provides the function `~ddg.datastructures.nets.conversion.sample_smooth_net`. It takes a :py:class:`~ddg.datastructures.nets.net.SmoothNet` and converts it to a :py:class:`~ddg.datastructures.nets.net.DiscreteNet`. Internally, `~ddg.datastructures.nets.conversion.sample_smooth_net` uses `~ddg.datastructures.nets.conversion.sample_smooth_domain` to sample the domain of the `SmoothNet` and then it concatenates the output with the net function of the `SmoothNet`. The `sampling` parameter of `~ddg.datastructures.nets.conversion.sample_smooth_net` is handled in the same way as that of `~ddg.datastructures.nets.conversion.sample_smooth_domain`. See :ref:`sampling of domains ` for a detailed description and examples! Examples ^^^^^^^^ **Bounded** directions can be sampled with **total** if we don't use an achor: .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet(lambda x, y: np.array([x, y, x * y]), [[-5, 5]] * 2) >>> discrete_net = ddg.sample_smooth_net(net, [10, "t"]) >>> discrete_net.domain.intervals [[0, 9], [0, 9]] >>> discrete_net = ddg.sample_smooth_net(net, [[10, "t"], [5, "t"]]) >>> discrete_net.domain.intervals [[0, 9], [0, 4]] >>> discrete_net = ddg.sample_smooth_net(net, [0.5, [5, "t"]]) >>> discrete_net.domain.intervals [[0, 20], [0, 4]] **Unbounded** directions and directions for which we want to set an **anchor** can *only* be sampled with **stepsize**: .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet( ... lambda x, y: np.array([x, y, x * y]), [[-np.inf, np.inf]] * 2 ... ) >>> discrete_net = ddg.sample_smooth_net(net, 0.1) >>> discrete_net.domain.intervals [[-inf, inf], [-inf, inf]] .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet(lambda x, y: np.array([x, y, x * y]), [[-5, 5]] * 2) >>> discrete_net = ddg.sample_smooth_net(net, [10, "t"]) >>> discrete_net.domain.intervals [[0, 9], [0, 9]] **Unbounded or Bounded?** In the case of quadrics for example we can not be sure whether the domain of the resulting net is unbounded or not. In this case, it is convenient to use the **compound** sampling, which can handle both cases. .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet( ... lambda x, y: np.array([x, y, x * y]), [[-np.inf, np.inf]] * 2 ... ) >>> discrete_net = ddg.sample_smooth_net(net, [0.5, 10, "c"]) >>> discrete_net.domain.intervals [[0, 9], [0, 9]] **Different sampling in both directions** .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet(lambda x, y: np.array([x, y, x * y]), [[-5, 5]] * 2) >>> discrete_net = ddg.sample_smooth_net(net, 0.5) >>> discrete_net.domain.intervals [[0, 20], [0, 20]] >>> (discrete_net(0, 0) == net(-5, -5)).all() True .. doctest:: >>> import ddg >>> import numpy as np >>> net = ddg.SmoothNet(lambda x, y: np.array([x, y, x * y]), [[-5, 5]] * 2) >>> discrete_net = ddg.sample_smooth_net(net, [0.5, 0.2]) >>> discrete_net.domain.intervals [[0, 20], [0, 49]] >>> discrete_net(10, 49) array([0. , 4.8, 0. ]) .. figure:: 15_sampling.png :align: center :scale: 40 % Smooth Net (l), coordinate lines for sampling: `0.5` (m), coordinate lines for sampling: `[0.5, 0.2]` (r) Conversion to Half-edge ----------------------- Nets of dimension 2 or less with bounded domains can be converted to a :ref:`half_edge` object using the function :py:func:`ddg.conversion.halfedge.nets.discrete_net_to_halfedge`. The vertices of the resulting half-edge object will have an attribute ``"co"`` containing the value of the net at that point and the order will be the same as that in the traverser of the domain. .. doctest:: >>> from ddg.conversion.halfedge.nets import discrete_net_to_halfedge >>> def f(x, y): ... return np.array([x, y, x**2 - y**2]) ... >>> net = ddg.DiscreteNet(f, [[0, 1], [0, 1]]) >>> surface = discrete_net_to_halfedge(net) >>> for c in net.traverser: ... print(net[c]) ... [0 0 0] [ 0 1 -1] [1 0 1] [1 1 0] >>> for v in surface.verts: ... print(v.co) ... [0 0 0] [ 0 1 -1] [1 0 1] [1 1 0] The name of the coordinate attribute can be changed using the `co_attr` argument of the conversion function. Visualization in Blender ------------------------ A guide on how to visualize nets in Blender can be found :ref:`here `.