Grouping & arranging
====================

Fib-o-mat provides some tools to group and arrange :class:`~fibomat.shapes.shape.Shape`\ s, :class:`~fibomat.pattern.Pattern`\ s and :class:`~fibomat.site.Site`\ s. This features are provided by the :mod:`~fibomat.layout` module.

Grouping
--------

Sometimes it can be useful to group :class:`~fibomat.shapes.shape.Shape`\ s, :class:`~fibomat.pattern.Pattern`\ s or :class:`~fibomat.site.Site`\ s
together, e.g. to transform them uniformly. Note, that a group can only contain elements of the same kind. For example, a shape and an pattern cannot be in the same group.

This can be done with the the :class:`~fibomat.layout.group.Group` class. The class takes the objects to be grouped as parameters.

Grouping of shapes
++++++++++++++++++

As an example, we create a marker pattern consisting of some lines and circles ::

    marker = Group([
        Line((-1, 0), (1, 0)),
        Line((0, -1), (0, 1)),
        Circle(r=.5, center=(0, 0)),
        Circle(r=.05, center=(1-.05, 1-0.05))
    ])

This marker can be added to a pattern as any other shape. All elements in the group will share the same patterning settings ::

    site.create_pattern(
        dim_shape=marker * U_('µm'),
        shape_mill=Mill(dwell_time=Q_('1 ms'), repeats=1),
        raster_style=raster_styles.one_d.Curve(pitch=Q_('1 nm'), scan_sequence=raster_styles.ScanSequence.CONSECUTIVE)
    )


The shapes contained in the group will be added to the backend by the order given in the group.
If the individual elements should have different patterning settings, see the next subsection.

Groups support the same transformations as any other shape.
Though, the :class:`~fibomat.linalg.transformable.Transformable.pivot` property in particular useful in combination with groups.

Suppose, the marker should be added in the four corners of a rectangle, so that the little circle (by default in the upper right corner) is always oriented outwards but the center of the cross lies on top of the corner of the rectangle.
All markers can be generated by rotating the original one by 90°, 180° and 270°. By setting the rotation origin to the center of the cross, the marker cross center is invariant under rotation.
To accomplish this, the pivot of the marker is set to ::

    marker.pivot = lambda self: self.elements[2].center

Here, ``self`` will be always a reference to the marker group and ``self.elements[2]`` is the third element in the group which is the circle with radius of 0.5 and center at the desired center of the cross.

Now, we can specify in the rotation call to use the pivot as rotation origin and we get the expected result. ::

    second_marker = marker.transformed(translate((-4, 4)) | rotate(np.pi/2, origin='pivot'))

The final result is shown in the plot below.

.. bokeh-plot-link:: ../examples/shape_groups.py
    :url: https://gitlab.com/viggge/fib-o-mat/-/blob/master/examples/shape_groups.py

Grouping of patterns
++++++++++++++++++++

In the same way as shapes can be grouped, :class:`~fibomat.pattern.Pattern`\ s can be grouped.

Changing the example from above, we replace the little circle in the upper right corner with a spot. In doing so, we cannot use the same rasterizing style for the whole group. The spot need another rasterizing style compared to the other shapes. Therefore, we create a group containing a pattern for the spot and a pattern for with remaining of the original marker. ::

    center_group = Group([
        Line((-1, 0), (1, 0)),
        Line((0, -1), (0, 1)),
        Circle(r=.5, center=(0, 0))
    ])

    marker_pattern = Group([
        Pattern(
            dim_shape=(center_group, U_('µm')),
            mill=Mill(dwell_time=Q_('1 ms'), repeats=1),
            raster_style=raster_styles.one_d.Curve(pitch=Q_('1 nm'), scan_sequence=raster_styles.ScanSequence.CONSECUTIVE)
        ),
        Pattern(
            dim_shape=(Spot((.75, .75)), U_('µm')),
            mill=Mill(dwell_time=Q_('1 ms'), repeats=1),
            raster_style=raster_styles.zero_d.SingleSpot()
        )
    ])

