diff --git a/docs/docs/v0.9.0rc1/images/einzel lens electron traces.png b/docs/docs/v0.9.0rc1/images/einzel lens electron traces.png new file mode 100644 index 0000000..a01e97c Binary files /dev/null and b/docs/docs/v0.9.0rc1/images/einzel lens electron traces.png differ diff --git a/docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png b/docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png new file mode 100644 index 0000000..74504f3 Binary files /dev/null and b/docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png differ diff --git a/docs/docs/v0.9.0rc1/images/einzel lens.png b/docs/docs/v0.9.0rc1/images/einzel lens.png new file mode 100644 index 0000000..1d328b5 Binary files /dev/null and b/docs/docs/v0.9.0rc1/images/einzel lens.png differ diff --git a/docs/docs/v0.9.0rc1/images/einzel_lens_radial.png b/docs/docs/v0.9.0rc1/images/einzel_lens_radial.png new file mode 100644 index 0000000..a36eb9d Binary files /dev/null and b/docs/docs/v0.9.0rc1/images/einzel_lens_radial.png differ diff --git a/docs/docs/v0.9.0rc1/traceon/einzel-lens.html b/docs/docs/v0.9.0rc1/traceon/einzel-lens.html new file mode 100644 index 0000000..e3f9d25 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/einzel-lens.html @@ -0,0 +1,211 @@ + + + + + + + + + + traceon API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ +

Einzel lens

+

Introduction

+

This example walks you through the code of examples/einzel-lens.py. We will +compute the electrostatic field inside an axial symmetric einzel lens and trace a number of electrons through the field. Please follow +the link to find the up-to-date version of the code, including the neccessary import statements to actually run the example. To install Traceon, +please first install Python and use the standard pip command to install the package:

+
pip install traceon
+
+

Defining the geometry

+

First, we have to define the geometry of the element we want to simulate. In the boundary element method (BEM) only the boundaries of the +objects need to be meshed. This implies that in a radial symmetric geometry (like our einzel lens) our elements will be lines. To find the true +3D representation of the einzel lens, image revolving the line elements around the z-axis. The code needed to define the geometry is given below.

+
# Dimensions of the einzel lens.
+THICKNESS = 0.5
+SPACING = 0.5
+RADIUS = 0.15
+
+# Start value of z chosen such that the middle of the einzel
+# lens is at z = 0mm.
+z0 = -THICKNESS - SPACING - THICKNESS/2
+
+boundary = G.Path.line([0., 0., 1.75],  [2.0, 0., 1.75]).extend_with_line([2.0, 0., -1.75]).extend_with_line([0., 0., -1.75])
+
+margin_right = 0.1
+extent = 2.0 - margin_right
+
+bottom = G.Path.aperture(THICKNESS, RADIUS, extent, -THICKNESS - SPACING)
+middle = G.Path.aperture(THICKNESS, RADIUS, extent)
+top = G.Path.aperture(THICKNESS, RADIUS, extent, THICKNESS + SPACING)
+
+boundary.name = 'boundary'
+bottom.name = 'ground'
+middle.name = 'lens'
+top.name = 'ground'
+
+

Note that we explicitely assign names to the different elements in our geometry. Later, we will use these names to apply the correct excitations +to the elements. Next, we mesh the geometry which transforms it into many small line elements used in the solver. Note, that you can either supply +a mesh_size or a mesh_size_factor to the Path.mesh() function.

+
mesh = (boundary + bottom + middle + top).mesh(mesh_size_factor=45)
+
+P.plot_mesh(mesh, lens='blue', ground='green', boundary='purple')
+P.show()
+
+

+

Applying excitations

+

We are now ready to apply excitations to our elements. We choose to put 0V on the 'ground' electrode, and 1800V on the 'lens' electrode. We specify +that the boundary electrode is an 'electrostatic boundary', which means that there is no electric field parallel to the surface ($\mathbf{n} \cdot \nabla V = 0$).

+
excitation = E.Excitation(mesh, E.Symmetry.RADIAL)
+
+# Excite the geometry, put ground at 0V and the lens electrode at 1800V.
+excitation.add_voltage(ground=0.0, lens=1800)
+excitation.add_electrostatic_boundary('boundary')
+
+

Solving for the field

+

Solving for the field is now just a matter of calling the solve_direct() function. The Field class returned +provides methods for calculating the resulting potential and electrostatic field, which we can subsequently use to trace electrons.

+
# Use the Boundary Element Method (BEM) to calculate the surface charges,
+# the surface charges gives rise to a electrostatic field.
+field = S.solve_direct(excitation)
+
+

Axial interpolation

+

Before tracing the electrons, we first construct an axial interpolation of the Einzel lens. In a radial symmetric system the field +close to the optical axis is completely determined by the higher order derivatives of the potential. This fact can be used to trace +electrons very rapidly. The unique strength of the BEM is that there exists closed form formulas for calculating the higher +order derivatives (from the computed charge distribution). In Traceon, we can make this interpolation in a single +line of code:

+
field_axial = FieldRadialAxial(field, -1.5, 1.5, 150)
+
+

Note that this field is only accurate close to the optical axis (z-axis). We can plot the potential along the axis to ensure ourselves +that the interpolation is working as expected:

+
z = np.linspace(-1.5, 1.5, 150)
+pot = [field.potential_at_point([0.0, 0.0, z_]) for z_ in z]
+pot_axial = [field_axial.potential_at_point([0.0, 0.0, z_]) for z_ in z]
+
+plt.title('Potential along axis')
+plt.plot(z, pot, label='Surface charge integration')
+plt.plot(z, pot_axial, linestyle='dashed', label='Interpolation')
+plt.xlabel('z (mm)')
+plt.ylabel('Potential (V)')
+plt.legend()
+
+

+

Tracing electrons

+

Tracing electrons is now just a matter of calling the .get_tracer() method. We provide +to this method the bounds in which we want to trace. Once an electron hits the edges of the bounds the tracing will +automatically stop.

+
# An instance of the tracer class allows us to easily find the trajectories of 
+# electrons. Here we specify that the interpolated field should be used, and that
+# the tracing should stop if the x,y value goes outside ±RADIUS/2 or the z value outside ±10 mm.
+tracer = field_axial.get_tracer( [(-RADIUS/2, RADIUS/2), (-RADIUS/2,RADIUS/2),  (-10, 10)] )
+
+# Start tracing from z=7mm
+r_start = np.linspace(-RADIUS/3, RADIUS/3, 7)
+
+# Initial velocity vector points downwards, with a 
+# initial speed corresponding to 1000eV.
+velocity = T.velocity_vec(1000, [0, 0, -1])
+
+trajectories = []
+
+for i, r0 in enumerate(r_start):
+    print(f'Tracing electron {i+1}/{len(r_start)}...')
+    _, positions = tracer(np.array([r0, 0, 5]), velocity)
+    trajectories.append(positions)
+
+

From the traces we can see the focusing effect of the lens. If we zoom in on the focus we can clearly see the +spherical aberration, thanks to the high accuracy of both the solver and the field interpolation.

+

+
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/excitation.html b/docs/docs/v0.9.0rc1/traceon/excitation.html new file mode 100644 index 0000000..cb23883 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/excitation.html @@ -0,0 +1,1240 @@ + + + + + + + + + + traceon.excitation API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.excitation

+
+ +
+

The excitation module allows to specify the excitation (or element types) of the different physical groups (electrodes) +created with the traceon.geometry module.

+

The possible excitations are as follows:

+
    +
  • Voltage (either fixed or as a function of position)
  • +
  • Dielectric, with arbitrary electric permittivity
  • +
  • Current coil (radial symmetric geometry)
  • +
  • Current lines (3D geometry)
  • +
  • Magnetostatic scalar potential
  • +
  • Magnetizable material, with arbitrary magnetic permeability
  • +
+

Once the excitation is specified, it can be passed to solve_direct() to compute the resulting field.

+
+ +
+
+ +
+
+ +
+
+ +
+

Classes

+
+ +
+ class Excitation + (mesh, symmetry) +
+ +
+ + + +
+ + Expand source code + +
class Excitation:
+    """ """
+     
+    def __init__(self, mesh, symmetry):
+        self.mesh = mesh
+        self.electrodes = mesh.get_electrodes()
+        self.excitation_types = {}
+        self.symmetry = symmetry
+         
+        if symmetry == Symmetry.RADIAL:
+            assert self.mesh.points.shape[1] == 2 or np.all(self.mesh.points[:, 1] == 0.), \
+                "When symmetry is RADIAL, the geometry should lie in the XZ plane"
+    
+    def __str__(self):
+        return f'<Traceon Excitation,\n\t' \
+            + '\n\t'.join([f'{n}={v} ({t})' for n, (t, v) in self.excitation_types.items()]) \
+            + '>'
+
+    def _ensure_electrode_is_lines(self, excitation_type, name):
+        assert name in self.electrodes, f"Electrode '{name}' is not present in the mesh"
+        assert name in self.mesh.physical_to_lines, f"Adding {excitation_type} excitation in {self.symmetry} symmetry is only supported if electrode '{name}' consists of lines"
+    
+    def _ensure_electrode_is_triangles(self, excitation_type, name):
+        assert name in self.electrodes, f"Electrode '{name}' is not present in the mesh"
+        assert name in self.mesh.physical_to_triangles, f"Adding {excitation_type} excitation in {self.symmetry} symmetry is only supported if electrode '{name}' consists of triangles"
+     
+    def add_voltage(self, **kwargs):
+        """
+        Apply a fixed voltage to the geometries assigned the given name.
+        
+        Parameters
+        ----------
+        **kwargs : dict
+            The keys of the dictionary are the geometry names, while the values are the voltages in units of Volt. For example,
+            calling the function as `add_voltage(lens=50)` assigns a 50V value to the geometry elements part of the 'lens' physical group.
+            Alternatively, the value can be a function, which takes x, y, z coordinates as argument and returns the voltage at that position.
+            Note that in 2D symmetries (such as radial symmetry) the z value for this function will always be zero.
+        
+        """
+        for name, voltage in kwargs.items():
+             
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('voltage', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('voltage', name)
+            
+            if isinstance(voltage, int) or isinstance(voltage, float):
+                self.excitation_types[name] = (ExcitationType.VOLTAGE_FIXED, voltage)
+            elif callable(voltage):
+                self.excitation_types[name] = (ExcitationType.VOLTAGE_FUN, voltage)
+            else:
+                raise NotImplementedError('Unrecognized voltage value')
+
+    def add_current(self, **kwargs):
+        """
+        Apply a fixed total current to the geometries assigned the given name. Note that a coil is assumed,
+        which implies that the current density is constant as a function of (r, z). In a solid piece of conducting material the current density would
+        be higher at small r (as the 'loop' around the axis is shorter and therefore the resistance is lower).
+        
+        Parameters
+        ----------
+        **kwargs : dict
+            The keys of the dictionary are the geometry names, while the values are the currents in units of Ampere. For example,
+            calling the function as `add_current(coild=10)` assigns a 10A value to the geometry elements part of the 'coil' physical group.
+        """
+        if self.symmetry == Symmetry.RADIAL:
+            for name, current in kwargs.items():
+                self._ensure_electrode_is_triangles("current", name)
+                self.excitation_types[name] = (ExcitationType.CURRENT, current)
+        elif self.symmetry == Symmetry.THREE_D:
+            for name, current in kwargs.items():
+                self._ensure_electrode_is_lines("current", name)
+                self.excitation_types[name] = (ExcitationType.CURRENT, current)
+        else:
+            raise ValueError('Symmetry should be one of RADIAL or THREE_D')
+
+    def has_current(self):
+        """Check whether a current is applied in this excitation."""
+        return any([t == ExcitationType.CURRENT for t, _ in self.excitation_types.values()])
+    
+    def is_electrostatic(self):
+        """Check whether the excitation contains electrostatic fields."""
+        return any([t in [ExcitationType.VOLTAGE_FIXED, ExcitationType.VOLTAGE_FUN] for t, _ in self.excitation_types.values()])
+     
+    def is_magnetostatic(self):
+        """Check whether the excitation contains magnetostatic fields."""
+        return any([t in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.CURRENT] for t, _ in self.excitation_types.values()])
+     
+    def add_magnetostatic_potential(self, **kwargs):
+        """
+        Apply a fixed magnetostatic potential to the geometries assigned the given name.
+        
+        Parameters
+        ----------
+        **kwargs : dict
+            The keys of the dictionary are the geometry names, while the values are the voltages in units of Ampere. For example,
+            calling the function as `add_magnetostatic_potential(lens=50)` assigns a 50A value to the geometry elements part of the 'lens' physical group.
+        """
+        for name, pot in kwargs.items():
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('magnetostatic potential', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('magnetostatic potential', name)
+             
+            self.excitation_types[name] = (ExcitationType.MAGNETOSTATIC_POT, pot)
+
+    def add_magnetizable(self, **kwargs):
+        """
+        Assign a relative magnetic permeability to the geometries assigned the given name.
+        
+        Parameters
+        ----------
+        **kwargs : dict
+            The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example,
+            calling the function as `add_dielectric(spacer=2)` assign the relative dielectric constant of 2 to the `spacer` physical group.
+         
+        """
+
+        for name, permeability in kwargs.items():
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('magnetizable', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('magnetizable', name)
+
+            self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability)
+     
+    def add_dielectric(self, **kwargs):
+        """
+        Assign a dielectric constant to the geometries assigned the given name.
+        
+        Parameters
+        ----------
+        **kwargs : dict
+            The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example,
+            calling the function as `add_dielectric(spacer=2)` assign the relative dielectric constant of 2 to the `spacer` physical group.
+         
+        """
+        for name, permittivity in kwargs.items():
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('dielectric', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('dielectric', name)
+
+            self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity)
+
+    def add_electrostatic_boundary(self, *args, ensure_inward_normals=True):
+        """
+        Specify geometry elements as electrostatic boundary elements. At the boundary we require E·n = 0 at every point on the boundary. This
+        is equivalent to stating that the directional derivative of the electrostatic potential through the boundary is zero. Placing boundaries between
+        the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a dielectric with a dielectric
+        constant of zero. This is how a boundary is actually implemented internally.
+        
+        Parameters
+        ----------
+        *args: list of str
+            The geometry names that should be considered a boundary.
+        """
+        if ensure_inward_normals:
+            for electrode in args:
+                self.mesh.ensure_inward_normals(electrode)
+        
+        for name in args:
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('electrostatic boundary', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('electrostatic boundary', name)
+
+        self.add_dielectric(**{a:0 for a in args})
+    
+    def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True):
+        """
+        Specify geometry elements as magnetostatic boundary elements. At the boundary we require H·n = 0 at every point on the boundary. This
+        is equivalent to stating that the directional derivative of the magnetostatic potential through the boundary is zero. Placing boundaries between
+        the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a magnetic material with a magnetic 
+        permeability of zero. This is how a boundary is actually implemented internally.
+        
+        Parameters
+        ----------
+        *args: list of str
+            The geometry names that should be considered a boundary.
+        """
+        if ensure_inward_normals:
+            for electrode in args:
+                print('flipping normals', electrode)
+                self.mesh.ensure_inward_normals(electrode)
+         
+        for name in args:
+            if self.symmetry == E.Symmetry.RADIAL:
+                self._ensure_electrode_is_lines('magnetostatic boundary', name)
+            elif self.symmetry == E.Symmetry.THREE_D:
+                self._ensure_electrode_is_triangles('magnetostatic boundary', name)
+         
+        self.add_magnetizable(**{a:0 for a in args})
+    
+    def _split_for_superposition(self):
+        
+        # Names that have a fixed voltage excitation, not equal to 0.0
+        types = self.excitation_types
+        non_zero_fixed = [n for n, (t, v) in types.items() if t in [ExcitationType.VOLTAGE_FIXED,
+                                                                    ExcitationType.CURRENT] and v != 0.0]
+        
+        excitations = []
+         
+        for name in non_zero_fixed:
+             
+            new_types_dict = {}
+             
+            for n, (t, v) in types.items():
+                assert t != ExcitationType.VOLTAGE_FUN, "VOLTAGE_FUN excitation not supported for superposition."
+                 
+                if n == name:
+                    new_types_dict[n] = (t, 1.0)
+                elif t == ExcitationType.VOLTAGE_FIXED:
+                    new_types_dict[n] = (t, 0.0)
+                elif t == ExcitationType.CURRENT:
+                    new_types_dict[n] = (t, 0.0)
+                else:
+                    new_types_dict[n] = (t, v)
+            
+            exc = Excitation(self.mesh, self.symmetry)
+            exc.excitation_types = new_types_dict
+            excitations.append(exc)
+
+        assert len(non_zero_fixed) == len(excitations)
+        return {n:e for (n,e) in zip(non_zero_fixed, excitations)}
+
+    def _get_active_elements(self, type_):
+        assert type_ in ['electrostatic', 'magnetostatic']
+        
+        if self.symmetry == Symmetry.RADIAL:
+            elements = self.mesh.lines
+            physicals = self.mesh.physical_to_lines
+        else:
+            elements = self.mesh.triangles
+            physicals = self.mesh.physical_to_triangles
+
+        def type_check(excitation_type):
+            if type_ == 'electrostatic':
+                return excitation_type.is_electrostatic()
+            else:
+                return excitation_type in [ExcitationType.MAGNETIZABLE, ExcitationType.MAGNETOSTATIC_POT]
+        
+        inactive = np.full(len(elements), True)
+        for name, value in self.excitation_types.items():
+            if type_check(value[0]):
+                inactive[ physicals[name] ] = False
+         
+        map_index = np.arange(len(elements)) - np.cumsum(inactive)
+        names = {n:map_index[i] for n, i in physicals.items() \
+                    if n in self.excitation_types and type_check(self.excitation_types[n][0])}
+         
+        return self.mesh.points[ elements[~inactive] ], names
+     
+    def get_electrostatic_active_elements(self):
+        """Get elements in the mesh that have an electrostatic excitation
+        applied to them. 
+         
+        Returns
+        --------
+        A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. \
+        This array contains the vertices of the line elements or the triangles. \
+        Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line \
+        element is given by a polynomial interpolation of the points. \
+        names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, \
+        while the values are Numpy arrays of indices that can be used to index the points array.
+        """
+        return self._get_active_elements('electrostatic')
+    
+    def get_magnetostatic_active_elements(self):
+        """Get elements in the mesh that have an magnetostatic excitation
+        applied to them. This does not include current excitation, as these are not part of the matrix.
+    
+        Returns
+        --------
+        A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. \
+        This array contains the vertices of the line elements or the triangles. \
+        Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line \
+        element is given by a polynomial interpolation of the points. \
+        names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, \
+        while the values are Numpy arrays of indices that can be used to index the points array.
+        """
+
+        return self._get_active_elements('magnetostatic')
+
+ +
+ + + +

Methods

+
+ +
+ + def add_current(self, **kwargs) +
+
+ + + +
+ + Expand source code + +
def add_current(self, **kwargs):
+    """
+    Apply a fixed total current to the geometries assigned the given name. Note that a coil is assumed,
+    which implies that the current density is constant as a function of (r, z). In a solid piece of conducting material the current density would
+    be higher at small r (as the 'loop' around the axis is shorter and therefore the resistance is lower).
+    
+    Parameters
+    ----------
+    **kwargs : dict
+        The keys of the dictionary are the geometry names, while the values are the currents in units of Ampere. For example,
+        calling the function as `add_current(coild=10)` assigns a 10A value to the geometry elements part of the 'coil' physical group.
+    """
+    if self.symmetry == Symmetry.RADIAL:
+        for name, current in kwargs.items():
+            self._ensure_electrode_is_triangles("current", name)
+            self.excitation_types[name] = (ExcitationType.CURRENT, current)
+    elif self.symmetry == Symmetry.THREE_D:
+        for name, current in kwargs.items():
+            self._ensure_electrode_is_lines("current", name)
+            self.excitation_types[name] = (ExcitationType.CURRENT, current)
+    else:
+        raise ValueError('Symmetry should be one of RADIAL or THREE_D')
+
+ +

Apply a fixed total current to the geometries assigned the given name. Note that a coil is assumed, +which implies that the current density is constant as a function of (r, z). In a solid piece of conducting material the current density would +be higher at small r (as the 'loop' around the axis is shorter and therefore the resistance is lower).

+

Parameters

+
+
**kwargs : dict
+
The keys of the dictionary are the geometry names, while the values are the currents in units of Ampere. For example, +calling the function as add_current(coild=10) assigns a 10A value to the geometry elements part of the 'coil' physical group.
+
+
+ + +
+ + def add_dielectric(self, **kwargs) +
+
+ + + +
+ + Expand source code + +
def add_dielectric(self, **kwargs):
+    """
+    Assign a dielectric constant to the geometries assigned the given name.
+    
+    Parameters
+    ----------
+    **kwargs : dict
+        The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example,
+        calling the function as `add_dielectric(spacer=2)` assign the relative dielectric constant of 2 to the `spacer` physical group.
+     
+    """
+    for name, permittivity in kwargs.items():
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('dielectric', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('dielectric', name)
+
+        self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity)
+
+ +

Assign a dielectric constant to the geometries assigned the given name.

+

Parameters

+
+
**kwargs : dict
+
The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example, +calling the function as add_dielectric(spacer=2) assign the relative dielectric constant of 2 to the spacer physical group.
+
+
+ + +
+ + def add_electrostatic_boundary(self, *args, ensure_inward_normals=True) +
+
+ + + +
+ + Expand source code + +
def add_electrostatic_boundary(self, *args, ensure_inward_normals=True):
+    """
+    Specify geometry elements as electrostatic boundary elements. At the boundary we require E·n = 0 at every point on the boundary. This
+    is equivalent to stating that the directional derivative of the electrostatic potential through the boundary is zero. Placing boundaries between
+    the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a dielectric with a dielectric
+    constant of zero. This is how a boundary is actually implemented internally.
+    
+    Parameters
+    ----------
+    *args: list of str
+        The geometry names that should be considered a boundary.
+    """
+    if ensure_inward_normals:
+        for electrode in args:
+            self.mesh.ensure_inward_normals(electrode)
+    
+    for name in args:
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('electrostatic boundary', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('electrostatic boundary', name)
+
+    self.add_dielectric(**{a:0 for a in args})
+
+ +

Specify geometry elements as electrostatic boundary elements. At the boundary we require E·n = 0 at every point on the boundary. This +is equivalent to stating that the directional derivative of the electrostatic potential through the boundary is zero. Placing boundaries between +the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a dielectric with a dielectric +constant of zero. This is how a boundary is actually implemented internally.

+

Parameters

+
+
*args : list of str
+
The geometry names that should be considered a boundary.
+
+
+ + +
+ + def add_magnetizable(self, **kwargs) +
+
+ + + +
+ + Expand source code + +
def add_magnetizable(self, **kwargs):
+    """
+    Assign a relative magnetic permeability to the geometries assigned the given name.
+    
+    Parameters
+    ----------
+    **kwargs : dict
+        The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example,
+        calling the function as `add_dielectric(spacer=2)` assign the relative dielectric constant of 2 to the `spacer` physical group.
+     
+    """
+
+    for name, permeability in kwargs.items():
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('magnetizable', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('magnetizable', name)
+
+        self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability)
+
+ +

Assign a relative magnetic permeability to the geometries assigned the given name.

+

Parameters

+
+
**kwargs : dict
+
The keys of the dictionary are the geometry names, while the values are the relative dielectric constants. For example, +calling the function as add_dielectric(spacer=2) assign the relative dielectric constant of 2 to the spacer physical group.
+
+
+ + +
+ + def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True) +
+
+ + + +
+ + Expand source code + +
def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True):
+    """
+    Specify geometry elements as magnetostatic boundary elements. At the boundary we require H·n = 0 at every point on the boundary. This
+    is equivalent to stating that the directional derivative of the magnetostatic potential through the boundary is zero. Placing boundaries between
+    the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a magnetic material with a magnetic 
+    permeability of zero. This is how a boundary is actually implemented internally.
+    
+    Parameters
+    ----------
+    *args: list of str
+        The geometry names that should be considered a boundary.
+    """
+    if ensure_inward_normals:
+        for electrode in args:
+            print('flipping normals', electrode)
+            self.mesh.ensure_inward_normals(electrode)
+     
+    for name in args:
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('magnetostatic boundary', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('magnetostatic boundary', name)
+     
+    self.add_magnetizable(**{a:0 for a in args})
+
+ +

Specify geometry elements as magnetostatic boundary elements. At the boundary we require H·n = 0 at every point on the boundary. This +is equivalent to stating that the directional derivative of the magnetostatic potential through the boundary is zero. Placing boundaries between +the spaces of electrodes usually helps convergence tremendously. Note that a boundary is equivalent to a magnetic material with a magnetic +permeability of zero. This is how a boundary is actually implemented internally.

+

Parameters

+
+
*args : list of str
+
The geometry names that should be considered a boundary.
+
+
+ + +
+ + def add_magnetostatic_potential(self, **kwargs) +
+
+ + + +
+ + Expand source code + +
def add_magnetostatic_potential(self, **kwargs):
+    """
+    Apply a fixed magnetostatic potential to the geometries assigned the given name.
+    
+    Parameters
+    ----------
+    **kwargs : dict
+        The keys of the dictionary are the geometry names, while the values are the voltages in units of Ampere. For example,
+        calling the function as `add_magnetostatic_potential(lens=50)` assigns a 50A value to the geometry elements part of the 'lens' physical group.
+    """
+    for name, pot in kwargs.items():
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('magnetostatic potential', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('magnetostatic potential', name)
+         
+        self.excitation_types[name] = (ExcitationType.MAGNETOSTATIC_POT, pot)
+
+ +

Apply a fixed magnetostatic potential to the geometries assigned the given name.

+

Parameters

+
+
**kwargs : dict
+
The keys of the dictionary are the geometry names, while the values are the voltages in units of Ampere. For example, +calling the function as add_magnetostatic_potential(lens=50) assigns a 50A value to the geometry elements part of the 'lens' physical group.
+
+
+ + +
+ + def add_voltage(self, **kwargs) +
+
+ + + +
+ + Expand source code + +
def add_voltage(self, **kwargs):
+    """
+    Apply a fixed voltage to the geometries assigned the given name.
+    
+    Parameters
+    ----------
+    **kwargs : dict
+        The keys of the dictionary are the geometry names, while the values are the voltages in units of Volt. For example,
+        calling the function as `add_voltage(lens=50)` assigns a 50V value to the geometry elements part of the 'lens' physical group.
+        Alternatively, the value can be a function, which takes x, y, z coordinates as argument and returns the voltage at that position.
+        Note that in 2D symmetries (such as radial symmetry) the z value for this function will always be zero.
+    
+    """
+    for name, voltage in kwargs.items():
+         
+        if self.symmetry == E.Symmetry.RADIAL:
+            self._ensure_electrode_is_lines('voltage', name)
+        elif self.symmetry == E.Symmetry.THREE_D:
+            self._ensure_electrode_is_triangles('voltage', name)
+        
+        if isinstance(voltage, int) or isinstance(voltage, float):
+            self.excitation_types[name] = (ExcitationType.VOLTAGE_FIXED, voltage)
+        elif callable(voltage):
+            self.excitation_types[name] = (ExcitationType.VOLTAGE_FUN, voltage)
+        else:
+            raise NotImplementedError('Unrecognized voltage value')
+
+ +

Apply a fixed voltage to the geometries assigned the given name.

+

Parameters

+
+
**kwargs : dict
+
The keys of the dictionary are the geometry names, while the values are the voltages in units of Volt. For example, +calling the function as add_voltage(lens=50) assigns a 50V value to the geometry elements part of the 'lens' physical group. +Alternatively, the value can be a function, which takes x, y, z coordinates as argument and returns the voltage at that position. +Note that in 2D symmetries (such as radial symmetry) the z value for this function will always be zero.
+
+
+ + +
+ + def get_electrostatic_active_elements(self) +
+
+ + + +
+ + Expand source code + +
def get_electrostatic_active_elements(self):
+    """Get elements in the mesh that have an electrostatic excitation
+    applied to them. 
+     
+    Returns
+    --------
+    A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. \
+    This array contains the vertices of the line elements or the triangles. \
+    Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line \
+    element is given by a polynomial interpolation of the points. \
+    names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, \
+    while the values are Numpy arrays of indices that can be used to index the points array.
+    """
+    return self._get_active_elements('electrostatic')
+
+ +

Get elements in the mesh that have an electrostatic excitation +applied to them.

+

Returns

+

A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. This array contains the vertices of the line elements or the triangles. Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line element is given by a polynomial interpolation of the points. names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, while the values are Numpy arrays of indices that can be used to index the points array.

+
+ + +
+ + def get_magnetostatic_active_elements(self) +
+
+ + + +
+ + Expand source code + +
def get_magnetostatic_active_elements(self):
+    """Get elements in the mesh that have an magnetostatic excitation
+    applied to them. This does not include current excitation, as these are not part of the matrix.
+
+    Returns
+    --------
+    A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. \
+    This array contains the vertices of the line elements or the triangles. \
+    Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line \
+    element is given by a polynomial interpolation of the points. \
+    names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, \
+    while the values are Numpy arrays of indices that can be used to index the points array.
+    """
+
+    return self._get_active_elements('magnetostatic')
+
+ +

Get elements in the mesh that have an magnetostatic excitation +applied to them. This does not include current excitation, as these are not part of the matrix.

+

Returns

+

A tuple of two elements: (points, names). points is a Numpy array of shape (N, 4, 3) in the case of 2D and (N, 3, 3) in the case of 3D. This array contains the vertices of the line elements or the triangles. Multiple points per line elements are used in the case of 2D since higher order BEM is employed, in which the true position on the line element is given by a polynomial interpolation of the points. names is a dictionary, the keys being the names of the physical groups mentioned by this excitation, while the values are Numpy arrays of indices that can be used to index the points array.

+
+ + +
+ + def has_current(self) +
+
+ + + +
+ + Expand source code + +
def has_current(self):
+    """Check whether a current is applied in this excitation."""
+    return any([t == ExcitationType.CURRENT for t, _ in self.excitation_types.values()])
+
+ +

Check whether a current is applied in this excitation.

+
+ + +
+ + def is_electrostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_electrostatic(self):
+    """Check whether the excitation contains electrostatic fields."""
+    return any([t in [ExcitationType.VOLTAGE_FIXED, ExcitationType.VOLTAGE_FUN] for t, _ in self.excitation_types.values()])
+
+ +

Check whether the excitation contains electrostatic fields.

+
+ + +
+ + def is_magnetostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_magnetostatic(self):
+    """Check whether the excitation contains magnetostatic fields."""
+    return any([t in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.CURRENT] for t, _ in self.excitation_types.values()])
+
+ +

Check whether the excitation contains magnetostatic fields.

+
+ +
+ + + +
+ +
+ class ExcitationType + (value, names=None, *, module=None, qualname=None, type=None, start=1) +
+ +
+ + + +
+ + Expand source code + +
class ExcitationType(IntEnum):
+    """Possible excitation that can be applied to elements of the geometry. See the methods of `Excitation` for documentation."""
+    VOLTAGE_FIXED = 1
+    VOLTAGE_FUN = 2
+    DIELECTRIC = 3
+     
+    CURRENT = 4
+    MAGNETOSTATIC_POT = 5
+    MAGNETIZABLE = 6
+     
+    def is_electrostatic(self):
+        return self in [ExcitationType.VOLTAGE_FIXED,
+                        ExcitationType.VOLTAGE_FUN,
+                        ExcitationType.DIELECTRIC]
+
+    def is_magnetostatic(self):
+        return self in [ExcitationType.MAGNETOSTATIC_POT,
+                        ExcitationType.MAGNETIZABLE,
+                        ExcitationType.CURRENT]
+     
+    def __str__(self):
+        if self == ExcitationType.VOLTAGE_FIXED:
+            return 'voltage fixed'
+        elif self == ExcitationType.VOLTAGE_FUN:
+            return 'voltage function'
+        elif self == ExcitationType.DIELECTRIC:
+            return 'dielectric'
+        elif self == ExcitationType.CURRENT:
+            return 'current'
+        elif self == ExcitationType.MAGNETOSTATIC_POT:
+            return 'magnetostatic potential'
+        elif self == ExcitationType.MAGNETIZABLE:
+            return 'magnetizable'
+         
+        raise RuntimeError('ExcitationType not understood in __str__ method')
+
+ +

Possible excitation that can be applied to elements of the geometry. See the methods of Excitation for documentation.

+ + +

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+ +

Class variables

+
+ +
var CURRENT
+
+ + + +
+
+ +
var DIELECTRIC
+
+ + + +
+
+ +
var MAGNETIZABLE
+
+ + + +
+
+ +
var MAGNETOSTATIC_POT
+
+ + + +
+
+ +
var VOLTAGE_FIXED
+
+ + + +
+
+ +
var VOLTAGE_FUN
+
+ + + +
+
+
+

Methods

+
+ +
+ + def is_electrostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_electrostatic(self):
+    return self in [ExcitationType.VOLTAGE_FIXED,
+                    ExcitationType.VOLTAGE_FUN,
+                    ExcitationType.DIELECTRIC]
+
+ +
+
+ + +
+ + def is_magnetostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_magnetostatic(self):
+    return self in [ExcitationType.MAGNETOSTATIC_POT,
+                    ExcitationType.MAGNETIZABLE,
+                    ExcitationType.CURRENT]
+
+ +
+
+ +
+ + + +
+ +
+ class Symmetry + (value, names=None, *, module=None, qualname=None, type=None, start=1) +
+ +
+ + + +
+ + Expand source code + +
class Symmetry(IntEnum):
+    """Symmetry to be used for solver. Used when deciding which formulas to use in the Boundary Element Method. The currently
+    supported symmetries are radial symmetry (also called cylindrical symmetry) and general 3D geometries.
+    """
+    RADIAL = 0
+    THREE_D = 2
+    
+    def __str__(self):
+        if self == Symmetry.RADIAL:
+            return 'radial'
+        elif self == Symmetry.THREE_D:
+            return '3d' 
+    
+    def is_2d(self):
+        return self == Symmetry.RADIAL
+        
+    def is_3d(self):
+        return self == Symmetry.THREE_D
+
+ +

Symmetry to be used for solver. Used when deciding which formulas to use in the Boundary Element Method. The currently +supported symmetries are radial symmetry (also called cylindrical symmetry) and general 3D geometries.

+ + +

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+ +

Class variables

+
+ +
var RADIAL
+
+ + + +
+
+ +
var THREE_D
+
+ + + +
+
+
+

Methods

+
+ +
+ + def is_2d(self) +
+
+ + + +
+ + Expand source code + +
def is_2d(self):
+    return self == Symmetry.RADIAL
+
+ +
+
+ + +
+ + def is_3d(self) +
+
+ + + +
+ + Expand source code + +
def is_3d(self):
+    return self == Symmetry.THREE_D
+
+ +
+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/field.html b/docs/docs/v0.9.0rc1/traceon/field.html new file mode 100644 index 0000000..86f3232 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/field.html @@ -0,0 +1,2041 @@ + + + + + + + + + + traceon.field API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.field

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+

Classes

+
+ +
+ class Field +
+ +
+ + + +
+ + Expand source code + +
class Field(ABC):
+    """The abstract `Field` class provides the method definitions that all field classes should implement. Note that
+    any child clas of the `Field` class can be passed to `traceon.tracing.Tracer` to trace particles through the field."""
+
+    def field_at_point(self, point):
+        """Convenience function for getting the field in the case that the field is purely electrostatic
+        or magneotstatic. Automatically picks one of `electrostatic_field_at_point` or `magnetostatic_field_at_point`.
+        Throws an exception when the field is both electrostatic and magnetostatic.
+
+        Parameters
+        ---------------------
+        point: (3,) np.ndarray of float64
+
+        Returns
+        --------------------
+        (3,) np.ndarray of float64. The electrostatic field \\(\\vec{E}\\) or the magnetostatic field \\(\\vec{H}\\).
+        """
+        elec, mag = self.is_electrostatic(), self.is_magnetostatic()
+        
+        if elec and not mag:
+            return self.electrostatic_field_at_point(point)
+        elif not elec and mag:
+            return self.magnetostatic_field_at_point(point)
+         
+        raise RuntimeError("Cannot use field_at_point when both electric and magnetic fields are present, " \
+            "use electrostatic_field_at_point or magnetostatic_potential_at_point")
+     
+    def potential_at_point(self, point):
+        """Convenience function for getting the potential in the case that the field is purely electrostatic
+        or magneotstatic. Automatically picks one of `electrostatic_potential_at_point` or `magnetostatic_potential_at_point`.
+        Throws an exception when the field is both electrostatic and magnetostatic.
+         
+        Parameters
+        ---------------------
+        point: (3,) np.ndarray of float64
+
+        Returns
+        --------------------
+        float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
+        """
+        elec, mag = self.is_electrostatic(), self.is_magnetostatic()
+         
+        if elec and not mag:
+            return self.electrostatic_potential_at_point(point)
+        elif not elec and mag:
+            return self.magnetostatic_potential_at_point(point) # type: ignore
+         
+        raise RuntimeError("Cannot use potential_at_point when both electric and magnetic fields are present, " \
+            "use electrostatic_potential_at_point or magnetostatic_potential_at_point")
+
+    @abstractmethod
+    def is_electrostatic(self):
+        ...
+    
+    @abstractmethod
+    def is_magnetostatic(self):
+        ...
+    
+    @abstractmethod
+    def electrostatic_potential_at_point(self, point):
+        ...
+    
+    @abstractmethod
+    def magnetostatic_field_at_point(self, point):
+        ...
+    
+    @abstractmethod
+    def electrostatic_field_at_point(self, point):
+        ...
+    
+    # Following function can be implemented to
+    # get a speedup while tracing. Return a 
+    # field function implemented in C and a ctypes
+    # argument needed. See the field_fun variable in backend/__init__.py 
+    # Note that by default it gives back a Python function, which gives no speedup
+    def get_low_level_trace_function(self):
+        fun = lambda pos, vel: (self.electrostatic_field_at_point(pos), self.magnetostatic_field_at_point(pos))
+        return backend.wrap_field_fun(fun), None
+
+ +

The abstract Field class provides the method definitions that all field classes should implement. Note that +any child clas of the Field class can be passed to Tracer to trace particles through the field.

+ + +

Ancestors

+
    +
  • abc.ABC
  • +
+ +

Subclasses

+ +

Methods

+
+ +
+ + def electrostatic_field_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def electrostatic_field_at_point(self, point):
+    ...
+
+ +
+
+ + +
+ + def electrostatic_potential_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def electrostatic_potential_at_point(self, point):
+    ...
+
+ +
+
+ + +
+ + def field_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
def field_at_point(self, point):
+    """Convenience function for getting the field in the case that the field is purely electrostatic
+    or magneotstatic. Automatically picks one of `electrostatic_field_at_point` or `magnetostatic_field_at_point`.
+    Throws an exception when the field is both electrostatic and magnetostatic.
+
+    Parameters
+    ---------------------
+    point: (3,) np.ndarray of float64
+
+    Returns
+    --------------------
+    (3,) np.ndarray of float64. The electrostatic field \\(\\vec{E}\\) or the magnetostatic field \\(\\vec{H}\\).
+    """
+    elec, mag = self.is_electrostatic(), self.is_magnetostatic()
+    
+    if elec and not mag:
+        return self.electrostatic_field_at_point(point)
+    elif not elec and mag:
+        return self.magnetostatic_field_at_point(point)
+     
+    raise RuntimeError("Cannot use field_at_point when both electric and magnetic fields are present, " \
+        "use electrostatic_field_at_point or magnetostatic_potential_at_point")
+
+ +

Convenience function for getting the field in the case that the field is purely electrostatic +or magneotstatic. Automatically picks one of electrostatic_field_at_point or magnetostatic_field_at_point. +Throws an exception when the field is both electrostatic and magnetostatic.

+

Parameters

+
+
point : (3,) np.ndarray of float64
+
 
+
+

Returns

+

(3,) np.ndarray of float64. The electrostatic field \vec{E} or the magnetostatic field \vec{H}.

+
+ + +
+ + def is_electrostatic(self) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def is_electrostatic(self):
+    ...
+
+ +
+
+ + +
+ + def is_magnetostatic(self) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def is_magnetostatic(self):
+    ...
+
+ +
+
+ + +
+ + def magnetostatic_field_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def magnetostatic_field_at_point(self, point):
+    ...
+
+ +
+
+ + +
+ + def potential_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
def potential_at_point(self, point):
+    """Convenience function for getting the potential in the case that the field is purely electrostatic
+    or magneotstatic. Automatically picks one of `electrostatic_potential_at_point` or `magnetostatic_potential_at_point`.
+    Throws an exception when the field is both electrostatic and magnetostatic.
+     
+    Parameters
+    ---------------------
+    point: (3,) np.ndarray of float64
+
+    Returns
+    --------------------
+    float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
+    """
+    elec, mag = self.is_electrostatic(), self.is_magnetostatic()
+     
+    if elec and not mag:
+        return self.electrostatic_potential_at_point(point)
+    elif not elec and mag:
+        return self.magnetostatic_potential_at_point(point) # type: ignore
+     
+    raise RuntimeError("Cannot use potential_at_point when both electric and magnetic fields are present, " \
+        "use electrostatic_potential_at_point or magnetostatic_potential_at_point")
+
+ +

Convenience function for getting the potential in the case that the field is purely electrostatic +or magneotstatic. Automatically picks one of electrostatic_potential_at_point or magnetostatic_potential_at_point. +Throws an exception when the field is both electrostatic and magnetostatic.

+

Parameters

+
+
point : (3,) np.ndarray of float64
+
 
+
+

Returns

+
+
float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
+
 
+
+
+ +
+ + + +
+ +
+ class FieldAxial + (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldAxial(Field, ABC):
+    """An electrostatic field resulting from a radial series expansion around the optical axis. You should
+    not initialize this class yourself, but it is used as a base class for the fields returned by the `axial_derivative_interpolation` methods. 
+    This base class overloads the +,*,- operators so it is very easy to take a superposition of different fields."""
+    
+    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
+        N = len(z)
+        assert z.shape == (N,)
+        assert electrostatic_coeffs is None or len(electrostatic_coeffs)== N-1
+        assert magnetostatic_coeffs is None or len(magnetostatic_coeffs) == N-1
+        assert electrostatic_coeffs is not None or magnetostatic_coeffs is not None
+        
+        assert z[0] < z[-1], "z values in axial interpolation should be ascending"
+         
+        self.z = z
+        self.electrostatic_coeffs = electrostatic_coeffs if electrostatic_coeffs is not None else np.zeros_like(magnetostatic_coeffs)
+        self.magnetostatic_coeffs = magnetostatic_coeffs if magnetostatic_coeffs is not None else np.zeros_like(electrostatic_coeffs)
+        
+        self.has_electrostatic = np.any(self.electrostatic_coeffs != 0.)
+        self.has_magnetostatic = np.any(self.magnetostatic_coeffs != 0.)
+     
+    def is_electrostatic(self):
+        return self.has_electrostatic
+
+    def is_magnetostatic(self):
+        return self.has_magnetostatic
+     
+    def __str__(self):
+        name = self.__class__.__name__
+        return f'<Traceon {name}, zmin={self.z[0]} mm, zmax={self.z[-1]} mm,\n\tNumber of samples on optical axis: {len(self.z)}>'
+     
+    def __add__(self, other):
+        if isinstance(other, FieldAxial):
+            assert np.array_equal(self.z, other.z), "Cannot add FieldAxial if optical axis sampling is different."
+            assert self.electrostatic_coeffs.shape == other.electrostatic_coeffs.shape, "Cannot add FieldAxial if shape of axial coefficients is unequal."
+            assert self.magnetostatic_coeffs.shape == other.magnetostatic_coeffs.shape, "Cannot add FieldAxial if shape of axial coefficients is unequal."
+            return self.__class__(self.z, self.electrostatic_coeffs+other.electrostatic_coeffs, self.magnetostatic_coeffs + other.magnetostatic_coeffs)
+         
+        return NotImplemented
+    
+    def __sub__(self, other):
+        return self.__add__(-other)
+    
+    def __radd__(self, other):
+        return self.__add__(other)
+     
+    def __mul__(self, other):
+        if isinstance(other, int) or isinstance(other, float):
+            return self.__class__(self.z, other*self.electrostatic_coeffs, other*self.magnetostatic_coeffs)
+         
+        return NotImplemented
+    
+    def __neg__(self):
+        return -1*self
+    
+    def __rmul__(self, other):
+        return self.__mul__(other)
+
+ +

An electrostatic field resulting from a radial series expansion around the optical axis. You should +not initialize this class yourself, but it is used as a base class for the fields returned by the axial_derivative_interpolation methods. +This base class overloads the +,*,- operators so it is very easy to take a superposition of different fields.

+ + +

Ancestors

+ + +

Subclasses

+ +

Methods

+
+ +
+ + def is_electrostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_electrostatic(self):
+    return self.has_electrostatic
+
+ +
+
+ + +
+ + def is_magnetostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_magnetostatic(self):
+    return self.has_magnetostatic
+
+ +
+
+ +
+ + +

Inherited members

+ + +
+ +
+ class FieldBEM + (electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) +
+ +
+ + + +
+ + Expand source code + +
class FieldBEM(Field, ABC):
+    """An electrostatic field (resulting from surface charges) as computed from the Boundary Element Method. You should
+    not initialize this class yourself, but it is used as a base class for the fields returned by the `solve_direct` function. 
+    This base class overloads the +,*,- operators so it is very easy to take a superposition of different fields."""
+    
+    def __init__(self, electrostatic_point_charges, magnetostatic_point_charges, current_point_charges):
+        assert all([isinstance(eff, EffectivePointCharges) for eff in [electrostatic_point_charges,
+                                                                       magnetostatic_point_charges,
+                                                                       current_point_charges]])
+        self.electrostatic_point_charges = electrostatic_point_charges
+        self.magnetostatic_point_charges = magnetostatic_point_charges
+        self.current_point_charges = current_point_charges
+        self.field_bounds = None
+     
+    def set_bounds(self, bounds):
+        """Set the field bounds. Outside the field bounds the field always returns zero (i.e. no field). Note
+        that even in 2D the field bounds needs to be specified for x,y and z axis. The trajectories in the presence
+        of magnetostatic field are in general 3D even in radial symmetric geometries.
+        
+        Parameters
+        -------------------
+        bounds: (3, 2) np.ndarray of float64
+            The min, max value of x, y, z respectively within the field is still computed.
+        """
+        self.field_bounds = np.array(bounds)
+        assert self.field_bounds.shape == (3,2)
+    
+    def is_electrostatic(self):
+        return len(self.electrostatic_point_charges) > 0
+
+    def is_magnetostatic(self):
+        return len(self.magnetostatic_point_charges) > 0 or len(self.current_point_charges) > 0 
+     
+    def __add__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__add__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__add__(other.magnetostatic_point_charges),
+            self.current_point_charges.__add__(other.current_point_charges))
+     
+    def __sub__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__sub__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__sub__(other.magnetostatic_point_charges),
+            self.current_point_charges.__sub__(other.current_point_charges))
+    
+    def __radd__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__radd__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__radd__(other.magnetostatic_point_charges),
+            self.current_point_charges.__radd__(other.current_point_charges))
+    
+    def __mul__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__mul__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__mul__(other.magnetostatic_point_charges),
+            self.current_point_charges.__mul__(other.current_point_charges))
+    
+    def __neg__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__neg__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__neg__(other.magnetostatic_point_charges),
+            self.current_point_charges.__neg__(other.current_point_charges))
+     
+    def __rmul__(self, other):
+        return self.__class__(
+            self.electrostatic_point_charges.__rmul__(other.electrostatic_point_charges),
+            self.magnetostatic_point_charges.__rmul__(other.magnetostatic_point_charges),
+            self.current_point_charges.__rmul__(other.current_point_charges))
+     
+    def area_of_elements(self, indices):
+        """Compute the total area of the elements at the given indices.
+        
+        Parameters
+        ------------
+        indices: int iterable
+            Indices giving which elements to include in the area calculation.
+
+        Returns
+        ---------------
+        The sum of the area of all elements with the given indices.
+        """
+        return sum(self.area_of_element(i) for i in indices) 
+
+    @abstractmethod
+    def area_of_element(self, i: int) -> float:
+        ...
+    
+    def charge_on_element(self, i):
+        return self.area_of_element(i) * self.electrostatic_point_charges.charges[i]
+    
+    def charge_on_elements(self, indices):
+        """Compute the sum of the charges present on the elements with the given indices. To
+        get the total charge of a physical group use `names['name']` for indices where `names` 
+        is returned by `traceon.excitation.Excitation.get_electrostatic_active_elements()`.
+
+        Parameters
+        ----------
+        indices: (N,) array of int
+            indices of the elements contributing to the charge sum. 
+         
+        Returns
+        -------
+        The sum of the charge. See the note about units on the front page."""
+        return sum(self.charge_on_element(i) for i in indices)
+    
+    def __str__(self):
+        name = self.__class__.__name__
+        return f'<Traceon {name}\n' \
+            f'\tNumber of electrostatic points: {len(self.electrostatic_point_charges)}\n' \
+            f'\tNumber of magnetizable points: {len(self.magnetostatic_point_charges)}\n' \
+            f'\tNumber of current rings: {len(self.current_point_charges)}>'
+    
+    @abstractmethod
+    def current_field_at_point(self, point_):
+        ...
+
+ +

An electrostatic field (resulting from surface charges) as computed from the Boundary Element Method. You should +not initialize this class yourself, but it is used as a base class for the fields returned by the solve_direct function. +This base class overloads the +,*,- operators so it is very easy to take a superposition of different fields.

+ + +

Ancestors

+ + +

Subclasses

+ +

Methods

+
+ +
+ + def area_of_element(self, i: int) ‑> float +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def area_of_element(self, i: int) -> float:
+    ...
+
+ +
+
+ + +
+ + def area_of_elements(self, indices) +
+
+ + + +
+ + Expand source code + +
def area_of_elements(self, indices):
+    """Compute the total area of the elements at the given indices.
+    
+    Parameters
+    ------------
+    indices: int iterable
+        Indices giving which elements to include in the area calculation.
+
+    Returns
+    ---------------
+    The sum of the area of all elements with the given indices.
+    """
+    return sum(self.area_of_element(i) for i in indices) 
+
+ +

Compute the total area of the elements at the given indices.

+

Parameters

+
+
indices : int iterable
+
Indices giving which elements to include in the area calculation.
+
+

Returns

+

The sum of the area of all elements with the given indices.

+
+ + +
+ + def charge_on_element(self, i) +
+
+ + + +
+ + Expand source code + +
def charge_on_element(self, i):
+    return self.area_of_element(i) * self.electrostatic_point_charges.charges[i]
+
+ +
+
+ + +
+ + def charge_on_elements(self, indices) +
+
+ + + +
+ + Expand source code + +
def charge_on_elements(self, indices):
+    """Compute the sum of the charges present on the elements with the given indices. To
+    get the total charge of a physical group use `names['name']` for indices where `names` 
+    is returned by `traceon.excitation.Excitation.get_electrostatic_active_elements()`.
+
+    Parameters
+    ----------
+    indices: (N,) array of int
+        indices of the elements contributing to the charge sum. 
+     
+    Returns
+    -------
+    The sum of the charge. See the note about units on the front page."""
+    return sum(self.charge_on_element(i) for i in indices)
+
+ +

Compute the sum of the charges present on the elements with the given indices. To +get the total charge of a physical group use names['name'] for indices where names +is returned by Excitation.get_electrostatic_active_elements().

+

Parameters

+
+
indices : (N,) array of int
+
indices of the elements contributing to the charge sum.
+
+

Returns

+

The sum of the charge. See the note about units on the front page.

+
+ + +
+ + def current_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def current_field_at_point(self, point_):
+    ...
+
+ +
+
+ + +
+ + def is_electrostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_electrostatic(self):
+    return len(self.electrostatic_point_charges) > 0
+
+ +
+
+ + +
+ + def is_magnetostatic(self) +
+
+ + + +
+ + Expand source code + +
def is_magnetostatic(self):
+    return len(self.magnetostatic_point_charges) > 0 or len(self.current_point_charges) > 0 
+
+ +
+
+ + +
+ + def set_bounds(self, bounds) +
+
+ + + +
+ + Expand source code + +
def set_bounds(self, bounds):
+    """Set the field bounds. Outside the field bounds the field always returns zero (i.e. no field). Note
+    that even in 2D the field bounds needs to be specified for x,y and z axis. The trajectories in the presence
+    of magnetostatic field are in general 3D even in radial symmetric geometries.
+    
+    Parameters
+    -------------------
+    bounds: (3, 2) np.ndarray of float64
+        The min, max value of x, y, z respectively within the field is still computed.
+    """
+    self.field_bounds = np.array(bounds)
+    assert self.field_bounds.shape == (3,2)
+
+ +

Set the field bounds. Outside the field bounds the field always returns zero (i.e. no field). Note +that even in 2D the field bounds needs to be specified for x,y and z axis. The trajectories in the presence +of magnetostatic field are in general 3D even in radial symmetric geometries.

+

Parameters

+
+
bounds : (3, 2) np.ndarray of float64
+
The min, max value of x, y, z respectively within the field is still computed.
+
+
+ +
+ + +

Inherited members

+ + +
+ +
+ class FieldRadialAxial + (field, zmin, zmax, N=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldRadialAxial(FieldAxial):
+    """ """
+    def __init__(self, field, zmin, zmax, N=None):
+        assert isinstance(field, FieldRadialBEM)
+
+        z, electrostatic_coeffs, magnetostatic_coeffs = FieldRadialAxial._get_interpolation_coefficients(field, zmin, zmax, N=N)
+        
+        super().__init__(z, electrostatic_coeffs, magnetostatic_coeffs)
+        
+        assert self.electrostatic_coeffs.shape == (len(z)-1, backend.DERIV_2D_MAX, 6)
+        assert self.magnetostatic_coeffs.shape == (len(z)-1, backend.DERIV_2D_MAX, 6)
+    
+    @staticmethod
+    def _get_interpolation_coefficients(field: FieldRadialBEM, zmin, zmax, N=None):
+        assert zmax > zmin, "zmax should be bigger than zmin"
+
+        N_charges = max(len(field.electrostatic_point_charges.charges), len(field.magnetostatic_point_charges.charges))
+        N = N if N is not None else int(FACTOR_AXIAL_DERIV_SAMPLING_2D*N_charges)
+        z = np.linspace(zmin, zmax, N)
+        
+        st = time.time()
+        elec_derivs = np.concatenate(util.split_collect(field.get_electrostatic_axial_potential_derivatives, z), axis=0)
+        elec_coeffs = _quintic_spline_coefficients(z, elec_derivs.T)
+        
+        mag_derivs = np.concatenate(util.split_collect(field.get_magnetostatic_axial_potential_derivatives, z), axis=0)
+        mag_coeffs = _quintic_spline_coefficients(z, mag_derivs.T)
+        
+        logging.log_info(f'Computing derivative interpolation took {(time.time()-st)*1000:.2f} ms ({len(z)} items)')
+
+        return z, elec_coeffs, mag_coeffs
+     
+    def electrostatic_field_at_point(self, point_):
+        """
+        Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\)
+        
+        Parameters
+        ----------
+        point: (2,) array of float64
+            Position at which to compute the field.
+             
+        Returns
+        -------
+        Numpy array containing the field strengths (in units of V/mm) in the r and z directions.
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        return backend.field_radial_derivs(point, self.z, self.electrostatic_coeffs)
+    
+    def magnetostatic_field_at_point(self, point_):
+        """
+        Compute the magnetic field \\( \\vec{H} \\)
+        
+        Parameters
+        ----------
+        point: (2,) array of float64
+            Position at which to compute the field.
+             
+        Returns
+        -------
+        (2,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        return backend.field_radial_derivs(point, self.z, self.magnetostatic_coeffs)
+     
+    def electrostatic_potential_at_point(self, point_):
+        """
+        Compute the electrostatic potential (close to the axis).
+
+        Parameters
+        ----------
+        point: (2,) array of float64
+            Position at which to compute the potential.
+        
+        Returns
+        -------
+        Potential as a float value (in units of V).
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        return backend.potential_radial_derivs(point, self.z, self.electrostatic_coeffs)
+    
+    def magnetostatic_potential_at_point(self, point_):
+        """
+        Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\)) close to the axis
+        
+        Parameters
+        ----------
+        point: (2,) array of float64
+            Position at which to compute the field.
+        
+        Returns
+        -------
+        Potential as a float value (in units of A).
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
+    
+    def get_tracer(self, bounds):
+        return T.Tracer(self, bounds)
+    
+    def get_low_level_trace_function(self):
+        args = backend.FieldDerivsArgs(self.z, self.electrostatic_coeffs, self.magnetostatic_coeffs)
+        return backend.field_fun(("field_radial_derivs_traceable", backend.backend_lib)), args
+
+ +
+ + +

Ancestors

+ + +

Methods

+
+ +
+ + def electrostatic_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def electrostatic_field_at_point(self, point_):
+    """
+    Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\)
+    
+    Parameters
+    ----------
+    point: (2,) array of float64
+        Position at which to compute the field.
+         
+    Returns
+    -------
+    Numpy array containing the field strengths (in units of V/mm) in the r and z directions.
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    return backend.field_radial_derivs(point, self.z, self.electrostatic_coeffs)
+
+ +

Compute the electric field, \vec{E} = -\nabla \phi

+

Parameters

+
+
point : (2,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Numpy array containing the field strengths (in units of V/mm) in the r and z directions.

+
+ + +
+ + def electrostatic_potential_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def electrostatic_potential_at_point(self, point_):
+    """
+    Compute the electrostatic potential (close to the axis).
+
+    Parameters
+    ----------
+    point: (2,) array of float64
+        Position at which to compute the potential.
+    
+    Returns
+    -------
+    Potential as a float value (in units of V).
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    return backend.potential_radial_derivs(point, self.z, self.electrostatic_coeffs)
+
+ +

Compute the electrostatic potential (close to the axis).

+

Parameters

+
+
point : (2,) array of float64
+
Position at which to compute the potential.
+
+

Returns

+

Potential as a float value (in units of V).

+
+ + +
+ + def get_tracer(self, bounds) +
+
+ + + +
+ + Expand source code + +
def get_tracer(self, bounds):
+    return T.Tracer(self, bounds)
+
+ +
+
+ + +
+ + def magnetostatic_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def magnetostatic_field_at_point(self, point_):
+    """
+    Compute the magnetic field \\( \\vec{H} \\)
+    
+    Parameters
+    ----------
+    point: (2,) array of float64
+        Position at which to compute the field.
+         
+    Returns
+    -------
+    (2,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    return backend.field_radial_derivs(point, self.z, self.magnetostatic_coeffs)
+
+ +

Compute the magnetic field \vec{H}

+

Parameters

+
+
point : (2,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(2,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.

+
+ + +
+ + def magnetostatic_potential_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def magnetostatic_potential_at_point(self, point_):
+    """
+    Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\)) close to the axis
+    
+    Parameters
+    ----------
+    point: (2,) array of float64
+        Position at which to compute the field.
+    
+    Returns
+    -------
+    Potential as a float value (in units of A).
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
+
+ +

Compute the magnetostatic scalar potential (satisfying \vec{H} = -\nabla \phi ) close to the axis

+

Parameters

+
+
point : (2,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of A).

+
+ +
+ + +

Inherited members

+ + +
+ +
+ class FieldRadialBEM + (electrostatic_point_charges=None,
magnetostatic_point_charges=None,
current_point_charges=None)
+
+ +
+ + + +
+ + Expand source code + +
class FieldRadialBEM(FieldBEM):
+    """A radially symmetric electrostatic field. The field is a result of the surface charges as computed by the
+    `solve_direct` function. See the comments in `FieldBEM`."""
+    
+    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None, current_point_charges=None):
+        if electrostatic_point_charges is None:
+            electrostatic_point_charges = EffectivePointCharges.empty_2d()
+        if magnetostatic_point_charges is None:
+            magnetostatic_point_charges = EffectivePointCharges.empty_2d()
+        if current_point_charges is None:
+            current_point_charges = EffectivePointCharges.empty_3d()
+         
+        self.symmetry = E.Symmetry.RADIAL
+        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges)
+         
+    def current_field_at_point(self, point_):
+        point = np.array(point_, dtype=np.double)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+            
+        currents = self.current_point_charges.charges
+        jacobians = self.current_point_charges.jacobians
+        positions = self.current_point_charges.positions
+        return backend.current_field_radial(point, currents, jacobians, positions)
+     
+    def electrostatic_field_at_point(self, point_):
+        """
+        Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\)
+        
+        Parameters
+        ----------
+        point: (3,) array of float64
+            Position at which to compute the field.
+        
+        Returns
+        -------
+        (3,) array of float64, containing the field strengths (units of V/m)
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+          
+        charges = self.electrostatic_point_charges.charges
+        jacobians = self.electrostatic_point_charges.jacobians
+        positions = self.electrostatic_point_charges.positions
+        return backend.field_radial(point, charges, jacobians, positions)
+     
+    def electrostatic_potential_at_point(self, point_):
+        """
+        Compute the electrostatic potential.
+        
+        Parameters
+        ----------
+        point: (3,) array of float64
+            Position at which to compute the field.
+        
+        Returns
+        -------
+        Potential as a float value (in units of V).
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        charges = self.electrostatic_point_charges.charges
+        jacobians = self.electrostatic_point_charges.jacobians
+        positions = self.electrostatic_point_charges.positions
+        return backend.potential_radial(point, charges, jacobians, positions)
+    
+    def magnetostatic_field_at_point(self, point_):
+        """
+        Compute the magnetic field \\( \\vec{H} \\)
+        
+        Parameters
+        ----------
+        point: (3,) array of float64
+            Position at which to compute the field.
+             
+        Returns
+        -------
+        (3,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        current_field = self.current_field_at_point(point)
+        
+        charges = self.magnetostatic_point_charges.charges
+        jacobians = self.magnetostatic_point_charges.jacobians
+        positions = self.magnetostatic_point_charges.positions
+        
+        mag_field = backend.field_radial(point, charges, jacobians, positions)
+
+        return current_field + mag_field
+
+    def magnetostatic_potential_at_point(self, point_):
+        """
+        Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\))
+        
+        Parameters
+        ----------
+        point: (3,) array of float64
+            Position at which to compute the field.
+        
+        Returns
+        -------
+        Potential as a float value (in units of A).
+        """
+        point = np.array(point_)
+        assert point.shape == (3,), "Please supply a three dimensional point"
+        charges = self.magnetostatic_point_charges.charges
+        jacobians = self.magnetostatic_point_charges.jacobians
+        positions = self.magnetostatic_point_charges.positions
+        return backend.potential_radial(point, charges, jacobians, positions)
+    
+    def current_potential_axial(self, z):
+        assert isinstance(z, float)
+        currents = self.current_point_charges.charges
+        jacobians = self.current_point_charges.jacobians
+        positions = self.current_point_charges.positions
+        return backend.current_potential_axial(z, currents, jacobians, positions)
+     
+    def get_electrostatic_axial_potential_derivatives(self, z):
+        """
+        Compute the derivatives of the electrostatic potential a points on the optical axis (z-axis). 
+         
+        Parameters
+        ----------
+        z : (N,) np.ndarray of float64
+            Positions on the optical axis at which to compute the derivatives.
+
+        Returns
+        ------- 
+        Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+        at position 0 the potential itself is returned). The highest derivative returned is a 
+        constant currently set to 9."""
+        charges = self.electrostatic_point_charges.charges
+        jacobians = self.electrostatic_point_charges.jacobians
+        positions = self.electrostatic_point_charges.positions
+        return backend.axial_derivatives_radial(z, charges, jacobians, positions)
+    
+    def get_magnetostatic_axial_potential_derivatives(self, z):
+        """
+        Compute the derivatives of the magnetostatic potential at points on the optical axis (z-axis). 
+         
+        Parameters
+        ----------
+        z : (N,) np.ndarray of float64
+            Positions on the optical axis at which to compute the derivatives.
+
+        Returns
+        ------- 
+        Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+        at position 0 the potential itself is returned). The highest derivative returned is a 
+        constant currently set to 9."""
+        charges = self.magnetostatic_point_charges.charges
+        jacobians = self.magnetostatic_point_charges.jacobians
+        positions = self.magnetostatic_point_charges.positions
+         
+        derivs_magnetic = backend.axial_derivatives_radial(z, charges, jacobians, positions)
+        derivs_current = self.get_current_axial_potential_derivatives(z)
+        return derivs_magnetic + derivs_current
+     
+    def get_current_axial_potential_derivatives(self, z):
+        """
+        Compute the derivatives of the current magnetostatic scalar potential at points on the optical axis.
+         
+        Parameters
+        ----------
+        z : (N,) np.ndarray of float64
+            Positions on the optical axis at which to compute the derivatives.
+         
+        Returns
+        ------- 
+        Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+        at position 0 the potential itself is returned). The highest derivative returned is a 
+        constant currently set to 9."""
+
+        currents = self.current_point_charges.charges
+        jacobians = self.current_point_charges.jacobians
+        positions = self.current_point_charges.positions
+        return backend.current_axial_derivatives_radial(z, currents, jacobians, positions)
+      
+    def area_of_element(self, i):
+        jacobians = self.electrostatic_point_charges.jacobians
+        positions = self.electrostatic_point_charges.positions
+        return 2*np.pi*np.sum(jacobians[i] * positions[i, :, 0])
+    
+    def get_tracer(self, bounds):
+        return T.Tracer(self, bounds)
+    
+    def get_low_level_trace_function(self):
+        args = backend.FieldEvaluationArgsRadial(self.electrostatic_point_charges, self.magnetostatic_point_charges, self.current_point_charges, self.field_bounds)
+        return backend.field_fun(("field_radial_traceable", backend.backend_lib)), args
+
+ +

A radially symmetric electrostatic field. The field is a result of the surface charges as computed by the +solve_direct function. See the comments in FieldBEM.

+ + +

Ancestors

+ + +

Methods

+
+ +
+ + def area_of_element(self, i) +
+
+ + + +
+ + Expand source code + +
def area_of_element(self, i):
+    jacobians = self.electrostatic_point_charges.jacobians
+    positions = self.electrostatic_point_charges.positions
+    return 2*np.pi*np.sum(jacobians[i] * positions[i, :, 0])
+
+ +
+
+ + +
+ + def current_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def current_field_at_point(self, point_):
+    point = np.array(point_, dtype=np.double)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+        
+    currents = self.current_point_charges.charges
+    jacobians = self.current_point_charges.jacobians
+    positions = self.current_point_charges.positions
+    return backend.current_field_radial(point, currents, jacobians, positions)
+
+ +
+
+ + +
+ + def current_potential_axial(self, z) +
+
+ + + +
+ + Expand source code + +
def current_potential_axial(self, z):
+    assert isinstance(z, float)
+    currents = self.current_point_charges.charges
+    jacobians = self.current_point_charges.jacobians
+    positions = self.current_point_charges.positions
+    return backend.current_potential_axial(z, currents, jacobians, positions)
+
+ +
+
+ + +
+ + def electrostatic_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def electrostatic_field_at_point(self, point_):
+    """
+    Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\)
+    
+    Parameters
+    ----------
+    point: (3,) array of float64
+        Position at which to compute the field.
+    
+    Returns
+    -------
+    (3,) array of float64, containing the field strengths (units of V/m)
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+      
+    charges = self.electrostatic_point_charges.charges
+    jacobians = self.electrostatic_point_charges.jacobians
+    positions = self.electrostatic_point_charges.positions
+    return backend.field_radial(point, charges, jacobians, positions)
+
+ +

Compute the electric field, \vec{E} = -\nabla \phi

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(3,) array of float64, containing the field strengths (units of V/m)

+
+ + +
+ + def electrostatic_potential_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def electrostatic_potential_at_point(self, point_):
+    """
+    Compute the electrostatic potential.
+    
+    Parameters
+    ----------
+    point: (3,) array of float64
+        Position at which to compute the field.
+    
+    Returns
+    -------
+    Potential as a float value (in units of V).
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    charges = self.electrostatic_point_charges.charges
+    jacobians = self.electrostatic_point_charges.jacobians
+    positions = self.electrostatic_point_charges.positions
+    return backend.potential_radial(point, charges, jacobians, positions)
+
+ +

Compute the electrostatic potential.

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of V).

+
+ + +
+ + def get_current_axial_potential_derivatives(self, z) +
+
+ + + +
+ + Expand source code + +
def get_current_axial_potential_derivatives(self, z):
+    """
+    Compute the derivatives of the current magnetostatic scalar potential at points on the optical axis.
+     
+    Parameters
+    ----------
+    z : (N,) np.ndarray of float64
+        Positions on the optical axis at which to compute the derivatives.
+     
+    Returns
+    ------- 
+    Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+    at position 0 the potential itself is returned). The highest derivative returned is a 
+    constant currently set to 9."""
+
+    currents = self.current_point_charges.charges
+    jacobians = self.current_point_charges.jacobians
+    positions = self.current_point_charges.positions
+    return backend.current_axial_derivatives_radial(z, currents, jacobians, positions)
+
+ +

Compute the derivatives of the current magnetostatic scalar potential at points on the optical axis.

+

Parameters

+
+
z : (N,) np.ndarray of float64
+
Positions on the optical axis at which to compute the derivatives.
+
+

Returns

+

Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so +at position 0 the potential itself is returned). The highest derivative returned is a +constant currently set to 9.

+
+ + +
+ + def get_electrostatic_axial_potential_derivatives(self, z) +
+
+ + + +
+ + Expand source code + +
def get_electrostatic_axial_potential_derivatives(self, z):
+    """
+    Compute the derivatives of the electrostatic potential a points on the optical axis (z-axis). 
+     
+    Parameters
+    ----------
+    z : (N,) np.ndarray of float64
+        Positions on the optical axis at which to compute the derivatives.
+
+    Returns
+    ------- 
+    Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+    at position 0 the potential itself is returned). The highest derivative returned is a 
+    constant currently set to 9."""
+    charges = self.electrostatic_point_charges.charges
+    jacobians = self.electrostatic_point_charges.jacobians
+    positions = self.electrostatic_point_charges.positions
+    return backend.axial_derivatives_radial(z, charges, jacobians, positions)
+
+ +

Compute the derivatives of the electrostatic potential a points on the optical axis (z-axis).

+

Parameters

+
+
z : (N,) np.ndarray of float64
+
Positions on the optical axis at which to compute the derivatives.
+
+

Returns

+

Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so +at position 0 the potential itself is returned). The highest derivative returned is a +constant currently set to 9.

+
+ + +
+ + def get_magnetostatic_axial_potential_derivatives(self, z) +
+
+ + + +
+ + Expand source code + +
def get_magnetostatic_axial_potential_derivatives(self, z):
+    """
+    Compute the derivatives of the magnetostatic potential at points on the optical axis (z-axis). 
+     
+    Parameters
+    ----------
+    z : (N,) np.ndarray of float64
+        Positions on the optical axis at which to compute the derivatives.
+
+    Returns
+    ------- 
+    Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so
+    at position 0 the potential itself is returned). The highest derivative returned is a 
+    constant currently set to 9."""
+    charges = self.magnetostatic_point_charges.charges
+    jacobians = self.magnetostatic_point_charges.jacobians
+    positions = self.magnetostatic_point_charges.positions
+     
+    derivs_magnetic = backend.axial_derivatives_radial(z, charges, jacobians, positions)
+    derivs_current = self.get_current_axial_potential_derivatives(z)
+    return derivs_magnetic + derivs_current
+
+ +

Compute the derivatives of the magnetostatic potential at points on the optical axis (z-axis).

+

Parameters

+
+
z : (N,) np.ndarray of float64
+
Positions on the optical axis at which to compute the derivatives.
+
+

Returns

+

Numpy array of shape (N, 9) containing the derivatives. At index i one finds the i-th derivative (so +at position 0 the potential itself is returned). The highest derivative returned is a +constant currently set to 9.

+
+ + +
+ + def get_tracer(self, bounds) +
+
+ + + +
+ + Expand source code + +
def get_tracer(self, bounds):
+    return T.Tracer(self, bounds)
+
+ +
+
+ + +
+ + def magnetostatic_field_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def magnetostatic_field_at_point(self, point_):
+    """
+    Compute the magnetic field \\( \\vec{H} \\)
+    
+    Parameters
+    ----------
+    point: (3,) array of float64
+        Position at which to compute the field.
+         
+    Returns
+    -------
+    (3,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    current_field = self.current_field_at_point(point)
+    
+    charges = self.magnetostatic_point_charges.charges
+    jacobians = self.magnetostatic_point_charges.jacobians
+    positions = self.magnetostatic_point_charges.positions
+    
+    mag_field = backend.field_radial(point, charges, jacobians, positions)
+
+    return current_field + mag_field
+
+ +

Compute the magnetic field \vec{H}

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(3,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.

+
+ + +
+ + def magnetostatic_potential_at_point(self, point_) +
+
+ + + +
+ + Expand source code + +
def magnetostatic_potential_at_point(self, point_):
+    """
+    Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\))
+    
+    Parameters
+    ----------
+    point: (3,) array of float64
+        Position at which to compute the field.
+    
+    Returns
+    -------
+    Potential as a float value (in units of A).
+    """
+    point = np.array(point_)
+    assert point.shape == (3,), "Please supply a three dimensional point"
+    charges = self.magnetostatic_point_charges.charges
+    jacobians = self.magnetostatic_point_charges.jacobians
+    positions = self.magnetostatic_point_charges.positions
+    return backend.potential_radial(point, charges, jacobians, positions)
+
+ +

Compute the magnetostatic scalar potential (satisfying \vec{H} = -\nabla \phi )

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of A).

+
+ +
+ + +

Inherited members

+ + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/focus.html b/docs/docs/v0.9.0rc1/traceon/focus.html new file mode 100644 index 0000000..47180fd --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/focus.html @@ -0,0 +1,199 @@ + + + + + + + + + + traceon.focus API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.focus

+
+ +
+

Module containing a single function to find the focus of a beam of electron trajecories.

+
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def focus_position(positions) +
+
+ + + +
+ + Expand source code + +
def focus_position(positions):
+    """
+    Find the focus of the given trajectories (which are returned from `traceon.tracing.Tracer.__call__`).
+    The focus is found using a least square method by considering the final positions and velocities of
+    the given trajectories and linearly extending the trajectories backwards.
+     
+    
+    Parameters
+    ------------
+    positions: iterable of (N,6) np.ndarray float64
+        Trajectories of electrons, as returned by `traceon.tracing.Tracer.__call__`
+    
+    
+    Returns
+    --------------
+    (3,) np.ndarray of float64, representing the position of the focus
+    """
+    assert all(p.shape == (len(p), 6) for p in positions)
+     
+    angles_x = np.array([p[-1, 3]/p[-1, 5] for p in positions])
+    angles_y = np.array([p[-1, 4]/p[-1, 5] for p in positions])
+    x, y, z = [np.array([p[-1, i] for p in positions]) for i in [0, 1, 2]]
+     
+    N = len(positions)
+    first_column = np.concatenate( (-angles_x, -angles_y) )
+    second_column = np.concatenate( (np.ones(N), np.zeros(N)) )
+    third_column = np.concatenate( (np.zeros(N), np.ones(N)) )
+    right_hand_side = np.concatenate( (x - z*angles_x, y - z*angles_y) )
+     
+    (z, x, y) = np.linalg.lstsq(
+        np.array([first_column, second_column, third_column]).T,
+        right_hand_side, rcond=None)[0]
+    
+    return np.array([x, y, z])
+
+ +

Find the focus of the given trajectories (which are returned from Tracer.__call__()). +The focus is found using a least square method by considering the final positions and velocities of +the given trajectories and linearly extending the trajectories backwards.

+

Parameters

+
+
positions : iterable of (N,6) np.ndarray float64
+
Trajectories of electrons, as returned by Tracer.__call__()
+
+

Returns

+

(3,) np.ndarray of float64, representing the position of the focus

+
+ +
+
+ +
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/geometry.html b/docs/docs/v0.9.0rc1/traceon/geometry.html new file mode 100644 index 0000000..cf4db93 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/geometry.html @@ -0,0 +1,5326 @@ + + + + + + + + + + traceon.geometry API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.geometry

+
+ +
+

The geometry module allows the creation of general meshes in 2D and 3D. +The builtin mesher uses so called parametric meshes, meaning +that for any mesh we construct a mathematical formula mapping to points on the mesh. This makes it +easy to generate structured (or transfinite) meshes. These meshes usually help the mesh to converge +to the right answer faster, since the symmetries of the mesh (radial, multipole, etc.) are better +represented.

+

The parametric mesher also has downsides, since it's for example harder to generate meshes with +lots of holes in them (the 'cut' operation is not supported). For these cases, Traceon makes it easy to import +meshes generated by other programs (e.g. GMSH or Comsol). Traceon can import meshio meshes +or any file format supported by meshio.

+
+ +
+
+ +
+
+ +
+
+ +
+

Classes

+
+ +
+ class Path + (fun, path_length, breakpoints=[], name=None) +
+ +
+ + + +
+ + Expand source code + +
class Path(GeometricObject):
+    """A path is a mapping from a number in the range [0, path_length] to a three dimensional point. Note that `Path` is a
+    subclass of `traceon.mesher.GeometricObject`, and therefore can be easily moved and rotated."""
+    
+    def __init__(self, fun, path_length, breakpoints=[], name=None):
+        # Assumption: fun takes in p, the path length
+        # and returns the point on the path
+        self.fun = fun
+        self.path_length = path_length
+        self.breakpoints = breakpoints
+        self.name = name
+    
+    @staticmethod
+    def from_irregular_function(to_point, N=100, breakpoints=[]):
+        """Construct a path from a function that is of the form u -> point, where 0 <= u <= 1.
+        The length of the path is determined by integration.
+
+        Parameters
+        ---------------------------------
+        to_point: callable
+            A function accepting a number in the range [0, 1] and returns a the dimensional point.
+        N: int
+            Number of samples to use in the cubic spline interpolation.
+        breakpoints: float iterable
+            Points (0 <= u <= 1) on the path where the function is non-differentiable. These points
+            are always included in the resulting mesh.
+
+        Returns
+        ---------------------------------
+        Path"""
+         
+        # path length = integrate |f'(x)|
+        fun = lambda u: np.array(to_point(u))
+        
+        u = np.linspace(0, 1, N)
+        samples = CubicSpline(u, [fun(u_) for u_ in u])
+        derivatives = samples.derivative()(u)
+        norm_derivatives = np.linalg.norm(derivatives, axis=1)
+        path_lengths = CubicSpline(u, norm_derivatives).antiderivative()(u)
+        interpolation = CubicSpline(path_lengths, u) # Path length to [0,1]
+
+        path_length = path_lengths[-1]
+        
+        return Path(lambda pl: fun(interpolation(pl)), path_length, breakpoints=[b*path_length for b in breakpoints])
+    
+    @staticmethod
+    def spline_through_points(points, N=100):
+        """Construct a path by fitting a cubic spline through the given points.
+
+        Parameters
+        -------------------------
+        points: (N, 3) ndarray of float
+            Three dimensional points through which the spline is fitted.
+
+        Returns
+        -------------------------
+        Path"""
+
+        x = np.linspace(0, 1, len(points))
+        interp = CubicSpline(x, points)
+        return Path.from_irregular_function(interp, N=N)
+     
+    def average(self, fun):
+        """Average a function along the path, by integrating 1/l * fun(path(l)) with 0 <= l <= path length.
+
+        Parameters
+        --------------------------
+        fun: callable (3,) -> float
+            A function taking a three dimensional point and returning a float.
+
+        Returns
+        -------------------------
+        float
+
+        The average value of the function along the point."""
+        return quad(lambda s: fun(self(s)), 0, self.path_length, points=self.breakpoints)[0]/self.path_length
+     
+    def map_points(self, fun):
+        """Return a new function by mapping a function over points along the path (see `traceon.mesher.GeometricObject`).
+        The path length is assumed to stay the same after this operation.
+        
+        Parameters
+        ----------------------------
+        fun: callable (3,) -> (3,)
+            Function taking three dimensional points and returning three dimensional points.
+
+        Returns
+        ---------------------------      
+
+        Path"""
+        return Path(lambda u: fun(self(u)), self.path_length, self.breakpoints, name=self.name)
+     
+    def __call__(self, t):
+        """Evaluate a point along the path.
+
+        Parameters
+        ------------------------
+        t: float
+            The length along the path.
+
+        Returns
+        ------------------------
+        (3,) float
+
+        Three dimensional point."""
+        return self.fun(t)
+     
+    def is_closed(self):
+        """Determine whether the path is closed, by comparing the starting and endpoint.
+
+        Returns
+        ----------------------
+        bool: True if the path is closed, False otherwise."""
+        return _points_close(self.starting_point(), self.endpoint())
+    
+    def add_phase(self, l):
+        """Add a phase to a closed path. A path is closed when the starting point is equal to the
+        end point. A phase of length l means that the path starts 'further down' the closed path.
+
+        Parameters
+        --------------------
+        l: float
+            The phase (expressed as a path length). The resulting path starts l distance along the 
+            original path.
+
+        Returns
+        --------------------
+        Path"""
+        assert self.is_closed()
+        
+        def fun(u):
+            return self( (l + u) % self.path_length )
+        
+        return Path(fun, self.path_length, sorted([(b-l)%self.path_length for b in self.breakpoints + [0.]]), name=self.name)
+     
+    def __rshift__(self, other):
+        """Combine two paths to create a single path. The endpoint of the first path needs
+        to match the starting point of the second path. This common point is marked as a breakpoint and
+        always included in the mesh. To use this function use the right shift operator (p1 >> p2).
+
+        Parameters
+        -----------------------
+        other: Path
+            The second path, to extend the current path.
+
+        Returns
+        -----------------------
+        Path"""
+
+        assert isinstance(other, Path), "Exteding path with object that is not actually a Path"
+
+        assert _points_close(self.endpoint(), other.starting_point())
+
+        total = self.path_length + other.path_length
+         
+        def f(t):
+            assert 0 <= t <= total
+            
+            if t <= self.path_length:
+                return self(t)
+            else:
+                return other(t - self.path_length)
+        
+        return Path(f, total, self.breakpoints + [self.path_length] + other.breakpoints, name=self.name)
+
+    def starting_point(self):
+        """Returns the starting point of the path.
+
+        Returns
+        ---------------------
+        (3,) float
+
+        The starting point of the path."""
+        return self(0.)
+    
+    def middle_point(self):
+        """Returns the midpoint of the path (in terms of length along the path.)
+
+        Returns
+        ----------------------
+        (3,) float
+        
+        The point at the middle of the path."""
+        return self(self.path_length/2)
+    
+    def endpoint(self):
+        """Returns the endpoint of the path.
+
+        Returns
+        ------------------------
+        (3,) float
+        
+        The endpoint of the path."""
+        return self(self.path_length)
+    
+    def line_to(self, point):
+        """Extend the current path by a line from the current endpoint to the given point.
+        The given point is marked a breakpoint.
+
+        Parameters
+        ----------------------
+        point: (3,) float
+            The new endpoint.
+
+        Returns
+        ---------------------
+        Path"""
+        warnings.warn("line_to() is deprecated and will be removed in version 0.8.0."
+        "Use extend_with_line() instead.",
+        DeprecationWarning,
+        stacklevel=2)
+
+        point = np.array(point)
+        assert point.shape == (3,), "Please supply a three dimensional point to .line_to(...)"
+        l = Path.line(self.endpoint(), point)
+        return self >> l
+    
+    def extend_with_line(self, point):
+        """Extend the current path by a line from the current endpoint to the given point.
+        The given point is marked a breakpoint.
+
+        Parameters
+        ----------------------
+        point: (3,) float
+            The new endpoint.
+
+        Returns
+        ---------------------
+        Path"""
+        point = np.array(point)
+        assert point.shape == (3,), "Please supply a three dimensional point to .extend_with_line(...)"
+        l = Path.line(self.endpoint(), point)
+        return self >> l
+     
+    @staticmethod
+    def circle_xz(x0, z0, radius, angle=2*pi):
+        """Returns (part of) a circle in the XZ plane around the x-axis. Starting on the positive x-axis.
+        
+        Parameters
+        --------------------------------
+        x0: float
+            x-coordinate of the center of the circle
+        z0: float
+            z-coordinate of the center of the circle
+        radius: float
+            radius of the circle
+        angle: float
+            The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+        Returns
+        ---------------------------------
+        Path"""
+        def f(u):
+            theta = u / radius 
+            return np.array([radius*cos(theta), 0., radius*sin(theta)])
+        return Path(f, angle*radius).move(dx=x0, dz=z0)
+    
+    @staticmethod
+    def circle_yz(y0, z0, radius, angle=2*pi):
+        """Returns (part of) a circle in the YZ plane around the x-axis. Starting on the positive y-axis.
+        
+        Parameters
+        --------------------------------
+        y0: float
+            x-coordinate of the center of the circle
+        z0: float
+            z-coordinate of the center of the circle
+        radius: float
+            radius of the circle
+        angle: float
+            The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+        Returns
+        ---------------------------------
+        Path"""
+        def f(u):
+            theta = u / radius 
+            return np.array([0., radius*cos(theta), radius*sin(theta)])
+        return Path(f, angle*radius).move(dy=y0, dz=z0)
+    
+    @staticmethod
+    def circle_xy(x0, y0, radius, angle=2*pi):
+        """Returns (part of) a circle in the XY plane around the z-axis. Starting on the positive X-axis.
+        
+        Parameters
+        --------------------------------
+        x0: float
+            x-coordinate of the center of the circle
+        y0: float
+            y-coordinate of the center of the circle
+        radius: float
+            radius of the circle
+        angle: float
+            The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+        Returns
+        ---------------------------------
+        Path"""
+        def f(u):
+            theta = u / radius 
+            return np.array([radius*cos(theta), radius*sin(theta), 0.])
+        return Path(f, angle*radius).move(dx=x0, dy=y0)
+     
+    def arc_to(self, center, end, reverse=False):
+        """Extend the current path using an arc.
+
+        Parameters
+        ----------------------------
+        center: (3,) float
+            The center point of the arc.
+        end: (3,) float
+            The endpoint of the arc, shoud lie on a circle determined
+            by the given centerpoint and the current endpoint.
+
+        Returns
+        -----------------------------
+        Path"""
+        warnings.warn("arc_to() is deprecated and will be removed in version 0.8.0."
+        "Use extend_with_arc() instead.",
+        DeprecationWarning,
+        stacklevel=2)
+
+        start = self.endpoint()
+        return self >> Path.arc(center, start, end, reverse=reverse)
+    
+    def extend_with_arc(self, center, end, reverse=False):
+        """Extend the current path using an arc.
+
+        Parameters
+        ----------------------------
+        center: (3,) float
+            The center point of the arc.
+        end: (3,) float
+            The endpoint of the arc, shoud lie on a circle determined
+            by the given centerpoint and the current endpoint.
+
+        Returns
+        -----------------------------
+        Path"""
+        start = self.endpoint()
+        return self >> Path.arc(center, start, end, reverse=reverse)
+    
+    @staticmethod
+    def arc(center, start, end, reverse=False):
+        """Return an arc by specifying the center, start and end point.
+
+        Parameters
+        ----------------------------
+        center: (3,) float
+            The center point of the arc.
+        start: (3,) float
+            The start point of the arc.
+        end: (3,) float
+            The endpoint of the arc.
+
+        Returns
+        ----------------------------
+        Path"""
+        start_arr, center_arr, end_arr = np.array(start), np.array(center), np.array(end)
+         
+        x_unit = start_arr - center_arr
+        x_unit /= np.linalg.norm(x_unit)
+
+        vector = end_arr - center_arr
+         
+        y_unit = vector - np.dot(vector, x_unit) * x_unit
+        y_unit /= np.linalg.norm(y_unit)
+
+        radius = np.linalg.norm(start_arr - center_arr) 
+        theta_max = atan2(np.dot(vector, y_unit), np.dot(vector, x_unit))
+
+        if reverse:
+            theta_max = theta_max - 2*pi
+
+        path_length = abs(theta_max * radius)
+          
+        def f(l):
+            theta = l/path_length * theta_max
+            return center + radius*cos(theta)*x_unit + radius*sin(theta)*y_unit
+        
+        return Path(f, path_length)
+     
+    def revolve_x(self, angle=2*pi):
+        """Create a surface by revolving the path anti-clockwise around the x-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+        Returns
+        -----------------------
+        Surface"""
+        
+        r_avg = self.average(lambda p: sqrt(p[1]**2 + p[2]**2))
+        length2 = 2*pi*r_avg
+         
+        def f(u, v):
+            p = self(u)
+            theta = atan2(p[2], p[1])
+            r = sqrt(p[1]**2 + p[2]**2)
+            return np.array([p[0], r*cos(theta + v/length2*angle), r*sin(theta + v/length2*angle)])
+         
+        return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+    
+    def revolve_y(self, angle=2*pi):
+        """Create a surface by revolving the path anti-clockwise around the y-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+        Returns
+        -----------------------
+        Surface"""
+
+        r_avg = self.average(lambda p: sqrt(p[0]**2 + p[2]**2))
+        length2 = 2*pi*r_avg
+         
+        def f(u, v):
+            p = self(u)
+            theta = atan2(p[2], p[0])
+            r = sqrt(p[0]*p[0] + p[2]*p[2])
+            return np.array([r*cos(theta + v/length2*angle), p[1], r*sin(theta + v/length2*angle)])
+         
+        return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+    
+    def revolve_z(self, angle=2*pi):
+        """Create a surface by revolving the path anti-clockwise around the z-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+        Returns
+        -----------------------
+        Surface"""
+
+        r_avg = self.average(lambda p: sqrt(p[0]**2 + p[1]**2))
+        length2 = 2*pi*r_avg
+        
+        def f(u, v):
+            p = self(u)
+            theta = atan2(p[1], p[0])
+            r = sqrt(p[0]*p[0] + p[1]*p[1])
+            return np.array([r*cos(theta + v/length2*angle), r*sin(theta + v/length2*angle), p[2]])
+        
+        return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+     
+    def extrude(self, vector):
+        """Create a surface by extruding the path along a vector. The vector gives both
+        the length and the direction of the extrusion.
+
+        Parameters
+        -------------------------
+        vector: (3,) float
+            The direction and length (norm of the vector) to extrude by.
+
+        Returns
+        -------------------------
+        Surface"""
+        vector = np.array(vector)
+        length = np.linalg.norm(vector)
+         
+        def f(u, v):
+            return self(u) + v/length*vector
+        
+        return Surface(f, self.path_length, length, self.breakpoints, name=self.name)
+    
+    def extrude_by_path(self, p2):
+        """Create a surface by extruding the path along a second path. The second
+        path does not need to start along the first path. Imagine the surface created
+        by moving the first path along the second path.
+
+        Parameters
+        -------------------------
+        p2: Path
+            The (second) path defining the extrusion.
+
+        Returns
+        ------------------------
+        Surface"""
+        p0 = p2.starting_point()
+         
+        def f(u, v):
+            return self(u) + p2(v) - p0
+
+        return Surface(f, self.path_length, p2.path_length, self.breakpoints, p2.breakpoints, name=self.name)
+
+    def close(self):
+        """Close the path, by making a straight line to the starting point.
+
+        Returns
+        -------------------
+        Path"""
+        return self.extend_with_line(self.starting_point())
+    
+    @staticmethod
+    def ellipse(major, minor):
+        """Create a path along the outline of an ellipse. The ellipse lies
+        in the XY plane, and the path starts on the positive x-axis.
+
+        Parameters
+        ---------------------------
+        major: float
+            The major axis of the ellipse (lies along the x-axis).
+        minor: float
+            The minor axis of the ellipse (lies along the y-axis).
+
+        Returns
+        ---------------------------
+        Path"""
+        # Crazy enough there is no closed formula
+        # to go from path length to a point on the ellipse.
+        # So we have to use `from_irregular_function`
+        def f(u):
+            return np.array([major*cos(2*pi*u), minor*sin(2*pi*u), 0.])
+        return Path.from_irregular_function(f)
+    
+    @staticmethod
+    def line(from_, to):
+        """Create a straight line between two points.
+
+        Parameters
+        ------------------------------
+        from_: (3,) float
+            The starting point of the path.
+        to: (3,) float
+            The endpoint of the path.
+
+        Returns
+        ---------------------------
+        Path"""
+        from_, to = np.array(from_), np.array(to)
+        length = np.linalg.norm(from_ - to)
+        return Path(lambda pl: (1-pl/length)*from_ + pl/length*to, length)
+
+    def cut(self, length):
+        """Cut the path in two at a specific length along the path.
+
+        Parameters
+        --------------------------------------
+        length: float
+            The length along the path at which to cut.
+
+        Returns
+        -------------------------------------
+        (Path, Path)
+        
+        A tuple containing two paths. The first path contains the path upto length, while the second path contains the rest."""
+        return (Path(self.fun, length, [b for b in self.breakpoints if b <= length], name=self.name),
+                Path(lambda l: self.fun(l + length), self.path_length - length, [b - length for b in self.breakpoints if b >= length], name=self.name))
+    
+    @staticmethod
+    def rectangle_xz(xmin, xmax, zmin, zmax):
+        """Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is 
+        counter clockwise around the y-axis.
+        
+        Parameters
+        ------------------------
+        xmin: float
+            Minimum x-coordinate of the corner points.
+        xmax: float
+            Maximum x-coordinate of the corner points.
+        zmin: float
+            Minimum z-coordinate of the corner points.
+        zmax: float
+            Maximum z-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Path"""
+        return Path.line([xmin, 0., zmin], [xmax, 0, zmin]) \
+            .extend_with_line([xmax, 0, zmax]).extend_with_line([xmin, 0., zmax]).close()
+     
+    @staticmethod
+    def rectangle_yz(ymin, ymax, zmin, zmax):
+        """Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is 
+        counter clockwise around the x-axis.
+        
+        Parameters
+        ------------------------
+        ymin: float
+            Minimum y-coordinate of the corner points.
+        ymax: float
+            Maximum y-coordinate of the corner points.
+        zmin: float
+            Minimum z-coordinate of the corner points.
+        zmax: float
+            Maximum z-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Path"""
+
+        return Path.line([0., ymin, zmin], [0, ymin, zmax]) \
+            .extend_with_line([0., ymax, zmax]).extend_with_line([0., ymax, zmin]).close()
+     
+    @staticmethod
+    def rectangle_xy(xmin, xmax, ymin, ymax):
+        """Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is 
+        counter clockwise around the z-axis.
+        
+        Parameters
+        ------------------------
+        xmin: float
+            Minimum x-coordinate of the corner points.
+        xmax: float
+            Maximum x-coordinate of the corner points.
+        ymin: float
+            Minimum y-coordinate of the corner points.
+        ymax: float
+            Maximum y-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Path"""
+        return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]) \
+            .extend_with_line([xmax, ymax, 0.]).extend_with_line([xmax, ymin, 0.]).close()
+    
+    @staticmethod
+    def aperture(height, radius, extent, z=0.):
+        """Create an 'aperture'. Note that in a radially symmetric geometry
+        an aperture is basically a rectangle with the right side 'open'. Revolving
+        this path around the z-axis would generate a cylindircal hole in the center. 
+        This is the most basic model of an aperture.
+
+        Parameters
+        ------------------------
+        height: float
+            The height of the aperture
+        radius: float
+            The radius of the aperture hole (distance to the z-axis)
+        extent: float
+            The maximum x value
+        z: float
+            The z-coordinate of the center of the aperture
+
+        Returns
+        ------------------------
+        Path"""
+        return Path.line([extent, 0., -height/2], [radius, 0., -height/2])\
+                .extend_with_line([radius, 0., height/2]).extend_with_line([extent, 0., height/2]).move(dz=z)
+
+    @staticmethod
+    def polar_arc(radius, angle, start, direction, plane_normal=[0,1,0]):
+        """Return an arc specified by polar coordinates. The arc lies in a plane defined by the 
+        provided normal vector and curves from the start point in the specified direction 
+        counterclockwise around the normal.
+
+        Parameters
+        ---------------------------
+        radius : float
+            The radius of the arc.
+        angle : float
+            The angle subtended by the arc (in radians)
+        start: (3,) float
+            The start point of the arc
+        plane_normal : (3,) float
+            The normal vector of the plane containing the arc
+        direction : (3,) float
+            A tangent of the arc at the starting point. 
+            Must lie in the specified plane. Does not need to be normalized. 
+        Returns
+        ----------------------------
+        Path"""
+        start = np.array(start, dtype=float)
+        plane_normal = np.array(plane_normal, dtype=float)
+        direction = np.array(direction, dtype=float)
+
+        direction_unit = direction / np.linalg.norm(direction)
+        plane_normal_unit = plane_normal / np.linalg.norm(plane_normal)
+
+        if not np.isclose(np.dot(direction_unit, plane_normal_unit), 0., atol=1e-7):
+            corrected_direction = direction - np.dot(direction, plane_normal_unit) * plane_normal_unit
+            raise AssertionError(
+                f"The provided direction {direction} does not lie in the specified plane. \n"
+                f"The closed valid direction is {np.round(corrected_direction, 10)}.")
+        
+        if angle < 0:
+            direction, angle = -direction, -angle
+
+        center = start - radius * np.cross(direction, plane_normal)
+        center_to_start = start - center
+        
+        def f(l):
+            theta = l/radius
+            return center + np.cos(theta) * center_to_start + np.sin(theta)*np.cross(plane_normal, center_to_start)
+        
+        return Path(f, radius*angle)
+    
+    def extend_with_polar_arc(self, radius, angle, plane_normal=[0, 1, 0]):
+        """Extend the current path by a smooth arc using polar coordinates.
+        The arc is defined by a specified radius and angle and rotates counterclockwise
+         around around the normal that defines the arcing plane.
+
+        Parameters
+        ---------------------------
+        radius : float
+            The radius of the arc
+        angle : float
+            The angle subtended by the arc (in radians)
+        plane_normal : (3,) float
+            The normal vector of the plane containing the arc
+
+        Returns
+        ----------------------------
+        Path"""
+        plane_normal = np.array(plane_normal, dtype=float)
+        start_point = self.endpoint()
+        direction = self.velocity_vector(self.path_length)
+
+        plane_normal_unit = plane_normal / np.linalg.norm(plane_normal)
+        direction_unit = direction / np.linalg.norm(direction)
+
+        if not np.isclose(np.dot(plane_normal_unit, direction_unit), 0,atol=1e-7):
+            corrected_normal = plane_normal - np.dot(direction_unit, plane_normal) * direction_unit
+            raise AssertionError(
+                f"The provided plane normal {plane_normal} is not orthogonal to the direction {direction}  \n"
+                f"of the path at the endpoint so no smooth arc can be made. The closest valid normal is "
+                f"{np.round(corrected_normal, 10)}.")
+        
+        return self >> Path.polar_arc(radius, angle, start_point, direction, plane_normal)
+
+    def reverse(self):
+        """Generate a reversed version of the current path.
+        The reversed path is created by inverting the traversal direction,
+        such that the start becomes the end and vice versa.
+
+        Returns
+        ----------------------------
+        Path"""
+        return Path(lambda t: self(self.path_length-t), self.path_length, 
+                    [self.path_length - b for b in self.breakpoints], self.name)
+    
+    def velocity_vector(self, t):
+        """Calculate the velocity (tangent) vector at a specific point on the path 
+        using cubic spline interpolation.
+
+        Parameters
+        ----------------------------
+        t : float
+            The point on the path at which to calculate the velocity
+        num_splines : int
+            The number of samples used for cubic spline interpolation
+
+        Returns
+        ----------------------------
+        (3,) np.ndarray of float"""
+
+        samples = np.linspace(t - self.path_length*1e-3, t + self.path_length*1e-3, 7) # Odd number to include t
+        samples_on_path = [s for s in samples if 0 <= s <= self.path_length]
+        assert len(samples_on_path), "Please supply a point that lies on the path"
+        return CubicSpline(samples_on_path, [self(s) for s in samples_on_path])(t, nu=1)
+    
+   
+    def __add__(self, other):
+        """Add two paths to create a PathCollection. Note that a PathCollection supports
+        a subset of the methods of Path (for example, movement, rotation and meshing). Use
+        the + operator to combine paths into a path collection: path1 + path2 + path3.
+
+        Returns
+        -------------------------
+        PathCollection"""
+         
+        if isinstance(other, Path):
+            return PathCollection([self, other])
+        
+        if isinstance(other, PathCollection):
+            return PathCollection([self] + [other.paths])
+
+        return NotImplemented
+     
+    def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None, ensure_outward_normals=True):
+        """Mesh the path, so it can be used in the BEM solver. The result of meshing a path
+        are (possibly curved) line elements.
+
+        Parameters
+        --------------------------
+        mesh_size: float
+            Determines amount of elements in the mesh. A smaller
+            mesh size leads to more elements.
+        mesh_size_factor: float
+            Alternative way to specify the mesh size, which scales
+            with the dimensions of the geometry, and therefore more
+            easily translates between different geometries.
+        higher_order: bool
+            Whether to generate a higher order mesh. A higher order
+            produces curved line elements (determined by 4 points on
+            each curved element). The BEM solver supports higher order
+            elements in radial symmetric geometries only.
+        name: str
+            Assign this name to the mesh, instead of the name value assinged to Surface.name
+        
+        Returns
+        ----------------------------
+        `traceon.mesher.Mesh`"""
+        u = discretize_path(self.path_length, self.breakpoints, mesh_size, mesh_size_factor, N_factor=3 if higher_order else 1)
+        
+        N = len(u) 
+        points = np.zeros( (N, 3) )
+         
+        for i in range(N):
+            points[i] = self(u[i])
+         
+        if not higher_order:
+            lines = np.array([np.arange(N-1), np.arange(1, N)]).T
+        else:
+            assert N % 3 == 1
+            r = np.arange(N)
+            p0 = r[0:-1:3]
+            p1 = r[3::3]
+            p2 = r[1::3]
+            p3 = r[2::3]
+            lines = np.array([p0, p1, p2, p3]).T
+          
+        assert lines.dtype == np.int64 or lines.dtype == np.int32
+        
+        name = self.name if name is None else name
+         
+        if name is not None:
+            physical_to_lines = {name:np.arange(len(lines))}
+        else:
+            physical_to_lines = {}
+        
+        return Mesh(points=points, lines=lines, physical_to_lines=physical_to_lines, ensure_outward_normals=ensure_outward_normals)
+
+    def __str__(self):
+        return f"<Path name:{self.name}, length:{self.path_length:.1e}, number of breakpoints:{len(self.breakpoints)}>"
+
+ +

A path is a mapping from a number in the range [0, path_length] to a three dimensional point. Note that Path is a +subclass of GeometricObject, and therefore can be easily moved and rotated.

+ + +

Ancestors

+ + +

Static methods

+
+ +
+ + def aperture(height, radius, extent, z=0.0) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def aperture(height, radius, extent, z=0.):
+    """Create an 'aperture'. Note that in a radially symmetric geometry
+    an aperture is basically a rectangle with the right side 'open'. Revolving
+    this path around the z-axis would generate a cylindircal hole in the center. 
+    This is the most basic model of an aperture.
+
+    Parameters
+    ------------------------
+    height: float
+        The height of the aperture
+    radius: float
+        The radius of the aperture hole (distance to the z-axis)
+    extent: float
+        The maximum x value
+    z: float
+        The z-coordinate of the center of the aperture
+
+    Returns
+    ------------------------
+    Path"""
+    return Path.line([extent, 0., -height/2], [radius, 0., -height/2])\
+            .extend_with_line([radius, 0., height/2]).extend_with_line([extent, 0., height/2]).move(dz=z)
+
+ +

Create an 'aperture'. Note that in a radially symmetric geometry +an aperture is basically a rectangle with the right side 'open'. Revolving +this path around the z-axis would generate a cylindircal hole in the center. +This is the most basic model of an aperture.

+

Parameters

+
+
height : float
+
The height of the aperture
+
radius : float
+
The radius of the aperture hole (distance to the z-axis)
+
extent : float
+
The maximum x value
+
z : float
+
The z-coordinate of the center of the aperture
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def arc(center, start, end, reverse=False) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def arc(center, start, end, reverse=False):
+    """Return an arc by specifying the center, start and end point.
+
+    Parameters
+    ----------------------------
+    center: (3,) float
+        The center point of the arc.
+    start: (3,) float
+        The start point of the arc.
+    end: (3,) float
+        The endpoint of the arc.
+
+    Returns
+    ----------------------------
+    Path"""
+    start_arr, center_arr, end_arr = np.array(start), np.array(center), np.array(end)
+     
+    x_unit = start_arr - center_arr
+    x_unit /= np.linalg.norm(x_unit)
+
+    vector = end_arr - center_arr
+     
+    y_unit = vector - np.dot(vector, x_unit) * x_unit
+    y_unit /= np.linalg.norm(y_unit)
+
+    radius = np.linalg.norm(start_arr - center_arr) 
+    theta_max = atan2(np.dot(vector, y_unit), np.dot(vector, x_unit))
+
+    if reverse:
+        theta_max = theta_max - 2*pi
+
+    path_length = abs(theta_max * radius)
+      
+    def f(l):
+        theta = l/path_length * theta_max
+        return center + radius*cos(theta)*x_unit + radius*sin(theta)*y_unit
+    
+    return Path(f, path_length)
+
+ +

Return an arc by specifying the center, start and end point.

+

Parameters

+
+
center : (3,) float
+
The center point of the arc.
+
start : (3,) float
+
The start point of the arc.
+
end : (3,) float
+
The endpoint of the arc.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def circle_xy(x0, y0, radius, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def circle_xy(x0, y0, radius, angle=2*pi):
+    """Returns (part of) a circle in the XY plane around the z-axis. Starting on the positive X-axis.
+    
+    Parameters
+    --------------------------------
+    x0: float
+        x-coordinate of the center of the circle
+    y0: float
+        y-coordinate of the center of the circle
+    radius: float
+        radius of the circle
+    angle: float
+        The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+    Returns
+    ---------------------------------
+    Path"""
+    def f(u):
+        theta = u / radius 
+        return np.array([radius*cos(theta), radius*sin(theta), 0.])
+    return Path(f, angle*radius).move(dx=x0, dy=y0)
+
+ +

Returns (part of) a circle in the XY plane around the z-axis. Starting on the positive X-axis.

+

Parameters

+
+
x0 : float
+
x-coordinate of the center of the circle
+
y0 : float
+
y-coordinate of the center of the circle
+
radius : float
+
radius of the circle
+
angle : float
+
The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def circle_xz(x0, z0, radius, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def circle_xz(x0, z0, radius, angle=2*pi):
+    """Returns (part of) a circle in the XZ plane around the x-axis. Starting on the positive x-axis.
+    
+    Parameters
+    --------------------------------
+    x0: float
+        x-coordinate of the center of the circle
+    z0: float
+        z-coordinate of the center of the circle
+    radius: float
+        radius of the circle
+    angle: float
+        The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+    Returns
+    ---------------------------------
+    Path"""
+    def f(u):
+        theta = u / radius 
+        return np.array([radius*cos(theta), 0., radius*sin(theta)])
+    return Path(f, angle*radius).move(dx=x0, dz=z0)
+
+ +

Returns (part of) a circle in the XZ plane around the x-axis. Starting on the positive x-axis.

+

Parameters

+
+
x0 : float
+
x-coordinate of the center of the circle
+
z0 : float
+
z-coordinate of the center of the circle
+
radius : float
+
radius of the circle
+
angle : float
+
The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def circle_yz(y0, z0, radius, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def circle_yz(y0, z0, radius, angle=2*pi):
+    """Returns (part of) a circle in the YZ plane around the x-axis. Starting on the positive y-axis.
+    
+    Parameters
+    --------------------------------
+    y0: float
+        x-coordinate of the center of the circle
+    z0: float
+        z-coordinate of the center of the circle
+    radius: float
+        radius of the circle
+    angle: float
+        The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+    Returns
+    ---------------------------------
+    Path"""
+    def f(u):
+        theta = u / radius 
+        return np.array([0., radius*cos(theta), radius*sin(theta)])
+    return Path(f, angle*radius).move(dy=y0, dz=z0)
+
+ +

Returns (part of) a circle in the YZ plane around the x-axis. Starting on the positive y-axis.

+

Parameters

+
+
y0 : float
+
x-coordinate of the center of the circle
+
z0 : float
+
z-coordinate of the center of the circle
+
radius : float
+
radius of the circle
+
angle : float
+
The circumference of the circle in radians. The default of 2*pi gives a full circle.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def ellipse(major, minor) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def ellipse(major, minor):
+    """Create a path along the outline of an ellipse. The ellipse lies
+    in the XY plane, and the path starts on the positive x-axis.
+
+    Parameters
+    ---------------------------
+    major: float
+        The major axis of the ellipse (lies along the x-axis).
+    minor: float
+        The minor axis of the ellipse (lies along the y-axis).
+
+    Returns
+    ---------------------------
+    Path"""
+    # Crazy enough there is no closed formula
+    # to go from path length to a point on the ellipse.
+    # So we have to use `from_irregular_function`
+    def f(u):
+        return np.array([major*cos(2*pi*u), minor*sin(2*pi*u), 0.])
+    return Path.from_irregular_function(f)
+
+ +

Create a path along the outline of an ellipse. The ellipse lies +in the XY plane, and the path starts on the positive x-axis.

+

Parameters

+
+
major : float
+
The major axis of the ellipse (lies along the x-axis).
+
minor : float
+
The minor axis of the ellipse (lies along the y-axis).
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def from_irregular_function(to_point, N=100, breakpoints=[]) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def from_irregular_function(to_point, N=100, breakpoints=[]):
+    """Construct a path from a function that is of the form u -> point, where 0 <= u <= 1.
+    The length of the path is determined by integration.
+
+    Parameters
+    ---------------------------------
+    to_point: callable
+        A function accepting a number in the range [0, 1] and returns a the dimensional point.
+    N: int
+        Number of samples to use in the cubic spline interpolation.
+    breakpoints: float iterable
+        Points (0 <= u <= 1) on the path where the function is non-differentiable. These points
+        are always included in the resulting mesh.
+
+    Returns
+    ---------------------------------
+    Path"""
+     
+    # path length = integrate |f'(x)|
+    fun = lambda u: np.array(to_point(u))
+    
+    u = np.linspace(0, 1, N)
+    samples = CubicSpline(u, [fun(u_) for u_ in u])
+    derivatives = samples.derivative()(u)
+    norm_derivatives = np.linalg.norm(derivatives, axis=1)
+    path_lengths = CubicSpline(u, norm_derivatives).antiderivative()(u)
+    interpolation = CubicSpline(path_lengths, u) # Path length to [0,1]
+
+    path_length = path_lengths[-1]
+    
+    return Path(lambda pl: fun(interpolation(pl)), path_length, breakpoints=[b*path_length for b in breakpoints])
+
+ +

Construct a path from a function that is of the form u -> point, where 0 <= u <= 1. +The length of the path is determined by integration.

+

Parameters

+
+
to_point : callable
+
A function accepting a number in the range [0, 1] and returns a the dimensional point.
+
N : int
+
Number of samples to use in the cubic spline interpolation.
+
breakpoints : float iterable
+
Points (0 <= u <= 1) on the path where the function is non-differentiable. These points +are always included in the resulting mesh.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def line(from_, to) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def line(from_, to):
+    """Create a straight line between two points.
+
+    Parameters
+    ------------------------------
+    from_: (3,) float
+        The starting point of the path.
+    to: (3,) float
+        The endpoint of the path.
+
+    Returns
+    ---------------------------
+    Path"""
+    from_, to = np.array(from_), np.array(to)
+    length = np.linalg.norm(from_ - to)
+    return Path(lambda pl: (1-pl/length)*from_ + pl/length*to, length)
+
+ +

Create a straight line between two points.

+

Parameters

+
+
from_ : (3,) float
+
The starting point of the path.
+
to : (3,) float
+
The endpoint of the path.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def polar_arc(radius, angle, start, direction, plane_normal=[0, 1, 0]) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def polar_arc(radius, angle, start, direction, plane_normal=[0,1,0]):
+    """Return an arc specified by polar coordinates. The arc lies in a plane defined by the 
+    provided normal vector and curves from the start point in the specified direction 
+    counterclockwise around the normal.
+
+    Parameters
+    ---------------------------
+    radius : float
+        The radius of the arc.
+    angle : float
+        The angle subtended by the arc (in radians)
+    start: (3,) float
+        The start point of the arc
+    plane_normal : (3,) float
+        The normal vector of the plane containing the arc
+    direction : (3,) float
+        A tangent of the arc at the starting point. 
+        Must lie in the specified plane. Does not need to be normalized. 
+    Returns
+    ----------------------------
+    Path"""
+    start = np.array(start, dtype=float)
+    plane_normal = np.array(plane_normal, dtype=float)
+    direction = np.array(direction, dtype=float)
+
+    direction_unit = direction / np.linalg.norm(direction)
+    plane_normal_unit = plane_normal / np.linalg.norm(plane_normal)
+
+    if not np.isclose(np.dot(direction_unit, plane_normal_unit), 0., atol=1e-7):
+        corrected_direction = direction - np.dot(direction, plane_normal_unit) * plane_normal_unit
+        raise AssertionError(
+            f"The provided direction {direction} does not lie in the specified plane. \n"
+            f"The closed valid direction is {np.round(corrected_direction, 10)}.")
+    
+    if angle < 0:
+        direction, angle = -direction, -angle
+
+    center = start - radius * np.cross(direction, plane_normal)
+    center_to_start = start - center
+    
+    def f(l):
+        theta = l/radius
+        return center + np.cos(theta) * center_to_start + np.sin(theta)*np.cross(plane_normal, center_to_start)
+    
+    return Path(f, radius*angle)
+
+ +

Return an arc specified by polar coordinates. The arc lies in a plane defined by the +provided normal vector and curves from the start point in the specified direction +counterclockwise around the normal.

+

Parameters

+
+
radius : float
+
The radius of the arc.
+
angle : float
+
The angle subtended by the arc (in radians)
+
start : (3,) float
+
The start point of the arc
+
plane_normal : (3,) float
+
The normal vector of the plane containing the arc
+
direction : (3,) float
+
A tangent of the arc at the starting point. +Must lie in the specified plane. Does not need to be normalized.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def rectangle_xy(xmin, xmax, ymin, ymax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_xy(xmin, xmax, ymin, ymax):
+    """Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is 
+    counter clockwise around the z-axis.
+    
+    Parameters
+    ------------------------
+    xmin: float
+        Minimum x-coordinate of the corner points.
+    xmax: float
+        Maximum x-coordinate of the corner points.
+    ymin: float
+        Minimum y-coordinate of the corner points.
+    ymax: float
+        Maximum y-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Path"""
+    return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]) \
+        .extend_with_line([xmax, ymax, 0.]).extend_with_line([xmax, ymin, 0.]).close()
+
+ +

Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is +counter clockwise around the z-axis.

+

Parameters

+
+
xmin : float
+
Minimum x-coordinate of the corner points.
+
xmax : float
+
Maximum x-coordinate of the corner points.
+
ymin : float
+
Minimum y-coordinate of the corner points.
+
ymax : float
+
Maximum y-coordinate of the corner points.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def rectangle_xz(xmin, xmax, zmin, zmax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_xz(xmin, xmax, zmin, zmax):
+    """Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is 
+    counter clockwise around the y-axis.
+    
+    Parameters
+    ------------------------
+    xmin: float
+        Minimum x-coordinate of the corner points.
+    xmax: float
+        Maximum x-coordinate of the corner points.
+    zmin: float
+        Minimum z-coordinate of the corner points.
+    zmax: float
+        Maximum z-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Path"""
+    return Path.line([xmin, 0., zmin], [xmax, 0, zmin]) \
+        .extend_with_line([xmax, 0, zmax]).extend_with_line([xmin, 0., zmax]).close()
+
+ +

Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is +counter clockwise around the y-axis.

+

Parameters

+
+
xmin : float
+
Minimum x-coordinate of the corner points.
+
xmax : float
+
Maximum x-coordinate of the corner points.
+
zmin : float
+
Minimum z-coordinate of the corner points.
+
zmax : float
+
Maximum z-coordinate of the corner points.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def rectangle_yz(ymin, ymax, zmin, zmax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_yz(ymin, ymax, zmin, zmax):
+    """Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is 
+    counter clockwise around the x-axis.
+    
+    Parameters
+    ------------------------
+    ymin: float
+        Minimum y-coordinate of the corner points.
+    ymax: float
+        Maximum y-coordinate of the corner points.
+    zmin: float
+        Minimum z-coordinate of the corner points.
+    zmax: float
+        Maximum z-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Path"""
+
+    return Path.line([0., ymin, zmin], [0, ymin, zmax]) \
+        .extend_with_line([0., ymax, zmax]).extend_with_line([0., ymax, zmin]).close()
+
+ +

Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is +counter clockwise around the x-axis.

+

Parameters

+
+
ymin : float
+
Minimum y-coordinate of the corner points.
+
ymax : float
+
Maximum y-coordinate of the corner points.
+
zmin : float
+
Minimum z-coordinate of the corner points.
+
zmax : float
+
Maximum z-coordinate of the corner points.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def spline_through_points(points, N=100) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def spline_through_points(points, N=100):
+    """Construct a path by fitting a cubic spline through the given points.
+
+    Parameters
+    -------------------------
+    points: (N, 3) ndarray of float
+        Three dimensional points through which the spline is fitted.
+
+    Returns
+    -------------------------
+    Path"""
+
+    x = np.linspace(0, 1, len(points))
+    interp = CubicSpline(x, points)
+    return Path.from_irregular_function(interp, N=N)
+
+ +

Construct a path by fitting a cubic spline through the given points.

+

Parameters

+
+
points : (N, 3) ndarray of float
+
Three dimensional points through which the spline is fitted.
+
+

Returns

+
+
Path
+
 
+
+
+ +
+

Methods

+
+ +
+ + def __add__(self, other) +
+
+ + + +
+ + Expand source code + +
def __add__(self, other):
+    """Add two paths to create a PathCollection. Note that a PathCollection supports
+    a subset of the methods of Path (for example, movement, rotation and meshing). Use
+    the + operator to combine paths into a path collection: path1 + path2 + path3.
+
+    Returns
+    -------------------------
+    PathCollection"""
+     
+    if isinstance(other, Path):
+        return PathCollection([self, other])
+    
+    if isinstance(other, PathCollection):
+        return PathCollection([self] + [other.paths])
+
+    return NotImplemented
+
+ +

Add two paths to create a PathCollection. Note that a PathCollection supports +a subset of the methods of Path (for example, movement, rotation and meshing). Use +the + operator to combine paths into a path collection: path1 + path2 + path3.

+

Returns

+
+
PathCollection
+
 
+
+
+ + +
+ + def __call__(self, t) +
+
+ + + +
+ + Expand source code + +
def __call__(self, t):
+    """Evaluate a point along the path.
+
+    Parameters
+    ------------------------
+    t: float
+        The length along the path.
+
+    Returns
+    ------------------------
+    (3,) float
+
+    Three dimensional point."""
+    return self.fun(t)
+
+ +

Evaluate a point along the path.

+

Parameters

+
+
t : float
+
The length along the path.
+
+

Returns

+

(3,) float

+

Three dimensional point.

+
+ + +
+ + def __rshift__(self, other) +
+
+ + + +
+ + Expand source code + +
def __rshift__(self, other):
+    """Combine two paths to create a single path. The endpoint of the first path needs
+    to match the starting point of the second path. This common point is marked as a breakpoint and
+    always included in the mesh. To use this function use the right shift operator (p1 >> p2).
+
+    Parameters
+    -----------------------
+    other: Path
+        The second path, to extend the current path.
+
+    Returns
+    -----------------------
+    Path"""
+
+    assert isinstance(other, Path), "Exteding path with object that is not actually a Path"
+
+    assert _points_close(self.endpoint(), other.starting_point())
+
+    total = self.path_length + other.path_length
+     
+    def f(t):
+        assert 0 <= t <= total
+        
+        if t <= self.path_length:
+            return self(t)
+        else:
+            return other(t - self.path_length)
+    
+    return Path(f, total, self.breakpoints + [self.path_length] + other.breakpoints, name=self.name)
+
+ +

Combine two paths to create a single path. The endpoint of the first path needs +to match the starting point of the second path. This common point is marked as a breakpoint and +always included in the mesh. To use this function use the right shift operator (p1 >> p2).

+

Parameters

+
+
other : Path
+
The second path, to extend the current path.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def add_phase(self, l) +
+
+ + + +
+ + Expand source code + +
def add_phase(self, l):
+    """Add a phase to a closed path. A path is closed when the starting point is equal to the
+    end point. A phase of length l means that the path starts 'further down' the closed path.
+
+    Parameters
+    --------------------
+    l: float
+        The phase (expressed as a path length). The resulting path starts l distance along the 
+        original path.
+
+    Returns
+    --------------------
+    Path"""
+    assert self.is_closed()
+    
+    def fun(u):
+        return self( (l + u) % self.path_length )
+    
+    return Path(fun, self.path_length, sorted([(b-l)%self.path_length for b in self.breakpoints + [0.]]), name=self.name)
+
+ +

Add a phase to a closed path. A path is closed when the starting point is equal to the +end point. A phase of length l means that the path starts 'further down' the closed path.

+

Parameters

+
+
l : float
+
The phase (expressed as a path length). The resulting path starts l distance along the +original path.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def average(self, fun) +
+
+ + + +
+ + Expand source code + +
def average(self, fun):
+    """Average a function along the path, by integrating 1/l * fun(path(l)) with 0 <= l <= path length.
+
+    Parameters
+    --------------------------
+    fun: callable (3,) -> float
+        A function taking a three dimensional point and returning a float.
+
+    Returns
+    -------------------------
+    float
+
+    The average value of the function along the point."""
+    return quad(lambda s: fun(self(s)), 0, self.path_length, points=self.breakpoints)[0]/self.path_length
+
+ +

Average a function along the path, by integrating 1/l * fun(path(l)) with 0 <= l <= path length.

+

Parameters

+
+
fun : callable (3,) -> float
+
A function taking a three dimensional point and returning a float.
+
+

Returns

+
+
float
+
 
+
+

The average value of the function along the point.

+
+ + +
+ + def close(self) +
+
+ + + +
+ + Expand source code + +
def close(self):
+    """Close the path, by making a straight line to the starting point.
+
+    Returns
+    -------------------
+    Path"""
+    return self.extend_with_line(self.starting_point())
+
+ +

Close the path, by making a straight line to the starting point.

+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def cut(self, length) +
+
+ + + +
+ + Expand source code + +
def cut(self, length):
+    """Cut the path in two at a specific length along the path.
+
+    Parameters
+    --------------------------------------
+    length: float
+        The length along the path at which to cut.
+
+    Returns
+    -------------------------------------
+    (Path, Path)
+    
+    A tuple containing two paths. The first path contains the path upto length, while the second path contains the rest."""
+    return (Path(self.fun, length, [b for b in self.breakpoints if b <= length], name=self.name),
+            Path(lambda l: self.fun(l + length), self.path_length - length, [b - length for b in self.breakpoints if b >= length], name=self.name))
+
+ +

Cut the path in two at a specific length along the path.

+

Parameters

+
+
length : float
+
The length along the path at which to cut.
+
+

Returns

+

(Path, Path)

+

A tuple containing two paths. The first path contains the path upto length, while the second path contains the rest.

+
+ + +
+ + def endpoint(self) +
+
+ + + +
+ + Expand source code + +
def endpoint(self):
+    """Returns the endpoint of the path.
+
+    Returns
+    ------------------------
+    (3,) float
+    
+    The endpoint of the path."""
+    return self(self.path_length)
+
+ +

Returns the endpoint of the path.

+

Returns

+

(3,) float

+

The endpoint of the path.

+
+ + +
+ + def extend_with_arc(self, center, end, reverse=False) +
+
+ + + +
+ + Expand source code + +
def extend_with_arc(self, center, end, reverse=False):
+    """Extend the current path using an arc.
+
+    Parameters
+    ----------------------------
+    center: (3,) float
+        The center point of the arc.
+    end: (3,) float
+        The endpoint of the arc, shoud lie on a circle determined
+        by the given centerpoint and the current endpoint.
+
+    Returns
+    -----------------------------
+    Path"""
+    start = self.endpoint()
+    return self >> Path.arc(center, start, end, reverse=reverse)
+
+ +

Extend the current path using an arc.

+

Parameters

+
+
center : (3,) float
+
The center point of the arc.
+
end : (3,) float
+
The endpoint of the arc, shoud lie on a circle determined +by the given centerpoint and the current endpoint.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def extend_with_line(self, point) +
+
+ + + +
+ + Expand source code + +
def extend_with_line(self, point):
+    """Extend the current path by a line from the current endpoint to the given point.
+    The given point is marked a breakpoint.
+
+    Parameters
+    ----------------------
+    point: (3,) float
+        The new endpoint.
+
+    Returns
+    ---------------------
+    Path"""
+    point = np.array(point)
+    assert point.shape == (3,), "Please supply a three dimensional point to .extend_with_line(...)"
+    l = Path.line(self.endpoint(), point)
+    return self >> l
+
+ +

Extend the current path by a line from the current endpoint to the given point. +The given point is marked a breakpoint.

+

Parameters

+
+
point : (3,) float
+
The new endpoint.
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def extend_with_polar_arc(self, radius, angle, plane_normal=[0, 1, 0]) +
+
+ + + +
+ + Expand source code + +
def extend_with_polar_arc(self, radius, angle, plane_normal=[0, 1, 0]):
+    """Extend the current path by a smooth arc using polar coordinates.
+    The arc is defined by a specified radius and angle and rotates counterclockwise
+     around around the normal that defines the arcing plane.
+
+    Parameters
+    ---------------------------
+    radius : float
+        The radius of the arc
+    angle : float
+        The angle subtended by the arc (in radians)
+    plane_normal : (3,) float
+        The normal vector of the plane containing the arc
+
+    Returns
+    ----------------------------
+    Path"""
+    plane_normal = np.array(plane_normal, dtype=float)
+    start_point = self.endpoint()
+    direction = self.velocity_vector(self.path_length)
+
+    plane_normal_unit = plane_normal / np.linalg.norm(plane_normal)
+    direction_unit = direction / np.linalg.norm(direction)
+
+    if not np.isclose(np.dot(plane_normal_unit, direction_unit), 0,atol=1e-7):
+        corrected_normal = plane_normal - np.dot(direction_unit, plane_normal) * direction_unit
+        raise AssertionError(
+            f"The provided plane normal {plane_normal} is not orthogonal to the direction {direction}  \n"
+            f"of the path at the endpoint so no smooth arc can be made. The closest valid normal is "
+            f"{np.round(corrected_normal, 10)}.")
+    
+    return self >> Path.polar_arc(radius, angle, start_point, direction, plane_normal)
+
+ +

Extend the current path by a smooth arc using polar coordinates. +The arc is defined by a specified radius and angle and rotates counterclockwise + around around the normal that defines the arcing plane.

+

Parameters

+
+
radius : float
+
The radius of the arc
+
angle : float
+
The angle subtended by the arc (in radians)
+
plane_normal : (3,) float
+
The normal vector of the plane containing the arc
+
+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def extrude(self, vector) +
+
+ + + +
+ + Expand source code + +
def extrude(self, vector):
+    """Create a surface by extruding the path along a vector. The vector gives both
+    the length and the direction of the extrusion.
+
+    Parameters
+    -------------------------
+    vector: (3,) float
+        The direction and length (norm of the vector) to extrude by.
+
+    Returns
+    -------------------------
+    Surface"""
+    vector = np.array(vector)
+    length = np.linalg.norm(vector)
+     
+    def f(u, v):
+        return self(u) + v/length*vector
+    
+    return Surface(f, self.path_length, length, self.breakpoints, name=self.name)
+
+ +

Create a surface by extruding the path along a vector. The vector gives both +the length and the direction of the extrusion.

+

Parameters

+
+
vector : (3,) float
+
The direction and length (norm of the vector) to extrude by.
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def extrude_by_path(self, p2) +
+
+ + + +
+ + Expand source code + +
def extrude_by_path(self, p2):
+    """Create a surface by extruding the path along a second path. The second
+    path does not need to start along the first path. Imagine the surface created
+    by moving the first path along the second path.
+
+    Parameters
+    -------------------------
+    p2: Path
+        The (second) path defining the extrusion.
+
+    Returns
+    ------------------------
+    Surface"""
+    p0 = p2.starting_point()
+     
+    def f(u, v):
+        return self(u) + p2(v) - p0
+
+    return Surface(f, self.path_length, p2.path_length, self.breakpoints, p2.breakpoints, name=self.name)
+
+ +

Create a surface by extruding the path along a second path. The second +path does not need to start along the first path. Imagine the surface created +by moving the first path along the second path.

+

Parameters

+
+
p2 : Path
+
The (second) path defining the extrusion.
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def is_closed(self) +
+
+ + + +
+ + Expand source code + +
def is_closed(self):
+    """Determine whether the path is closed, by comparing the starting and endpoint.
+
+    Returns
+    ----------------------
+    bool: True if the path is closed, False otherwise."""
+    return _points_close(self.starting_point(), self.endpoint())
+
+ +

Determine whether the path is closed, by comparing the starting and endpoint.

+

Returns

+

bool: True if the path is closed, False otherwise.

+
+ + +
+ + def map_points(self, fun) +
+
+ + + +
+ + Expand source code + +
def map_points(self, fun):
+    """Return a new function by mapping a function over points along the path (see `traceon.mesher.GeometricObject`).
+    The path length is assumed to stay the same after this operation.
+    
+    Parameters
+    ----------------------------
+    fun: callable (3,) -> (3,)
+        Function taking three dimensional points and returning three dimensional points.
+
+    Returns
+    ---------------------------      
+
+    Path"""
+    return Path(lambda u: fun(self(u)), self.path_length, self.breakpoints, name=self.name)
+
+ +

Return a new function by mapping a function over points along the path (see GeometricObject). +The path length is assumed to stay the same after this operation.

+

Parameters

+
+
fun : callable (3,) -> (3,)
+
Function taking three dimensional points and returning three dimensional points.
+
+

Returns

+

Path

+
+ + +
+ + def mesh(self,
mesh_size=None,
mesh_size_factor=None,
higher_order=False,
name=None,
ensure_outward_normals=True)
+
+
+ + + +
+ + Expand source code + +
def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None, ensure_outward_normals=True):
+    """Mesh the path, so it can be used in the BEM solver. The result of meshing a path
+    are (possibly curved) line elements.
+
+    Parameters
+    --------------------------
+    mesh_size: float
+        Determines amount of elements in the mesh. A smaller
+        mesh size leads to more elements.
+    mesh_size_factor: float
+        Alternative way to specify the mesh size, which scales
+        with the dimensions of the geometry, and therefore more
+        easily translates between different geometries.
+    higher_order: bool
+        Whether to generate a higher order mesh. A higher order
+        produces curved line elements (determined by 4 points on
+        each curved element). The BEM solver supports higher order
+        elements in radial symmetric geometries only.
+    name: str
+        Assign this name to the mesh, instead of the name value assinged to Surface.name
+    
+    Returns
+    ----------------------------
+    `traceon.mesher.Mesh`"""
+    u = discretize_path(self.path_length, self.breakpoints, mesh_size, mesh_size_factor, N_factor=3 if higher_order else 1)
+    
+    N = len(u) 
+    points = np.zeros( (N, 3) )
+     
+    for i in range(N):
+        points[i] = self(u[i])
+     
+    if not higher_order:
+        lines = np.array([np.arange(N-1), np.arange(1, N)]).T
+    else:
+        assert N % 3 == 1
+        r = np.arange(N)
+        p0 = r[0:-1:3]
+        p1 = r[3::3]
+        p2 = r[1::3]
+        p3 = r[2::3]
+        lines = np.array([p0, p1, p2, p3]).T
+      
+    assert lines.dtype == np.int64 or lines.dtype == np.int32
+    
+    name = self.name if name is None else name
+     
+    if name is not None:
+        physical_to_lines = {name:np.arange(len(lines))}
+    else:
+        physical_to_lines = {}
+    
+    return Mesh(points=points, lines=lines, physical_to_lines=physical_to_lines, ensure_outward_normals=ensure_outward_normals)
+
+ +

Mesh the path, so it can be used in the BEM solver. The result of meshing a path +are (possibly curved) line elements.

+

Parameters

+
+
mesh_size : float
+
Determines amount of elements in the mesh. A smaller +mesh size leads to more elements.
+
mesh_size_factor : float
+
Alternative way to specify the mesh size, which scales +with the dimensions of the geometry, and therefore more +easily translates between different geometries.
+
higher_order : bool
+
Whether to generate a higher order mesh. A higher order +produces curved line elements (determined by 4 points on +each curved element). The BEM solver supports higher order +elements in radial symmetric geometries only.
+
name : str
+
Assign this name to the mesh, instead of the name value assinged to Surface.name
+
+

Returns

+

Mesh

+
+ + +
+ + def middle_point(self) +
+
+ + + +
+ + Expand source code + +
def middle_point(self):
+    """Returns the midpoint of the path (in terms of length along the path.)
+
+    Returns
+    ----------------------
+    (3,) float
+    
+    The point at the middle of the path."""
+    return self(self.path_length/2)
+
+ +

Returns the midpoint of the path (in terms of length along the path.)

+

Returns

+

(3,) float

+

The point at the middle of the path.

+
+ + +
+ + def reverse(self) +
+
+ + + +
+ + Expand source code + +
def reverse(self):
+    """Generate a reversed version of the current path.
+    The reversed path is created by inverting the traversal direction,
+    such that the start becomes the end and vice versa.
+
+    Returns
+    ----------------------------
+    Path"""
+    return Path(lambda t: self(self.path_length-t), self.path_length, 
+                [self.path_length - b for b in self.breakpoints], self.name)
+
+ +

Generate a reversed version of the current path. +The reversed path is created by inverting the traversal direction, +such that the start becomes the end and vice versa.

+

Returns

+
+
Path
+
 
+
+
+ + +
+ + def revolve_x(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_x(self, angle=2*pi):
+    """Create a surface by revolving the path anti-clockwise around the x-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+    Returns
+    -----------------------
+    Surface"""
+    
+    r_avg = self.average(lambda p: sqrt(p[1]**2 + p[2]**2))
+    length2 = 2*pi*r_avg
+     
+    def f(u, v):
+        p = self(u)
+        theta = atan2(p[2], p[1])
+        r = sqrt(p[1]**2 + p[2]**2)
+        return np.array([p[0], r*cos(theta + v/length2*angle), r*sin(theta + v/length2*angle)])
+     
+    return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+
+ +

Create a surface by revolving the path anti-clockwise around the x-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def revolve_y(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_y(self, angle=2*pi):
+    """Create a surface by revolving the path anti-clockwise around the y-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+    Returns
+    -----------------------
+    Surface"""
+
+    r_avg = self.average(lambda p: sqrt(p[0]**2 + p[2]**2))
+    length2 = 2*pi*r_avg
+     
+    def f(u, v):
+        p = self(u)
+        theta = atan2(p[2], p[0])
+        r = sqrt(p[0]*p[0] + p[2]*p[2])
+        return np.array([r*cos(theta + v/length2*angle), p[1], r*sin(theta + v/length2*angle)])
+     
+    return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+
+ +

Create a surface by revolving the path anti-clockwise around the y-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def revolve_z(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_z(self, angle=2*pi):
+    """Create a surface by revolving the path anti-clockwise around the z-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+    Returns
+    -----------------------
+    Surface"""
+
+    r_avg = self.average(lambda p: sqrt(p[0]**2 + p[1]**2))
+    length2 = 2*pi*r_avg
+    
+    def f(u, v):
+        p = self(u)
+        theta = atan2(p[1], p[0])
+        r = sqrt(p[0]*p[0] + p[1]*p[1])
+        return np.array([r*cos(theta + v/length2*angle), r*sin(theta + v/length2*angle), p[2]])
+    
+    return Surface(f, self.path_length, length2, self.breakpoints, name=self.name)
+
+ +

Create a surface by revolving the path anti-clockwise around the z-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def starting_point(self) +
+
+ + + +
+ + Expand source code + +
def starting_point(self):
+    """Returns the starting point of the path.
+
+    Returns
+    ---------------------
+    (3,) float
+
+    The starting point of the path."""
+    return self(0.)
+
+ +

Returns the starting point of the path.

+

Returns

+

(3,) float

+

The starting point of the path.

+
+ + +
+ + def velocity_vector(self, t) +
+
+ + + +
+ + Expand source code + +
def velocity_vector(self, t):
+    """Calculate the velocity (tangent) vector at a specific point on the path 
+    using cubic spline interpolation.
+
+    Parameters
+    ----------------------------
+    t : float
+        The point on the path at which to calculate the velocity
+    num_splines : int
+        The number of samples used for cubic spline interpolation
+
+    Returns
+    ----------------------------
+    (3,) np.ndarray of float"""
+
+    samples = np.linspace(t - self.path_length*1e-3, t + self.path_length*1e-3, 7) # Odd number to include t
+    samples_on_path = [s for s in samples if 0 <= s <= self.path_length]
+    assert len(samples_on_path), "Please supply a point that lies on the path"
+    return CubicSpline(samples_on_path, [self(s) for s in samples_on_path])(t, nu=1)
+
+ +

Calculate the velocity (tangent) vector at a specific point on the path +using cubic spline interpolation.

+

Parameters

+
+
t : float
+
The point on the path at which to calculate the velocity
+
num_splines : int
+
The number of samples used for cubic spline interpolation
+
+

Returns

+

(3,) np.ndarray of float

+
+ +
+ + +

Inherited members

+ + +
+ +
+ class PathCollection + (paths) +
+ +
+ + + +
+ + Expand source code + +
class PathCollection(GeometricObject):
+    """A PathCollection is a collection of `Path`. It can be created using the + operator (for example path1+path2).
+    Note that `PathCollection` is a subclass of `traceon.mesher.GeometricObject`, and therefore can be easily moved and rotated."""
+    
+    def __init__(self, paths):
+        assert all([isinstance(p, Path) for p in paths])
+        self.paths = paths
+        self._name = None
+    
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+         
+        for path in self.paths:
+            path.name = name
+     
+    def map_points(self, fun):
+        return PathCollection([p.map_points(fun) for p in self.paths])
+     
+    def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None, ensure_outward_normals=True):
+        """See `Path.mesh`"""
+        mesh = Mesh()
+        
+        name = self.name if name is None else name
+        
+        for p in self.paths:
+            mesh = mesh + p.mesh(mesh_size=mesh_size, mesh_size_factor=mesh_size_factor,
+                                higher_order=higher_order, name=name, ensure_outward_normals=ensure_outward_normals)
+
+        return mesh
+
+    def _map_to_surfaces(self, f, *args, **kwargs):
+        surfaces = []
+
+        for p in self.paths:
+            surfaces.append(f(p, *args, **kwargs))
+
+        return SurfaceCollection(surfaces)
+    
+    def __add__(self, other):
+        """Allows you to combine paths and path collection using the + operator (path1 + path2)."""
+        if isinstance(other, Path):
+            return PathCollection(self.paths+[other])
+        
+        if isinstance(other, PathCollection):
+            return PathCollection(self.paths+other.paths)
+
+        return NotImplemented
+      
+    def __iadd__(self, other):
+        """Allows you to add paths to the collection using the += operator."""
+        assert isinstance(other, PathCollection) or isinstance(other, Path)
+
+        if isinstance(other, Path):
+            self.paths.append(other)
+        else:
+            self.paths.extend(other.paths)
+    
+    def __getitem__(self, index):
+        selection = np.array(self.paths, dtype=object).__getitem__(index)
+        if isinstance(selection, np.ndarray):
+            return PathCollection(selection.tolist())
+        else:
+            return selection
+    
+    def __len__(self):
+        return len(self.paths)
+
+    def __iter__(self):
+        return iter(self.paths)
+    
+    def revolve_x(self, angle=2*pi):
+        return self._map_to_surfaces(Path.revolve_x, angle=angle)
+    def revolve_y(self, angle=2*pi):
+        return self._map_to_surfaces(Path.revolve_y, angle=angle)
+    def revolve_z(self, angle=2*pi):
+        return self._map_to_surfaces(Path.revolve_z, angle=angle)
+    def extrude(self, vector):
+        return self._map_to_surfaces(Path.extrude, vector)
+    def extrude_by_path(self, p2):
+        return self._map_to_surfaces(Path.extrude_by_path, p2)
+    
+    def __str__(self):
+        return f"<PathCollection with {len(self.paths)} paths, name: {self.name}>"
+
+ +

A PathCollection is a collection of Path. It can be created using the + operator (for example path1+path2). +Note that PathCollection is a subclass of GeometricObject, and therefore can be easily moved and rotated.

+ + +

Ancestors

+ + +

Instance variables

+
+ +
prop name
+
+ + + +
+ + Expand source code + +
@property
+def name(self):
+    return self._name
+
+ +
+
+
+

Methods

+
+ +
+ + def __add__(self, other) +
+
+ + + +
+ + Expand source code + +
def __add__(self, other):
+    """Allows you to combine paths and path collection using the + operator (path1 + path2)."""
+    if isinstance(other, Path):
+        return PathCollection(self.paths+[other])
+    
+    if isinstance(other, PathCollection):
+        return PathCollection(self.paths+other.paths)
+
+    return NotImplemented
+
+ +

Allows you to combine paths and path collection using the + operator (path1 + path2).

+
+ + +
+ + def __iadd__(self, other) +
+
+ + + +
+ + Expand source code + +
def __iadd__(self, other):
+    """Allows you to add paths to the collection using the += operator."""
+    assert isinstance(other, PathCollection) or isinstance(other, Path)
+
+    if isinstance(other, Path):
+        self.paths.append(other)
+    else:
+        self.paths.extend(other.paths)
+
+ +

Allows you to add paths to the collection using the += operator.

+
+ + +
+ + def extrude(self, vector) +
+
+ + + +
+ + Expand source code + +
def extrude(self, vector):
+    return self._map_to_surfaces(Path.extrude, vector)
+
+ +
+
+ + +
+ + def extrude_by_path(self, p2) +
+
+ + + +
+ + Expand source code + +
def extrude_by_path(self, p2):
+    return self._map_to_surfaces(Path.extrude_by_path, p2)
+
+ +
+
+ + +
+ + def mesh(self,
mesh_size=None,
mesh_size_factor=None,
higher_order=False,
name=None,
ensure_outward_normals=True)
+
+
+ + + +
+ + Expand source code + +
def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None, ensure_outward_normals=True):
+    """See `Path.mesh`"""
+    mesh = Mesh()
+    
+    name = self.name if name is None else name
+    
+    for p in self.paths:
+        mesh = mesh + p.mesh(mesh_size=mesh_size, mesh_size_factor=mesh_size_factor,
+                            higher_order=higher_order, name=name, ensure_outward_normals=ensure_outward_normals)
+
+    return mesh
+
+ + +
+ + +
+ + def revolve_x(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_x(self, angle=2*pi):
+    return self._map_to_surfaces(Path.revolve_x, angle=angle)
+
+ +
+
+ + +
+ + def revolve_y(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_y(self, angle=2*pi):
+    return self._map_to_surfaces(Path.revolve_y, angle=angle)
+
+ +
+
+ + +
+ + def revolve_z(self, angle=6.283185307179586) +
+
+ + + +
+ + Expand source code + +
def revolve_z(self, angle=2*pi):
+    return self._map_to_surfaces(Path.revolve_z, angle=angle)
+
+ +
+
+ +
+ + +

Inherited members

+ + +
+ +
+ class Surface + (fun, path_length1, path_length2, breakpoints1=[], breakpoints2=[], name=None) +
+ +
+ + + +
+ + Expand source code + +
class Surface(GeometricObject):
+    """A Surface is a mapping from two numbers to a three dimensional point.
+    Note that `Surface` is a subclass of `traceon.mesher.GeometricObject`, and therefore can be easily moved and rotated."""
+
+    def __init__(self, fun, path_length1, path_length2, breakpoints1=[], breakpoints2=[], name=None):
+        self.fun = fun
+        self.path_length1 = path_length1
+        self.path_length2 = path_length2
+        self.breakpoints1 = breakpoints1
+        self.breakpoints2 = breakpoints2
+        self.name = name
+
+    def _sections(self): 
+        b1 = [0.] + self.breakpoints1 + [self.path_length1]
+        b2 = [0.] + self.breakpoints2 + [self.path_length2]
+
+        for u0, u1 in zip(b1[:-1], b1[1:]):
+            for v0, v1 in zip(b2[:-1], b2[1:]):
+                def fun(u, v, u0_=u0, v0_=v0):
+                    return self(u0_+u, v0_+v)
+                yield Surface(fun, u1-u0, v1-v0, [], [])
+       
+    def __call__(self, u, v):
+        """Evaluate the surface at point (u, v). Returns a three dimensional point.
+
+        Parameters
+        ------------------------------
+        u: float
+            First coordinate, should be 0 <= u <= self.path_length1
+        v: float
+            Second coordinate, should be 0 <= v <= self.path_length2
+
+        Returns
+        ----------------------------
+        (3,) np.ndarray of double"""
+        return self.fun(u, v)
+
+    def map_points(self, fun):
+        return Surface(lambda u, v: fun(self(u, v)),
+            self.path_length1, self.path_length2,
+            self.breakpoints1, self.breakpoints2, name=self.name)
+    
+    @staticmethod
+    def spanned_by_paths(path1, path2):
+        """Create a surface by considering the area between two paths. Imagine two points
+        progressing along the path simultaneously and at each step drawing a straight line
+        between the points.
+
+        Parameters
+        --------------------------
+        path1: Path
+            The path characterizing one edge of the surface
+        path2: Path
+            The path characterizing the opposite edge of the surface
+
+        Returns
+        --------------------------
+        Surface"""
+        length1 = max(path1.path_length, path2.path_length)
+        
+        length_start = np.linalg.norm(path1.starting_point() - path2.starting_point())
+        length_final = np.linalg.norm(path1.endpoint() - path2.endpoint())
+        length2 = (length_start + length_final)/2
+         
+        def f(u, v):
+            p1 = path1(u/length1*path1.path_length) # u/l*p = b, u = l*b/p
+            p2 = path2(u/length1*path2.path_length)
+            return (1-v/length2)*p1 + v/length2*p2
+
+        breakpoints = sorted([length1*b/path1.path_length for b in path1.breakpoints] + \
+                                [length1*b/path2.path_length for b in path2.breakpoints])
+         
+        return Surface(f, length1, length2, breakpoints)
+
+    @staticmethod
+    def sphere(radius):
+        """Create a sphere with the given radius, the center of the sphere is
+        at the origin, but can easily be moved by using the `mesher.GeometricObject.move` method.
+
+        Parameters
+        ------------------------------
+        radius: float
+            The radius of the sphere
+
+        Returns
+        -----------------------------
+        Surface representing the sphere"""
+        
+        length1 = 2*pi*radius
+        length2 = pi*radius
+         
+        def f(u, v):
+            phi = u/radius
+            theta = v/radius
+            
+            return np.array([
+                radius*sin(theta)*cos(phi),
+                radius*sin(theta)*sin(phi),
+                radius*cos(theta)]) 
+        
+        return Surface(f, length1, length2)
+
+    @staticmethod
+    def box(p0, p1):
+        """Create a box with the two given points at opposite corners.
+
+        Parameters
+        -------------------------------
+        p0: (3,) np.ndarray double
+            One corner of the box
+        p1: (3,) np.ndarray double
+            The opposite corner of the box
+
+        Returns
+        -------------------------------
+        Surface representing the box"""
+
+        x0, y0, z0 = p0
+        x1, y1, z1 = p1
+
+        xmin, ymin, zmin = min(x0, x1), min(y0, y1), min(z0, z1)
+        xmax, ymax, zmax = max(x0, x1), max(y0, y1), max(z0, z1)
+        
+        path1 = Path.line([xmin, ymin, zmax], [xmax, ymin, zmax])
+        path2 = Path.line([xmin, ymin, zmin], [xmax, ymin, zmin])
+        path3 = Path.line([xmin, ymax, zmax], [xmax, ymax, zmax])
+        path4 = Path.line([xmin, ymax, zmin], [xmax, ymax, zmin])
+        
+        side_path = Path.line([xmin, ymin, zmin], [xmax, ymin, zmin])\
+            .extend_with_line([xmax, ymin, zmax])\
+            .extend_with_line([xmin, ymin, zmax])\
+            .close()
+
+        side_surface = side_path.extrude([0.0, ymax-ymin, 0.0])
+        top = Surface.spanned_by_paths(path1, path2)
+        bottom = Surface.spanned_by_paths(path4, path3)
+         
+        return (top + bottom + side_surface)
+
+    @staticmethod
+    def from_boundary_paths(p1, p2, p3, p4):
+        """Create a surface with the four given paths as the boundary.
+
+        Parameters
+        ----------------------------------
+        p1: Path
+            First edge of the surface
+        p2: Path
+            Second edge of the surface
+        p3: Path
+            Third edge of the surface
+        p4: Path
+            Fourth edge of the surface
+
+        Returns
+        ------------------------------------
+        Surface with the four giving paths as the boundary
+        """
+        path_length_p1_and_p3 = (p1.path_length + p3.path_length)/2
+        path_length_p2_and_p4 = (p2.path_length + p4.path_length)/2
+
+        def f(u, v):
+            u /= path_length_p1_and_p3
+            v /= path_length_p2_and_p4
+            
+            a = (1-v)
+            b = (1-u)
+             
+            c = v
+            d = u
+            
+            return 1/2*(a*p1(u*p1.path_length) + \
+                        b*p4((1-v)*p4.path_length) + \
+                        c*p3((1-u)*p3.path_length) + \
+                        d*p2(v*p2.path_length))
+        
+        # Scale the breakpoints appropriately
+        b1 = sorted([b/p1.path_length * path_length_p1_and_p3 for b in p1.breakpoints] + \
+                [b/p3.path_length * path_length_p1_and_p3 for b in p3.breakpoints])
+        b2 = sorted([b/p2.path_length * path_length_p2_and_p4 for b in p2.breakpoints] + \
+                [b/p4.path_length * path_length_p2_and_p4 for b in p4.breakpoints])
+        
+        return Surface(f, path_length_p1_and_p3, path_length_p2_and_p4, b1, b2)
+     
+    @staticmethod
+    def disk_xz(x0, z0, radius):
+        """Create a disk in the XZ plane.         
+        
+        Parameters
+        ------------------------
+        x0: float
+            x-coordiante of the center of the disk
+        z0: float
+            z-coordinate of the center of the disk
+        radius: float
+            radius of the disk
+        Returns
+        -----------------------
+        Surface"""
+        assert radius > 0, "radius must be a positive number"
+        disk_at_origin = Path.line([0.0, 0.0, 0.0], [radius, 0.0, 0.0]).revolve_y()
+        return disk_at_origin.move(dx=x0, dz=z0)
+    
+    @staticmethod
+    def disk_yz(y0, z0, radius):
+        """Create a disk in the YZ plane.         
+        
+        Parameters
+        ------------------------
+        y0: float
+            y-coordiante of the center of the disk
+        z0: float
+            z-coordinate of the center of the disk
+        radius: float
+            radius of the disk
+        Returns
+        -----------------------
+        Surface"""
+        assert radius > 0, "radius must be a positive number"
+        disk_at_origin = Path.line([0.0, 0.0, 0.0], [0.0, radius, 0.0]).revolve_x()
+        return disk_at_origin.move(dy=y0, dz=z0)
+
+    @staticmethod
+    def disk_xy(x0, y0, radius):
+        """Create a disk in the XY plane.
+        
+        Parameters
+        ------------------------
+        x0: float
+            x-coordiante of the center of the disk
+        y0: float
+            y-coordinate of the center of the disk
+        radius: float
+            radius of the disk
+        Returns
+        -----------------------
+        Surface"""
+        assert radius > 0, "radius must be a positive number"
+        disk_at_origin = Path.line([0.0, 0.0, 0.0], [radius, 0.0, 0.0]).revolve_z()
+        return disk_at_origin.move(dx=x0, dy=y0)
+     
+    @staticmethod
+    def rectangle_xz(xmin, xmax, zmin, zmax):
+        """Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is 
+        counter clockwise around the y-axis.
+        
+        Parameters
+        ------------------------
+        xmin: float
+            Minimum x-coordinate of the corner points.
+        xmax: float
+            Maximum x-coordinate of the corner points.
+        zmin: float
+            Minimum z-coordinate of the corner points.
+        zmax: float
+            Maximum z-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Surface representing the rectangle"""
+        return Path.line([xmin, 0., zmin], [xmin, 0, zmax]).extrude([xmax-xmin, 0., 0.])
+     
+    @staticmethod
+    def rectangle_yz(ymin, ymax, zmin, zmax):
+        """Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is 
+        counter clockwise around the x-axis.
+        
+        Parameters
+        ------------------------
+        ymin: float
+            Minimum y-coordinate of the corner points.
+        ymax: float
+            Maximum y-coordinate of the corner points.
+        zmin: float
+            Minimum z-coordinate of the corner points.
+        zmax: float
+            Maximum z-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Surface representing the rectangle"""
+        return Path.line([0., ymin, zmin], [0., ymin, zmax]).extrude([0., ymax-ymin, 0.])
+     
+    @staticmethod
+    def rectangle_xy(xmin, xmax, ymin, ymax):
+        """Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is 
+        counter clockwise around the z-axis.
+        
+        Parameters
+        ------------------------
+        xmin: float
+            Minimum x-coordinate of the corner points.
+        xmax: float
+            Maximum x-coordinate of the corner points.
+        ymin: float
+            Minimum y-coordinate of the corner points.
+        ymax: float
+            Maximum y-coordinate of the corner points.
+        
+        Returns
+        -----------------------
+        Surface representing the rectangle"""
+        return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]).extrude([xmax-xmin, 0., 0.])
+    
+    @staticmethod
+    def annulus_xy(x0, y0, inner_radius, outer_radius):
+        """Create a annulus in the XY plane.         
+        
+        Parameters
+        ------------------------
+        x0: float
+            x-coordiante of the center of the annulus
+        y0: float
+            y-coordinate of the center of the annulus
+        inner_radius: float
+            inner radius of the annulus
+        outer_radius:
+            outer radius of the annulus
+        Returns
+        -----------------------
+        Surface"""
+        assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+        assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+        annulus_at_origin = Path.line([inner_radius, 0.0, 0.0], [outer_radius, 0.0, 0.0]).revolve_z()
+        return annulus_at_origin.move(dx=x0, dy=y0)
+
+    @staticmethod
+    def annulus_xz(x0, z0, inner_radius, outer_radius):
+        """Create a annulus in the XZ plane.         
+        
+        Parameters
+        ------------------------
+        x0: float
+            x-coordiante of the center of the annulus
+        z0: float
+            z-coordinate of the center of the annulus
+        inner_radius: float
+            inner radius of the annulus
+        outer_radius:
+            outer radius of the annulus
+        Returns
+        -----------------------
+        Surface"""
+        assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+        assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+        annulus_at_origin = Path.line([inner_radius, 0.0, 0.0], [outer_radius, 0.0, 0.0]).revolve_y()
+        return annulus_at_origin.move(dx=x0, dz=z0)
+    
+    @staticmethod
+    def annulus_yz(y0, z0, inner_radius, outer_radius):
+        """Create a annulus in the YZ plane.         
+        
+        Parameters
+        ------------------------
+        y0: float
+            y-coordiante of the center of the annulus
+        z0: float
+            z-coordinate of the center of the annulus
+        inner_radius: float
+            inner radius of the annulus
+        outer_radius:
+            outer radius of the annulus
+        Returns
+        -----------------------
+        Surface"""
+        assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+        assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+        annulus_at_origin = Path.line([0.0, inner_radius, 0.0], [0.0, outer_radius, 0.0]).revolve_x()
+        return annulus_at_origin.move(dy=y0, dz=z0)
+
+    @staticmethod
+    def aperture(height, radius, extent, z=0.):
+        return Path.aperture(height, radius, extent, z=z).revolve_z()
+    
+    def get_boundary_paths(self):
+        """Get the boundary paths of the surface.
+        Computes the boundary paths (edges) of the surface and combines them into a `PathCollection`.
+        Non-closed paths get filtered out when closed paths are present, as only closed paths 
+        represent true boundaries in this case.
+        Note that this function might behave unexpectedly for surfaces without any boundaries (e.g a sphere).
+
+        Returns
+        ----------------------------
+        PathCollection representing the boundary paths of the surface"""
+        
+        b1 = Path(lambda u: self(u, 0.), self.path_length1, self.breakpoints1, self.name)
+        b2 = Path(lambda u: self(u, self.path_length2), self.path_length1, self.breakpoints1, self.name)
+        b3 = Path(lambda v: self(0., v), self.path_length2, self.breakpoints2, self.name)
+        b4 = Path(lambda v: self(self.path_length1, v), self.path_length2, self.breakpoints2, self.name)
+        
+        boundary = b1 + b2 + b3 + b4
+
+        if any([b.is_closed() for b in boundary.paths]):
+            boundary = PathCollection([b for b in boundary.paths if b.is_closed()])
+        
+        return boundary
+
+    def extrude_boundary(self, vector, enclose=True):
+        """
+        Extrude the boundary paths of the surface along a vector. The vector gives both
+        the length and the direction of the extrusion.
+
+        Parameters
+        -------------------------
+        vector: (3,) float
+            The direction and length (norm of the vector) to extrude by.
+        enclose: bool
+            Whether enclose the extrusion by adding a copy of the original surface 
+            moved by the extrusion vector to the resulting SurfaceCollection.
+
+        Returns
+        -------------------------
+        SurfaceCollection"""
+
+        boundary = self.get_boundary_paths()
+        extruded_boundary = boundary.extrude(vector)
+
+        if enclose:
+            return self + extruded_boundary + self.move(*vector) 
+        else:
+            return self + extruded_boundary
+    
+    def extrude_boundary_by_path(self, path, enclose=True):
+        """Extrude the boundary paths of a surface along a path. The path 
+        does not need to start at the surface. Imagine the  extrusion surface 
+        created by moving the boundary paths along the path.
+
+        Parameters
+        -------------------------
+        path: Path
+            The path defining the extrusion.
+        enclose: bool
+            Whether to enclose the extrusion by adding a copy of the original surface 
+            moved by the extrusion vector to the resulting SurfaceCollection.
+            
+        Returns
+        ------------------------
+        SurfaceCollection"""
+        
+        boundary = self.get_boundary_paths()
+        extruded_boundary = boundary.extrude(path)
+
+        if enclose:
+            path_vector = path.endpoint() - path.starting_point()
+            return self + extruded_boundary + self.move(*path_vector) 
+        else:
+            return self + extruded_boundary
+    
+    def revolve_boundary_x(self, angle=2*pi, enclose=True):
+        """Revolve the boundary paths of the surface anti-clockwise around the x-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+        enclose: bool
+            Whether enclose the revolution by adding a copy of the original surface 
+            rotated over the angle to the resulting SurfaceCollection.
+
+        Returns
+        -----------------------
+        SurfaceCollection"""
+
+        boundary = self.get_boundary_paths()
+        revolved_boundary = boundary.revolve_x(angle)
+
+        if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+            return self + revolved_boundary + self.rotate(Rx=angle)
+        else:
+            return self + revolved_boundary
+        
+    def revolve_boundary_y(self, angle=2*pi, enclose=True):
+        """Revolve the boundary paths of the surface anti-clockwise around the y-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+        cap_extension: bool
+            Whether to enclose the revolution by adding a copy of the original surface 
+            rotated over the angle to the resulting SurfaceCollection.
+
+        Returns
+        -----------------------
+        SurfaceCollection"""
+
+        boundary = self.get_boundary_paths()
+        revolved_boundary = boundary.revolve_y(angle)
+
+        if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+            return self + revolved_boundary + self.rotate(Ry=angle)
+        else:
+            return self + revolved_boundary
+    
+    def revolve_boundary_z(self, angle=2*pi, enclose=True):
+        """Revolve the boundary paths of the surface anti-clockwise around the z-axis.
+        
+        Parameters
+        -----------------------
+        angle: float
+            The angle by which to revolve. THe default 2*pi gives a full revolution.
+        cap_extension: bool
+            Whether to enclose the revolution by adding a copy of the original surface 
+            rotated over the angle to the resulting SurfaceCollection.
+
+        Returns
+        -----------------------
+        SurfaceCollection"""
+
+        boundary = self.get_boundary_paths()
+        revolved_boundary = boundary.revolve_z(angle)
+        
+        if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+            return self + revolved_boundary + self.rotate(Rz=angle)
+        else:
+            return self + revolved_boundary
+
+    def __add__(self, other):
+        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
+        if isinstance(other, Surface):
+            return SurfaceCollection([self, other])
+        
+        if isinstance(other, SurfaceCollection):
+            return SurfaceCollection([self] + other.surfaces)
+
+        return NotImplemented
+     
+    def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True):
+        """Mesh the surface, so it can be used in the BEM solver. The result of meshing
+        a surface are triangles.
+
+        Parameters
+        --------------------------
+        mesh_size: float
+            Determines amount of elements in the mesh. A smaller
+            mesh size leads to more elements.
+        mesh_size_factor: float
+            Alternative way to specify the mesh size, which scales
+            with the dimensions of the geometry, and therefore more
+            easily translates between different geometries.
+        name: str
+            Assign this name to the mesh, instead of the name value assinged to Surface.name
+        
+        Returns
+        ----------------------------
+        `traceon.mesher.Mesh`"""
+         
+        if mesh_size is None:
+            path_length = min(self.path_length1, self.path_length2)
+             
+            mesh_size = path_length / 4
+
+            if mesh_size_factor is not None:
+                mesh_size /= sqrt(mesh_size_factor)
+
+        name = self.name if name is None else name
+        return _mesh(self, mesh_size, name=name, ensure_outward_normals=ensure_outward_normals)
+    
+    def __str__(self):
+        return f"<Surface with name: {self.name}>"
+
+ +

A Surface is a mapping from two numbers to a three dimensional point. +Note that Surface is a subclass of GeometricObject, and therefore can be easily moved and rotated.

+ + +

Ancestors

+ + +

Static methods

+
+ +
+ + def annulus_xy(x0, y0, inner_radius, outer_radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def annulus_xy(x0, y0, inner_radius, outer_radius):
+    """Create a annulus in the XY plane.         
+    
+    Parameters
+    ------------------------
+    x0: float
+        x-coordiante of the center of the annulus
+    y0: float
+        y-coordinate of the center of the annulus
+    inner_radius: float
+        inner radius of the annulus
+    outer_radius:
+        outer radius of the annulus
+    Returns
+    -----------------------
+    Surface"""
+    assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+    assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+    annulus_at_origin = Path.line([inner_radius, 0.0, 0.0], [outer_radius, 0.0, 0.0]).revolve_z()
+    return annulus_at_origin.move(dx=x0, dy=y0)
+
+ +

Create a annulus in the XY plane.

+

Parameters

+
+
x0 : float
+
x-coordiante of the center of the annulus
+
y0 : float
+
y-coordinate of the center of the annulus
+
inner_radius : float
+
inner radius of the annulus
+
+

outer_radius: + outer radius of the annulus +Returns

+
+
+
Surface
+
 
+
+
+ + +
+ + def annulus_xz(x0, z0, inner_radius, outer_radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def annulus_xz(x0, z0, inner_radius, outer_radius):
+    """Create a annulus in the XZ plane.         
+    
+    Parameters
+    ------------------------
+    x0: float
+        x-coordiante of the center of the annulus
+    z0: float
+        z-coordinate of the center of the annulus
+    inner_radius: float
+        inner radius of the annulus
+    outer_radius:
+        outer radius of the annulus
+    Returns
+    -----------------------
+    Surface"""
+    assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+    assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+    annulus_at_origin = Path.line([inner_radius, 0.0, 0.0], [outer_radius, 0.0, 0.0]).revolve_y()
+    return annulus_at_origin.move(dx=x0, dz=z0)
+
+ +

Create a annulus in the XZ plane.

+

Parameters

+
+
x0 : float
+
x-coordiante of the center of the annulus
+
z0 : float
+
z-coordinate of the center of the annulus
+
inner_radius : float
+
inner radius of the annulus
+
+

outer_radius: + outer radius of the annulus +Returns

+
+
+
Surface
+
 
+
+
+ + +
+ + def annulus_yz(y0, z0, inner_radius, outer_radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def annulus_yz(y0, z0, inner_radius, outer_radius):
+    """Create a annulus in the YZ plane.         
+    
+    Parameters
+    ------------------------
+    y0: float
+        y-coordiante of the center of the annulus
+    z0: float
+        z-coordinate of the center of the annulus
+    inner_radius: float
+        inner radius of the annulus
+    outer_radius:
+        outer radius of the annulus
+    Returns
+    -----------------------
+    Surface"""
+    assert inner_radius > 0 and outer_radius > 0, "radii must be positive"
+    assert outer_radius > inner_radius, "outer radius must be larger than inner radius"
+
+    annulus_at_origin = Path.line([0.0, inner_radius, 0.0], [0.0, outer_radius, 0.0]).revolve_x()
+    return annulus_at_origin.move(dy=y0, dz=z0)
+
+ +

Create a annulus in the YZ plane.

+

Parameters

+
+
y0 : float
+
y-coordiante of the center of the annulus
+
z0 : float
+
z-coordinate of the center of the annulus
+
inner_radius : float
+
inner radius of the annulus
+
+

outer_radius: + outer radius of the annulus +Returns

+
+
+
Surface
+
 
+
+
+ + +
+ + def aperture(height, radius, extent, z=0.0) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def aperture(height, radius, extent, z=0.):
+    return Path.aperture(height, radius, extent, z=z).revolve_z()
+
+ +
+
+ + +
+ + def box(p0, p1) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def box(p0, p1):
+    """Create a box with the two given points at opposite corners.
+
+    Parameters
+    -------------------------------
+    p0: (3,) np.ndarray double
+        One corner of the box
+    p1: (3,) np.ndarray double
+        The opposite corner of the box
+
+    Returns
+    -------------------------------
+    Surface representing the box"""
+
+    x0, y0, z0 = p0
+    x1, y1, z1 = p1
+
+    xmin, ymin, zmin = min(x0, x1), min(y0, y1), min(z0, z1)
+    xmax, ymax, zmax = max(x0, x1), max(y0, y1), max(z0, z1)
+    
+    path1 = Path.line([xmin, ymin, zmax], [xmax, ymin, zmax])
+    path2 = Path.line([xmin, ymin, zmin], [xmax, ymin, zmin])
+    path3 = Path.line([xmin, ymax, zmax], [xmax, ymax, zmax])
+    path4 = Path.line([xmin, ymax, zmin], [xmax, ymax, zmin])
+    
+    side_path = Path.line([xmin, ymin, zmin], [xmax, ymin, zmin])\
+        .extend_with_line([xmax, ymin, zmax])\
+        .extend_with_line([xmin, ymin, zmax])\
+        .close()
+
+    side_surface = side_path.extrude([0.0, ymax-ymin, 0.0])
+    top = Surface.spanned_by_paths(path1, path2)
+    bottom = Surface.spanned_by_paths(path4, path3)
+     
+    return (top + bottom + side_surface)
+
+ +

Create a box with the two given points at opposite corners.

+

Parameters

+
+
p0 : (3,) np.ndarray double
+
One corner of the box
+
p1 : (3,) np.ndarray double
+
The opposite corner of the box
+
+

Returns

+
+
Surface representing the box
+
 
+
+
+ + +
+ + def disk_xy(x0, y0, radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def disk_xy(x0, y0, radius):
+    """Create a disk in the XY plane.
+    
+    Parameters
+    ------------------------
+    x0: float
+        x-coordiante of the center of the disk
+    y0: float
+        y-coordinate of the center of the disk
+    radius: float
+        radius of the disk
+    Returns
+    -----------------------
+    Surface"""
+    assert radius > 0, "radius must be a positive number"
+    disk_at_origin = Path.line([0.0, 0.0, 0.0], [radius, 0.0, 0.0]).revolve_z()
+    return disk_at_origin.move(dx=x0, dy=y0)
+
+ +

Create a disk in the XY plane.

+

Parameters

+
+
x0 : float
+
x-coordiante of the center of the disk
+
y0 : float
+
y-coordinate of the center of the disk
+
radius : float
+
radius of the disk
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def disk_xz(x0, z0, radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def disk_xz(x0, z0, radius):
+    """Create a disk in the XZ plane.         
+    
+    Parameters
+    ------------------------
+    x0: float
+        x-coordiante of the center of the disk
+    z0: float
+        z-coordinate of the center of the disk
+    radius: float
+        radius of the disk
+    Returns
+    -----------------------
+    Surface"""
+    assert radius > 0, "radius must be a positive number"
+    disk_at_origin = Path.line([0.0, 0.0, 0.0], [radius, 0.0, 0.0]).revolve_y()
+    return disk_at_origin.move(dx=x0, dz=z0)
+
+ +

Create a disk in the XZ plane.

+

Parameters

+
+
x0 : float
+
x-coordiante of the center of the disk
+
z0 : float
+
z-coordinate of the center of the disk
+
radius : float
+
radius of the disk
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def disk_yz(y0, z0, radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def disk_yz(y0, z0, radius):
+    """Create a disk in the YZ plane.         
+    
+    Parameters
+    ------------------------
+    y0: float
+        y-coordiante of the center of the disk
+    z0: float
+        z-coordinate of the center of the disk
+    radius: float
+        radius of the disk
+    Returns
+    -----------------------
+    Surface"""
+    assert radius > 0, "radius must be a positive number"
+    disk_at_origin = Path.line([0.0, 0.0, 0.0], [0.0, radius, 0.0]).revolve_x()
+    return disk_at_origin.move(dy=y0, dz=z0)
+
+ +

Create a disk in the YZ plane.

+

Parameters

+
+
y0 : float
+
y-coordiante of the center of the disk
+
z0 : float
+
z-coordinate of the center of the disk
+
radius : float
+
radius of the disk
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def from_boundary_paths(p1, p2, p3, p4) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def from_boundary_paths(p1, p2, p3, p4):
+    """Create a surface with the four given paths as the boundary.
+
+    Parameters
+    ----------------------------------
+    p1: Path
+        First edge of the surface
+    p2: Path
+        Second edge of the surface
+    p3: Path
+        Third edge of the surface
+    p4: Path
+        Fourth edge of the surface
+
+    Returns
+    ------------------------------------
+    Surface with the four giving paths as the boundary
+    """
+    path_length_p1_and_p3 = (p1.path_length + p3.path_length)/2
+    path_length_p2_and_p4 = (p2.path_length + p4.path_length)/2
+
+    def f(u, v):
+        u /= path_length_p1_and_p3
+        v /= path_length_p2_and_p4
+        
+        a = (1-v)
+        b = (1-u)
+         
+        c = v
+        d = u
+        
+        return 1/2*(a*p1(u*p1.path_length) + \
+                    b*p4((1-v)*p4.path_length) + \
+                    c*p3((1-u)*p3.path_length) + \
+                    d*p2(v*p2.path_length))
+    
+    # Scale the breakpoints appropriately
+    b1 = sorted([b/p1.path_length * path_length_p1_and_p3 for b in p1.breakpoints] + \
+            [b/p3.path_length * path_length_p1_and_p3 for b in p3.breakpoints])
+    b2 = sorted([b/p2.path_length * path_length_p2_and_p4 for b in p2.breakpoints] + \
+            [b/p4.path_length * path_length_p2_and_p4 for b in p4.breakpoints])
+    
+    return Surface(f, path_length_p1_and_p3, path_length_p2_and_p4, b1, b2)
+
+ +

Create a surface with the four given paths as the boundary.

+

Parameters

+
+
p1 : Path
+
First edge of the surface
+
p2 : Path
+
Second edge of the surface
+
p3 : Path
+
Third edge of the surface
+
p4 : Path
+
Fourth edge of the surface
+
+

Returns

+
+
Surface with the four giving paths as the boundary
+
 
+
+
+ + +
+ + def rectangle_xy(xmin, xmax, ymin, ymax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_xy(xmin, xmax, ymin, ymax):
+    """Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is 
+    counter clockwise around the z-axis.
+    
+    Parameters
+    ------------------------
+    xmin: float
+        Minimum x-coordinate of the corner points.
+    xmax: float
+        Maximum x-coordinate of the corner points.
+    ymin: float
+        Minimum y-coordinate of the corner points.
+    ymax: float
+        Maximum y-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Surface representing the rectangle"""
+    return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]).extrude([xmax-xmin, 0., 0.])
+
+ +

Create a rectangle in the XY plane. The path starts at (xmin, ymin, 0), and is +counter clockwise around the z-axis.

+

Parameters

+
+
xmin : float
+
Minimum x-coordinate of the corner points.
+
xmax : float
+
Maximum x-coordinate of the corner points.
+
ymin : float
+
Minimum y-coordinate of the corner points.
+
ymax : float
+
Maximum y-coordinate of the corner points.
+
+

Returns

+
+
Surface representing the rectangle
+
 
+
+
+ + +
+ + def rectangle_xz(xmin, xmax, zmin, zmax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_xz(xmin, xmax, zmin, zmax):
+    """Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is 
+    counter clockwise around the y-axis.
+    
+    Parameters
+    ------------------------
+    xmin: float
+        Minimum x-coordinate of the corner points.
+    xmax: float
+        Maximum x-coordinate of the corner points.
+    zmin: float
+        Minimum z-coordinate of the corner points.
+    zmax: float
+        Maximum z-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Surface representing the rectangle"""
+    return Path.line([xmin, 0., zmin], [xmin, 0, zmax]).extrude([xmax-xmin, 0., 0.])
+
+ +

Create a rectangle in the XZ plane. The path starts at (xmin, 0, zmin), and is +counter clockwise around the y-axis.

+

Parameters

+
+
xmin : float
+
Minimum x-coordinate of the corner points.
+
xmax : float
+
Maximum x-coordinate of the corner points.
+
zmin : float
+
Minimum z-coordinate of the corner points.
+
zmax : float
+
Maximum z-coordinate of the corner points.
+
+

Returns

+
+
Surface representing the rectangle
+
 
+
+
+ + +
+ + def rectangle_yz(ymin, ymax, zmin, zmax) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def rectangle_yz(ymin, ymax, zmin, zmax):
+    """Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is 
+    counter clockwise around the x-axis.
+    
+    Parameters
+    ------------------------
+    ymin: float
+        Minimum y-coordinate of the corner points.
+    ymax: float
+        Maximum y-coordinate of the corner points.
+    zmin: float
+        Minimum z-coordinate of the corner points.
+    zmax: float
+        Maximum z-coordinate of the corner points.
+    
+    Returns
+    -----------------------
+    Surface representing the rectangle"""
+    return Path.line([0., ymin, zmin], [0., ymin, zmax]).extrude([0., ymax-ymin, 0.])
+
+ +

Create a rectangle in the YZ plane. The path starts at (0, ymin, zmin), and is +counter clockwise around the x-axis.

+

Parameters

+
+
ymin : float
+
Minimum y-coordinate of the corner points.
+
ymax : float
+
Maximum y-coordinate of the corner points.
+
zmin : float
+
Minimum z-coordinate of the corner points.
+
zmax : float
+
Maximum z-coordinate of the corner points.
+
+

Returns

+
+
Surface representing the rectangle
+
 
+
+
+ + +
+ + def spanned_by_paths(path1, path2) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def spanned_by_paths(path1, path2):
+    """Create a surface by considering the area between two paths. Imagine two points
+    progressing along the path simultaneously and at each step drawing a straight line
+    between the points.
+
+    Parameters
+    --------------------------
+    path1: Path
+        The path characterizing one edge of the surface
+    path2: Path
+        The path characterizing the opposite edge of the surface
+
+    Returns
+    --------------------------
+    Surface"""
+    length1 = max(path1.path_length, path2.path_length)
+    
+    length_start = np.linalg.norm(path1.starting_point() - path2.starting_point())
+    length_final = np.linalg.norm(path1.endpoint() - path2.endpoint())
+    length2 = (length_start + length_final)/2
+     
+    def f(u, v):
+        p1 = path1(u/length1*path1.path_length) # u/l*p = b, u = l*b/p
+        p2 = path2(u/length1*path2.path_length)
+        return (1-v/length2)*p1 + v/length2*p2
+
+    breakpoints = sorted([length1*b/path1.path_length for b in path1.breakpoints] + \
+                            [length1*b/path2.path_length for b in path2.breakpoints])
+     
+    return Surface(f, length1, length2, breakpoints)
+
+ +

Create a surface by considering the area between two paths. Imagine two points +progressing along the path simultaneously and at each step drawing a straight line +between the points.

+

Parameters

+
+
path1 : Path
+
The path characterizing one edge of the surface
+
path2 : Path
+
The path characterizing the opposite edge of the surface
+
+

Returns

+
+
Surface
+
 
+
+
+ + +
+ + def sphere(radius) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def sphere(radius):
+    """Create a sphere with the given radius, the center of the sphere is
+    at the origin, but can easily be moved by using the `mesher.GeometricObject.move` method.
+
+    Parameters
+    ------------------------------
+    radius: float
+        The radius of the sphere
+
+    Returns
+    -----------------------------
+    Surface representing the sphere"""
+    
+    length1 = 2*pi*radius
+    length2 = pi*radius
+     
+    def f(u, v):
+        phi = u/radius
+        theta = v/radius
+        
+        return np.array([
+            radius*sin(theta)*cos(phi),
+            radius*sin(theta)*sin(phi),
+            radius*cos(theta)]) 
+    
+    return Surface(f, length1, length2)
+
+ +

Create a sphere with the given radius, the center of the sphere is +at the origin, but can easily be moved by using the mesher.GeometricObject.move method.

+

Parameters

+
+
radius : float
+
The radius of the sphere
+
+

Returns

+
+
Surface representing the sphere
+
 
+
+
+ +
+

Methods

+
+ +
+ + def __add__(self, other) +
+
+ + + +
+ + Expand source code + +
def __add__(self, other):
+    """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
+    if isinstance(other, Surface):
+        return SurfaceCollection([self, other])
+    
+    if isinstance(other, SurfaceCollection):
+        return SurfaceCollection([self] + other.surfaces)
+
+    return NotImplemented
+
+ +

Allows you to combine surfaces into a SurfaceCollection using the + operator (surface1 + surface2).

+
+ + +
+ + def __call__(self, u, v) +
+
+ + + +
+ + Expand source code + +
def __call__(self, u, v):
+    """Evaluate the surface at point (u, v). Returns a three dimensional point.
+
+    Parameters
+    ------------------------------
+    u: float
+        First coordinate, should be 0 <= u <= self.path_length1
+    v: float
+        Second coordinate, should be 0 <= v <= self.path_length2
+
+    Returns
+    ----------------------------
+    (3,) np.ndarray of double"""
+    return self.fun(u, v)
+
+ +

Evaluate the surface at point (u, v). Returns a three dimensional point.

+

Parameters

+
+
u : float
+
First coordinate, should be 0 <= u <= self.path_length1
+
v : float
+
Second coordinate, should be 0 <= v <= self.path_length2
+
+

Returns

+

(3,) np.ndarray of double

+
+ + +
+ + def extrude_boundary(self, vector, enclose=True) +
+
+ + + +
+ + Expand source code + +
def extrude_boundary(self, vector, enclose=True):
+    """
+    Extrude the boundary paths of the surface along a vector. The vector gives both
+    the length and the direction of the extrusion.
+
+    Parameters
+    -------------------------
+    vector: (3,) float
+        The direction and length (norm of the vector) to extrude by.
+    enclose: bool
+        Whether enclose the extrusion by adding a copy of the original surface 
+        moved by the extrusion vector to the resulting SurfaceCollection.
+
+    Returns
+    -------------------------
+    SurfaceCollection"""
+
+    boundary = self.get_boundary_paths()
+    extruded_boundary = boundary.extrude(vector)
+
+    if enclose:
+        return self + extruded_boundary + self.move(*vector) 
+    else:
+        return self + extruded_boundary
+
+ +

Extrude the boundary paths of the surface along a vector. The vector gives both +the length and the direction of the extrusion.

+

Parameters

+
+
vector : (3,) float
+
The direction and length (norm of the vector) to extrude by.
+
enclose : bool
+
Whether enclose the extrusion by adding a copy of the original surface +moved by the extrusion vector to the resulting SurfaceCollection.
+
+

Returns

+
+
SurfaceCollection
+
 
+
+
+ + +
+ + def extrude_boundary_by_path(self, path, enclose=True) +
+
+ + + +
+ + Expand source code + +
def extrude_boundary_by_path(self, path, enclose=True):
+    """Extrude the boundary paths of a surface along a path. The path 
+    does not need to start at the surface. Imagine the  extrusion surface 
+    created by moving the boundary paths along the path.
+
+    Parameters
+    -------------------------
+    path: Path
+        The path defining the extrusion.
+    enclose: bool
+        Whether to enclose the extrusion by adding a copy of the original surface 
+        moved by the extrusion vector to the resulting SurfaceCollection.
+        
+    Returns
+    ------------------------
+    SurfaceCollection"""
+    
+    boundary = self.get_boundary_paths()
+    extruded_boundary = boundary.extrude(path)
+
+    if enclose:
+        path_vector = path.endpoint() - path.starting_point()
+        return self + extruded_boundary + self.move(*path_vector) 
+    else:
+        return self + extruded_boundary
+
+ +

Extrude the boundary paths of a surface along a path. The path +does not need to start at the surface. Imagine the extrusion surface +created by moving the boundary paths along the path.

+

Parameters

+
+
path : Path
+
The path defining the extrusion.
+
enclose : bool
+
Whether to enclose the extrusion by adding a copy of the original surface +moved by the extrusion vector to the resulting SurfaceCollection.
+
+

Returns

+
+
SurfaceCollection
+
 
+
+
+ + +
+ + def get_boundary_paths(self) +
+
+ + + +
+ + Expand source code + +
def get_boundary_paths(self):
+    """Get the boundary paths of the surface.
+    Computes the boundary paths (edges) of the surface and combines them into a `PathCollection`.
+    Non-closed paths get filtered out when closed paths are present, as only closed paths 
+    represent true boundaries in this case.
+    Note that this function might behave unexpectedly for surfaces without any boundaries (e.g a sphere).
+
+    Returns
+    ----------------------------
+    PathCollection representing the boundary paths of the surface"""
+    
+    b1 = Path(lambda u: self(u, 0.), self.path_length1, self.breakpoints1, self.name)
+    b2 = Path(lambda u: self(u, self.path_length2), self.path_length1, self.breakpoints1, self.name)
+    b3 = Path(lambda v: self(0., v), self.path_length2, self.breakpoints2, self.name)
+    b4 = Path(lambda v: self(self.path_length1, v), self.path_length2, self.breakpoints2, self.name)
+    
+    boundary = b1 + b2 + b3 + b4
+
+    if any([b.is_closed() for b in boundary.paths]):
+        boundary = PathCollection([b for b in boundary.paths if b.is_closed()])
+    
+    return boundary
+
+ +

Get the boundary paths of the surface. +Computes the boundary paths (edges) of the surface and combines them into a PathCollection. +Non-closed paths get filtered out when closed paths are present, as only closed paths +represent true boundaries in this case. +Note that this function might behave unexpectedly for surfaces without any boundaries (e.g a sphere).

+

Returns

+
+
PathCollection representing the boundary paths of the surface
+
 
+
+
+ + +
+ + def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True) +
+
+ + + +
+ + Expand source code + +
def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True):
+    """Mesh the surface, so it can be used in the BEM solver. The result of meshing
+    a surface are triangles.
+
+    Parameters
+    --------------------------
+    mesh_size: float
+        Determines amount of elements in the mesh. A smaller
+        mesh size leads to more elements.
+    mesh_size_factor: float
+        Alternative way to specify the mesh size, which scales
+        with the dimensions of the geometry, and therefore more
+        easily translates between different geometries.
+    name: str
+        Assign this name to the mesh, instead of the name value assinged to Surface.name
+    
+    Returns
+    ----------------------------
+    `traceon.mesher.Mesh`"""
+     
+    if mesh_size is None:
+        path_length = min(self.path_length1, self.path_length2)
+         
+        mesh_size = path_length / 4
+
+        if mesh_size_factor is not None:
+            mesh_size /= sqrt(mesh_size_factor)
+
+    name = self.name if name is None else name
+    return _mesh(self, mesh_size, name=name, ensure_outward_normals=ensure_outward_normals)
+
+ +

Mesh the surface, so it can be used in the BEM solver. The result of meshing +a surface are triangles.

+

Parameters

+
+
mesh_size : float
+
Determines amount of elements in the mesh. A smaller +mesh size leads to more elements.
+
mesh_size_factor : float
+
Alternative way to specify the mesh size, which scales +with the dimensions of the geometry, and therefore more +easily translates between different geometries.
+
name : str
+
Assign this name to the mesh, instead of the name value assinged to Surface.name
+
+

Returns

+

Mesh

+
+ + +
+ + def revolve_boundary_x(self, angle=6.283185307179586, enclose=True) +
+
+ + + +
+ + Expand source code + +
def revolve_boundary_x(self, angle=2*pi, enclose=True):
+    """Revolve the boundary paths of the surface anti-clockwise around the x-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+    enclose: bool
+        Whether enclose the revolution by adding a copy of the original surface 
+        rotated over the angle to the resulting SurfaceCollection.
+
+    Returns
+    -----------------------
+    SurfaceCollection"""
+
+    boundary = self.get_boundary_paths()
+    revolved_boundary = boundary.revolve_x(angle)
+
+    if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+        return self + revolved_boundary + self.rotate(Rx=angle)
+    else:
+        return self + revolved_boundary
+
+ +

Revolve the boundary paths of the surface anti-clockwise around the x-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
enclose : bool
+
Whether enclose the revolution by adding a copy of the original surface +rotated over the angle to the resulting SurfaceCollection.
+
+

Returns

+
+
SurfaceCollection
+
 
+
+
+ + +
+ + def revolve_boundary_y(self, angle=6.283185307179586, enclose=True) +
+
+ + + +
+ + Expand source code + +
def revolve_boundary_y(self, angle=2*pi, enclose=True):
+    """Revolve the boundary paths of the surface anti-clockwise around the y-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+    cap_extension: bool
+        Whether to enclose the revolution by adding a copy of the original surface 
+        rotated over the angle to the resulting SurfaceCollection.
+
+    Returns
+    -----------------------
+    SurfaceCollection"""
+
+    boundary = self.get_boundary_paths()
+    revolved_boundary = boundary.revolve_y(angle)
+
+    if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+        return self + revolved_boundary + self.rotate(Ry=angle)
+    else:
+        return self + revolved_boundary
+
+ +

Revolve the boundary paths of the surface anti-clockwise around the y-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
cap_extension : bool
+
Whether to enclose the revolution by adding a copy of the original surface +rotated over the angle to the resulting SurfaceCollection.
+
+

Returns

+
+
SurfaceCollection
+
 
+
+
+ + +
+ + def revolve_boundary_z(self, angle=6.283185307179586, enclose=True) +
+
+ + + +
+ + Expand source code + +
def revolve_boundary_z(self, angle=2*pi, enclose=True):
+    """Revolve the boundary paths of the surface anti-clockwise around the z-axis.
+    
+    Parameters
+    -----------------------
+    angle: float
+        The angle by which to revolve. THe default 2*pi gives a full revolution.
+    cap_extension: bool
+        Whether to enclose the revolution by adding a copy of the original surface 
+        rotated over the angle to the resulting SurfaceCollection.
+
+    Returns
+    -----------------------
+    SurfaceCollection"""
+
+    boundary = self.get_boundary_paths()
+    revolved_boundary = boundary.revolve_z(angle)
+    
+    if enclose and not np.isclose(angle, 2*pi, atol=1e-8):
+        return self + revolved_boundary + self.rotate(Rz=angle)
+    else:
+        return self + revolved_boundary
+
+ +

Revolve the boundary paths of the surface anti-clockwise around the z-axis.

+

Parameters

+
+
angle : float
+
The angle by which to revolve. THe default 2*pi gives a full revolution.
+
cap_extension : bool
+
Whether to enclose the revolution by adding a copy of the original surface +rotated over the angle to the resulting SurfaceCollection.
+
+

Returns

+
+
SurfaceCollection
+
 
+
+
+ +
+ + +

Inherited members

+ + +
+ +
+ class SurfaceCollection + (surfaces) +
+ +
+ + + +
+ + Expand source code + +
class SurfaceCollection(GeometricObject):
+    """A SurfaceCollection is a collection of `Surface`. It can be created using the + operator (for example surface1+surface2).
+    Note that `SurfaceCollection` is a subclass of `traceon.mesher.GeometricObject`, and therefore can be easily moved and rotated."""
+     
+    def __init__(self, surfaces):
+        assert all([isinstance(s, Surface) for s in surfaces])
+        self.surfaces = surfaces
+        self._name = None
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+         
+        for surf in self.surfaces:
+            surf.name = name
+     
+    def map_points(self, fun):
+        return SurfaceCollection([s.map_points(fun) for s in self.surfaces])
+     
+    def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True):
+        """See `Surface.mesh`"""
+        mesh = Mesh()
+        
+        name = self.name if name is None else name
+        
+        for s in self.surfaces:
+            mesh = mesh + s.mesh(mesh_size=mesh_size, mesh_size_factor=mesh_size_factor, name=name, ensure_outward_normals=ensure_outward_normals)
+         
+        return mesh
+     
+    def __add__(self, other):
+        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
+        if isinstance(other, Surface):
+            return SurfaceCollection(self.surfaces+[other])
+
+        if isinstance(other, SurfaceCollection):
+            return SurfaceCollection(self.surfaces+other.surfaces)
+
+        return NotImplemented
+     
+    def __iadd__(self, other):
+        """Allows you to add surfaces to the collection using the += operator."""
+        assert isinstance(other, SurfaceCollection) or isinstance(other, Surface)
+        
+        if isinstance(other, Surface):
+            self.surfaces.append(other)
+        else:
+            self.surfaces.extend(other.surfaces)
+        
+    def __getitem__(self, index):
+        selection = np.array(self.surfaces, dtype=object).__getitem__(index)
+        if isinstance(selection, np.ndarray):
+            return SurfaceCollection(selection.tolist())
+        else:
+            return selection
+    
+    def __len__(self):
+        return len(self.surfaces)
+
+    def __iter__(self):
+        return iter(self.surfaces)
+
+    def __str__(self):
+        return f"<SurfaceCollection with {len(self.surfaces)} surfaces, name: {self.name}>"
+
+ +

A SurfaceCollection is a collection of Surface. It can be created using the + operator (for example surface1+surface2). +Note that SurfaceCollection is a subclass of GeometricObject, and therefore can be easily moved and rotated.

+ + +

Ancestors

+ + +

Instance variables

+
+ +
prop name
+
+ + + +
+ + Expand source code + +
@property
+def name(self):
+    return self._name
+
+ +
+
+
+

Methods

+
+ +
+ + def __add__(self, other) +
+
+ + + +
+ + Expand source code + +
def __add__(self, other):
+    """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
+    if isinstance(other, Surface):
+        return SurfaceCollection(self.surfaces+[other])
+
+    if isinstance(other, SurfaceCollection):
+        return SurfaceCollection(self.surfaces+other.surfaces)
+
+    return NotImplemented
+
+ +

Allows you to combine surfaces into a SurfaceCollection using the + operator (surface1 + surface2).

+
+ + +
+ + def __iadd__(self, other) +
+
+ + + +
+ + Expand source code + +
def __iadd__(self, other):
+    """Allows you to add surfaces to the collection using the += operator."""
+    assert isinstance(other, SurfaceCollection) or isinstance(other, Surface)
+    
+    if isinstance(other, Surface):
+        self.surfaces.append(other)
+    else:
+        self.surfaces.extend(other.surfaces)
+
+ +

Allows you to add surfaces to the collection using the += operator.

+
+ + +
+ + def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True) +
+
+ + + +
+ + Expand source code + +
def mesh(self, mesh_size=None, mesh_size_factor=None, name=None, ensure_outward_normals=True):
+    """See `Surface.mesh`"""
+    mesh = Mesh()
+    
+    name = self.name if name is None else name
+    
+    for s in self.surfaces:
+        mesh = mesh + s.mesh(mesh_size=mesh_size, mesh_size_factor=mesh_size_factor, name=name, ensure_outward_normals=ensure_outward_normals)
+     
+    return mesh
+
+ + +
+ +
+ + +

Inherited members

+ + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/index.html b/docs/docs/v0.9.0rc1/traceon/index.html new file mode 100644 index 0000000..b6b6154 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/index.html @@ -0,0 +1,204 @@ + + + + + + + + + + traceon API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Package traceon

+
+ +
+

Welcome!

+

Traceon is a general software package used for numerical electron optics. Its main feature is the implementation of the Boundary Element Method (BEM) to quickly calculate the surface charge distribution. +The program supports both radial symmetry and general three-dimensional geometries. +Electron tracing can be done very quickly using accurate radial series interpolation in both geometries. +The electron trajectories obtained can help determine the aberrations of the optical components under study.

+

If you have any issues using the package, please open an issue on the Traceon Github page.

+

The software is currently distributed under the MPL 2.0 license.

+

Usage

+

In general, one starts with the traceon.geometry module to create a mesh. For the BEM only the boundary of +electrodes needs to be meshed. So in 2D (radial symmetry) the mesh consists of line elements while in 3D the +mesh consists of triangles. Next, one specifies a suitable excitation (voltages) using the traceon.excitation module. +The excited geometry can then be passed to the solve_direct() function, which computes the resulting field. +The field can be passed to the Tracer class to compute the trajectory of electrons moving through the field.

+

Validations

+

To make sure the software is correct, various problems from the literature with known solutions are analyzed using the Traceon software and the +results compared. In this manner it has been shown that the software produces very accurate results very quickly. The validations can be found in the +/validations directory in the Github project. After installing Traceon, the validations can be +executed as follows:

+
    git clone https://github.com/leon-vv/Traceon
+    cd traceon
+    python3 ./validation/edwards2007.py --help
+
+

Units

+

SI units are used throughout the codebase. Except for charge, which is stored as \frac{ \sigma}{ \epsilon_0} .

+
+ +
+

Sub-modules

+
+
traceon.excitation
+
+ +

The excitation module allows to specify the excitation (or element types) of the different physical groups (electrodes) +created with the …

+
+
traceon.field
+
+ +
+
+
traceon.focus
+
+ +

Module containing a single function to find the focus of a beam of electron trajecories.

+
+
traceon.geometry
+
+ +

The geometry module allows the creation of general meshes in 2D and 3D. +The builtin mesher uses so called parametric meshes, meaning +that for any …

+
+
traceon.logging
+
+ +
+
+
traceon.mesher
+
+ +
+
+
traceon.plotting
+
+ +

The traceon.plotting module uses the vedo plotting library to provide some convenience functions +to show the line and triangle meshes generated by …

+
+
traceon.solver
+
+ +

The solver module uses the Boundary Element Method (BEM) to compute the surface charge distribution of a given +geometry and excitation. Once the …

+
+
traceon.tracing
+
+ +

The tracing module allows to trace charged particles within any field type returned by the traceon.solver module. The tracing algorithm +used is RK45 …

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/logging.html b/docs/docs/v0.9.0rc1/traceon/logging.html new file mode 100644 index 0000000..750a101 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/logging.html @@ -0,0 +1,276 @@ + + + + + + + + + + traceon.logging API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.logging

+
+ +
+ +
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def set_log_level(level) +
+
+ + + +
+ + Expand source code + +
def set_log_level(level):
+    """Set the current `LogLevel`. Note that the log level can also 
+    be set by setting the environment value TRACEON_LOG_LEVEL to one
+    of 'debug', 'info', 'warning', 'error' or 'silent'."""
+    global _log_level
+    assert isinstance(level, LogLevel)
+    _log_level = level
+
+ +

Set the current LogLevel. Note that the log level can also +be set by setting the environment value TRACEON_LOG_LEVEL to one +of 'debug', 'info', 'warning', 'error' or 'silent'.

+
+ +
+
+ +
+

Classes

+
+ +
+ class LogLevel + (value, names=None, *, module=None, qualname=None, type=None, start=1) +
+ +
+ + + +
+ + Expand source code + +
class LogLevel(IntEnum):
+    """Enumeration representing a certain verbosity of logging."""
+    
+    DEBUG = 0
+    """Print debug, info, warning and error information."""
+
+    INFO = 1
+    """Print info, warning and error information."""
+
+    WARNING = 2
+    """Print only warnings and errors."""
+
+    ERROR = 3
+    """Print only errors."""
+     
+    SILENT = 4
+    """Do not print anything."""
+
+ +

Enumeration representing a certain verbosity of logging.

+ + +

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+ +

Class variables

+
+ +
var DEBUG
+
+ + + +

Print debug, info, warning and error information.

+
+ +
var ERROR
+
+ + + +

Print only errors.

+
+ +
var INFO
+
+ + + +

Print info, warning and error information.

+
+ +
var SILENT
+
+ + + +

Do not print anything.

+
+ +
var WARNING
+
+ + + +

Print only warnings and errors.

+
+
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/mesher.html b/docs/docs/v0.9.0rc1/traceon/mesher.html new file mode 100644 index 0000000..8d77f51 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/mesher.html @@ -0,0 +1,1864 @@ + + + + + + + + + + traceon.mesher API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.mesher

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+

Classes

+
+ +
+ class GeometricObject +
+ +
+ + + +
+ + Expand source code + +
class GeometricObject(ABC):
+    """The Mesh class (and the classes defined in `traceon.geometry`) are subclasses
+    of `traceon.mesher.GeometricObject`. This means that they all can be moved, rotated, mirrored."""
+    
+    @abstractmethod
+    def map_points(self, fun: Callable[[np.ndarray], np.ndarray]) -> Any:
+        """Create a new geometric object, by mapping each point by a function.
+        
+        Parameters
+        -------------------------
+        fun: (3,) float -> (3,) float
+            Function taking a three dimensional point and returning a 
+            three dimensional point.
+
+        Returns
+        ------------------------
+        GeometricObject
+
+        This function returns the same type as the object on which this method was called."""
+        ...
+    
+    def move(self, dx=0., dy=0., dz=0.):
+        """Move along x, y or z axis.
+
+        Parameters
+        ---------------------------
+        dx: float
+            Amount to move along the x-axis.
+        dy: float
+            Amount to move along the y-axis.
+        dz: float
+            Amount to move along the z-axis.
+
+        Returns
+        ---------------------------
+        GeometricObject
+        
+        This function returns the same type as the object on which this method was called."""
+    
+        assert all([isinstance(d, float) or isinstance(d, int) for d in [dx, dy, dz]])
+        return self.map_points(lambda p: p + np.array([dx, dy, dz]))
+     
+    def rotate(self, Rx=0., Ry=0., Rz=0., origin=[0., 0., 0.]):
+        """Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time
+        (rotations do not commute).
+
+        Parameters
+        ------------------------------------
+        Rx: float
+            Amount to rotate around the x-axis (radians).
+        Ry: float
+            Amount to rotate around the y-axis (radians).
+        Rz: float
+            Amount to rotate around the z-axis (radians).
+        origin: (3,) float
+            Point around which to rotate, which is the origin by default.
+
+        Returns
+        --------------------------------------
+        GeometricObject
+        
+        This function returns the same type as the object on which this method was called."""
+        
+        assert sum([Rx==0., Ry==0., Rz==0.]) >= 2, "Only supply one axis of rotation"
+
+        if Rx !=0.:
+            return self.rotate_around_axis([1,0,0], angle=Rx, origin=origin)
+        if Ry !=0.:
+            return self.rotate_around_axis([0,1,0], angle=Ry, origin=origin)
+        if Rz !=0.:
+            return self.rotate_around_axis([0,0,1], angle=Rz, origin=origin)
+        
+
+    
+    def rotate_around_axis(self, axis=[0., 0, 1.], angle=0., origin=[0., 0., 0.]):
+        """
+        Rotate  counterclockwise around a general axis defined by a vector.
+        
+        Parameters
+        ------------------------------------
+        axis: (3,) float
+            Vector defining the axis of rotation. Must be non-zero.
+        angle: float
+            Amount to rotate around the axis (radians).
+        origin: (3,) float
+            Point around which to rotate, which is the origin by default.
+
+        Returns
+        --------------------------------------
+        GeometricObject
+        
+        This function returns the same type as the object on which this method was called.
+        """
+        origin = np.array(origin, dtype=float)
+        assert origin.shape == (3,), "Please supply a 3D point for origin"
+        axis = np.array(axis, dtype=float)
+        assert axis.shape == (3,), "Please supply a 3D vector for axis"
+        axis = axis / np.linalg.norm(axis)
+
+        if angle == 0:
+            return self.map_points(lambda x: x)
+        
+        # Rotation is implemented using Rodrigues' rotation formula
+        # See: https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
+        K = np.array([
+            [0, -axis[2], axis[1]],
+            [axis[2], 0, -axis[0]],
+            [-axis[1], axis[0], 0]])
+        
+        K2 = K @ K # Transformation matrix that maps a vector to its cross product with the rotation axis
+        I = np.eye(3)
+        R = I + np.sin(angle) * K + (1 - np.cos(angle)) * K2
+
+        return self.map_points(lambda p: origin + R @ (p - origin))
+
+    def mirror_xz(self):
+        """Mirror object in the XZ plane.
+
+        Returns
+        --------------------------------------
+        GeometricObject
+        
+        This function returns the same type as the object on which this method was called."""
+        return self.map_points(lambda p: np.array([p[0], -p[1], p[2]]))
+     
+    def mirror_yz(self):
+        """Mirror object in the YZ plane.
+
+        Returns
+        --------------------------------------
+        GeometricObject
+        This function returns the same type as the object on which this method was called."""
+        return self.map_points(lambda p: np.array([-p[0], p[1], p[2]]))
+    
+    def mirror_xy(self):
+        """Mirror object in the XY plane.
+
+        Returns
+        --------------------------------------
+        GeometricObject
+        
+        This function returns the same type as the object on which this method was called."""
+        return self.map_points(lambda p: np.array([p[0], p[1], -p[2]]))
+
+ +

The Mesh class (and the classes defined in traceon.geometry) are subclasses +of GeometricObject. This means that they all can be moved, rotated, mirrored.

+ + +

Ancestors

+
    +
  • abc.ABC
  • +
+ +

Subclasses

+ +

Methods

+
+ +
+ + def map_points(self, fun: Callable[[numpy.ndarray], numpy.ndarray]) ‑> Any +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def map_points(self, fun: Callable[[np.ndarray], np.ndarray]) -> Any:
+    """Create a new geometric object, by mapping each point by a function.
+    
+    Parameters
+    -------------------------
+    fun: (3,) float -> (3,) float
+        Function taking a three dimensional point and returning a 
+        three dimensional point.
+
+    Returns
+    ------------------------
+    GeometricObject
+
+    This function returns the same type as the object on which this method was called."""
+    ...
+
+ +

Create a new geometric object, by mapping each point by a function.

+

Parameters

+
+
fun : (3,) float -> (3,) float
+
Function taking a three dimensional point and returning a +three dimensional point.
+
+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def mirror_xy(self) +
+
+ + + +
+ + Expand source code + +
def mirror_xy(self):
+    """Mirror object in the XY plane.
+
+    Returns
+    --------------------------------------
+    GeometricObject
+    
+    This function returns the same type as the object on which this method was called."""
+    return self.map_points(lambda p: np.array([p[0], p[1], -p[2]]))
+
+ +

Mirror object in the XY plane.

+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def mirror_xz(self) +
+
+ + + +
+ + Expand source code + +
def mirror_xz(self):
+    """Mirror object in the XZ plane.
+
+    Returns
+    --------------------------------------
+    GeometricObject
+    
+    This function returns the same type as the object on which this method was called."""
+    return self.map_points(lambda p: np.array([p[0], -p[1], p[2]]))
+
+ +

Mirror object in the XZ plane.

+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def mirror_yz(self) +
+
+ + + +
+ + Expand source code + +
def mirror_yz(self):
+    """Mirror object in the YZ plane.
+
+    Returns
+    --------------------------------------
+    GeometricObject
+    This function returns the same type as the object on which this method was called."""
+    return self.map_points(lambda p: np.array([-p[0], p[1], p[2]]))
+
+ +

Mirror object in the YZ plane.

+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def move(self, dx=0.0, dy=0.0, dz=0.0) +
+
+ + + +
+ + Expand source code + +
def move(self, dx=0., dy=0., dz=0.):
+    """Move along x, y or z axis.
+
+    Parameters
+    ---------------------------
+    dx: float
+        Amount to move along the x-axis.
+    dy: float
+        Amount to move along the y-axis.
+    dz: float
+        Amount to move along the z-axis.
+
+    Returns
+    ---------------------------
+    GeometricObject
+    
+    This function returns the same type as the object on which this method was called."""
+
+    assert all([isinstance(d, float) or isinstance(d, int) for d in [dx, dy, dz]])
+    return self.map_points(lambda p: p + np.array([dx, dy, dz]))
+
+ +

Move along x, y or z axis.

+

Parameters

+
+
dx : float
+
Amount to move along the x-axis.
+
dy : float
+
Amount to move along the y-axis.
+
dz : float
+
Amount to move along the z-axis.
+
+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def rotate(self, Rx=0.0, Ry=0.0, Rz=0.0, origin=[0.0, 0.0, 0.0]) +
+
+ + + +
+ + Expand source code + +
def rotate(self, Rx=0., Ry=0., Rz=0., origin=[0., 0., 0.]):
+    """Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time
+    (rotations do not commute).
+
+    Parameters
+    ------------------------------------
+    Rx: float
+        Amount to rotate around the x-axis (radians).
+    Ry: float
+        Amount to rotate around the y-axis (radians).
+    Rz: float
+        Amount to rotate around the z-axis (radians).
+    origin: (3,) float
+        Point around which to rotate, which is the origin by default.
+
+    Returns
+    --------------------------------------
+    GeometricObject
+    
+    This function returns the same type as the object on which this method was called."""
+    
+    assert sum([Rx==0., Ry==0., Rz==0.]) >= 2, "Only supply one axis of rotation"
+
+    if Rx !=0.:
+        return self.rotate_around_axis([1,0,0], angle=Rx, origin=origin)
+    if Ry !=0.:
+        return self.rotate_around_axis([0,1,0], angle=Ry, origin=origin)
+    if Rz !=0.:
+        return self.rotate_around_axis([0,0,1], angle=Rz, origin=origin)
+
+ +

Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time +(rotations do not commute).

+

Parameters

+
+
Rx : float
+
Amount to rotate around the x-axis (radians).
+
Ry : float
+
Amount to rotate around the y-axis (radians).
+
Rz : float
+
Amount to rotate around the z-axis (radians).
+
origin : (3,) float
+
Point around which to rotate, which is the origin by default.
+
+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ + +
+ + def rotate_around_axis(self, axis=[0.0, 0, 1.0], angle=0.0, origin=[0.0, 0.0, 0.0]) +
+
+ + + +
+ + Expand source code + +
def rotate_around_axis(self, axis=[0., 0, 1.], angle=0., origin=[0., 0., 0.]):
+    """
+    Rotate  counterclockwise around a general axis defined by a vector.
+    
+    Parameters
+    ------------------------------------
+    axis: (3,) float
+        Vector defining the axis of rotation. Must be non-zero.
+    angle: float
+        Amount to rotate around the axis (radians).
+    origin: (3,) float
+        Point around which to rotate, which is the origin by default.
+
+    Returns
+    --------------------------------------
+    GeometricObject
+    
+    This function returns the same type as the object on which this method was called.
+    """
+    origin = np.array(origin, dtype=float)
+    assert origin.shape == (3,), "Please supply a 3D point for origin"
+    axis = np.array(axis, dtype=float)
+    assert axis.shape == (3,), "Please supply a 3D vector for axis"
+    axis = axis / np.linalg.norm(axis)
+
+    if angle == 0:
+        return self.map_points(lambda x: x)
+    
+    # Rotation is implemented using Rodrigues' rotation formula
+    # See: https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
+    K = np.array([
+        [0, -axis[2], axis[1]],
+        [axis[2], 0, -axis[0]],
+        [-axis[1], axis[0], 0]])
+    
+    K2 = K @ K # Transformation matrix that maps a vector to its cross product with the rotation axis
+    I = np.eye(3)
+    R = I + np.sin(angle) * K + (1 - np.cos(angle)) * K2
+
+    return self.map_points(lambda p: origin + R @ (p - origin))
+
+ +

Rotate counterclockwise around a general axis defined by a vector.

+

Parameters

+
+
axis : (3,) float
+
Vector defining the axis of rotation. Must be non-zero.
+
angle : float
+
Amount to rotate around the axis (radians).
+
origin : (3,) float
+
Point around which to rotate, which is the origin by default.
+
+

Returns

+
+
GeometricObject
+
 
+
+

This function returns the same type as the object on which this method was called.

+
+ +
+ + + +
+ +
+ class Mesh + (points=[],
lines=[],
triangles=[],
physical_to_lines={},
physical_to_triangles={},
ensure_outward_normals=True)
+
+ +
+ + + +
+ + Expand source code + +
class Mesh(Saveable, GeometricObject):
+    """Mesh containing lines and triangles. Groups of lines or triangles can be named. These
+    names are later used to apply the correct excitation. Line elements can be curved (or 'higher order'), 
+    in which case they are represented by four points per element.  Note that `Mesh` is a subclass of
+    `traceon.mesher.GeometricObject`, and therefore can be easily moved and rotated."""
+     
+    def __init__(self,
+            points=[],
+            lines=[],
+            triangles=[],
+            physical_to_lines={},
+            physical_to_triangles={},
+            ensure_outward_normals=True):
+        
+        # Ensure the correct shape even if empty arrays
+        if len(points):
+            self.points = np.array(points, dtype=np.float64)
+        else:
+            self.points = np.empty((0,3), dtype=np.float64)
+         
+        if len(lines) or (isinstance(lines, np.ndarray) and len(lines.shape)==2):
+            self.lines = np.array(lines, dtype=np.uint64)
+        else:
+            self.lines = np.empty((0,2), dtype=np.uint64)
+    
+        if len(triangles):
+            self.triangles = np.array(triangles, dtype=np.uint64)
+        else:
+            self.triangles = np.empty((0, 3), dtype=np.uint64)
+         
+        self.physical_to_lines = physical_to_lines.copy()
+        self.physical_to_triangles = physical_to_triangles.copy()
+
+        self._remove_degenerate_triangles()
+        self._deduplicate_points()
+
+        if ensure_outward_normals:
+            for el in self.get_electrodes():
+                self.ensure_outward_normals(el)
+         
+        assert np.all( (0 <= self.lines) & (self.lines < len(self.points)) ), "Lines reference points outside points array"
+        assert np.all( (0 <= self.triangles) & (self.triangles < len(self.points)) ), "Triangles reference points outside points array"
+        assert np.all([np.all( (0 <= group) & (group < len(self.lines)) ) for group in self.physical_to_lines.values()])
+        assert np.all([np.all( (0 <= group) & (group < len(self.triangles)) ) for group in self.physical_to_triangles.values()])
+        assert not len(self.lines) or self.lines.shape[1] in [2,4], "Lines should contain either 2 or 4 points."
+        assert not len(self.triangles) or self.triangles.shape[1] in [3,6], "Triangles should contain either 3 or 6 points"
+    
+    def is_higher_order(self):
+        """Whether the mesh contains higher order elements.
+
+        Returns
+        ----------------------------
+        bool"""
+        return isinstance(self.lines, np.ndarray) and len(self.lines.shape) == 2 and self.lines.shape[1] == 4
+    
+    def map_points(self, fun):
+        """See `GeometricObject`
+
+        """
+        new_points = np.empty_like(self.points)
+        for i in range(len(self.points)):
+            new_points[i] = fun(self.points[i])
+        assert new_points.shape == self.points.shape and new_points.dtype == self.points.dtype
+        
+        return Mesh(new_points, self.lines, self.triangles, self.physical_to_lines, self.physical_to_triangles)
+    
+    def _remove_degenerate_triangles(self):
+        areas = triangle_areas(self.points[self.triangles[:,:3]])
+        degenerate = areas < 1e-12
+        map_index = np.arange(len(self.triangles)) - np.cumsum(degenerate)
+         
+        self.triangles = self.triangles[~degenerate]
+        
+        for k in self.physical_to_triangles.keys():
+            v = self.physical_to_triangles[k]
+            self.physical_to_triangles[k] = map_index[v[~degenerate[v]]]
+         
+        if np.any(degenerate):
+            log_debug(f'Removed {sum(degenerate)} degenerate triangles')
+
+    def _deduplicate_points(self):
+        if not len(self.points):
+            return
+         
+        # Step 1: Make a copy of the points array using np.array
+        points_copy = np.array(self.points, dtype=np.float64)
+
+        # Step 2: Zero the low 16 bits of the mantissa of the X, Y, Z coordinates
+        points_copy.view(np.uint64)[:] &= np.uint64(0xFFFFFFFFFFFF0000)
+
+        # Step 3: Use Numpy lexsort directly on points_copy
+        sorted_indices = np.lexsort(points_copy.T)
+        points_sorted = points_copy[sorted_indices]
+
+        # Step 4: Create a mask to identify unique points
+        equal_to_previous = np.all(points_sorted[1:] == points_sorted[:-1], axis=1)
+        keep_mask = np.concatenate(([True], ~equal_to_previous))
+
+        # Step 5: Compute new indices for the unique points
+        new_indices_in_sorted_order = np.cumsum(keep_mask) - 1
+
+        # Map old indices to new indices
+        old_to_new_indices = np.empty(len(points_copy), dtype=np.uint64)
+        old_to_new_indices[sorted_indices] = new_indices_in_sorted_order
+        
+        # Step 6: Update the points array with unique points
+        self.points = points_sorted[keep_mask]
+
+        # Step 7: Update all indices
+        if len(self.triangles):
+            self.triangles = old_to_new_indices[self.triangles]
+        if len(self.lines):
+            self.lines = old_to_new_indices[self.lines]
+    
+    @staticmethod
+    def _merge_dicts(dict1, dict2):
+        dict_: dict[str, np.ndarray] = {}
+        
+        for (k, v) in chain(dict1.items(), dict2.items()):
+            if k in dict_:
+                dict_[k] = np.concatenate( (dict_[k], v), axis=0)
+            else:
+                dict_[k] = v
+
+        return dict_
+     
+    def __add__(self, other):
+        """Add meshes together, using the + operator (mesh1 + mesh2).
+        
+        Returns
+        ------------------------------
+        Mesh
+
+        A new mesh consisting of the elements of the added meshes"""
+        if not isinstance(other, Mesh):
+            return NotImplemented
+         
+        N_points = len(self.points)
+        N_lines = len(self.lines)
+        N_triangles = len(self.triangles)
+         
+        points = _concat_arrays(self.points, other.points)
+        lines = _concat_arrays(self.lines, other.lines+N_points)
+        triangles = _concat_arrays(self.triangles, other.triangles+N_points)
+
+        other_physical_to_lines = {k:(v+N_lines) for k, v in other.physical_to_lines.items()}
+        other_physical_to_triangles = {k:(v+N_triangles) for k, v in other.physical_to_triangles.items()}
+         
+        physical_lines = Mesh._merge_dicts(self.physical_to_lines, other_physical_to_lines)
+        physical_triangles = Mesh._merge_dicts(self.physical_to_triangles, other_physical_to_triangles)
+         
+        return Mesh(points=points,
+                    lines=lines,
+                    triangles=triangles,
+                    physical_to_lines=physical_lines,
+                    physical_to_triangles=physical_triangles)
+     
+    def extract_physical_group(self, name):
+        """Extract a named group from the mesh.
+
+        Parameters
+        ---------------------------
+        name: str
+            Name of the group of elements
+
+        Returns
+        --------------------------
+        Mesh
+
+        Subset of the mesh consisting only of the elements with the given name."""
+        assert name in self.physical_to_lines or name in self.physical_to_triangles, "Physical group not in mesh, so cannot extract"
+
+        if name in self.physical_to_lines:
+            elements = self.lines
+            physical = self.physical_to_lines
+        elif name in self.physical_to_triangles:
+            elements = self.triangles
+            physical = self.physical_to_triangles
+         
+        elements_indices = np.unique(physical[name])
+        elements = elements[elements_indices]
+          
+        points_mask = np.full(len(self.points), False)
+        points_mask[elements] = True
+        points = self.points[points_mask]
+          
+        new_index = np.cumsum(points_mask) - 1
+        elements = new_index[elements]
+        physical_to_elements = {name:np.arange(len(elements))}
+         
+        if name in self.physical_to_lines:
+            return Mesh(points=points, lines=elements, physical_to_lines=physical_to_elements)
+        elif name in self.physical_to_triangles:
+            return Mesh(points=points, triangles=elements, physical_to_triangles=physical_to_elements)
+     
+    @staticmethod
+    def read_file(filename,  name=None):
+        """Create a mesh from a given file. All formats supported by meshio are accepted.
+
+        Parameters
+        ------------------------------
+        filename: str
+            Path of the file to convert to Mesh
+        name: str
+            (optional) name to assign to all elements readed
+
+        Returns
+        -------------------------------
+        Mesh"""
+        meshio_obj = meshio.read(filename)
+        mesh = Mesh.from_meshio(meshio_obj)
+         
+        if name is not None:
+            mesh.physical_to_lines[name] = np.arange(len(mesh.lines))
+            mesh.physical_to_triangles[name] = np.arange(len(mesh.triangles))
+         
+        return mesh
+     
+    def write_file(self, filename):
+        """Write a mesh to a given file. The format is determined from the file extension.
+        All formats supported by meshio are supported.
+
+        Parameters
+        ----------------------------------
+        filename: str
+            The name of the file to write the mesh to."""
+        meshio_obj = self.to_meshio()
+        meshio_obj.write(filename)
+    
+    def write(self, filename):
+        self.write_file(filename)
+        
+     
+    def to_meshio(self):
+        """Convert the Mesh to a meshio object.
+
+        Returns
+        ------------------------------------
+        meshio.Mesh"""
+        to_write = []
+        
+        if len(self.lines):
+            line_type = 'line' if self.lines.shape[1] == 2 else 'line4'
+            to_write.append( (line_type, self.lines) )
+        
+        if len(self.triangles):
+            triangle_type = 'triangle' if self.triangles.shape[1] == 3 else 'triangle6'
+            to_write.append( (triangle_type, self.triangles) )
+        
+        return meshio.Mesh(self.points, to_write)
+     
+    @staticmethod
+    def from_meshio(mesh: meshio.Mesh):
+        """Create a Traceon mesh from a meshio.Mesh object.
+
+        Parameters
+        --------------------------
+        mesh: meshio.Mesh
+            The mesh to convert to a Traceon mesh
+
+        Returns
+        -------------------------
+        Mesh"""
+        def extract(type_):
+            elements = mesh.cells_dict[type_]
+            physical = {k:v[type_] for k,v in mesh.cell_sets_dict.items() if type_ in v}
+            return elements, physical
+        
+        lines, physical_lines = [], {}
+        triangles, physical_triangles = [], {}
+        
+        if 'line' in mesh.cells_dict:
+            lines, physical_lines = extract('line')
+        elif 'line4' in mesh.cells_dict:
+            lines, physical_lines = extract('line4')
+        
+        if 'triangle' in mesh.cells_dict:
+            triangles, physical_triangles = extract('triangle')
+        elif 'triangle6' in mesh.cells_dict:
+            triangles, physical_triangles = extract('triangle6')
+        
+        return Mesh(points=mesh.points,
+            lines=lines, physical_to_lines=physical_lines,
+            triangles=triangles, physical_to_triangles=physical_triangles)
+     
+    def is_3d(self):
+        """Check if the mesh is three dimensional by checking whether any z coordinate is non-zero.
+
+        Returns
+        ----------------
+        bool
+
+        Whether the mesh is three dimensional"""
+        return np.any(self.points[:, 1] != 0.)
+    
+    def is_2d(self):
+        """Check if the mesh is two dimensional, by checking that all z coordinates are zero.
+        
+        Returns
+        ----------------
+        bool
+
+        Whether the mesh is two dimensional"""
+        return np.all(self.points[:, 1] == 0.)
+    
+    def flip_normals(self):
+        """Flip the normals in the mesh by inverting the 'orientation' of the elements.
+
+        Returns
+        ----------------------------
+        Mesh"""
+        lines = self.lines
+        triangles = self.triangles
+        
+        # Flip the orientation of the lines
+        if lines.shape[1] == 4:
+            p0, p1, p2, p3 = lines.T
+            lines = np.array([p1, p0, p3, p2]).T
+        else:
+            p0, p1 = lines.T
+            lines = np.array([p1, p0]).T
+          
+        # Flip the orientation of the triangles
+        if triangles.shape[1] == 6:
+            p0, p1, p2, p3, p4, p5 = triangles.T
+            triangles = np.array([p0, p2, p1, p5, p4, p3]).T
+        else:
+            p0, p1, p2 = triangles.T
+            triangles = np.array([p0, p2, p1]).T
+        
+        return Mesh(self.points, lines, triangles,
+            physical_to_lines=self.physical_to_lines,
+            physical_to_triangles=self.physical_to_triangles)
+     
+    def remove_lines(self):
+        """Remove all the lines from the mesh.
+
+        Returns
+        -----------------------------
+        Mesh"""
+        return Mesh(self.points, triangles=self.triangles, physical_to_triangles=self.physical_to_triangles)
+    
+    def remove_triangles(self):
+        """Remove all triangles from the mesh.
+
+        Returns
+        -------------------------------------
+        Mesh"""
+        return Mesh(self.points, lines=self.lines, physical_to_lines=self.physical_to_lines)
+     
+    def get_electrodes(self):
+        """Get the names of all the named groups (i.e. electrodes) in the mesh
+         
+        Returns
+        ---------
+        str iterable
+
+        Names
+        """
+        return list(self.physical_to_lines.keys()) + list(self.physical_to_triangles.keys())
+     
+    @staticmethod
+    def _lines_to_higher_order(points, elements):
+        N_elements = len(elements)
+        N_points = len(points)
+         
+        v0, v1 = elements.T
+        p2 = points[v0] + (points[v1] - points[v0]) * 1/3
+        p3 = points[v0] + (points[v1] - points[v0]) * 2/3
+         
+        assert all(p.shape == (N_elements, points.shape[1]) for p in [p2, p3])
+         
+        points = np.concatenate( (points, p2, p3), axis=0)
+          
+        elements = np.array([
+            elements[:, 0], elements[:, 1], 
+            np.arange(N_points, N_points + N_elements, dtype=np.uint64),
+            np.arange(N_points + N_elements, N_points + 2*N_elements, dtype=np.uint64)]).T
+         
+        assert np.allclose(p2, points[elements[:, 2]]) and np.allclose(p3, points[elements[:, 3]])
+        return points, elements
+
+
+    def _to_higher_order_mesh(self):
+        # The matrix solver currently only works with higher order meshes.
+        # We can however convert a simple mesh easily to a higher order mesh, and solve that.
+        
+        points, lines, triangles = self.points, self.lines, self.triangles
+
+        if not len(lines):
+            lines = np.empty( (0, 4), dtype=np.float64)
+        elif len(lines) and lines.shape[1] == 2:
+            points, lines = Mesh._lines_to_higher_order(points, lines)
+        
+        assert lines.shape == (len(lines), 4)
+
+        return Mesh(points=points,
+            lines=lines, physical_to_lines=self.physical_to_lines,
+            triangles=triangles, physical_to_triangles=self.physical_to_triangles)
+     
+    def __str__(self):
+        physical_lines = ', '.join(self.physical_to_lines.keys())
+        physical_lines_nums = ', '.join([str(len(self.physical_to_lines[n])) for n in self.physical_to_lines.keys()])
+        physical_triangles = ', '.join(self.physical_to_triangles.keys())
+        physical_triangles_nums = ', '.join([str(len(self.physical_to_triangles[n])) for n in self.physical_to_triangles.keys()])
+        
+        return f'<Traceon Mesh,\n' \
+            f'\tNumber of points: {len(self.points)}\n' \
+            f'\tNumber of lines: {len(self.lines)}\n' \
+            f'\tNumber of triangles: {len(self.triangles)}\n' \
+            f'\tPhysical lines: {physical_lines}\n' \
+            f'\tElements in physical line groups: {physical_lines_nums}\n' \
+            f'\tPhysical triangles: {physical_triangles}\n' \
+            f'\tElements in physical triangle groups: {physical_triangles_nums}>'
+
+    def _ensure_normal_orientation_triangles(self, electrode, outwards):
+        assert electrode in self.physical_to_triangles, "electrode should be part of mesh"
+        
+        triangle_indices = self.physical_to_triangles[electrode]
+        electrode_triangles = self.triangles[triangle_indices]
+          
+        if not len(electrode_triangles):
+            return
+        
+        connected_indices = _get_connected_elements(electrode_triangles)
+        
+        for indices in connected_indices:
+            connected_triangles = electrode_triangles[indices]
+            _ensure_triangle_orientation(connected_triangles, self.points, outwards)
+            electrode_triangles[indices] = connected_triangles
+
+        self.triangles[triangle_indices] = electrode_triangles
+     
+    def _ensure_normal_orientation_lines(self, electrode, outwards):
+        assert electrode in self.physical_to_lines, "electrode should be part of mesh"
+        
+        line_indices = self.physical_to_lines[electrode]
+        electrode_lines = self.lines[line_indices]
+          
+        if not len(electrode_lines):
+            return
+        
+        connected_indices = _get_connected_elements(electrode_lines)
+        
+        for indices in connected_indices:
+            connected_lines = electrode_lines[indices]
+            _ensure_line_orientation(connected_lines, self.points, outwards)
+            electrode_lines[indices] = connected_lines
+
+        self.lines[line_indices] = electrode_lines
+     
+    def ensure_outward_normals(self, electrode):
+        if electrode in self.physical_to_triangles:
+            self._ensure_normal_orientation_triangles(electrode, True)
+        
+        if electrode in self.physical_to_lines:
+            self._ensure_normal_orientation_lines(electrode, True)
+     
+    def ensure_inward_normals(self, electrode):
+        if electrode in self.physical_to_triangles:
+            self._ensure_normal_orientation_triangles(electrode, False)
+         
+        if electrode in self.physical_to_lines:
+            self._ensure_normal_orientation_lines(electrode, False)
+
+ +

Mesh containing lines and triangles. Groups of lines or triangles can be named. These +names are later used to apply the correct excitation. Line elements can be curved (or 'higher order'), +in which case they are represented by four points per element. Note that Mesh is a subclass of +GeometricObject, and therefore can be easily moved and rotated.

+ + +

Ancestors

+ + +

Static methods

+
+ +
+ + def from_meshio(mesh: meshio._mesh.Mesh) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def from_meshio(mesh: meshio.Mesh):
+    """Create a Traceon mesh from a meshio.Mesh object.
+
+    Parameters
+    --------------------------
+    mesh: meshio.Mesh
+        The mesh to convert to a Traceon mesh
+
+    Returns
+    -------------------------
+    Mesh"""
+    def extract(type_):
+        elements = mesh.cells_dict[type_]
+        physical = {k:v[type_] for k,v in mesh.cell_sets_dict.items() if type_ in v}
+        return elements, physical
+    
+    lines, physical_lines = [], {}
+    triangles, physical_triangles = [], {}
+    
+    if 'line' in mesh.cells_dict:
+        lines, physical_lines = extract('line')
+    elif 'line4' in mesh.cells_dict:
+        lines, physical_lines = extract('line4')
+    
+    if 'triangle' in mesh.cells_dict:
+        triangles, physical_triangles = extract('triangle')
+    elif 'triangle6' in mesh.cells_dict:
+        triangles, physical_triangles = extract('triangle6')
+    
+    return Mesh(points=mesh.points,
+        lines=lines, physical_to_lines=physical_lines,
+        triangles=triangles, physical_to_triangles=physical_triangles)
+
+ +

Create a Traceon mesh from a meshio.Mesh object.

+

Parameters

+
+
mesh : meshio.Mesh
+
The mesh to convert to a Traceon mesh
+
+

Returns

+
+
Mesh
+
 
+
+
+ + +
+ + def read_file(filename, name=None) +
+
+ + + +
+ + Expand source code + +
@staticmethod
+def read_file(filename,  name=None):
+    """Create a mesh from a given file. All formats supported by meshio are accepted.
+
+    Parameters
+    ------------------------------
+    filename: str
+        Path of the file to convert to Mesh
+    name: str
+        (optional) name to assign to all elements readed
+
+    Returns
+    -------------------------------
+    Mesh"""
+    meshio_obj = meshio.read(filename)
+    mesh = Mesh.from_meshio(meshio_obj)
+     
+    if name is not None:
+        mesh.physical_to_lines[name] = np.arange(len(mesh.lines))
+        mesh.physical_to_triangles[name] = np.arange(len(mesh.triangles))
+     
+    return mesh
+
+ +

Create a mesh from a given file. All formats supported by meshio are accepted.

+

Parameters

+
+
filename : str
+
Path of the file to convert to Mesh
+
name : str
+
(optional) name to assign to all elements readed
+
+

Returns

+
+
Mesh
+
 
+
+
+ +
+

Methods

+
+ +
+ + def __add__(self, other) +
+
+ + + +
+ + Expand source code + +
def __add__(self, other):
+    """Add meshes together, using the + operator (mesh1 + mesh2).
+    
+    Returns
+    ------------------------------
+    Mesh
+
+    A new mesh consisting of the elements of the added meshes"""
+    if not isinstance(other, Mesh):
+        return NotImplemented
+     
+    N_points = len(self.points)
+    N_lines = len(self.lines)
+    N_triangles = len(self.triangles)
+     
+    points = _concat_arrays(self.points, other.points)
+    lines = _concat_arrays(self.lines, other.lines+N_points)
+    triangles = _concat_arrays(self.triangles, other.triangles+N_points)
+
+    other_physical_to_lines = {k:(v+N_lines) for k, v in other.physical_to_lines.items()}
+    other_physical_to_triangles = {k:(v+N_triangles) for k, v in other.physical_to_triangles.items()}
+     
+    physical_lines = Mesh._merge_dicts(self.physical_to_lines, other_physical_to_lines)
+    physical_triangles = Mesh._merge_dicts(self.physical_to_triangles, other_physical_to_triangles)
+     
+    return Mesh(points=points,
+                lines=lines,
+                triangles=triangles,
+                physical_to_lines=physical_lines,
+                physical_to_triangles=physical_triangles)
+
+ +

Add meshes together, using the + operator (mesh1 + mesh2).

+

Returns

+
+
Mesh
+
 
+
A new mesh consisting of the elements of the added meshes
+
 
+
+
+ + +
+ + def ensure_inward_normals(self, electrode) +
+
+ + + +
+ + Expand source code + +
def ensure_inward_normals(self, electrode):
+    if electrode in self.physical_to_triangles:
+        self._ensure_normal_orientation_triangles(electrode, False)
+     
+    if electrode in self.physical_to_lines:
+        self._ensure_normal_orientation_lines(electrode, False)
+
+ +
+
+ + +
+ + def ensure_outward_normals(self, electrode) +
+
+ + + +
+ + Expand source code + +
def ensure_outward_normals(self, electrode):
+    if electrode in self.physical_to_triangles:
+        self._ensure_normal_orientation_triangles(electrode, True)
+    
+    if electrode in self.physical_to_lines:
+        self._ensure_normal_orientation_lines(electrode, True)
+
+ +
+
+ + +
+ + def extract_physical_group(self, name) +
+
+ + + +
+ + Expand source code + +
def extract_physical_group(self, name):
+    """Extract a named group from the mesh.
+
+    Parameters
+    ---------------------------
+    name: str
+        Name of the group of elements
+
+    Returns
+    --------------------------
+    Mesh
+
+    Subset of the mesh consisting only of the elements with the given name."""
+    assert name in self.physical_to_lines or name in self.physical_to_triangles, "Physical group not in mesh, so cannot extract"
+
+    if name in self.physical_to_lines:
+        elements = self.lines
+        physical = self.physical_to_lines
+    elif name in self.physical_to_triangles:
+        elements = self.triangles
+        physical = self.physical_to_triangles
+     
+    elements_indices = np.unique(physical[name])
+    elements = elements[elements_indices]
+      
+    points_mask = np.full(len(self.points), False)
+    points_mask[elements] = True
+    points = self.points[points_mask]
+      
+    new_index = np.cumsum(points_mask) - 1
+    elements = new_index[elements]
+    physical_to_elements = {name:np.arange(len(elements))}
+     
+    if name in self.physical_to_lines:
+        return Mesh(points=points, lines=elements, physical_to_lines=physical_to_elements)
+    elif name in self.physical_to_triangles:
+        return Mesh(points=points, triangles=elements, physical_to_triangles=physical_to_elements)
+
+ +

Extract a named group from the mesh.

+

Parameters

+
+
name : str
+
Name of the group of elements
+
+

Returns

+
+
Mesh
+
 
+
+

Subset of the mesh consisting only of the elements with the given name.

+
+ + +
+ + def flip_normals(self) +
+
+ + + +
+ + Expand source code + +
def flip_normals(self):
+    """Flip the normals in the mesh by inverting the 'orientation' of the elements.
+
+    Returns
+    ----------------------------
+    Mesh"""
+    lines = self.lines
+    triangles = self.triangles
+    
+    # Flip the orientation of the lines
+    if lines.shape[1] == 4:
+        p0, p1, p2, p3 = lines.T
+        lines = np.array([p1, p0, p3, p2]).T
+    else:
+        p0, p1 = lines.T
+        lines = np.array([p1, p0]).T
+      
+    # Flip the orientation of the triangles
+    if triangles.shape[1] == 6:
+        p0, p1, p2, p3, p4, p5 = triangles.T
+        triangles = np.array([p0, p2, p1, p5, p4, p3]).T
+    else:
+        p0, p1, p2 = triangles.T
+        triangles = np.array([p0, p2, p1]).T
+    
+    return Mesh(self.points, lines, triangles,
+        physical_to_lines=self.physical_to_lines,
+        physical_to_triangles=self.physical_to_triangles)
+
+ +

Flip the normals in the mesh by inverting the 'orientation' of the elements.

+

Returns

+
+
Mesh
+
 
+
+
+ + +
+ + def get_electrodes(self) +
+
+ + + +
+ + Expand source code + +
def get_electrodes(self):
+    """Get the names of all the named groups (i.e. electrodes) in the mesh
+     
+    Returns
+    ---------
+    str iterable
+
+    Names
+    """
+    return list(self.physical_to_lines.keys()) + list(self.physical_to_triangles.keys())
+
+ +

Get the names of all the named groups (i.e. electrodes) in the mesh

+

Returns

+
+
str iterable
+
 
+
Names
+
 
+
+
+ + +
+ + def is_2d(self) +
+
+ + + +
+ + Expand source code + +
def is_2d(self):
+    """Check if the mesh is two dimensional, by checking that all z coordinates are zero.
+    
+    Returns
+    ----------------
+    bool
+
+    Whether the mesh is two dimensional"""
+    return np.all(self.points[:, 1] == 0.)
+
+ +

Check if the mesh is two dimensional, by checking that all z coordinates are zero.

+

Returns

+
+
bool
+
 
+
Whether the mesh is two dimensional
+
 
+
+
+ + +
+ + def is_3d(self) +
+
+ + + +
+ + Expand source code + +
def is_3d(self):
+    """Check if the mesh is three dimensional by checking whether any z coordinate is non-zero.
+
+    Returns
+    ----------------
+    bool
+
+    Whether the mesh is three dimensional"""
+    return np.any(self.points[:, 1] != 0.)
+
+ +

Check if the mesh is three dimensional by checking whether any z coordinate is non-zero.

+

Returns

+
+
bool
+
 
+
Whether the mesh is three dimensional
+
 
+
+
+ + +
+ + def is_higher_order(self) +
+
+ + + +
+ + Expand source code + +
def is_higher_order(self):
+    """Whether the mesh contains higher order elements.
+
+    Returns
+    ----------------------------
+    bool"""
+    return isinstance(self.lines, np.ndarray) and len(self.lines.shape) == 2 and self.lines.shape[1] == 4
+
+ +

Whether the mesh contains higher order elements.

+

Returns

+
+
bool
+
 
+
+
+ + +
+ + def map_points(self, fun) +
+
+ + + +
+ + Expand source code + +
def map_points(self, fun):
+    """See `GeometricObject`
+
+    """
+    new_points = np.empty_like(self.points)
+    for i in range(len(self.points)):
+        new_points[i] = fun(self.points[i])
+    assert new_points.shape == self.points.shape and new_points.dtype == self.points.dtype
+    
+    return Mesh(new_points, self.lines, self.triangles, self.physical_to_lines, self.physical_to_triangles)
+
+ + +
+ + +
+ + def remove_lines(self) +
+
+ + + +
+ + Expand source code + +
def remove_lines(self):
+    """Remove all the lines from the mesh.
+
+    Returns
+    -----------------------------
+    Mesh"""
+    return Mesh(self.points, triangles=self.triangles, physical_to_triangles=self.physical_to_triangles)
+
+ +

Remove all the lines from the mesh.

+

Returns

+
+
Mesh
+
 
+
+
+ + +
+ + def remove_triangles(self) +
+
+ + + +
+ + Expand source code + +
def remove_triangles(self):
+    """Remove all triangles from the mesh.
+
+    Returns
+    -------------------------------------
+    Mesh"""
+    return Mesh(self.points, lines=self.lines, physical_to_lines=self.physical_to_lines)
+
+ +

Remove all triangles from the mesh.

+

Returns

+
+
Mesh
+
 
+
+
+ + +
+ + def to_meshio(self) +
+
+ + + +
+ + Expand source code + +
def to_meshio(self):
+    """Convert the Mesh to a meshio object.
+
+    Returns
+    ------------------------------------
+    meshio.Mesh"""
+    to_write = []
+    
+    if len(self.lines):
+        line_type = 'line' if self.lines.shape[1] == 2 else 'line4'
+        to_write.append( (line_type, self.lines) )
+    
+    if len(self.triangles):
+        triangle_type = 'triangle' if self.triangles.shape[1] == 3 else 'triangle6'
+        to_write.append( (triangle_type, self.triangles) )
+    
+    return meshio.Mesh(self.points, to_write)
+
+ +

Convert the Mesh to a meshio object.

+

Returns

+
+
meshio.Mesh
+
 
+
+
+ + +
+ + def write(self, filename) +
+
+ + + +
+ + Expand source code + +
def write(self, filename):
+    self.write_file(filename)
+
+ +

Write a mesh to a file. The pickle module will be used +to save the Geometry object.

+

Args

+
+
filename
+
name of the file
+
+
+ + +
+ + def write_file(self, filename) +
+
+ + + +
+ + Expand source code + +
def write_file(self, filename):
+    """Write a mesh to a given file. The format is determined from the file extension.
+    All formats supported by meshio are supported.
+
+    Parameters
+    ----------------------------------
+    filename: str
+        The name of the file to write the mesh to."""
+    meshio_obj = self.to_meshio()
+    meshio_obj.write(filename)
+
+ +

Write a mesh to a given file. The format is determined from the file extension. +All formats supported by meshio are supported.

+

Parameters

+
+
filename : str
+
The name of the file to write the mesh to.
+
+
+ +
+ + +

Inherited members

+ + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/plotting.html b/docs/docs/v0.9.0rc1/traceon/plotting.html new file mode 100644 index 0000000..5bafe07 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/plotting.html @@ -0,0 +1,839 @@ + + + + + + + + + + traceon.plotting API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.plotting

+
+ +
+

The traceon.plotting module uses the vedo plotting library to provide some convenience functions +to show the line and triangle meshes generated by Traceon.

+

To show a mesh, for example use:

+
plt.new_figure()
+plt.plot_mesh(mesh)
+plt.show()
+
+

Where mesh is created using the traceon.geometry module.

+
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def get_current_figure() ‑> Figure +
+
+ + + +
+ + Expand source code + +
def get_current_figure() -> Figure:
+    """Get the currently active figure. If no figure has been created yet
+    a new figure will be returned.
+
+    Returns
+    --------------------
+    `Figure`"""
+    if len(_current_figures):
+        return _current_figures[-1]
+    
+    return new_figure()
+
+ +

Get the currently active figure. If no figure has been created yet +a new figure will be returned.

+

Returns

+

Figure

+
+ + +
+ + def new_figure(show_legend=True) +
+
+ + + +
+ + Expand source code + +
def new_figure(show_legend=True):
+    """Create a new figure and make it the current figure.
+    
+    Parameters
+    ----------------------
+    show_legend: bool
+        Whether to show the legend in the corner of the figure
+
+    Returns
+    ----------------------
+    `Figure`"""
+    global _current_figures
+    f = Figure(show_legend=show_legend)
+    _current_figures.append(f)
+    return f
+
+ +

Create a new figure and make it the current figure.

+

Parameters

+
+
show_legend : bool
+
Whether to show the legend in the corner of the figure
+
+

Returns

+

Figure

+
+ + +
+ + def plot_charge_density(*args, **kwargs) +
+
+ + + +
+ + Expand source code + +
def plot_charge_density(*args, **kwargs):
+    """Calls `Figure.plot_charge_density` on the current `Figure`"""
+    get_current_figure().plot_charge_density(*args, **kwargs)
+
+ + +
+ + +
+ + def plot_equipotential_lines(*args, **kwargs) +
+
+ + + +
+ + Expand source code + +
def plot_equipotential_lines(*args, **kwargs):
+    """Calls `Figure.plot_equipotential_lines` on the current `Figure`"""
+    get_current_figure().plot_equipotential_lines(*args, **kwargs)
+
+ + +
+ + +
+ + def plot_mesh(*args, **kwargs) +
+
+ + + +
+ + Expand source code + +
def plot_mesh(*args, **kwargs):
+    """Calls `Figure.plot_mesh` on the current `Figure`"""
+    get_current_figure().plot_mesh(*args, **kwargs)
+
+ +

Calls Figure.plot_mesh() on the current Figure

+
+ + +
+ + def plot_trajectories(*args, **kwargs) +
+
+ + + +
+ + Expand source code + +
def plot_trajectories(*args, **kwargs):
+    """Calls `Figure.plot_trajectories` on the current `Figure`"""
+    get_current_figure().plot_trajectories(*args, **kwargs)
+
+ +

Calls Figure.plot_trajectories() on the current Figure

+
+ + +
+ + def show() +
+
+ + + +
+ + Expand source code + +
def show():
+    """Calls `Figure.show` on the current `Figure`"""
+    global _current_figures
+        
+    for f in _current_figures:
+        f.show()
+
+    _current_figures = []
+
+ +

Calls Figure.show() on the current Figure

+
+ +
+
+ +
+

Classes

+
+ +
+ class Figure + (show_legend=True) +
+ +
+ + + +
+ + Expand source code + +
class Figure:
+    def __init__(self, show_legend=True):
+        self.show_legend = show_legend
+        self.is_2d = True
+        self.legend_entries = []
+        self.to_plot = []
+     
+    def plot_mesh(self, mesh, show_normals=False, **colors):
+        """Plot mesh using the Vedo library. Optionally showing normal vectors.
+
+        Parameters
+        ---------------------
+        mesh: `traceon.mesher.Mesh`
+            The mesh to plot
+        show_normals: bool
+            Whether to show the normal vectors at every element
+        colors: dict of (string, string)
+            Use keyword arguments to specify colors, for example `plot_mesh(mesh, lens='blue', ground='green')`
+        """
+        if not len(mesh.triangles) and not len(mesh.lines):
+            raise RuntimeError("Trying to plot empty mesh.")
+
+        triangle_normals, line_normals = None, None
+        
+        if len(mesh.triangles):
+            meshes, triangle_normals = _get_vedo_triangles_and_normals(mesh, **colors)
+            self.legend_entries.extend(meshes)
+            self.to_plot.append(meshes)
+        
+        if len(mesh.lines):
+            lines, line_normals = _get_vedo_lines_and_normals(mesh, **colors)
+            self.legend_entries.extend(lines)
+            self.to_plot.append(lines)
+         
+        if show_normals:
+            if triangle_normals is not None:
+                self.to_plot.append(triangle_normals)
+            if line_normals is not None:
+                self.to_plot.append(line_normals)
+        
+        self.is_2d &= mesh.is_2d()
+
+    def plot_equipotential_lines(self, field, surface, N0=75, N1=75, color_map='coolwarm', N_isolines=40, isolines_width=1, isolines_color='#444444'):
+        """Make potential color plot including equipotential lines.
+
+        Parameters
+        -------------------------------------
+        field: `traceon.solver.Field`
+            The field used to compute the potential values (note that any field returned from the solver can be used)
+        surface: `traceon.geometry.Surface`
+            The surface in 3D space which will be 'colored in'
+        N0: int
+            Number of pixels to use along the first 'axis' of the surface
+        N1: int
+            Number of pixels to use along the second 'axis' of the surface
+        color_map: str
+            Color map to use to color in the surface
+        N_isolines: int
+            Number of equipotential lines to plot
+        isolines_width: int
+            The width to use for the isolines. Pass in 0 to disable the isolines.
+        isolines_color: str
+            Color to use for the isolines"""
+        grid = _get_vedo_grid(field, surface, N0, N1)
+        isolines = grid.isolines(n=N_isolines).color(isolines_color).lw(isolines_width) # type: ignore
+        grid.cmap(color_map)
+        self.to_plot.append(grid)
+        self.to_plot.append(isolines)
+    
+    def plot_trajectories(self, trajectories, 
+                xmin=None, xmax=None,
+                ymin=None, ymax=None,
+                zmin=None, zmax=None,
+                color='#00AA00', line_width=1):
+        """Plot particle trajectories.
+
+        Parameters
+        ------------------------------------
+        trajectories: list of numpy.ndarray
+            List of positions as returned by `traceon.tracing.Tracer.__call__`
+        xmin, xmax: float
+            Only plot trajectory points for which xmin <= x <= xmax
+        ymin, ymax: float
+            Only plot trajectory points for which ymin <= y <= ymax
+        zmin, zmax: float
+            Only plot trajectory points for which zmin <= z <= zmax
+        color: str
+            Color to use for the particle trajectories
+        line_width: int
+            Width of the trajectory lines
+        """
+        for t in trajectories:
+            if not len(t):
+                continue
+            
+            mask = np.full(len(t), True)
+
+            if xmin is not None:
+                mask &= t[:, 0] >= xmin
+            if xmax is not None:
+                mask &= t[:, 0] <= xmax
+            if ymin is not None:
+                mask &= t[:, 1] >= ymin
+            if ymax is not None:
+                mask &= t[:, 1] <= ymax
+            if zmin is not None:
+                mask &= t[:, 2] >= zmin
+            if zmax is not None:
+                mask &= t[:, 2] <= zmax
+            
+            t = t[mask]
+            
+            if not len(t):
+                continue
+            
+            lines = vedo.shapes.Lines(start_pts=t[:-1, :3], end_pts=t[1:, :3], c=color, lw=line_width)
+            self.to_plot.append(lines)
+
+    def plot_charge_density(self, excitation, field, color_map='coolwarm'):
+        """Plot charge density using the Vedo library.
+        
+        Parameters
+        ---------------------
+        excitation: `traceon.excitation.Excitation`
+            Excitation applied
+        field: `traceon.solver.FieldBEM`
+            Field that resulted after solving for the applied excitation
+        color_map: str
+            Name of the color map to use to color the charge density values
+        """
+        mesh = excitation.mesh
+        
+        if not len(mesh.triangles) and not len(mesh.lines):
+            raise RuntimeError("Trying to plot empty mesh.")
+        
+        if len(mesh.triangles):
+            meshes = _get_vedo_charge_density_3d(excitation, field, color_map)
+            self.to_plot.append(meshes)
+            
+        if len(mesh.lines):
+            lines = _get_vedo_charge_density_2d(excitation, field, color_map)
+            self.to_plot.append(lines)
+         
+        self.is_2d &= mesh.is_2d()
+    
+    def show(self):
+        """Show the figure."""
+        plotter = vedo.Plotter() 
+
+        for t in self.to_plot:
+            plotter += t
+        
+        if self.show_legend:
+            lb = vedo.LegendBox(self.legend_entries)
+            plotter += lb
+        
+        if self.is_2d:
+            plotter.add_global_axes(dict(number_of_divisions=[12, 0, 12], zxgrid=True, xaxis_rotation=90))
+        else:
+            plotter.add_global_axes(dict(number_of_divisions=[10, 10, 10]))
+        
+        plotter.look_at(plane='xz')
+        plotter.show()
+
+ +
+ + + +

Methods

+
+ +
+ + def plot_charge_density(self, excitation, field, color_map='coolwarm') +
+
+ + + +
+ + Expand source code + +
def plot_charge_density(self, excitation, field, color_map='coolwarm'):
+    """Plot charge density using the Vedo library.
+    
+    Parameters
+    ---------------------
+    excitation: `traceon.excitation.Excitation`
+        Excitation applied
+    field: `traceon.solver.FieldBEM`
+        Field that resulted after solving for the applied excitation
+    color_map: str
+        Name of the color map to use to color the charge density values
+    """
+    mesh = excitation.mesh
+    
+    if not len(mesh.triangles) and not len(mesh.lines):
+        raise RuntimeError("Trying to plot empty mesh.")
+    
+    if len(mesh.triangles):
+        meshes = _get_vedo_charge_density_3d(excitation, field, color_map)
+        self.to_plot.append(meshes)
+        
+    if len(mesh.lines):
+        lines = _get_vedo_charge_density_2d(excitation, field, color_map)
+        self.to_plot.append(lines)
+     
+    self.is_2d &= mesh.is_2d()
+
+ +

Plot charge density using the Vedo library.

+

Parameters

+
+
excitation : Excitation
+
Excitation applied
+
field : traceon.solver.FieldBEM
+
Field that resulted after solving for the applied excitation
+
color_map : str
+
Name of the color map to use to color the charge density values
+
+
+ + +
+ + def plot_equipotential_lines(self,
field,
surface,
N0=75,
N1=75,
color_map='coolwarm',
N_isolines=40,
isolines_width=1,
isolines_color='#444444')
+
+
+ + + +
+ + Expand source code + +
def plot_equipotential_lines(self, field, surface, N0=75, N1=75, color_map='coolwarm', N_isolines=40, isolines_width=1, isolines_color='#444444'):
+    """Make potential color plot including equipotential lines.
+
+    Parameters
+    -------------------------------------
+    field: `traceon.solver.Field`
+        The field used to compute the potential values (note that any field returned from the solver can be used)
+    surface: `traceon.geometry.Surface`
+        The surface in 3D space which will be 'colored in'
+    N0: int
+        Number of pixels to use along the first 'axis' of the surface
+    N1: int
+        Number of pixels to use along the second 'axis' of the surface
+    color_map: str
+        Color map to use to color in the surface
+    N_isolines: int
+        Number of equipotential lines to plot
+    isolines_width: int
+        The width to use for the isolines. Pass in 0 to disable the isolines.
+    isolines_color: str
+        Color to use for the isolines"""
+    grid = _get_vedo_grid(field, surface, N0, N1)
+    isolines = grid.isolines(n=N_isolines).color(isolines_color).lw(isolines_width) # type: ignore
+    grid.cmap(color_map)
+    self.to_plot.append(grid)
+    self.to_plot.append(isolines)
+
+ +

Make potential color plot including equipotential lines.

+

Parameters

+
+
field : traceon.solver.Field
+
The field used to compute the potential values (note that any field returned from the solver can be used)
+
surface : Surface
+
The surface in 3D space which will be 'colored in'
+
N0 : int
+
Number of pixels to use along the first 'axis' of the surface
+
N1 : int
+
Number of pixels to use along the second 'axis' of the surface
+
color_map : str
+
Color map to use to color in the surface
+
N_isolines : int
+
Number of equipotential lines to plot
+
isolines_width : int
+
The width to use for the isolines. Pass in 0 to disable the isolines.
+
isolines_color : str
+
Color to use for the isolines
+
+
+ + +
+ + def plot_mesh(self, mesh, show_normals=False, **colors) +
+
+ + + +
+ + Expand source code + +
def plot_mesh(self, mesh, show_normals=False, **colors):
+    """Plot mesh using the Vedo library. Optionally showing normal vectors.
+
+    Parameters
+    ---------------------
+    mesh: `traceon.mesher.Mesh`
+        The mesh to plot
+    show_normals: bool
+        Whether to show the normal vectors at every element
+    colors: dict of (string, string)
+        Use keyword arguments to specify colors, for example `plot_mesh(mesh, lens='blue', ground='green')`
+    """
+    if not len(mesh.triangles) and not len(mesh.lines):
+        raise RuntimeError("Trying to plot empty mesh.")
+
+    triangle_normals, line_normals = None, None
+    
+    if len(mesh.triangles):
+        meshes, triangle_normals = _get_vedo_triangles_and_normals(mesh, **colors)
+        self.legend_entries.extend(meshes)
+        self.to_plot.append(meshes)
+    
+    if len(mesh.lines):
+        lines, line_normals = _get_vedo_lines_and_normals(mesh, **colors)
+        self.legend_entries.extend(lines)
+        self.to_plot.append(lines)
+     
+    if show_normals:
+        if triangle_normals is not None:
+            self.to_plot.append(triangle_normals)
+        if line_normals is not None:
+            self.to_plot.append(line_normals)
+    
+    self.is_2d &= mesh.is_2d()
+
+ +

Plot mesh using the Vedo library. Optionally showing normal vectors.

+

Parameters

+
+
mesh : Mesh
+
The mesh to plot
+
show_normals : bool
+
Whether to show the normal vectors at every element
+
colors : dict of (string, string)
+
Use keyword arguments to specify colors, for example plot_mesh(mesh, lens='blue', ground='green')
+
+
+ + +
+ + def plot_trajectories(self,
trajectories,
xmin=None,
xmax=None,
ymin=None,
ymax=None,
zmin=None,
zmax=None,
color='#00AA00',
line_width=1)
+
+
+ + + +
+ + Expand source code + +
def plot_trajectories(self, trajectories, 
+            xmin=None, xmax=None,
+            ymin=None, ymax=None,
+            zmin=None, zmax=None,
+            color='#00AA00', line_width=1):
+    """Plot particle trajectories.
+
+    Parameters
+    ------------------------------------
+    trajectories: list of numpy.ndarray
+        List of positions as returned by `traceon.tracing.Tracer.__call__`
+    xmin, xmax: float
+        Only plot trajectory points for which xmin <= x <= xmax
+    ymin, ymax: float
+        Only plot trajectory points for which ymin <= y <= ymax
+    zmin, zmax: float
+        Only plot trajectory points for which zmin <= z <= zmax
+    color: str
+        Color to use for the particle trajectories
+    line_width: int
+        Width of the trajectory lines
+    """
+    for t in trajectories:
+        if not len(t):
+            continue
+        
+        mask = np.full(len(t), True)
+
+        if xmin is not None:
+            mask &= t[:, 0] >= xmin
+        if xmax is not None:
+            mask &= t[:, 0] <= xmax
+        if ymin is not None:
+            mask &= t[:, 1] >= ymin
+        if ymax is not None:
+            mask &= t[:, 1] <= ymax
+        if zmin is not None:
+            mask &= t[:, 2] >= zmin
+        if zmax is not None:
+            mask &= t[:, 2] <= zmax
+        
+        t = t[mask]
+        
+        if not len(t):
+            continue
+        
+        lines = vedo.shapes.Lines(start_pts=t[:-1, :3], end_pts=t[1:, :3], c=color, lw=line_width)
+        self.to_plot.append(lines)
+
+ +

Plot particle trajectories.

+

Parameters

+
+
trajectories : list of numpy.ndarray
+
List of positions as returned by Tracer.__call__()
+
xmin, xmax : float
+
Only plot trajectory points for which xmin <= x <= xmax
+
ymin, ymax : float
+
Only plot trajectory points for which ymin <= y <= ymax
+
zmin, zmax : float
+
Only plot trajectory points for which zmin <= z <= zmax
+
color : str
+
Color to use for the particle trajectories
+
line_width : int
+
Width of the trajectory lines
+
+
+ + +
+ + def show(self) +
+
+ + + +
+ + Expand source code + +
def show(self):
+    """Show the figure."""
+    plotter = vedo.Plotter() 
+
+    for t in self.to_plot:
+        plotter += t
+    
+    if self.show_legend:
+        lb = vedo.LegendBox(self.legend_entries)
+        plotter += lb
+    
+    if self.is_2d:
+        plotter.add_global_axes(dict(number_of_divisions=[12, 0, 12], zxgrid=True, xaxis_rotation=90))
+    else:
+        plotter.add_global_axes(dict(number_of_divisions=[10, 10, 10]))
+    
+    plotter.look_at(plane='xz')
+    plotter.show()
+
+ +

Show the figure.

+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/solver.html b/docs/docs/v0.9.0rc1/traceon/solver.html new file mode 100644 index 0000000..dd7800d --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/solver.html @@ -0,0 +1,283 @@ + + + + + + + + + + traceon.solver API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.solver

+
+ +
+

The solver module uses the Boundary Element Method (BEM) to compute the surface charge distribution of a given +geometry and excitation. Once the surface charge distribution is known, the field at any arbitrary position in space +can be calculated by integration over the charged boundary. However, doing a field evaluation in this manner is very slow +as for every field evaluation an iteration needs to be done over all elements in the mesh. Especially for particle tracing it +is crucial that the field evaluation can be done faster. To achieve this, interpolation techniques can be used.

+

The solver package offers interpolation in the form of radial series expansions to drastically increase the speed of ray tracing. For +this consider the axial_derivative_interpolation methods documented below.

+

Radial series expansion in cylindrical symmetry

+

Let \phi_0(z) be the potential along the optical axis. We can express the potential around the optical axis as:

+

+\phi = \phi_0(z_0) - \frac{r^2}{4} \frac{\partial \phi_0^2}{\partial z^2} + \frac{r^4}{64} \frac{\partial^4 \phi_0}{\partial z^4} - \frac{r^6}{2304} \frac{\partial \phi_0^6}{\partial z^6} + \cdots +

+

Therefore, if we can efficiently compute the axial potential derivatives \frac{\partial \phi_0^n}{\partial z^n} we can compute the potential and therefore the fields around the optical axis. +For the derivatives of \phi_0(z) closed form formulas exist in the case of radially symmetric geometries, see for example formula 13.16a in [1]. Traceon uses a recursive version of these formulas to +very efficiently compute the axial derivatives of the potential.

+

Radial series expansion in 3D

+

In a general three dimensional geometry the potential will be dependent not only on the distance from the optical axis but also on the angle \theta around the optical axis +at which the potential is sampled. It turns out (equation (35, 24) in [2]) the potential can be written as follows:

+

+\phi = \sum_{\nu=0}^\infty \sum_{m=0}^\infty r^{2\nu + m} \left( A^\nu_m \cos(m\theta) + B^\nu_m \sin(m\theta) \right) +

+

The A^\nu_m and B^\nu_m coefficients can be expressed in directional derivatives perpendicular to the optical axis, analogous to the radial symmetric case. The +mathematics of calculating these coefficients quickly and accurately gets quite involved, but all details have been abstracted away from the user.

+

References

+

[1] P. Hawkes, E. Kasper. Principles of Electron Optics. Volume one: Basic Geometrical Optics. 2018.

+

[2] W. Glaser. Grundlagen der Elektronenoptik. 1952.

+
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def solve_direct(excitation) +
+
+ + + +
+ + Expand source code + +
def solve_direct(excitation):
+    """
+    Solve for the charges on the surface of the geometry by using a direct method and taking
+    into account the specified `excitation`. 
+
+    Parameters
+    ----------
+    excitation : traceon.excitation.Excitation
+        The excitation that produces the resulting field.
+     
+    Returns
+    -------
+    `FieldRadialBEM`
+    """
+    if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order():
+        excitation = _excitation_to_higher_order(excitation)
+    
+    mag, elec = excitation.is_magnetostatic(), excitation.is_electrostatic()
+
+    assert mag or elec, "Solving for an empty excitation"
+
+    if mag and elec:
+        elec_field = ElectrostaticSolverRadial(excitation).solve_matrix()[0]
+        mag_field = MagnetostaticSolverRadial(excitation).solve_matrix()[0]
+        return elec_field + mag_field # type: ignore
+    elif elec and not mag:
+        return ElectrostaticSolverRadial(excitation).solve_matrix()[0]
+    elif mag and not elec:
+        return MagnetostaticSolverRadial(excitation).solve_matrix()[0]
+
+ +

Solve for the charges on the surface of the geometry by using a direct method and taking +into account the specified excitation.

+

Parameters

+
+
excitation : Excitation
+
The excitation that produces the resulting field.
+
+

Returns

+

FieldRadialBEM

+
+ + +
+ + def solve_direct_superposition(excitation) +
+
+ + + +
+ + Expand source code + +
def solve_direct_superposition(excitation):
+    """
+    When using superposition multiple fields are computed at once. Each field corresponds with a unity excitation (1V)
+    of an electrode that was assigned a non-zero fixed voltage value. This is useful when a geometry needs
+    to be analyzed for many different voltage settings. In this case taking a linear superposition of the returned fields
+    allows to select a different voltage 'setting' without inducing any computational cost. There is no computational cost
+    involved in using `superposition=True` since a direct solver is used which easily allows for multiple right hand sides (the
+    matrix does not have to be inverted multiple times). However, voltage functions are invalid in the superposition process (position dependent voltages).
+    
+    Returns
+    ---------------------------
+    dict of `traceon.field.Field`
+        Each key is the name of an electrode on which a voltage (or current) was applied, the corresponding values are the fields.
+    """
+    if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order():
+        excitation = _excitation_to_higher_order(excitation)
+    
+    # Speedup: invert matrix only once, when using superposition
+    excitations = excitation._split_for_superposition()
+    
+    # Solve for elec fields
+    elec_names = [n for n, v in excitations.items() if v.is_electrostatic()]
+    right_hand_sides = np.array([ElectrostaticSolverRadial(excitations[n]).get_right_hand_side() for n in elec_names])
+    solutions = ElectrostaticSolverRadial(excitation).solve_matrix(right_hand_sides)
+    elec_dict = {n:s for n, s in zip(elec_names, solutions)}
+    
+    # Solve for mag fields 
+    mag_names = [n for n, v in excitations.items() if v.is_magnetostatic()]
+    right_hand_sides = np.array([MagnetostaticSolverRadial(excitations[n]).get_right_hand_side() for n in mag_names])
+    solutions = MagnetostaticSolverRadial(excitation).solve_matrix(right_hand_sides)
+    mag_dict = {n:s for n, s in zip(mag_names, solutions)}
+        
+    return {**elec_dict, **mag_dict}
+
+ +

When using superposition multiple fields are computed at once. Each field corresponds with a unity excitation (1V) +of an electrode that was assigned a non-zero fixed voltage value. This is useful when a geometry needs +to be analyzed for many different voltage settings. In this case taking a linear superposition of the returned fields +allows to select a different voltage 'setting' without inducing any computational cost. There is no computational cost +involved in using superposition=True since a direct solver is used which easily allows for multiple right hand sides (the +matrix does not have to be inverted multiple times). However, voltage functions are invalid in the superposition process (position dependent voltages).

+

Returns

+

dict of Field + Each key is the name of an electrode on which a voltage (or current) was applied, the corresponding values are the fields.

+
+ +
+
+ +
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon/tracing.html b/docs/docs/v0.9.0rc1/traceon/tracing.html new file mode 100644 index 0000000..4018950 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/tracing.html @@ -0,0 +1,724 @@ + + + + + + + + + + traceon.tracing API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.tracing

+
+ +
+

The tracing module allows to trace charged particles within any field type returned by the traceon.solver module. The tracing algorithm +used is RK45 with adaptive step size control [1]. The tracing code is implemented in C (see traceon.backend) and has therefore +excellent performance. The module also provides various helper functions to define appropriate initial velocity vectors and to +compute intersections of the computed traces with various planes.

+

References

+

[1] Erwin Fehlberg. Low-Order Classical Runge-Kutta Formulas With Stepsize Control and their Application to Some Heat +Transfer Problems. 1969. National Aeronautics and Space Administration.

+
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def axis_intersection(positions) +
+
+ + + +
+ + Expand source code + +
def axis_intersection(positions):
+    """Compute the z-value of the intersection of the trajectory with the x=0 plane.
+    Note that this function will not work properly if the trajectory crosses the x=0 plane zero or multiple times.
+    
+    Parameters
+    ----------
+    positions: (N, 6) np.ndarray of float64
+        Positions (and velocities) of a charged particle as returned by `Tracer`.
+    
+    Returns
+    --------
+    float, z-value of the intersection with the x=0 plane
+    """
+    return yz_plane_intersection(positions, 0.0)[2]
+
+ +

Compute the z-value of the intersection of the trajectory with the x=0 plane. +Note that this function will not work properly if the trajectory crosses the x=0 plane zero or multiple times.

+

Parameters

+
+
positions : (N, 6) np.ndarray of float64
+
Positions (and velocities) of a charged particle as returned by Tracer.
+
+

Returns

+
+
float, z-value of the intersection with the x=0 plane
+
 
+
+
+ + +
+ + def plane_intersection(positions, p0, normal) +
+
+ + + +
+ + Expand source code + +
def plane_intersection(positions, p0, normal):
+    """Compute the intersection of a trajectory with a general plane in 3D. The plane is specified
+    by a point (p0) in the plane and a normal vector (normal) to the plane. The intersection
+    point is calculated using a linear interpolation.
+    
+    Parameters
+    ----------
+    positions: (N, 6) np.ndarray of float64
+        Positions of a charged particle as returned by `Tracer`.
+    
+    p0: (3,) np.ndarray of float64
+        A point that lies in the plane.
+
+    normal: (3,) np.ndarray of float64
+        A vector that is normal to the plane. A point p lies in the plane iff `dot(normal, p - p0) = 0` where
+        dot is the dot product.
+    
+    Returns
+    --------
+    np.ndarray of shape (6,) containing the position and velocity of the particle at the intersection point.
+    """
+
+    assert positions.shape == (len(positions), 6), "The positions array should have shape (N, 6)"
+    return backend.plane_intersection(positions, p0, normal)
+
+ +

Compute the intersection of a trajectory with a general plane in 3D. The plane is specified +by a point (p0) in the plane and a normal vector (normal) to the plane. The intersection +point is calculated using a linear interpolation.

+

Parameters

+
+
positions : (N, 6) np.ndarray of float64
+
Positions of a charged particle as returned by Tracer.
+
p0 : (3,) np.ndarray of float64
+
A point that lies in the plane.
+
normal : (3,) np.ndarray of float64
+
A vector that is normal to the plane. A point p lies in the plane iff dot(normal, p - p0) = 0 where +dot is the dot product.
+
+

Returns

+

np.ndarray of shape (6,) containing the position and velocity of the particle at the intersection point.

+
+ + +
+ + def velocity_vec(eV, direction_) +
+
+ + + +
+ + Expand source code + +
def velocity_vec(eV, direction_):
+    """Compute an initial velocity vector in the correct units and direction.
+    
+    Parameters
+    ----------
+    eV: float
+        initial energy in units of eV
+    direction: (3,) numpy array
+        vector giving the correct direction of the initial velocity vector. Does not
+        have to be a unit vector as it is always normalized.
+
+    Returns
+    -------
+    Initial velocity vector with magnitude corresponding to the supplied energy (in eV).
+    The shape of the resulting vector is the same as the shape of `direction`.
+    """
+    assert eV > 0.0, "Please provide a positive energy in eV"
+
+    direction = np.array(direction_)
+    assert direction.shape == (3,), "Please provide a three dimensional direction vector"
+    
+    if eV > 40000:
+        logging.log_warning(f'Velocity vector with large energy ({eV} eV) requested. Note that relativistic tracing is not yet implemented.')
+    
+    return eV * np.array(direction)/np.linalg.norm(direction)
+
+ +

Compute an initial velocity vector in the correct units and direction.

+

Parameters

+
+
eV : float
+
initial energy in units of eV
+
direction : (3,) numpy array
+
vector giving the correct direction of the initial velocity vector. Does not +have to be a unit vector as it is always normalized.
+
+

Returns

+

Initial velocity vector with magnitude corresponding to the supplied energy (in eV). +The shape of the resulting vector is the same as the shape of direction.

+
+ + +
+ + def velocity_vec_spherical(eV, theta, phi) +
+
+ + + +
+ + Expand source code + +
def velocity_vec_spherical(eV, theta, phi):
+    """Compute initial velocity vector given energy and direction computed from spherical coordinates.
+    
+    Parameters
+    ----------
+    eV: float
+        initial energy in units of eV
+    theta: float
+        angle with z-axis (same definition as theta in a spherical coordinate system)
+    phi: float
+        angle with the x-axis (same definition as phi in a spherical coordinate system)
+
+    Returns
+    ------
+    Initial velocity vector of shape (3,) with magnitude corresponding to the supplied energy (in eV).
+    """
+    return velocity_vec(eV, [sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta)])
+
+ +

Compute initial velocity vector given energy and direction computed from spherical coordinates.

+

Parameters

+
+
eV : float
+
initial energy in units of eV
+
theta : float
+
angle with z-axis (same definition as theta in a spherical coordinate system)
+
phi : float
+
angle with the x-axis (same definition as phi in a spherical coordinate system)
+
+

Returns

+

Initial velocity vector of shape (3,) with magnitude corresponding to the supplied energy (in eV).

+
+ + +
+ + def velocity_vec_xz_plane(eV, angle, downward=True) +
+
+ + + +
+ + Expand source code + +
def velocity_vec_xz_plane(eV, angle, downward=True):
+    """Compute initial velocity vector in the xz plane with the given energy and angle with z-axis.
+    
+    Parameters
+    ----------
+    eV: float
+        initial energy in units of eV
+    angle: float
+        angle with z-axis
+    downward: bool
+        whether the velocity vector should point upward or downwards
+     
+    Returns
+    ------
+    Initial velocity vector of shape (3,) with magnitude corresponding to the supplied energy (in eV).
+    """
+    sign = -1 if downward else 1
+    direction = [sin(angle), 0.0, sign*cos(angle)]
+    return velocity_vec(eV, direction)
+
+ +

Compute initial velocity vector in the xz plane with the given energy and angle with z-axis.

+

Parameters

+
+
eV : float
+
initial energy in units of eV
+
angle : float
+
angle with z-axis
+
downward : bool
+
whether the velocity vector should point upward or downwards
+
+

Returns

+

Initial velocity vector of shape (3,) with magnitude corresponding to the supplied energy (in eV).

+
+ + +
+ + def xy_plane_intersection(positions, z) +
+
+ + + +
+ + Expand source code + +
def xy_plane_intersection(positions, z):
+    """Compute the intersection of a trajectory with an xy-plane.
+
+    Parameters
+    ----------
+    positions: (N, 6) np.ndarray of float64
+        Positions (and velocities) of a charged particle as returned by `Tracer`.
+    z: float
+        z-coordinate of the plane with which to compute the intersection
+    
+    Returns
+    --------
+    (6,) array of float64, containing the position and velocity of the particle at the intersection point.
+    """
+    return plane_intersection(positions, np.array([0.,0.,z]), np.array([0., 0., 1.0]))
+
+ +

Compute the intersection of a trajectory with an xy-plane.

+

Parameters

+
+
positions : (N, 6) np.ndarray of float64
+
Positions (and velocities) of a charged particle as returned by Tracer.
+
z : float
+
z-coordinate of the plane with which to compute the intersection
+
+

Returns

+

(6,) array of float64, containing the position and velocity of the particle at the intersection point.

+
+ + +
+ + def xz_plane_intersection(positions, y) +
+
+ + + +
+ + Expand source code + +
def xz_plane_intersection(positions, y):
+    """Compute the intersection of a trajectory with an xz-plane.
+
+    Parameters
+    ----------
+    positions: (N, 6) np.ndarray of float64
+        Positions (and velocities) of a charged particle as returned by `Tracer`.
+    z: float
+        z-coordinate of the plane with which to compute the intersection
+    
+    Returns
+    --------
+    (6,) array of float64, containing the position and velocity of the particle at the intersection point.
+    """
+    return plane_intersection(positions, np.array([0.,y,0.]), np.array([0., 1.0, 0.]))
+
+ +

Compute the intersection of a trajectory with an xz-plane.

+

Parameters

+
+
positions : (N, 6) np.ndarray of float64
+
Positions (and velocities) of a charged particle as returned by Tracer.
+
z : float
+
z-coordinate of the plane with which to compute the intersection
+
+

Returns

+

(6,) array of float64, containing the position and velocity of the particle at the intersection point.

+
+ + +
+ + def yz_plane_intersection(positions, x) +
+
+ + + +
+ + Expand source code + +
def yz_plane_intersection(positions, x):
+    """Compute the intersection of a trajectory with an yz-plane.
+
+    Parameters
+    ----------
+    positions: (N, 6) np.ndarray of float64
+        Positions (and velocities) of a charged particle as returned by `Tracer`.
+    z: float
+        z-coordinate of the plane with which to compute the intersection
+    
+    Returns
+    --------
+    (6,) array of float64, containing the position and velocity of the particle at the intersection point.
+    """
+    return plane_intersection(positions, np.array([x,0.,0.]), np.array([1.0, 0., 0.]))
+
+ +

Compute the intersection of a trajectory with an yz-plane.

+

Parameters

+
+
positions : (N, 6) np.ndarray of float64
+
Positions (and velocities) of a charged particle as returned by Tracer.
+
z : float
+
z-coordinate of the plane with which to compute the intersection
+
+

Returns

+

(6,) array of float64, containing the position and velocity of the particle at the intersection point.

+
+ +
+
+ +
+

Classes

+
+ +
+ class Tracer + (field, bounds) +
+ +
+ + + +
+ + Expand source code + +
class Tracer:
+    """General tracer class for charged particles. Can trace charged particles given any field class from `traceon.solver`.
+
+    Parameters
+    ----------
+    field: traceon.solver.Field (or any class inheriting Field)
+        The field used to compute the force felt by the charged particle.
+    bounds: (3, 2) np.ndarray of float64
+        Once the particle reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
+    """
+    
+    def __init__(self, field, bounds):
+        self.field = field
+         
+        bounds = np.array(bounds).astype(np.float64)
+        assert bounds.shape == (3,2)
+        self.bounds = bounds
+
+        self.trace_fun, self.low_level_args, *keep_alive = field.get_low_level_trace_function()
+        
+        # Allow functions to optionally return references to objects that need
+        # to be kept alive as long as the tracer is kept alive. This prevents
+        # memory from being reclaimed while the C backend is still working with it.
+        self.keep_alive = keep_alive
+
+        if self.low_level_args is None:
+            self.trace_args = None
+        elif isinstance(self.low_level_args, int): # Interpret as literal memory address
+            self.trace_args = ctypes.c_void_p(self.low_level_args)
+        else: # Interpret as anything ctypes can make sense of
+            self.trace_args = ctypes.cast(ctypes.pointer(self.low_level_args), ctypes.c_void_p)
+     
+    def __str__(self):
+        field_name = self.field.__class__.__name__
+        bounds_str = ' '.join([f'({bmin:.2f}, {bmax:.2f})' for bmin, bmax in self.bounds])
+        return f'<Traceon Tracer of {field_name},\n\t' \
+            + 'Bounds: ' + bounds_str + ' mm >'
+    
+    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8):
+        """Trace a charged particle.
+
+        Parameters
+        ----------
+        position: (3,) np.ndarray of float64
+            Initial position of the particle.
+        velocity: (3,) np.ndarray of float64
+            Initial velocity (expressed in a vector whose magnitude has units of eV). Use one of the utility functions documented
+            above to create the initial velocity vector.
+        mass: float
+            Particle mass in kilogram (kg). The default value is the electron mass: m_e = 9.1093837015e-31 kg.
+        charge: float
+            Particle charge in Coulomb (C). The default value is the electron charge: -1 * e = -1.602176634e-19 C.
+        atol: float
+            Absolute tolerance determining the accuracy of the trace.
+        
+        Returns
+        -------
+        `(times, positions)` which is a tuple of two numpy arrays. `times` is one dimensional and contains the times
+        at which the positions have been computed. The `positions` array is two dimensional, `positions[i]` correspond
+        to time step `times[i]`. One element of the positions array has shape (6,).
+        The first three elements in the `positions[i]` array contain the x,y,z positions.
+        The last three elements in `positions[i]` contain the vx,vy,vz velocities.
+        """
+        charge_over_mass = charge / mass
+        velocity = _convert_velocity_to_SI(velocity, mass)
+
+        return backend.trace_particle(
+                position,
+                velocity,
+                charge_over_mass, 
+                self.trace_fun,
+                self.bounds,
+                atol,
+                self.trace_args)
+
+ +

General tracer class for charged particles. Can trace charged particles given any field class from traceon.solver.

+

Parameters

+
+
field : traceon.solver.Field (or any class inheriting Field)
+
The field used to compute the force felt by the charged particle.
+
bounds : (3, 2) np.ndarray of float64
+
Once the particle reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
+
+ + + +

Methods

+
+ +
+ + def __call__(self, position, velocity, mass=9.1093837015e-31, charge=-1.602176634e-19, atol=1e-08) +
+
+ + + +
+ + Expand source code + +
def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8):
+    """Trace a charged particle.
+
+    Parameters
+    ----------
+    position: (3,) np.ndarray of float64
+        Initial position of the particle.
+    velocity: (3,) np.ndarray of float64
+        Initial velocity (expressed in a vector whose magnitude has units of eV). Use one of the utility functions documented
+        above to create the initial velocity vector.
+    mass: float
+        Particle mass in kilogram (kg). The default value is the electron mass: m_e = 9.1093837015e-31 kg.
+    charge: float
+        Particle charge in Coulomb (C). The default value is the electron charge: -1 * e = -1.602176634e-19 C.
+    atol: float
+        Absolute tolerance determining the accuracy of the trace.
+    
+    Returns
+    -------
+    `(times, positions)` which is a tuple of two numpy arrays. `times` is one dimensional and contains the times
+    at which the positions have been computed. The `positions` array is two dimensional, `positions[i]` correspond
+    to time step `times[i]`. One element of the positions array has shape (6,).
+    The first three elements in the `positions[i]` array contain the x,y,z positions.
+    The last three elements in `positions[i]` contain the vx,vy,vz velocities.
+    """
+    charge_over_mass = charge / mass
+    velocity = _convert_velocity_to_SI(velocity, mass)
+
+    return backend.trace_particle(
+            position,
+            velocity,
+            charge_over_mass, 
+            self.trace_fun,
+            self.bounds,
+            atol,
+            self.trace_args)
+
+ +

Trace a charged particle.

+

Parameters

+
+
position : (3,) np.ndarray of float64
+
Initial position of the particle.
+
velocity : (3,) np.ndarray of float64
+
Initial velocity (expressed in a vector whose magnitude has units of eV). Use one of the utility functions documented +above to create the initial velocity vector.
+
mass : float
+
Particle mass in kilogram (kg). The default value is the electron mass: m_e = 9.1093837015e-31 kg.
+
charge : float
+
Particle charge in Coulomb (C). The default value is the electron charge: -1 * e = -1.602176634e-19 C.
+
atol : float
+
Absolute tolerance determining the accuracy of the trace.
+
+

Returns

+

(times, positions) which is a tuple of two numpy arrays. times is one dimensional and contains the times +at which the positions have been computed. The positions array is two dimensional, positions[i] correspond +to time step times[i]. One element of the positions array has shape (6,). +The first three elements in the positions[i] array contain the x,y,z positions. +The last three elements in positions[i] contain the vx,vy,vz velocities.

+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon_pro/field.html b/docs/docs/v0.9.0rc1/traceon_pro/field.html new file mode 100644 index 0000000..0d6b52e --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/field.html @@ -0,0 +1,484 @@ + + + + + + + + + + traceon_pro.field API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.field

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+

Classes

+
+ +
+ class Field3DAxial + (field, zmin, zmax, N=None) +
+ +
+ + + +

Field computed using a radial series expansion around the optical axis (z-axis). See comments at the start of this page.

+

Use a radial series expansion around the optical axis to allow for very fast field +evaluations. Constructing the radial series expansion in 3D is much more complicated +than the radial symmetric case, but all details have been abstracted away from the user.

+

Parameters

+
+
zmin : float
+
Location on the optical axis where to start sampling the radial expansion coefficients.
+
zmax : float
+
Location on the optical axis where to stop sampling the radial expansion coefficients. Any field +evaluation outside [zmin, zmax] will return a zero field strength.
+
N : int, optional
+
Number of samples to take on the optical axis, if N=None the amount of samples +is determined by taking into account the number of elements in the mesh.
+
+

Returns

+

Field3DAxial object allowing fast field evaluations.

+ + +

Ancestors

+ + +

Methods

+
+ +
+ + def electrostatic_field_at_point(self, point) +
+
+ + + +

Compute the electric field, \vec{E} = -\nabla \phi

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Numpy array containing the field strengths (in units of V/mm) in the x, y and z directions.

+
+ + +
+ + def electrostatic_potential_at_point(self, point) +
+
+ + + +

Compute the electrostatic potential.

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the potential.
+
+

Returns

+

Potential as a float value (in units of V).

+
+ + +
+ + def get_low_level_trace_function(self) +
+
+ + + +
+
+ + +
+ + def get_tracer(self, bounds) +
+
+ + + +
+
+ + +
+ + def magnetostatic_field_at_point(self, point) +
+
+ + + +

Compute the magnetic field \vec{H}

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(3,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.

+
+ + +
+ + def magnetostatic_potential_at_point(self, point) +
+
+ + + +

Compute the magnetostatic scalar potential (satisfying \vec{H} = -\nabla \phi ) close to the axis.

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of A).

+
+ +
+ + +

Inherited members

+ + +
+ +
+ class Field3D_BEM + (electrostatic_point_charges=None,
magnetostatic_point_charges=None,
current_point_charges=None)
+
+ +
+ + + +

An electrostatic field resulting from a general 3D geometry. The field is a result of the surface charges as computed by the +solve_direct function. See the comments in FieldBEM.

+ + +

Ancestors

+ + +

Methods

+
+ +
+ + def area_of_element(self, i) +
+
+ + + +
+
+ + +
+ + def current_field_at_point(self, point_) +
+
+ + + +
+
+ + +
+ + def electrostatic_field_at_point(self, point_) +
+
+ + + +

Compute the electric field, \vec{E} = -\nabla \phi

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(3,) array of float64 representing the electric field

+
+ + +
+ + def electrostatic_potential_at_point(self, point_) +
+
+ + + +

Compute the electrostatic potential.

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of V).

+
+ + +
+ + def get_low_level_trace_function(self) +
+
+ + + +
+
+ + +
+ + def get_tracer(self, bounds) +
+
+ + + +
+
+ + +
+ + def magnetostatic_field_at_point(self, point_) +
+
+ + + +

Compute the magnetic field \vec{H}

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

(3,) np.ndarray of float64 containing the field strength (in units of A/m) in the x, y and z directions.

+
+ + +
+ + def magnetostatic_potential_at_point(self, point_) +
+
+ + + +

Compute the magnetostatic scalar potential (satisfying \vec{H} = -\nabla \phi )

+

Parameters

+
+
point : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

Potential as a float value (in units of A).

+
+ +
+ + +

Inherited members

+ + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon_pro/index.html b/docs/docs/v0.9.0rc1/traceon_pro/index.html new file mode 100644 index 0000000..fb507fe --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/index.html @@ -0,0 +1,145 @@ + + + + + + + + + + traceon_pro API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Package traceon_pro

+
+ +
+ +
+ +
+

Sub-modules

+
+
traceon_pro.field
+
+ +
+
+
traceon_pro.solver
+
+ +
+
+
traceon_pro.traceon_pro
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon_pro/solver.html b/docs/docs/v0.9.0rc1/traceon_pro/solver.html new file mode 100644 index 0000000..f688548 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/solver.html @@ -0,0 +1,579 @@ + + + + + + + + + + traceon_pro.solver API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.solver

+
+ +
+ +
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def solve_direct(excitation) +
+
+ + + +

Solve for the charges on the surface of the geometry by using a direct method and taking +into account the specified excitation.

+

Parameters

+
+
excitation : Excitation
+
The excitation that produces the resulting field.
+
+

Returns

+

traceon.field.Field3D_BEM

+
+ + +
+ + def solve_direct_superposition(excitation) +
+
+ + + +

superposition : bool + When using superposition the function returns multiple fields. Each field corresponds with a unity excitation (1V) + of a physical group that was previously assigned a non-zero fixed voltage value. This is useful when a geometry needs + to be analyzed for many different voltage settings. In this case taking a linear superposition of the returned fields + allows to select a different voltage 'setting' without inducing any computational cost. There is no computational cost + involved in using superposition=True since a direct solver is used which easily allows for multiple right hand sides (the + matrix does not have to be inverted multiple times). However, voltage functions are invalid in the superposition process (position dependent voltages).

+
+ + +
+ + def solve_fmm(excitation, N_max=256, l_max=12) +
+
+ + + +
+
+ + +
+ + def solve_fmm_superposition(excitation, N_max=256, l_max=12) +
+
+ + + +
+
+ +
+
+ +
+

Classes

+
+ +
+ class ElectrostaticSolver3D + (*args, **kwargs) +
+ +
+ + + +

Helper class that provides a standard way to create an ABC using +inheritance.

+ + +

Ancestors

+
    +
  • Solver3D
  • +
  • traceon.solver.Solver
  • +
  • abc.ABC
  • +
+ +

Subclasses

+ +

Methods

+
+ +
+ + def charges_to_field(self, charges) +
+
+ + + +
+
+ + +
+ + def get_active_elements(self) +
+
+ + + +
+
+ + +
+ + def get_preexisting_field(self, point) +
+
+ + + +

Get a field that exists even if all the charges are zero. This field +is currently always a result of currents, but can in the future be extended +to for example support permanent magnets.

+
+ +
+ + + +
+ +
+ class ElectrostaticSolverFMM + (*args, **kwargs) +
+ +
+ + + +

Helper class that provides a standard way to create an ABC using +inheritance.

+ + +

Ancestors

+ + +

Methods

+
+ +
+ + def solve_fmm(self, N_max=256, l_max=12) +
+
+ + + +
+
+ +
+ + +

Inherited members

+ + +
+ +
+ class MagnetostaticSolver3D + (*args, **kwargs) +
+ +
+ + + +

Helper class that provides a standard way to create an ABC using +inheritance.

+ + +

Ancestors

+
    +
  • Solver3D
  • +
  • traceon.solver.Solver
  • +
  • abc.ABC
  • +
+ +

Methods

+
+ +
+ + def charges_to_field(self, charges) +
+
+ + + +
+
+ + +
+ + def get_active_elements(self) +
+
+ + + +
+
+ + +
+ + def get_current_field(self) ‑> FieldBEM +
+
+ + + +
+
+ + +
+ + def get_preexisting_field(self, point) +
+
+ + + +

Get a field that exists even if all the charges are zero. This field +is currently always a result of currents, but can in the future be extended +to for example support permanent magnets.

+
+ +
+ + + +
+ +
+ class ProgressPrinter + (target_value) +
+ +
+ + + +
+ + + +

Methods

+
+ +
+ + def print_end(self) +
+
+ + + +
+
+ + +
+ + def print_progress(self, current_value, iterations) +
+
+ + + +
+
+ +
+ + + +
+ +
+ class Solver3D + (*args, **kwargs) +
+ +
+ + + +

Helper class that provides a standard way to create an ABC using +inheritance.

+ + +

Ancestors

+
    +
  • traceon.solver.Solver
  • +
  • abc.ABC
  • +
+ +

Subclasses

+ +

Methods

+
+ +
+ + def get_jacobians_and_positions(self, vertices: numpy.ndarray) ‑> Tuple[numpy.ndarray, numpy.ndarray] +
+
+ + + +
+
+ + +
+ + def get_matrix(self) +
+
+ + + +
+
+ + +
+ + def get_normal_vectors(self) +
+
+ + + +
+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html b/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html new file mode 100644 index 0000000..5946601 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html @@ -0,0 +1,577 @@ + + + + + + + + + + traceon_pro.traceon_pro API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.traceon_pro

+
+ +
+ +
+ +
+
+ +
+
+ +
+

Functions

+
+ +
+ + def directional_derivative_triangle(vertices, target, normal) +
+
+ + + +
+
+ + +
+ + def field_3d_traceable_address() +
+
+ + + +
+
+ + +
+ + def fill_matrix_3d(triangles, excitation_types, excitation_values) +
+
+ + + +
+
+ + +
+ + def potential_triangle(vertices, target) +
+
+ + + +
+
+ + +
+ + def self_potential_triangle(vertices) +
+
+ + + +
+
+ + +
+ + def self_potential_triangle_v0(vertices) +
+
+ + + +
+
+ +
+
+ +
+

Classes

+
+ +
+ class EffectivePointCharges3D + (charges, jacobians, positions) +
+ +
+ + + +
+ + + +

Static methods

+
+ +
+ + def empty() +
+
+ + + +
+
+ +
+

Instance variables

+
+ +
var charges
+
+ + + +
+
+ +
var jacobians
+
+ + + +
+
+ +
var positions
+
+ + + +
+
+
+

Methods

+
+ +
+ + def field(self, /, target) +
+
+ + + +
+
+ + +
+ + def potential(self, /, target) +
+
+ + + +
+
+ +
+ + + +
+ +
+ class EffectivePointCurrents3D + (currents, jacobians, positions, directions) +
+ +
+ + + +
+ + + +

Static methods

+
+ +
+ + def empty() +
+
+ + + +
+
+ +
+

Instance variables

+
+ +
var currents
+
+ + + +
+
+ +
var directions
+
+ + + +
+
+ +
var jacobians
+
+ + + +
+
+ +
var positions
+
+ + + +
+
+
+

Methods

+
+ +
+ + def field(self, /, target) +
+
+ + + +
+
+ +
+ + + +
+ +
+ class FastMultipoleMethodPoints + (points_arr, N_max, l_max, normals=None) +
+ +
+ + + +
+ + + +

Methods

+
+ +
+ + def potentials(self, /, charges_arr) +
+
+ + + +
+
+ + +
+ + def potentials_and_derivatives(self, /, charges_arr) +
+
+ + + +
+
+ +
+ + + +
+ +
+ class FastMultipoleMethodTriangles + (points_arr, N_max, l_max, normals=None) +
+ +
+ + + +
+ + + +

Methods

+
+ +
+ + def potentials(self, /, charges_arr) +
+
+ + + +
+
+ + +
+ + def potentials_and_derivatives(self, /, charges_arr) +
+
+ + + +
+
+ +
+ + + +
+ +
+ class FieldEvaluationArgs + (elec, mag, currents, bounds=None) +
+ +
+ + + +
+ + + +

Methods

+
+ +
+ + def address(self, /) +
+
+ + + +
+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file diff --git a/examples/einzel-lens.py b/examples/einzel-lens.py index 0bdc2c6..3fdcaf3 100644 --- a/examples/einzel-lens.py +++ b/examples/einzel-lens.py @@ -37,7 +37,7 @@ excitation = E.Excitation(mesh, E.Symmetry.RADIAL) -# Excite the geometry, put ground at 0V and the lens electrode at 1000V. +# Excite the geometry, put ground at 0V and the lens electrode at 1800V. excitation.add_voltage(ground=0.0, lens=1800) excitation.add_electrostatic_boundary('boundary') diff --git a/generate_docs/custom_pdoc.py b/generate_docs/custom_pdoc.py new file mode 100644 index 0000000..985fa1d --- /dev/null +++ b/generate_docs/custom_pdoc.py @@ -0,0 +1,252 @@ +#!usr/bin/env python3 +"""pdoc's CLI interface and helper functions.""" + +## USE THE FOLLOWING COMMAND: +## python3 ./custom_pdoc.py traceon -o docs --force --html --config latex_math=True + +import argparse +import ast +import os +import os.path as path +import re +import sys +from pathlib import Path +from typing import Dict, List, Sequence +from warnings import warn + +import pdoc +from pdoc import _get_config +from pdoc import tpl_lookup + +parser = argparse.ArgumentParser( + description="Automatically generate API docs for Python modules.", + epilog="Further documentation is available at .", +) +aa = parser.add_argument +mode_aa = parser.add_mutually_exclusive_group().add_argument + +aa( + '--version', action='version', version=f'%(prog)s {pdoc.__version__}') +aa( + "modules", + type=str, + metavar='MODULE', + nargs="+", + help="The Python module name. This may be an import path resolvable in " + "the current environment, or a file path to a Python module or " + "package.", +) +aa( + "-c", "--config", + type=str, + metavar='OPTION=VALUE', + action='append', + default=[], + help="Override template options. This is an alternative to using " + "a custom config.mako file in --template-dir. This option " + "can be specified multiple times.", +) +aa( + "--filter", + type=str, + metavar='STRING', + default=None, + help="Comma-separated list of filters. When specified, " + "only identifiers containing the specified string " + "will be shown in the output. Search is case sensitive. " + "Has no effect when --http is set.", +) +aa( + "-f", "--force", + action="store_true", + help="Overwrite any existing generated (--output-dir) files.", +) +mode_aa( + "--html", + action="store_true", + help="When set, the output will be HTML formatted.", +) +aa( + "--html-dir", + type=str, + help=argparse.SUPPRESS, +) +aa( + "-o", "--output-dir", + type=str, + metavar='DIR', + help="The directory to output generated HTML/markdown files to " + "(default: ./html for --html).", +) +aa( + "--html-no-source", + action="store_true", + help=argparse.SUPPRESS, +) +aa( + "--overwrite", + action="store_true", + help=argparse.SUPPRESS, +) +aa( + "--external-links", + action="store_true", + help=argparse.SUPPRESS, +) +aa( + "--link-prefix", + type=str, + help=argparse.SUPPRESS, +) +aa( + "--close-stdin", + action="store_true", + help="When set, stdin will be closed before importing, to account for " + "ill-behaved modules that block on stdin." +) + +aa( + "--skip-errors", + action="store_true", + help="Upon unimportable modules, warn instead of raising." +) + +args = argparse.Namespace() + + +def module_path(m: pdoc.Module, ext: str): + return path.join(args.output_dir, *re.sub(r'\.html$', ext, m.url()).split('/')) + + +def write_module_html(module, page_content=None, **kwargs): + config = _get_config(**kwargs) + t = tpl_lookup.get_template('/html.mako') + return t.render(module=module, page_content=page_content, **config).strip() + + +def recursive_write_files(m: pdoc.Module, ext: str, pages, **kwargs): + assert ext in ('.html', '.md') + filepath = module_path(m, ext=ext) + + dirpath = path.dirname(filepath) + if not os.access(dirpath, os.R_OK): + os.makedirs(dirpath) + + with open(filepath, 'w', encoding='utf-8') as f: + print(filepath) + f.write(write_module_html(m, pages=pages, **kwargs)) + + for submodule in m.submodules(): + recursive_write_files(submodule, pages=pages, ext=ext, **kwargs) + +def write_page(m: pdoc.Module, file_in, **kwargs): + + with open('pages/' + file_in, 'r', encoding='utf-8') as file: + file_content = file.read() + + output_file = path.join(args.output_dir, 'traceon', file_in.replace('.md', '.html')) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(write_module_html(m, page_content=file_content, **kwargs)) + + print('Written page: ' + output_file) + +def _warn_deprecated(option, alternative='', use_config_mako=False): + msg = f'Program option `{option}` is deprecated.' + if alternative: + msg += f' Use `{alternative}`' + if use_config_mako: + msg += ' or override config.mako template' + msg += '.' + warn(msg, DeprecationWarning, stacklevel=2) + + +def main(_args=None): + """ Command-line entry point """ + global args + args = _args or parser.parse_args() + + if args.close_stdin: + sys.stdin.close() + + if (args.html or args.http) and not args.output_dir: + args.output_dir = 'html' + + if args.html_dir: + _warn_deprecated('--html-dir', '--output-dir') + args.output_dir = args.html_dir + if args.overwrite: + _warn_deprecated('--overwrite', '--force') + args.force = args.overwrite + + template_config = {} + for config_str in args.config: + try: + key, value = config_str.split('=', 1) + value = ast.literal_eval(value) + template_config[key] = value + except Exception: + raise ValueError( + f'Error evaluating --config statement "{config_str}". ' + 'Make sure string values are quoted?' + ) + + if args.html_no_source: + _warn_deprecated('--html-no-source', '-c show_source_code=False', True) + template_config['show_source_code'] = False + if args.link_prefix: + _warn_deprecated('--link-prefix', '-c link_prefix="foo"', True) + template_config['link_prefix'] = args.link_prefix + if args.external_links: + _warn_deprecated('--external-links') + template_config['external_links'] = True + + assert path.isdir('templates') + pdoc.tpl_lookup.directories.insert(0, 'templates') + + # Support loading modules specified as python paths relative to cwd + sys.path.append(os.getcwd()) + + from glob import glob + from sysconfig import get_path + libdir = get_path("platlib") + sys.path.append(libdir) + + if args.filter and args.filter.strip(): + def docfilter(obj, _filters=args.filter.strip().split(',')): + return any(f in obj.refname or + isinstance(obj, pdoc.Class) and f in obj.doc + for f in _filters) + else: + docfilter = None + + traceon_module = pdoc.Module('traceon', docfilter=docfilter, skip_errors=args.skip_errors) + traceon_pro_module = pdoc.Module('traceon_pro', docfilter=docfilter, skip_errors=args.skip_errors) + + modules = [traceon_module, traceon_pro_module] + + pdoc.link_inheritance() + + # Loading is done. Output stage ... + config = pdoc._get_config(**template_config) + + # Load configured global markdown extensions + # XXX: This is hereby enabled only for CLI usage as for + # API use I couldn't figure out where reliably to put it. + if config.get('md_extensions'): + from pdoc.html_helpers import _md + _kwargs = {'extensions': [], 'configs': {}} + _kwargs.update(config.get('md_extensions', {})) + _md.registerExtensions(**_kwargs) + + pages = { + 'einzel-lens.md': 'Einzel lens', + } + + for module in modules: + recursive_write_files(module, pages=pages, ext='.html', **template_config, traceon_module=traceon_module, traceon_pro_module=traceon_pro_module) + + for p in pages.keys(): + write_page(traceon_module, p, **template_config, pages=pages, traceon_module=traceon_module, traceon_pro_module=traceon_pro_module) + +main(parser.parse_args()) diff --git a/generate_docs/images/einzel lens electron traces.png b/generate_docs/images/einzel lens electron traces.png new file mode 100644 index 0000000..a01e97c Binary files /dev/null and b/generate_docs/images/einzel lens electron traces.png differ diff --git a/generate_docs/images/einzel lens potential along axis.png b/generate_docs/images/einzel lens potential along axis.png new file mode 100644 index 0000000..74504f3 Binary files /dev/null and b/generate_docs/images/einzel lens potential along axis.png differ diff --git a/generate_docs/images/einzel lens.png b/generate_docs/images/einzel lens.png new file mode 100644 index 0000000..1d328b5 Binary files /dev/null and b/generate_docs/images/einzel lens.png differ diff --git a/generate_docs/images/einzel_lens_radial.png b/generate_docs/images/einzel_lens_radial.png new file mode 100644 index 0000000..a36eb9d Binary files /dev/null and b/generate_docs/images/einzel_lens_radial.png differ diff --git a/generate_docs/make_docs.sh b/generate_docs/make_docs.sh new file mode 100644 index 0000000..d8669d3 --- /dev/null +++ b/generate_docs/make_docs.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +VERSION=$(python -c "from importlib.metadata import version; print(version('traceon'))") +DIR=../docs/docs/v$VERSION/ + +if [ -d $DIR ]; then + echo "Directory $DIR exists." + exit +fi + +python ./custom_pdoc.py traceon -o $DIR --force --html --config latex_math=True + +cp -r ./images $DIR/images/ + +echo "Done creating documentation for $VERSION" diff --git a/generate_docs/pages/einzel-lens.md b/generate_docs/pages/einzel-lens.md new file mode 100644 index 0000000..781b2b8 --- /dev/null +++ b/generate_docs/pages/einzel-lens.md @@ -0,0 +1,139 @@ +# Einzel lens + +## Introduction + +This example walks you through the code of [examples/einzel-lens.py](https://github.com/leon-vv/Traceon/blob/main/examples/einzel-lens.py). We will +compute the electrostatic field inside an axial symmetric einzel lens and trace a number of electrons through the field. Please follow +the link to find the up-to-date version of the code, including the neccessary `import` statements to actually run the example. To install Traceon, +please first install [Python](https://www.python.org/downloads/) and use the standard `pip` command to install the package: +```bash +pip install traceon +``` + +## Defining the geometry + +First, we have to define the geometry of the element we want to simulate. In the boundary element method (BEM) only the boundaries of the +objects need to be meshed. This implies that in a radial symmetric geometry (like our einzel lens) our elements will be lines. To find the true +3D representation of the einzel lens, image revolving the line elements around the z-axis. The code needed to define the geometry is given below. +```Python +# Dimensions of the einzel lens. +THICKNESS = 0.5 +SPACING = 0.5 +RADIUS = 0.15 + +# Start value of z chosen such that the middle of the einzel +# lens is at z = 0mm. +z0 = -THICKNESS - SPACING - THICKNESS/2 + +boundary = G.Path.line([0., 0., 1.75], [2.0, 0., 1.75]).extend_with_line([2.0, 0., -1.75]).extend_with_line([0., 0., -1.75]) + +margin_right = 0.1 +extent = 2.0 - margin_right + +bottom = G.Path.aperture(THICKNESS, RADIUS, extent, -THICKNESS - SPACING) +middle = G.Path.aperture(THICKNESS, RADIUS, extent) +top = G.Path.aperture(THICKNESS, RADIUS, extent, THICKNESS + SPACING) + +boundary.name = 'boundary' +bottom.name = 'ground' +middle.name = 'lens' +top.name = 'ground' +``` + +Note that we explicitely assign names to the different elements in our geometry. Later, we will use these names to apply the correct _excitations_ +to the elements. Next, we mesh the geometry which transforms it into many small line elements used in the solver. Note, that you can either supply +a `mesh_size` or a `mesh_size_factor` to the `traceon.geometry.Path.mesh` function. + +```Python +mesh = (boundary + bottom + middle + top).mesh(mesh_size_factor=45) + +P.plot_mesh(mesh, lens='blue', ground='green', boundary='purple') +P.show() +``` + + + +## Applying excitations + +We are now ready to apply excitations to our elements. We choose to put 0V on the 'ground' electrode, and 1800V on the 'lens' electrode. We specify +that the boundary electrode is an 'electrostatic boundary', which means that there is no electric field parallel to the surface ($\mathbf{n} \cdot \nabla V = 0$). + +```Python +excitation = E.Excitation(mesh, E.Symmetry.RADIAL) + +# Excite the geometry, put ground at 0V and the lens electrode at 1800V. +excitation.add_voltage(ground=0.0, lens=1800) +excitation.add_electrostatic_boundary('boundary') +``` + +## Solving for the field + +Solving for the field is now just a matter of calling the `traceon.solver.solve_direct` function. The `traceon.field.Field` class returned +provides methods for calculating the resulting potential and electrostatic field, which we can subsequently use to trace electrons. + +```Python +# Use the Boundary Element Method (BEM) to calculate the surface charges, +# the surface charges gives rise to a electrostatic field. +field = S.solve_direct(excitation) +``` + +## Axial interpolation + +Before tracing the electrons, we first construct an axial interpolation of the Einzel lens. In a radial symmetric system the field +close to the optical axis is completely determined by the higher order derivatives of the potential. This fact can be used to trace +electrons very rapidly. The unique strength of the BEM is that there exists closed form formulas for calculating the higher +order derivatives (from the computed charge distribution). In Traceon, we can make this interpolation in a single +line of code: +```Python +field_axial = FieldRadialAxial(field, -1.5, 1.5, 150) +``` +Note that this field is only accurate close to the optical axis (z-axis). We can plot the potential along the axis to ensure ourselves +that the interpolation is working as expected: + +```Python +z = np.linspace(-1.5, 1.5, 150) +pot = [field.potential_at_point([0.0, 0.0, z_]) for z_ in z] +pot_axial = [field_axial.potential_at_point([0.0, 0.0, z_]) for z_ in z] + +plt.title('Potential along axis') +plt.plot(z, pot, label='Surface charge integration') +plt.plot(z, pot_axial, linestyle='dashed', label='Interpolation') +plt.xlabel('z (mm)') +plt.ylabel('Potential (V)') +plt.legend() +``` + + + +## Tracing electrons + +Tracing electrons is now just a matter of calling the `.get_tracer()` method. We provide +to this method the bounds in which we want to trace. Once an electron hits the edges of the bounds the tracing will +automatically stop. + +```Python +# An instance of the tracer class allows us to easily find the trajectories of +# electrons. Here we specify that the interpolated field should be used, and that +# the tracing should stop if the x,y value goes outside ±RADIUS/2 or the z value outside ±10 mm. +tracer = field_axial.get_tracer( [(-RADIUS/2, RADIUS/2), (-RADIUS/2,RADIUS/2), (-10, 10)] ) + +# Start tracing from z=7mm +r_start = np.linspace(-RADIUS/3, RADIUS/3, 7) + +# Initial velocity vector points downwards, with a +# initial speed corresponding to 1000eV. +velocity = T.velocity_vec(1000, [0, 0, -1]) + +trajectories = [] + +for i, r0 in enumerate(r_start): + print(f'Tracing electron {i+1}/{len(r_start)}...') + _, positions = tracer(np.array([r0, 0, 5]), velocity) + trajectories.append(positions) +``` + +From the traces we can see the focusing effect of the lens. If we zoom in on the focus we can clearly see the +spherical aberration, thanks to the high accuracy of both the solver and the field interpolation. + + + diff --git a/generate_docs/templates/css.mako b/generate_docs/templates/css.mako new file mode 100644 index 0000000..b38aaf8 --- /dev/null +++ b/generate_docs/templates/css.mako @@ -0,0 +1,432 @@ +<%! + from pdoc.html_helpers import minify_css +%> + +<%def name="mobile()" filter="minify_css"> + :root { + --highlight-color: #fe9; + } + .flex { + display: flex !important; + } + + body { + line-height: 1.5em; + } + + #content { + padding: 20px; + } + + #content .doc-image { + padding:0px; + display:block; + margin: 0px auto; + } + + #sidebar { + padding: 1.5em; + padding-left:2.5em; + overflow: hidden; + } + + #sidebar > *:last-child { + margin-bottom: 2cm; + } + + #sidebar .title { + font-weight:bold; + } + + + + + .http-server-breadcrumbs { + font-size: 130%; + margin: 0 0 15px 0; + } + + #footer { + font-size: .75em; + padding: 5px 30px; + border-top: 1px solid #ddd; + text-align: right; + } + #footer p { + margin: 0 0 0 1em; + display: inline-block; + } + #footer p:last-child { + margin-right: 30px; + } + + h1, h2, h3, h4, h5 { + font-weight: 300; + } + h1 { + font-size: 2.5em; + line-height: 1.1em; + } + h2 { + font-size: 1.75em; + margin: 2em 0 .50em 0; + } + h3 { + font-size: 1.4em; + margin: 1.6em 0 .7em 0; + } + h4 { + margin: 0; + font-size: 105%; + } + h1:target, + h2:target, + h3:target, + h4:target, + h5:target, + h6:target { + background: var(--highlight-color); + padding: .2em 0; + } + + a { + color: #058; + text-decoration: none; + transition: color .2s ease-in-out; + } + a:visited {color: #503} + a:hover {color: #b62} + + .title code { + font-weight: bold; + } + h2[id^="header-"] { + margin-top: 2em; + } + .ident { + color: #900; + font-weight: bold; + } + + pre code { + font-size: .8em; + line-height: 1.4em; + padding: 1em; + display: block; + } + code { + background: #f3f3f3; + font-family: "DejaVu Sans Mono", monospace; + padding: 1px 4px; + overflow-wrap: break-word; + } + h1 code { background: transparent } + + pre { + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + margin: 1em 0; + } + + #http-server-module-list { + display: flex; + flex-flow: column; + } + #http-server-module-list div { + display: flex; + } + #http-server-module-list dt { + min-width: 10%; + } + #http-server-module-list p { + margin-top: 0; + } + + .toc ul, + #index { + list-style-type: none; + margin: 0; + padding: 0; + } + #index code { + background: transparent; + } + #index .selected { + background: transparent; + font-weight:bold; + } + #index a { + color: #058; + } + #index h3 { + border-bottom: 1px solid #ddd; + } + #index ul { + padding: 0; + } + #index h4 { + margin-top: .6em; + font-weight: bold; + } + /* Make TOC lists have 2+ columns when viewport is wide enough. + Assuming ~20-character identifiers and ~30% wide sidebar. */ + @media (min-width: 200ex) { #index .two-column { column-count: 2 } } + @media (min-width: 300ex) { #index .two-column { column-count: 3 } } + + dl { + margin-bottom: 2em; + } + dl dl:last-child { + margin-bottom: 4em; + } + dd { + margin: 0 0 1em 3em; + } + #header-classes + dl > dd { + margin-bottom: 3em; + } + dd dd { + margin-left: 2em; + } + dd p { + margin: 10px 0; + } + .name { + background: #eee; + font-size: .85em; + padding: 5px 10px; + display: inline-block; + min-width: 40%; + } + .name:hover { + background: #e0e0e0; + } + dt:target .name { + background: var(--highlight-color); + } + .name > span:first-child { + white-space: nowrap; + } + .name.class > span:nth-child(2) { + margin-left: .4em; + } + .inherited { + color: #999; + border-left: 5px solid #eee; + padding-left: 1em; + } + .inheritance em { + font-style: normal; + font-weight: bold; + } + + /* Docstrings titles, e.g. in numpydoc format */ + .desc h2 { + font-weight: 400; + font-size: 1.25em; + } + .desc h3 { + font-size: 1em; + } + .desc dt code { + background: inherit; /* Don't grey-back parameters */ + } + + .source > summary, + .git-link-div { + color: #666; + text-align: right; + font-weight: 400; + font-size: .8em; + text-transform: uppercase; + } + .source summary > * { + white-space: nowrap; + cursor: pointer; + } + .git-link { + color: inherit; + margin-left: 1em; + } + .source pre { + max-height: 500px; + overflow: auto; + margin: 0; + } + .source pre code { + font-size: 12px; + overflow: visible; + min-width: max-content; + } + .hlist { + list-style: none; + } + .hlist li { + display: inline; + } + .hlist li:after { + content: ',\2002'; + } + .hlist li:last-child:after { + content: none; + } + .hlist .hlist { + display: inline; + padding-left: 1em; + } + + img { + max-width: 100%; + } + td { + padding: 0 .5em; + } + + .admonition { + padding: .1em 1em; + margin: 1em 0; + } + .admonition-title { + font-weight: bold; + } + .admonition.note, + .admonition.info, + .admonition.important { + background: #aef; + } + .admonition.todo, + .admonition.versionadded, + .admonition.tip, + .admonition.hint { + background: #dfd; + } + .admonition.warning, + .admonition.versionchanged, + .admonition.deprecated { + background: #fd4; + } + .admonition.error, + .admonition.danger, + .admonition.caution { + background: lightpink; + } + + +<%def name="desktop()" filter="minify_css"> + @media screen and (min-width: 700px) { + #sidebar { + width: 29%; + min-width:525px; + height: 100vh; + overflow: auto; + position: sticky; + top: 0; + } + #content { + width: 80%; + max-width: 110ch; + padding: 3em 6em; + border-left: 1px solid #ddd; + } + pre code { + font-size: 1em; + } + .name { + font-size: 1em; + } + main { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + .toc ul ul, + #index ul ul { + padding-left: 1em; + } + .toc > ul > li { + margin-top: .5em; + } + } + + +<%def name="print()" filter="minify_css"> +@media print { + #sidebar h1 { + page-break-before: always; + } + .source { + display: none; + } +} +@media print { + * { + background: transparent !important; + color: #000 !important; /* Black prints faster: h5bp.com/s */ + box-shadow: none !important; + text-shadow: none !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + font-size: 90%; + } + /* Internal, documentation links, recognized by having a title, + don't need the URL explicity stated. */ + a[href][title]:after { + content: none; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + /* + * Don't show links for images, or javascript/internal links + */ + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; /* h5bp.com/t */ + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page { + margin: 0.5cm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } +} + diff --git a/generate_docs/templates/html.mako b/generate_docs/templates/html.mako new file mode 100644 index 0000000..77d3aa3 --- /dev/null +++ b/generate_docs/templates/html.mako @@ -0,0 +1,478 @@ +<% + import os + import os.path as path + + import pdoc + from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link + + + def link(dobj: pdoc.Doc, name=None): + name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') + if isinstance(dobj, pdoc.External) and not external_links: + return name + url = dobj.url(relative_to=module, link_prefix=link_prefix, + top_ancestor=not show_inherited_members) + return f'{name}' + + + def to_html(text): + return _to_html(text, docformat=docformat, module=module, link=link, latex_math=latex_math) + + + def get_annotation(bound_method, sep=':'): + annot = show_type_annotations and bound_method(link=link) or '' + if annot: + annot = ' ' + sep + '\N{NBSP}' + annot + return annot + + show_source_code = 'traceon_pro' not in module.name +%> + +<%def name="ident(name)">${name} + +<%def name="show_source(d)"> + % if (show_source_code or git_link_template) and \ + not isinstance(d, pdoc.Module) and d.source and \ + d.obj is not getattr(d.inherits, 'obj', None): + <% git_link = format_git_link(git_link_template, d) %> + % if show_source_code: +
+ + Expand source code + % if git_link: + Browse git + %endif + +
${d.source | h}
+
+ % elif git_link: + + %endif + %endif + + +<%def name="show_desc(d, short=False)"> + <% + inherits = ' inherited' if d.inherits else '' + docstring = glimpse(d.docstring) if short or inherits else d.docstring + %> + % if d.inherits: +

+ Inherited from: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls)}.${link(d.inherits, d.name)} + % else: + ${link(d.inherits)} + % endif +

+ % endif + % if not isinstance(d, pdoc.Module): + ${show_source(d)} + % endif +
${docstring | to_html}
+ + +<%def name="show_module_list(modules)"> +

Python module list

+ +% if not modules: +

No modules found.

+% else: +
+ % for name, desc in modules: +
+
${name}
+
${desc | glimpse, to_html}
+
+ % endfor +
+% endif + + +<%def name="show_column_list(items)"> + <% + two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) + %> + + + +<%def name="show_module(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + %> + + <%def name="show_func(f)"> +
+ <% + params = f.params(annotate=show_type_annotations, link=link) + sep = ',
' if sum(map(len, params)) > 75 else ', ' + params = sep.join(params) + return_type = get_annotation(f.return_annotation, '\N{non-breaking hyphen}>') + %> + ${f.funcdef()} ${ident(f.name)}(${params})${return_type} +
+
${show_desc(f)}
+ + +
+ % if http_server: + + % endif +

${'Namespace' if module.is_namespace else \ + 'Package' if module.is_package and not module.supermodule else \ + 'Module'} ${module.name}

+
+ +
+ ${module.docstring | to_html} +
+ +
+ % if submodules: +

Sub-modules

+
+ % for m in submodules: +
${link(m)}
+
${show_desc(m, short=True)}
+ % endfor +
+ % endif +
+ +
+ % if variables: +

Global variables

+
+ % for v in variables: + <% return_type = get_annotation(v.type_annotation) %> +
var ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif +
+ +
+ % if functions: +

Functions

+
+ % for f in functions: + ${show_func(f)} + % endfor +
+ % endif +
+ +
+ % if classes: +

Classes

+
+ % for c in classes: + <% + class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) + smethods = c.functions(show_inherited_members, sort=sort_identifiers) + inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) + methods = c.methods(show_inherited_members, sort=sort_identifiers) + mro = c.mro() + subclasses = c.subclasses() + params = c.params(annotate=show_type_annotations, link=link) + sep = ',
' if sum(map(len, params)) > 75 else ', ' + params = sep.join(params) + %> +
+ class ${ident(c.name)} + % if params: + (${params}) + % endif +
+ +
${show_desc(c)} + + % if mro: +

Ancestors

+
    + % for cls in mro: +
  • ${link(cls)}
  • + % endfor +
+ %endif + + % if subclasses: +

Subclasses

+
    + % for sub in subclasses: +
  • ${link(sub)}
  • + % endfor +
+ % endif + % if class_vars: +

Class variables

+
+ % for v in class_vars: + <% return_type = get_annotation(v.type_annotation) %> +
var ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if smethods: +

Static methods

+
+ % for f in smethods: + ${show_func(f)} + % endfor +
+ % endif + % if inst_vars: +

Instance variables

+
+ % for v in inst_vars: + <% return_type = get_annotation(v.type_annotation) %> +
${v.kind} ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if methods: +

Methods

+
+ % for f in methods: + ${show_func(f)} + % endfor +
+ % endif + + % if not show_inherited_members: + <% + members = c.inherited_members() + %> + % if members: +

Inherited members

+
    + % for cls, mems in members: +
  • ${link(cls)}: +
      + % for m in mems: +
    • ${link(m, name=m.name)}
    • + % endfor +
    + +
  • + % endfor +
+ % endif + % endif + +
+ % endfor +
+ % endif +
+ + +<%def name="module_index(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + supermodule = module.supermodule + %> + + + + + + + + + + +<% + module_list = 'modules' in context.keys() # Whether we're showing module list in server mode +%> + + % if module_list: + Python module list + + % else: + ${module.name} API documentation + + % endif + + + + % if syntax_highlighting: + + %endif + + <%namespace name="css" file="css.mako" /> + + + + + % if google_analytics: + + + % endif + + % if google_search_query: + + + + % endif + + % if latex_math: + + + % endif + + % if syntax_highlighting: + + + % endif + + <%include file="head.mako"/> + + +
+ % if page_content: +
+ + ${ page_content | to_html } +
+ ${module_index(module)} + % else: +
+ ${show_module(module)} +
+ ${module_index(module)} + % endif +
+ + + +% if http_server and module: ## Auto-reload on file change in dev mode + +% endif + + diff --git a/setup.py b/setup.py index d0c3738..8b43ec4 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name='traceon', - version='0.8.0', + version='0.9.0rc1', description='Solver and tracer for electrostatic problems', url='https://github.com/leon-vv/Traceon', author='Léon van Velzen', diff --git a/traceon/excitation.py b/traceon/excitation.py index c67a257..7d2770a 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -3,15 +3,13 @@ The possible excitations are as follows: -- Fixed voltage (electrode connect to a power supply) -- Voltage function (a generic Python function specifies the voltage as a function of position) +- Voltage (either fixed or as a function of position) - Dielectric, with arbitrary electric permittivity -- Current coil, with fixed total amount of current (only in radial symmetry) +- Current coil (radial symmetric geometry) +- Current lines (3D geometry) - Magnetostatic scalar potential - Magnetizable material, with arbitrary magnetic permeability -Currently current excitations are not supported in 3D. But magnetostatic fields can still be computed using the magnetostatic scalar potential. - Once the excitation is specified, it can be passed to `traceon.solver.solve_direct` to compute the resulting field. """ from enum import IntEnum diff --git a/traceon/field.py b/traceon/field.py index 6ce55b6..3ce06c4 100644 --- a/traceon/field.py +++ b/traceon/field.py @@ -10,6 +10,12 @@ from . import logging from . import backend +__pdoc__ = {} +__pdoc__['EffectivePointCharges'] = False +__pdoc__['Field.get_low_level_trace_function'] = False +__pdoc__['FieldRadialBEM.get_low_level_trace_function'] = False +__pdoc__['FieldRadialAxial.get_low_level_trace_function'] = False + class EffectivePointCharges: def __init__(self, charges, jacobians, positions, directions=None): self.charges = np.array(charges, dtype=np.float64) @@ -77,6 +83,9 @@ def __str__(self): class Field(ABC): + """The abstract `Field` class provides the method definitions that all field classes should implement. Note that + any child clas of the `Field` class can be passed to `traceon.tracing.Tracer` to trace particles through the field.""" + def field_at_point(self, point): """Convenience function for getting the field in the case that the field is purely electrostatic or magneotstatic. Automatically picks one of `electrostatic_field_at_point` or `magnetostatic_field_at_point`. diff --git a/traceon/mesher.py b/traceon/mesher.py index 2210b58..54e201c 100644 --- a/traceon/mesher.py +++ b/traceon/mesher.py @@ -23,7 +23,7 @@ class GeometricObject(ABC): """The Mesh class (and the classes defined in `traceon.geometry`) are subclasses - of GeometricObject. This means that they all can be moved, rotated, mirrored.""" + of `traceon.mesher.GeometricObject`. This means that they all can be moved, rotated, mirrored.""" @abstractmethod def map_points(self, fun: Callable[[np.ndarray], np.ndarray]) -> Any: diff --git a/traceon/solver.py b/traceon/solver.py index 81db056..d6070c4 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -41,7 +41,10 @@ __pdoc__ = {} __pdoc__['EffectivePointCharges'] = False __pdoc__['ElectrostaticSolver'] = False +__pdoc__['ElectrostaticSolverRadial'] = False __pdoc__['MagnetostaticSolver'] = False +__pdoc__['MagnetostaticSolverRadial'] = False +__pdoc__['SolverRadial'] = False __pdoc__['Solver'] = False import math as m @@ -352,13 +355,17 @@ def _excitation_to_higher_order(excitation): def solve_direct_superposition(excitation): """ - superposition : bool - When using superposition the function returns multiple fields. Each field corresponds with a unity excitation (1V) - of a physical group that was previously assigned a non-zero fixed voltage value. This is useful when a geometry needs - to be analyzed for many different voltage settings. In this case taking a linear superposition of the returned fields - allows to select a different voltage 'setting' without inducing any computational cost. There is no computational cost - involved in using `superposition=True` since a direct solver is used which easily allows for multiple right hand sides (the - matrix does not have to be inverted multiple times). However, voltage functions are invalid in the superposition process (position dependent voltages). + When using superposition multiple fields are computed at once. Each field corresponds with a unity excitation (1V) + of an electrode that was assigned a non-zero fixed voltage value. This is useful when a geometry needs + to be analyzed for many different voltage settings. In this case taking a linear superposition of the returned fields + allows to select a different voltage 'setting' without inducing any computational cost. There is no computational cost + involved in using `superposition=True` since a direct solver is used which easily allows for multiple right hand sides (the + matrix does not have to be inverted multiple times). However, voltage functions are invalid in the superposition process (position dependent voltages). + + Returns + --------------------------- + dict of `traceon.field.Field` + Each key is the name of an electrode on which a voltage (or current) was applied, the corresponding values are the fields. """ if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order(): excitation = _excitation_to_higher_order(excitation)