The marker pattern can be added to a site as usual ::

    site += marker_pattern

and can also be rotated and translated with the restriction stated above if the pivot is set correctly ::

    # we chose again the center of circle located at third element of the  of the first pattern
    # note: this looks quite ugly 8and it is!) but will be simplified in the future
    marker_pattern.pivot = lambda self: self.elements[0].dim_shape.elements[2].shape.center * self.elements[0].dim_shape.elements[2].unit

    second_marker_pattern = marker_pattern.transformed(translate((-4, 4) * U_('µm')) | rotate(np.pi/2, origin='pivot'))

Note, that we have to pass a dimensioned vector to the ``translate`` function.

.. bokeh-plot-link:: ../examples/pattern_groups.py
    :url: https://gitlab.com/viggge/fib-o-mat/-/blob/master/examples/pattern_groups.py

Grouping sites
++++++++++++++
Similar to patterns, :class:`~fibomat.site.Site`\ s can be collected and manipulated in groups.


Arranging
---------

Fib-o-mat supports arranging of objects (:class:`~fibomat.shapes.shape.Shape`\ s, :class:`~fibomat.pattern.Pattern`\ s and :class:`~fibomat.site.Site`\ s, again) in 2d-lattices via the :class:`~fibomat.layout.lattice.Lattice` class. Any object added to a lattice is translated automatically so that its pivot is equivalent to the set lattice point.

These lattice classes are quite flexible. All `2d Bravais lattices <https://en.wikipedia.org/wiki/Bravais_lattice#In_2_dimensions>`__ can be generated. By adding grouped shapes to the lattice, also other non-Bravais lattices can be generated like the graphene honeycomb lattice (in this case the group represent a unit cell containing multiple lattice points).

In general, a 2d-lattice is defined by two lattice vectors u and v and the objects in the unit cell.
The :class:`~fibomat.layout.lattice.Lattice` class expects:

    * the lengths of two lattice vector (du, dv)
    * the angle between the lattice vectors (alpha)
    * the number of elements in u and v direction (nu, nv)

The lattice class has to options to set the objects at each lattice points. This can be a fixed shape/group of shape ore a generating function.

.. Either, the element can be defined in the ``__init__`` method. If doing so, the element is copied to all lattice sites automatically. On the other hand, no element is defined in the ``__init__`` method and individual lattice sites can be set with the index operator (see example below). All vacant lattice sites are ignored on exporting.

In the following, all different methods to generate a lattice are introduced with examples ranging from simple to more involved methods.

In all cases, the indices of the lattice points are given by ::

             -- u -->

        |   ( 0,  0) | ( 0,  1) | ( 0,  2)
        v    ---------+----------+---------
        V   ( 1,  0) | ( 1,  1) | ( 1,  2)
            ---------+----------+---------  ...
            ( 2,  0) | ( 2,  1) | ( 2,  2)
            ---------+----------+---------
            ( 3,  0) | ( 3,  1) | ( 3,  2)
                         :
                         :

Note, that u and v can be any non-collinear vectors.

Rectangular lattices
++++++++++++++++++++

To generate a rectangular lattice (lattice with orthogonal unit vectors), the length of the lattice vectors and the number of rows/columns must be defined as well as a shape. ::

    from fibomat.layout import Lattice
    from fibomat.shapes import Spot

    spot_lattice = Lattice.generate_rect(nu=2, nv=5, du=.2, dv=.1, element=Spot((0, 0)))  # the position of the spot does not matter

The center of the lattice is chosen automatically so that the mean value of all lattice elements (not the shapes' centers!) is at (0, 0).

Suppose now, the element at each lattice point should be altered depending on the lattice point index. To accomplish this, we can pass a element generator function as element.
This generator function expects three parameters: first, a tuple containing the coordiantes of the lattice point and second a tuple containing the indices of the lattice points.

In the following example, an ellipse is scaled based on the position in the lattice ::

    from fibomat.layout import Lattice
    from fibomat.shapes import Ellipse

    def ellipse_generator(pos_xy: Tuple[float, float], pos_uv: Tuple[int, int]):
        ellipse_base_radius = 1

        u, v = pos_uv

        return Ellipse(a=ellipse_base_radius + 0.1 * u, b=ellipse_base_radius + 0.1 * v)

    ellipse_lattice = Lattice.generate_rect(nu=4, nv=3, du=2, dv=2, element=ellipse_gen)


If the generating function returns None, the lattice site is kept vacant. This can be used to generate a checkerboard pattern, for example. ::

    def checkerboard_generator(pos_xy: Tuple[float, float], pos_uv: Tuple[int, int]):
        u, v = pos_uv

        if (u + v) % 2 == 0
            return Rect(width=1, height=1)
        else:
            return None

     checkerboard_lattice = Lattice.generate_rect(nu=8, nv=8, du=.5, dv=.5, element=ellipse_gen)

Below, all three examples are shown.

.. bokeh-plot-link:: ../examples/rectangular_lattices.py
    :url: https://gitlab.com/viggge/fib-o-mat/-/blob/master/examples/rectangular_lattices.py


General lattices
++++++++++++++++

To get a finer control on the lattice generation, the Lattice.generate class method can be used.
This function expects an boundary curve, two lattice vectors and an element or element generating function.

To demonstrate the capabilities, first, a Kagome spot lattice is generated with a triangular boundary curve.
This type of lattice can be generated with unit cell containing three spots arranged in a equilteral triangle and zwo equal length lattice vector which are rotated by 60° with repsect to each other.

::

    unit_cell = Group([Spot((-.25/2, 0)), Spot((.25/2, 0)), Spot((0, .25 * np.sqrt(3) / 2))])

    kagome_lattice = Lattice.generate(
        boundary=Rect(10.1, 10.1),  u=(.5, 0), v=Vector(.5, 0).rotated(np.pi/3), element=unit_cell
    )

As shown in the plot below (left site), the lattice is generated but some spots of the unit cell are placced outside of the boundary.
This can be prevent by "exploding" the unit cell in its individual shapes and set the remove_outliers parameter (right site in the plot) ::

    kagome_lattice = Lattice.generate(
        boundary=Rect(10.1, 10.1),  u=(.5, 0), v=Vector(.5, 0).rotated(np.pi/3), element=unit_cell, explode=True, remove_outliers=True
    )

Due to numerical glitches, it is advisable to use boundary edges which are not lattice planes.

.. bokeh-plot-link:: ../examples/bravais_lattices.py
    :url: https://gitlab.com/viggge/fib-o-mat/-/blob/master/examples/bravais_lattices.py


Lastly, to demonstrate the potential of the lattice generator, we present a non-useful but hopefully good looking example.
In this case, a custom generator function is use to create a 'dissolving' honeycomb lattice.




Probably the most simple lattice can be defined like ::

    lattice = Lattice(nu=5, nv=5, du=1, dv=1, alpha=np.pi/2, element=Spot((0,0))

This will generate a 5x5 square lattice with a single Spot on each lattice site.

If lattice sites should contain different elements, the following method can be used ::

    lattice = Lattice(nu=5, nv=5, du=1, dv=1, alpha=np.pi/2)

    lattice[0, 0] = Spot((0, 0))
    lattice[1, 0] = Circle(r=1)
    # ...

Not that the added shapes will be translated so that their pivot is equal the the lattice point (hence it does not matter, which center is initially set).

Latte points can also groups or even another lattice. Various combinations are shown in the plot below. See the source for details.

If patterns or sites are added to lattice, these are translated in their respective units. If one pattern has a shape defined in `µm` another one with shape in `nm`, the resulting layout will *not* be a regular lattice. Compare also :ref:`Grouping of patterns`.


Even sites can be ordered in a lattice as the following plot demonstrates.

..
    .. bokeh-plot-link:: ../examples/lattices.py
        :url: https://gitlab.com/viggge/fib-o-mat/-/blob/master/examples/lattices.py
