From 506ed58a17b54f0a2a7298b326b3a9b557836fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 7 Dec 2024 22:56:51 +0100 Subject: [PATCH 01/85] Start implementing current_field_at_point for 3D --- tests/test_three_d.py | 43 ++++++++++++++ traceon/backend/__init__.py | 12 ++++ traceon/backend/three_d.c | 31 +++++++++++ traceon/excitation.py | 16 ++++-- traceon/solver.py | 108 +++++++++++++++++++++++++++++++----- 5 files changed, 189 insertions(+), 21 deletions(-) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 3ede83c..2abb38f 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -73,6 +73,49 @@ def test_dohi_meshing(self): geom.mesh(mesh_size_factor=4) +class TestCurrentLoop(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.radius = 5. + cls.current = 2.5 + + path = G.Path.circle_xy(0., 0., cls.radius) + path.name = 'loop' + + mesh = path.mesh(mesh_size_factor=200) + + exc = E.Excitation(mesh, E.Symmetry.THREE_D) + exc.add_current(loop=cls.current) + + solver = S.MagnetostaticSolver(exc) + cls.field = solver.current_field + + def test_field_on_axis(self): + z = np.linspace(-20, 20, 25) + # http://hyperphysics.phy-astr.gsu.edu/hbase/magnetic/curloo.html + correct = 1/2 * self.current * self.radius**2 / (z**2 + self.radius**2)**(3/2) + approx = np.array([self.field.current_field_at_point([0., 0., z_]) for z_ in z]) + + assert np.allclose(approx[:, 0], 0.0) + assert np.allclose(approx[:, 1], 0.0) + assert np.allclose(correct, approx[:, 2], rtol=1e-4) + + def test_field_in_loop(self): + def get_exact(x_): + radius = self.radius + f = lambda t: -self.current*radius**2/(4*np.pi*x_**3) * (x_/radius * np.cos(t) - 1)*(1 + radius**2/x_**2 - 2*radius/x_*np.cos(t))**(-3/2) + return quad(f, 0, 2*np.pi)[0] + + x = np.concatenate( (np.linspace(0.25*self.radius, 0.75*self.radius, 10), np.linspace(1.25*self.radius, 2*self.radius, 10)) ) + correct = [get_exact(x_) for x_ in x] + + approx = np.array([self.field.current_field_at_point([x_, 0., 0.]) for x_ in x]) + + assert np.allclose(approx[:, 0], 0.0) + assert np.allclose(approx[:, 1], 0.0) + assert np.allclose(correct, approx[:, 2], rtol=1e-4) + class TestFlatEinzelLens(unittest.TestCase): diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index e8ef612..d818bf9 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -171,6 +171,7 @@ def __init__(self, eff, *args, **kwargs): 'dz1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'axial_coefficients_3d': (None, charges_3d, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), arr(ndim=3), sz, z_values, arr(ndim=4), sz), + 'fill_jacobian_buffer_current_three_d': (None, lines, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), sz), 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), @@ -456,6 +457,17 @@ def axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, z): return output_coeffs +def fill_jacobian_buffer_current_three_d(lines): + assert lines.shape == (len(lines), 2, 3) + + jacobians = np.zeros( (len(lines), N_QUAD_2D) ) + positions = np.zeros( (len(lines), N_QUAD_2D, 3) ) + directions = np.zeros( (len(lines), N_QUAD_2D, 3) ) + + backend_lib.fill_jacobian_buffer_current_three_d(lines, jacobians, positions, directions, len(lines)) + + return jacobians, positions, directions + def potential_3d(point, charges, jac_buffer, pos_buffer): assert point.shape == (3,) N = len(charges) diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index efba5a4..e42fbce 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -29,6 +29,37 @@ struct field_derivs_args { size_t N_z; }; +EXPORT void +fill_jacobian_buffer_current_three_d( + double (*line_points)[2][3], + double (*jacobians)[N_QUAD_2D], + double (*positions)[N_QUAD_2D][3], + double (*directions)[N_QUAD_2D][3], + size_t N_lines) +{ + for(int i = 0; i < N_lines; i++) { + double *v1 = &line_points[i][0][0]; + double *v2 = &line_points[i][1][0]; + + double length = distance_3d(v1, v2); + + for(int k = 0; k < N_QUAD_2D; k++) { + double g = 0.5*(GAUSS_QUAD_POINTS[k] + 1); // Rescale to 0, 1 + + jacobians[i][k] = 0.5*GAUSS_QUAD_WEIGHTS[k]*length; + + positions[i][k][0] = v1[0] + (v2[0] - v1[0])*g; + positions[i][k][1] = v1[1] + (v2[1] - v1[1])*g; + positions[i][k][2] = v1[2] + (v2[2] - v1[2])*g; + + directions[i][k][0] = (v2[0] - v1[0])/length; + directions[i][k][1] = (v2[1] - v1[1])/length; + directions[i][k][2] = (v2[2] - v1[2])/length; + } + } +} + + EXPORT double diff --git a/traceon/excitation.py b/traceon/excitation.py index f7c7ea7..ea912ee 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -128,12 +128,16 @@ def add_current(self, **kwargs): 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. """ - - assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes" - - for name, current in kwargs.items(): - assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode" - self.excitation_types[name] = (ExcitationType.CURRENT, current) + if self.symmetry == Symmetry.RADIAL: + for name, current in kwargs.items(): + assert name in self.mesh.physical_to_triangles.keys(), "Current should be applied to triangles in radial symmetry" + self.excitation_types[name] = (ExcitationType.CURRENT, current) + elif self.symmetry == Symmetry.THREE_D: + for name, current in kwargs.items(): + assert name in self.mesh.physical_to_lines.keys(), "Current should be applied to lines in 3D symmetry" + 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.""" diff --git a/traceon/solver.py b/traceon/solver.py index 05c8c3e..1d76026 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -247,9 +247,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Field produced by the current excitations on the coils - self.current_charges = self.get_current_charges() - self.current_field = FieldRadialBEM(current_point_charges=self.current_charges) - + self.current_field = self.get_current_field() + # TODO: optimize in backend? N = len(self.vertices) normals = np.zeros( (N, 2) if self.is_2d() else (N, 3) ) @@ -270,8 +269,16 @@ def get_active_elements(self): def get_flux_indices(self): N = self.get_number_of_matrix_elements() return np.arange(N)[self.excitation_types == int(E.ExcitationType.MAGNETIZABLE)] - - def get_current_charges(self): + + def get_current_field(self): + if self.excitation.symmetry == E.Symmetry.RADIAL: + return self.get_current_field_radial() + elif self.excitation.symmetry == E.Symmetry.THREE_D: + return self.get_current_field_three_d() + + raise ValueError('Symmetry should be one of RADIAL or THREE_D') + + def get_current_field_radial(self): currents: list[np.ndarray] = [] jacobians = [] positions = [] @@ -279,7 +286,7 @@ def get_current_charges(self): mesh = self.excitation.mesh if not len(mesh.triangles) or not self.excitation.has_current(): - return EffectivePointCharges.empty_3d() + return FieldRadialBEM(current_point_charges=EffectivePointCharges.empty_3d()) jac, pos = backend.fill_jacobian_buffer_3d(mesh.points[mesh.triangles]) @@ -300,10 +307,47 @@ def get_current_charges(self): positions.extend(pos[indices]) if not len(currents): - return EffectivePointCharges.empty_3d() + return FieldRadialBEM(current_point_charges=EffectivePointCharges.empty_3d()) - return EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions)) - + return FieldRadialBEM(current_point_charges=EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions))) + + def get_current_field_three_d(self): + currents: list[np.ndarray] = [] + jacobians = [] + positions = [] + directions = [] + + mesh = self.excitation.mesh + + if not len(mesh.lines) or not self.excitation.has_current(): + return Field3D_BEM(current_point_charges=EffectivePointCharges.empty_3d()) + + jac, pos, dir_ = backend.fill_jacobian_buffer_current_three_d(mesh.points[mesh.lines[:, :2]]) + + for n, v in self.excitation.excitation_types.items(): + if not v[0] == E.ExcitationType.CURRENT or not n in mesh.physical_to_lines: + continue + + indices = mesh.physical_to_lines[n] + + if not len(indices): + continue + + # Current supplied is total current, not a current density, therefore + # divide by the length + length = np.sum(jac[indices]) + currents.extend(np.full(len(indices), v[1])) + jacobians.extend(jac[indices]) + positions.extend(pos[indices]) + directions.extend(dir_[indices]) + + if len(currents): + eff = EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions), directions=np.array(directions)) + else: + eff = Field3D_BEM(EffectivePointCharges.empty_3d()) + + return Field3D_BEM(current_point_charges=eff) + def get_right_hand_side(self): st = time.time() N = self.get_number_of_matrix_elements() @@ -335,15 +379,17 @@ def charges_to_field(self, charges): return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_charges) class EffectivePointCharges: - def __init__(self, charges, jacobians, positions): + def __init__(self, charges, jacobians, positions, directions=None): self.charges = np.array(charges, dtype=np.float64) self.jacobians = np.array(jacobians, dtype=np.float64) self.positions = np.array(positions, dtype=np.float64) - + self.directions = directions # Current elements will have a direction + N = len(self.charges) N_QUAD = self.jacobians.shape[1] assert self.charges.shape == (N,) and self.jacobians.shape == (N, N_QUAD) assert self.positions.shape == (N, N_QUAD, 3) or self.positions.shape == (N, N_QUAD, 2) + assert self.directions is None or self.directions.shape == (len(self.charges), N_QUAD, 3) @staticmethod def empty_2d(): @@ -536,8 +582,9 @@ def magnetostatic_field_at_point(self, point): def electrostatic_field_at_point(self, point): ... - - + @abstractmethod + def current_field_at_point(self, point_): + ... class FieldBEM(Field, ABC): @@ -844,14 +891,16 @@ class Field3D_BEM(FieldBEM): """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`.""" - def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None): + 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_3d() if magnetostatic_point_charges is None: magnetostatic_point_charges = EffectivePointCharges.empty_3d() + if current_point_charges is None: + current_point_charges = EffectivePointCharges.empty_3d() - super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d()) + super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) self.symmetry = E.Symmetry.THREE_D @@ -860,6 +909,35 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges assert eff.charges.shape == (N,) assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD) assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) + + def current_field_at_point(self, point_): + point = np.array(point_) + assert point.shape == (3,), "Please supply a three dimensional point" + + if self.current_point_charges is None: + return np.zeros(3) + + # Biot savart law + r_prime = point_ - self.current_point_charges.positions + + r_prime_norm = np.linalg.norm(r_prime, axis=2) + + assert r_prime_norm.shape == self.current_point_charges.jacobians.shape + + currents = self.current_point_charges.charges # In this case actually currents + jacobians = self.current_point_charges.jacobians + cross_product = np.cross(self.current_point_charges.directions, r_prime, axis=2) + + fields = currents[:, np.newaxis, np.newaxis] * (jacobians/(4*m.pi*r_prime_norm**3))[:, :, np.newaxis] * cross_product + + assert fields.shape == (jacobians.shape[0], jacobians.shape[1], 3) + + # Sum over all but the last axis + field = np.sum(fields.reshape( (fields.shape[0]*fields.shape[1], 3) ), axis=0) + + assert field.shape == (3,) + + return field def electrostatic_field_at_point(self, point_): """ From 8f422c5541c73bec126c1238ba298472f1b835f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:17:56 +0100 Subject: [PATCH 02/85] Properly cast points to double in current_field_at_point --- traceon/solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index 1d76026..e92e7e1 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -716,7 +716,7 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) def current_field_at_point(self, point_): - point = np.array(point_) + point = np.array(point_, dtype=np.double) assert point.shape == (3,), "Please supply a three dimensional point" currents = self.current_point_charges.charges @@ -911,7 +911,7 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) def current_field_at_point(self, point_): - point = np.array(point_) + point = np.array(point_, dtype=np.double) assert point.shape == (3,), "Please supply a three dimensional point" if self.current_point_charges is None: From eaccdcedae34d4f7d771c04c317496d38a0b6c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:19:25 +0100 Subject: [PATCH 03/85] Fix 'radial' check in solver.py --- traceon/solver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index e92e7e1..d92858d 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -90,14 +90,14 @@ def __init__(self, excitation): self.excitation_types = excitation_types self.excitation_values = excitation_values - two_d = self.is_2d() + is_radial = self.excitation.symmetry == E.Symmetry.RADIAL higher_order = self.is_higher_order() - assert not higher_order or two_d, "Higher order not supported in 3D" + assert not higher_order or is_radial, "Higher order not supported in 3D" - if two_d and higher_order: + if is_radial and higher_order: jac, pos = backend.fill_jacobian_buffer_radial(vertices) - elif not two_d: + elif not is_radial: jac, pos = backend.fill_jacobian_buffer_3d(vertices) else: raise ValueError('Input excitation is 2D but not higher order, this solver input is currently not supported. Consider upgrading mesh to higher order.') From 0aeafc6792463f4160c483e6369b250df5f70966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:19:47 +0100 Subject: [PATCH 04/85] Check current loop against axial symmetric case --- tests/test_three_d.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 2abb38f..2aa6270 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -90,6 +90,17 @@ def setUpClass(cls): solver = S.MagnetostaticSolver(exc) cls.field = solver.current_field + + # Now for axial + small_circle = G.Surface.disk_xz(5., 0., 0.01) + small_circle.name = 'loop' + + import traceon.plotting as P + + mesh = small_circle.mesh(mesh_size_factor=10)._to_higher_order_mesh() + exc = E.Excitation(mesh, E.Symmetry.RADIAL) + exc.add_current(loop=cls.current) + cls.axial_field = S.MagnetostaticSolver(exc).current_field def test_field_on_axis(self): z = np.linspace(-20, 20, 25) @@ -116,6 +127,26 @@ def get_exact(x_): assert np.allclose(approx[:, 1], 0.0) assert np.allclose(correct, approx[:, 2], rtol=1e-4) + def test_field_with_radial_symmetric(self): + points = [ + [0., 0., 0.], # Bunch of arbitrary points + [1., 0., 0.], + [0., 0., 1.], + [1., 1., 1.], + [-1., 1., -1.], + [2., 2., 2.], + [2., 2., 0.], + [4., 4., 3.], + [-4., -4., -3.], + [np.sqrt(5), np.sqrt(5), 10], # Above the line element + [-np.sqrt(5), -np.sqrt(5), -10], + [100, 100, 100], # Very far away + [200, 200, -100], + ] + + for p in points: + assert np.allclose(self.field.current_field_at_point(p), self.axial_field.current_field_at_point(p), rtol=1e-4) + class TestFlatEinzelLens(unittest.TestCase): From b4ad19062ec79662eb5975e604d6c94da71254db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:20:02 +0100 Subject: [PATCH 05/85] Add TestCurrentLine to test_three_d.py --- tests/test_three_d.py | 46 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 2aa6270..11a9f21 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -1,5 +1,5 @@ import unittest -from math import pi, sqrt +from math import pi, sqrt, atan2 import os.path as path import numpy as np @@ -147,6 +147,50 @@ def test_field_with_radial_symmetric(self): for p in points: assert np.allclose(self.field.current_field_at_point(p), self.axial_field.current_field_at_point(p), rtol=1e-4) +class TestCurrentLine(unittest.TestCase): + @classmethod + def setUpClass(cls): + line = G.Path.line([-50., 0., 0.], [50., 0., 0.]) + line.name = 'line' + + mesh = line.mesh(mesh_size_factor=50) + + exc = E.Excitation(mesh, E.Symmetry.THREE_D) + cls.current = 2.5 + exc.add_current(line=cls.current) + + cls.current_field = S.MagnetostaticSolver(exc).current_field + + def test_with_ampere_law(self): + def correct(point): + r = np.linalg.norm(point[1:]) + field_strength = self.current/(2*pi*r) + angle = atan2(point[2], point[1]) + return field_strength * np.array([ 0., -np.sin(angle), np.cos(angle)]) + + sample_points = [ + [0.2, 0.2, -0.2], + [0.2, 0.2, 0.2], + [0.2, -0.2, -0.2], + [0.2, -0.2, 0.2], + [-0.2, 0.2, -0.2], + [-0.2, 0.2, 0.2], + [-0.2, -0.2, -0.2], + [-0.2, -0.2, 0.2], + + [0.4, 0.6, 0.], + [0.4, 0.6, 0.2] + ] + + for p in sample_points: + print('-'*10) + print(correct(p)) + print(self.current_field.current_field_at_point(p)) + assert np.allclose(correct(p), self.current_field.current_field_at_point(p), rtol=1e-4) + + + + class TestFlatEinzelLens(unittest.TestCase): From 86f416c656bed300bdc01c661d31de4b6aa8133a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:55:30 +0100 Subject: [PATCH 06/85] Remove print statements in test_three_d.py --- tests/test_three_d.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 11a9f21..8e259bd 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -183,9 +183,6 @@ def correct(point): ] for p in sample_points: - print('-'*10) - print(correct(p)) - print(self.current_field.current_field_at_point(p)) assert np.allclose(correct(p), self.current_field.current_field_at_point(p), rtol=1e-4) From ec0cc8beed37e58e034848b22d8f514e722d5b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 10:56:38 +0100 Subject: [PATCH 07/85] Implement current_field_at_point_3d in backend --- traceon/backend/__init__.py | 35 ++++++++++++++++++++++++++++++++++ traceon/backend/three_d.c | 38 +++++++++++++++++++++++++++++++++++++ traceon/solver.py | 28 ++------------------------- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index d818bf9..bddcfd7 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -130,6 +130,31 @@ def __init__(self, eff, *args, **kwargs): self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) self.N = len(eff) +class EffectivePointCurrents3D(C.Structure): + _fields_ = [ + ("currents", dbl_p), + ("jacobians", dbl_p), + ("positions", dbl_p), + ("directions", dbl_p), + ("N", C.c_size_t) + ] + + def __init__(self, currents, jacobians, positions, directions, *args, **kwargs): + super(EffectivePointCurrents3D, self).__init__(*args, **kwargs) + + N = len(currents) + assert currents.shape == (N,) and currents.dtype == np.double + assert jacobians.shape == (N, N_QUAD_2D) and jacobians.dtype == np.double + assert positions.shape == (N, N_QUAD_2D, 3) and positions.dtype == np.double + assert directions.shape == (N, N_QUAD_2D, 3) and directions.dtype == np.double + + self.currents = ensure_contiguous_aligned(currents).ctypes.data_as(dbl_p) + self.jacobians = ensure_contiguous_aligned(jacobians).ctypes.data_as(dbl_p) + self.positions = ensure_contiguous_aligned(positions).ctypes.data_as(dbl_p) + self.directions = ensure_contiguous_aligned(directions).ctypes.data_as(dbl_p) + self.N = N + + bounds = arr(shape=(3, 2)) times_block = arr(shape=(TRACING_BLOCK_SIZE,)) @@ -172,6 +197,7 @@ def __init__(self, eff, *args, **kwargs): 'potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'axial_coefficients_3d': (None, charges_3d, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), arr(ndim=3), sz, z_values, arr(ndim=4), sz), 'fill_jacobian_buffer_current_three_d': (None, lines, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), sz), + 'current_field_at_point_3d': (None, v3, EffectivePointCurrents3D, v3), 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), @@ -468,6 +494,15 @@ def fill_jacobian_buffer_current_three_d(lines): return jacobians, positions, directions +def current_field_at_point_3d(point, currents, jacobians, positions, directions): + assert point.shape == (3,) + + eff = EffectivePointCurrents3D(currents, jacobians, positions, directions) + + result = np.zeros(3) + backend_lib.current_field_at_point_3d(point, eff, result) + return result + def potential_3d(point, charges, jac_buffer, pos_buffer): assert point.shape == (3,) N = len(charges) diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index e42fbce..c789878 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -22,6 +22,16 @@ struct effective_point_charges_3d { size_t N; }; +struct effective_point_currents_3d { + double *currents; + double (*jacobians)[N_QUAD_2D]; + double (*positions)[N_QUAD_2D][3]; + double (*directions)[N_QUAD_2D][3]; + size_t N; +}; + + + struct field_derivs_args { double *z_interpolation; double *electrostatic_axial_coeffs; @@ -59,7 +69,35 @@ fill_jacobian_buffer_current_three_d( } } +EXPORT void +current_field_at_point_3d(double point[3], struct effective_point_currents_3d epc, double field_out[3]) { + // Use Biot-Savart law + + double field_sum[3] = {0.0, 0.0, 0.0}; + + for (size_t i = 0; i < epc.N; i++) + for (int k = 0; k < N_QUAD_2D; k++) { + double r_prime[3]; + r_prime[0] = point[0] - epc.positions[i][k][0]; + r_prime[1] = point[1] - epc.positions[i][k][1]; + r_prime[2] = point[2] - epc.positions[i][k][2]; + double r_norm = norm_3d(r_prime[0], r_prime[1], r_prime[2]); + + double cross[3]; + cross_product_3d(epc.directions[i][k], r_prime, cross); + + double factor = (epc.currents[i] * epc.jacobians[i][k]) / (4.0 * M_PI * (r_norm * r_norm * r_norm)); + + field_sum[0] += factor * cross[0]; + field_sum[1] += factor * cross[1]; + field_sum[2] += factor * cross[2]; + } + + field_out[0] = field_sum[0]; + field_out[1] = field_sum[1]; + field_out[2] = field_sum[2]; +} EXPORT double diff --git a/traceon/solver.py b/traceon/solver.py index d92858d..8b45ce3 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -912,33 +912,9 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_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" - - if self.current_point_charges is None: - return np.zeros(3) - - # Biot savart law - r_prime = point_ - self.current_point_charges.positions - - r_prime_norm = np.linalg.norm(r_prime, axis=2) - - assert r_prime_norm.shape == self.current_point_charges.jacobians.shape - - currents = self.current_point_charges.charges # In this case actually currents - jacobians = self.current_point_charges.jacobians - cross_product = np.cross(self.current_point_charges.directions, r_prime, axis=2) - - fields = currents[:, np.newaxis, np.newaxis] * (jacobians/(4*m.pi*r_prime_norm**3))[:, :, np.newaxis] * cross_product - - assert fields.shape == (jacobians.shape[0], jacobians.shape[1], 3) - - # Sum over all but the last axis - field = np.sum(fields.reshape( (fields.shape[0]*fields.shape[1], 3) ), axis=0) + eff = self.current_point_charges + return backend.current_field_at_point_3d(point, eff.charges, eff.jacobians, eff.positions, eff.directions) - assert field.shape == (3,) - - return field - def electrostatic_field_at_point(self, point_): """ Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\) From 3404ddc84b4344f37adf0e68740b4a9ab417c0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 8 Dec 2024 11:01:34 +0100 Subject: [PATCH 08/85] Move abstract method current_field_at_point to FieldBEM --- traceon/solver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index 8b45ce3..b8dc564 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -582,9 +582,7 @@ def magnetostatic_field_at_point(self, point): def electrostatic_field_at_point(self, point): ... - @abstractmethod - def current_field_at_point(self, point_): - ... + class FieldBEM(Field, ABC): @@ -698,6 +696,10 @@ def __str__(self): 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_): + ... class FieldRadialBEM(FieldBEM): From 64ac833f4c58c1b7c7274eee7c6fb2c1d7e7cce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 11 Dec 2024 22:04:47 +0100 Subject: [PATCH 09/85] Fix undefined current_charges in MagnetostaticSolver --- traceon/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traceon/solver.py b/traceon/solver.py index b8dc564..d66c3b6 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -376,7 +376,7 @@ def charges_to_field(self, charges): if self.is_3d(): return Field3D_BEM(magnetostatic_point_charges=charges) else: - return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_charges) + return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) class EffectivePointCharges: def __init__(self, charges, jacobians, positions, directions=None): From 45b314b3e90c0f32f159de4c067297c7c40f1e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 11 Dec 2024 22:05:07 +0100 Subject: [PATCH 10/85] Fix get_right_hand_side in presence of 3D current lines --- traceon/solver.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index d66c3b6..2f2c12f 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -364,8 +364,12 @@ def get_right_hand_side(self): # and the normal vector of the vertex. center = self.get_center_of_element(i) field_at_center = self.current_field.current_field_at_point(center) - #flux_to_charge_factor = (value - 1)/np.pi - field_dotted = field_at_center[0] * self.normals[i, 0] + field_at_center[2]*self.normals[i, 1] + + n = self.normals[i] + if n.shape == (2,): + n = [n[0], 0., n[1]] + + field_dotted = np.dot(field_at_center, n) F[i] = -backend.flux_density_to_charge_factor(value) * field_dotted assert np.all(np.isfinite(F)) From ba276ed1d12db41257c39f2181bee6d505477d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 11 Dec 2024 22:05:52 +0100 Subject: [PATCH 11/85] Add test_simple_current_line_against_radial --- tests/test_three_d.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 8e259bd..dbdc182 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -72,6 +72,42 @@ def test_dohi_meshing(self): geom = geom.revolve_z() geom.mesh(mesh_size_factor=4) + def test_simple_current_line_against_radial(self): + rect = G.Path.rectangle_xz(1, 2, -1, 1) + rect.name = 'rect' + + coil = G.Surface.disk_xz(3, 0, 0.05) + coil.name = 'coil' + + mesh_size = 0.0125 + mesh = rect.mesh(mesh_size=mesh_size) + coil.mesh(mesh_size=mesh_size) + + e = E.Excitation(mesh, E.Symmetry.RADIAL) + e.add_current(coil=2.5) + e.add_magnetizable(rect=10) + + field = S.solve_direct(e) + + z = np.linspace(-10, 10, 50) + pot_radial = [field.potential_at_point([0., 0., z_]) for z_ in z] + + rect = rect.revolve_z() + coil = G.Path.circle_xy(0., 0., 3) + coil.name = 'coil' + + mesh_size = 0.70 + mesh = rect.mesh(mesh_size=mesh_size) + coil.mesh(mesh_size=0.2) + + e = E.Excitation(mesh, E.Symmetry.THREE_D) + e.add_current(coil=2.5) + e.add_magnetizable(rect=10) + + field_three_d = S.solve_direct(e) + pot_three_d = [field_three_d.potential_at_point([0., 0., z_]) for z_ in z] + + # Accuracy is better if mesh size is increased, but + # makes tests too slow + assert np.allclose(pot_three_d, pot_radial, atol=0.020) class TestCurrentLoop(unittest.TestCase): From 399e9cb1bf845a2af10debb77498bfe44a18ce92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 11 Dec 2024 22:12:24 +0100 Subject: [PATCH 12/85] Add test_magnetostatic_potential_against_radial_symmetric --- tests/test_three_d.py | 32 ++++++++++++++++++++++++++++++++ traceon/solver.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index dbdc182..4d66bc7 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -8,6 +8,7 @@ from scipy.interpolate import CubicSpline import traceon.geometry as G +import traceon.plotting as P import traceon.solver as S import traceon.excitation as E import traceon.tracing as T @@ -108,6 +109,37 @@ def test_simple_current_line_against_radial(self): # Accuracy is better if mesh size is increased, but # makes tests too slow assert np.allclose(pot_three_d, pot_radial, atol=0.020) + + def test_magnetostatic_potential_against_radial_symmetric(self): + rect = G.Path.rectangle_xz(1, 2, -1, 1) + rect.name = 'rect' + + mesh_size = 0.02 + mesh = rect.mesh(mesh_size=mesh_size) + + e = E.Excitation(mesh, E.Symmetry.RADIAL) + e.add_magnetostatic_potential(rect=10) + + field = S.solve_direct(e) + + z = np.linspace(-10, 10, 50) + pot_radial = [field.potential_at_point([0.25, 0., z_]) for z_ in z] + + rect = rect.revolve_z() + + mesh_size = 0.70 + mesh = rect.mesh(mesh_size=mesh_size) + + e = E.Excitation(mesh, E.Symmetry.THREE_D) + e.add_magnetostatic_potential(rect=10) + + field_three_d = S.solve_direct(e) + pot_three_d = [field_three_d.potential_at_point([0.25, 0., z_]) for z_ in z] + + # Accuracy is better if mesh size is increased, but + # makes tests too slow + assert np.allclose(pot_three_d, pot_radial, rtol=8e-3) + class TestCurrentLoop(unittest.TestCase): diff --git a/traceon/solver.py b/traceon/solver.py index 2f2c12f..1167599 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -344,7 +344,7 @@ def get_current_field_three_d(self): if len(currents): eff = EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions), directions=np.array(directions)) else: - eff = Field3D_BEM(EffectivePointCharges.empty_3d()) + eff = EffectivePointCharges.empty_3d() return Field3D_BEM(current_point_charges=eff) From 3aa116ccbddabd72b9a6d85bcffafdcb752883ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Thu, 12 Dec 2024 21:19:30 +0100 Subject: [PATCH 13/85] Remove stray import of traceon.plotting in test_three_d.py --- tests/test_three_d.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 4d66bc7..89c9f82 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -163,8 +163,6 @@ def setUpClass(cls): small_circle = G.Surface.disk_xz(5., 0., 0.01) small_circle.name = 'loop' - import traceon.plotting as P - mesh = small_circle.mesh(mesh_size_factor=10)._to_higher_order_mesh() exc = E.Excitation(mesh, E.Symmetry.RADIAL) exc.add_current(loop=cls.current) @@ -215,6 +213,7 @@ def test_field_with_radial_symmetric(self): for p in points: assert np.allclose(self.field.current_field_at_point(p), self.axial_field.current_field_at_point(p), rtol=1e-4) + class TestCurrentLine(unittest.TestCase): @classmethod def setUpClass(cls): From 1b78433e0c57873040cc697c5ef329f2c12d98cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Thu, 12 Dec 2024 23:44:22 +0100 Subject: [PATCH 14/85] Implement empty_line_3d and use in Field3D_BEM --- traceon/solver.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index 1167599..6d36f9d 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -378,7 +378,7 @@ def get_right_hand_side(self): def charges_to_field(self, charges): if self.is_3d(): - return Field3D_BEM(magnetostatic_point_charges=charges) + return Field3D_BEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) else: return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) @@ -404,6 +404,11 @@ def empty_2d(): def empty_3d(): N_TRIANGLE_QUAD = backend.N_TRIANGLE_QUAD return EffectivePointCharges(np.empty((0,)), np.empty((0, N_TRIANGLE_QUAD)), np.empty((0, N_TRIANGLE_QUAD, 3))) + + @staticmethod + def empty_line_3d(): + N_QUAD_2D = backend.N_QUAD_2D + return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0, N_QUAD_2D, 3)), np.empty((0, N_QUAD_2D, 3))) def is_2d(self): return self.jacobians.shape[1] == backend.N_QUAD_2D @@ -904,7 +909,7 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges if magnetostatic_point_charges is None: magnetostatic_point_charges = EffectivePointCharges.empty_3d() if current_point_charges is None: - current_point_charges = EffectivePointCharges.empty_3d() + current_point_charges = EffectivePointCharges.empty_line_3d() super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) From 0375e4bed058fdec96cc3ec4e62000fe28331506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Thu, 12 Dec 2024 23:44:44 +0100 Subject: [PATCH 15/85] Compute current_field in Field3D_BEM.magnetostatic_field_at_point --- traceon/solver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/traceon/solver.py b/traceon/solver.py index 6d36f9d..a80adf5 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -981,10 +981,15 @@ def magnetostatic_field_at_point(self, point_): """ 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 - return backend.field_3d(point, charges, jacobians, positions) + + mag_field = backend.field_3d(point, charges, jacobians, positions) + + return current_field + mag_field def magnetostatic_potential_at_point(self, point_): """ From 71f049f7805d4a70fe8d78187a07526e667d7172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Thu, 12 Dec 2024 23:45:06 +0100 Subject: [PATCH 16/85] Add test_magnetostatic_line_sphere --- tests/test_three_d.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_three_d.py b/tests/test_three_d.py index 89c9f82..ffee9cf 100644 --- a/tests/test_three_d.py +++ b/tests/test_three_d.py @@ -140,6 +140,31 @@ def test_magnetostatic_potential_against_radial_symmetric(self): # makes tests too slow assert np.allclose(pot_three_d, pot_radial, rtol=8e-3) + def test_magnetostatic_line_sphere(self): + # Benchmark against FEM package + sphere = G.Surface.sphere(0.1).move(dx=0.2, dy=0.2) + sphere.name = 'sphere' + + line = G.Path.line([-0.2, -0.2, 0.], [-0.2, 0.2, 0.]) + line.name = 'line' + + ms = 0.020 + mesh = sphere.mesh(mesh_size=ms) + line.mesh(mesh_size_factor=5) + + e = E.Excitation(mesh, E.Symmetry.THREE_D) + e.add_current(line=-2.0) + e.add_magnetizable(sphere=10.) + + field = S.solve_direct(e) + + f1 = field.field_at_point([0.2, 0.2, -0.125]) + f2 = field.field_at_point([0.2, 0.2, 0.2]) + + assert np.isclose(f1[0], 0.05156, rtol=3e-2) # FEM fails to resolve y component + assert np.isclose(f1[2], 0.44525, rtol=1e-2) + assert np.isclose(f2[0], -0.10423, rtol=2e-2) + assert np.isclose(f2[2], 0.25941, rtol=1e-2) + class TestCurrentLoop(unittest.TestCase): From 5df9dc41c1c66c5246c787d39b5453467681a965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 14 Dec 2024 16:43:35 +0100 Subject: [PATCH 17/85] Take into account current fields when tracing in 3D --- tests/test_tracing.py | 63 ++++++++++++++++++++++++++++++++++++- traceon/backend/__init__.py | 29 ++++++++++------- traceon/backend/radial.c | 2 +- traceon/backend/tracing.c | 23 +++++++++++--- traceon/solver.py | 2 +- traceon/tracing.py | 3 +- 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index c15d72c..7a959ec 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -1,11 +1,14 @@ import unittest from math import sqrt +import matplotlib.pyplot as plt import numpy as np from scipy.integrate import solve_ivp from scipy.constants import m_e, e, mu_0, epsilon_0 from scipy.interpolate import CubicSpline +import traceon.geometry as G +import traceon.excitation as E import traceon.backend as B import traceon.solver as S import traceon.tracing as T @@ -115,7 +118,65 @@ def lorentz_force(_, y): interp = CubicSpline(positions[::-1, 2], np.array([positions[::-1, 0], positions[::-1, 1]]).T) assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T, atol=1e-4, rtol=5e-5) - + + def test_magnetic_deflection_radial_three_d(self): + # Radial symmetric + circle1 = G.Surface.disk_xz(1.0, 3.0, 0.1) + circle1.name = 'circle1' + + circle2 = G.Surface.disk_xz(1.0, -3.0, 0.1) + circle2.name = 'circle2' + + mesh_radial = (circle1+circle2).mesh(mesh_size_factor=2) + + exc = E.Excitation(mesh_radial, E.Symmetry.RADIAL) + exc.add_current(circle1=2.0, circle2=2.0) + + field_radial = S.solve_direct(exc) + + # Three D + + circle1 = G.Path.circle_xy(0., 0., 1.0).move(dz=3.0) + circle1.name = 'circle1' + + circle2 = G.Path.circle_xy(0., 0., 1.0).move(dz=-3.0) + circle2.name = 'circle2' + + mesh_three_d = (circle1+circle2).mesh(mesh_size_factor=15) + + exc = E.Excitation(mesh_three_d, E.Symmetry.THREE_D) + exc.add_current(circle1=2.0, circle2=2.0) + + field_three_d = S.solve_direct(exc) + + x = np.linspace(-30, 30, 10) + H_radial = [field_radial.current_field_at_point([x_, 0., 0.])[2] for x_ in x] + H_three_d = [field_three_d.current_field_at_point([x_, 0., 0.])[2] for x_ in x] + + assert np.allclose(H_radial, H_three_d, rtol=1.5e-2) + + # Trace particles and compare + + bounds = [(-20, 2.5), (-10, 10), (-10, 10)] + tracer_radial = field_radial.get_tracer(bounds) + tracer_three_d = field_three_d.get_tracer(bounds) + + v0 = [100., 0., 0.] + x0s = [-10] + + positions_radial = [tracer_radial([x0, 0., 0.], v0, atol=1e-5)[1] for x0 in x0s][0] + positions_three_d = [tracer_three_d([x0, 0., 0.], v0, atol=1e-5)[1] for x0 in x0s][0] + + assert np.allclose(positions_radial[:, 2], 0.0) + assert np.allclose(positions_three_d[:, 2], 0.0) + + spline_radial = CubicSpline(positions_radial[:, 0], positions_radial[:, 1]) + spline_three_d = CubicSpline(positions_three_d[:, 0], positions_three_d[:, 1]) + + x = np.linspace(-1.5, 5.0, 10) + + assert np.allclose(spline_radial(x), spline_three_d(x), atol=0.0005) + def test_plane_intersection(self): p = np.array([ [3, 0, 0, 0, 0, 0], diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 56f2ca6..6081841 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -139,19 +139,23 @@ class EffectivePointCurrents3D(C.Structure): ("N", C.c_size_t) ] - def __init__(self, currents, jacobians, positions, directions, *args, **kwargs): + def __init__(self, eff, *args, **kwargs): super(EffectivePointCurrents3D, self).__init__(*args, **kwargs) + # In solver.py we use consistently the EffectivePointCharges class + # so when storing effective point currents, the charges are actually currents + currents = eff.charges + N = len(currents) assert currents.shape == (N,) and currents.dtype == np.double - assert jacobians.shape == (N, N_QUAD_2D) and jacobians.dtype == np.double - assert positions.shape == (N, N_QUAD_2D, 3) and positions.dtype == np.double - assert directions.shape == (N, N_QUAD_2D, 3) and directions.dtype == np.double + assert eff.jacobians.shape == (N, N_QUAD_2D) and eff.jacobians.dtype == np.double + assert eff.positions.shape == (N, N_QUAD_2D, 3) and eff.positions.dtype == np.double + assert eff.directions.shape == (N, N_QUAD_2D, 3) and eff.directions.dtype == np.double self.currents = ensure_contiguous_aligned(currents).ctypes.data_as(dbl_p) - self.jacobians = ensure_contiguous_aligned(jacobians).ctypes.data_as(dbl_p) - self.positions = ensure_contiguous_aligned(positions).ctypes.data_as(dbl_p) - self.directions = ensure_contiguous_aligned(directions).ctypes.data_as(dbl_p) + self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) + self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) + self.directions = ensure_contiguous_aligned(eff.directions).ctypes.data_as(dbl_p) self.N = N @@ -201,7 +205,7 @@ def __init__(self, currents, jacobians, positions, directions, *args, **kwargs): 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), - 'trace_particle_3d': (sz, times_block, tracing_block, dbl, bounds, dbl, EffectivePointCharges3D, EffectivePointCharges3D, dbl_p), + 'trace_particle_3d': (sz, times_block, tracing_block, dbl, bounds, dbl, EffectivePointCharges3D, EffectivePointCharges3D, EffectivePointCurrents3D, dbl_p), 'field_3d_derivs': (None, v3, v3, z_values, arr(ndim=5), sz), 'trace_particle_3d_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, arr(ndim=5), arr(ndim=5), sz), 'current_potential_axial_radial_ring': (dbl, dbl, dbl, dbl), @@ -390,7 +394,7 @@ def trace_particle_radial_derivs(position, velocity, charge_over_mass, bounds, a return times, positions -def trace_particle_3d(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, field_bounds=None): +def trace_particle_3d(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_currents, field_bounds=None): assert field_bounds is None or field_bounds.shape == (3,2) bounds = np.array(bounds) @@ -399,9 +403,10 @@ def trace_particle_3d(position, velocity, charge_over_mass, bounds, atol, eff_el eff_elec = EffectivePointCharges3D(eff_elec) eff_mag = EffectivePointCharges3D(eff_mag) + eff_currents = EffectivePointCurrents3D(eff_currents) return trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_3d(T, P, charge_over_mass, bounds, atol, eff_elec, eff_mag, field_bounds)) + lambda T, P: backend_lib.trace_particle_3d(T, P, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_currents, field_bounds)) def trace_particle_3d_derivs(position, velocity, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs): assert electrostatic_coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) @@ -494,10 +499,10 @@ def fill_jacobian_buffer_current_three_d(lines): return jacobians, positions, directions -def current_field_at_point_3d(point, currents, jacobians, positions, directions): +def current_field_at_point_3d(point, eff): assert point.shape == (3,) - eff = EffectivePointCurrents3D(currents, jacobians, positions, directions) + eff = EffectivePointCurrents3D(eff) result = np.zeros(3) backend_lib.current_field_at_point_3d(point, eff, result) diff --git a/traceon/backend/radial.c b/traceon/backend/radial.c index e5e5fa2..ab8a9ed 100644 --- a/traceon/backend/radial.c +++ b/traceon/backend/radial.c @@ -175,7 +175,7 @@ field_radial(double point[3], double result[3], double* charges, jacobian_buffer struct field_evaluation_args { void *elec_charges; void *mag_charges; - void *current_charges; + void *currents; double *bounds; }; diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index ded1b47..410a2ee 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -117,7 +117,7 @@ field_radial_traceable(double point[6], double result[3], void *args_p) { struct effective_point_charges_2d *elec_charges = (struct effective_point_charges_2d*) args->elec_charges; struct effective_point_charges_2d *mag_charges = (struct effective_point_charges_2d*) args->mag_charges; - struct effective_point_charges_3d *current_charges = (struct effective_point_charges_3d*) args->current_charges; + struct effective_point_charges_3d *current_charges = (struct effective_point_charges_3d*) args->currents; double (*bounds)[2] = (double (*)[2]) args->bounds; @@ -157,7 +157,7 @@ trace_particle_radial(double *times_array, double *pos_array, double charge_over struct field_evaluation_args args = { .elec_charges = (void*) &eff_elec, .mag_charges = (void*) &eff_mag, - .current_charges = (void*) &eff_current, + .currents = (void*) &eff_current, .bounds = field_bounds }; @@ -192,6 +192,7 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; struct effective_point_charges_3d *mag_charges = (struct effective_point_charges_3d*) args->mag_charges; + struct effective_point_currents_3d *currents = (struct effective_point_currents_3d*) args->currents; double (*bounds)[2] = (double (*)[2]) args->bounds; @@ -205,6 +206,8 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { field_3d(point, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); + current_field_at_point_3d(point, *currents, curr_field); + field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); } else { @@ -215,10 +218,20 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { } EXPORT size_t -trace_particle_3d(double *times_array, double *pos_array, double charge_over_mass, double tracer_bounds[3][2], double atol, - struct effective_point_charges_3d eff_elec, struct effective_point_charges_3d eff_mag, double *field_bounds) { +trace_particle_3d(double *times_array, double *pos_array, double charge_over_mass, + double tracer_bounds[3][2], + double atol, + struct effective_point_charges_3d eff_elec, + struct effective_point_charges_3d eff_mag, + struct effective_point_currents_3d eff_currents, + double *field_bounds) { - struct field_evaluation_args args = {.elec_charges = (void*) &eff_elec, .mag_charges = (void*) &eff_mag, .bounds = field_bounds}; + struct field_evaluation_args args = { + .elec_charges = (void*) &eff_elec, + .mag_charges = (void*) &eff_mag, + .currents = (void*) &eff_currents, + .bounds = field_bounds + }; return trace_particle(times_array, pos_array, charge_over_mass, field_3d_traceable, tracer_bounds, atol, (void*) &args); } diff --git a/traceon/solver.py b/traceon/solver.py index a80adf5..d949cdd 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -924,7 +924,7 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges def current_field_at_point(self, point_): point = np.array(point_, dtype=np.double) eff = self.current_point_charges - return backend.current_field_at_point_3d(point, eff.charges, eff.jacobians, eff.positions, eff.directions) + return backend.current_field_at_point_3d(point, eff) def electrostatic_field_at_point(self, point_): """ diff --git a/traceon/tracing.py b/traceon/tracing.py index 465b280..d62dfaa 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -180,7 +180,8 @@ def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): charge_over_mass = charge / mass velocity = _convert_velocity_to_SI(velocity, mass) elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges - return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, field_bounds=self.field.field_bounds) + currents = self.field.current_point_charges + return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, currents, field_bounds=self.field.field_bounds) class Tracer3DAxial(Tracer): def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): From 366a4d9d246b2a1c1287a5337aeee9613879cb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 14 Dec 2024 17:52:08 +0100 Subject: [PATCH 18/85] Rename current_field to current_field_radial --- tests/test_radial.py | 2 +- traceon/backend/__init__.py | 6 +++--- traceon/backend/radial.c | 2 +- traceon/backend/tracing.c | 2 +- traceon/solver.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_radial.py b/tests/test_radial.py index df55e48..05450b6 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -118,7 +118,7 @@ def test_current_field_ring(self): eff = get_ring_effective_point_charges(current, 1) for p in np.random.rand(10, 3): - field = mu_0 * B.current_field(p, eff.charges, eff.jacobians, eff.positions) + field = mu_0 * B.current_field_radial(p, eff.charges, eff.jacobians, eff.positions) correct = biot_savart_loop(current, p) assert np.allclose(field, correct) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 6081841..d14a05a 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -211,7 +211,7 @@ def __init__(self, eff, *args, **kwargs): 'current_potential_axial_radial_ring': (dbl, dbl, dbl, dbl), 'current_potential_axial': (dbl, dbl, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), 'current_field_radial_ring': (None, dbl, dbl, dbl, dbl, v2), - 'current_field': (None, v3, v3, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), + 'current_field_radial': (None, v3, v3, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), 'current_axial_derivatives_radial': (None, arr(ndim=2), currents_2d, jac_buffer_3d, pos_buffer_3d, sz, z_values, sz), 'fill_jacobian_buffer_radial': (None, jac_buffer_2d, pos_buffer_2d, vertices, sz), 'self_potential_radial': (dbl, dbl, vp), @@ -557,7 +557,7 @@ def current_field_radial_ring(x0, y0, x, y): backend_lib.current_field_radial_ring(x0, y0, x, y, res) return res -def current_field(p0, currents, jac_buffer, pos_buffer): +def current_field_radial(p0, currents, jac_buffer, pos_buffer): assert p0.shape == (3,) N = len(currents) assert currents.shape == (N,) @@ -567,7 +567,7 @@ def current_field(p0, currents, jac_buffer, pos_buffer): assert np.all(pos_buffer[:, :, 1] == 0.) result = np.zeros( (3,) ) - backend_lib.current_field(p0, result, currents, jac_buffer, pos_buffer, N) + backend_lib.current_field_radial(p0, result, currents, jac_buffer, pos_buffer, N) return result def current_axial_derivatives_radial(z, currents, jac_buffer, pos_buffer): diff --git a/traceon/backend/radial.c b/traceon/backend/radial.c index ab8a9ed..e7bc190 100644 --- a/traceon/backend/radial.c +++ b/traceon/backend/radial.c @@ -53,7 +53,7 @@ current_potential_axial(double z0, double *currents, } EXPORT void -current_field(double point[3], double result[3], double *currents, +current_field_radial(double point[3], double result[3], double *currents, jacobian_buffer_3d jacobian_buffer, position_buffer_3d position_buffer, size_t N_vertices) { double Br = 0., Bz = 0.; diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 410a2ee..2727768 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -134,7 +134,7 @@ field_radial_traceable(double point[6], double result[3], void *args_p) { field_radial(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - current_field(point, curr_field, + current_field_radial(point, curr_field, current_charges->charges, current_charges->jacobians, current_charges->positions, current_charges->N); combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); diff --git a/traceon/solver.py b/traceon/solver.py index d949cdd..0279b28 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -733,7 +733,7 @@ def current_field_at_point(self, point_): currents = self.current_point_charges.charges jacobians = self.current_point_charges.jacobians positions = self.current_point_charges.positions - return backend.current_field(point, currents, jacobians, positions) + return backend.current_field_radial(point, currents, jacobians, positions) def electrostatic_field_at_point(self, point_): """ From 5aaeaedec21ea1b78fb97f06d1aa8160c99855b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 14 Dec 2024 17:53:56 +0100 Subject: [PATCH 19/85] Rename current_field_at_point_3d to current_field_3d --- traceon/backend/__init__.py | 6 +++--- traceon/backend/three_d.c | 2 +- traceon/backend/tracing.c | 2 +- traceon/solver.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index d14a05a..3befda7 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -201,7 +201,7 @@ def __init__(self, eff, *args, **kwargs): 'potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'axial_coefficients_3d': (None, charges_3d, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), arr(ndim=3), sz, z_values, arr(ndim=4), sz), 'fill_jacobian_buffer_current_three_d': (None, lines, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), sz), - 'current_field_at_point_3d': (None, v3, EffectivePointCurrents3D, v3), + 'current_field_3d': (None, v3, EffectivePointCurrents3D, v3), 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), @@ -499,13 +499,13 @@ def fill_jacobian_buffer_current_three_d(lines): return jacobians, positions, directions -def current_field_at_point_3d(point, eff): +def current_field_3d(point, eff): assert point.shape == (3,) eff = EffectivePointCurrents3D(eff) result = np.zeros(3) - backend_lib.current_field_at_point_3d(point, eff, result) + backend_lib.current_field_3d(point, eff, result) return result def potential_3d(point, charges, jac_buffer, pos_buffer): diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index a75259e..761afef 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -70,7 +70,7 @@ fill_jacobian_buffer_current_three_d( } EXPORT void -current_field_at_point_3d(double point[3], struct effective_point_currents_3d epc, double field_out[3]) { +current_field_3d(double point[3], struct effective_point_currents_3d epc, double field_out[3]) { // Use Biot-Savart law double field_sum[3] = {0.0, 0.0, 0.0}; diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 2727768..656be7f 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -206,7 +206,7 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { field_3d(point, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - current_field_at_point_3d(point, *currents, curr_field); + current_field_3d(point, *currents, curr_field); field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); } diff --git a/traceon/solver.py b/traceon/solver.py index 0279b28..60afcdf 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -924,7 +924,7 @@ def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges def current_field_at_point(self, point_): point = np.array(point_, dtype=np.double) eff = self.current_point_charges - return backend.current_field_at_point_3d(point, eff) + return backend.current_field_3d(point, eff) def electrostatic_field_at_point(self, point_): """ From bb065f0045b82ba3dc1db3f9626ed00271cfb1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 14 Dec 2024 18:04:08 +0100 Subject: [PATCH 20/85] Update version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 97490af..6de96fc 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name='traceon', - version='0.7.2', + version='0.8.0rc1', description='Solver and tracer for electrostatic problems', url='https://github.com/leon-vv/Traceon', author='Léon van Velzen', From 3d2026599dee15f8265d363cd09548207bb63bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 10:34:45 +0100 Subject: [PATCH 21/85] Add type signatures to traceon/backend/__init__.py --- traceon/backend/__init__.py | 162 +++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 66 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index bbb91c2..3468668 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -5,6 +5,7 @@ import ctypes as C import os.path as path import platform +from typing import Callable, Optional, Tuple, List from numpy.ctypeslib import ndpointer from numpy.typing import DTypeLike @@ -227,40 +228,36 @@ def backend_check_numpy_requirements_wrapper(*args, _cfun_reference=libfun, _cfu ellipem1 = np.vectorize(backend_lib.ellipem1) ellipe = np.vectorize(backend_lib.ellipe) -def kronrod_adaptive(fun, a, b, epsabs=1.49e-08, epsrel=1.49e-08): +def kronrod_adaptive(fun: Callable[[float], float], a: float, b: float, + epsabs: float=1.49e-08, epsrel: float=1.49e-08) -> float: callback = integration_cb_1d(lambda x, _: fun(x)) return backend_lib.kronrod_adaptive(callback, a, b, vp(None), epsabs, epsrel) -def higher_order_normal_radial(alpha, vertices): +def higher_order_normal_radial(alpha: float, vertices: np.ndarray) -> np.ndarray: normal = np.zeros(2) backend_lib.higher_order_normal_radial(alpha, vertices[0, :2], vertices[2, :2], vertices[3, :2], vertices[1, :2], normal) assert np.isclose(np.linalg.norm(normal), 1.0) return normal -def normal_2d(p1, p2): +def normal_2d(p1: np.ndarray, p2: np.ndarray) -> np.ndarray: normal = np.zeros( (2,) ) backend_lib.normal_2d(p1, p2, normal) return normal -# Remove the last argument, which is usually a void pointer to optional data -# passed to the function. In Python we don't need this functionality -# as we can simply use closures. -def remove_arg(fun): - return lambda *args: fun(*args[:-1]) - def normal_3d(tri: np.ndarray) -> np.ndarray: normal = np.zeros( (3,) ) backend_lib.normal_3d(tri, normal) return normal - -def trace_particle_wrapper(position_, velocity_, fill_positions_fun): + +def trace_particle_wrapper(position_: np.ndarray, velocity_: np.ndarray, + fill_positions_fun: Callable[[np.ndarray, np.ndarray], int]) -> Tuple[np.ndarray, np.ndarray]: position = np.array(position_) velocity = np.array(velocity_) assert position.shape == (3,) and velocity.shape == (3,) N = TRACING_BLOCK_SIZE - pos_blocks: list[np.ndarray] = [] - times_blocks: list[np.ndarray] = [] + pos_blocks: List[np.ndarray] = [] + times_blocks: List[np.ndarray] = [] times = np.zeros(TRACING_BLOCK_SIZE) positions = np.zeros( (TRACING_BLOCK_SIZE, 6) ) @@ -291,8 +288,7 @@ def trace_particle_wrapper(position_, velocity_, fill_positions_fun): else: return np.concatenate(times_blocks), np.concatenate(pos_blocks) -def wrap_field_fun(ff): - +def wrap_field_fun(ff: Callable) -> Callable: def wrapper(y, result, _): field = ff(y[0], y[1], y[2], y[3], y[4], y[5]) assert field.shape == (3,) @@ -302,7 +298,7 @@ def wrapper(y, result, _): return field_fun(wrapper) -def position_and_jacobian_3d(alpha, beta, triangle): +def position_and_jacobian_3d(alpha: float, beta: float, triangle: np.ndarray) -> Tuple[float, np.ndarray]: assert triangle.shape == (3, 3) pos = np.zeros(3) @@ -313,7 +309,7 @@ def position_and_jacobian_3d(alpha, beta, triangle): return jac.value, pos -def position_and_jacobian_radial(alpha, v1, v2, v3, v4): +def position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndarray, v3: np.ndarray, v4: np.ndarray) -> Tuple[float, np.ndarray]: assert v1.shape == (2,) or v1.shape == (3,) assert v2.shape == (2,) or v2.shape == (3,) assert v3.shape == (2,) or v3.shape == (3,) @@ -328,7 +324,7 @@ def position_and_jacobian_radial(alpha, v1, v2, v3, v4): return jac.value, pos -def delta_position_and_jacobian_radial(alpha, v1, v2, v3, v4): +def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndarray, v3: np.ndarray, v4: np.ndarray) -> Tuple[float, np.ndarray]: assert v1.shape == (2,) or v1.shape == (3,) assert v2.shape == (2,) or v2.shape == (3,) assert v3.shape == (2,) or v3.shape == (3,) @@ -345,28 +341,31 @@ def delta_position_and_jacobian_radial(alpha, v1, v2, v3, v4): -def trace_particle(position, velocity, charge_over_mass, field, bounds, atol): +def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field: Callable, bounds: np.ndarray, atol: float) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) return trace_particle_wrapper(position, velocity, lambda T, P: backend_lib.trace_particle(T, P, charge_over_mass, wrap_field_fun(field), bounds, atol, None)) -def trace_particle_radial(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_current, field_bounds=None): - +def trace_particle_radial(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, eff_elec, eff_mag, eff_current, + field_bounds: Optional[np.ndarray]=None) -> Tuple[np.ndarray, np.ndarray]: eff_elec = EffectivePointCharges2D(eff_elec) eff_mag = EffectivePointCharges2D(eff_mag) eff_current = EffectivePointCharges3D(eff_current) bounds = np.array(bounds) - field_bounds = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None + field_bounds_ptr = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None times, positions = trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_radial(T, P, charge_over_mass, bounds, atol, field_bounds, eff_elec, eff_mag, eff_current)) + lambda T, P: backend_lib.trace_particle_radial(T, P, charge_over_mass, bounds, atol, field_bounds_ptr, eff_elec, eff_mag, eff_current)) return times, positions -def trace_particle_radial_derivs(position, velocity, charge_over_mass, bounds, atol, z, elec_coeffs, mag_coeffs): +def trace_particle_radial_derivs(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, z: np.ndarray, + elec_coeffs: np.ndarray, mag_coeffs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert elec_coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) assert mag_coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) @@ -380,20 +379,25 @@ def trace_particle_radial_derivs(position, velocity, charge_over_mass, bounds, a return times, positions -def trace_particle_3d(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, field_bounds=None): +def trace_particle_3d(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, eff_elec, eff_mag, + field_bounds: Optional[np.ndarray]=None) -> Tuple[np.ndarray, np.ndarray]: assert field_bounds is None or field_bounds.shape == (3,2) bounds = np.array(bounds) - field_bounds = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None + field_bounds_ptr = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None eff_elec = EffectivePointCharges3D(eff_elec) eff_mag = EffectivePointCharges3D(eff_mag) return trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_3d(T, P, charge_over_mass, bounds, atol, eff_elec, eff_mag, field_bounds)) + lambda T, P: backend_lib.trace_particle_3d(T, P, charge_over_mass, bounds, atol, eff_elec, eff_mag, field_bounds_ptr)) -def trace_particle_3d_derivs(position, velocity, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs): + +def trace_particle_3d_derivs(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, z: np.ndarray, + electrostatic_coeffs: np.ndarray, magnetostatic_coeffs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert electrostatic_coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert magnetostatic_coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) @@ -402,11 +406,16 @@ def trace_particle_3d_derivs(position, velocity, charge_over_mass, bounds, atol, return trace_particle_wrapper(position, velocity, lambda T, P: backend_lib.trace_particle_3d_derivs(T, P, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs, len(z))) -potential_radial_ring = lambda *args: backend_lib.potential_radial_ring(*args, None) -dr1_potential_radial_ring = lambda *args: backend_lib.dr1_potential_radial_ring(*args, None) -dz1_potential_radial_ring = lambda *args: backend_lib.dz1_potential_radial_ring(*args, None) +def potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.potential_radial_ring(r0, z0, delta_r, delta_z) + +def dr1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.dr1_potential_radial_ring(r0, z0, delta_r, delta_z) + +def dz1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.dz1_potential_radial_ring(r0, z0, delta_r, delta_z) -def axial_derivatives_radial(z, charges, jac_buffer, pos_buffer): +def axial_derivatives_radial(z: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: derivs = np.zeros( (z.size, DERIV_2D_MAX) ) assert jac_buffer.shape == (len(charges), N_QUAD_2D) @@ -416,21 +425,21 @@ def axial_derivatives_radial(z, charges, jac_buffer, pos_buffer): backend_lib.axial_derivatives_radial(derivs,charges, jac_buffer, pos_buffer, len(charges), z, len(z)) return derivs -def potential_radial(point, charges, jac_buffer, pos_buffer): +def potential_radial(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: assert point.shape == (3,) assert jac_buffer.shape == (len(charges), N_QUAD_2D) assert pos_buffer.shape == (len(charges), N_QUAD_2D, 2) return backend_lib.potential_radial(point.astype(np.float64), charges, jac_buffer, pos_buffer, len(charges)) -def potential_radial_derivs(point, z, coeffs): +def potential_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) return backend_lib.potential_radial_derivs(point.astype(np.float64), z, coeffs, len(z)) -def charge_radial(vertices, charge): +def charge_radial(vertices: np.ndarray, charge: float) -> float: assert vertices.shape == (len(vertices), 3) return backend_lib.charge_radial(vertices, charge) -def field_radial(point, charges, jac_buffer, pos_buffer): +def field_radial(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert jac_buffer.shape == (len(charges), N_QUAD_2D) assert pos_buffer.shape == (len(charges), N_QUAD_2D, 2) @@ -440,25 +449,34 @@ def field_radial(point, charges, jac_buffer, pos_buffer): backend_lib.field_radial(point.astype(np.float64), field, charges, jac_buffer, pos_buffer, len(charges)) return field -def combine_elec_magnetic_field(vel, elec, mag, current): +def combine_elec_magnetic_field(vel: np.ndarray, elec: np.ndarray, mag: np.ndarray, current: np.ndarray) -> np.ndarray: result = np.zeros( (3,) ) backend_lib.combine_elec_magnetic_field(vel, elec, mag, current, result) return result -def field_radial_derivs(point, z, coeffs): +def field_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) field = np.zeros( (3,) ) backend_lib.field_radial_derivs(point.astype(np.float64), field, z, coeffs, len(z)) return field -dx1_potential_3d_point = remove_arg(backend_lib.dx1_potential_3d_point) -dy1_potential_3d_point = remove_arg(backend_lib.dy1_potential_3d_point) -dz1_potential_3d_point = remove_arg(backend_lib.dz1_potential_3d_point) -potential_3d_point = remove_arg(backend_lib.potential_3d_point) -flux_density_to_charge_factor = backend_lib.flux_density_to_charge_factor +def dx1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dx1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) -def axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, z): +def dy1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dy1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def dz1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dz1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def flux_density_to_charge_factor(K: float) -> float: + return backend_lib.flux_density_to_charge_factor(K) + +def axial_coefficients_3d(charges: np.ndarray, jacobian_buffer: np.ndarray, pos_buffer: np.ndarray, z: np.ndarray) -> np.ndarray: assert jacobian_buffer.shape == (len(charges), N_TRIANGLE_QUAD) assert pos_buffer.shape == (len(charges), N_TRIANGLE_QUAD, 3) @@ -467,13 +485,12 @@ def axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, z): trig_cos_buffer = np.zeros( (len(charges), N_TRIANGLE_QUAD, M_MAX) ) trig_sin_buffer = np.zeros( (len(charges), N_TRIANGLE_QUAD, M_MAX) ) - backend_lib.axial_coefficients_3d(charges, - jacobian_buffer, pos_buffer, trig_cos_buffer, trig_sin_buffer, - len(charges), z, output_coeffs, len(z)) + backend_lib.axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, trig_cos_buffer, trig_sin_buffer, + len(charges), z, output_coeffs, len(z)) return output_coeffs -def potential_3d(point, charges, jac_buffer, pos_buffer): +def potential_3d(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: assert point.shape == (3,) N = len(charges) assert charges.shape == (N,) @@ -482,13 +499,13 @@ def potential_3d(point, charges, jac_buffer, pos_buffer): return backend_lib.potential_3d(point.astype(np.float64), charges, jac_buffer, pos_buffer, N) -def potential_3d_derivs(point, z, coeffs): +def potential_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert point.shape == (3,) return backend_lib.potential_3d_derivs(point.astype(np.float64), z, coeffs, len(z)) -def field_3d(point, charges, jacobian_buffer, position_buffer): +def field_3d(point: np.ndarray, charges: np.ndarray, jacobian_buffer: np.ndarray, position_buffer: np.ndarray) -> np.ndarray: N = len(charges) assert point.shape == (3,) assert jacobian_buffer.shape == (N, N_TRIANGLE_QUAD) @@ -498,16 +515,18 @@ def field_3d(point, charges, jacobian_buffer, position_buffer): backend_lib.field_3d(point.astype(np.float64), field, charges, jacobian_buffer, position_buffer, N) return field -def field_3d_derivs(point, z, coeffs): +def field_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) field = np.zeros( (3,) ) backend_lib.field_3d_derivs(point.astype(np.float64), field, z, coeffs, len(z)) + return field -current_potential_axial_radial_ring = backend_lib.current_potential_axial_radial_ring +def current_potential_axial_radial_ring(z0: float, r: float, z: float) -> float: + return backend_lib.current_potential_axial_radial_ring(z0, r, z) -def current_potential_axial(z, currents, jac_buffer, pos_buffer): +def current_potential_axial(z: float, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: N = len(currents) assert currents.shape == (N,) assert jac_buffer.shape == (N, N_TRIANGLE_QUAD) @@ -517,12 +536,12 @@ def current_potential_axial(z, currents, jac_buffer, pos_buffer): return backend_lib.current_potential_axial(z, currents, jac_buffer, pos_buffer, N) -def current_field_radial_ring(x0, y0, x, y): - res = np.zeros( (2,) ) +def current_field_radial_ring(x0: float, y0: float, x: float, y: float) -> np.ndarray: + res = np.zeros((2,)) backend_lib.current_field_radial_ring(x0, y0, x, y, res) return res -def current_field(p0, currents, jac_buffer, pos_buffer): +def current_field(p0: np.ndarray, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: assert p0.shape == (3,) N = len(currents) assert currents.shape == (N,) @@ -535,7 +554,7 @@ def current_field(p0, currents, jac_buffer, pos_buffer): backend_lib.current_field(p0, result, currents, jac_buffer, pos_buffer, N) return result -def current_axial_derivatives_radial(z, currents, jac_buffer, pos_buffer): +def current_axial_derivatives_radial(z: np.ndarray, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: N_z = len(z) N_vertices = len(currents) @@ -549,7 +568,7 @@ def current_axial_derivatives_radial(z, currents, jac_buffer, pos_buffer): return derivs -def fill_jacobian_buffer_radial(vertices): +def fill_jacobian_buffer_radial(vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert vertices.shape == (len(vertices), 4, 3) assert np.all(vertices[:, :, 1] == 0.) @@ -561,7 +580,7 @@ def fill_jacobian_buffer_radial(vertices): return jac_buffer, pos_buffer -def self_potential_radial(vertices): +def self_potential_radial(vertices: np.ndarray) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) low_level = LowLevelCallable(backend_lib.self_potential_radial, user_data=user_data) @@ -570,7 +589,7 @@ def self_potential_radial(vertices): class SelfFieldDotNormalRadialArgs(C.Structure): _fields_ = [("line_points", C.POINTER(C.c_double)), ("K", C.c_double)] -def self_field_dot_normal_radial(vertices, K): +def self_field_dot_normal_radial(vertices: np.ndarray, K: float) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) @@ -585,7 +604,13 @@ def self_field_dot_normal_radial(vertices, K): -def fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_buffer, pos_buffer, start_index, end_index): +def fill_matrix_radial(matrix: np.ndarray, + lines: np.ndarray, + excitation_types: np.ndarray, + excitation_values: np.ndarray, + jac_buffer: np.ndarray, + pos_buffer: np.ndarray, + start_index: int, end_index: int): N = len(lines) assert np.all(lines[:, :, 1] == 0.0) assert matrix.shape[0] == N and matrix.shape[1] == N and matrix.shape[0] == matrix.shape[1] @@ -598,7 +623,7 @@ def fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_b backend_lib.fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_buffer, pos_buffer, N, matrix.shape[0], start_index, end_index) -def fill_jacobian_buffer_3d(vertices): +def fill_jacobian_buffer_3d(vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: N = len(vertices) assert vertices.shape == (N, 3, 3) jac_buffer = np.zeros( (N, N_TRIANGLE_QUAD) ) @@ -609,8 +634,13 @@ def fill_jacobian_buffer_3d(vertices): return jac_buffer, pos_buffer - -def fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_buffer, pos_buffer, start_index, end_index): +def fill_matrix_3d(matrix: np.ndarray, + vertices: np.ndarray, + excitation_types: np.ndarray, + excitation_values: np.ndarray, + jac_buffer: np.ndarray, + pos_buffer: np.ndarray, + start_index: int, end_index: int): N = len(vertices) assert matrix.shape[0] == N and matrix.shape[1] == N and matrix.shape[0] == matrix.shape[1] assert vertices.shape == (N, 3, 3) @@ -622,7 +652,7 @@ def fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_bu backend_lib.fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_buffer, pos_buffer, N, matrix.shape[0], start_index, end_index) -def plane_intersection(positions, p0, normal): +def plane_intersection(positions: np.ndarray, p0: np.ndarray, normal: np.ndarray) -> np.ndarray: assert p0.shape == (3,) assert normal.shape == (3,) assert positions.shape == (len(positions), 6) @@ -635,7 +665,7 @@ def plane_intersection(positions, p0, normal): return result -def triangle_areas(triangles): +def triangle_areas(triangles: np.ndarray) -> np.ndarray: assert triangles.shape == (len(triangles), 3, 3) out = np.zeros(len(triangles)) backend_lib.triangle_areas(triangles, out, len(triangles)) From 7aad5f9c18c7c25681d3238ca5d0950b1f8899b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 13:09:29 +0100 Subject: [PATCH 22/85] Add type: ignore to self_potential_radial (backend/__init__.py) --- traceon/backend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 3468668..0f529f8 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -584,7 +584,7 @@ def self_potential_radial(vertices: np.ndarray) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) low_level = LowLevelCallable(backend_lib.self_potential_radial, user_data=user_data) - return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] + return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] # type: ignore class SelfFieldDotNormalRadialArgs(C.Structure): _fields_ = [("line_points", C.POINTER(C.c_double)), ("K", C.c_double)] From 5af2efa0f353ca29505fd2de116464af0c542043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 13:14:31 +0100 Subject: [PATCH 23/85] Add missing parameter in backend.potential_radial_ring functions --- traceon/backend/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 0f529f8..d3024ee 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -407,13 +407,13 @@ def trace_particle_3d_derivs(position: np.ndarray, velocity: np.ndarray, charge_ lambda T, P: backend_lib.trace_particle_3d_derivs(T, P, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs, len(z))) def potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: - return backend_lib.potential_radial_ring(r0, z0, delta_r, delta_z) + return backend_lib.potential_radial_ring(r0, z0, delta_r, delta_z, None) def dr1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: - return backend_lib.dr1_potential_radial_ring(r0, z0, delta_r, delta_z) + return backend_lib.dr1_potential_radial_ring(r0, z0, delta_r, delta_z, None) def dz1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: - return backend_lib.dz1_potential_radial_ring(r0, z0, delta_r, delta_z) + return backend_lib.dz1_potential_radial_ring(r0, z0, delta_r, delta_z, None) def axial_derivatives_radial(z: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: derivs = np.zeros( (z.size, DERIV_2D_MAX) ) From e771904ebe93f7578f853fde9b668bf6b40150e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 13:19:53 +0100 Subject: [PATCH 24/85] Type triangle functions in backend/__init__.py --- traceon/backend/__init__.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index d3024ee..7138a0f 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -218,10 +218,24 @@ def backend_check_numpy_requirements_wrapper(*args, _cfun_reference=libfun, _cfu libfun.restype = res libfun.argtypes = args -self_potential_triangle_v0 = backend_lib.self_potential_triangle_v0 -self_potential_triangle = backend_lib.self_potential_triangle -potential_triangle = backend_lib.potential_triangle -flux_triangle = backend_lib.flux_triangle +def self_potential_triangle_v0(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray) -> float: + assert v0.shape == (3,) and v1.shape == (3,) and v2.shape == (3,) + return backend_lib.self_potential_triangle_v0(v0, v1, v2) + +def self_potential_triangle(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, target: np.ndarray) -> float: + assert v0.shape == (3,) and v1.shape == (3,) and v2.shape == (3,) + assert target.shape == (3,) + return backend_lib.self_potential_triangle(v0, v1, v2, target) + +def potential_triangle(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, target: np.ndarray) -> float: + assert v0.shape == (3,) and v1.shape == (3,) and v2.shape == (3,) + assert target.shape == (3,) + return backend_lib.potential_triangle(v0, v1, v2, target) + +def flux_triangle(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, target: np.ndarray, normal: np.ndarray) -> float: + assert v0.shape == (3,) and v1.shape == (3,) and v2.shape == (3,) + assert target.shape == (3,) and normal.shape == (3,) + return backend_lib.flux_triangle(v0, v1, v2, target, normal) ellipkm1 = np.vectorize(backend_lib.ellipkm1) ellipk = np.vectorize(backend_lib.ellipk) From c16c14aeee55d67507b3f6c914f964b2d9b3e303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 14:21:40 +0100 Subject: [PATCH 25/85] Generalize interface between Tracer and Field for radial symmetry --- traceon/backend/__init__.py | 63 +++++++++++++++++++++++++------------ traceon/backend/tracing.c | 4 +-- traceon/interpolation.py | 7 ++++- traceon/solver.py | 12 ++++--- traceon/tracing.py | 30 ++++++++++-------- 5 files changed, 76 insertions(+), 40 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 7138a0f..3cab243 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -131,6 +131,46 @@ def __init__(self, eff, *args, **kwargs): self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) self.N = len(eff) +class FieldEvaluationArgs(C.Structure): + _fields_ = [ + ("elec_charges", C.c_void_p), + ("mag_charges", C.c_void_p), + ("current_charges", C.c_void_p), + ("bounds", C.POINTER(C.c_double)) + ] + + def __init__(self, elec, mag, current, bounds, *args, **kwargs): + super(FieldEvaluationArgs, self).__init__(*args, **kwargs) + assert bounds is None or bounds.shape == (3, 2) + + self.elec_charges = C.cast(C.pointer(EffectivePointCharges2D(elec)), C.c_void_p) + self.mag_charges = C.cast(C.pointer(EffectivePointCharges2D(mag)), C.c_void_p) + self.current_charges = C.cast(C.pointer(EffectivePointCharges3D(current)), C.c_void_p) + + if bounds is None: + self.bounds = None + else: + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(C.c_double_p) + +class FieldDerivsArgs(C.Structure): + _fields_ = [ + ("z_interpolation", dbl_p), + ("electrostatic_axial_coeffs", dbl_p), + ("magnetostatic_axial_coeffs", dbl_p), + ("N_z", C.c_size_t) + ] + + def __init__(self, z, elec, mag, *args, **kwargs): + super().__init__(*args, **kwargs) + assert z.shape == (len(z),) + assert elec.shape[0] == len(z)-1 + assert mag.shape[0] == len(z)-1 + + self.z_interpolation = ensure_contiguous_aligned(z).ctypes.data_as(dbl_p) + self.electrostatic_axial_coeffs = ensure_contiguous_aligned(elec).ctypes.data_as(dbl_p) + self.magnetostatic_axial_coeffs = ensure_contiguous_aligned(mag).ctypes.data_as(dbl_p) + self.N_z = len(z) + bounds = arr(shape=(3, 2)) times_block = arr(shape=(TRACING_BLOCK_SIZE,)) @@ -165,7 +205,6 @@ def __init__(self, eff, *args, **kwargs): 'charge_radial': (dbl, arr(ndim=2), dbl), 'field_radial': (None, v3, v3, charges_2d, jac_buffer_2d, pos_buffer_2d, sz), 'combine_elec_magnetic_field': (None, v3, v3, v3, v3, v3), - 'trace_particle_radial': (sz, times_block, tracing_block, dbl, bounds, dbl, dbl_p, EffectivePointCharges2D, EffectivePointCharges2D, EffectivePointCharges3D), 'field_radial_derivs': (None, v3, v3, z_values, arr(ndim=3), sz), 'trace_particle_radial_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, radial_coeffs, radial_coeffs, sz), 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), @@ -355,27 +394,11 @@ def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndar -def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field: Callable, bounds: np.ndarray, atol: float) -> Tuple[np.ndarray, np.ndarray]: - bounds = np.array(bounds) - - return trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle(T, P, charge_over_mass, wrap_field_fun(field), bounds, atol, None)) - -def trace_particle_radial(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, - bounds: np.ndarray, atol: float, eff_elec, eff_mag, eff_current, - field_bounds: Optional[np.ndarray]=None) -> Tuple[np.ndarray, np.ndarray]: - eff_elec = EffectivePointCharges2D(eff_elec) - eff_mag = EffectivePointCharges2D(eff_mag) - eff_current = EffectivePointCharges3D(eff_current) - +def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) - field_bounds_ptr = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None - - times, positions = trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_radial(T, P, charge_over_mass, bounds, atol, field_bounds_ptr, eff_elec, eff_mag, eff_current)) - - return times, positions + return trace_particle_wrapper(position, velocity, + lambda T, P: backend_lib.trace_particle(T, P, charge_over_mass, field, bounds, atol, args)) def trace_particle_radial_derivs(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, bounds: np.ndarray, atol: float, z: np.ndarray, diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index ded1b47..5cba336 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -110,7 +110,7 @@ trace_particle(double *times_array, double *pos_array, double charge_over_mass, return N; } -void +EXPORT void field_radial_traceable(double point[6], double result[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*) args_p; @@ -164,7 +164,7 @@ trace_particle_radial(double *times_array, double *pos_array, double charge_over return trace_particle(times_array, pos_array, charge_over_mass, field_radial_traceable, tracer_bounds, atol, (void*) &args); } -void +EXPORT void field_radial_derivs_traceable(double point[6], double field[3], void *args_p) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; diff --git a/traceon/interpolation.py b/traceon/interpolation.py index 05a264b..3b0791d 100644 --- a/traceon/interpolation.py +++ b/traceon/interpolation.py @@ -199,6 +199,11 @@ def magnetostatic_potential_at_point(self, point_): return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs) def get_tracer(self, bounds): - return T.TracerRadialAxial(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 + diff --git a/traceon/solver.py b/traceon/solver.py index 05c8c3e..90f3d2c 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -837,7 +837,12 @@ def area_of_element(self, i): return 2*np.pi*np.sum(jacobians[i] * positions[i, :, 0]) def get_tracer(self, bounds): - return T.TracerRadialBEM(self, bounds) + return T.Tracer(self, bounds) + + def get_low_level_trace_function(self): + args = backend.FieldEvaluationArgs(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 + class Field3D_BEM(FieldBEM): @@ -1085,6 +1090,5 @@ def magnetostatic_potential_at_point(self, point_): return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs) def get_tracer(self, bounds): - return T.TracerRadialAxial(self, bounds) - - + return T.Tracer(self, bounds) + diff --git a/traceon/tracing.py b/traceon/tracing.py index 465b280..72030c3 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -7,7 +7,7 @@ [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.""" - +import ctypes from math import sqrt, cos, sin, atan2 import time from enum import Enum @@ -116,7 +116,16 @@ def __init__(self, field, bounds): bounds = np.array(bounds).astype(np.float64) assert bounds.shape == (3,2) self.bounds = bounds - + + fun = field.get_low_level_trace_function() + + if fun is None: + self.low_level_trace_function = None + self.low_level_trace_args = None + else: + self.low_level_trace_function, args = fun + self.low_level_trace_args = ctypes.cast(ctypes.pointer(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]) @@ -148,23 +157,18 @@ def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8): 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. """ - raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance') - -class TracerRadialBEM(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): charge_over_mass = charge / mass velocity = _convert_velocity_to_SI(velocity, mass) - - return backend.trace_particle_radial( + + return backend.trace_particle( position, velocity, charge_over_mass, + self.low_level_trace_function, self.bounds, - atol, - self.field.electrostatic_point_charges, - self.field.magnetostatic_point_charges, - self.field.current_point_charges, - field_bounds=self.field.field_bounds) + atol, + self.low_level_trace_args) + class TracerRadialAxial(Tracer): def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): From 03361722cb3da08179e19e44ccccb145d6305308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 18:35:57 +0100 Subject: [PATCH 26/85] Fix Makefile dependencies --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1023a3b..b88e0c3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: clean bdist -traceon/backend/traceon_backend.so: traceon/backend/traceon-backend.c +traceon/backend/traceon_backend.so: traceon/backend/*.c clang -O3 -march=native -shared -fPIC -ffast-math -Wno-extern-initializer ./traceon/backend/traceon-backend.c -o traceon/backend/traceon_backend.so -lm clean: From f35d33dd6482a680a9bb67ddf6a41b591d42f283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 18:36:26 +0100 Subject: [PATCH 27/85] Generalize interface between Tracer and Field for 3D symmetry --- tests/test_tracing.py | 6 +++--- traceon/backend/__init__.py | 31 +++++++++++++++++++++++++++---- traceon/backend/tracing.c | 2 +- traceon/solver.py | 9 +++++++-- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index c15d72c..1830fc0 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -28,7 +28,7 @@ def field(*_): return acceleration() / EM bounds = ((-2.0, 2.0), (-2.0, 2.0), (-2.0, np.sqrt(12)+1)) - times, positions = B.trace_particle(np.zeros( (3,) ), np.array([0., 0., 3.]), EM, field, bounds, 1e-10) + times, positions = B.trace_particle(np.zeros( (3,) ), np.array([0., 0., 3.]), EM, B.wrap_field_fun(field), bounds, 1e-10) correct_x = 3/2*times**2 correct_z = 3*times @@ -49,7 +49,7 @@ def traceon_acc(*y): v0 = np.array([0., 1, -1.]) bounds = ((-5.0, 5.0), (-5.0, 5.0), (-40.0, 10.0)) - times, positions = B.trace_particle(p0, v0, EM, traceon_acc, bounds, 1e-10) + times, positions = B.trace_particle(p0, v0, EM, B.wrap_field_fun(traceon_acc), bounds, 1e-10) sol = solve_ivp(acceleration, (0, 30), np.hstack( (p0, v0) ), method='DOP853', rtol=1e-10, atol=1e-10) @@ -200,7 +200,7 @@ def field(x, y, z, vx, vy, vz): mag_field = np.array([0, 0, -1.]) return np.cross(v, mag_field) / EM # Return acceleration - times, positions = B.trace_particle(x0, v0, EM, field, bounds, 1e-10) + times, positions = B.trace_particle(x0, v0, EM, B.wrap_field_fun(field), bounds, 1e-10) # Map y-position to state interp = CubicSpline(positions[-10:, 1][::-1], positions[-10:][::-1]) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 3cab243..cc63a51 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -124,14 +124,14 @@ class EffectivePointCharges3D(C.Structure): def __init__(self, eff, *args, **kwargs): assert eff.is_3d() - super(EffectivePointCharges3D, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.charges = ensure_contiguous_aligned(eff.charges).ctypes.data_as(dbl_p) self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) self.N = len(eff) -class FieldEvaluationArgs(C.Structure): +class FieldEvaluationArgsRadial(C.Structure): _fields_ = [ ("elec_charges", C.c_void_p), ("mag_charges", C.c_void_p), @@ -140,7 +140,7 @@ class FieldEvaluationArgs(C.Structure): ] def __init__(self, elec, mag, current, bounds, *args, **kwargs): - super(FieldEvaluationArgs, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) assert bounds is None or bounds.shape == (3, 2) self.elec_charges = C.cast(C.pointer(EffectivePointCharges2D(elec)), C.c_void_p) @@ -152,6 +152,29 @@ def __init__(self, elec, mag, current, bounds, *args, **kwargs): else: self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(C.c_double_p) +class FieldEvaluationArgs3D(C.Structure): + _fields_ = [ + ("elec_charges", C.c_void_p), + ("mag_charges", C.c_void_p), + ("current_charges", C.c_void_p), + ("bounds", C.POINTER(C.c_double)) + ] + + def __init__(self, elec, mag, bounds, *args, **kwargs): + super().__init__(*args, **kwargs) + assert bounds is None or bounds.shape == (3, 2) + + self.elec_charges = C.cast(C.pointer(EffectivePointCharges3D(elec)), C.c_void_p) + self.mag_charges = C.cast(C.pointer(EffectivePointCharges3D(mag)), C.c_void_p) + self.current_charges = None + + if bounds is None: + self.bounds = None + else: + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(C.c_double_p) + + + class FieldDerivsArgs(C.Structure): _fields_ = [ ("z_interpolation", dbl_p), @@ -394,7 +417,7 @@ def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndar -def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p) -> Tuple[np.ndarray, np.ndarray]: +def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p=None) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) return trace_particle_wrapper(position, velocity, diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 5cba336..ae0956a 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -187,7 +187,7 @@ trace_particle_radial_derivs(double *times_array, double *pos_array, double char return trace_particle(times_array, pos_array, charge_over_mass, field_radial_derivs_traceable, bounds, atol, (void*) &args); } -void +EXPORT void field_3d_traceable(double point[6], double result[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; diff --git a/traceon/solver.py b/traceon/solver.py index 90f3d2c..624c152 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -840,7 +840,7 @@ def get_tracer(self, bounds): return T.Tracer(self, bounds) def get_low_level_trace_function(self): - args = backend.FieldEvaluationArgs(self.electrostatic_point_charges, self.magnetostatic_point_charges, self.current_point_charges, self.field_bounds) + 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 @@ -951,7 +951,12 @@ def area_of_element(self, i): return np.sum(jacobians[i]) def get_tracer(self, bounds): - return T.Tracer3D_BEM(self, bounds) + return T.Tracer(self, bounds) + + def get_low_level_trace_function(self): + args = backend.FieldEvaluationArgs3D(self.electrostatic_point_charges, self.magnetostatic_point_charges, self.field_bounds) + return backend.field_fun(("field_3d_traceable", backend.backend_lib)), args + class FieldAxial(Field): From e3d2688d469201a53218ed02738b1fba5737a7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 18:38:14 +0100 Subject: [PATCH 28/85] Remove dead code related to old tracing functions --- traceon/backend/__init__.py | 3 --- traceon/backend/tracing.c | 46 ------------------------------------- traceon/tracing.py | 23 ------------------- 3 files changed, 72 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index cc63a51..1c3ec1e 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -229,7 +229,6 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'field_radial': (None, v3, v3, charges_2d, jac_buffer_2d, pos_buffer_2d, sz), 'combine_elec_magnetic_field': (None, v3, v3, v3, v3, v3), 'field_radial_derivs': (None, v3, v3, z_values, arr(ndim=3), sz), - 'trace_particle_radial_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, radial_coeffs, radial_coeffs, sz), 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dy1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dz1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), @@ -238,9 +237,7 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), - 'trace_particle_3d': (sz, times_block, tracing_block, dbl, bounds, dbl, EffectivePointCharges3D, EffectivePointCharges3D, dbl_p), 'field_3d_derivs': (None, v3, v3, z_values, arr(ndim=5), sz), - 'trace_particle_3d_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, arr(ndim=5), arr(ndim=5), sz), 'current_potential_axial_radial_ring': (dbl, dbl, dbl, dbl), 'current_potential_axial': (dbl, dbl, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), 'current_field_radial_ring': (None, dbl, dbl, dbl, dbl, v2), diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index ae0956a..2c87c3a 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -147,23 +147,6 @@ field_radial_traceable(double point[6], double result[3], void *args_p) { } - -EXPORT size_t -trace_particle_radial(double *times_array, double *pos_array, double charge_over_mass, double tracer_bounds[3][2], double atol, double *field_bounds, - struct effective_point_charges_2d eff_elec, - struct effective_point_charges_2d eff_mag, - struct effective_point_charges_3d eff_current) { - - struct field_evaluation_args args = { - .elec_charges = (void*) &eff_elec, - .mag_charges = (void*) &eff_mag, - .current_charges = (void*) &eff_current, - .bounds = field_bounds - }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_radial_traceable, tracer_bounds, atol, (void*) &args); -} - EXPORT void field_radial_derivs_traceable(double point[6], double field[3], void *args_p) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; @@ -178,15 +161,6 @@ field_radial_derivs_traceable(double point[6], double field[3], void *args_p) { combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); } -EXPORT size_t -trace_particle_radial_derivs(double *times_array, double *pos_array, double charge_over_mass, double bounds[3][2], double atol, - double *z_interpolation, double *electrostatic_coeffs, double *magnetostatic_coeffs, size_t N_z) { - - struct field_derivs_args args = { z_interpolation, electrostatic_coeffs, magnetostatic_coeffs, N_z }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_radial_derivs_traceable, bounds, atol, (void*) &args); -} - EXPORT void field_3d_traceable(double point[6], double result[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; @@ -214,16 +188,6 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { } } -EXPORT size_t -trace_particle_3d(double *times_array, double *pos_array, double charge_over_mass, double tracer_bounds[3][2], double atol, - struct effective_point_charges_3d eff_elec, struct effective_point_charges_3d eff_mag, double *field_bounds) { - - struct field_evaluation_args args = {.elec_charges = (void*) &eff_elec, .mag_charges = (void*) &eff_mag, .bounds = field_bounds}; - - return trace_particle(times_array, pos_array, charge_over_mass, field_3d_traceable, tracer_bounds, atol, (void*) &args); -} - - void field_3d_derivs_traceable(double point[6], double field[3], void *args_p) { @@ -239,16 +203,6 @@ field_3d_derivs_traceable(double point[6], double field[3], void *args_p) { combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); } -EXPORT size_t -trace_particle_3d_derivs(double *times_array, double *pos_array, double charge_over_mass, double bounds[3][2], double atol, - double *z_interpolation, double *electrostatic_coeffs, double *magnetostatic_coeffs, size_t N_z) { - - struct field_derivs_args args = { z_interpolation, electrostatic_coeffs, magnetostatic_coeffs, N_z }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_3d_derivs_traceable, bounds, atol, (void*) &args); -} - - EXPORT void fill_jacobian_buffer_3d( jacobian_buffer_3d jacobian_buffer, position_buffer_3d pos_buffer, diff --git a/traceon/tracing.py b/traceon/tracing.py index 72030c3..db8fbf9 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -170,29 +170,6 @@ def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8): self.low_level_trace_args) -class TracerRadialAxial(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - - elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs - - return backend.trace_particle_radial_derivs(position, velocity, charge_over_mass, self.bounds, atol, self.field.z, elec, mag) - -class Tracer3D_BEM(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges - return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, field_bounds=self.field.field_bounds) - -class Tracer3DAxial(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - return backend.trace_particle_3d_derivs(position, velocity, charge_over_mass, self.bounds, atol, - self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs) - 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 From d3441741e0f3121af2d2594d2c63f452c5e429ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 18:40:27 +0100 Subject: [PATCH 29/85] Move wrap_field_fun in backend closer to trace_particle --- traceon/backend/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 1c3ec1e..c3f74d2 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -361,15 +361,7 @@ def trace_particle_wrapper(position_: np.ndarray, velocity_: np.ndarray, else: return np.concatenate(times_blocks), np.concatenate(pos_blocks) -def wrap_field_fun(ff: Callable) -> Callable: - def wrapper(y, result, _): - field = ff(y[0], y[1], y[2], y[3], y[4], y[5]) - assert field.shape == (3,) - result[0] = field[0] - result[1] = field[1] - result[2] = field[2] - - return field_fun(wrapper) + def position_and_jacobian_3d(alpha: float, beta: float, triangle: np.ndarray) -> Tuple[float, np.ndarray]: assert triangle.shape == (3, 3) @@ -412,7 +404,15 @@ def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndar return jac.value, pos - +def wrap_field_fun(ff: Callable) -> Callable: + def wrapper(y, result, _): + field = ff(y[0], y[1], y[2], y[3], y[4], y[5]) + assert field.shape == (3,) + result[0] = field[0] + result[1] = field[1] + result[2] = field[2] + + return field_fun(wrapper) def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p=None) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) From c21220a894cf0cd0c9c7e29b050d66bf302ec61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 19:01:50 +0100 Subject: [PATCH 30/85] Split position and velocity in field_fun --- tests/test_tracing.py | 7 +++-- traceon/backend/__init__.py | 18 +++++------ traceon/backend/tracing.c | 61 +++++++++++++++++++------------------ 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 1830fc0..2a8d0e9 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -42,7 +42,8 @@ def acceleration(_, y): B = np.array([0, 0, 1]) return np.hstack( (v, np.cross(v, B)) ) - def traceon_acc(*y): + def traceon_acc(pos, vel): + y = np.concatenate( (pos, vel) ) return acceleration(0., y)[3:] / EM p0 = np.zeros(3) @@ -195,7 +196,9 @@ def test_cyclotron_radius(self): v0 = np.array([0., 1., 0.]) bounds = ((-2., 2.), (0., 2.), (-2., 2.)) - def field(x, y, z, vx, vy, vz): + def field(pos, vel): + x, y, z = pos + vx, vy, vz = vel p, v = np.array([x,y,z]), np.array([vx, vy, vz]) mag_field = np.array([0, 0, -1.]) return np.cross(v, mag_field) / EM # Return acceleration diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index c3f74d2..1d23d04 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -79,7 +79,7 @@ def arr(ndim=None, shape=None, dtype:DTypeLike=np.float64): sz = C.c_size_t integration_cb_1d = C.CFUNCTYPE(dbl, dbl, vp) -field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), vp); +field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), C.POINTER(dbl), vp); vertices = arr(ndim=3) lines = arr(ndim=3) @@ -405,14 +405,14 @@ def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndar return jac.value, pos def wrap_field_fun(ff: Callable) -> Callable: - def wrapper(y, result, _): - field = ff(y[0], y[1], y[2], y[3], y[4], y[5]) - assert field.shape == (3,) - result[0] = field[0] - result[1] = field[1] - result[2] = field[2] - - return field_fun(wrapper) + def field_fun_wrapper(pos, vel, result, _): + acceleration = ff(np.array([pos[0], pos[1], pos[2]]), np.array([vel[0], vel[1], vel[2]])) + assert acceleration.shape == (3,) + result[0] = acceleration[0] + result[1] = acceleration[1] + result[2] = acceleration[2] + + return field_fun(field_fun_wrapper) def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p=None) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 2c87c3a..ba53fac 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -15,7 +15,7 @@ const double B2[] = {2./9.}; const double CH[] = {47./450., 0., 12./25., 32./225., 1./30., 6./25.}; const double CT[] = {-1./150., 0., 3./100., -16./75., -1./20., 6./25.}; -typedef void (*field_fun)(double pos[6], double field[3], void* args); +typedef void (*field_fun)(double position[3], double velocity[3], double field_out[3], void* args); void produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { @@ -34,7 +34,7 @@ produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { void produce_new_k(double ys[6][6], double ks[6][6], size_t index, double h, double charge_over_mass, field_fun ff, void *args) { double field[3] = { 0. }; - ff(ys[index], field, args); + ff(&ys[index][0], &ys[index][3], field, args); ks[index][0] = h*ys[index][3]; ks[index][1] = h*ys[index][4]; @@ -111,7 +111,7 @@ trace_particle(double *times_array, double *pos_array, double charge_over_mass, } EXPORT void -field_radial_traceable(double point[6], double result[3], void *args_p) { +field_radial_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*) args_p; @@ -121,86 +121,87 @@ field_radial_traceable(double point[6], double result[3], void *args_p) { double (*bounds)[2] = (double (*)[2]) args->bounds; - if(args->bounds == NULL || ((bounds[0][0] < point[0]) && (point[0] < bounds[0][1]) - && (bounds[1][0] < point[1]) && (point[1] < bounds[1][1]))) { + if(args->bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) + && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]))) { double elec_field[3] = {0.}; double mag_field[3] = {0.}; double curr_field[3] = {0.}; - field_radial(point, elec_field, + field_radial(position, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_radial(point, mag_field, + field_radial(position, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - current_field(point, curr_field, + current_field(position, curr_field, current_charges->charges, current_charges->jacobians, current_charges->positions, current_charges->N); - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); + combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); } else { - result[0] = 0.; - result[1] = 0.; - result[2] = 0.; + field_out[0] = 0.; + field_out[1] = 0.; + field_out[2] = 0.; } } EXPORT void -field_radial_derivs_traceable(double point[6], double field[3], void *args_p) { +field_radial_derivs_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; double elec_field[3]; - field_radial_derivs(point, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_radial_derivs(position, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); double mag_field[3]; - field_radial_derivs(point, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); + field_radial_derivs(position, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); + combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); } EXPORT void -field_3d_traceable(double point[6], double result[3], void *args_p) { +field_3d_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { + struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; struct effective_point_charges_3d *mag_charges = (struct effective_point_charges_3d*) args->mag_charges; double (*bounds)[2] = (double (*)[2]) args->bounds; - if( bounds == NULL || ((bounds[0][0] < point[0]) && (point[0] < bounds[0][1]) - && (bounds[1][0] < point[1]) && (point[1] < bounds[1][1]) - && (bounds[2][0] < point[2]) && (point[2] < bounds[2][1])) ) { + if( bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) + && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]) + && (bounds[2][0] < position[2]) && (position[2] < bounds[2][1])) ) { double elec_field[3] = {0.}; double mag_field[3] = {0.}; double curr_field[3] = {0.}; - field_3d(point, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); + field_3d(position, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); + field_3d(position, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); + combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); } else { - result[0] = 0.0; - result[1] = 0.0; - result[2] = 0.0; + field_out[0] = 0.0; + field_out[1] = 0.0; + field_out[2] = 0.0; } } void -field_3d_derivs_traceable(double point[6], double field[3], void *args_p) { +field_3d_derivs_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; double elec_field[3]; - field_3d_derivs(point, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_3d_derivs(position, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); double mag_field[3]; - field_3d_derivs(point, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); + field_3d_derivs(position, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); + combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); } EXPORT void fill_jacobian_buffer_3d( From 902856635d078a3d7d972cf071a67b9ec02a4066 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Mon, 16 Dec 2024 20:46:38 +0100 Subject: [PATCH 31/85] made collections iterable and subscriptable --- traceon/geometry.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/traceon/geometry.py b/traceon/geometry.py index 652edb4..999bd06 100644 --- a/traceon/geometry.py +++ b/traceon/geometry.py @@ -964,7 +964,20 @@ def __iadd__(self, other): 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): @@ -977,7 +990,7 @@ def extrude_by_path(self, p2): return self._map_to_surfaces(Path.extrude_by_path, p2) def __str__(self): - return f"" + return f"" class Surface(GeometricObject): @@ -1528,6 +1541,19 @@ def __iadd__(self, other): 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"" From 3b90a4272082f43e838335062f7cf8604ad22828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 21:36:18 +0100 Subject: [PATCH 32/85] Simplify field_fun by making it return electric, magnetic field --- tests/test_tracing.py | 13 ++--- tests/test_utilities_3d.py | 7 --- traceon/backend/__init__.py | 32 +++++++----- traceon/backend/tracing.c | 90 ++++++++++++++++++---------------- traceon/backend/utilities_3d.c | 29 ----------- 5 files changed, 70 insertions(+), 101 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 2a8d0e9..0da6a19 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -25,7 +25,7 @@ def acceleration(*_): def field(*_): acceleration_x = 3 # mm/ns - return acceleration() / EM + return acceleration() / EM, np.zeros(3) bounds = ((-2.0, 2.0), (-2.0, 2.0), (-2.0, np.sqrt(12)+1)) times, positions = B.trace_particle(np.zeros( (3,) ), np.array([0., 0., 3.]), EM, B.wrap_field_fun(field), bounds, 1e-10) @@ -43,9 +43,8 @@ def acceleration(_, y): return np.hstack( (v, np.cross(v, B)) ) def traceon_acc(pos, vel): - y = np.concatenate( (pos, vel) ) - return acceleration(0., y)[3:] / EM - + return np.zeros(3), np.array([0, 0, 1])/(mu_0*EM) + p0 = np.zeros(3) v0 = np.array([0., 1, -1.]) @@ -197,11 +196,7 @@ def test_cyclotron_radius(self): bounds = ((-2., 2.), (0., 2.), (-2., 2.)) def field(pos, vel): - x, y, z = pos - vx, vy, vz = vel - p, v = np.array([x,y,z]), np.array([vx, vy, vz]) - mag_field = np.array([0, 0, -1.]) - return np.cross(v, mag_field) / EM # Return acceleration + return np.zeros(3), np.array([0, 0, -1.])/(EM*mu_0) times, positions = B.trace_particle(x0, v0, EM, B.wrap_field_fun(field), bounds, 1e-10) diff --git a/tests/test_utilities_3d.py b/tests/test_utilities_3d.py index 5c92b3c..dc315e9 100644 --- a/tests/test_utilities_3d.py +++ b/tests/test_utilities_3d.py @@ -40,10 +40,3 @@ def test_position_and_jacobian(self): _, pos = B.position_and_jacobian_3d(alpha, beta, tri) assert np.allclose(pos, v0 + vec1*alpha + beta*vec2) - - def test_combine_elec_magnetic(self): - - for i in range(20): - vel, elec, mag, current = np.random.rand(4, 3) - result = B.combine_elec_magnetic_field(vel, elec, mag, current) - assert np.allclose(result, elec + mu_0*np.cross(vel, mag + current)) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 1d23d04..34769b0 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -79,7 +79,14 @@ def arr(ndim=None, shape=None, dtype:DTypeLike=np.float64): sz = C.c_size_t integration_cb_1d = C.CFUNCTYPE(dbl, dbl, vp) -field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), C.POINTER(dbl), vp); + +# The low level field function has the following arguments: +# - A pointer to position (three doubles) +# - A pointer to velocity (three doubles) +# - A pointer to auxillary data the field function needs +# - A pointer to write the computed electric field (three doubles) +# - A pointer to write the computed magnetic field (three doubles) +field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), vp, C.POINTER(dbl), C.POINTER(dbl)); vertices = arr(ndim=3) lines = arr(ndim=3) @@ -227,7 +234,6 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'flux_density_to_charge_factor': (dbl, dbl), 'charge_radial': (dbl, arr(ndim=2), dbl), 'field_radial': (None, v3, v3, charges_2d, jac_buffer_2d, pos_buffer_2d, sz), - 'combine_elec_magnetic_field': (None, v3, v3, v3, v3, v3), 'field_radial_derivs': (None, v3, v3, z_values, arr(ndim=3), sz), 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dy1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), @@ -405,12 +411,17 @@ def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndar return jac.value, pos def wrap_field_fun(ff: Callable) -> Callable: - def field_fun_wrapper(pos, vel, result, _): - acceleration = ff(np.array([pos[0], pos[1], pos[2]]), np.array([vel[0], vel[1], vel[2]])) - assert acceleration.shape == (3,) - result[0] = acceleration[0] - result[1] = acceleration[1] - result[2] = acceleration[2] + def field_fun_wrapper(pos, vel, _, elec_out, mag_out): + elec, mag = ff(np.array([pos[0], pos[1], pos[2]]), np.array([vel[0], vel[1], vel[2]])) + assert elec.shape == (3,) and mag.shape == (3,) + + elec_out[0] = elec[0] + elec_out[1] = elec[1] + elec_out[2] = elec[2] + + mag_out[0] = mag[0] + mag_out[1] = mag[1] + mag_out[2] = mag[2] return field_fun(field_fun_wrapper) @@ -506,11 +517,6 @@ def field_radial(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, backend_lib.field_radial(point.astype(np.float64), field, charges, jac_buffer, pos_buffer, len(charges)) return field -def combine_elec_magnetic_field(vel: np.ndarray, elec: np.ndarray, mag: np.ndarray, current: np.ndarray) -> np.ndarray: - result = np.zeros( (3,) ) - backend_lib.combine_elec_magnetic_field(vel, elec, mag, current, result) - return result - def field_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index ba53fac..a1070da 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -15,7 +15,7 @@ const double B2[] = {2./9.}; const double CH[] = {47./450., 0., 12./25., 32./225., 1./30., 6./25.}; const double CT[] = {-1./150., 0., 3./100., -16./75., -1./20., 6./25.}; -typedef void (*field_fun)(double position[3], double velocity[3], double field_out[3], void* args); +typedef void (*field_fun)(double position[3], double velocity[3], void* args, double elec_out[3], double mag_out[3]); void produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { @@ -33,9 +33,21 @@ produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { void produce_new_k(double ys[6][6], double ks[6][6], size_t index, double h, double charge_over_mass, field_fun ff, void *args) { - double field[3] = { 0. }; - ff(&ys[index][0], &ys[index][3], field, args); + double elec[3] = { 0. }; + double mag[3] = { 0. }; + ff(&ys[index][0], &ys[index][3], args, elec, mag); + + // Convert to acceleration using Lorentz force law + double cross[3] = { 0. }; + cross_product_3d(&ys[index][3], mag, cross); // Compute v x H + + double field[3] = { // Compute E + v x B + elec[0] + MU_0*cross[0], + elec[1] + MU_0*cross[1], + elec[2] + MU_0*cross[2] + }; + ks[index][0] = h*ys[index][3]; ks[index][1] = h*ys[index][4]; ks[index][2] = h*ys[index][5]; @@ -111,7 +123,7 @@ trace_particle(double *times_array, double *pos_array, double charge_over_mass, } EXPORT void -field_radial_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { +field_radial_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_evaluation_args *args = (struct field_evaluation_args*) args_p; @@ -124,45 +136,44 @@ field_radial_traceable(double position[3], double velocity[3], double field_out[ if(args->bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]))) { - double elec_field[3] = {0.}; - double mag_field[3] = {0.}; - double curr_field[3] = {0.}; - field_radial(position, elec_field, + field_radial(position, elec_out, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_radial(position, mag_field, + field_radial(position, mag_out, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); + double curr_field[3] = {0.}; + current_field(position, curr_field, current_charges->charges, current_charges->jacobians, current_charges->positions, current_charges->N); - - combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); + + mag_out[0] += curr_field[0]; + mag_out[1] += curr_field[1]; + mag_out[2] += curr_field[2]; } else { - field_out[0] = 0.; - field_out[1] = 0.; - field_out[2] = 0.; + elec_out[0] = 0.; + elec_out[1] = 0.; + elec_out[2] = 0.; + + mag_out[0] = 0.; + mag_out[1] = 0.; + mag_out[2] = 0.; } } EXPORT void -field_radial_derivs_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { +field_radial_derivs_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; - - double elec_field[3]; - field_radial_derivs(position, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); - double mag_field[3]; - field_radial_derivs(position, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); - - double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); + field_radial_derivs(position, elec_out, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_radial_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } EXPORT void -field_3d_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { +field_3d_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; @@ -174,34 +185,27 @@ field_3d_traceable(double position[3], double velocity[3], double field_out[3], && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]) && (bounds[2][0] < position[2]) && (position[2] < bounds[2][1])) ) { - double elec_field[3] = {0.}; - double mag_field[3] = {0.}; - double curr_field[3] = {0.}; - - field_3d(position, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_3d(position, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); + field_3d(position, elec_out, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); + field_3d(position, mag_out, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); } else { - field_out[0] = 0.0; - field_out[1] = 0.0; - field_out[2] = 0.0; + elec_out[0] = 0.0; + elec_out[1] = 0.0; + elec_out[2] = 0.0; + + mag_out[0] = 0.0; + mag_out[1] = 0.0; + mag_out[2] = 0.0; } } void -field_3d_derivs_traceable(double position[3], double velocity[3], double field_out[3], void *args_p) { +field_3d_derivs_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; - double elec_field[3]; - field_3d_derivs(position, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); - - double mag_field[3]; - field_3d_derivs(position, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); - - double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(velocity, elec_field, mag_field, curr_field, field_out); + field_3d_derivs(position, elec_out, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_3d_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } EXPORT void fill_jacobian_buffer_3d( diff --git a/traceon/backend/utilities_3d.c b/traceon/backend/utilities_3d.c index 2850213..7838da9 100644 --- a/traceon/backend/utilities_3d.c +++ b/traceon/backend/utilities_3d.c @@ -132,32 +132,3 @@ INLINE void position_and_jacobian_3d(double alpha, double beta, triangle t, doub pos_out[2] = z; *jac = 2*triangle_area(t[0], t[1], t[2]); } - -// Compute E + v x B, which is used in the Lorentz force law to calculate the force -// on the particle. The magnetic field produced by magnetiziation and the magnetic field -// produced by currents are passed in separately, but can simpy be summed to find the total -// magnetic field. -EXPORT void -combine_elec_magnetic_field(double velocity[3], double elec_field[3], - double mag_field[3], double current_field[3], double result[3]) { - - double total_mag[3] = {0.}; // Total magnetic field, produced by charges and currents - - // Important: Traceon always computes the H field - // Therefore when converting from H to B we need to multiply - // by mu_0. - total_mag[0] = MU_0*(mag_field[0] + current_field[0]); - total_mag[1] = MU_0*(mag_field[1] + current_field[1]); - total_mag[2] = MU_0*(mag_field[2] + current_field[2]); - - double cross[3] = {0.}; - - // Calculate v x B - cross_product_3d(velocity, total_mag, cross); - - result[0] = elec_field[0] + cross[0]; - result[1] = elec_field[1] + cross[1]; - result[2] = elec_field[2] + cross[2]; -} - - From 5ba767ef0a2fcb7be0281271d58ff1e6a85f33f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 22:00:18 +0100 Subject: [PATCH 33/85] Add get_low_level_trace_function to Field and use it in tracing.py --- tests/test_tracing.py | 38 ++++++++++++++++++++++++++++++++++++++ traceon/solver.py | 18 ++++++++++-------- traceon/tracing.py | 16 ++++++---------- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 0da6a19..89f85ac 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -57,6 +57,44 @@ def traceon_acc(pos, vel): assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T) + def test_tracing_helix_against_scipy_custom_field(self): + def acceleration(_, y): + v = y[3:] + B = np.array([0, 0, 1]) + return np.hstack( (v, np.cross(v, B)) ) + + class CustomField(S.Field): + def magnetostatic_field_at_point(self, point): + return np.array([0, 0, 1])/(mu_0*EM) + + def electrostatic_field_at_point(self, point): + return np.array([0, 0, 0]) + + def electrostatic_potential_at_point(self): + return 0.0 + + def is_magnetostatic(self): + return True + + def is_electrostatic(self): + return False + + p0 = np.zeros(3) + v0 = np.array([0., 1, -1.]) + + bounds = ((-5.0, 5.0), (-5.0, 5.0), (-40.0, 10.0)) + tracer = T.Tracer(CustomField(), bounds) + + # Note that we transform velocity to eV, since it's being converted back to m/s in the Tracer.__call__ function + times, positions = tracer(p0, v0*4.020347574230144e-12, atol=1e-10) + + sol = solve_ivp(acceleration, (0, 30), np.hstack( (p0, v0) ), method='DOP853', rtol=1e-10, atol=1e-10) + + interp = CubicSpline(positions[::-1, 2], np.array([positions[::-1, 0], positions[::-1, 1]]).T) + + assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T) + + def test_tracing_against_scipy_current_loop(self): # Constants current = 100 # Ampere on current loop diff --git a/traceon/solver.py b/traceon/solver.py index 624c152..0535bcc 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -54,6 +54,7 @@ import numpy as np from scipy.special import legendre +from scipy.constants import e, mu_0, m_e from . import geometry as G from . import excitation as E @@ -520,10 +521,6 @@ def is_electrostatic(self): def is_magnetostatic(self): ... - @abstractmethod - def magnetostatic_potential_at_point(self, point): - ... - @abstractmethod def electrostatic_potential_at_point(self, point): ... @@ -535,11 +532,16 @@ 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 + 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. diff --git a/traceon/tracing.py b/traceon/tracing.py index db8fbf9..22e73e5 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -14,7 +14,8 @@ import numpy as np import scipy -from scipy.constants import m_e, e +from scipy.constants import m_e, e, mu_0 + from . import backend from . import logging @@ -117,14 +118,9 @@ def __init__(self, field, bounds): assert bounds.shape == (3,2) self.bounds = bounds - fun = field.get_low_level_trace_function() + self.trace_fun, args = field.get_low_level_trace_function() - if fun is None: - self.low_level_trace_function = None - self.low_level_trace_args = None - else: - self.low_level_trace_function, args = fun - self.low_level_trace_args = ctypes.cast(ctypes.pointer(args), ctypes.c_void_p) + self.trace_args = args if args is None else ctypes.cast(ctypes.pointer(args), ctypes.c_void_p) def __str__(self): field_name = self.field.__class__.__name__ @@ -164,10 +160,10 @@ def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8): position, velocity, charge_over_mass, - self.low_level_trace_function, + self.trace_fun, self.bounds, atol, - self.low_level_trace_args) + self.trace_args) def plane_intersection(positions, p0, normal): From 5a9121689b9c47f38c88b2b4d164ec7dd7dd8e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 16 Dec 2024 22:04:23 +0100 Subject: [PATCH 34/85] Fix typing warnings in backend/__init__.py and solver.py --- traceon/backend/__init__.py | 6 +++--- traceon/solver.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 34769b0..9950bd2 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -157,7 +157,7 @@ def __init__(self, elec, mag, current, bounds, *args, **kwargs): if bounds is None: self.bounds = None else: - self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(C.c_double_p) + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) class FieldEvaluationArgs3D(C.Structure): _fields_ = [ @@ -178,7 +178,7 @@ def __init__(self, elec, mag, bounds, *args, **kwargs): if bounds is None: self.bounds = None else: - self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(C.c_double_p) + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) @@ -425,7 +425,7 @@ def field_fun_wrapper(pos, vel, _, elec_out, mag_out): return field_fun(field_fun_wrapper) -def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args: C.c_void_p=None) -> Tuple[np.ndarray, np.ndarray]: +def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args =None) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) return trace_particle_wrapper(position, velocity, diff --git a/traceon/solver.py b/traceon/solver.py index 0535bcc..87cf66d 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -508,7 +508,7 @@ def potential_at_point(self, point): if elec and not mag: return self.electrostatic_potential_at_point(point) elif not elec and mag: - return self.magnetostatic_potential_at_point(point) + 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") From 04fd1e010d152ec0aadf064a8250c0c7ad029797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Tue, 17 Dec 2024 21:52:48 +0100 Subject: [PATCH 35/85] Add develop to branches in type-checking.yaml, add workflow_dispatch --- .github/workflows/type-checking.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/type-checking.yaml b/.github/workflows/type-checking.yaml index 47dd9d9..49b58e2 100644 --- a/.github/workflows/type-checking.yaml +++ b/.github/workflows/type-checking.yaml @@ -5,9 +5,12 @@ on: pull_request: branches: - main + - develop push: branches: - main + - develop + workflow_dispatch: jobs: type-check: From 908f315a57e96b06eeedb602bb8ca1ad58ba925a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 21 Dec 2024 13:36:19 +0100 Subject: [PATCH 36/85] Add generate_docs/ directory --- generate_docs/custom_pdoc.py | 252 ++++++++++++++ generate_docs/images/einzel lens.png | Bin 0 -> 401105 bytes generate_docs/make_docs.sh | 15 + generate_docs/pages/einzel-lens.md | 21 ++ generate_docs/templates/css.mako | 425 ++++++++++++++++++++++++ generate_docs/templates/html.mako | 476 +++++++++++++++++++++++++++ 6 files changed, 1189 insertions(+) create mode 100644 generate_docs/custom_pdoc.py create mode 100644 generate_docs/images/einzel lens.png create mode 100644 generate_docs/make_docs.sh create mode 100644 generate_docs/pages/einzel-lens.md create mode 100644 generate_docs/templates/css.mako create mode 100644 generate_docs/templates/html.mako 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.png b/generate_docs/images/einzel lens.png new file mode 100644 index 0000000000000000000000000000000000000000..1d328b5a2d1cce40c6d14c1d79117c864ca023bb GIT binary patch literal 401105 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 z

b625sE+FlhWr` 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 R>_Wir=_6(NJUOGc{|ks_M+yJ{ literal 0 HcmV?d00001 diff --git a/generate_docs/make_docs.sh b/generate_docs/make_docs.sh new file mode 100644 index 0000000..b5b073f --- /dev/null +++ b/generate_docs/make_docs.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +VERSION=$(python3 -c "from importlib.metadata import version; print(version('traceon'))") +DIR=../docs/docs/v$VERSION/ + +if [ -d $DIR ]; then + echo "Directory $DIR exists." + exit +fi + +python3 ./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..fc7e2f6 --- /dev/null +++ b/generate_docs/pages/einzel-lens.md @@ -0,0 +1,21 @@ +# Einzel lens + + +## Introduction + +Some introductory text + +## Defining the geometry + +```Python +import traceon.geometry as G + +p = G.Path.line([0., 0., 0.], [1., 0., 0.]) +``` + +An einzel lens bla bla bla + +## Result + + + diff --git a/generate_docs/templates/css.mako b/generate_docs/templates/css.mako new file mode 100644 index 0000000..e3d4bcc --- /dev/null +++ b/generate_docs/templates/css.mako @@ -0,0 +1,425 @@ +<%! + 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; + } + + #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: 25%; + 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..77a2082 --- /dev/null +++ b/generate_docs/templates/html.mako @@ -0,0 +1,476 @@ +<% + 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) + %> +
    + % for item in items: +
  • ${link(item, item.name)}
  • + % endfor +
+ + +<%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 + + From c2ac87274ef115233c5545d59c4aeb215749a37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 21 Dec 2024 13:36:38 +0100 Subject: [PATCH 37/85] Bump version number to v0.9.0rc1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', From 21b53d0d82a37793fe8c1a144e3ad8e5e8a3383b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 21 Dec 2024 13:37:04 +0100 Subject: [PATCH 38/85] Add documentation for v0.9.0rc1 --- docs/docs/v0.9.0rc1/images/einzel lens.png | Bin 0 -> 401105 bytes docs/docs/v0.9.0rc1/traceon/einzel-lens.html | 115 + docs/docs/v0.9.0rc1/traceon/excitation.html | 1168 ++++ docs/docs/v0.9.0rc1/traceon/focus.html | 200 + docs/docs/v0.9.0rc1/traceon/geometry.html | 5080 +++++++++++++++++ docs/docs/v0.9.0rc1/traceon/index.html | 205 + .../docs/v0.9.0rc1/traceon/interpolation.html | 633 ++ docs/docs/v0.9.0rc1/traceon/logging.html | 277 + docs/docs/v0.9.0rc1/traceon/mesher.html | 1767 ++++++ docs/docs/v0.9.0rc1/traceon/plotting.html | 840 +++ docs/docs/v0.9.0rc1/traceon/solver.html | 2538 ++++++++ docs/docs/v0.9.0rc1/traceon/tracing.html | 925 +++ docs/docs/v0.9.0rc1/traceon_pro/index.html | 146 + .../v0.9.0rc1/traceon_pro/interpolation.html | 290 + docs/docs/v0.9.0rc1/traceon_pro/solver.html | 221 + .../v0.9.0rc1/traceon_pro/traceon_pro.html | 247 + 16 files changed, 14652 insertions(+) create mode 100644 docs/docs/v0.9.0rc1/images/einzel lens.png create mode 100644 docs/docs/v0.9.0rc1/traceon/einzel-lens.html create mode 100644 docs/docs/v0.9.0rc1/traceon/excitation.html create mode 100644 docs/docs/v0.9.0rc1/traceon/focus.html create mode 100644 docs/docs/v0.9.0rc1/traceon/geometry.html create mode 100644 docs/docs/v0.9.0rc1/traceon/index.html create mode 100644 docs/docs/v0.9.0rc1/traceon/interpolation.html create mode 100644 docs/docs/v0.9.0rc1/traceon/logging.html create mode 100644 docs/docs/v0.9.0rc1/traceon/mesher.html create mode 100644 docs/docs/v0.9.0rc1/traceon/plotting.html create mode 100644 docs/docs/v0.9.0rc1/traceon/solver.html create mode 100644 docs/docs/v0.9.0rc1/traceon/tracing.html create mode 100644 docs/docs/v0.9.0rc1/traceon_pro/index.html create mode 100644 docs/docs/v0.9.0rc1/traceon_pro/interpolation.html create mode 100644 docs/docs/v0.9.0rc1/traceon_pro/solver.html create mode 100644 docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html 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 0000000000000000000000000000000000000000..1d328b5a2d1cce40c6d14c1d79117c864ca023bb GIT binary patch literal 401105 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 z

b625sE+FlhWr` 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 R>_Wir=_6(NJUOGc{|ks_M+yJ{ literal 0 HcmV?d00001 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..1e88920 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/einzel-lens.html @@ -0,0 +1,115 @@ + + + + + + + + + + traceon API documentation + + + + + + + + + + + + + + + + + + + + + + +

+
+ +

Einzel lens

+

Introduction

+

Some introductory text

+

Defining the geometry

+
import traceon.geometry as G
+
+p = G.Path.line([0., 0., 0.], [1., 0., 0.])
+
+

An einzel lens bla bla bla

+

Result

+

+
+ + + + +
+ + + + + \ 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..31e1550 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/excitation.html @@ -0,0 +1,1168 @@ + + + + + + + + + + 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:

+
    +
  • Fixed voltage (electrode connect to a power supply)
  • +
  • Voltage function (a generic Python function specifies the voltage as a function of position)
  • +
  • Dielectric, with arbitrary electric permittivity
  • +
  • Current coil, with fixed total amount of current (only in radial symmetry)
  • +
  • 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 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 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():
+            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+            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.
+        """
+
+        assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
+         
+        for name, current in kwargs.items():
+            assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
+            self.excitation_types[name] = (ExcitationType.CURRENT, current)
+
+    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():
+            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+            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():
+            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+            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():
+            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+            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)
+        
+        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)
+        
+        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.
+    """
+
+    assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
+     
+    for name, current in kwargs.items():
+        assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
+        self.excitation_types[name] = (ExcitationType.CURRENT, current)
+
+ +

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():
+        assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+        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)
+    
+    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():
+        assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+        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)
+    
+    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():
+        assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+        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():
+        assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
+        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/focus.html b/docs/docs/v0.9.0rc1/traceon/focus.html new file mode 100644 index 0000000..bd2915b --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/focus.html @@ -0,0 +1,200 @@ + + + + + + + + + + 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..17ef014 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/geometry.html @@ -0,0 +1,5080 @@ + + + + + + + + + + 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 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 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..6eed6e7 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/index.html @@ -0,0 +1,205 @@ + + + + + + + + + + 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.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.interpolation
+
+ +
+
+
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/interpolation.html b/docs/docs/v0.9.0rc1/traceon/interpolation.html new file mode 100644 index 0000000..7152c36 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/interpolation.html @@ -0,0 +1,633 @@ + + + + + + + + + + traceon.interpolation API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon.interpolation

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

Classes

+
+ +
+ class FieldAxial + (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldAxial(S.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 FieldRadialAxial + (field, zmin, zmax, N=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldRadialAxial(FieldAxial):
+    """ """
+    def __init__(self, field, zmin, zmax, N=None):
+        assert isinstance(field, S.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: S.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.TracerRadialAxial(self, bounds)
+
+ +
+ + +

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.TracerRadialAxial(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

+ + +
+
+
+ +
+ + + + +
+ + + + + \ 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..12acb87 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/logging.html @@ -0,0 +1,277 @@ + + + + + + + + + + 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..8839566 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/mesher.html @@ -0,0 +1,1767 @@ + + + + + + + + + + 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 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 counter clockwise 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"
+        origin = np.array(origin)
+        assert origin.shape == (3,), "Please supply a 3D point for origin"
+         
+        if Rx != 0.:
+            matrix = np.array([[1, 0, 0],
+                [0, np.cos(Rx), -np.sin(Rx)],
+                [0, np.sin(Rx), np.cos(Rx)]])
+        elif Ry != 0.:
+            matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)],
+                [0, 1, 0],
+                [-np.sin(Ry), 0, np.cos(Ry)]])
+        elif Rz != 0.:
+            matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0],
+                [np.sin(Rz), np.cos(Rz), 0],
+                [0, 0, 1]])
+
+        return self.map_points(lambda p: origin + matrix @ (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 counter clockwise 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"
+    origin = np.array(origin)
+    assert origin.shape == (3,), "Please supply a 3D point for origin"
+     
+    if Rx != 0.:
+        matrix = np.array([[1, 0, 0],
+            [0, np.cos(Rx), -np.sin(Rx)],
+            [0, np.sin(Rx), np.cos(Rx)]])
+    elif Ry != 0.:
+        matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)],
+            [0, 1, 0],
+            [-np.sin(Ry), 0, np.cos(Ry)]])
+    elif Rz != 0.:
+        matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0],
+            [np.sin(Rz), np.cos(Rz), 0],
+            [0, 0, 1]])
+
+    return self.map_points(lambda p: origin + matrix @ (p - origin))
+
+ +

Rotate counter clockwise 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.

+
+ +
+ + + +
+ +
+ 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..09d8fa2 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/plotting.html @@ -0,0 +1,840 @@ + + + + + + + + + + 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 : 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 : 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..ba26415 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/solver.html @@ -0,0 +1,2538 @@ + + + + + + + + + + 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
+    -------
+    A `FieldRadialBEM` if the geometry (contained in the given `excitation`) is radially symmetric. If the geometry is a three
+    dimensional geometry `Field3D_BEM` is returned. 
+    """
+    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 = ElectrostaticSolver(excitation).solve_matrix()[0]
+        mag_field = MagnetostaticSolver(excitation).solve_matrix()[0]
+        return elec_field + mag_field # type: ignore
+    elif elec and not mag:
+        return ElectrostaticSolver(excitation).solve_matrix()[0]
+    elif mag and not elec:
+        return MagnetostaticSolver(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

+

A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a three +dimensional geometry Field3D_BEM is returned.

+
+ + +
+ + def solve_direct_superposition(excitation) +
+
+ + + +
+ + Expand source code + +
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).
+    """
+    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([ElectrostaticSolver(excitations[n]).get_right_hand_side() for n in elec_names])
+    solutions = ElectrostaticSolver(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([MagnetostaticSolver(excitations[n]).get_right_hand_side() for n in mag_names])
+    solutions = MagnetostaticSolver(excitation).solve_matrix(right_hand_sides)
+    mag_dict = {n:s for n, s in zip(mag_names, solutions)}
+        
+    return {**elec_dict, **mag_dict}
+
+ +

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).

+
+ +
+
+ +
+

Classes

+
+ +
+ class Field +
+ +
+ + + +
+ + Expand source code + +
class Field(ABC):
+    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)
+         
+        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 magnetostatic_potential_at_point(self, point):
+        ...
+    
+    @abstractmethod
+    def electrostatic_potential_at_point(self, point):
+        ...
+    
+    @abstractmethod
+    def magnetostatic_field_at_point(self, point):
+        ...
+    
+    @abstractmethod
+    def electrostatic_field_at_point(self, point):
+        ...
+
+ +

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

+ + +

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 magnetostatic_potential_at_point(self, point) +
+
+ + + +
+ + Expand source code + +
@abstractmethod
+def magnetostatic_potential_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)
+     
+    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 Field3D_BEM + (electrostatic_point_charges=None, magnetostatic_point_charges=None) +
+ +
+ + + +
+ + Expand source code + +
class Field3D_BEM(FieldBEM):
+    """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`."""
+     
+    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
+        
+        if electrostatic_point_charges is None:
+            electrostatic_point_charges = EffectivePointCharges.empty_3d()
+        if magnetostatic_point_charges is None:
+            magnetostatic_point_charges = EffectivePointCharges.empty_3d()
+         
+        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d())
+        
+        self.symmetry = E.Symmetry.THREE_D
+
+        for eff in [electrostatic_point_charges, magnetostatic_point_charges]:
+            N = len(eff.charges)
+            assert eff.charges.shape == (N,)
+            assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD)
+            assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3)
+     
+    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 
+        """
+        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_3d(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_3d(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"
+        charges = self.magnetostatic_point_charges.charges
+        jacobians = self.magnetostatic_point_charges.jacobians
+        positions = self.magnetostatic_point_charges.positions
+        return backend.field_3d(point, charges, jacobians, positions)
+     
+    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_3d(point, charges, jacobians, positions)
+     
+    def area_of_element(self, i):
+        jacobians = self.electrostatic_point_charges.jacobians
+        return np.sum(jacobians[i])
+    
+    def get_tracer(self, bounds):
+        return T.Tracer3D_BEM(self, bounds)
+
+ +

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) +
+
+ + + +
+ + Expand source code + +
def area_of_element(self, i):
+    jacobians = self.electrostatic_point_charges.jacobians
+    return np.sum(jacobians[i])
+
+ +
+
+ + +
+ + 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 representing the electric field 
+    """
+    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_3d(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 representing the electric field

+
+ + +
+ + 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_3d(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_tracer(self, bounds) +
+
+ + + +
+ + Expand source code + +
def get_tracer(self, bounds):
+    return T.Tracer3D_BEM(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"
+    charges = self.magnetostatic_point_charges.charges
+    jacobians = self.magnetostatic_point_charges.jacobians
+    positions = self.magnetostatic_point_charges.positions
+    return backend.field_3d(point, charges, jacobians, positions)
+
+ +

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_3d(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

+ + +
+ +
+ class FieldAxial + (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldAxial(Field):
+    """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)}>'
+
+ +

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 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 + (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
+ +
+ + + +
+ + Expand source code + +
class FieldRadialAxial(FieldAxial):
+    """ """
+    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
+        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)
+        self.symmetry = E.Symmetry.RADIAL
+    
+    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"
+        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: (3,) array of float64
+            Position at which to compute the field.
+             
+        Returns
+        -------
+        (3,) array of float64, containing the field strengths (units of A/m)
+        """
+        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: (3,) 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: (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"
+        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
+    
+    def get_tracer(self, bounds):
+        return T.TracerRadialAxial(self, bounds)
+
+ +
+ + +

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: (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"
+    return backend.field_radial_derivs(point, self.z, self.electrostatic_coeffs)
+
+ +

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 (close to the axis).
+
+    Parameters
+    ----------
+    point: (3,) 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 : (3,) 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.TracerRadialAxial(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,) array of float64, containing the field strengths (units of A/m)
+    """
+    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 : (3,) array of float64
+
Position at which to compute the field.
+
+

Returns

+

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

+
+ + +
+ + 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: (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"
+    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 : (3,) 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_)
+        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(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.TracerRadialBEM(self, bounds)
+
+ +

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_)
+    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(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.TracerRadialBEM(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/tracing.html b/docs/docs/v0.9.0rc1/traceon/tracing.html new file mode 100644 index 0000000..7918ab4 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon/tracing.html @@ -0,0 +1,925 @@ + + + + + + + + + + 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
+    
+    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.
+        """
+        raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance')
+
+ +

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

+

Parameters

+
+
field : 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) ).
+
+ + + +

Subclasses

+ +

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.
+    """
+    raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance')
+
+ +

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.

+
+ +
+ + + +
+ +
+ class Tracer3DAxial + (field, bounds) +
+ +
+ + + +
+ + Expand source code + +
class Tracer3DAxial(Tracer):
+    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
+        charge_over_mass = charge / mass
+        velocity = _convert_velocity_to_SI(velocity, mass)
+        return backend.trace_particle_3d_derivs(position, velocity, charge_over_mass, self.bounds, atol,
+            self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs)
+
+ +

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

+

Parameters

+
+
field : 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) ).
+
+ + +

Ancestors

+ + + + +

Inherited members

+ + +
+ +
+ class Tracer3D_BEM + (field, bounds) +
+ +
+ + + +
+ + Expand source code + +
class Tracer3D_BEM(Tracer):
+    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
+        charge_over_mass = charge / mass
+        velocity = _convert_velocity_to_SI(velocity, mass)
+        elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges
+        return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, field_bounds=self.field.field_bounds)
+
+ +

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

+

Parameters

+
+
field : 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) ).
+
+ + +

Ancestors

+ + + + +

Inherited members

+ + +
+ +
+ class TracerRadialAxial + (field, bounds) +
+ +
+ + + +
+ + Expand source code + +
class TracerRadialAxial(Tracer):
+    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
+        charge_over_mass = charge / mass
+        velocity = _convert_velocity_to_SI(velocity, mass)
+        
+        elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs
+        
+        return backend.trace_particle_radial_derivs(position, velocity, charge_over_mass, self.bounds, atol, self.field.z, elec, mag)
+
+ +

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

+

Parameters

+
+
field : 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) ).
+
+ + +

Ancestors

+ + + + +

Inherited members

+ + +
+ +
+ class TracerRadialBEM + (field, bounds) +
+ +
+ + + +
+ + Expand source code + +
class TracerRadialBEM(Tracer):
+    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
+        charge_over_mass = charge / mass
+        velocity = _convert_velocity_to_SI(velocity, mass)
+        
+        return backend.trace_particle_radial(
+                position,
+                velocity,
+                charge_over_mass, 
+                self.bounds,
+                atol, 
+                self.field.electrostatic_point_charges,
+                self.field.magnetostatic_point_charges,
+                self.field.current_point_charges,
+                field_bounds=self.field.field_bounds)
+
+ +

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

+

Parameters

+
+
field : 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) ).
+
+ + +

Ancestors

+ + + + +

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..b1b4c85 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/index.html @@ -0,0 +1,146 @@ + + + + + + + + + + traceon_pro API documentation + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html b/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html new file mode 100644 index 0000000..da3ecce --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html @@ -0,0 +1,290 @@ + + + + + + + + + + traceon_pro.interpolation API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.interpolation

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

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_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

+ + +
+
+
+ +
+ + + + +
+ + + + + \ 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..c2b3ae3 --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/solver.html @@ -0,0 +1,221 @@ + + + + + + + + + + traceon_pro.solver API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.solver

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

Functions

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

Classes

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

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

+ + +

Ancestors

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

Methods

+
+ +
+ + def solve_fmm(self, N_max=256, l_max=12) +
+
+ + + +
+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ 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..85348fc --- /dev/null +++ b/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html @@ -0,0 +1,247 @@ + + + + + + + + + + traceon_pro.traceon_pro API documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

Module traceon_pro.traceon_pro

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

Classes

+
+ +
+ 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) +
+
+ + + +
+
+ +
+ + + +
+
+
+ +
+ + + + +
+ + + + + \ No newline at end of file From 4fff3bf16c44f96526e5158b9716110a484558f7 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Sat, 21 Dec 2024 14:44:27 +0100 Subject: [PATCH 39/85] implemented general axis rotation for GeometricObject --- traceon/mesher.py | 66 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/traceon/mesher.py b/traceon/mesher.py index 8ea0b68..cb105e2 100644 --- a/traceon/mesher.py +++ b/traceon/mesher.py @@ -64,7 +64,7 @@ def move(self, dx=0., dy=0., dz=0.): 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 counter clockwise around the x, y or z axis. Only one axis supported at the same time + """Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time (rotations do not commute). Parameters @@ -85,23 +85,55 @@ def rotate(self, Rx=0., Ry=0., Rz=0., origin=[0., 0., 0.]): 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" - origin = np.array(origin) + + 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. + If the normal points away, it rotates clockwise. + + 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" - - if Rx != 0.: - matrix = np.array([[1, 0, 0], - [0, np.cos(Rx), -np.sin(Rx)], - [0, np.sin(Rx), np.cos(Rx)]]) - elif Ry != 0.: - matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)], - [0, 1, 0], - [-np.sin(Ry), 0, np.cos(Ry)]]) - elif Rz != 0.: - matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0], - [np.sin(Rz), np.cos(Rz), 0], - [0, 0, 1]]) - - return self.map_points(lambda p: origin + matrix @ (p - 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 + + K = np.array([ + [0, -axis[2], axis[1]], + [axis[2], 0, -axis[0]], + [-axis[1], axis[0], 0]]) + + K2 = K @ K + 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. From 89685d07cb9ce43621d48deb0a7f382d5f86a895 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Sat, 21 Dec 2024 14:44:41 +0100 Subject: [PATCH 40/85] unittests for general rotation --- tests/geometric_tests.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/geometric_tests.py b/tests/geometric_tests.py index ae61ef7..788280e 100644 --- a/tests/geometric_tests.py +++ b/tests/geometric_tests.py @@ -7,6 +7,7 @@ from traceon.geometry import * import traceon.plotting as P + class MeshTests(unittest.TestCase): def test_loading_mesh(self): @@ -94,6 +95,23 @@ def test_rotate_around_point(self): assert np.allclose(y(sqrt(0)), origin) assert np.allclose(y(sqrt(2)), origin + np.array([0., 1., 1.])) + def test_rotate_around_axis(self): + p = Path.line([0,0,0], [1,1,1]) + origin=[0,0,0] + angle = pi/2 + p_rot_par = p.rotate_around_axis([1,1,1], angle, origin) + assert np.allclose(p_rot_par.starting_point(), origin) + assert np.allclose(p_rot_par.endpoint(), np.array([1,1,1])) + p_rot_ort = p.rotate_around_axis([0,1,-1], angle, origin) + assert np.allclose(p_rot_ort.starting_point(), origin) + assert np.allclose(p_rot_ort.endpoint(), np.array([sqrt(2), -1/sqrt(2), -1/sqrt(2)])) + + origin = [2,1,-1] + p_rot_gen = p.rotate_around_axis([1,1,0], -np.pi/2, [2,1,-1]) + print(p_rot_gen.starting_point()) + assert np.allclose(p_rot_gen.starting_point(), np.array([-0.207107,0.207107,-1.707107])) + assert np.allclose(p_rot_gen.endpoint(), np.array([0.085786,1.914214,-1.707107])) + def test_discretize_path(self): path_length = 10 breakpoints = [3.33, 5., 9.] From 1edffbf647a4c5b60f9a3cee4b36e300ef583be8 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Sat, 21 Dec 2024 19:02:24 +0100 Subject: [PATCH 41/85] Removed some redundancy in the docstring. added few explanory comments --- traceon/mesher.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/traceon/mesher.py b/traceon/mesher.py index cb105e2..2210b58 100644 --- a/traceon/mesher.py +++ b/traceon/mesher.py @@ -98,7 +98,6 @@ def rotate(self, Rx=0., Ry=0., Rz=0., origin=[0., 0., 0.]): 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. - If the normal points away, it rotates clockwise. Parameters ------------------------------------ @@ -122,14 +121,16 @@ def rotate_around_axis(self, axis=[0., 0, 1.], angle=0., origin=[0., 0., 0.]): axis = axis / np.linalg.norm(axis) if angle == 0: - return self + 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 + 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 From 74d2d701ac1a89167787490216791371f2124a79 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Sat, 21 Dec 2024 19:03:44 +0100 Subject: [PATCH 42/85] removed print. added few explanotory statements and trivial unittest --- tests/geometric_tests.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/geometric_tests.py b/tests/geometric_tests.py index ae61ef7..03f0637 100644 --- a/tests/geometric_tests.py +++ b/tests/geometric_tests.py @@ -7,6 +7,7 @@ from traceon.geometry import * import traceon.plotting as P + class MeshTests(unittest.TestCase): def test_loading_mesh(self): @@ -94,6 +95,33 @@ def test_rotate_around_point(self): assert np.allclose(y(sqrt(0)), origin) assert np.allclose(y(sqrt(2)), origin + np.array([0., 1., 1.])) + def test_rotate_around_axis(self): + + p = Path.line([0,0,0], [1,1,1]) + origin=[0,0,0] + angle = pi/2 + p_rot_par = p.rotate_around_axis([1,1,1], angle, origin) + assert np.allclose(p_rot_par.starting_point(), origin) + assert np.allclose(p_rot_par.endpoint(), np.array([1,1,1])) + + p_rot_ort = p.rotate_around_axis([0,1,-1], angle, origin) + assert np.allclose(p_rot_ort.starting_point(), origin) + assert np.allclose(p_rot_ort.endpoint(), np.array([sqrt(2), -1/sqrt(2), -1/sqrt(2)])) + + # general example calculated by hand using Rodrigues' formula as follows: + # Translate v to v' = v - origin. + # Rotate around [0,0,0] using v_rot' = v'cos(theta) + (k \cross v')sin(theta) + (k(k\dot v))(1-cos(theta)) + # Translate back to v_rot using v_rot = origin + v_rot' + origin = [2,1,-1] + p_rot_gen = p.rotate_around_axis([1,1,0], -pi/2, [2,1,-1]) + assert np.allclose(p_rot_gen.starting_point(), np.array([-0.207107,0.207107,-1.707107])) + assert np.allclose(p_rot_gen.endpoint(), np.array([0.085786, 1.914214, -1.707107])) + + # 2pi rotation should map object to itself + p_rot_2pi = p.rotate_around_axis([1,1,0], 2*pi, [2,1,-1]) + assert np.allclose(p.starting_point(), p_rot_2pi.starting_point()) + assert np.allclose(p.endpoint(), p_rot_2pi.endpoint()) + def test_discretize_path(self): path_length = 10 breakpoints = [3.33, 5., 9.] From 66563817361bf859136f5c723fd0e3cc8fee8caf Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Sat, 21 Dec 2024 19:09:43 +0100 Subject: [PATCH 43/85] added annulus to surface. --- traceon/geometry.py | 69 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/traceon/geometry.py b/traceon/geometry.py index 999bd06..b614293 100644 --- a/traceon/geometry.py +++ b/traceon/geometry.py @@ -1296,6 +1296,75 @@ def rectangle_xy(xmin, xmax, ymin, ymax): ----------------------- 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.): From 2b69be692cda833241447f2c00e89f0b625d554a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 21 Dec 2024 14:41:47 +0100 Subject: [PATCH 44/85] Merge branch develop and remove 3D solver Merge branch develop into line-currents-3d Move field classes from solver,interpolation to traceon.field Split solver classes in radial and 3D Start moving 3D solver (and tests) to pro Import from traceon_pro.field instead of traceon_pro.interpolation Remove fill_matrix_3d function (moved to pro) --- .github/workflows/type-checking.yaml | 3 + Makefile | 2 +- README.md | 34 +- docs/docs/latest/excitation.html | 812 +--- docs/docs/latest/focus.html | 138 +- docs/docs/latest/geometry.html | 3931 +++++++++++--------- docs/docs/latest/index.html | 105 +- docs/docs/latest/interpolation.html | 374 ++ docs/docs/latest/logging.html | 148 + docs/docs/latest/mesher.html | 933 +++++ docs/docs/latest/plotting.html | 222 +- docs/docs/latest/solver.html | 2848 ++------------ docs/docs/latest/tracing.html | 889 ++--- docs/docs/v0.7.3/excitation.html | 662 ++++ docs/docs/v0.7.3/focus.html | 81 + docs/docs/v0.7.3/geometry.html | 2599 +++++++++++++ docs/docs/v0.7.3/index.html | 139 + docs/docs/v0.7.3/interpolation.html | 374 ++ docs/docs/v0.7.3/logging.html | 148 + docs/docs/v0.7.3/mesher.html | 933 +++++ docs/docs/v0.7.3/plotting.html | 83 + docs/docs/v0.7.3/solver.html | 1397 +++++++ docs/docs/v0.7.3/tracing.html | 494 +++ docs/docs/v0.8.0/excitation.html | 670 ++++ docs/docs/v0.8.0/focus.html | 81 + docs/docs/v0.8.0/geometry.html | 2859 ++++++++++++++ docs/docs/v0.8.0/index.html | 139 + docs/docs/v0.8.0/interpolation.html | 374 ++ docs/docs/v0.8.0/logging.html | 148 + docs/docs/v0.8.0/mesher.html | 1037 ++++++ docs/docs/v0.8.0/plotting.html | 409 ++ docs/docs/v0.8.0/solver.html | 1397 +++++++ docs/docs/v0.8.0/tracing.html | 504 +++ examples/deflector-3d.py | 4 +- examples/dohi-mirror.py | 4 +- examples/einzel-lens.py | 4 +- setup.py | 2 +- tests/geometric_tests.py | 40 +- tests/test_radial.py | 16 +- tests/test_radial_ring.py | 16 +- tests/test_three_d.py | 354 -- tests/test_tracing.py | 119 +- tests/test_triangle.py | 386 -- tests/test_utilities_2d.py | 29 +- tests/test_utilities_3d.py | 7 - traceon/backend/__init__.py | 300 +- traceon/backend/radial.c | 26 +- traceon/backend/radial_ring.c | 27 +- traceon/backend/tracing.c | 260 +- traceon/backend/triangle.c | 179 - traceon/backend/utilities_2d.c | 21 + traceon/backend/utilities_3d.c | 29 - traceon/field.py | 785 ++++ traceon/geometry.py | 202 +- traceon/interpolation.py | 204 - traceon/solver.py | 1039 +----- traceon/tracing.py | 51 +- validation/capacitance_sphere.py | 2 +- validation/dohi.py | 6 +- validation/edwards2007.py | 4 +- validation/einzel_lens.py | 6 +- validation/magnetic_einzel_lens.py | 6 +- validation/rectangular_coil.py | 2 +- validation/rectangular_coil_with_circle.py | 2 +- validation/simple_mirror.py | 6 +- validation/spherical_capacitor.py | 2 +- validation/two_current_coils.py | 2 +- validation/two_cylinder_edwards.py | 4 +- 68 files changed, 20545 insertions(+), 8568 deletions(-) create mode 100644 docs/docs/latest/interpolation.html create mode 100644 docs/docs/latest/logging.html create mode 100644 docs/docs/latest/mesher.html create mode 100644 docs/docs/v0.7.3/excitation.html create mode 100644 docs/docs/v0.7.3/focus.html create mode 100644 docs/docs/v0.7.3/geometry.html create mode 100644 docs/docs/v0.7.3/index.html create mode 100644 docs/docs/v0.7.3/interpolation.html create mode 100644 docs/docs/v0.7.3/logging.html create mode 100644 docs/docs/v0.7.3/mesher.html create mode 100644 docs/docs/v0.7.3/plotting.html create mode 100644 docs/docs/v0.7.3/solver.html create mode 100644 docs/docs/v0.7.3/tracing.html create mode 100644 docs/docs/v0.8.0/excitation.html create mode 100644 docs/docs/v0.8.0/focus.html create mode 100644 docs/docs/v0.8.0/geometry.html create mode 100644 docs/docs/v0.8.0/index.html create mode 100644 docs/docs/v0.8.0/interpolation.html create mode 100644 docs/docs/v0.8.0/logging.html create mode 100644 docs/docs/v0.8.0/mesher.html create mode 100644 docs/docs/v0.8.0/plotting.html create mode 100644 docs/docs/v0.8.0/solver.html create mode 100644 docs/docs/v0.8.0/tracing.html delete mode 100644 tests/test_three_d.py delete mode 100644 tests/test_triangle.py create mode 100644 traceon/field.py delete mode 100644 traceon/interpolation.py diff --git a/.github/workflows/type-checking.yaml b/.github/workflows/type-checking.yaml index 47dd9d9..49b58e2 100644 --- a/.github/workflows/type-checking.yaml +++ b/.github/workflows/type-checking.yaml @@ -5,9 +5,12 @@ on: pull_request: branches: - main + - develop push: branches: - main + - develop + workflow_dispatch: jobs: type-check: diff --git a/Makefile b/Makefile index 1023a3b..b88e0c3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: clean bdist -traceon/backend/traceon_backend.so: traceon/backend/traceon-backend.c +traceon/backend/traceon_backend.so: traceon/backend/*.c clang -O3 -march=native -shared -fPIC -ffast-math -Wno-extern-initializer ./traceon/backend/traceon-backend.c -o traceon/backend/traceon_backend.so -lm clean: diff --git a/README.md b/README.md index 6084a9c..db6f871 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ The core of Traceon is completely free to use and open source. There is a commer [Examples](https://github.com/leon-vv/Traceon/tree/main/examples) -[API documentation v0.7.0](https://traceon.org/docs/v0.7.0/index.html) +[API documentation v0.8.0](https://traceon.org/docs/v0.8.0/index.html) + +[API documentation v0.7.3](https://traceon.org/docs/v0.7.3/index.html) [API documentation v0.6.0](https://traceon.org/docs/v0.6.0/index.html) @@ -19,7 +21,7 @@ The core of Traceon is completely free to use and open source. There is a commer Please cite the software as follows: ``` -L.B. van Velzen. Traceon software (version 0.7.0). 2024. https://doi.org/10.5281/zenodo.14176070 +L.B. van Velzen. Traceon software (version 0.8.0). 2024. https://doi.org/10.5281/zenodo.14500008 ``` ## Installation @@ -59,9 +61,21 @@ Don't worry. You can reach me. ![Image of Dohi mirror](https://raw.githubusercontent.com/leon-vv/traceon/main/images/dohi-mirror.png) ![Image of Einzel lens traces](https://raw.githubusercontent.com/leon-vv/traceon/main/images/einzel-lens-traces.png) -## Features +## Release notes + +### v0.8.0 +- New plotting module (charge density, equipotential lines) +- Automatic orientation of normal vectors +- Geometry functions for extruding/revolving edges of surfaces +- Tracing of particles other than electrons (charge, mass as input) +- Various bug fixes and improvements + +**Breaking changes**: +- Call `P.show()` after `P.plot_mesh()` to show figures +- Normal vectors should be oriented automatically (please check if this works correctly for your geometry) +
-v0.7.0: +### v0.7.0 - Generate structured, high quality meshes using the new parametric mesher (drop GMSH) - Consistenly use 3D points and geometries throughout codebase - Add support for Fast Multipole Method (Traceon Pro) @@ -69,7 +83,7 @@ v0.7.0: - Big improvements to code quality, testing, infrastructure - Drop dependency on GSL -v0.6.0: +### v0.6.0: - New methods to integrate triangle potential and field contribution over a triangle - Fix 3D convergence issues by more accurately calculating neighbouring triangle interactions - Fix error calculation in particle tracing @@ -77,18 +91,18 @@ v0.6.0: - Clean up unit tests - Remove higher order (curved) triangle support, in preparation of parametric meshers and improved FFM implementation -v0.5.0: +### v0.5.0: - Add preliminary support for magnetostatics - Improve and generalize mesh class (allow import/export) - Make consistent use of SI units -v0.4.0: +### v0.4.0: - Introduce Fast Multipole Method (FMM) for large 3D problems - Compute 3D radial expansion coefficients using analytical formulas - Further speed up computation of 3D radial expansion coefficients - Big code quality improvement of validation/ files -v0.3.0: +### v0.3.0: - Use adaptive integration using GNU Scientific Library (GSL) - Add support for boundary constraint - Use [Vedo](https://vedo.embl.es/) for better plotting capabilities @@ -96,12 +110,12 @@ v0.3.0: - Precompute jacobians/positions for better performance - First implementation of element splitting based on charges (work in progress) -v0.2.0: +### v0.2.0: - Use higher order charge distribution on line elements in radial symmetry - Use higher order line elements (polynomials) in radial symmetry - Better integration techniques, especially with regards to the logarithmic singularities -v0.1.0: +### v0.1.0: - Uses the powerful [GMSH library](https://gmsh.info/) for meshing - Solve for surface charge distribution using BEM - General 3D geometries and radially symmetric geometries diff --git a/docs/docs/latest/excitation.html b/docs/docs/latest/excitation.html index f19454b..775fe97 100644 --- a/docs/docs/latest/excitation.html +++ b/docs/docs/latest/excitation.html @@ -2,20 +2,24 @@ - - + + traceon.excitation API documentation - - - - - +created with the …"> + + + + + + - - + +
@@ -36,342 +40,7 @@

Module traceon.excitation

  • 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 solve_bem() to compute the resulting field.

    -
    - -Expand source code - -
    """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:
    -
    -- Fixed voltage (electrode connect to a power supply)
    -- Voltage function (a generic Python function specifies the voltage as a function of position)
    -- Dielectric, with arbitrary electric permittivity
    -- Current coil, with fixed total amount of current (only in radial symmetry)
    -- 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_bem` to compute the resulting field.
    -"""
    -
    -
    -from enum import IntEnum
    -
    -import numpy as np
    -
    -from .geometry import Symmetry
    -from .backend import N_QUAD_2D
    -
    -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')
    -     
    -
    -class Excitation:
    -    """ """
    -     
    -    def __init__(self, mesh):
    -        self.mesh = mesh
    -        self.electrodes = mesh.get_electrodes()
    -        self.excitation_types = {}
    -    
    -    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 add_voltage(self, **kwargs):
    -        """
    -        Apply a fixed voltage to the geometries assigned the given name (or physical group in GMSH terminology).
    -        
    -        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():
    -            assert name in self.electrodes
    -            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 (or physical group in GMSH terminology). 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.
    -        """
    -
    -        assert self.mesh.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
    -         
    -        for name, current in kwargs.items():
    -            assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
    -            self.excitation_types[name] = (ExcitationType.CURRENT, current)
    -
    -    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 (or physical group in GMSH terminology).
    -        
    -        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():
    -            assert name in self.electrodes
    -            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 (or physical group in GMSH terminology).
    -        
    -        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():
    -            assert name in self.electrodes
    -            self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability)
    -     
    -    def add_dielectric(self, **kwargs):
    -        """
    -        Assign a dielectric constant to the geometries assigned the given name (or physical group in GMSH terminology).
    -        
    -        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():
    -            assert name in self.electrodes
    -            self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity)
    -
    -    def add_electrostatic_boundary(self, *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.
    -        """
    -        self.add_dielectric(**{a:0 for a in args})
    -    
    -    def add_magnetostatic_boundary(self, *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. 
    -        
    -        Parameters
    -        ----------
    -        *args: list of str
    -            The geometry names that should be considered a boundary.
    -        """
    -
    -        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)
    -            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.mesh.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_number_of_active_elements(self, type_):
    -        assert type_ in ['electrostatic', 'magnetostatic']
    -         
    -        if self.mesh.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.MAGNETOSTATIC_POT, ExcitationType.MAGNETIZABLE]
    -         
    -        return sum(len(physicals[n]) for n, v in self.excitation_types.items() if type_check(v[0]))
    -    
    -    def get_electrostatic_active_elements(self):
    -        """Get elements in the mesh that are active, in the sense that
    -        an excitation to them has been applied. 
    -    
    -        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 are active, in the sense that an excitation to them has been applied. 
    -    
    -        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')
    -     
    -    def get_number_of_electrostatic_active_elements(self):
    -        """Get elements in the mesh that are active, in the sense that
    -        an excitation to them has been applied. This is the length of the points
    -        array returned by the `Excitation.get_electrostatic_active_elements`.
    -
    -        Returns
    -        --------
    -        int, giving the number of elements. """
    -        return self._get_number_of_active_elements('electrostatic')
    -    
    -    def get_number_of_electrostatic_matrix_elements(self):
    -        """Gets the number of elements along one axis of the matrix. If this function returns N, the
    -        matrix will have size NxN. The matrix consists of 64bit float values. Therefore the size of the matrix
    -        in bytes is 8·NxN.
    -
    -        Returns
    -        ---------
    -        integer number
    -        """
    -        return self._get_number_of_active_elements('magnetostatic')
    -
    -        
    -
    +

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

    @@ -384,7 +53,7 @@

    Classes

    class Excitation -(mesh) +(mesh, symmetry)
    @@ -395,10 +64,15 @@

    Classes

    class Excitation:
         """ """
          
    -    def __init__(self, mesh):
    +    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' \
    @@ -407,7 +81,7 @@ 

    Classes

    def add_voltage(self, **kwargs): """ - Apply a fixed voltage to the geometries assigned the given name (or physical group in GMSH terminology). + Apply a fixed voltage to the geometries assigned the given name. Parameters ---------- @@ -419,7 +93,7 @@

    Classes

    """ for name, voltage in kwargs.items(): - assert name in self.electrodes + assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' if isinstance(voltage, int) or isinstance(voltage, float): self.excitation_types[name] = (ExcitationType.VOLTAGE_FIXED, voltage) elif callable(voltage): @@ -429,7 +103,7 @@

    Classes

    def add_current(self, **kwargs): """ - Apply a fixed total current to the geometries assigned the given name (or physical group in GMSH terminology). Note that a coil is assumed, + 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). @@ -440,7 +114,7 @@

    Classes

    calling the function as `add_current(coild=10)` assigns a 10A value to the geometry elements part of the 'coil' physical group. """ - assert self.mesh.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes" + assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes" for name, current in kwargs.items(): assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode" @@ -460,7 +134,7 @@

    Classes

    def add_magnetostatic_potential(self, **kwargs): """ - Apply a fixed magnetostatic potential to the geometries assigned the given name (or physical group in GMSH terminology). + Apply a fixed magnetostatic potential to the geometries assigned the given name. Parameters ---------- @@ -469,12 +143,12 @@

    Classes

    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(): - assert name in self.electrodes + assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' 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 (or physical group in GMSH terminology). + Assign a relative magnetic permeability to the geometries assigned the given name. Parameters ---------- @@ -485,12 +159,12 @@

    Classes

    """ for name, permeability in kwargs.items(): - assert name in self.electrodes + assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability) def add_dielectric(self, **kwargs): """ - Assign a dielectric constant to the geometries assigned the given name (or physical group in GMSH terminology). + Assign a dielectric constant to the geometries assigned the given name. Parameters ---------- @@ -500,7 +174,7 @@

    Classes

    """ for name, permittivity in kwargs.items(): - assert name in self.electrodes + assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity) def add_electrostatic_boundary(self, *args): @@ -521,7 +195,8 @@

    Classes

    """ 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. + 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 ---------- @@ -556,7 +231,7 @@

    Classes

    else: new_types_dict[n] = (t, v) - exc = Excitation(self.mesh) + exc = Excitation(self.mesh, self.symmetry) exc.excitation_types = new_types_dict excitations.append(exc) @@ -566,7 +241,7 @@

    Classes

    def _get_active_elements(self, type_): assert type_ in ['electrostatic', 'magnetostatic'] - if self.mesh.symmetry == Symmetry.RADIAL: + if self.symmetry == Symmetry.RADIAL: elements = self.mesh.lines physicals = self.mesh.physical_to_lines else: @@ -589,29 +264,11 @@

    Classes

    if n in self.excitation_types and type_check(self.excitation_types[n][0])} return self.mesh.points[ elements[~inactive] ], names - - def _get_number_of_active_elements(self, type_): - assert type_ in ['electrostatic', 'magnetostatic'] - - if self.mesh.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.MAGNETOSTATIC_POT, ExcitationType.MAGNETIZABLE] - - return sum(len(physicals[n]) for n, v in self.excitation_types.items() if type_check(v[0])) - + def get_electrostatic_active_elements(self): - """Get elements in the mesh that are active, in the sense that - an excitation to them has been applied. - + """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. \ @@ -624,7 +281,8 @@

    Classes

    return self._get_active_elements('electrostatic') def get_magnetostatic_active_elements(self): - """Get elements in the mesh that are active, in the sense that an excitation to them has been applied. + """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 -------- @@ -636,28 +294,7 @@

    Classes

    while the values are Numpy arrays of indices that can be used to index the points array. """ - return self._get_active_elements('magnetostatic') - - def get_number_of_electrostatic_active_elements(self): - """Get elements in the mesh that are active, in the sense that - an excitation to them has been applied. This is the length of the points - array returned by the `Excitation.get_electrostatic_active_elements`. - - Returns - -------- - int, giving the number of elements. """ - return self._get_number_of_active_elements('electrostatic') - - def get_number_of_electrostatic_matrix_elements(self): - """Gets the number of elements along one axis of the matrix. If this function returns N, the - matrix will have size NxN. The matrix consists of 64bit float values. Therefore the size of the matrix - in bytes is 8·NxN. - - Returns - --------- - integer number - """ - return self._get_number_of_active_elements('magnetostatic')
    + return self._get_active_elements('magnetostatic')

    Methods

    @@ -665,7 +302,7 @@

    Methods

    def add_current(self, **kwargs)
    -

    Apply a fixed total current to the geometries assigned the given name (or physical group in GMSH terminology). Note that a coil is assumed, +

    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

    @@ -674,60 +311,18 @@

    Parameters

    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.
    -
    - -Expand source code - -
    def add_current(self, **kwargs):
    -    """
    -    Apply a fixed total current to the geometries assigned the given name (or physical group in GMSH terminology). 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.
    -    """
    -
    -    assert self.mesh.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
    -     
    -    for name, current in kwargs.items():
    -        assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
    -        self.excitation_types[name] = (ExcitationType.CURRENT, current)
    -
    def add_dielectric(self, **kwargs)
    -

    Assign a dielectric constant to the geometries assigned the given name (or physical group in GMSH terminology).

    +

    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.
    -
    - -Expand source code - -
    def add_dielectric(self, **kwargs):
    -    """
    -    Assign a dielectric constant to the geometries assigned the given name (or physical group in GMSH terminology).
    -    
    -    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():
    -        assert name in self.electrodes
    -        self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity)
    -
    def add_electrostatic_boundary(self, *args) @@ -742,56 +337,18 @@

    Parameters

    *args : list of str
    The geometry names that should be considered a boundary.
    -
    - -Expand source code - -
    def add_electrostatic_boundary(self, *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.
    -    """
    -    self.add_dielectric(**{a:0 for a in args})
    -
    def add_magnetizable(self, **kwargs)
    -

    Assign a relative magnetic permeability to the geometries assigned the given name (or physical group in GMSH terminology).

    +

    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.
    -
    - -Expand source code - -
    def add_magnetizable(self, **kwargs):
    -    """
    -    Assign a relative magnetic permeability to the geometries assigned the given name (or physical group in GMSH terminology).
    -    
    -    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():
    -        assert name in self.electrodes
    -        self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability)
    -
    def add_magnetostatic_boundary(self, *args) @@ -799,66 +356,31 @@

    Parameters

    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.

    +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.
    -
    - -Expand source code - -
    def add_magnetostatic_boundary(self, *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. 
    -    
    -    Parameters
    -    ----------
    -    *args: list of str
    -        The geometry names that should be considered a boundary.
    -    """
    -
    -    self.add_magnetizable(**{a:0 for a in args})
    -
    def add_magnetostatic_potential(self, **kwargs)
    -

    Apply a fixed magnetostatic potential to the geometries assigned the given name (or physical group in GMSH terminology).

    +

    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.
    -
    - -Expand source code - -
    def add_magnetostatic_potential(self, **kwargs):
    -    """
    -    Apply a fixed magnetostatic potential to the geometries assigned the given name (or physical group in GMSH terminology).
    -    
    -    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():
    -        assert name in self.electrodes
    -        self.excitation_types[name] = (ExcitationType.MAGNETOSTATIC_POT, pot)
    -
    def add_voltage(self, **kwargs)
    -

    Apply a fixed voltage to the geometries assigned the given name (or physical group in GMSH terminology).

    +

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

    Parameters

    **kwargs : dict
    @@ -867,39 +389,13 @@

    Parameters

    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.
    -
    - -Expand source code - -
    def add_voltage(self, **kwargs):
    -    """
    -    Apply a fixed voltage to the geometries assigned the given name (or physical group in GMSH terminology).
    -    
    -    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():
    -        assert name in self.electrodes
    -        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 get_electrostatic_active_elements(self)
    -

    Get elements in the mesh that are active, in the sense that -an excitation to them has been applied.

    +

    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. @@ -907,31 +403,13 @@

    Returns

    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.

    -
    - -Expand source code - -
    def get_electrostatic_active_elements(self):
    -    """Get elements in the mesh that are active, in the sense that
    -    an excitation to them has been applied. 
    -
    -    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 are active, in the sense that an excitation to them has been applied.

    +

    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. @@ -939,119 +417,24 @@

    Returns

    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.

    -
    - -Expand source code - -
    def get_magnetostatic_active_elements(self):
    -    """Get elements in the mesh that are active, in the sense that an excitation to them has been applied. 
    -
    -    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')
    -
    -
    -
    -def get_number_of_electrostatic_active_elements(self) -
    -
    -

    Get elements in the mesh that are active, in the sense that -an excitation to them has been applied. This is the length of the points -array returned by the Excitation.get_electrostatic_active_elements().

    -

    Returns

    -

    int, giving the number of elements.

    -
    - -Expand source code - -
    def get_number_of_electrostatic_active_elements(self):
    -    """Get elements in the mesh that are active, in the sense that
    -    an excitation to them has been applied. This is the length of the points
    -    array returned by the `Excitation.get_electrostatic_active_elements`.
    -
    -    Returns
    -    --------
    -    int, giving the number of elements. """
    -    return self._get_number_of_active_elements('electrostatic')
    -
    -
    -
    -def get_number_of_electrostatic_matrix_elements(self) -
    -
    -

    Gets the number of elements along one axis of the matrix. If this function returns N, the -matrix will have size NxN. The matrix consists of 64bit float values. Therefore the size of the matrix -in bytes is 8·NxN.

    -

    Returns

    -
    -
    integer number
    -
     
    -
    -
    - -Expand source code - -
    def get_number_of_electrostatic_matrix_elements(self):
    -    """Gets the number of elements along one axis of the matrix. If this function returns N, the
    -    matrix will have size NxN. The matrix consists of 64bit float values. Therefore the size of the matrix
    -    in bytes is 8·NxN.
    -
    -    Returns
    -    ---------
    -    integer number
    -    """
    -    return self._get_number_of_active_elements('magnetostatic')
    -
    def has_current(self)

    Check whether a current is applied in this excitation.

    -
    - -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()])
    -
    def is_electrostatic(self)

    Check whether the excitation contains electrostatic fields.

    -
    - -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()])
    -
    def is_magnetostatic(self)

    Check whether the excitation contains magnetostatic fields.

    -
    - -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()])
    -
    @@ -1141,30 +524,75 @@

    Methods

    -
    - -Expand source code - -
    def is_electrostatic(self):
    -    return self in [ExcitationType.VOLTAGE_FIXED,
    -                    ExcitationType.VOLTAGE_FUN,
    -                    ExcitationType.DIELECTRIC]
    -
    def is_magnetostatic(self)
    +
    + + +
    +class Symmetry +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    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.

    Expand source code -
    def is_magnetostatic(self):
    -    return self in [ExcitationType.MAGNETOSTATIC_POT,
    -                    ExcitationType.MAGNETIZABLE,
    -                    ExcitationType.CURRENT]
    +
    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
    +

    Ancestors

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

    Class variables

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

    Methods

    +
    +
    +def is_2d(self) +
    +
    +
    +
    +
    +def is_3d(self) +
    +
    +
    @@ -1172,7 +600,6 @@

    Methods

    - \ No newline at end of file + diff --git a/docs/docs/latest/focus.html b/docs/docs/latest/focus.html index 4f59217..32981b8 100644 --- a/docs/docs/latest/focus.html +++ b/docs/docs/latest/focus.html @@ -2,19 +2,23 @@ - - + + traceon.focus API documentation - - - - - - + + + + + + + - - + +
    @@ -24,62 +28,6 @@

    Module traceon.focus

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

    -
    - -Expand source code - -
    """
    -Module containing a single function to find the focus of a beam of electron trajecories.
    -"""
    -
    -import numpy as np
    -
    -
    -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,4) or (N,6) np.ndarray float64
    -        Trajectories of electrons, as returned by `traceon.tracing.Tracer.__call__`
    -    
    -    
    -    Returns
    -    --------------
    -    A tuple of size two or three, depending on whether the input positions are 2D or 3D trajectories. The \
    -    returned position is the (r, z) or (x, y, z) position of the focus.
    -
    -    """
    -    two_d = positions[0].shape[1] == 4
    -    
    -    # Also for 2D, extend to 3D
    -    if two_d:
    -        positions = [np.column_stack(
    -            (p[:, 0], np.zeros(len(p)), p[:, 1], p[:, 2], np.zeros(len(p)), p[:, 3])) for p in positions]
    -      
    -    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 (x, y, z) if not two_d else (x, z)
    -    
    -
    @@ -97,60 +45,11 @@

    Functions

    the given trajectories and linearly extending the trajectories backwards.

    Parameters

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

    Returns

    -

    A tuple of size two or three, depending on whether the input positions are 2D or 3D trajectories. The -returned position is the (r, z) or (x, y, z) position of the focus.

    -
    - -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,4) or (N,6) np.ndarray float64
    -        Trajectories of electrons, as returned by `traceon.tracing.Tracer.__call__`
    -    
    -    
    -    Returns
    -    --------------
    -    A tuple of size two or three, depending on whether the input positions are 2D or 3D trajectories. The \
    -    returned position is the (r, z) or (x, y, z) position of the focus.
    -
    -    """
    -    two_d = positions[0].shape[1] == 4
    -    
    -    # Also for 2D, extend to 3D
    -    if two_d:
    -        positions = [np.column_stack(
    -            (p[:, 0], np.zeros(len(p)), p[:, 1], p[:, 2], np.zeros(len(p)), p[:, 3])) for p in positions]
    -      
    -    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 (x, y, z) if not two_d else (x, z)
    -
    +

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

    @@ -158,7 +57,6 @@

    Returns

    - \ No newline at end of file + diff --git a/docs/docs/latest/geometry.html b/docs/docs/latest/geometry.html index ab592db..caf7b71 100644 --- a/docs/docs/latest/geometry.html +++ b/docs/docs/latest/geometry.html @@ -2,20 +2,25 @@ - - + + traceon.geometry API documentation - - - - - - + + + + + + + - - + +
    @@ -24,2011 +29,2470 @@

    Module traceon.geometry

    -

    The geometry module allows the creation of general geometries in 2D and 3D and generate -the resulting meshes. The heavy lifting is done by the powerful GMSH library, and we access this library -through the convenient pygmsh library.

    -

    The GMSH library has the concept of physical groups. These are simply elements inside your geometry which -are assigned a given name. When using Traceon, usually every electrode gets assigned its -own name (or physical group) with which it can be referenced when later specifying the excitation -of this electrode.

    -

    From this module you will likely use either the Geometry class when creating arbitrary geometries, -or the MEMSStack class, if your geometry consists of a stack of MEMS fabricated elements.

    +

    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) +
    +
    +

    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.

    Expand source code -
    """The geometry module allows the creation of general geometries in 2D and 3D and generate
    -the resulting meshes. The heavy lifting is done by the powerful [GMSH](https://gmsh.info/) library, and we access this library
    -through the convenient [pygmsh](https://github.com/nschloe/pygmsh) library.
    -
    -The GMSH library has the concept of _physical groups_. These are simply elements inside your geometry which
    -are assigned a given name. When using Traceon, usually every electrode gets assigned its
    -own name (or physical group) with which it can be referenced when later specifying the excitation
    -of this electrode.
    -
    -From this module you will likely use either the `Geometry` class when creating arbitrary geometries,
    -or the `MEMSStack` class, if your geometry consists of a stack of MEMS fabricated elements.
    -"""
    -from math import sqrt
    -from enum import Enum
    -import pickle
    -
    -import numpy as np
    -from pygmsh import *
    -import gmsh
    -import meshio
    -
    -from .util import Saveable
    -from .backend import N_QUAD_2D, position_and_jacobian_radial, position_and_jacobian_3d, normal_3d
    -
    -def revolve_around_optical_axis(geom, elements, factor=1.0):
    -    """
    -    Revolve geometry elements around the optical axis. Useful when you
    -    want to generate 3D geometries from a cylindrically symmetric 2D geometry.
    +
    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."""
         
    -    Parameters
    -    ----------
    -    geom : Geometry
    -         
    -    elements : list of GMSH elements
    -        The geometry elements to revolve. These should have been returned previously from for example
    -        a call to geom.add_line(...).
    -        
    -    factor : float
    -         How far the elements should be revolved around the optical axis. factor=1.0 corresponds
    -         to a full revolution ( \(2\pi \) radians) around the optical axis, while for example 0.5
    -         corresponds to a revolution of only \(\pi\) radians. (Default value = 1.0).
    -
    -    Returns
    -    -------
    -    A list of surface elements representing the revolution around the optical axis.
    -    """
    -    revolved = []
    +    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
    +        assert self.path_length > 0
    +        self.breakpoints = breakpoints
    +        self.name = name
         
    -    for e in (elements if isinstance(elements, list) else [elements]):
    -        
    -        top = e
    -        for i in range(4):
    -            top, extruded, lateral = geom.revolve(top, [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], factor*0.5*np.pi)
    -            revolved.append(extruded)
    -     
    -    return revolved
    +    @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.
     
    -class Symmetry(Enum):
    -    """
    -    Symmetry of the geometry. 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
    +        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.
     
    -    def __str__(self):
    -        if self == Symmetry.RADIAL:
    -            return 'radial'
    -        elif self == Symmetry.THREE_D:
    -            return '3d' 
    -    
    -    def is_2d(self):
    -        return self == Symmetry.RADIAL
    +        Returns
    +        ---------------------------------
    +        Path"""
    +         
    +        # path length = integrate |f'(x)|
    +        fun = lambda u: np.array(to_point(u))
             
    -    def is_3d(self):
    -        return self == Symmetry.THREE_D
    -
    -class Geometry(geo.Geometry):
    -    """
    -    Small wrapper class around pygmsh.geo.Geometry which itself is a small wrapper around the powerful GMSH library.
    -    See the GMSH and pygmsh documentation to learn how to build any 2D or 3D geometry. This class makes it easier to control
    -    the mesh size (using the _mesh size factor_) and optionally allows to scale the mesh size with the distance from the optical
    -    axis. It also add support for multiple calls to the `add_physical` method with the same name.
    -    
    -    Parameters
    -    ---------
    -    symmetry: Symmetry
    -
    -    size_from_distance: bool, optional
    -        Scale the mesh size with the distance from the optical axis (z-axis).
    -
    -    zmin: float, optional
    -    zmax: float, optional
    -        When `size_from_distance=True` geometric elements that touch the optical axis (as in electrostatic mirrors)
    -        will imply a zero mesh size and therefore cause singularities. The zmin and zmax arguments
    -        allow to specify which section of the optical axis will be reachable by the electrons. When
    -        calculating the mesh size the distance from the closest point on this section of the optical axis is used.
    -        This prevents singularities.
    -    """
    -    def __init__(self, symmetry, size_from_distance=False, zmin=None, zmax=None, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -        self.size_from_distance = size_from_distance
    -        self.zmin = zmin
    -        self.zmax = zmax
    -        self.MSF = None
    -        self.symmetry = symmetry
    -        self._physical_queue = dict()
    +        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]
     
    -    def __str__(self):
    -        if self.zmin is not None and self.zmax is not None:
    -            return f'<Traceon Geometry {self.symmetry}, zmin={self.zmin:.2f} mm, zmax={self.zmax:.2f} mm'
    -        else:
    -            return f'<Traceon Geometry {self.symmetry}>'
    +        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.
     
    -    def is_3d(self):
    -        """Check if the geometry is three dimensional.
    +        Parameters
    +        -------------------------
    +        points: (N, 3) ndarray of float
    +            Three dimensional points through which the spline is fitted.
     
             Returns
    -        ----------------
    -        True if geometry is three dimensional, False if the geometry is two dimensional"""
    -        return self.symmetry.is_3d()
    +        -------------------------
    +        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.
     
    -    def is_2d(self):
    -        """Check if the geometry is two dimensional.
    +        Parameters
    +        --------------------------
    +        fun: callable (3,) -> float
    +            A function taking a three dimensional point and returning a float.
     
             Returns
    -        ----------------
    -        True if geometry is two dimensional, False if the geometry is three dimensional"""
    +        -------------------------
    +        float
     
    -        return self.symmetry.is_2d()
    +        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 add_physical(self, entities, name):
    -        """
    -
    -        Parameters
    -        ----------
    -        entities : list of GMSH elements or GMSH element
    -            Geometric entities to assign the given name (the given _physical group_ in GMSH terminology).
    -        name : string
    -            Name of the physical group.
    -        """
    -        if not isinstance(entities, list):
    -            entities = [entities]
    +    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.
             
    -        if name in self._physical_queue:
    -            self._physical_queue[name].extend(entities)
    -        else:
    -            self._physical_queue[name] = entities
    +        Parameters
    +        ----------------------------
    +        fun: callable (3,) -> (3,)
    +            Function taking three dimensional points and returning three dimensional points.
     
    -    def _generate_mesh(self, dimension, higher_order=False, *args, **kwargs):
    -        assert dimension == 1 or dimension == 2, "Currently only line and triangle meshes supported (dimension 1 or 2)"
    -        
    -        for label, entities in self._physical_queue.items():
    -            super().add_physical(entities, label)
    -          
    -        if self.size_from_distance:
    -            self.set_mesh_size_callback(self._mesh_size_callback)
    -        
    -        if dimension == 1 and higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 3)
    -        elif dimension == 1 and not higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 1)
    -        elif dimension == 2 and higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 2)
    -        elif dimension == 2 and not higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 1)
    -        
    -        return Mesh.from_meshio(super().generate_mesh(dim=dimension, *args, **kwargs), self.symmetry)
    +        Returns
    +        ---------------------------      
     
    -    def generate_line_mesh(self, higher_order, *args, **kwargs):
    -        """Generate boundary mesh in 2D, by splitting the boundary in line elements.
    +        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
    -        -----------------
    -        higher_order: bool
    -            Whether to use higher order (curved) line elements.
    +        ------------------------
    +        t: float
    +            The length along the path.
     
             Returns
    -        ----------------
    -        `Mesh`
    -        """
    -        if self.MSF is not None:
    -            gmsh.option.setNumber('Mesh.MeshSizeFactor', 1/self.MSF)
    -        return self._generate_mesh(*args, higher_order=higher_order, dimension=1, **kwargs)
    +        ------------------------
    +        (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 generate_triangle_mesh(self, higher_order, *args, **kwargs):
    -        """Generate triangle mesh. Note that also 2D meshes can have triangles, which can current coils.
    -        
    +    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
    -        -----------------
    -        higher_order: bool
    -            Whether to use higher order (curved) line elements.
    -        
    +        --------------------
    +        l: float
    +            The phase (expressed as a path length). The resulting path starts l distance along the 
    +            original path.
    +
             Returns
    -        ----------------
    -        `Mesh`
    -        """
    -        if self.MSF is not None:
    -            # GMSH seems to produce meshes which contain way more elements for 3D geometries
    -            # with the same mesh factor. This is confusing for users and therefore we arbtrarily
    -            # increase the mesh size to roughly correspond with the 2D number of elements.
    -            gmsh.option.setNumber('Mesh.MeshSizeFactor', 4*sqrt(1/self.MSF))
    -        return self._generate_mesh(*args, higher_order=higher_order, dimension=2, **kwargs)
    -    
    -    def set_mesh_size_factor(self, factor):
    -        """
    -        Set the mesh size factor. Which simply scales with the total number of elements in the mesh.
    +        --------------------
    +        Path"""
    +        assert self.is_closed()
             
    -        Parameters
    -        ----------
    -        factor : float
    -            The mesh size factor to use. 
    +        def fun(u):
    +            return self( (l + u) % self.path_length )
             
    -        """
    -        self.MSF = factor
    +        return Path(fun, self.path_length, sorted([(b-l)%self.path_length for b in self.breakpoints + [0.]]), name=self.name)
          
    -    def set_minimum_mesh_size(self, size):
    -        """
    -        Set the minimum mesh size possible. Especially useful when geometric elements touch
    -        the optical axis and cause singularities when used with `size_from_distance=True`.
    -        
    +    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
    -        ----------
    -        size : float
    -            The minimum mesh size.  
    +        -----------------------
    +        other: Path
    +            The second path, to extend the current path.
     
    -        """
    -        gmsh.option.setNumber('Mesh.MeshSizeMin', size)
    -     
    -    def _mesh_size_callback(self, dim, tag, x, y, z, _):
    -        # Scale mesh size with distance to optical axis, but only the part of the optical
    -        # axis that lies between zmin and zmax
    -        
    -        z_optical = y if self.symmetry == Symmetry.RADIAL else z
    -         
    -        if self.zmin is not None:
    -            z_optical = max(z_optical, self.zmin)
    -        if self.zmax is not None:
    -            z_optical = min(z_optical, self.zmax)
    +        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
              
    -        if self.symmetry == Symmetry.RADIAL:
    -            return sqrt( x**2 + (y-z_optical)**2 )
    -        else:
    -            return sqrt( x**2 + y**2 + (z-z_optical)**2 )
    +        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 _concat_arrays(arr1, arr2):
    -    if not len(arr1):
    -        return np.copy(arr2)
    -    if not len(arr2):
    -        return np.copy(arr1)
    -      
    -    assert arr1.shape[1:] == arr2.shape[1:], "Cannot add meshes if one is higher order and the other is not"
    -    
    -    return np.concatenate( (arr1, arr2), axis=0)
    +    def starting_point(self):
    +        """Returns the starting point of the path.
    +
    +        Returns
    +        ---------------------
    +        (3,) float
     
    -class Mesh(Saveable):
    -    """Class containing a mesh.
    -    For now, to make things manageable only lines and triangles are supported.
    -    Lines and triangles can be higher order (curved) or not. But a mesh cannot contain
    -    both curved and simple elements at the same time.
    +        The starting point of the path."""
    +        return self(0.)
         
    -    When the elements are higher order (curved), triangles consists of 6 points and lines of four points.
    -    These correspond with the GMSH line4 and triangle6 types."""
    -     
    -    def __init__(self, symmetry,
    -            points=[],
    -            lines=[],
    -            triangles=[],
    -            physical_to_lines={},
    -            physical_to_triangles={}):
    -        
    -        assert isinstance(symmetry, Symmetry)
    -        self.symmetry = symmetry
    +    def middle_point(self):
    +        """Returns the midpoint of the path (in terms of length along the path.)
    +
    +        Returns
    +        ----------------------
    +        (3,) float
             
    -        # 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):
    -            self.lines = np.array(lines)
    -        else:
    -            self.lines = np.empty((0,2), dtype=np.uint64)
    +        The point at the middle of the path."""
    +        return self(self.path_length/2)
         
    -        if len(triangles):
    -            self.triangles = np.array(triangles)
    -        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()
    -        
    -        if symmetry.is_2d():
    -            assert points.shape[1] == 2 or np.allclose(points[:, 2], 0.), "Cannot have three dimensional points when symmetry is 2D"
    +    def endpoint(self):
    +        """Returns the endpoint of the path.
    +
    +        Returns
    +        ------------------------
    +        (3,) float
             
    -        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"
    +        The endpoint of the path."""
    +        return self(self.path_length)
         
    -    def is_higher_order(self):
    -        return (len(self.lines) and self.lines.shape[1] == 4) or (len(self.triangles) and self.triangles.shape[1] == 6)
    -    
    -    def move(self, vector):
    -        self.points += vector
    -     
    -    def __add__(self, other):
    -        assert isinstance(other, Mesh)
    -        assert self.symmetry == other.symmetry, "Cannot add meshes with different symmetries"
    -         
    -        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)
    -         
    -        physical_lines = {**self.physical_to_lines, **{k:(v+N_lines) for k, v in other.physical_to_lines.items()}}
    -        physical_triangles = {**self.physical_to_triangles, **{k:(v+N_triangles) for k, v in other.physical_to_triangles.items()}}
    -         
    -        return Mesh(self.symmetry,
    -                        points=points,
    -                        lines=lines,
    -                        triangles=triangles,
    -                        physical_to_lines=physical_lines,
    -                        physical_to_triangles=physical_triangles)
    +    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 extract_physical_group(self, 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(self.symmetry, points=points, lines=elements, physical_to_lines=physical_to_elements)
    -        elif name in self.physical_to_triangles:
    -            return Mesh(self.symmetry, points=points, triangles=triangles, physical_to_triangles=physical_to_elements)
    -     
    -    def import_file(filename, symmetry,  name=None):
    -        meshio_obj = meshio.read(filename)
    -        mesh = Mesh.from_meshio(meshio_obj, symmetry)
    -         
    -        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 export_file(self, filename):
    -        meshio_obj = self.to_meshio()
    -        meshio_obj.write(filename)
    -     
    -    def to_meshio(self):
    -        to_export = []
    -        
    -        if len(self.lines):
    -            line_type = 'line' if self.lines.shape[1] == 2 else 'line4'
    -            to_export.append( (line_type, self.lines) )
    -        
    -        if len(self.triangles):
    -            triangle_type = 'triangle' if self.triangles.shape[1] == 3 else 'triangle6'
    -            to_export.append( (triangle_type, self.triangles) )
    -        
    -        return meshio.Mesh(self.points, to_export)
    +    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
          
    -    def from_meshio(mesh, symmetry):
    -        """Generate a Traceon Mesh from a [meshio](https://github.com/nschloe/meshio) mesh.
    +    @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
    -        ----------
    -        symmetry: Symmetry
    -            Specifies a radially symmetric geometry (RADIAL) or a general 3D geometry (THREE_D).
    -        
    +        --------------------------------
    +        x0: float
    +            x-coordinate of the center of the circle
    +        z0: float
    +            z-coordiante 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
    -        ---------
    -        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')
    +        ---------------------------------
    +        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.
             
    -        return Mesh(symmetry,
    -            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.
    +        Parameters
    +        --------------------------------
    +        y0: float
    +            x-coordinate of the center of the circle
    +        z0: float
    +            z-coordiante 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
    -        ----------------
    -        True if mesh is three dimensional, False if the mesh is two dimensional"""
    -        return self.symmetry.is_3d()
    +        ---------------------------------
    +        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)
         
    -    def is_2d(self):
    -        """Check if the mesh is two dimensional.
    +    @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
    +        --------------------------------
    +        y0: float
    +            x-coordinate of the center of the circle
    +        y0: float
    +            y-coordiante 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
    -        ----------------
    -        True if mesh is two dimensional, False if the mesh is three dimensional"""
    -        return self.symmetry.is_2d()
    +        ---------------------------------
    +        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.
     
    -    def remove_lines(self):
    -        return Mesh(self.symmetry, self.points, triangles=self.triangles, physical_to_triangles=self.physical_to_triangles)
    +        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 remove_triangles(self):
    -        return Mesh(self.symmetry, self.points, lines=self.lines, physical_to_lines=self.physical_to_lines)
    -     
    -    def get_electrodes(self):
    -        """Get the names of all the electrodes in the geometry.
    -         
    +    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
    -        ---------
    -        List of electrode names
    +        -----------------------------
    +        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.
     
    -        """
    -        return list(self.physical_to_lines.keys()) + list(self.physical_to_triangles.keys())
    -     
    -    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])
    +        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)
              
    -        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
    +        x_unit = start_arr - center_arr
    +        x_unit /= np.linalg.norm(x_unit)
    +
    +        vector = end_arr - center_arr
              
    -        assert np.allclose(p2, points[elements[:, 2]]) and np.allclose(p3, points[elements[:, 3]])
    -        return points, elements
    +        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))
     
    -    def _triangles_to_higher_order(points, elements):
    -        N_elements = len(elements)
    -        N_points = len(points)
    -         
    -        v0, v1, v2 = elements.T
    -        p3 = (points[v0] + points[v1])/2
    -        p4 = (points[v1] + points[v2])/2
    -        p5 = (points[v2] + points[v0])/2
    -         
    -        assert all(p.shape == (N_elements, points.shape[1]) for p in [p3,p4,p5])
    -          
    -        points = np.concatenate( (points, p3, p4, p5), axis=0)
    +        if reverse:
    +            theta_max = theta_max - 2*pi
    +
    +        path_length = abs(theta_max * radius)
               
    -        elements = np.array([
    -            elements[:, 0], elements[:, 1], elements[:, 2],
    -            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),
    -            np.arange(N_points + 2*N_elements, N_points + 3*N_elements, dtype=np.uint64)]).T
    -         
    -        assert np.allclose(p3, points[elements[:, 3]])
    -        assert np.allclose(p4, points[elements[:, 4]])
    -        assert np.allclose(p5, points[elements[:, 5]])
    +        def f(l):
    +            theta = l/path_length * theta_max
    +            return center + radius*cos(theta)*x_unit + radius*sin(theta)*y_unit
             
    -        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.
    +        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.
             
    -        points, lines, triangles = self.points, self.lines, self.triangles
    +        Parameters
    +        -----------------------
    +        angle: float
    +            The angle by which to revolve. THe default 2*pi gives a full revolution.
     
    -        if len(lines) and lines.shape[1] == 2:
    -            points, lines = Mesh._lines_to_higher_order(points, lines)
    -        if len(triangles) and triangles.shape[1] == 3:
    -            points, triangles = Mesh._triangles_to_higher_order(points, triangles) 
    -         
    -        return Mesh(self.symmetry,
    -            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()])
    +        Returns
    +        -----------------------
    +        Surface"""
             
    -        return f'<Traceon Mesh {self.symmetry},\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}>'
    -
    -
    -class MEMSStack(Geometry):
    -    """Geometry consisting of a stack of MEMS fabricated elements. This geometry is modelled using a stack
    -    of rectangularly shaped elements with a variable spacing in between. Useful when doing calculations on MEMS fabricated
    -    lenses and mirrors.
    +        pstart, pmiddle, pend = self.starting_point(), self.middle_point(), self.endpoint()
    +        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)
         
    -    Parameters
    -    ----------
    -    z0: float
    -        Starting z-value to begin building up the MEMS elements from.
    -    revolve_factor: float
    -        Revolve the resulting geometry around the optical axis to generate a 3D geometry. When `revolve_factor=0.0` a
    -        2D geometry is returned. For `0 < revolve_factor <= 1.0` see the documentation of `revolve_around_optical_axis`.
    -    rmax: float
    -        The rectangular MEMS objects extend to \( r = r_{max} \).
    -    margin: float
    -        The distance between the electrodes and the top and bottom boundary.
    -    margin_right: float
    -        Distance between the boundary on the right and the MEMS electrodes.
    -    symmetry: `Symmetry`
    -        What symmetry to use for the resulting geometry
    -    """
    +    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"""
    +
    +        pstart, pend = self.starting_point(), self.endpoint()
    +        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 __init__(self, *args, z0=0.0, revolve_factor=0.0, rmax=2, margin=0.5, margin_right=0.1, symmetry=None, **kwargs):
    +    def revolve_z(self, angle=2*pi):
    +        """Create a surface by revolving the path anti-clockwise around the z-axis.
             
    -        if symmetry is None:
    -            symmetry = Symmetry.RADIAL if revolve_factor == 0.0 else Symmetry.THREE_D
    -        else:
    -            symmetry = symmetry
    -            
    -            if revolve_factor == 0.0 and symmetry.is_3d():
    -                revolve_factor = 1.0
    +        Parameters
    +        -----------------------
    +        angle: float
    +            The angle by which to revolve. THe default 2*pi gives a full revolution.
    +
    +        Returns
    +        -----------------------
    +        Surface"""
     
    -        super().__init__(symmetry, *args, **kwargs)
    +        pstart, pend = self.starting_point(), self.endpoint()
    +        r_avg = self.average(lambda p: sqrt(p[0]**2 + p[1]**2))
    +        length2 = 2*pi*r_avg
             
    -        self.z0 = z0
    -        self.revolve_factor = revolve_factor
    -        self.rmax = rmax
    +        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]])
             
    -        self.margin = margin
    -        self.margin_right = margin_right
    -         
    -        self._current_z = z0 + margin
    -        self._last_name = None
    -    
    -    def add_spacer(self, thickness):
    -        """
    +        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
    -        ----------
    -        thickness : float
    -            Add the given amount of space between the previous and next electrode.
    +        -------------------------
    +        vector: (3,) float
    +            The direction and length (norm of the vector) to extrude by.
     
    -        """
    -        self._current_z += thickness
    -    
    -    def _add_boundary(self):
    -        points = [[0.0, self._current_z+self.margin],
    -                  [self.rmax+self.margin_right, self._current_z+self.margin],
    -                  [self.rmax+self.margin_right, self.z0],
    -                  [0.0, self.z0]]
    +        Returns
    +        -------------------------
    +        Surface"""
    +        vector = np.array(vector)
    +        length = np.linalg.norm(vector)
              
    -        self._add_lines_from_points(points, 'boundary')
    -        self._current_z += self.margin
    -     
    -    def _add_lines_from_points(self, points, name):
    -        if self.is_3d():
    -            points = [self.add_point([p[0], 0.0, p[1]]) for p in points[::-1]]
    -        else:
    -            points = [self.add_point(p) for p in points]
    +        def f(u, v):
    +            return self(u) + v/length*vector
             
    -        Np = len(points)
    -        lines = [self.add_line(points[i], points[j]) for i, j in zip(range(0,Np-1), range(1,Np))]
    -         
    -        if self.is_3d():
    -            revolved = revolve_around_optical_axis(self, lines, self.revolve_factor)
    -            self.add_physical(revolved, name)
    -        else:
    -            self.add_physical(lines, name)
    -
    -    def add_electrode(self, radius, thickness, name):
    -        """
    +        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
    -        ----------
    -        radius : float
    -            Distance from the electrode to the optical axis (in mm).
    -            
    -        thickness : float
    -            Thickness of the electrode (in mm).
    -            
    -        name : str
    -            Name to assign to the electode. Needed to later specify the correct excitation.
    -        """
    -        cz = self._current_z
    -        points = [[self.rmax, cz], [radius, cz], [radius, cz+thickness], [self.rmax, cz+thickness]]
    -        self._add_lines_from_points(points, name)
    -        self._current_z += thickness
    -
    -    def generate_line_mesh(self, *args, **kwargs):
    -        self._add_boundary()
    -        return super().generate_line_mesh(*args, **kwargs)
    -    
    -    def generate_triangle_mesh(self, *args, **kwargs):
    -        self._add_boundary()
    -        return super().generate_triangle_mesh(*args, **kwargs)
    - 
    -    
    -        
    -
    -
    -
    -
    -
    -
    -
    -

    Functions

    -
    -
    -def revolve_around_optical_axis(geom, elements, factor=1.0) -
    -
    -

    Revolve geometry elements around the optical axis. Useful when you -want to generate 3D geometries from a cylindrically symmetric 2D geometry.

    -

    Parameters

    -
    -
    geom : Geometry
    -
     
    -
    elements : list of GMSH elements
    -
    The geometry elements to revolve. These should have been returned previously from for example -a call to geom.add_line(…).
    -
    factor : float
    -
    How far the elements should be revolved around the optical axis. factor=1.0 corresponds -to a full revolution ( 2\pi radians) around the optical axis, while for example 0.5 -corresponds to a revolution of only \pi radians. (Default value = 1.0).
    -
    -

    Returns

    -

    A list of surface elements representing the revolution around the optical axis.

    -
    - -Expand source code - -
    def revolve_around_optical_axis(geom, elements, factor=1.0):
    -    """
    -    Revolve geometry elements around the optical axis. Useful when you
    -    want to generate 3D geometries from a cylindrically symmetric 2D geometry.
    -    
    -    Parameters
    -    ----------
    -    geom : Geometry
    +        -------------------------
    +        p2: Path
    +            The (second) path defining the extrusion.
    +
    +        Returns
    +        ------------------------
    +        Surface"""
    +        p0 = p2.starting_point()
              
    -    elements : list of GMSH elements
    -        The geometry elements to revolve. These should have been returned previously from for example
    -        a call to geom.add_line(...).
    -        
    -    factor : float
    -         How far the elements should be revolved around the optical axis. factor=1.0 corresponds
    -         to a full revolution ( \(2\pi \) radians) around the optical axis, while for example 0.5
    -         corresponds to a revolution of only \(\pi\) radians. (Default value = 1.0).
    -
    -    Returns
    -    -------
    -    A list of surface elements representing the revolution around the optical axis.
    -    """
    -    revolved = []
    -    
    -    for e in (elements if isinstance(elements, list) else [elements]):
    -        
    -        top = e
    -        for i in range(4):
    -            top, extruded, lateral = geom.revolve(top, [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], factor*0.5*np.pi)
    -            revolved.append(extruded)
    -     
    -    return revolved
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class Geometry -(symmetry, size_from_distance=False, zmin=None, zmax=None, *args, **kwargs) -
    -
    -

    Small wrapper class around pygmsh.geo.Geometry which itself is a small wrapper around the powerful GMSH library. -See the GMSH and pygmsh documentation to learn how to build any 2D or 3D geometry. This class makes it easier to control -the mesh size (using the mesh size factor) and optionally allows to scale the mesh size with the distance from the optical -axis. It also add support for multiple calls to the add_physical method with the same name.

    -

    Parameters

    -
    -
    symmetry : Symmetry
    -
     
    -
    size_from_distance : bool, optional
    -
    Scale the mesh size with the distance from the optical axis (z-axis).
    -
    zmin : float, optional
    -
     
    -
    zmax : float, optional
    -
    When size_from_distance=True geometric elements that touch the optical axis (as in electrostatic mirrors) -will imply a zero mesh size and therefore cause singularities. The zmin and zmax arguments -allow to specify which section of the optical axis will be reachable by the electrons. When -calculating the mesh size the distance from the closest point on this section of the optical axis is used. -This prevents singularities.
    -
    -
    - -Expand source code - -
    class Geometry(geo.Geometry):
    -    """
    -    Small wrapper class around pygmsh.geo.Geometry which itself is a small wrapper around the powerful GMSH library.
    -    See the GMSH and pygmsh documentation to learn how to build any 2D or 3D geometry. This class makes it easier to control
    -    the mesh size (using the _mesh size factor_) and optionally allows to scale the mesh size with the distance from the optical
    -    axis. It also add support for multiple calls to the `add_physical` method with the same name.
    -    
    -    Parameters
    -    ---------
    -    symmetry: Symmetry
    -
    -    size_from_distance: bool, optional
    -        Scale the mesh size with the distance from the optical axis (z-axis).
    -
    -    zmin: float, optional
    -    zmax: float, optional
    -        When `size_from_distance=True` geometric elements that touch the optical axis (as in electrostatic mirrors)
    -        will imply a zero mesh size and therefore cause singularities. The zmin and zmax arguments
    -        allow to specify which section of the optical axis will be reachable by the electrons. When
    -        calculating the mesh size the distance from the closest point on this section of the optical axis is used.
    -        This prevents singularities.
    -    """
    -    def __init__(self, symmetry, size_from_distance=False, zmin=None, zmax=None, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -        self.size_from_distance = size_from_distance
    -        self.zmin = zmin
    -        self.zmax = zmax
    -        self.MSF = None
    -        self.symmetry = symmetry
    -        self._physical_queue = dict()
    +        def f(u, v):
    +            return self(u) + p2(v) - p0
     
    -    def __str__(self):
    -        if self.zmin is not None and self.zmax is not None:
    -            return f'<Traceon Geometry {self.symmetry}, zmin={self.zmin:.2f} mm, zmax={self.zmax:.2f} mm'
    -        else:
    -            return f'<Traceon Geometry {self.symmetry}>'
    +        return Surface(f, self.path_length, p2.path_length, self.breakpoints, p2.breakpoints, name=self.name)
     
    -    def is_3d(self):
    -        """Check if the geometry is three dimensional.
    +    def close(self):
    +        """Close the path, by making a straight line to the starting point.
     
             Returns
    -        ----------------
    -        True if geometry is three dimensional, False if the geometry is two dimensional"""
    -        return self.symmetry.is_3d()
    +        -------------------
    +        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.
     
    -    def is_2d(self):
    -        """Check if the geometry is two dimensional.
    +        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
    -        ----------------
    -        True if geometry is two dimensional, False if the geometry is three dimensional"""
    -
    -        return self.symmetry.is_2d()
    -     
    -    def add_physical(self, entities, name):
    -        """
    +        ---------------------------
    +        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
    -        ----------
    -        entities : list of GMSH elements or GMSH element
    -            Geometric entities to assign the given name (the given _physical group_ in GMSH terminology).
    -        name : string
    -            Name of the physical group.
    -        """
    -        if not isinstance(entities, list):
    -            entities = [entities]
    -        
    -        if name in self._physical_queue:
    -            self._physical_queue[name].extend(entities)
    -        else:
    -            self._physical_queue[name] = entities
    +        ------------------------------
    +        from_: (3,) float
    +            The starting point of the path.
    +        to: (3,) float
    +            The endpoint of the path.
     
    -    def _generate_mesh(self, dimension, higher_order=False, *args, **kwargs):
    -        assert dimension == 1 or dimension == 2, "Currently only line and triangle meshes supported (dimension 1 or 2)"
    -        
    -        for label, entities in self._physical_queue.items():
    -            super().add_physical(entities, label)
    -          
    -        if self.size_from_distance:
    -            self.set_mesh_size_callback(self._mesh_size_callback)
    -        
    -        if dimension == 1 and higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 3)
    -        elif dimension == 1 and not higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 1)
    -        elif dimension == 2 and higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 2)
    -        elif dimension == 2 and not higher_order:
    -            gmsh.option.setNumber('Mesh.ElementOrder', 1)
    -        
    -        return Mesh.from_meshio(super().generate_mesh(dim=dimension, *args, **kwargs), self.symmetry)
    +        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 generate_line_mesh(self, higher_order, *args, **kwargs):
    -        """Generate boundary mesh in 2D, by splitting the boundary in line elements.
    +    def cut(self, length):
    +        """Cut the path in two at a specific length along the path.
     
             Parameters
    -        -----------------
    -        higher_order: bool
    -            Whether to use higher order (curved) line elements.
    +        --------------------------------------
    +        length: float
    +            The length along the path at which to cut.
     
             Returns
    -        ----------------
    -        `Mesh`
    -        """
    -        if self.MSF is not None:
    -            gmsh.option.setNumber('Mesh.MeshSizeFactor', 1/self.MSF)
    -        return self._generate_mesh(*args, higher_order=higher_order, dimension=1, **kwargs)
    +        -------------------------------------
    +        (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))
         
    -    def generate_triangle_mesh(self, higher_order, *args, **kwargs):
    -        """Generate triangle mesh. Note that also 2D meshes can have triangles, which can current coils.
    +    @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
    -        -----------------
    -        higher_order: bool
    -            Whether to use higher order (curved) line elements.
    +        ------------------------
    +        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
    -        ----------------
    -        `Mesh`
    -        """
    -        if self.MSF is not None:
    -            # GMSH seems to produce meshes which contain way more elements for 3D geometries
    -            # with the same mesh factor. This is confusing for users and therefore we arbtrarily
    -            # increase the mesh size to roughly correspond with the 2D number of elements.
    -            gmsh.option.setNumber('Mesh.MeshSizeFactor', 4*sqrt(1/self.MSF))
    -        return self._generate_mesh(*args, higher_order=higher_order, dimension=2, **kwargs)
    -    
    -    def set_mesh_size_factor(self, factor):
    -        """
    -        Set the mesh size factor. Which simply scales with the total number of elements in the mesh.
    +        -----------------------
    +        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
    -        ----------
    -        factor : float
    -            The mesh size factor to use. 
    +        ------------------------
    +        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.
             
    -        """
    -        self.MSF = factor
    +        Returns
    +        -----------------------
    +        Path"""
    +
    +        return Path.line([0., ymin, zmin], [0, ymin, zmax]) \
    +            .extend_with_line([0., ymax, zmax]).extend_with_line([0., ymax, zmin]).close()
          
    -    def set_minimum_mesh_size(self, size):
    -        """
    -        Set the minimum mesh size possible. Especially useful when geometric elements touch
    -        the optical axis and cause singularities when used with `size_from_distance=True`.
    +    @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
    -        ----------
    -        size : float
    -            The minimum mesh size.  
    +        ------------------------
    +        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.
     
    -        """
    -        gmsh.option.setNumber('Mesh.MeshSizeMin', size)
    +        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 not isinstance(other, Path) and not isinstance(other, PathCollection):
    +            return NotImplemented
    +        
    +        if isinstance(other, Path):
    +            return PathCollection([self, other])
    +        elif isinstance(other, PathCollection):
    +            return PathCollection([self] + [other.paths])
          
    -    def _mesh_size_callback(self, dim, tag, x, y, z, _):
    -        # Scale mesh size with distance to optical axis, but only the part of the optical
    -        # axis that lies between zmin and zmax
    +    def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None):
    +        """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)
             
    -        z_optical = y if self.symmetry == Symmetry.RADIAL else z
    +        N = len(u) 
    +        points = np.zeros( (N, 3) )
              
    -        if self.zmin is not None:
    -            z_optical = max(z_optical, self.zmin)
    -        if self.zmax is not None:
    -            z_optical = min(z_optical, self.zmax)
    +        for i in range(N):
    +            points[i] = self(u[i])
              
    -        if self.symmetry == Symmetry.RADIAL:
    -            return sqrt( x**2 + (y-z_optical)**2 )
    +        if not higher_order:
    +            lines = np.array([np.arange(N-1), np.arange(1, N)]).T
             else:
    -            return sqrt( x**2 + y**2 + (z-z_optical)**2 )
    + 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) + + def __str__(self): + return f"<Path name:{self.name}, length:{self.path_length:.1e}, number of breakpoints:{len(self.breakpoints)}>"

    Ancestors

      -
    • pygmsh.geo.geometry.Geometry
    • -
    • pygmsh.common.geometry.CommonGeometry
    • -
    -

    Subclasses

    - +

    Static methods

    +
    +
    +def aperture(height, radius, extent, z=0.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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def circle_xy(x0, y0, radius, angle=6.283185307179586) +
    +
    +

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

    +

    Parameters

    +
    +
    y0 : float
    +
    x-coordinate of the center of the circle
    +
    y0 : float
    +
    y-coordiante 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) +
    +
    +

    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-coordiante 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) +
    +
    +

    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-coordiante 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) +
    +
    +

    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=[]) +
    +
    +

    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) +
    +
    +

    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]) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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_physical(self, entities, name) +
    +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
    +
     
    +
    +
    +
    +def __call__(self, 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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

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

    +

    Returns

    +
    +
    Path
    +
     
    +
    +
    +
    +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.

    +
    +
    +def endpoint(self) +
    +
    +

    Returns the endpoint of the path.

    +

    Returns

    +

    (3,) float

    +

    The endpoint of the path.

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def extend_with_polar_arc(self, radius, angle, plane_normal=[0, 1, 0])
    -

    Parameters

    +

    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

    -
    entities : list of GMSH elements or GMSH element
    -
    Geometric entities to assign the given name (the given physical group in GMSH terminology).
    -
    name : string
    -
    Name of the physical group.
    +
    Path
    +
     
    -
    - -Expand source code - -
    def add_physical(self, entities, name):
    -    """
    -
    -    Parameters
    -    ----------
    -    entities : list of GMSH elements or GMSH element
    -        Geometric entities to assign the given name (the given _physical group_ in GMSH terminology).
    -    name : string
    -        Name of the physical group.
    -    """
    -    if not isinstance(entities, list):
    -        entities = [entities]
    -    
    -    if name in self._physical_queue:
    -        self._physical_queue[name].extend(entities)
    -    else:
    -        self._physical_queue[name] = entities
    -
    -
    -def generate_line_mesh(self, higher_order, *args, **kwargs) +
    +def extrude(self, vector)
    -

    Generate boundary mesh in 2D, by splitting the boundary in line elements.

    +

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

    Parameters

    -
    higher_order : bool
    -
    Whether to use higher order (curved) line elements.
    +
    vector : (3,) float
    +
    The direction and length (norm of the vector) to extrude by.

    Returns

    -

    Mesh

    -
    - -Expand source code - -
    def generate_line_mesh(self, higher_order, *args, **kwargs):
    -    """Generate boundary mesh in 2D, by splitting the boundary in line elements.
    -
    -    Parameters
    -    -----------------
    -    higher_order: bool
    -        Whether to use higher order (curved) line elements.
    -
    -    Returns
    -    ----------------
    -    `Mesh`
    -    """
    -    if self.MSF is not None:
    -        gmsh.option.setNumber('Mesh.MeshSizeFactor', 1/self.MSF)
    -    return self._generate_mesh(*args, higher_order=higher_order, dimension=1, **kwargs)
    -
    +
    +
    Surface
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def map_points(self, fun) +
    +
    +

    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 generate_triangle_mesh(self, higher_order, *args, **kwargs) +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None)
    -

    Generate triangle mesh. Note that also 2D meshes can have triangles, which can current coils.

    +

    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 use higher order (curved) line elements.
    +
    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

    -
    - -Expand source code - -
    def generate_triangle_mesh(self, higher_order, *args, **kwargs):
    -    """Generate triangle mesh. Note that also 2D meshes can have triangles, which can current coils.
    -    
    -    Parameters
    -    -----------------
    -    higher_order: bool
    -        Whether to use higher order (curved) line elements.
    -    
    -    Returns
    -    ----------------
    -    `Mesh`
    -    """
    -    if self.MSF is not None:
    -        # GMSH seems to produce meshes which contain way more elements for 3D geometries
    -        # with the same mesh factor. This is confusing for users and therefore we arbtrarily
    -        # increase the mesh size to roughly correspond with the 2D number of elements.
    -        gmsh.option.setNumber('Mesh.MeshSizeFactor', 4*sqrt(1/self.MSF))
    -    return self._generate_mesh(*args, higher_order=higher_order, dimension=2, **kwargs)
    -
    +

    Mesh

    +
    +
    +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.

    -
    -def is_2d(self) +
    +def reverse(self)
    -

    Check if the geometry is two dimensional.

    +

    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

    -
    True if geometry is two dimensional, False if the geometry is three dimensional
    +
    Path
     
    -
    - -Expand source code - -
    def is_2d(self):
    -    """Check if the geometry is two dimensional.
    -
    -    Returns
    -    ----------------
    -    True if geometry is two dimensional, False if the geometry is three dimensional"""
    -
    -    return self.symmetry.is_2d()
    -
    -
    -def is_3d(self) +
    +def revolve_x(self, angle=6.283185307179586)
    -

    Check if the geometry is three dimensional.

    +

    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

    -
    True if geometry is three dimensional, False if the geometry is two dimensional
    +
    Surface
     
    -
    - -Expand source code - -
    def is_3d(self):
    -    """Check if the geometry is three dimensional.
    -
    -    Returns
    -    ----------------
    -    True if geometry is three dimensional, False if the geometry is two dimensional"""
    -    return self.symmetry.is_3d()
    -
    -
    -def set_mesh_size_factor(self, factor) +
    +def revolve_y(self, angle=6.283185307179586)
    -

    Set the mesh size factor. Which simply scales with the total number of elements in the mesh.

    +

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

    Parameters

    -
    factor : float
    -
    The mesh size factor to use.
    +
    angle : float
    +
    The angle by which to revolve. THe default 2*pi gives a full revolution.
    +
    +

    Returns

    +
    +
    Surface
    +
     
    -
    - -Expand source code - -
    def set_mesh_size_factor(self, factor):
    -    """
    -    Set the mesh size factor. Which simply scales with the total number of elements in the mesh.
    -    
    -    Parameters
    -    ----------
    -    factor : float
    -        The mesh size factor to use. 
    -    
    -    """
    -    self.MSF = factor
    -
    -
    -def set_minimum_mesh_size(self, size) +
    +def revolve_z(self, angle=6.283185307179586)
    -

    Set the minimum mesh size possible. Especially useful when geometric elements touch -the optical axis and cause singularities when used with size_from_distance=True.

    +

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

    Parameters

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

    Returns

    +
    +
    Surface
    +
     
    -
    - -Expand source code - -
    def set_minimum_mesh_size(self, size):
    -    """
    -    Set the minimum mesh size possible. Especially useful when geometric elements touch
    -    the optical axis and cause singularities when used with `size_from_distance=True`.
    -    
    -    Parameters
    -    ----------
    -    size : float
    -        The minimum mesh size.  
    -
    -    """
    -    gmsh.option.setNumber('Mesh.MeshSizeMin', size)
    -
    -
    +
    +def starting_point(self) +
    +
    +

    Returns the starting point of the path.

    +

    Returns

    +

    (3,) float

    +

    The starting point of the path.

    -
    -class MEMSStack -(*args, z0=0.0, revolve_factor=0.0, rmax=2, margin=0.5, margin_right=0.1, symmetry=None, **kwargs) +
    +def velocity_vector(self, t)
    -

    Geometry consisting of a stack of MEMS fabricated elements. This geometry is modelled using a stack -of rectangularly shaped elements with a variable spacing in between. Useful when doing calculations on MEMS fabricated -lenses and mirrors.

    +

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

    Parameters

    -
    z0 : float
    -
    Starting z-value to begin building up the MEMS elements from.
    -
    revolve_factor : float
    -
    Revolve the resulting geometry around the optical axis to generate a 3D geometry. When revolve_factor=0.0 a -2D geometry is returned. For 0 < revolve_factor <= 1.0 see the documentation of revolve_around_optical_axis().
    -
    rmax : float
    -
    The rectangular MEMS objects extend to r = r_{max} .
    -
    margin : float
    -
    The distance between the electrodes and the top and bottom boundary.
    -
    margin_right : float
    -
    Distance between the boundary on the right and the MEMS electrodes.
    -
    symmetry : Symmetry
    -
    What symmetry to use for the resulting geometry
    -
    +
    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) +
    +
    +

    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.

    Expand source code -
    class MEMSStack(Geometry):
    -    """Geometry consisting of a stack of MEMS fabricated elements. This geometry is modelled using a stack
    -    of rectangularly shaped elements with a variable spacing in between. Useful when doing calculations on MEMS fabricated
    -    lenses and mirrors.
    +
    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."""
         
    -    Parameters
    -    ----------
    -    z0: float
    -        Starting z-value to begin building up the MEMS elements from.
    -    revolve_factor: float
    -        Revolve the resulting geometry around the optical axis to generate a 3D geometry. When `revolve_factor=0.0` a
    -        2D geometry is returned. For `0 < revolve_factor <= 1.0` see the documentation of `revolve_around_optical_axis`.
    -    rmax: float
    -        The rectangular MEMS objects extend to \( r = r_{max} \).
    -    margin: float
    -        The distance between the electrodes and the top and bottom boundary.
    -    margin_right: float
    -        Distance between the boundary on the right and the MEMS electrodes.
    -    symmetry: `Symmetry`
    -        What symmetry to use for the resulting geometry
    -    """
    +    def __init__(self, paths):
    +        assert all([isinstance(p, Path) for p in paths])
    +        self.paths = paths
    +        self._name = None
         
    -    def __init__(self, *args, z0=0.0, revolve_factor=0.0, rmax=2, margin=0.5, margin_right=0.1, symmetry=None, **kwargs):
    -        
    -        if symmetry is None:
    -            symmetry = Symmetry.RADIAL if revolve_factor == 0.0 else Symmetry.THREE_D
    -        else:
    -            symmetry = symmetry
    -            
    -            if revolve_factor == 0.0 and symmetry.is_3d():
    -                revolve_factor = 1.0
    +    @property
    +    def name(self):
    +        return self._name
     
    -        super().__init__(symmetry, *args, **kwargs)
    +    @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):
    +        """See `Path.mesh`"""
    +        mesh = Mesh()
             
    -        self.z0 = z0
    -        self.revolve_factor = revolve_factor
    -        self.rmax = rmax
    +        name = self.name if name is None else name
             
    -        self.margin = margin
    -        self.margin_right = margin_right
    -         
    -        self._current_z = z0 + margin
    -        self._last_name = None
    -    
    -    def add_spacer(self, thickness):
    -        """
    +        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)
     
    -        Parameters
    -        ----------
    -        thickness : float
    -            Add the given amount of space between the previous and next electrode.
    +        return mesh
     
    -        """
    -        self._current_z += thickness
    +    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_boundary(self):
    -        points = [[0.0, self._current_z+self.margin],
    -                  [self.rmax+self.margin_right, self._current_z+self.margin],
    -                  [self.rmax+self.margin_right, self.z0],
    -                  [0.0, self.z0]]
    -         
    -        self._add_lines_from_points(points, 'boundary')
    -        self._current_z += self.margin
    -     
    -    def _add_lines_from_points(self, points, name):
    -        if self.is_3d():
    -            points = [self.add_point([p[0], 0.0, p[1]]) for p in points[::-1]]
    -        else:
    -            points = [self.add_point(p) for p in points]
    +    def __add__(self, other):
    +        """Allows you to combine paths and path collection using the + operator (path1 + path2)."""
    +        if not isinstance(other, Path) and not isinstance(other, PathCollection):
    +            return NotImplemented
             
    -        Np = len(points)
    -        lines = [self.add_line(points[i], points[j]) for i, j in zip(range(0,Np-1), range(1,Np))]
    -         
    -        if self.is_3d():
    -            revolved = revolve_around_optical_axis(self, lines, self.revolve_factor)
    -            self.add_physical(revolved, name)
    +        if isinstance(other, Path):
    +            return PathCollection(self.paths+[other])
             else:
    -            self.add_physical(lines, name)
    -
    -    def add_electrode(self, radius, thickness, name):
    -        """
    +            return PathCollection(self.paths+other.paths)
    +      
    +    def __iadd__(self, other):
    +        """Allows you to add paths to the collection using the += operator."""
    +        assert isinstance(other, PathCollection) or isinstance(other, Path)
     
    -        Parameters
    -        ----------
    -        radius : float
    -            Distance from the electrode to the optical axis (in mm).
    -            
    -        thickness : float
    -            Thickness of the electrode (in mm).
    -            
    -        name : str
    -            Name to assign to the electode. Needed to later specify the correct excitation.
    -        """
    -        cz = self._current_z
    -        points = [[self.rmax, cz], [radius, cz], [radius, cz+thickness], [self.rmax, cz+thickness]]
    -        self._add_lines_from_points(points, name)
    -        self._current_z += thickness
    -
    -    def generate_line_mesh(self, *args, **kwargs):
    -        self._add_boundary()
    -        return super().generate_line_mesh(*args, **kwargs)
    +        if isinstance(other, Path):
    +            self.paths.append(other)
    +        else:
    +            self.paths.extend(other.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 generate_triangle_mesh(self, *args, **kwargs):
    -        self._add_boundary()
    -        return super().generate_triangle_mesh(*args, **kwargs)
    + def __str__(self): + return f"<PathCollection with {len(self.paths)} surfaces, name: {self.name}>"

    Ancestors

    -

    Methods

    +

    Instance variables

    -
    -def add_electrode(self, radius, thickness, name) -
    +
    prop name
    -

    Parameters

    -
    -
    radius : float
    -
    Distance from the electrode to the optical axis (in mm).
    -
    thickness : float
    -
    Thickness of the electrode (in mm).
    -
    name : str
    -
    Name to assign to the electode. Needed to later specify the correct excitation.
    -
    +
    Expand source code -
    def add_electrode(self, radius, thickness, name):
    -    """
    -
    -    Parameters
    -    ----------
    -    radius : float
    -        Distance from the electrode to the optical axis (in mm).
    -        
    -    thickness : float
    -        Thickness of the electrode (in mm).
    -        
    -    name : str
    -        Name to assign to the electode. Needed to later specify the correct excitation.
    -    """
    -    cz = self._current_z
    -    points = [[self.rmax, cz], [radius, cz], [radius, cz+thickness], [self.rmax, cz+thickness]]
    -    self._add_lines_from_points(points, name)
    -    self._current_z += thickness
    +
    @property
    +def name(self):
    +    return self._name
    -
    -def add_spacer(self, thickness) +
    +

    Methods

    +
    +
    +def __add__(self, other)
    -

    Parameters

    -
    -
    thickness : float
    -
    Add the given amount of space between the previous and next electrode.
    -
    -
    - -Expand source code - -
    def add_spacer(self, thickness):
    -    """
    -
    -    Parameters
    -    ----------
    -    thickness : float
    -        Add the given amount of space between the previous and next electrode.
    -
    -    """
    -    self._current_z += thickness
    -
    +

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

    +
    +
    +def __iadd__(self, other) +
    +
    +

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

    +
    +
    +def extrude(self, vector) +
    +
    +
    +
    +
    +def extrude_by_path(self, p2) +
    +
    +
    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None) +
    +
    + +
    +
    +def revolve_x(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_y(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_z(self, angle=6.283185307179586) +
    +
    +

    Inherited members

    -
    -class Mesh -(symmetry, points=[], lines=[], triangles=[], physical_to_lines={}, physical_to_triangles={}) +
    +class Surface +(fun, path_length1, path_length2, breakpoints1=[], breakpoints2=[], name=None)
    -

    Class containing a mesh. -For now, to make things manageable only lines and triangles are supported. -Lines and triangles can be higher order (curved) or not. But a mesh cannot contain -both curved and simple elements at the same time.

    -

    When the elements are higher order (curved), triangles consists of 6 points and lines of four points. -These correspond with the GMSH line4 and triangle6 types.

    +

    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.

    Expand source code -
    class Mesh(Saveable):
    -    """Class containing a mesh.
    -    For now, to make things manageable only lines and triangles are supported.
    -    Lines and triangles can be higher order (curved) or not. But a mesh cannot contain
    -    both curved and simple elements at the same time.
    -    
    -    When the elements are higher order (curved), triangles consists of 6 points and lines of four points.
    -    These correspond with the GMSH line4 and triangle6 types."""
    +
    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
    +        assert self.path_length1 > 0 and self.path_length2 > 0
    +        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)
          
    -    def __init__(self, symmetry,
    -            points=[],
    -            lines=[],
    -            triangles=[],
    -            physical_to_lines={},
    -            physical_to_triangles={}):
    -        
    -        assert isinstance(symmetry, Symmetry)
    -        self.symmetry = symmetry
    +    @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)
             
    -        # 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)
    +        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
              
    -        if len(lines):
    -            self.lines = np.array(lines)
    -        else:
    -            self.lines = np.empty((0,2), dtype=np.uint64)
    -    
    -        if len(triangles):
    -            self.triangles = np.array(triangles)
    -        else:
    -            self.triangles = np.empty((0, 3), dtype=np.uint64)
    +        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])
              
    -        self.physical_to_lines = physical_to_lines.copy()
    -        self.physical_to_triangles = physical_to_triangles.copy()
    -        
    -        if symmetry.is_2d():
    -            assert points.shape[1] == 2 or np.allclose(points[:, 2], 0.), "Cannot have three dimensional points when symmetry is 2D"
    +        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"""
             
    -        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):
    -        return (len(self.lines) and self.lines.shape[1] == 4) or (len(self.triangles) and self.triangles.shape[1] == 6)
    -    
    -    def move(self, vector):
    -        self.points += vector
    -     
    -    def __add__(self, other):
    -        assert isinstance(other, Mesh)
    -        assert self.symmetry == other.symmetry, "Cannot add meshes with different symmetries"
    -         
    -        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)
    -         
    -        physical_lines = {**self.physical_to_lines, **{k:(v+N_lines) for k, v in other.physical_to_lines.items()}}
    -        physical_triangles = {**self.physical_to_triangles, **{k:(v+N_triangles) for k, v in other.physical_to_triangles.items()}}
    -         
    -        return Mesh(self.symmetry,
    -                        points=points,
    -                        lines=lines,
    -                        triangles=triangles,
    -                        physical_to_lines=physical_lines,
    -                        physical_to_triangles=physical_triangles)
    -    
    -    def extract_physical_group(self, 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))}
    +        length1 = 2*pi*radius
    +        length2 = pi*radius
              
    -        if name in self.physical_to_lines:
    -            return Mesh(self.symmetry, points=points, lines=elements, physical_to_lines=physical_to_elements)
    -        elif name in self.physical_to_triangles:
    -            return Mesh(self.symmetry, points=points, triangles=triangles, physical_to_triangles=physical_to_elements)
    -     
    -    def import_file(filename, symmetry,  name=None):
    -        meshio_obj = meshio.read(filename)
    -        mesh = Mesh.from_meshio(meshio_obj, symmetry)
    -         
    -        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))
    +        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 mesh
    -     
    -    def export_file(self, filename):
    -        meshio_obj = self.to_meshio()
    -        meshio_obj.write(filename)
    +        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)
          
    -    def to_meshio(self):
    -        to_export = []
    +    @staticmethod
    +    def disk_xz(x0, z0, radius):
    +        """Create a disk in the XZ plane.         
             
    -        if len(self.lines):
    -            line_type = 'line' if self.lines.shape[1] == 2 else 'line4'
    -            to_export.append( (line_type, self.lines) )
    +        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.         
             
    -        if len(self.triangles):
    -            triangle_type = 'triangle' if self.triangles.shape[1] == 3 else 'triangle6'
    -            to_export.append( (triangle_type, self.triangles) )
    +        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.
             
    -        return meshio.Mesh(self.points, to_export)
    +        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)
          
    -    def from_meshio(mesh, symmetry):
    -        """Generate a Traceon Mesh from a [meshio](https://github.com/nschloe/meshio) mesh.
    +    @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
    -        ----------
    -        symmetry: Symmetry
    -            Specifies a radially symmetric geometry (RADIAL) or a general 3D geometry (THREE_D).
    +        ------------------------
    +        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
    -        ---------
    -        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')
    +        -----------------------
    +        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.
             
    -        if 'triangle' in mesh.cells_dict:
    -            triangles, physical_triangles = extract('triangle')
    -        elif 'triangle6' in mesh.cells_dict:
    -            triangles, physical_triangles = extract('triangle6')
    +        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.
             
    -        return Mesh(symmetry,
    -            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.
    -
             Returns
    -        ----------------
    -        True if mesh is three dimensional, False if the mesh is two dimensional"""
    -        return self.symmetry.is_3d()
    -    
    -    def is_2d(self):
    -        """Check if the mesh is two dimensional.
    +        -----------------------
    +        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
    -        ----------------
    -        True if mesh is two dimensional, False if the mesh is three dimensional"""
    -        return self.symmetry.is_2d()
    +        -----------------------
    +        Surface representing the rectangle"""
    +        return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]).extrude([xmax-xmin, 0., 0.])
     
    -    def remove_lines(self):
    -        return Mesh(self.symmetry, self.points, triangles=self.triangles, physical_to_triangles=self.physical_to_triangles)
    -    
    -    def remove_triangles(self):
    -        return Mesh(self.symmetry, self.points, lines=self.lines, physical_to_lines=self.physical_to_lines)
    +    @staticmethod
    +    def aperture(height, radius, extent, z=0.):
    +        return Path.aperture(height, radius, extent, z=z).revolve_z()
          
    -    def get_electrodes(self):
    -        """Get the names of all the electrodes in the geometry.
    -         
    -        Returns
    -        ---------
    -        List of electrode names
    +    def __add__(self, other):
    +        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
    +        if not isinstance(other, Surface) and not isinstance(other, SurfaceCollection):
    +            return NotImplemented
     
    -        """
    -        return list(self.physical_to_lines.keys()) + list(self.physical_to_triangles.keys())
    +        if isinstance(other, Surface):
    +            return SurfaceCollection([self, other])
    +        else:
    +            return SurfaceCollection([self] + other.surfaces)
          
    -    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 mesh(self, mesh_size=None, mesh_size_factor=None, name=None):
    +        """Mesh the surface, so it can be used in the BEM solver. The result of meshing
    +        a surface are triangles.
     
    -
    -    def _triangles_to_higher_order(points, elements):
    -        N_elements = len(elements)
    -        N_points = len(points)
    -         
    -        v0, v1, v2 = elements.T
    -        p3 = (points[v0] + points[v1])/2
    -        p4 = (points[v1] + points[v2])/2
    -        p5 = (points[v2] + points[v0])/2
    -         
    -        assert all(p.shape == (N_elements, points.shape[1]) for p in [p3,p4,p5])
    -          
    -        points = np.concatenate( (points, p3, p4, p5), axis=0)
    -          
    -        elements = np.array([
    -            elements[:, 0], elements[:, 1], elements[:, 2],
    -            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),
    -            np.arange(N_points + 2*N_elements, N_points + 3*N_elements, dtype=np.uint64)]).T
    -         
    -        assert np.allclose(p3, points[elements[:, 3]])
    -        assert np.allclose(p4, points[elements[:, 4]])
    -        assert np.allclose(p5, points[elements[:, 5]])
    +        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
             
    -        return points, elements
    +        Returns
    +        ----------------------------
    +        `traceon.mesher.Mesh`"""
    +         
    +        if mesh_size is None:
    +            path_length = min(self.path_length1, self.path_length2)
    +             
    +            mesh_size = path_length / 4
     
    -    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 mesh_size_factor is not None:
    +                mesh_size /= sqrt(mesh_size_factor)
     
    -        if len(lines) and lines.shape[1] == 2:
    -            points, lines = Mesh._lines_to_higher_order(points, lines)
    -        if len(triangles) and triangles.shape[1] == 3:
    -            points, triangles = Mesh._triangles_to_higher_order(points, triangles) 
    -         
    -        return Mesh(self.symmetry,
    -            points=points,
    -            lines=lines, physical_to_lines=self.physical_to_lines,
    -            triangles=triangles, physical_to_triangles=self.physical_to_triangles)
    -     
    +        name = self.name if name is None else name
    +        return _mesh(self, mesh_size, name=name)
    +    
         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 {self.symmetry},\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}>'
    + return f"<Surface with name: {self.name}>"

    Ancestors

    -

    Methods

    +

    Static methods

    -
    -def export_file(self, filename) +
    +def aperture(height, radius, extent, z=0.0)
    -
    - -Expand source code - -
    def export_file(self, filename):
    -    meshio_obj = self.to_meshio()
    -    meshio_obj.write(filename)
    -
    -
    -def extract_physical_group(self, name) +
    +def box(p0, p1)
    -
    -
    - -Expand source code - -
    def extract_physical_group(self, 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(self.symmetry, points=points, lines=elements, physical_to_lines=physical_to_elements)
    -    elif name in self.physical_to_triangles:
    -        return Mesh(self.symmetry, points=points, triangles=triangles, physical_to_triangles=physical_to_elements)
    -
    +

    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 from_meshio(mesh, symmetry) +
    +def disk_xy(x0, y0, radius)
    -

    Generate a Traceon Mesh from a meshio mesh.

    +

    Create a disk in the XY plane.

    Parameters

    -
    symmetry : Symmetry
    -
    Specifies a radially symmetric geometry (RADIAL) or a general 3D geometry (THREE_D).
    +
    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

    -
    Mesh
    +
    Surface
     
    -
    - -Expand source code - -
    def from_meshio(mesh, symmetry):
    -    """Generate a Traceon Mesh from a [meshio](https://github.com/nschloe/meshio) mesh.
    -    
    -    Parameters
    -    ----------
    -    symmetry: Symmetry
    -        Specifies a radially symmetric geometry (RADIAL) or a general 3D geometry (THREE_D).
    -    
    -    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(symmetry,
    -        points=mesh.points,
    -        lines=lines, physical_to_lines=physical_lines,
    -        triangles=triangles, physical_to_triangles=physical_triangles)
    -
    -
    -def get_electrodes(self) +
    +def disk_xz(x0, z0, radius)
    -

    Get the names of all the electrodes in the geometry.

    +

    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

    -
    List of electrode names
    +
    Surface
     
    -
    - -Expand source code - -
    def get_electrodes(self):
    -    """Get the names of all the electrodes in the geometry.
    -     
    -    Returns
    -    ---------
    -    List of electrode names
    -
    -    """
    -    return list(self.physical_to_lines.keys()) + list(self.physical_to_triangles.keys())
    -
    -
    -def import_file(filename, symmetry, name=None) +
    +def disk_yz(y0, z0, radius)
    -
    -
    - -Expand source code - -
    def import_file(filename, symmetry,  name=None):
    -    meshio_obj = meshio.read(filename)
    -    mesh = Mesh.from_meshio(meshio_obj, symmetry)
    -     
    -    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 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 is_2d(self) +
    +def from_boundary_paths(p1, p2, p3, p4)
    -

    Check if the mesh is two dimensional.

    +

    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

    -
    True if mesh is two dimensional, False if the mesh is three dimensional
    +
    Surface with the four giving paths as the boundary
     
    -
    - -Expand source code - -
    def is_2d(self):
    -    """Check if the mesh is two dimensional.
    -    
    -    Returns
    -    ----------------
    -    True if mesh is two dimensional, False if the mesh is three dimensional"""
    -    return self.symmetry.is_2d()
    -
    -
    -def is_3d(self) +
    +def rectangle_xy(xmin, xmax, ymin, ymax)
    -

    Check if the mesh is three dimensional.

    +

    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

    -
    True if mesh is three dimensional, False if the mesh is two dimensional
    +
    Surface representing the rectangle
     
    -
    - -Expand source code - -
    def is_3d(self):
    -    """Check if the mesh is three dimensional.
    -
    -    Returns
    -    ----------------
    -    True if mesh is three dimensional, False if the mesh is two dimensional"""
    -    return self.symmetry.is_3d()
    -
    -
    -def is_higher_order(self) +
    +def rectangle_xz(xmin, xmax, zmin, zmax)
    -
    -
    - -Expand source code - -
    def is_higher_order(self):
    -    return (len(self.lines) and self.lines.shape[1] == 4) or (len(self.triangles) and self.triangles.shape[1] == 6)
    -
    +

    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 move(self, vector) +
    +def rectangle_yz(ymin, ymax, zmin, zmax)
    -
    -
    - -Expand source code - -
    def move(self, vector):
    -    self.points += vector
    -
    +

    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 remove_lines(self) +
    +def spanned_by_paths(path1, path2)
    -
    -
    - -Expand source code - -
    def remove_lines(self):
    -    return Mesh(self.symmetry, self.points, triangles=self.triangles, physical_to_triangles=self.physical_to_triangles)
    -
    +

    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 remove_triangles(self) +
    +def sphere(radius)
    -
    -
    - -Expand source code - -
    def remove_triangles(self):
    -    return Mesh(self.symmetry, self.points, lines=self.lines, physical_to_lines=self.physical_to_lines)
    -
    +

    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) +
    +
    +

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

    -
    -def to_meshio(self) +
    +def __call__(self, u, v)
    -
    -
    - -Expand source code - -
    def to_meshio(self):
    -    to_export = []
    -    
    -    if len(self.lines):
    -        line_type = 'line' if self.lines.shape[1] == 2 else 'line4'
    -        to_export.append( (line_type, self.lines) )
    -    
    -    if len(self.triangles):
    -        triangle_type = 'triangle' if self.triangles.shape[1] == 3 else 'triangle6'
    -        to_export.append( (triangle_type, self.triangles) )
    -    
    -    return meshio.Mesh(self.points, to_export)
    -
    +

    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 mesh(self, mesh_size=None, mesh_size_factor=None, name=None) +
    +
    +

    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

    +

    Inherited members

    +
    -
    -class Symmetry -(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +class SurfaceCollection +(surfaces)
    -

    Symmetry of the geometry. 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.

    +

    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.

    Expand source code -
    class Symmetry(Enum):
    -    """
    -    Symmetry of the geometry. 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
    +
    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):
    +        """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)
    +         
    +        return mesh
    +     
    +    def __add__(self, other):
    +        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
    +        if not isinstance(other, Surface) and not isinstance(other, SurfaceCollection):
    +            return NotImplemented
    +              
    +        if isinstance(other, Surface):
    +            return SurfaceCollection(self.surfaces+[other])
    +        else:
    +            return SurfaceCollection(self.surfaces+other.surfaces)
    +     
    +    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 __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
    + return f"<SurfaceCollection with {len(self.surfaces)} surfaces, name: {self.name}>"

    Ancestors

    -

    Class variables

    +

    Instance variables

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

    Methods

    -
    -def is_2d(self) +
    +def __add__(self, other)
    -
    -
    - -Expand source code - -
    def is_2d(self):
    -    return self == Symmetry.RADIAL
    -
    +

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

    -
    -def is_3d(self) +
    +def __iadd__(self, other)
    -
    -
    - -Expand source code - -
    def is_3d(self):
    -    return self == Symmetry.THREE_D
    -
    +

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

    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, name=None) +
    +
    +
    +

    Inherited members

    +
    - \ No newline at end of file + diff --git a/docs/docs/latest/index.html b/docs/docs/latest/index.html index d32c258..cb9b6e9 100644 --- a/docs/docs/latest/index.html +++ b/docs/docs/latest/index.html @@ -2,19 +2,23 @@ - - + + traceon API documentation - - - - - - + + + + + + + - - + +
    @@ -29,13 +33,13 @@

    Package traceon

    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 AGPLv3 license.

    +

    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_bem() function, which computes the resulting field. +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 @@ -48,58 +52,6 @@

    Validations

    Units

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

    -
    - -Expand source code - -
    """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](https://github.com/leon-vv/Traceon).
    -
    -The software is currently distributed under the `AGPLv3` 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 `traceon.solver.solve_bem` function, which computes the resulting field. 
    -The field can be passed to the `traceon.tracing.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](https://github.com/leon-vv/Traceon/tree/main/validation) directory in the Github project. After installing Traceon, the validations can be 
    -executed as follows:
    -
    -```bash
    -    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} \).
    -"""
    -
    -import warnings
    -
    -__pdoc__ = {}
    -__pdoc__['util'] = False
    -__pdoc__['backend'] = False
    -__pdoc__['data'] = False
    -__pdoc__['fast_multipole_method'] = False
    -__pdoc__['traceon.tracing.Tracer.__call__'] = True
    -
    -warnings.filterwarnings('ignore', '.*The value of the smallest subnormal for.* type is zero.')
    -

    Sub-modules

    @@ -115,8 +67,21 @@

    Sub-modules

    traceon.geometry
    -

    The geometry module allows the creation of general geometries in 2D and 3D and generate -the resulting meshes. The heavy lifting is done by the …

    +

    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.interpolation
    +
    +
    +
    +
    traceon.logging
    +
    +
    +
    +
    traceon.mesher
    +
    +
    traceon.plotting
    @@ -143,7 +108,6 @@

    Sub-modules

    - \ No newline at end of file + diff --git a/docs/docs/latest/interpolation.html b/docs/docs/latest/interpolation.html new file mode 100644 index 0000000..1520734 --- /dev/null +++ b/docs/docs/latest/interpolation.html @@ -0,0 +1,374 @@ + + + + + + +traceon.interpolation API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.interpolation

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class FieldAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +

    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.

    +
    + +Expand source code + +
    class FieldAxial(S.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)
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +

    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, S.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: S.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.TracerRadialAxial(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +def get_tracer(self, bounds) +
    +
    +
    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/latest/logging.html b/docs/docs/latest/logging.html new file mode 100644 index 0000000..7142c27 --- /dev/null +++ b/docs/docs/latest/logging.html @@ -0,0 +1,148 @@ + + + + + + +traceon.logging API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.logging

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +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'.

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class LogLevel +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    Enumeration representing a certain verbosity of logging.

    +
    + +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."""
    +
    +

    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.

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/latest/mesher.html b/docs/docs/latest/mesher.html new file mode 100644 index 0000000..9369e62 --- /dev/null +++ b/docs/docs/latest/mesher.html @@ -0,0 +1,933 @@ + + + + + + +traceon.mesher API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.mesher

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class GeometricObject +
    +
    +

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

    +
    + +Expand source code + +
    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."""
    +    
    +    @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 counter clockwise 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"
    +        origin = np.array(origin)
    +        assert origin.shape == (3,), "Please supply a 3D point for origin"
    +         
    +        if Rx != 0.:
    +            matrix = np.array([[1, 0, 0],
    +                [0, np.cos(Rx), -np.sin(Rx)],
    +                [0, np.sin(Rx), np.cos(Rx)]])
    +        elif Ry != 0.:
    +            matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)],
    +                [0, 1, 0],
    +                [-np.sin(Ry), 0, np.cos(Ry)]])
    +        elif Rz != 0.:
    +            matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0],
    +                [np.sin(Rz), np.cos(Rz), 0],
    +                [0, 0, 1]])
    +
    +        return self.map_points(lambda p: origin + matrix @ (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]]))
    +
    +

    Ancestors

    +
      +
    • abc.ABC
    • +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def map_points(self, fun: Callable[[numpy.ndarray], numpy.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 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.

    +
    +
    +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.

    +
    +
    +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.

    +
    +
    +def move(self, dx=0.0, dy=0.0, dz=0.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.

    +
    +
    +def rotate(self, Rx=0.0, Ry=0.0, Rz=0.0, origin=[0.0, 0.0, 0.0]) +
    +
    +

    Rotate counter clockwise 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.

    +
    +
    +
    +
    +class Mesh +(points=[], lines=[], triangles=[], physical_to_lines={}, physical_to_triangles={}) +
    +
    +

    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.

    +
    + +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 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()
    +        
    +        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')
    +    
    +    @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}>'
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def from_meshio(mesh: meshio._mesh.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 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
    +
     
    +
    +
    +
    +

    Methods

    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def flip_normals(self) +
    +
    +

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

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def get_electrodes(self) +
    +
    +

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

    +

    Returns

    +
    +
    str iterable
    +
     
    +
    Names
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def is_higher_order(self) +
    +
    +

    Whether the mesh contains higher order elements.

    +

    Returns

    +
    +
    bool
    +
     
    +
    +
    +
    +def map_points(self, fun) +
    +
    + +
    +
    +def remove_lines(self) +
    +
    +

    Remove all the lines from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def remove_triangles(self) +
    +
    +

    Remove all triangles from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def to_meshio(self) +
    +
    +

    Convert the Mesh to a meshio object.

    +

    Returns

    +
    +
    meshio.Mesh
    +
     
    +
    +
    +
    +def write(self, 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) +
    +
    +

    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

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/latest/plotting.html b/docs/docs/latest/plotting.html index c9c3b4a..a38e672 100644 --- a/docs/docs/latest/plotting.html +++ b/docs/docs/latest/plotting.html @@ -2,20 +2,24 @@ - - + + traceon.plotting API documentation - - - - - +to show the line and triangle meshes generated by …"> + + + + + + - - + +
    @@ -26,162 +30,6 @@

    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.

    -
    - -Expand source code - -
    """The `traceon.plotting` module uses the `vedo` plotting library to provide some convenience functions
    -to show the line and triangle meshes generated by Traceon."""
    -
    -from math import sqrt
    -from scipy.interpolate import *
    -import numpy as np
    -import vedo
    -
    -from . import backend
    -from .geometry import Symmetry
    -
    -def _create_point_to_physical_dict(mesh):
    -    d = {}
    -    
    -    for physical, elements in [(mesh.physical_to_lines, mesh.lines), (mesh.physical_to_triangles, mesh.triangles)]:
    -        for k, v in physical.items():
    -            for element_index in v:
    -                for p in elements[element_index]:
    -                    d[p] = k
    -     
    -    return d
    -
    -
    -def plot_mesh(mesh, show_normals=False, show_legend=True, **colors):
    -    """Plot mesh using the Vedo library. Optionally showing normal vectors.
    -
    -    Parameters
    -    ---------------------
    -    show_normals: bool
    -        Whether to show the normal vectors at every element
    -    show_legend: bool
    -        Whether to show the legend
    -    colors: dict of (string, string)
    -        Use keyword arguments to specify colors, for example `plot_mesh(mesh, lens='blue', ground='green')`
    -    """
    -    plotter = vedo.Plotter()
    -    points_to_physical = _create_point_to_physical_dict(mesh)
    -    legend_entries = []
    -    
    -    if len(mesh.triangles):
    -        meshes = _plot_triangle_mesh(mesh, plotter, points_to_physical, show_normals=show_normals, **colors)
    -        legend_entries.extend(meshes)
    -    
    -    if len(mesh.lines) and not mesh.is_3d():
    -        lines = _plot_line_mesh(mesh, plotter, points_to_physical, show_normals=show_normals, **colors)
    -        legend_entries.extend(lines)
    -
    -    if show_legend:
    -        lb = vedo.LegendBox(legend_entries)
    -        plotter += lb
    -    
    -    if mesh.points.shape[1] == 3 and np.any(mesh.points[:, 2] != 0.):
    -        plotter.show(viewup='z', axes={'xtitle': 'x (mm)', 'ytitle': 'y (mm)', 'ztitle': 'z (mm)'})
    -    else:
    -        plotter.show(axes={'xtitle': 'x (mm)', 'ytitle': 'y (mm)'})
    -
    -def _plot_triangle_mesh(mesh, plotter, points_to_physical, show_normals=False, **phys_colors):
    -    triangles = mesh.triangles[:, :3]
    -    normals = np.array([backend.normal_3d(*mesh.points[t]) for t in triangles])
    -    
    -    colors = np.full(len(triangles), '#CCCCCC')
    -    dict_ = points_to_physical
    -    
    -    for i, (A, B, C) in enumerate(triangles):
    -        if A in dict_ and B in dict_ and C in dict_:
    -            phys1, phys2, phys3 = dict_[A], dict_[B], dict_[C]
    -            if phys1 == phys2 and phys2 == phys3 and phys1 in phys_colors:
    -                colors[i] = phys_colors[phys1]
    -     
    -    meshes = []
    -    
    -    for c in set(colors):
    -        mask = colors == c
    -        vm = vedo.Mesh([mesh.points, triangles[mask]], c)
    -        vm.linecolor('black').linewidth(2)
    -        
    -        key = [k for k, col in phys_colors.items() if c==col]
    -        if len(key):
    -            vm.legend(key[0])
    -        
    -        plotter += vm
    -        meshes.append(vm)
    -    
    -    if show_normals:
    -        start_to_end = np.zeros( (len(triangles), 6) )
    -        for i, t in enumerate(triangles):
    -            v1, v2, v3 = mesh.points[t]
    -            middle = (v1 + v2 + v3)/3
    -            area = 1/2*np.linalg.norm(np.cross(v2-v1, v3-v1))
    -            side_length = sqrt( (4*area) / sqrt(3) ) # Equilateral triangle, side length with equal area
    -            normal = 0.75*side_length*normals[i]
    -            start_to_end[i] = [*middle, *(middle+normal)]
    -         
    -        arrows = vedo.shapes.Arrows(start_to_end[:, :3], start_to_end[:, 3:], res=20, c='black')
    -        plotter.add(arrows)
    -     
    -    return meshes
    -
    -
    -def _plot_line_mesh(mesh, plotter, points_to_physical, show_normals=False, **phys_colors):
    -    start = []
    -    end = []
    -    colors_ = []
    -    normals = []
    -    dict_ = points_to_physical
    -    lines = mesh.lines[:, :2]
    -     
    -    for i, (A, B) in enumerate(lines):
    -            color = '#CCCCC'
    -            
    -            if A in dict_ and B in dict_:
    -                phys1, phys2 = dict_[A], dict_[B]
    -                if phys1 == phys2 and phys1 in phys_colors:
    -                    color = phys_colors[phys1]
    -            
    -            p1, p2 = mesh.points[A], mesh.points[B]
    -            start.append(p1)
    -            end.append(p2)
    -            colors_.append(color)
    -            normals.append(backend.normal_2d(p1[:2], p2[:2]))
    -     
    -    start, end = np.array(start), np.array(end)
    -    colors_ = np.array(colors_)
    -    vedo_lines = []
    -     
    -    for c in set(colors_):
    -        mask = colors_ == c
    -        l = vedo.Lines(start[mask], end[mask], lw=3, c=c)
    -        
    -        key = [k for k, col in phys_colors.items() if c==col]
    -        if len(key):
    -            l.legend(key[0])
    -         
    -        plotter += l
    -        vedo_lines.append(l)
    -     
    -    if show_normals:
    -        arrows_to_plot = np.zeros( (len(normals), 4) )
    -        
    -        for i, (v1, v2) in enumerate(zip(start, end)):
    -            v1, v2 = v1[:2], v2[:2]
    -            middle = (v1 + v2)/2
    -            length = np.linalg.norm(v2-v1)
    -            normal = 3*length*normals[i]
    -            arrows_to_plot[i] = [*middle, *(middle+normal)]
    -        
    -        arrows = vedo.shapes.Arrows(arrows_to_plot[:, :2], arrows_to_plot[:, 2:], c='black')
    -        plotter.add(arrows)
    -
    -    return vedo_lines
    -    
    -
    @@ -204,43 +52,6 @@

    Parameters

    colors : dict of (string, string)
    Use keyword arguments to specify colors, for example plot_mesh(mesh, lens='blue', ground='green')
    -
    - -Expand source code - -
    def plot_mesh(mesh, show_normals=False, show_legend=True, **colors):
    -    """Plot mesh using the Vedo library. Optionally showing normal vectors.
    -
    -    Parameters
    -    ---------------------
    -    show_normals: bool
    -        Whether to show the normal vectors at every element
    -    show_legend: bool
    -        Whether to show the legend
    -    colors: dict of (string, string)
    -        Use keyword arguments to specify colors, for example `plot_mesh(mesh, lens='blue', ground='green')`
    -    """
    -    plotter = vedo.Plotter()
    -    points_to_physical = _create_point_to_physical_dict(mesh)
    -    legend_entries = []
    -    
    -    if len(mesh.triangles):
    -        meshes = _plot_triangle_mesh(mesh, plotter, points_to_physical, show_normals=show_normals, **colors)
    -        legend_entries.extend(meshes)
    -    
    -    if len(mesh.lines) and not mesh.is_3d():
    -        lines = _plot_line_mesh(mesh, plotter, points_to_physical, show_normals=show_normals, **colors)
    -        legend_entries.extend(lines)
    -
    -    if show_legend:
    -        lb = vedo.LegendBox(legend_entries)
    -        plotter += lb
    -    
    -    if mesh.points.shape[1] == 3 and np.any(mesh.points[:, 2] != 0.):
    -        plotter.show(viewup='z', axes={'xtitle': 'x (mm)', 'ytitle': 'y (mm)', 'ztitle': 'z (mm)'})
    -    else:
    -        plotter.show(axes={'xtitle': 'x (mm)', 'ytitle': 'y (mm)'})
    -
    @@ -248,7 +59,6 @@

    Parameters

    - \ No newline at end of file + diff --git a/docs/docs/latest/solver.html b/docs/docs/latest/solver.html index 9da3816..6c969d6 100644 --- a/docs/docs/latest/solver.html +++ b/docs/docs/latest/solver.html @@ -2,20 +2,24 @@ - - + + traceon.solver API documentation - - - - - +geometry and excitation. Once the …"> + + + + + + - - + +
    @@ -54,1266 +58,6 @@

    Radial series expansion in 3D

    References

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

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

    -
    - -Expand source code - -
    """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.
    -
    -"""
    -
    -__pdoc__ = {}
    -__pdoc__['EffectivePointCharges'] = False
    -__pdoc__['ElectrostaticSolver'] = False
    -__pdoc__['MagnetostaticSolver'] = False
    -__pdoc__['Solver'] = False
    -
    -import math as m
    -import time
    -from threading import Thread
    -import os.path as path
    -import copy
    -
    -import numpy as np
    -from scipy.interpolate import CubicSpline, BPoly, PPoly
    -from scipy.special import legendre
    -from scipy.integrate import quad
    -
    -from . import geometry as G
    -from . import excitation as E
    -from . import backend
    -from . import util
    -from . import fast_multipole_method
    -
    -FACTOR_AXIAL_DERIV_SAMPLING_2D = 0.2
    -FACTOR_AXIAL_DERIV_SAMPLING_3D = 0.06
    -
    -class Solver:
    -    
    -    def __init__(self, excitation):
    -        self.excitation = excitation
    -        vertices, names = self.get_active_elements()
    -        
    -        self.excitation = excitation
    -        self.vertices = vertices
    -        self.names = names
    -         
    -        N = len(vertices)
    -        excitation_types = np.zeros(N, dtype=np.uint8)
    -        excitation_values = np.zeros(N)
    -         
    -        for n, indices in names.items():
    -            type_ = excitation.excitation_types[n][0]
    -            excitation_types[indices] = int(type_)
    -            
    -            if type_ != E.ExcitationType.VOLTAGE_FUN:
    -                excitation_values[indices] = excitation.excitation_types[n][1]
    -            else:
    -                function = excitation.excitation_types[n][1]
    -                excitation_values[indices] = [function(*self.get_center_of_element(i)) for i in indices]
    -          
    -        self.excitation_types = excitation_types
    -        self.excitation_values = excitation_values
    -
    -        two_d = self.is_2d()
    -        higher_order = self.is_higher_order()
    -        
    -        if two_d and higher_order:
    -            jac, pos = backend.fill_jacobian_buffer_radial(vertices)
    -        elif not two_d and higher_order:
    -            jac, pos = backend.fill_jacobian_buffer_3d_higher_order(vertices)
    -        elif not two_d and not higher_order:
    -            jac, pos = backend.fill_jacobian_buffer_3d(vertices)
    -        else:
    -            raise ValueError('Input excitation is 2D but not higher order, this solver input is currently not supported. Consider upgrading mesh to higher order.')
    -        
    -        self.jac_buffer = jac
    -        self.pos_buffer = pos
    -     
    -    def get_active_elements(self):
    -        pass
    -
    -    def get_number_of_matrix_elements(self):
    -        return len(self.vertices)
    -        
    -    def is_higher_order(self):
    -        return self.excitation.mesh.is_higher_order()
    -        
    -    def is_3d(self):
    -        return self.excitation.mesh.is_3d()
    -    
    -    def is_2d(self):
    -        return self.excitation.mesh.is_2d()
    -     
    -    def get_flux_indices(self):
    -        """Get the indices of the vertices that are of type DIELECTRIC or MAGNETIZABLE.
    -        For these indices we don't compute the potential but the flux through the element (the inner 
    -        product of the field with the normal of the vertex. The method is implemented in the derived classes."""
    -        pass
    -         
    -    def get_center_of_element(self, index):
    -        two_d = self.is_2d()
    -        higher_order = self.is_higher_order()
    -         
    -        if not self.is_higher_order():
    -            return np.mean(self.vertices[index], axis=0)
    -        elif two_d:
    -            v = self.vertices
    -            return backend.position_and_jacobian_radial(0, v[index, 0], v[index, 2], v[index, 3], v[index, 1])[1]
    -        else:
    -            return backend.position_and_jacobian_3d(0.5, 0.5, self.vertices[index])[1]
    -     
    -    def get_right_hand_side(self):
    -        pass
    -         
    -    def get_matrix(self):
    -        assert self.is_higher_order(), "Can only produce matrix for higher order meshes. Consider upgrading your mesh."
    -        
    -        N_matrix = self.get_number_of_matrix_elements()
    -        matrix = np.zeros( (N_matrix, N_matrix) )
    -        print(f'Using matrix solver, number of elements: {N_matrix}, size of matrix: {N_matrix} ({matrix.nbytes/1e6:.0f} MB), symmetry: {self.excitation.mesh.symmetry}, higher order: {self.excitation.mesh.is_higher_order()}')
    -        
    -        fill_fun = backend.fill_matrix_3d if self.is_3d() else backend.fill_matrix_radial
    -        
    -        def fill_matrix_rows(rows):
    -            fill_fun(matrix,
    -                self.vertices,
    -                self.excitation_types,
    -                self.excitation_values,
    -                self.jac_buffer, self.pos_buffer, rows[0], rows[-1])
    -        
    -        st = time.time()
    -        util.split_collect(fill_matrix_rows, np.arange(N_matrix))    
    -        print(f'Time for building matrix: {(time.time()-st)*1000:.0f} ms')
    -        
    -        assert np.all(np.isfinite(matrix))
    -         
    -        return matrix
    -    
    -    def charges_to_field(self, charges):
    -        pass
    -         
    -    def solve_matrix(self, right_hand_side=None):
    -        F = np.array([self.get_right_hand_side()]) if right_hand_side is None else right_hand_side
    -        
    -        N = self.get_number_of_matrix_elements()
    -         
    -        if N == 0:
    -            return [self.charges_to_field(EffectivePointCharges.empty_2d() if self.is_2d() else EffectivePointCharges.empty_3d()) \
    -                        for _ in range(len(F))]
    -        
    -        assert all([f.shape == (N,) for f in F])
    -        matrix = self.get_matrix()
    -         
    -        st = time.time()
    -        charges = np.linalg.solve(matrix, F.T).T
    -        print(f'Time for solving matrix: {(time.time()-st)*1000:.0f} ms')
    -        assert np.all(np.isfinite(charges)) and charges.shape == F.shape
    -        
    -        result = [self.charges_to_field(EffectivePointCharges(c, self.jac_buffer, self.pos_buffer)) for c in charges]
    -         
    -        assert len(result) == len(F)
    -        return result
    -        
    -    def solve_fmm(self, precision=0):
    -        assert self.is_3d() and not self.is_higher_order(), "Fast multipole method is only supported for simple 3D geometries (non higher order triangles)."
    -        assert isinstance(precision, int) and -2 <= precision <= 5, "Precision should be an intenger -2 <= precision <= 5"
    -         
    -        triangles = self.vertices
    -        print(f'Using FMM solver, number of elements: {len(triangles)}, symmetry: {self.excitation.mesh.symmetry}, precision: {precision}')
    -        
    -        N = len(triangles)
    -        assert triangles.shape == (N, 3, 3)
    -         
    -        F = self.get_right_hand_side()
    -        assert F.shape == (N,)
    -         
    -        st = time.time()
    -        dielectric_indices = self.get_flux_indices()
    -        dielectric_values = self.excitation_values[dielectric_indices]
    -        charges, count = fast_multipole_method.solve_iteratively(self.vertices, dielectric_indices, dielectric_values, F, precision=precision)
    -        print(f'Time for solving FMM: {(time.time()-st)*1000:.0f} ms (iterations: {count})')
    -        
    -        return self.charges_to_field(EffectivePointCharges(charges, self.jac_buffer, self.pos_buffer))
    -
    -class ElectrostaticSolver(Solver):
    -    
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -    
    -    def get_flux_indices(self):
    -        N = self.get_number_of_matrix_elements()
    -        return np.arange(N)[self.excitation_types == int(E.ExcitationType.DIELECTRIC)]
    -
    -    def get_active_elements(self):
    -        return self.excitation.get_electrostatic_active_elements()
    -     
    -    def get_right_hand_side(self):
    -        N = self.get_number_of_matrix_elements()
    -        F = np.zeros( (N,) )
    -         
    -        assert self.excitation_types.shape ==(N,) and self.excitation_values.shape == (N,)
    -         
    -        # TODO: optimize in backend?
    -        for i, (type_, value) in enumerate(zip(self.excitation_types, self.excitation_values)):
    -            if type_ in [E.ExcitationType.VOLTAGE_FIXED, E.ExcitationType.VOLTAGE_FUN]:
    -                F[i] = value
    -            elif type_ == E.ExcitationType.DIELECTRIC:
    -                F[i] = 0
    -         
    -        assert np.all(np.isfinite(F))
    -        return F
    -
    -    def charges_to_field(self, charges):
    -        if self.is_3d():
    -            return Field3D_BEM(electrostatic_point_charges=charges)
    -        else:
    -            return FieldRadialBEM(electrostatic_point_charges=charges)
    -
    -class MagnetostaticSolver(Solver):
    -    
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -         
    -        # Field produced by the current excitations on the coils
    -        self.current_charges = self.get_current_charges()
    -        self.current_field = FieldRadialBEM(current_point_charges=self.current_charges)
    -         
    -        # TODO: optimize in backend?
    -        N = len(self.vertices) 
    -        normals = np.zeros( (N, 2) if self.is_2d() else (N, 3) )
    -        
    -        for i, v in enumerate(self.vertices):
    -            if self.is_2d() and not self.is_higher_order():
    -                normals[i] = backend.normal_2d(v[0], v[1])
    -            elif self.is_2d() and self.is_higher_order():
    -                normals[i] = backend.higher_order_normal_radial(0.0, v[:, :2])
    -            elif self.is_3d() and not self.is_higher_order():
    -                normals[i] = backend.normal_3d(*v)
    -            elif self.is_3d() and self.is_higher_order():
    -                normals[i] = backend.higher_order_normal_3d(1/3, 1/3, v)
    -
    -        self.normals = normals
    -    
    -    def get_active_elements(self):
    -        return self.excitation.get_magnetostatic_active_elements()
    -    
    -    def get_flux_indices(self):
    -        N = self.get_number_of_matrix_elements()
    -        return np.arange(N)[self.excitation_types == int(E.ExcitationType.MAGNETIZABLE)]
    -     
    -    def get_current_charges(self):
    -        currents = []
    -        jacobians = []
    -        positions = []
    -        
    -        mesh = self.excitation.mesh
    -        
    -        if not len(mesh.triangles) or not self.excitation.has_current():
    -            return EffectivePointCharges.empty_3d()
    -         
    -        jac, pos = backend.fill_jacobian_buffer_3d_higher_order(mesh.points[mesh.triangles])
    -        
    -        for n, v in self.excitation.excitation_types.items():
    -            if not v[0] == E.ExcitationType.CURRENT or not n in mesh.physical_to_triangles:
    -                continue
    -            
    -            indices = mesh.physical_to_triangles[n]
    -            
    -            if not len(indices):
    -                continue
    -             
    -            # Current supplied is total current, not a current density, therefore
    -            # divide by the area.
    -            area = np.sum(jac[indices])
    -            currents.extend(np.full(len(indices), v[1]/area))
    -            jacobians.extend(jac[indices])
    -            positions.extend(pos[indices])
    -        
    -        if not len(currents):
    -            return EffectivePointCharges.empty_3d()
    -        
    -        return EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions))
    -     
    -    def get_right_hand_side(self):
    -        st = time.time()
    -        N = self.get_number_of_matrix_elements()
    -        F = np.zeros( (N,) )
    -         
    -        assert self.excitation_types.shape ==(N,) and self.excitation_values.shape == (N,)
    -         
    -        # TODO: optimize in backend?
    -        for i, (type_, value) in enumerate(zip(self.excitation_types, self.excitation_values)):
    -            if type_ == E.ExcitationType.MAGNETOSTATIC_POT:
    -                F[i] = value
    -            elif type_ == E.ExcitationType.MAGNETIZABLE:
    -                # Here we compute the inner product of the field generated by the current excitations
    -                # and the normal vector of the vertex.
    -                center = self.get_center_of_element(i)
    -                field_at_center = self.current_field.current_field_at_point(center)
    -                #flux_to_charge_factor = (value - 1)/np.pi
    -                F[i] = -backend.flux_density_to_charge_factor(value) * np.dot(field_at_center, self.normals[i])
    -         
    -        assert np.all(np.isfinite(F))
    -        print(f'Computing right hand side of linear system took {(time.time()-st)*1000:.0f} ms')
    -        return F
    -     
    -    def charges_to_field(self, charges):
    -        if self.is_3d():
    -            return Field3D_BEM(magnetostatic_point_charges=charges)
    -        else:
    -            return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_charges)
    -
    -class EffectivePointCharges:
    -    def __init__(self, charges, jacobians, positions):
    -        self.charges = np.array(charges, dtype=np.float64)
    -        self.jacobians = np.array(jacobians, dtype=np.float64)
    -        self.positions = np.array(positions, dtype=np.float64)
    -         
    -        N = len(self.charges)
    -        N_QUAD = self.jacobians.shape[1]
    -        assert self.charges.shape == (N,) and self.jacobians.shape == (N, N_QUAD)
    -        assert self.positions.shape == (N, N_QUAD, 3) or self.positions.shape == (N, N_QUAD, 2)
    -
    -
    -    def empty_2d():
    -        N_QUAD_2D = backend.N_QUAD_2D
    -        return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0,N_QUAD_2D,2)))
    -
    -    def empty_3d():
    -        N_TRIANGLE_QUAD = backend.N_TRIANGLE_QUAD
    -        return EffectivePointCharges(np.empty((0,)), np.empty((0, N_TRIANGLE_QUAD)), np.empty((0, N_TRIANGLE_QUAD, 3)))
    -
    -    def is_2d(self):
    -        return self.jacobians.shape[1] == backend.N_QUAD_2D
    -    
    -    def is_3d(self):
    -        return self.jacobians.shape[1] == backend.N_TRIANGLE_QUAD
    -     
    -    def __len__(self):
    -        return len(self.charges)
    -     
    -    def __add__(self, other):
    -        if np.array_equal(self.positions, other.positions) and np.array_equal(self.jacobians, other.jacobians):
    -            return EffectivePointCharges(self.charges + other.charges, self.jacobians, self.positions)
    -        else:
    -            return EffectivePointCharges(
    -                np.concatenate([self.charges, other.charges]),
    -                np.concatenate([self.jacobians, other.jacobians]),
    -                np.concatenate([self.positions, other.positions]))
    -    
    -    def __mul__(self, other):
    -        if isinstance(other, int) or isinstance(other, float):
    -            return EffectivePointCharges(other*self.charges, self.jacobians, self.positions)
    -        
    -        return NotImpemented
    -    
    -    def __neg__(self):
    -        return -1*self
    -    
    -    def __rmul__(self, other):
    -        return self.__mul__(other)
    -
    -    def __str__(self):
    -        dim = '2D' if self.is_2d() else '3D'
    -        return f'<EffectivePointCharges {dim}\n' \
    -               f'\tNumber of charges: {len(self.charges)}\n' \
    -               f'\tJacobian shape:  {self.jacobians.shape}\n' \
    -               f'\tPositions shape: {self.positions.shape}>'
    -
    -        
    -     
    -def _excitation_to_higher_order(excitation):
    -    print('Upgrading mesh to higher to be compatible with matrix solver')
    -    # Upgrade mesh, such that matrix solver will support it
    -    excitation = copy.copy(excitation)
    -    mesh = copy.copy(excitation.mesh)
    -    excitation.mesh = mesh._to_higher_order_mesh()
    -    return excitation
    -
    -def solve_bem(excitation, superposition=False, use_fmm=False, fmm_precision=0):
    -    """
    -    Solve for the charges on the surface of the geometry by using the Boundary Element Method (BEM) and taking
    -    into account the specified `excitation`. 
    -
    -    Parameters
    -    ----------
    -    excitation : traceon.excitation.Excitation
    -        The excitation that produces the resulting field.
    -     
    -    superposition : bool
    -        When `superposition=True` 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).
    -    
    -    use_fmm : bool
    -        Use the fast multipole method to calculate the charge distribution. This method is currently only implemented for 3D geometries without
    -        higher order elements. This function only works if [pyfmmlib](https://github.com/inducer/pyfmmlib) is installed
    -        (version 2023.1 or later). The fast multipole method is usually slower for small 3D problems, but scales much better to problems with >10^5
    -        number of triangles.
    -
    -    fmm_precision : int
    -        Precision flag passed to the fast multipole library (see iprec argument in the [official documentation](https://github.com/zgimbutas/fmmlib3d/blob/master/doc/fmm3dpart_manual3.pdf)).
    -        Usually values -1, 0, 1, 2 will work, choose higher numbers if more precision is desired.
    -    
    -    Returns
    -    -------
    -    A `FieldRadialBEM` if the geometry (contained in the given `excitation`) is radially symmetric. If the geometry is a generic three
    -    dimensional geometry `Field3D_BEM` is returned. Alternatively, when `superposition=True` a dictionary is returned, where the keys
    -    are the physical groups with unity excitation, and the values are the resulting fields.
    -    """
    -    if use_fmm:
    -        assert not excitation.is_magnetostatic(), "Magnetostatic not yet supported for FMM"
    -        if superposition:
    -            excitations = excitation._split_for_superposition()
    -            return {name:ElectrostaticSolver(exc).solve_fmm(fmm_precision) for name, exc in excitations.items()}
    -        else:
    -            return ElectrostaticSolver(excitation).solve_fmm(fmm_precision)
    -    else:
    -        if not excitation.mesh.is_higher_order():
    -            excitation = _excitation_to_higher_order(excitation)
    -         
    -        if superposition:
    -            # 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([ElectrostaticSolver(excitations[n]).get_right_hand_side() for n in elec_names])
    -            solutions = ElectrostaticSolver(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([MagnetostaticSolver(excitations[n]).get_right_hand_side() for n in mag_names])
    -            solutions = MagnetostaticSolver(excitation).solve_matrix(right_hand_sides)
    -            mag_dict = {n:s for n, s in zip(mag_names, solutions)}
    -             
    -            return {**elec_dict, **mag_dict}
    -        else:
    -            mag, elec = excitation.is_magnetostatic(), excitation.is_electrostatic()
    -
    -            assert mag or elec, "Solving for an empty excitation"
    -             
    -            if mag and elec:
    -                elec_field = ElectrostaticSolver(excitation).solve_matrix()[0]
    -                mag_field = MagnetostaticSolver(excitation).solve_matrix()[0]
    -                return elec_field + mag_field
    -            elif elec and not mag:
    -                return ElectrostaticSolver(excitation).solve_matrix()[0]
    -            elif mag and not elec:
    -                return MagnetostaticSolver(excitation).solve_matrix()[0]
    -
    -def _get_one_dimensional_high_order_ppoly(z, y, dydz, dydz2):
    -    bpoly = BPoly.from_derivatives(z, np.array([y, dydz, dydz2]).T)
    -    return PPoly.from_bernstein_basis(bpoly)
    -
    -def _quintic_spline_coefficients(z, derivs):
    -    # k is degree of polynomial
    -    #assert derivs.shape == (z.size, backend.DERIV_2D_MAX)
    -    c = np.zeros( (z.size-1, 9, 6) )
    -    
    -    dz = z[1] - z[0]
    -    assert np.all(np.isclose(np.diff(z), dz)) # Equally spaced
    -     
    -    for i, d in enumerate(derivs):
    -        high_order = i + 2 < len(derivs)
    -        
    -        if high_order:
    -            ppoly = _get_one_dimensional_high_order_ppoly(z, d, derivs[i+1], derivs[i+2])
    -            start_index = 0
    -        else:
    -            ppoly = CubicSpline(z, d)
    -            start_index = 2
    -        
    -        c[:, i, start_index:], x, k = ppoly.c.T, ppoly.x, ppoly.c.shape[0]-1
    -        assert np.all(x == z)
    -        assert (high_order and k == 5) or (not high_order and k == 3)
    -    
    -    return c
    -
    -class 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: (2,) or (3,) np.ndarray of float64
    -
    -        Returns
    -        --------------------
    -        (2,) or (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: (2,) or (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)
    -         
    -        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")
    -     
    -    
    -    
    -class FieldBEM(Field):
    -    """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_bem` 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_on_element(i) for i in indices) 
    -    
    -    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)}>'
    -
    -
    -class FieldRadialBEM(FieldBEM):
    -    """A radially symmetric electrostatic field. The field is a result of the surface charges as computed by the
    -    `solve_bem` 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()
    -         
    -        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges)
    -         
    -    def current_field_at_point(self, point):
    -        currents = self.current_point_charges.charges
    -        jacobians = self.current_point_charges.jacobians
    -        positions = self.current_point_charges.positions
    -         
    -        if point.shape == (2,):
    -            # Input point is 2D, so return a 2D point
    -            result = backend.current_field(backend._vec_2d_to_3d(point), currents, jacobians, positions)
    -            assert np.isclose(result[1], 0.)
    -            return backend._vec_3d_to_2d(result)
    -        else:
    -            return backend.current_field(point, currents, jacobians, positions)
    -     
    -    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.   
    -        """
    -        assert point.shape == (2,) or point.shape == (3,)
    -        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: (2,) array of float64
    -            Position at which to compute the field.
    -        
    -        Returns
    -        -------
    -        Potential as a float value (in units of V).
    -        """
    -        point = np.array(point).astype(np.float64)
    -        assert point.shape == (2,) or point.shape == (3,)
    -        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: (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).astype(np.float64)
    -        assert point.shape == (2,)
    -        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: (2,) array of float64
    -            Position at which to compute the field.
    -        
    -        Returns
    -        -------
    -        Potential as a float value (in units of A).
    -        """
    -
    -        assert point.shape == (2,)
    -        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_ring(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_ring(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_ring(z, currents, jacobians, positions)
    -      
    -    def axial_derivative_interpolation(self, zmin, zmax, N=None):
    -        """
    -        Use a radial series expansion based on the potential derivatives at the optical axis
    -        to allow very fast field evaluations.
    -        
    -        Parameters
    -        ----------
    -        zmin : float
    -            Location on the optical axis where to start sampling the derivatives.
    -            
    -        zmax : float
    -            Location on the optical axis where to stop sampling the derivatives. 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
    -        -------
    -        `FieldRadialAxial` object allowing fast field evaluations.
    -
    -        """
    -        assert zmax > zmin
    -        N_charges = max(len(self.electrostatic_point_charges.charges), len(self.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(self.get_electrostatic_axial_potential_derivatives, z), axis=0)
    -        elec_coeffs = _quintic_spline_coefficients(z, elec_derivs.T)
    -        
    -        mag_derivs = np.concatenate(util.split_collect(self.get_magnetostatic_axial_potential_derivatives, z), axis=0)
    -        mag_coeffs = _quintic_spline_coefficients(z, mag_derivs.T)
    -        
    -        print(f'Computing derivative interpolation took {(time.time()-st)*1000:.2f} ms ({len(z)} items)')
    -        
    -        return FieldRadialAxial(z, elec_coeffs, mag_coeffs)
    -    
    -    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])
    -    
    -class Field3D_BEM(FieldBEM):
    -    """An electrostatic field resulting from a general 3D geometry. The field is a result of the surface charges as computed by the
    -    `solve_bem` function. See the comments in `FieldBEM`."""
    -     
    -    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
    -        
    -        if electrostatic_point_charges is None:
    -            electrostatic_point_charges = EffectivePointCharges.empty_3d()
    -        if magnetostatic_point_charges is None:
    -            magnetostatic_point_charges = EffectivePointCharges.empty_3d()
    -         
    -        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d())
    -
    -        for eff in [electrostatic_point_charges, magnetostatic_point_charges]:
    -            N = len(eff.charges)
    -            assert eff.charges.shape == (N,)
    -            assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD)
    -            assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3)
    -     
    -    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.
    -        """
    -        assert point.shape == (3,)
    -        charges = self.electrostatic_point_charges.charges
    -        jacobians = self.electrostatic_point_charges.jacobians
    -        positions = self.electrostatic_point_charges.positions
    -        return backend.field_3d(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).astype(np.float64)
    -        assert point.shape == (3,)
    -        charges = self.electrostatic_point_charges.charges
    -        jacobians = self.electrostatic_point_charges.jacobians
    -        positions = self.electrostatic_point_charges.positions
    -        return backend.potential_3d(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).astype(np.float64)
    -        assert point.shape == (3,)
    -        charges = self.magnetostatic_point_charges.charges
    -        jacobians = self.magnetostatic_point_charges.jacobians
    -        positions = self.magnetostatic_point_charges.positions
    -        return backend.field_3d(point, charges, jacobians, positions)
    -     
    -    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).
    -        """
    -        assert point.shape == (3,)
    -        charges = self.magnetostatic_point_charges.charges
    -        jacobians = self.magnetostatic_point_charges.jacobians
    -        positions = self.magnetostatic_point_charges.positions
    -        return backend.potential_3d(point, charges, jacobians, positions)
    -    
    -    
    -    def axial_derivative_interpolation(self, zmin, zmax, N=None):
    -        """
    -        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.
    -
    -        """
    -        assert zmax > zmin
    -        N_charges = max(len(self.electrostatic_point_charges.charges), len(self.magnetostatic_point_charges.charges))
    -        N = N if N is not None else int(FACTOR_AXIAL_DERIV_SAMPLING_3D*N_charges)
    -        z = np.linspace(zmin, zmax, N)
    -        
    -        print(f'Number of points on z-axis: {len(z)}')
    -        st = time.time()
    -        elec_coeff = self._effective_point_charges_to_coeff(self.electrostatic_point_charges, z)
    -        mag_coeff = self._effective_point_charges_to_coeff(self.magnetostatic_point_charges, z)
    -        print(f'Time for calculating radial series expansion coefficients: {(time.time()-st)*1000:.0f} ms ({len(z)} items)')
    -        
    -        return Field3DAxial(z, elec_coeff, mag_coeff)
    -    
    -    def _effective_point_charges_to_coeff(self, eff, z): 
    -        charges = eff.charges
    -        jacobians = eff.jacobians
    -        positions = eff.positions
    -        coeffs = util.split_collect(lambda z: backend.axial_coefficients_3d(charges, jacobians, positions, z), z)
    -        coeffs = np.concatenate(coeffs, axis=0)
    -        interpolated_coeffs = CubicSpline(z, coeffs).c
    -        interpolated_coeffs = np.moveaxis(interpolated_coeffs, 0, -1)
    -        return np.require(interpolated_coeffs, requirements=('C_CONTIGUOUS', 'ALIGNED'))
    -    
    -    def area_of_element(self, i):
    -        jacobians = self.electrostatic_point_charges.jacobians
    -        return np.sum(jacobians[i])
    -
    -     
    -
    -class FieldAxial(Field):
    -    """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 NotImpemented
    -    
    -    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 NotImpemented
    -    
    -    def __neg__(self):
    -        return -1*self
    -    
    -    def __rmul__(self, other):
    -        return self.__mul__(other)
    -
    -                
    -class FieldRadialAxial(FieldAxial):
    -    """ """
    -    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    -        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)
    -    
    -    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.
    -        """
    -        assert point.shape == (2,)
    -        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).astype(np.float64)
    -        assert point.shape == (2,)
    -        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).astype(np.float64)
    -        assert point.shape == (2,)
    -        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).
    -        """
    -        assert point.shape == (2,)
    -        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    -    
    -class Field3DAxial(FieldAxial):
    -    """Field computed using a radial series expansion around the optical axis (z-axis). See comments at the start of this page.
    -     """
    -    
    -    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    -        super().__init__(z, electrostatic_coeffs, magnetostatic_coeffs)
    -        
    -        assert electrostatic_coeffs.shape == (len(z)-1, 2, backend.NU_MAX, backend.M_MAX, 4)
    -        assert magnetostatic_coeffs.shape == (len(z)-1, 2, backend.NU_MAX, backend.M_MAX, 4)
    -     
    -    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.
    -        """
    -        assert point.shape == (3,)
    -        return backend.field_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -     
    -    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).
    -        """
    -        point = np.array(point).astype(np.float64)
    -        assert point.shape == (3,)
    -        return backend.potential_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -    
    -    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).astype(np.float64)
    -        assert point.shape == (3,)
    -        return backend.field_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    -     
    -    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).
    -        """
    -        assert point.shape == (3,)
    -        return backend.potential_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    -    
    -
    -    
    -
    @@ -1322,511 +66,201 @@

    References

    Functions

    -
    -def solve_bem(excitation, superposition=False, use_fmm=False, fmm_precision=0) +
    +def solve_direct(excitation)
    -

    Solve for the charges on the surface of the geometry by using the Boundary Element Method (BEM) and taking +

    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.
    -
    superposition : bool
    -
    When superposition=True 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).
    -
    use_fmm : bool
    -
    Use the fast multipole method to calculate the charge distribution. This method is currently only implemented for 3D geometries without -higher order elements. This function only works if pyfmmlib is installed -(version 2023.1 or later). The fast multipole method is usually slower for small 3D problems, but scales much better to problems with >10^5 -number of triangles.
    -
    fmm_precision : int
    -
    Precision flag passed to the fast multipole library (see iprec argument in the official documentation). -Usually values -1, 0, 1, 2 will work, choose higher numbers if more precision is desired.
    -
    -

    Returns

    -

    A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a generic three -dimensional geometry Field3D_BEM is returned. Alternatively, when superposition=True a dictionary is returned, where the keys -are the physical groups with unity excitation, and the values are the resulting fields.

    -
    - -Expand source code - -
    def solve_bem(excitation, superposition=False, use_fmm=False, fmm_precision=0):
    -    """
    -    Solve for the charges on the surface of the geometry by using the Boundary Element Method (BEM) and taking
    -    into account the specified `excitation`. 
    -
    -    Parameters
    -    ----------
    -    excitation : traceon.excitation.Excitation
    -        The excitation that produces the resulting field.
    -     
    -    superposition : bool
    -        When `superposition=True` 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).
    -    
    -    use_fmm : bool
    -        Use the fast multipole method to calculate the charge distribution. This method is currently only implemented for 3D geometries without
    -        higher order elements. This function only works if [pyfmmlib](https://github.com/inducer/pyfmmlib) is installed
    -        (version 2023.1 or later). The fast multipole method is usually slower for small 3D problems, but scales much better to problems with >10^5
    -        number of triangles.
    -
    -    fmm_precision : int
    -        Precision flag passed to the fast multipole library (see iprec argument in the [official documentation](https://github.com/zgimbutas/fmmlib3d/blob/master/doc/fmm3dpart_manual3.pdf)).
    -        Usually values -1, 0, 1, 2 will work, choose higher numbers if more precision is desired.
    -    
    -    Returns
    -    -------
    -    A `FieldRadialBEM` if the geometry (contained in the given `excitation`) is radially symmetric. If the geometry is a generic three
    -    dimensional geometry `Field3D_BEM` is returned. Alternatively, when `superposition=True` a dictionary is returned, where the keys
    -    are the physical groups with unity excitation, and the values are the resulting fields.
    -    """
    -    if use_fmm:
    -        assert not excitation.is_magnetostatic(), "Magnetostatic not yet supported for FMM"
    -        if superposition:
    -            excitations = excitation._split_for_superposition()
    -            return {name:ElectrostaticSolver(exc).solve_fmm(fmm_precision) for name, exc in excitations.items()}
    -        else:
    -            return ElectrostaticSolver(excitation).solve_fmm(fmm_precision)
    -    else:
    -        if not excitation.mesh.is_higher_order():
    -            excitation = _excitation_to_higher_order(excitation)
    -         
    -        if superposition:
    -            # 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([ElectrostaticSolver(excitations[n]).get_right_hand_side() for n in elec_names])
    -            solutions = ElectrostaticSolver(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([MagnetostaticSolver(excitations[n]).get_right_hand_side() for n in mag_names])
    -            solutions = MagnetostaticSolver(excitation).solve_matrix(right_hand_sides)
    -            mag_dict = {n:s for n, s in zip(mag_names, solutions)}
    -             
    -            return {**elec_dict, **mag_dict}
    -        else:
    -            mag, elec = excitation.is_magnetostatic(), excitation.is_electrostatic()
    -
    -            assert mag or elec, "Solving for an empty excitation"
    -             
    -            if mag and elec:
    -                elec_field = ElectrostaticSolver(excitation).solve_matrix()[0]
    -                mag_field = MagnetostaticSolver(excitation).solve_matrix()[0]
    -                return elec_field + mag_field
    -            elif elec and not mag:
    -                return ElectrostaticSolver(excitation).solve_matrix()[0]
    -            elif mag and not elec:
    -                return MagnetostaticSolver(excitation).solve_matrix()[0]
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class Field -
    -
    -
    -
    - -Expand source code - -
    class 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: (2,) or (3,) np.ndarray of float64
    -
    -        Returns
    -        --------------------
    -        (2,) or (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: (2,) or (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)
    -         
    -        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")
    -
    -

    Subclasses

    - -

    Methods

    -
    -
    -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 : (2,) or (3,) np.ndarray of float64
    -
     
    -
    -

    Returns

    -

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

    -
    - -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: (2,) or (3,) np.ndarray of float64
    -
    -    Returns
    -    --------------------
    -    (2,) or (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 : (2,) or (3,) np.ndarray of float64
    -
     

    Returns

    -
    -
    float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
    -
     
    -
    -
    - -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: (2,) or (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)
    -     
    -    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")
    -
    -
    -
    +

    A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a three +dimensional geometry Field3D_BEM is returned.

    -
    -class Field3DAxial -(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +def solve_direct_superposition(excitation)
    -

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

    -
    - -Expand source code - -
    class Field3DAxial(FieldAxial):
    -    """Field computed using a radial series expansion around the optical axis (z-axis). See comments at the start of this page.
    -     """
    -    
    -    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    -        super().__init__(z, electrostatic_coeffs, magnetostatic_coeffs)
    -        
    -        assert electrostatic_coeffs.shape == (len(z)-1, 2, backend.NU_MAX, backend.M_MAX, 4)
    -        assert magnetostatic_coeffs.shape == (len(z)-1, 2, backend.NU_MAX, backend.M_MAX, 4)
    -     
    -    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.
    -        """
    -        assert point.shape == (3,)
    -        return backend.field_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -     
    -    def electrostatic_potential_at_point(self, point):
    -        """
    -        Compute the electrostatic potential.
    +

    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).

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Field +
    +
    +

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

    +
    + +Expand source code + +
    class Field(ABC):
    +    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,) array of float64
    -            Position at which to compute the potential.
    -        
    +        ---------------------
    +        point: (3,) np.ndarray of float64
    +
             Returns
    -        -------
    -        Potential as a float value (in units of V).
    -        """
    -        point = np.array(point).astype(np.float64)
    -        assert point.shape == (3,)
    -        return backend.potential_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -    
    -    def magnetostatic_field_at_point(self, point):
    +        --------------------
    +        (3,) np.ndarray of float64. The electrostatic field \\(\\vec{E}\\) or the magnetostatic field \\(\\vec{H}\\).
             """
    -        Compute the magnetic field \\( \\vec{H} \\)
    +        elec, mag = self.is_electrostatic(), self.is_magnetostatic()
             
    -        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).astype(np.float64)
    -        assert point.shape == (3,)
    -        return backend.field_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    +        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 magnetostatic_potential_at_point(self, point):
    -        """
    -        Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\)) close to the axis.
    -        
    +    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,) array of float64
    -            Position at which to compute the field.
    -        
    +        ---------------------
    +        point: (3,) np.ndarray of float64
    +
             Returns
    -        -------
    -        Potential as a float value (in units of A).
    +        --------------------
    +        float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
             """
    -        assert point.shape == (3,)
    -        return backend.potential_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    + 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) + + 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 magnetostatic_potential_at_point(self, point): + ... + + @abstractmethod + def electrostatic_potential_at_point(self, point): + ... + + @abstractmethod + def magnetostatic_field_at_point(self, point): + ... + + @abstractmethod + def electrostatic_field_at_point(self, point): + ...

    Ancestors

      +
    • abc.ABC
    • +
    +

    Subclasses

    +

    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.

    -
    - -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
    -    -------
    -    Numpy array containing the field strengths (in units of V/mm) in the x, y and z directions.
    -    """
    -    assert point.shape == (3,)
    -    return backend.field_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -
    +
    -
    +
    def electrostatic_potential_at_point(self, point)
    -

    Compute the electrostatic potential.

    +
    +
    +
    +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,) array of float64
    -
    Position at which to compute the potential.
    +
    point : (3,) np.ndarray of float64
    +
     

    Returns

    -

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

    -
    - -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 potential.
    -    
    -    Returns
    -    -------
    -    Potential as a float value (in units of V).
    -    """
    -    point = np.array(point).astype(np.float64)
    -    assert point.shape == (3,)
    -    return backend.potential_3d_derivs(point, self.z, self.electrostatic_coeffs)
    -
    +

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

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    -
    +
    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.

    -
    - -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).astype(np.float64)
    -    assert point.shape == (3,)
    -    return backend.field_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    -
    +
    -
    +
    def magnetostatic_potential_at_point(self, point)
    -

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

    +
    +
    +
    +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,) array of float64
    -
    Position at which to compute the field.
    +
    point : (3,) np.ndarray of float64
    +
     

    Returns

    -

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

    -
    - -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: (3,) array of float64
    -        Position at which to compute the field.
    -    
    -    Returns
    -    -------
    -    Potential as a float value (in units of A).
    -    """
    -    assert point.shape == (3,)
    -    return backend.potential_3d_derivs(point, self.z, self.magnetostatic_coeffs)
    -
    +
    +
    float. The electrostatic potential (unit Volt) or magnetostaic scalar potential (unit Ampere)
    +
     
    +
    -

    Inherited members

    -
    class Field3D_BEM @@ -1834,14 +268,14 @@

    Inherited members

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

    +solve_direct() function. See the comments in FieldBEM.

    Expand source code
    class Field3D_BEM(FieldBEM):
         """An electrostatic field resulting from a general 3D geometry. The field is a result of the surface charges as computed by the
    -    `solve_bem` function. See the comments in `FieldBEM`."""
    +    `solve_direct` function. See the comments in `FieldBEM`."""
          
         def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
             
    @@ -1851,6 +285,8 @@ 

    Inherited members

    magnetostatic_point_charges = EffectivePointCharges.empty_3d() super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d()) + + self.symmetry = E.Symmetry.THREE_D for eff in [electrostatic_point_charges, magnetostatic_point_charges]: N = len(eff.charges) @@ -1858,9 +294,9 @@

    Inherited members

    assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD) assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) - def electrostatic_field_at_point(self, point): + def electrostatic_field_at_point(self, point_): """ - Compute the electric field, \( \\vec{E} = -\\nabla \phi \) + Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\) Parameters ---------- @@ -1869,15 +305,16 @@

    Inherited members

    Returns ------- - Numpy array containing the field strengths (in units of V/mm) in the x, y and z directions. + (3,) array of float64 representing the electric field """ - assert point.shape == (3,) + 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_3d(point, charges, jacobians, positions) - def electrostatic_potential_at_point(self, point): + def electrostatic_potential_at_point(self, point_): """ Compute the electrostatic potential. @@ -1890,14 +327,14 @@

    Inherited members

    ------- Potential as a float value (in units of V). """ - point = np.array(point).astype(np.float64) - assert point.shape == (3,) + 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_3d(point, charges, jacobians, positions) - def magnetostatic_field_at_point(self, point): + def magnetostatic_field_at_point(self, point_): """ Compute the magnetic field \\( \\vec{H} \\) @@ -1910,14 +347,14 @@

    Inherited members

    ------- (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).astype(np.float64) - assert point.shape == (3,) + 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.field_3d(point, charges, jacobians, positions) - def magnetostatic_potential_at_point(self, point): + def magnetostatic_potential_at_point(self, point_): """ Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\)) @@ -1930,67 +367,25 @@

    Inherited members

    ------- Potential as a float value (in units of A). """ - assert point.shape == (3,) + 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_3d(point, charges, jacobians, positions) - - - def axial_derivative_interpolation(self, zmin, zmax, N=None): - """ - 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. - - """ - assert zmax > zmin - N_charges = max(len(self.electrostatic_point_charges.charges), len(self.magnetostatic_point_charges.charges)) - N = N if N is not None else int(FACTOR_AXIAL_DERIV_SAMPLING_3D*N_charges) - z = np.linspace(zmin, zmax, N) - - print(f'Number of points on z-axis: {len(z)}') - st = time.time() - elec_coeff = self._effective_point_charges_to_coeff(self.electrostatic_point_charges, z) - mag_coeff = self._effective_point_charges_to_coeff(self.magnetostatic_point_charges, z) - print(f'Time for calculating radial series expansion coefficients: {(time.time()-st)*1000:.0f} ms ({len(z)} items)') - - return Field3DAxial(z, elec_coeff, mag_coeff) - - def _effective_point_charges_to_coeff(self, eff, z): - charges = eff.charges - jacobians = eff.jacobians - positions = eff.positions - coeffs = util.split_collect(lambda z: backend.axial_coefficients_3d(charges, jacobians, positions, z), z) - coeffs = np.concatenate(coeffs, axis=0) - interpolated_coeffs = CubicSpline(z, coeffs).c - interpolated_coeffs = np.moveaxis(interpolated_coeffs, 0, -1) - return np.require(interpolated_coeffs, requirements=('C_CONTIGUOUS', 'ALIGNED')) - + def area_of_element(self, i): jacobians = self.electrostatic_point_charges.jacobians - return np.sum(jacobians[i])
    + return np.sum(jacobians[i]) + + def get_tracer(self, bounds): + return T.Tracer3D_BEM(self, bounds)

    Ancestors

    Methods

    @@ -1999,78 +394,9 @@

    Methods

    -
    - -Expand source code - -
    def area_of_element(self, i):
    -    jacobians = self.electrostatic_point_charges.jacobians
    -    return np.sum(jacobians[i])
    -
    -
    -
    -def axial_derivative_interpolation(self, zmin, zmax, N=None) -
    -
    -

    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.

    -
    - -Expand source code - -
    def axial_derivative_interpolation(self, zmin, zmax, N=None):
    -    """
    -    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.
    -
    -    """
    -    assert zmax > zmin
    -    N_charges = max(len(self.electrostatic_point_charges.charges), len(self.magnetostatic_point_charges.charges))
    -    N = N if N is not None else int(FACTOR_AXIAL_DERIV_SAMPLING_3D*N_charges)
    -    z = np.linspace(zmin, zmax, N)
    -    
    -    print(f'Number of points on z-axis: {len(z)}')
    -    st = time.time()
    -    elec_coeff = self._effective_point_charges_to_coeff(self.electrostatic_point_charges, z)
    -    mag_coeff = self._effective_point_charges_to_coeff(self.magnetostatic_point_charges, z)
    -    print(f'Time for calculating radial series expansion coefficients: {(time.time()-st)*1000:.0f} ms ({len(z)} items)')
    -    
    -    return Field3DAxial(z, elec_coeff, mag_coeff)
    -
    -def electrostatic_field_at_point(self, point) +def electrostatic_field_at_point(self, point_)

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

    @@ -2080,33 +406,10 @@

    Parameters

    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.

    -
    - -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
    -    -------
    -    Numpy array containing the field strengths (in units of V/mm) in the x, y and z directions.
    -    """
    -    assert point.shape == (3,)
    -    charges = self.electrostatic_point_charges.charges
    -    jacobians = self.electrostatic_point_charges.jacobians
    -    positions = self.electrostatic_point_charges.positions
    -    return backend.field_3d(point, charges, jacobians, positions)
    -
    +

    (3,) array of float64 representing the electric field

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

    Compute the electrostatic potential.

    @@ -2117,33 +420,15 @@

    Parameters

    Returns

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

    -
    - -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).astype(np.float64)
    -    assert point.shape == (3,)
    -    charges = self.electrostatic_point_charges.charges
    -    jacobians = self.electrostatic_point_charges.jacobians
    -    positions = self.electrostatic_point_charges.positions
    -    return backend.potential_3d(point, charges, jacobians, positions)
    -
    + +
    +def get_tracer(self, bounds) +
    +
    +
    -def magnetostatic_field_at_point(self, point) +def magnetostatic_field_at_point(self, point_)

    Compute the magnetic field \vec{H}

    @@ -2154,33 +439,9 @@

    Parameters

    Returns

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

    -
    - -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).astype(np.float64)
    -    assert point.shape == (3,)
    -    charges = self.magnetostatic_point_charges.charges
    -    jacobians = self.magnetostatic_point_charges.jacobians
    -    positions = self.magnetostatic_point_charges.positions
    -    return backend.field_3d(point, charges, jacobians, positions)
    -
    -def magnetostatic_potential_at_point(self, point) +def magnetostatic_potential_at_point(self, point_)

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

    @@ -2191,29 +452,6 @@

    Parameters

    Returns

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

    -
    - -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).
    -    """
    -    assert point.shape == (3,)
    -    charges = self.magnetostatic_point_charges.charges
    -    jacobians = self.magnetostatic_point_charges.jacobians
    -    positions = self.magnetostatic_point_charges.positions
    -    return backend.potential_3d(point, charges, jacobians, positions)
    -

    Inherited members

    @@ -2279,7 +517,7 @@

    Inherited members

    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 NotImpemented + return NotImplemented def __sub__(self, other): return self.__add__(-other) @@ -2291,7 +529,7 @@

    Inherited members

    if isinstance(other, int) or isinstance(other, float): return self.__class__(self.z, other*self.electrostatic_coeffs, other*self.magnetostatic_coeffs) - return NotImpemented + return NotImplemented def __neg__(self): return -1*self @@ -2302,10 +540,10 @@

    Inherited members

    Ancestors

    Subclasses

    Methods

    @@ -2315,26 +553,12 @@

    Methods

    -
    - -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

    @@ -2353,15 +577,15 @@

    Inherited members

    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_bem() function. +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.

    Expand source code -
    class FieldBEM(Field):
    +
    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_bem` function. 
    +    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):
    @@ -2440,7 +664,11 @@ 

    Inherited members

    --------------- The sum of the area of all elements with the given indices. """ - return sum(self.area_on_element(i) for i in 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] @@ -2470,6 +698,7 @@

    Inherited members

    Ancestors

    Subclasses

      @@ -2478,6 +707,12 @@

      Subclasses

    Methods

    +
    +def area_of_element(self, i: int) ‑> float +
    +
    +
    +
    def area_of_elements(self, indices)
    @@ -2490,37 +725,12 @@

    Parameters

    Returns

    The sum of the area of all elements with the given 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_on_element(i) for i in 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) @@ -2536,51 +746,18 @@

    Parameters

    Returns

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

    -
    - -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)
    -
    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) @@ -2594,23 +771,6 @@

    Parameters

    bounds : (3, 2) np.ndarray of float64
    The min, max value of x, y, z respectively within the field is still computed.
    -
    - -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)
    -

    Inherited members

    @@ -2637,216 +797,146 @@

    Inherited members

    """ """ def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None): 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) + self.symmetry = E.Symmetry.RADIAL - def electrostatic_field_at_point(self, point): + def electrostatic_field_at_point(self, point_): """ - Compute the electric field, \( \\vec{E} = -\\nabla \phi \) + Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\) Parameters ---------- - point: (2,) array of float64 + 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 r and z directions. + (3,) array of float64, containing the field strengths (units of V/m) """ - assert point.shape == (2,) + 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): + def magnetostatic_field_at_point(self, point_): """ Compute the magnetic field \\( \\vec{H} \\) Parameters ---------- - point: (2,) array of float64 + point: (3,) 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. + (3,) array of float64, containing the field strengths (units of A/m) """ - point = np.array(point).astype(np.float64) - assert point.shape == (2,) + 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): + def electrostatic_potential_at_point(self, point_): """ Compute the electrostatic potential (close to the axis). Parameters ---------- - point: (2,) array of float64 + point: (3,) array of float64 Position at which to compute the potential. Returns ------- Potential as a float value (in units of V). """ - point = np.array(point).astype(np.float64) - assert point.shape == (2,) + 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): + 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 + point: (3,) array of float64 Position at which to compute the field. Returns ------- Potential as a float value (in units of A). """ - assert point.shape == (2,) - return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    + 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.TracerRadialAxial(self, bounds)

    Ancestors

    Methods

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

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

    Parameters

    -
    point : (2,) array of float64
    +
    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 r and z directions.

    -
    - -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.
    -    """
    -    assert point.shape == (2,)
    -    return backend.field_radial_derivs(point, self.z, self.electrostatic_coeffs)
    -
    +

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

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

    Compute the electrostatic potential (close to the axis).

    Parameters

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

    Returns

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

    -
    - -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).astype(np.float64)
    -    assert point.shape == (2,)
    -    return backend.potential_radial_derivs(point, self.z, self.electrostatic_coeffs)
    -
    +
    +
    +def get_tracer(self, bounds) +
    +
    +
    -def magnetostatic_field_at_point(self, point) +def magnetostatic_field_at_point(self, point_)

    Compute the magnetic field \vec{H}

    Parameters

    -
    point : (2,) array of float64
    +
    point : (3,) 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.

    -
    - -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).astype(np.float64)
    -    assert point.shape == (2,)
    -    return backend.field_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    -
    +

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

    -def magnetostatic_potential_at_point(self, point) +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
    +
    point : (3,) array of float64
    Position at which to compute the field.

    Returns

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

    -
    - -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).
    -    """
    -    assert point.shape == (2,)
    -    return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    -

    Inherited members

    @@ -2865,14 +955,14 @@

    Inherited members

    A radially symmetric electrostatic field. The field is a result of the surface charges as computed by the -solve_bem() function. See the comments in FieldBEM.

    +solve_direct() function. See the comments in FieldBEM.

    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_bem` function. See the comments in `FieldBEM`."""
    +    `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:
    @@ -2882,75 +972,74 @@ 

    Inherited members

    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): + def current_field_at_point(self, point_): + point = np.array(point_) + 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 - - if point.shape == (2,): - # Input point is 2D, so return a 2D point - result = backend.current_field(backend._vec_2d_to_3d(point), currents, jacobians, positions) - assert np.isclose(result[1], 0.) - return backend._vec_3d_to_2d(result) - else: - return backend.current_field(point, currents, jacobians, positions) + return backend.current_field(point, currents, jacobians, positions) - def electrostatic_field_at_point(self, point): + def electrostatic_field_at_point(self, point_): """ - Compute the electric field, \( \\vec{E} = -\\nabla \phi \) + Compute the electric field, \\( \\vec{E} = -\\nabla \\phi \\) Parameters ---------- - point: (2,) array of float64 + 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 r and z directions. + (3,) array of float64, containing the field strengths (units of V/m) """ - assert point.shape == (2,) or point.shape == (3,) + 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): + def electrostatic_potential_at_point(self, point_): """ Compute the electrostatic potential. Parameters ---------- - point: (2,) array of float64 + 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).astype(np.float64) - assert point.shape == (2,) or point.shape == (3,) + 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): + def magnetostatic_field_at_point(self, point_): """ Compute the magnetic field \\( \\vec{H} \\) Parameters ---------- - point: (2,) array of float64 + point: (3,) 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. + (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).astype(np.float64) - assert point.shape == (2,) + 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 @@ -2961,21 +1050,21 @@

    Inherited members

    return current_field + mag_field - def magnetostatic_potential_at_point(self, point): + def magnetostatic_potential_at_point(self, point_): """ Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\)) Parameters ---------- - point: (2,) array of float64 + point: (3,) array of float64 Position at which to compute the field. Returns ------- Potential as a float value (in units of A). """ - - assert point.shape == (2,) + 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 @@ -3005,7 +1094,7 @@

    Inherited members

    charges = self.electrostatic_point_charges.charges jacobians = self.electrostatic_point_charges.jacobians positions = self.electrostatic_point_charges.positions - return backend.axial_derivatives_radial_ring(z, charges, jacobians, positions) + return backend.axial_derivatives_radial(z, charges, jacobians, positions) def get_magnetostatic_axial_potential_derivatives(self, z): """ @@ -3025,7 +1114,7 @@

    Inherited members

    jacobians = self.magnetostatic_point_charges.jacobians positions = self.magnetostatic_point_charges.positions - derivs_magnetic = backend.axial_derivatives_radial_ring(z, charges, jacobians, 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 @@ -3047,56 +1136,21 @@

    Inherited members

    currents = self.current_point_charges.charges jacobians = self.current_point_charges.jacobians positions = self.current_point_charges.positions - return backend.current_axial_derivatives_radial_ring(z, currents, jacobians, positions) + return backend.current_axial_derivatives_radial(z, currents, jacobians, positions) - def axial_derivative_interpolation(self, zmin, zmax, N=None): - """ - Use a radial series expansion based on the potential derivatives at the optical axis - to allow very fast field evaluations. - - Parameters - ---------- - zmin : float - Location on the optical axis where to start sampling the derivatives. - - zmax : float - Location on the optical axis where to stop sampling the derivatives. 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 - ------- - `FieldRadialAxial` object allowing fast field evaluations. - - """ - assert zmax > zmin - N_charges = max(len(self.electrostatic_point_charges.charges), len(self.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(self.get_electrostatic_axial_potential_derivatives, z), axis=0) - elec_coeffs = _quintic_spline_coefficients(z, elec_derivs.T) - - mag_derivs = np.concatenate(util.split_collect(self.get_magnetostatic_axial_potential_derivatives, z), axis=0) - mag_coeffs = _quintic_spline_coefficients(z, mag_derivs.T) - - print(f'Computing derivative interpolation took {(time.time()-st)*1000:.2f} ms ({len(z)} items)') - - return FieldRadialAxial(z, elec_coeffs, mag_coeffs) - 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])
    + return 2*np.pi*np.sum(jacobians[i] * positions[i, :, 0]) + + def get_tracer(self, bounds): + return T.TracerRadialBEM(self, bounds)

    Ancestors

    Methods

    @@ -3105,191 +1159,44 @@

    Methods

    -
    - -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 axial_derivative_interpolation(self, zmin, zmax, N=None) -
    -
    -

    Use a radial series expansion based on the potential derivatives at the optical axis -to allow very fast field evaluations.

    -

    Parameters

    -
    -
    zmin : float
    -
    Location on the optical axis where to start sampling the derivatives.
    -
    zmax : float
    -
    Location on the optical axis where to stop sampling the derivatives. 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

    -

    FieldRadialAxial object allowing fast field evaluations.

    -
    - -Expand source code - -
    def axial_derivative_interpolation(self, zmin, zmax, N=None):
    -    """
    -    Use a radial series expansion based on the potential derivatives at the optical axis
    -    to allow very fast field evaluations.
    -    
    -    Parameters
    -    ----------
    -    zmin : float
    -        Location on the optical axis where to start sampling the derivatives.
    -        
    -    zmax : float
    -        Location on the optical axis where to stop sampling the derivatives. 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
    -    -------
    -    `FieldRadialAxial` object allowing fast field evaluations.
    -
    -    """
    -    assert zmax > zmin
    -    N_charges = max(len(self.electrostatic_point_charges.charges), len(self.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(self.get_electrostatic_axial_potential_derivatives, z), axis=0)
    -    elec_coeffs = _quintic_spline_coefficients(z, elec_derivs.T)
    -    
    -    mag_derivs = np.concatenate(util.split_collect(self.get_magnetostatic_axial_potential_derivatives, z), axis=0)
    -    mag_coeffs = _quintic_spline_coefficients(z, mag_derivs.T)
    -    
    -    print(f'Computing derivative interpolation took {(time.time()-st)*1000:.2f} ms ({len(z)} items)')
    -    
    -    return FieldRadialAxial(z, elec_coeffs, mag_coeffs)
    -
    -def current_field_at_point(self, point) +def current_field_at_point(self, point_)
    -
    - -Expand source code - -
    def current_field_at_point(self, point):
    -    currents = self.current_point_charges.charges
    -    jacobians = self.current_point_charges.jacobians
    -    positions = self.current_point_charges.positions
    -     
    -    if point.shape == (2,):
    -        # Input point is 2D, so return a 2D point
    -        result = backend.current_field(backend._vec_2d_to_3d(point), currents, jacobians, positions)
    -        assert np.isclose(result[1], 0.)
    -        return backend._vec_3d_to_2d(result)
    -    else:
    -        return backend.current_field(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) +def electrostatic_field_at_point(self, point_)

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

    Parameters

    -
    point : (2,) array of float64
    +
    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 r and z directions.

    -
    - -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.   
    -    """
    -    assert point.shape == (2,) or point.shape == (3,)
    -    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)
    -
    +

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

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

    Compute the electrostatic potential.

    Parameters

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

    Returns

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

    -
    - -Expand source code - -
    def electrostatic_potential_at_point(self, point):
    -    """
    -    Compute the electrostatic potential.
    -    
    -    Parameters
    -    ----------
    -    point: (2,) array of float64
    -        Position at which to compute the field.
    -    
    -    Returns
    -    -------
    -    Potential as a float value (in units of V).
    -    """
    -    point = np.array(point).astype(np.float64)
    -    assert point.shape == (2,) or point.shape == (3,)
    -    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 get_current_axial_potential_derivatives(self, z) @@ -3305,30 +1212,6 @@

    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.

    -
    - -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_ring(z, currents, jacobians, positions)
    -
    def get_electrostatic_axial_potential_derivatives(self, z) @@ -3344,29 +1227,6 @@

    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.

    -
    - -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_ring(z, charges, jacobians, positions)
    -
    def get_magnetostatic_axial_potential_derivatives(self, z) @@ -3382,111 +1242,38 @@

    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.

    -
    - -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_ring(z, charges, jacobians, positions)
    -    derivs_current = self.get_current_axial_potential_derivatives(z)
    -    return derivs_magnetic + derivs_current
    -
    + +
    +def get_tracer(self, bounds) +
    +
    +
    -def magnetostatic_field_at_point(self, point) +def magnetostatic_field_at_point(self, point_)

    Compute the magnetic field \vec{H}

    Parameters

    -
    point : (2,) array of float64
    +
    point : (3,) 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.

    -
    - -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).astype(np.float64)
    -    assert point.shape == (2,)
    -    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
    -
    +

    (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) +def magnetostatic_potential_at_point(self, point_)

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

    Parameters

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

    Returns

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

    -
    - -Expand source code - -
    def magnetostatic_potential_at_point(self, point):
    -    """
    -    Compute the magnetostatic scalar potential (satisfying \\(\\vec{H} = -\\nabla \\phi \\))
    -    
    -    Parameters
    -    ----------
    -    point: (2,) array of float64
    -        Position at which to compute the field.
    -    
    -    Returns
    -    -------
    -    Potential as a float value (in units of A).
    -    """
    -
    -    assert point.shape == (2,)
    -    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)
    -

    Inherited members

    @@ -3506,7 +1293,6 @@

    Inherited members

    - \ No newline at end of file + diff --git a/docs/docs/v0.7.3/excitation.html b/docs/docs/v0.7.3/excitation.html new file mode 100644 index 0000000..775fe97 --- /dev/null +++ b/docs/docs/v0.7.3/excitation.html @@ -0,0 +1,662 @@ + + + + + + +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:

    +
      +
    • Fixed voltage (electrode connect to a power supply)
    • +
    • Voltage function (a generic Python function specifies the voltage as a function of position)
    • +
    • Dielectric, with arbitrary electric permittivity
    • +
    • Current coil, with fixed total amount of current (only in radial symmetry)
    • +
    • 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 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 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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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.
    +        """
    +
    +        assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
    +         
    +        for name, current in kwargs.items():
    +            assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
    +            self.excitation_types[name] = (ExcitationType.CURRENT, current)
    +
    +    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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            self.excitation_types[name] = (ExcitationType.DIELECTRIC, permittivity)
    +
    +    def add_electrostatic_boundary(self, *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.
    +        """
    +        self.add_dielectric(**{a:0 for a in args})
    +    
    +    def add_magnetostatic_boundary(self, *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.
    +        """
    +
    +        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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Check whether a current is applied in this excitation.

    +
    +
    +def is_electrostatic(self) +
    +
    +

    Check whether the excitation contains electrostatic fields.

    +
    +
    +def is_magnetostatic(self) +
    +
    +

    Check whether the excitation contains magnetostatic fields.

    +
    +
    +
    +
    +class ExcitationType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

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

    +
    + +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')
    +
    +

    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) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +
    +
    +class Symmetry +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    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.

    +
    + +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
    +
    +

    Ancestors

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

    Class variables

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

    Methods

    +
    +
    +def is_2d(self) +
    +
    +
    +
    +
    +def is_3d(self) +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/focus.html b/docs/docs/v0.7.3/focus.html new file mode 100644 index 0000000..32981b8 --- /dev/null +++ b/docs/docs/v0.7.3/focus.html @@ -0,0 +1,81 @@ + + + + + + +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) +
    +
    +

    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

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/geometry.html b/docs/docs/v0.7.3/geometry.html new file mode 100644 index 0000000..caf7b71 --- /dev/null +++ b/docs/docs/v0.7.3/geometry.html @@ -0,0 +1,2599 @@ + + + + + + +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) +
    +
    +

    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.

    +
    + +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
    +        assert self.path_length > 0
    +        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-coordiante 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-coordiante 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
    +        --------------------------------
    +        y0: float
    +            x-coordinate of the center of the circle
    +        y0: float
    +            y-coordiante 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"""
    +        
    +        pstart, pmiddle, pend = self.starting_point(), self.middle_point(), self.endpoint()
    +        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"""
    +
    +        pstart, pend = self.starting_point(), self.endpoint()
    +        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"""
    +
    +        pstart, pend = self.starting_point(), self.endpoint()
    +        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 not isinstance(other, Path) and not isinstance(other, PathCollection):
    +            return NotImplemented
    +        
    +        if isinstance(other, Path):
    +            return PathCollection([self, other])
    +        elif isinstance(other, PathCollection):
    +            return PathCollection([self] + [other.paths])
    +     
    +    def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None):
    +        """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)
    +
    +    def __str__(self):
    +        return f"<Path name:{self.name}, length:{self.path_length:.1e}, number of breakpoints:{len(self.breakpoints)}>"
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def aperture(height, radius, extent, z=0.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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def circle_xy(x0, y0, radius, angle=6.283185307179586) +
    +
    +

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

    +

    Parameters

    +
    +
    y0 : float
    +
    x-coordinate of the center of the circle
    +
    y0 : float
    +
    y-coordiante 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) +
    +
    +

    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-coordiante 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) +
    +
    +

    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-coordiante 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) +
    +
    +

    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=[]) +
    +
    +

    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) +
    +
    +

    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]) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Evaluate a point along the path.

    +

    Parameters

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

    Returns

    +

    (3,) float

    +

    Three dimensional point.

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def close(self) +
    +
    +

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

    +

    Returns

    +
    +
    Path
    +
     
    +
    +
    +
    +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.

    +
    +
    +def endpoint(self) +
    +
    +

    Returns the endpoint of the path.

    +

    Returns

    +

    (3,) float

    +

    The endpoint of the path.

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def map_points(self, fun) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Returns the starting point of the path.

    +

    Returns

    +

    (3,) float

    +

    The starting point of the path.

    +
    +
    +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

    +
    +
    +

    Inherited members

    + +
    +
    +class PathCollection +(paths) +
    +
    +

    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.

    +
    + +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):
    +        """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)
    +
    +        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 not isinstance(other, Path) and not isinstance(other, PathCollection):
    +            return NotImplemented
    +        
    +        if isinstance(other, Path):
    +            return PathCollection(self.paths+[other])
    +        else:
    +            return PathCollection(self.paths+other.paths)
    +      
    +    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 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)} surfaces, name: {self.name}>"
    +
    +

    Ancestors

    + +

    Instance variables

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

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +def __iadd__(self, other) +
    +
    +

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

    +
    +
    +def extrude(self, vector) +
    +
    +
    +
    +
    +def extrude_by_path(self, p2) +
    +
    +
    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None) +
    +
    + +
    +
    +def revolve_x(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_y(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_z(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class Surface +(fun, path_length1, path_length2, breakpoints1=[], breakpoints2=[], name=None) +
    +
    +

    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.

    +
    + +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
    +        assert self.path_length1 > 0 and self.path_length2 > 0
    +        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 aperture(height, radius, extent, z=0.):
    +        return Path.aperture(height, radius, extent, z=z).revolve_z()
    +     
    +    def __add__(self, other):
    +        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
    +        if not isinstance(other, Surface) and not isinstance(other, SurfaceCollection):
    +            return NotImplemented
    +
    +        if isinstance(other, Surface):
    +            return SurfaceCollection([self, other])
    +        else:
    +            return SurfaceCollection([self] + other.surfaces)
    +     
    +    def mesh(self, mesh_size=None, mesh_size_factor=None, name=None):
    +        """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)
    +    
    +    def __str__(self):
    +        return f"<Surface with name: {self.name}>"
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def aperture(height, radius, extent, z=0.0) +
    +
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +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

    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, name=None) +
    +
    +

    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

    +
    +
    +

    Inherited members

    + +
    +
    +class SurfaceCollection +(surfaces) +
    +
    +

    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.

    +
    + +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):
    +        """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)
    +         
    +        return mesh
    +     
    +    def __add__(self, other):
    +        """Allows you to combine surfaces into a `SurfaceCollection` using the + operator (surface1 + surface2)."""
    +        if not isinstance(other, Surface) and not isinstance(other, SurfaceCollection):
    +            return NotImplemented
    +              
    +        if isinstance(other, Surface):
    +            return SurfaceCollection(self.surfaces+[other])
    +        else:
    +            return SurfaceCollection(self.surfaces+other.surfaces)
    +     
    +    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 __str__(self):
    +        return f"<SurfaceCollection with {len(self.surfaces)} surfaces, name: {self.name}>"
    +
    +

    Ancestors

    + +

    Instance variables

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

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +def __iadd__(self, other) +
    +
    +

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

    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, name=None) +
    +
    + +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/index.html b/docs/docs/v0.7.3/index.html new file mode 100644 index 0000000..cb9b6e9 --- /dev/null +++ b/docs/docs/v0.7.3/index.html @@ -0,0 +1,139 @@ + + + + + + +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.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.interpolation
    +
    +
    +
    +
    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 electrons within any field type returned by the traceon.solver module. The tracing algorithm +used is RK45 with …

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/interpolation.html b/docs/docs/v0.7.3/interpolation.html new file mode 100644 index 0000000..1520734 --- /dev/null +++ b/docs/docs/v0.7.3/interpolation.html @@ -0,0 +1,374 @@ + + + + + + +traceon.interpolation API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.interpolation

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class FieldAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +

    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.

    +
    + +Expand source code + +
    class FieldAxial(S.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)
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +

    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, S.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: S.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.TracerRadialAxial(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +def get_tracer(self, bounds) +
    +
    +
    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/logging.html b/docs/docs/v0.7.3/logging.html new file mode 100644 index 0000000..7142c27 --- /dev/null +++ b/docs/docs/v0.7.3/logging.html @@ -0,0 +1,148 @@ + + + + + + +traceon.logging API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.logging

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +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'.

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class LogLevel +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    Enumeration representing a certain verbosity of logging.

    +
    + +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."""
    +
    +

    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.

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/mesher.html b/docs/docs/v0.7.3/mesher.html new file mode 100644 index 0000000..9369e62 --- /dev/null +++ b/docs/docs/v0.7.3/mesher.html @@ -0,0 +1,933 @@ + + + + + + +traceon.mesher API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.mesher

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class GeometricObject +
    +
    +

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

    +
    + +Expand source code + +
    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."""
    +    
    +    @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 counter clockwise 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"
    +        origin = np.array(origin)
    +        assert origin.shape == (3,), "Please supply a 3D point for origin"
    +         
    +        if Rx != 0.:
    +            matrix = np.array([[1, 0, 0],
    +                [0, np.cos(Rx), -np.sin(Rx)],
    +                [0, np.sin(Rx), np.cos(Rx)]])
    +        elif Ry != 0.:
    +            matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)],
    +                [0, 1, 0],
    +                [-np.sin(Ry), 0, np.cos(Ry)]])
    +        elif Rz != 0.:
    +            matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0],
    +                [np.sin(Rz), np.cos(Rz), 0],
    +                [0, 0, 1]])
    +
    +        return self.map_points(lambda p: origin + matrix @ (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]]))
    +
    +

    Ancestors

    +
      +
    • abc.ABC
    • +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def map_points(self, fun: Callable[[numpy.ndarray], numpy.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 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.

    +
    +
    +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.

    +
    +
    +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.

    +
    +
    +def move(self, dx=0.0, dy=0.0, dz=0.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.

    +
    +
    +def rotate(self, Rx=0.0, Ry=0.0, Rz=0.0, origin=[0.0, 0.0, 0.0]) +
    +
    +

    Rotate counter clockwise 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.

    +
    +
    +
    +
    +class Mesh +(points=[], lines=[], triangles=[], physical_to_lines={}, physical_to_triangles={}) +
    +
    +

    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.

    +
    + +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 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()
    +        
    +        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')
    +    
    +    @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}>'
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def from_meshio(mesh: meshio._mesh.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 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
    +
     
    +
    +
    +
    +

    Methods

    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def flip_normals(self) +
    +
    +

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

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def get_electrodes(self) +
    +
    +

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

    +

    Returns

    +
    +
    str iterable
    +
     
    +
    Names
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def is_higher_order(self) +
    +
    +

    Whether the mesh contains higher order elements.

    +

    Returns

    +
    +
    bool
    +
     
    +
    +
    +
    +def map_points(self, fun) +
    +
    + +
    +
    +def remove_lines(self) +
    +
    +

    Remove all the lines from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def remove_triangles(self) +
    +
    +

    Remove all triangles from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def to_meshio(self) +
    +
    +

    Convert the Mesh to a meshio object.

    +

    Returns

    +
    +
    meshio.Mesh
    +
     
    +
    +
    +
    +def write(self, 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) +
    +
    +

    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

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/plotting.html b/docs/docs/v0.7.3/plotting.html new file mode 100644 index 0000000..a38e672 --- /dev/null +++ b/docs/docs/v0.7.3/plotting.html @@ -0,0 +1,83 @@ + + + + + + +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.

    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def plot_mesh(mesh, show_normals=False, show_legend=True, **colors) +
    +
    +

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

    +

    Parameters

    +
    +
    show_normals : bool
    +
    Whether to show the normal vectors at every element
    +
    show_legend : bool
    +
    Whether to show the legend
    +
    colors : dict of (string, string)
    +
    Use keyword arguments to specify colors, for example plot_mesh(mesh, lens='blue', ground='green')
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/solver.html b/docs/docs/v0.7.3/solver.html new file mode 100644 index 0000000..6c969d6 --- /dev/null +++ b/docs/docs/v0.7.3/solver.html @@ -0,0 +1,1397 @@ + + + + + + +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) +
    +
    +

    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

    +

    A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a three +dimensional geometry Field3D_BEM is returned.

    +
    +
    +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).

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Field +
    +
    +

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

    +
    + +Expand source code + +
    class Field(ABC):
    +    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)
    +         
    +        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 magnetostatic_potential_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def electrostatic_potential_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def magnetostatic_field_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def electrostatic_field_at_point(self, point):
    +        ...
    +
    +

    Ancestors

    +
      +
    • abc.ABC
    • +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def electrostatic_field_at_point(self, point) +
    +
    +
    +
    +
    +def electrostatic_potential_at_point(self, point) +
    +
    +
    +
    +
    +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}.

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +def magnetostatic_field_at_point(self, point) +
    +
    +
    +
    +
    +def magnetostatic_potential_at_point(self, 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)
    +
     
    +
    +
    +
    +
    +
    +class Field3D_BEM +(electrostatic_point_charges=None, magnetostatic_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.

    +
    + +Expand source code + +
    class Field3D_BEM(FieldBEM):
    +    """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`."""
    +     
    +    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
    +        
    +        if electrostatic_point_charges is None:
    +            electrostatic_point_charges = EffectivePointCharges.empty_3d()
    +        if magnetostatic_point_charges is None:
    +            magnetostatic_point_charges = EffectivePointCharges.empty_3d()
    +         
    +        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d())
    +        
    +        self.symmetry = E.Symmetry.THREE_D
    +
    +        for eff in [electrostatic_point_charges, magnetostatic_point_charges]:
    +            N = len(eff.charges)
    +            assert eff.charges.shape == (N,)
    +            assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD)
    +            assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3)
    +     
    +    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 
    +        """
    +        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_3d(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_3d(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"
    +        charges = self.magnetostatic_point_charges.charges
    +        jacobians = self.magnetostatic_point_charges.jacobians
    +        positions = self.magnetostatic_point_charges.positions
    +        return backend.field_3d(point, charges, jacobians, positions)
    +     
    +    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_3d(point, charges, jacobians, positions)
    +     
    +    def area_of_element(self, i):
    +        jacobians = self.electrostatic_point_charges.jacobians
    +        return np.sum(jacobians[i])
    +    
    +    def get_tracer(self, bounds):
    +        return T.Tracer3D_BEM(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def area_of_element(self, i) +
    +
    +
    +
    +
    +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_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

    + +
    +
    +class FieldAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +

    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.

    +
    + +Expand source code + +
    class FieldAxial(Field):
    +    """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)
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class FieldBEM +(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) +
    +
    +

    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.

    +
    + +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)}>'
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def area_of_element(self, i: int) ‑> float +
    +
    +
    +
    +
    +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.

    +
    +
    +def charge_on_element(self, 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 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 is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +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.
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class FieldRadialAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +
    +
    + +Expand source code + +
    class FieldRadialAxial(FieldAxial):
    +    """ """
    +    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    +        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)
    +        self.symmetry = E.Symmetry.RADIAL
    +    
    +    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"
    +        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: (3,) array of float64
    +            Position at which to compute the field.
    +             
    +        Returns
    +        -------
    +        (3,) array of float64, containing the field strengths (units of A/m)
    +        """
    +        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: (3,) 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: (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"
    +        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    +    
    +    def get_tracer(self, bounds):
    +        return T.TracerRadialAxial(self, bounds)
    +
    +

    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

    +

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

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

    Compute the electrostatic potential (close to the axis).

    +

    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_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,) array of float64, containing the field strengths (units of A/m)

    +
    +
    +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 FieldRadialBEM +(electrostatic_point_charges=None, magnetostatic_point_charges=None, current_point_charges=None) +
    +
    +

    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.

    +
    + +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_)
    +        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(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.TracerRadialBEM(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def area_of_element(self, i) +
    +
    +
    +
    +
    +def current_field_at_point(self, point_) +
    +
    +
    +
    +
    +def current_potential_axial(self, z) +
    +
    +
    +
    +
    +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)

    +
    +
    +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_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.

    +
    +
    +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.

    +
    +
    +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.

    +
    +
    +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

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.7.3/tracing.html b/docs/docs/v0.7.3/tracing.html new file mode 100644 index 0000000..fb20b34 --- /dev/null +++ b/docs/docs/v0.7.3/tracing.html @@ -0,0 +1,494 @@ + + + + + + +traceon.tracing API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.tracing

    +
    +
    +

    The tracing module allows to trace electrons 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) +
    +
    +

    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 an electron as returned by Tracer.
    +
    +

    Returns

    +
    +
    float, z-value of the intersection with the x=0 plane
    +
     
    +
    +
    +
    +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 an electron 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 electron at the intersection point.

    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +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).

    +
    +
    +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 an electron 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 electron at the intersection point.

    +
    +
    +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 an electron 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 electron at the intersection point.

    +
    +
    +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 an electron 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 electron at the intersection point.

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Tracer +(field, bounds) +
    +
    +

    General electron tracer class. Can trace electrons given any field class from traceon.solver.

    +

    Parameters

    +
    +
    field : Field (or any class inheriting Field)
    +
    The field used to compute the force felt by the electron.
    +
    bounds : (3, 2) np.ndarray of float64
    +
    Once the electron reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
    +
    +
    + +Expand source code + +
    class Tracer:
    +    """General electron tracer class. Can trace electrons 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 electron.
    +    bounds: (3, 2) np.ndarray of float64
    +        Once the electron 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
    +    
    +    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):
    +        """Trace an electron.
    +
    +        Parameters
    +        ----------
    +        position: (3,) np.ndarray of float64
    +            Initial position of electron.
    +        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.
    +        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
    +        (in ns) 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.
    +        """
    +        raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance')
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def __call__(self, position, velocity) +
    +
    +

    Trace an electron.

    +

    Parameters

    +
    +
    position : (3,) np.ndarray of float64
    +
    Initial position of electron.
    +
    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.
    +
    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 +(in ns) 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.

    +
    +
    +
    +
    +class Tracer3DAxial +(field, bounds) +
    +
    +

    General electron tracer class. Can trace electrons given any field class from traceon.solver.

    +

    Parameters

    +
    +
    field : Field (or any class inheriting Field)
    +
    The field used to compute the force felt by the electron.
    +
    bounds : (3, 2) np.ndarray of float64
    +
    Once the electron reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
    +
    +
    + +Expand source code + +
    class Tracer3DAxial(Tracer):
    +    def __call__(self, position, velocity, atol=1e-10):
    +        velocity = _convert_velocity_to_SI(velocity)
    +        return backend.trace_particle_3d_derivs(position, velocity, self.bounds, atol,
    +            self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class Tracer3D_BEM +(field, bounds) +
    +
    +

    General electron tracer class. Can trace electrons given any field class from traceon.solver.

    +

    Parameters

    +
    +
    field : Field (or any class inheriting Field)
    +
    The field used to compute the force felt by the electron.
    +
    bounds : (3, 2) np.ndarray of float64
    +
    Once the electron reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
    +
    +
    + +Expand source code + +
    class Tracer3D_BEM(Tracer):
    +    def __call__(self, position, velocity, atol=1e-10):
    +        velocity = _convert_velocity_to_SI(velocity)
    +        bounds = self.field.field_bounds
    +        elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges
    +        return backend.trace_particle_3d(position, velocity, self.bounds, atol, elec, mag)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class TracerRadialAxial +(field, bounds) +
    +
    +

    General electron tracer class. Can trace electrons given any field class from traceon.solver.

    +

    Parameters

    +
    +
    field : Field (or any class inheriting Field)
    +
    The field used to compute the force felt by the electron.
    +
    bounds : (3, 2) np.ndarray of float64
    +
    Once the electron reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
    +
    +
    + +Expand source code + +
    class TracerRadialAxial(Tracer):
    +    def __call__(self, position, velocity, atol=1e-10):
    +
    +        velocity = _convert_velocity_to_SI(velocity)
    +        
    +        elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs
    +        
    +        return backend.trace_particle_radial_derivs(position, velocity, self.bounds, atol, self.field.z, elec, mag)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class TracerRadialBEM +(field, bounds) +
    +
    +

    General electron tracer class. Can trace electrons given any field class from traceon.solver.

    +

    Parameters

    +
    +
    field : Field (or any class inheriting Field)
    +
    The field used to compute the force felt by the electron.
    +
    bounds : (3, 2) np.ndarray of float64
    +
    Once the electron reaches one of the boundaries the tracing stops. The bounds are of the form ( (xmin, xmax), (ymin, ymax), (zmin, zmax) ).
    +
    +
    + +Expand source code + +
    class TracerRadialBEM(Tracer):
    +    def __call__(self, position, velocity, atol=1e-10):
    +        
    +        velocity = _convert_velocity_to_SI(velocity)
    +        
    +        return backend.trace_particle_radial(
    +                position,
    +                velocity, 
    +                self.bounds,
    +                atol, 
    +                self.field.electrostatic_point_charges,
    +                self.field.magnetostatic_point_charges,
    +                self.field.current_point_charges,
    +                field_bounds=self.field.field_bounds)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/excitation.html b/docs/docs/v0.8.0/excitation.html new file mode 100644 index 0000000..1e99b86 --- /dev/null +++ b/docs/docs/v0.8.0/excitation.html @@ -0,0 +1,670 @@ + + + + + + +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:

    +
      +
    • Fixed voltage (electrode connect to a power supply)
    • +
    • Voltage function (a generic Python function specifies the voltage as a function of position)
    • +
    • Dielectric, with arbitrary electric permittivity
    • +
    • Current coil, with fixed total amount of current (only in radial symmetry)
    • +
    • 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 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 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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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.
    +        """
    +
    +        assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes"
    +         
    +        for name, current in kwargs.items():
    +            assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode"
    +            self.excitation_types[name] = (ExcitationType.CURRENT, current)
    +
    +    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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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():
    +            assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh'
    +            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)
    +        
    +        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)
    +        
    +        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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Check whether a current is applied in this excitation.

    +
    +
    +def is_electrostatic(self) +
    +
    +

    Check whether the excitation contains electrostatic fields.

    +
    +
    +def is_magnetostatic(self) +
    +
    +

    Check whether the excitation contains magnetostatic fields.

    +
    +
    +
    +
    +class ExcitationType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

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

    +
    + +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')
    +
    +

    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) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +
    +
    +class Symmetry +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    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.

    +
    + +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
    +
    +

    Ancestors

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

    Class variables

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

    Methods

    +
    +
    +def is_2d(self) +
    +
    +
    +
    +
    +def is_3d(self) +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/focus.html b/docs/docs/v0.8.0/focus.html new file mode 100644 index 0000000..32981b8 --- /dev/null +++ b/docs/docs/v0.8.0/focus.html @@ -0,0 +1,81 @@ + + + + + + +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) +
    +
    +

    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

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/geometry.html b/docs/docs/v0.8.0/geometry.html new file mode 100644 index 0000000..c0f6daf --- /dev/null +++ b/docs/docs/v0.8.0/geometry.html @@ -0,0 +1,2859 @@ + + + + + + +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) +
    +
    +

    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.

    +
    + +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)}>"
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def aperture(height, radius, extent, z=0.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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def circle_xy(x0, y0, radius, angle=6.283185307179586) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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=[]) +
    +
    +

    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) +
    +
    +

    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]) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Evaluate a point along the path.

    +

    Parameters

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

    Returns

    +

    (3,) float

    +

    Three dimensional point.

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def close(self) +
    +
    +

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

    +

    Returns

    +
    +
    Path
    +
     
    +
    +
    +
    +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.

    +
    +
    +def endpoint(self) +
    +
    +

    Returns the endpoint of the path.

    +

    Returns

    +

    (3,) float

    +

    The endpoint of the path.

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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.

    +
    +
    +def map_points(self, fun) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Returns the starting point of the path.

    +

    Returns

    +

    (3,) float

    +

    The starting point of the path.

    +
    +
    +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

    +
    +
    +

    Inherited members

    + +
    +
    +class PathCollection +(paths) +
    +
    +

    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.

    +
    + +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 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)} surfaces, name: {self.name}>"
    +
    +

    Ancestors

    + +

    Instance variables

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

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +def __iadd__(self, other) +
    +
    +

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

    +
    +
    +def extrude(self, vector) +
    +
    +
    +
    +
    +def extrude_by_path(self, p2) +
    +
    +
    +
    +
    +def mesh(self, mesh_size=None, mesh_size_factor=None, higher_order=False, name=None, ensure_outward_normals=True) +
    +
    + +
    +
    +def revolve_x(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_y(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +def revolve_z(self, angle=6.283185307179586) +
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class Surface +(fun, path_length1, path_length2, breakpoints1=[], breakpoints2=[], name=None) +
    +
    +

    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.

    +
    + +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 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}>"
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def aperture(height, radius, extent, z=0.0) +
    +
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +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

    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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

    +

    Mesh

    +
    +
    +def revolve_boundary_x(self, angle=6.283185307179586, 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
    +
     
    +
    +
    +
    +def revolve_boundary_y(self, angle=6.283185307179586, 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
    +
     
    +
    +
    +
    +def revolve_boundary_z(self, angle=6.283185307179586, 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
    +
     
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class SurfaceCollection +(surfaces) +
    +
    +

    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.

    +
    + +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 __str__(self):
    +        return f"<SurfaceCollection with {len(self.surfaces)} surfaces, name: {self.name}>"
    +
    +

    Ancestors

    + +

    Instance variables

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

    Methods

    +
    +
    +def __add__(self, other) +
    +
    +

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

    +
    +
    +def __iadd__(self, other) +
    +
    +

    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) +
    +
    + +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/index.html b/docs/docs/v0.8.0/index.html new file mode 100644 index 0000000..764a2fb --- /dev/null +++ b/docs/docs/v0.8.0/index.html @@ -0,0 +1,139 @@ + + + + + + +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.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.interpolation
    +
    +
    +
    +
    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 …

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/interpolation.html b/docs/docs/v0.8.0/interpolation.html new file mode 100644 index 0000000..1520734 --- /dev/null +++ b/docs/docs/v0.8.0/interpolation.html @@ -0,0 +1,374 @@ + + + + + + +traceon.interpolation API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.interpolation

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class FieldAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +

    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.

    +
    + +Expand source code + +
    class FieldAxial(S.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)
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +

    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, S.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: S.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.TracerRadialAxial(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +def get_tracer(self, bounds) +
    +
    +
    +
    +
    +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.

    +
    +
    +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).

    +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/logging.html b/docs/docs/v0.8.0/logging.html new file mode 100644 index 0000000..7142c27 --- /dev/null +++ b/docs/docs/v0.8.0/logging.html @@ -0,0 +1,148 @@ + + + + + + +traceon.logging API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.logging

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +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'.

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class LogLevel +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    Enumeration representing a certain verbosity of logging.

    +
    + +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."""
    +
    +

    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.

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/mesher.html b/docs/docs/v0.8.0/mesher.html new file mode 100644 index 0000000..dfeb849 --- /dev/null +++ b/docs/docs/v0.8.0/mesher.html @@ -0,0 +1,1037 @@ + + + + + + +traceon.mesher API documentation + + + + + + + + + + + + + +
    +
    +
    +

    Module traceon.mesher

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class GeometricObject +
    +
    +

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

    +
    + +Expand source code + +
    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."""
    +    
    +    @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 counter clockwise 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"
    +        origin = np.array(origin)
    +        assert origin.shape == (3,), "Please supply a 3D point for origin"
    +         
    +        if Rx != 0.:
    +            matrix = np.array([[1, 0, 0],
    +                [0, np.cos(Rx), -np.sin(Rx)],
    +                [0, np.sin(Rx), np.cos(Rx)]])
    +        elif Ry != 0.:
    +            matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)],
    +                [0, 1, 0],
    +                [-np.sin(Ry), 0, np.cos(Ry)]])
    +        elif Rz != 0.:
    +            matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0],
    +                [np.sin(Rz), np.cos(Rz), 0],
    +                [0, 0, 1]])
    +
    +        return self.map_points(lambda p: origin + matrix @ (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]]))
    +
    +

    Ancestors

    +
      +
    • abc.ABC
    • +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def map_points(self, fun: Callable[[numpy.ndarray], numpy.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 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.

    +
    +
    +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.

    +
    +
    +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.

    +
    +
    +def move(self, dx=0.0, dy=0.0, dz=0.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.

    +
    +
    +def rotate(self, Rx=0.0, Ry=0.0, Rz=0.0, origin=[0.0, 0.0, 0.0]) +
    +
    +

    Rotate counter clockwise 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.

    +
    +
    +
    +
    +class Mesh +(points=[], lines=[], triangles=[], physical_to_lines={}, physical_to_triangles={}, ensure_outward_normals=True) +
    +
    +

    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.

    +
    + +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)
    +
    +

    Ancestors

    + +

    Static methods

    +
    +
    +def from_meshio(mesh: meshio._mesh.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 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
    +
     
    +
    +
    +
    +

    Methods

    +
    +
    +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
    +
     
    +
    +
    +
    +def ensure_inward_normals(self, electrode) +
    +
    +
    +
    +
    +def ensure_outward_normals(self, electrode) +
    +
    +
    +
    +
    +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.

    +
    +
    +def flip_normals(self) +
    +
    +

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

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def get_electrodes(self) +
    +
    +

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

    +

    Returns

    +
    +
    str iterable
    +
     
    +
    Names
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +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
    +
     
    +
    +
    +
    +def is_higher_order(self) +
    +
    +

    Whether the mesh contains higher order elements.

    +

    Returns

    +
    +
    bool
    +
     
    +
    +
    +
    +def map_points(self, fun) +
    +
    + +
    +
    +def remove_lines(self) +
    +
    +

    Remove all the lines from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def remove_triangles(self) +
    +
    +

    Remove all triangles from the mesh.

    +

    Returns

    +
    +
    Mesh
    +
     
    +
    +
    +
    +def to_meshio(self) +
    +
    +

    Convert the Mesh to a meshio object.

    +

    Returns

    +
    +
    meshio.Mesh
    +
     
    +
    +
    +
    +def write(self, 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) +
    +
    +

    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

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/plotting.html b/docs/docs/v0.8.0/plotting.html new file mode 100644 index 0000000..f13df81 --- /dev/null +++ b/docs/docs/v0.8.0/plotting.html @@ -0,0 +1,409 @@ + + + + + + +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 +
    +
    +

    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) +
    +
    +

    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) +
    +
    + +
    +
    +def plot_equipotential_lines(*args, **kwargs) +
    +
    + +
    +
    +def plot_mesh(*args, **kwargs) +
    +
    +

    Calls Figure.plot_mesh() on the current Figure

    +
    +
    +def plot_trajectories(*args, **kwargs) +
    +
    +

    Calls Figure.plot_trajectories() on the current Figure

    +
    +
    +def show() +
    +
    +

    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') +
    +
    +

    Plot charge density using the Vedo library.

    +

    Parameters

    +
    +
    excitation : Excitation
    +
    Excitation applied
    +
    field : 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') +
    +
    +

    Make potential color plot including equipotential lines.

    +

    Parameters

    +
    +
    field : 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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    Show the figure.

    +
    +
    +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/solver.html b/docs/docs/v0.8.0/solver.html new file mode 100644 index 0000000..6c969d6 --- /dev/null +++ b/docs/docs/v0.8.0/solver.html @@ -0,0 +1,1397 @@ + + + + + + +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) +
    +
    +

    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

    +

    A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a three +dimensional geometry Field3D_BEM is returned.

    +
    +
    +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).

    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Field +
    +
    +

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

    +
    + +Expand source code + +
    class Field(ABC):
    +    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)
    +         
    +        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 magnetostatic_potential_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def electrostatic_potential_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def magnetostatic_field_at_point(self, point):
    +        ...
    +    
    +    @abstractmethod
    +    def electrostatic_field_at_point(self, point):
    +        ...
    +
    +

    Ancestors

    +
      +
    • abc.ABC
    • +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def electrostatic_field_at_point(self, point) +
    +
    +
    +
    +
    +def electrostatic_potential_at_point(self, point) +
    +
    +
    +
    +
    +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}.

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +def magnetostatic_field_at_point(self, point) +
    +
    +
    +
    +
    +def magnetostatic_potential_at_point(self, 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)
    +
     
    +
    +
    +
    +
    +
    +class Field3D_BEM +(electrostatic_point_charges=None, magnetostatic_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.

    +
    + +Expand source code + +
    class Field3D_BEM(FieldBEM):
    +    """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`."""
    +     
    +    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
    +        
    +        if electrostatic_point_charges is None:
    +            electrostatic_point_charges = EffectivePointCharges.empty_3d()
    +        if magnetostatic_point_charges is None:
    +            magnetostatic_point_charges = EffectivePointCharges.empty_3d()
    +         
    +        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d())
    +        
    +        self.symmetry = E.Symmetry.THREE_D
    +
    +        for eff in [electrostatic_point_charges, magnetostatic_point_charges]:
    +            N = len(eff.charges)
    +            assert eff.charges.shape == (N,)
    +            assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD)
    +            assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3)
    +     
    +    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 
    +        """
    +        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_3d(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_3d(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"
    +        charges = self.magnetostatic_point_charges.charges
    +        jacobians = self.magnetostatic_point_charges.jacobians
    +        positions = self.magnetostatic_point_charges.positions
    +        return backend.field_3d(point, charges, jacobians, positions)
    +     
    +    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_3d(point, charges, jacobians, positions)
    +     
    +    def area_of_element(self, i):
    +        jacobians = self.electrostatic_point_charges.jacobians
    +        return np.sum(jacobians[i])
    +    
    +    def get_tracer(self, bounds):
    +        return T.Tracer3D_BEM(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def area_of_element(self, i) +
    +
    +
    +
    +
    +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_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

    + +
    +
    +class FieldAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +

    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.

    +
    + +Expand source code + +
    class FieldAxial(Field):
    +    """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)
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class FieldBEM +(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) +
    +
    +

    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.

    +
    + +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)}>'
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def area_of_element(self, i: int) ‑> float +
    +
    +
    +
    +
    +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.

    +
    +
    +def charge_on_element(self, 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 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 is_electrostatic(self) +
    +
    +
    +
    +
    +def is_magnetostatic(self) +
    +
    +
    +
    +
    +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.
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class FieldRadialAxial +(z, electrostatic_coeffs=None, magnetostatic_coeffs=None) +
    +
    +
    +
    + +Expand source code + +
    class FieldRadialAxial(FieldAxial):
    +    """ """
    +    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    +        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)
    +        self.symmetry = E.Symmetry.RADIAL
    +    
    +    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"
    +        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: (3,) array of float64
    +            Position at which to compute the field.
    +             
    +        Returns
    +        -------
    +        (3,) array of float64, containing the field strengths (units of A/m)
    +        """
    +        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: (3,) 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: (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"
    +        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    +    
    +    def get_tracer(self, bounds):
    +        return T.TracerRadialAxial(self, bounds)
    +
    +

    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

    +

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

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

    Compute the electrostatic potential (close to the axis).

    +

    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_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,) array of float64, containing the field strengths (units of A/m)

    +
    +
    +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 FieldRadialBEM +(electrostatic_point_charges=None, magnetostatic_point_charges=None, current_point_charges=None) +
    +
    +

    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.

    +
    + +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_)
    +        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(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.TracerRadialBEM(self, bounds)
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def area_of_element(self, i) +
    +
    +
    +
    +
    +def current_field_at_point(self, point_) +
    +
    +
    +
    +
    +def current_potential_axial(self, z) +
    +
    +
    +
    +
    +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)

    +
    +
    +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_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.

    +
    +
    +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.

    +
    +
    +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.

    +
    +
    +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

    + +
    +
    +
    +
    + +
    + + + diff --git a/docs/docs/v0.8.0/tracing.html b/docs/docs/v0.8.0/tracing.html new file mode 100644 index 0000000..e798f65 --- /dev/null +++ b/docs/docs/v0.8.0/tracing.html @@ -0,0 +1,504 @@ + + + + + + +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) +
    +
    +

    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) +
    +
    +

    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_) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

    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) +
    +
    +

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

    +

    Parameters

    +
    +
    field : 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) ).
    +
    +
    + +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
    +    
    +    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.
    +        """
    +        raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance')
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def __call__(self, position, velocity, mass=9.1093837015e-31, charge=-1.602176634e-19, atol=1e-08) +
    +
    +

    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.

    +
    +
    +
    +
    +class Tracer3DAxial +(field, bounds) +
    +
    +

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

    +

    Parameters

    +
    +
    field : 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) ).
    +
    +
    + +Expand source code + +
    class Tracer3DAxial(Tracer):
    +    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    +        charge_over_mass = charge / mass
    +        velocity = _convert_velocity_to_SI(velocity, mass)
    +        return backend.trace_particle_3d_derivs(position, velocity, charge_over_mass, self.bounds, atol,
    +            self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class Tracer3D_BEM +(field, bounds) +
    +
    +

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

    +

    Parameters

    +
    +
    field : 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) ).
    +
    +
    + +Expand source code + +
    class Tracer3D_BEM(Tracer):
    +    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    +        charge_over_mass = charge / mass
    +        velocity = _convert_velocity_to_SI(velocity, mass)
    +        elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges
    +        return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, field_bounds=self.field.field_bounds)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class TracerRadialAxial +(field, bounds) +
    +
    +

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

    +

    Parameters

    +
    +
    field : 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) ).
    +
    +
    + +Expand source code + +
    class TracerRadialAxial(Tracer):
    +    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    +        charge_over_mass = charge / mass
    +        velocity = _convert_velocity_to_SI(velocity, mass)
    +        
    +        elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs
    +        
    +        return backend.trace_particle_radial_derivs(position, velocity, charge_over_mass, self.bounds, atol, self.field.z, elec, mag)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class TracerRadialBEM +(field, bounds) +
    +
    +

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

    +

    Parameters

    +
    +
    field : 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) ).
    +
    +
    + +Expand source code + +
    class TracerRadialBEM(Tracer):
    +    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    +        charge_over_mass = charge / mass
    +        velocity = _convert_velocity_to_SI(velocity, mass)
    +        
    +        return backend.trace_particle_radial(
    +                position,
    +                velocity,
    +                charge_over_mass, 
    +                self.bounds,
    +                atol, 
    +                self.field.electrostatic_point_charges,
    +                self.field.magnetostatic_point_charges,
    +                self.field.current_point_charges,
    +                field_bounds=self.field.field_bounds)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + diff --git a/examples/deflector-3d.py b/examples/deflector-3d.py index e45d0af..c4edd49 100644 --- a/examples/deflector-3d.py +++ b/examples/deflector-3d.py @@ -24,8 +24,8 @@ def round_electrode(z0, name): path = G.Path.line([RADIUS, 0.0, z0], [RADIUS, 0.0, z0+THICKNESS])\ - .line_to([RADIUS+ELECTRODE_WIDTH, 0.0, z0+THICKNESS])\ - .line_to([RADIUS+ELECTRODE_WIDTH, 0.0, z0])\ + .extend_with_line([RADIUS+ELECTRODE_WIDTH, 0.0, z0+THICKNESS])\ + .extend_with_line([RADIUS+ELECTRODE_WIDTH, 0.0, z0])\ .close() surf = path.revolve_z() diff --git a/examples/dohi-mirror.py b/examples/dohi-mirror.py index 12c35d1..0aa13c1 100644 --- a/examples/dohi-mirror.py +++ b/examples/dohi-mirror.py @@ -8,7 +8,7 @@ import traceon.solver as S import traceon.plotting as P import traceon.focus as F -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial @@ -116,4 +116,4 @@ print(f"mirror: {mirror_displacement} \n") print(f"Focus of perfectly aligned system: {[0,0,15]}") -print(f"Focus of displaced system: {focus_loc}") \ No newline at end of file +print(f"Focus of displaced system: {focus_loc}") diff --git a/examples/einzel-lens.py b/examples/einzel-lens.py index 33ff044..0bdc2c6 100644 --- a/examples/einzel-lens.py +++ b/examples/einzel-lens.py @@ -6,7 +6,7 @@ import traceon.excitation as E import traceon.plotting as P import traceon.tracing as T -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial # Dimensions of the einzel lens. THICKNESS = 0.5 @@ -18,7 +18,7 @@ z0 = -THICKNESS - SPACING - THICKNESS/2 boundary = G.Path.line([0., 0., 1.75], [2.0, 0., 1.75])\ - .line_to([2.0, 0., -1.75]).line_to([0., 0., -1.75]) + .extend_with_line([2.0, 0., -1.75]).extend_with_line([0., 0., -1.75]) margin_right = 0.1 diff --git a/setup.py b/setup.py index 6de96fc..d0c3738 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name='traceon', - version='0.8.0rc1', + version='0.8.0', description='Solver and tracer for electrostatic problems', url='https://github.com/leon-vv/Traceon', author='Léon van Velzen', diff --git a/tests/geometric_tests.py b/tests/geometric_tests.py index a922a5b..ae61ef7 100644 --- a/tests/geometric_tests.py +++ b/tests/geometric_tests.py @@ -140,11 +140,43 @@ def test_arc(self): p = Path.arc(center, [0., 0., 0.], [0., r, -r], reverse=True) assert np.isclose(p.path_length, 3/4* 2*pi*r) assert np.allclose(p(1/2 * 3/4*2*pi*r), center + np.array([0., r/sqrt(2), r/sqrt(2)])) - - + def test_polar_arc(self): + r = 1.0 + angle = pi/2 + p = Path.polar_arc(r, angle, start=[0,0,1], plane_normal=[0,1,0], direction=[1,0,0]) + + expected_length = r* angle + self.assertTrue(isclose(p.path_length, expected_length, rel_tol=1e-7)) + + start_point = p(0.) + end_point = p(p.path_length) + + assert np.allclose(start_point, [0., 0., 1.], atol=1e-7) + assert np.allclose(end_point, [1., 0., 0.], atol=1e-7) + + def test_extend_with_polar_arc(self): + base_path = Path.line([0.,0.,0.], [1.,0.,0.]) + extended_path = base_path.extend_with_polar_arc(radius=1.0, angle=pi/2, plane_normal=[0,1,0]) - + expected_length = 1.0 + (pi/2) + assert np.isclose(extended_path.path_length, expected_length) + + end_point = extended_path(extended_path.path_length) + assert np.allclose(end_point, [2.,0.,-1.], atol=1e-7) # should with right hand rule around y + + def test_velocity_vector(self): + circle = Path.circle_xy(x0=0.0, y0=0., radius=1., angle=2*pi) + + t_start = circle.velocity_vector(0.) + t_start_norm = t_start / np.linalg.norm(t_start) + assert np.allclose(t_start_norm, [0.,1.,0.], atol=1e-7) + + half_length = circle.path_length/2 + t_half = circle.velocity_vector(half_length) + t_half_norm = t_half / np.linalg.norm(t_half) + assert np.allclose(t_half_norm, [0.,-1.,0.], atol=1e-7) + class SurfaceTests(unittest.TestCase): def test_spanned_by_paths(self): @@ -169,7 +201,7 @@ def test_non_degenerate_triangles(self): [0, 0, 15]] inner = Path.line(points[0], points[1])\ - .line_to(points[2]).line_to(points[3]).revolve_z() + .extend_with_line(points[2]).extend_with_line(points[3]).revolve_z() inner.name = 'test' mesh = inner.mesh(mesh_size=10/2) diff --git a/tests/test_radial.py b/tests/test_radial.py index 05450b6..5074d7e 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -13,7 +13,7 @@ import traceon.tracing as T import traceon.backend as B import traceon.logging as logging -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial from tests.test_radial_ring import potential_of_ring_arbitrary, biot_savart_loop, magnetic_field_of_loop @@ -182,8 +182,8 @@ def test_interpolation_current_loop(self): def test_mag_pot_derivatives(self): boundary = G.Path.line([0., 0., 5.], [5., 0., 5.])\ - .line_to([5., 0., -5.])\ - .line_to([0., 0., -5.]) + .extend_with_line([5., 0., -5.])\ + .extend_with_line([0., 0., -5.]) r1 = G.Path.rectangle_xz(1, 2, 2, 3) r2 = G.Path.rectangle_xz(1, 2, -3, -2) @@ -247,10 +247,10 @@ def setUpClass(cls): coil.name = 'coil' pole = G.Path.line([1, 0, 0], [4, 0, 0])\ - .line_to([4, 0, 1])\ - .line_to([2, 0, 1])\ - .line_to([2, 0, 3])\ - .line_to([1, 0, 3])\ + .extend_with_line([4, 0, 1])\ + .extend_with_line([2, 0, 1])\ + .extend_with_line([2, 0, 3])\ + .extend_with_line([1, 0, 3])\ .close() pole.name = 'pole' @@ -302,7 +302,7 @@ class TestFlatEinzelLens(unittest.TestCase): @classmethod def setUpClass(cls): boundary = G.Path.line([0.1, 0.0, 1.0], [1.0, 0.0, 1.0])\ - .line_to([1.0, 0.0, -1.0]).line_to([0.1, 0.0, -1.0]) + .extend_with_line([1.0, 0.0, -1.0]).extend_with_line([0.1, 0.0, -1.0]) ground_top = G.Path.line([0.25, 0.0, 0.5], [0.75, 0.0, 0.5]) lens = G.Path.line([0.25, 0.0, 0.0], [0.75, 0.0, 0.0]) diff --git a/tests/test_radial_ring.py b/tests/test_radial_ring.py index 14dcdc1..0b941a6 100644 --- a/tests/test_radial_ring.py +++ b/tests/test_radial_ring.py @@ -67,20 +67,20 @@ class TestRadialRing(unittest.TestCase): def test_potential_radial_ring(self): for r0, z0, r, z in np.random.rand(10, 4): - assert np.isclose(B.potential_radial_ring(r0, z0, r, z)/epsilon_0, potential_of_ring_arbitrary(1., r0, z0, r, z)) + assert np.isclose(B.potential_radial_ring(r0, z0, r-r0, z-z0)/epsilon_0, potential_of_ring_arbitrary(1., r0, z0, r, z)) def test_potential_field_radial_single_ring(self): rs = 1 # Source point zs = 0 r = np.linspace(1.1, 2, 10000) - pot = [B.potential_radial_ring(r_, zs, rs, zs) for r_ in r] - deriv = [B.dr1_potential_radial_ring(r_, zs, rs, zs) for r_ in r] + pot = [B.potential_radial_ring(r_, zs, rs-r_, 0.) for r_ in r] + deriv = [B.dr1_potential_radial_ring(r_, zs, rs-r_, 0.) for r_ in r] assert np.allclose(CubicSpline(r, pot)(r, 1), deriv, atol=0., rtol=1e-9) z = np.linspace(0.1, 1, 10000) - pot = [B.potential_radial_ring(rs, z_, rs, zs) for z_ in z] - deriv = [B.dz1_potential_radial_ring(rs, z_, rs, zs) for z_ in z] + pot = [B.potential_radial_ring(rs, z_, 0., zs-z_) for z_ in z] + deriv = [B.dz1_potential_radial_ring(rs, z_, 0., zs-z_) for z_ in z] assert np.allclose(CubicSpline(z, pot)(z, 1), deriv, atol=0., rtol=1e-9) def test_field_radial_ring(self): @@ -88,14 +88,14 @@ def test_field_radial_ring(self): dz = z0/5e4 deriv = (potential_of_ring_arbitrary(1., r0, z0+dz, r, z) - potential_of_ring_arbitrary(1., r0, z0-dz, r, z)) / (2*dz) - assert np.isclose(B.dz1_potential_radial_ring(r0, z0, r, z)/epsilon_0, deriv, atol=0., rtol=1e-4), (r0, z0, r, z) + assert np.isclose(B.dz1_potential_radial_ring(r0, z0, r-r0, z-z0)/epsilon_0, deriv, atol=0., rtol=1e-4), (r0, z0, r, z) if r0 < 1e-3: continue dr = r0/5e4 deriv = (potential_of_ring_arbitrary(1., r0+dr, z0, r, z) - potential_of_ring_arbitrary(1., r0-dr, z0, r, z)) / (2*dr) - assert np.isclose(B.dr1_potential_radial_ring(r0, z0, r, z)/epsilon_0, deriv, atol=0., rtol=1e-4), (r0, z0, r, z) + assert np.isclose(B.dr1_potential_radial_ring(r0, z0, r-r0, z-z0)/epsilon_0, deriv, atol=0., rtol=1e-4), (r0, z0, r, z) def test_axial(self): z = np.linspace(-10, 10, 200) @@ -108,7 +108,7 @@ def test_axial(self): correct = k * Q / np.sqrt(z**2 + r**2) pot = [potential_of_ring_arbitrary(dq, 0., z0, r, 0.) for z0 in z] - traceon = [dq/epsilon_0*B.potential_radial_ring(0., z0, r, 0.) for z0 in z] + traceon = [dq/epsilon_0*B.potential_radial_ring(0., z0, r, 0.-z0) for z0 in z] assert np.allclose(pot, correct) assert np.allclose(traceon, correct) diff --git a/tests/test_three_d.py b/tests/test_three_d.py deleted file mode 100644 index ffee9cf..0000000 --- a/tests/test_three_d.py +++ /dev/null @@ -1,354 +0,0 @@ -import unittest -from math import pi, sqrt, atan2 -import os.path as path - -import numpy as np -from scipy.integrate import quad, dblquad -from scipy.constants import epsilon_0, mu_0 -from scipy.interpolate import CubicSpline - -import traceon.geometry as G -import traceon.plotting as P -import traceon.solver as S -import traceon.excitation as E -import traceon.tracing as T -import traceon.backend as B -import traceon.logging as logging - -logging.set_log_level(logging.LogLevel.SILENT) - -class TestThreeD(unittest.TestCase): - def test_revolved_rectangle(self): - #Define surface - THICKNESS = 1 - path = G.Path.line([0.0, 0.0, 0.0], [0.0, 0.0,THICKNESS])\ - .line_to([1., 0.0, THICKNESS])\ - .line_to([1., 0.0, 0.0])\ - .close() - - surf = path.revolve_z() - surf.name = 'revolvedrectangle' - - #create mesh - mesh = surf.mesh(mesh_size_factor=12) - - #add excitation - excitation = E.Excitation(mesh, E.Symmetry.THREE_D) - excitation.add_voltage(revolvedrectangle = -5.) - - #try solving - field = S.solve_direct(excitation) - - z = np.array([0.25, 0.5, 0.75]) - axial_potential = [field.potential_at_point([0.0, 0.0, z_]) for z_ in [0.25, 0.5, 0.75]] - - assert np.allclose(axial_potential, -5, rtol=1e-4) - - def test_dohi_meshing(self): - rmax = 1.0 - margin = 0.3 - extent = rmax-0.1 - - t = 0.15 # thickness - r = 0.075 # radius - st = 0.5 # spacer thickness - - mirror = G.Path.aperture(0.15, r, extent, z=t/2) - mirror.name = 'mirror' - - mirror_line = G.Path.line([0., 0., 0.], [r, 0., 0.]) - mirror_line.name = 'mirror' - - lens = G.Path.aperture(0.15, r, extent, z=t + st + t/2) - lens.name = 'lens' - - ground = G.Path.aperture(0.15, r, extent, z=t+st+t+st+t/2) - ground.name = 'ground' - - boundary = G.Path.line([0., 0., 1.75], [rmax, 0., 1.75]) \ - .line_to([rmax, 0., -0.3]).line_to([0., 0., -0.3]) - boundary.name = 'boundary' - - geom = mirror+mirror_line+lens+ground+boundary - geom = geom.revolve_z() - geom.mesh(mesh_size_factor=4) - - def test_simple_current_line_against_radial(self): - rect = G.Path.rectangle_xz(1, 2, -1, 1) - rect.name = 'rect' - - coil = G.Surface.disk_xz(3, 0, 0.05) - coil.name = 'coil' - - mesh_size = 0.0125 - mesh = rect.mesh(mesh_size=mesh_size) + coil.mesh(mesh_size=mesh_size) - - e = E.Excitation(mesh, E.Symmetry.RADIAL) - e.add_current(coil=2.5) - e.add_magnetizable(rect=10) - - field = S.solve_direct(e) - - z = np.linspace(-10, 10, 50) - pot_radial = [field.potential_at_point([0., 0., z_]) for z_ in z] - - rect = rect.revolve_z() - coil = G.Path.circle_xy(0., 0., 3) - coil.name = 'coil' - - mesh_size = 0.70 - mesh = rect.mesh(mesh_size=mesh_size) + coil.mesh(mesh_size=0.2) - - e = E.Excitation(mesh, E.Symmetry.THREE_D) - e.add_current(coil=2.5) - e.add_magnetizable(rect=10) - - field_three_d = S.solve_direct(e) - pot_three_d = [field_three_d.potential_at_point([0., 0., z_]) for z_ in z] - - # Accuracy is better if mesh size is increased, but - # makes tests too slow - assert np.allclose(pot_three_d, pot_radial, atol=0.020) - - def test_magnetostatic_potential_against_radial_symmetric(self): - rect = G.Path.rectangle_xz(1, 2, -1, 1) - rect.name = 'rect' - - mesh_size = 0.02 - mesh = rect.mesh(mesh_size=mesh_size) - - e = E.Excitation(mesh, E.Symmetry.RADIAL) - e.add_magnetostatic_potential(rect=10) - - field = S.solve_direct(e) - - z = np.linspace(-10, 10, 50) - pot_radial = [field.potential_at_point([0.25, 0., z_]) for z_ in z] - - rect = rect.revolve_z() - - mesh_size = 0.70 - mesh = rect.mesh(mesh_size=mesh_size) - - e = E.Excitation(mesh, E.Symmetry.THREE_D) - e.add_magnetostatic_potential(rect=10) - - field_three_d = S.solve_direct(e) - pot_three_d = [field_three_d.potential_at_point([0.25, 0., z_]) for z_ in z] - - # Accuracy is better if mesh size is increased, but - # makes tests too slow - assert np.allclose(pot_three_d, pot_radial, rtol=8e-3) - - def test_magnetostatic_line_sphere(self): - # Benchmark against FEM package - sphere = G.Surface.sphere(0.1).move(dx=0.2, dy=0.2) - sphere.name = 'sphere' - - line = G.Path.line([-0.2, -0.2, 0.], [-0.2, 0.2, 0.]) - line.name = 'line' - - ms = 0.020 - mesh = sphere.mesh(mesh_size=ms) + line.mesh(mesh_size_factor=5) - - e = E.Excitation(mesh, E.Symmetry.THREE_D) - e.add_current(line=-2.0) - e.add_magnetizable(sphere=10.) - - field = S.solve_direct(e) - - f1 = field.field_at_point([0.2, 0.2, -0.125]) - f2 = field.field_at_point([0.2, 0.2, 0.2]) - - assert np.isclose(f1[0], 0.05156, rtol=3e-2) # FEM fails to resolve y component - assert np.isclose(f1[2], 0.44525, rtol=1e-2) - assert np.isclose(f2[0], -0.10423, rtol=2e-2) - assert np.isclose(f2[2], 0.25941, rtol=1e-2) - - -class TestCurrentLoop(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.radius = 5. - cls.current = 2.5 - - path = G.Path.circle_xy(0., 0., cls.radius) - path.name = 'loop' - - mesh = path.mesh(mesh_size_factor=200) - - exc = E.Excitation(mesh, E.Symmetry.THREE_D) - exc.add_current(loop=cls.current) - - solver = S.MagnetostaticSolver(exc) - cls.field = solver.current_field - - # Now for axial - small_circle = G.Surface.disk_xz(5., 0., 0.01) - small_circle.name = 'loop' - - mesh = small_circle.mesh(mesh_size_factor=10)._to_higher_order_mesh() - exc = E.Excitation(mesh, E.Symmetry.RADIAL) - exc.add_current(loop=cls.current) - cls.axial_field = S.MagnetostaticSolver(exc).current_field - - def test_field_on_axis(self): - z = np.linspace(-20, 20, 25) - # http://hyperphysics.phy-astr.gsu.edu/hbase/magnetic/curloo.html - correct = 1/2 * self.current * self.radius**2 / (z**2 + self.radius**2)**(3/2) - approx = np.array([self.field.current_field_at_point([0., 0., z_]) for z_ in z]) - - assert np.allclose(approx[:, 0], 0.0) - assert np.allclose(approx[:, 1], 0.0) - assert np.allclose(correct, approx[:, 2], rtol=1e-4) - - def test_field_in_loop(self): - def get_exact(x_): - radius = self.radius - f = lambda t: -self.current*radius**2/(4*np.pi*x_**3) * (x_/radius * np.cos(t) - 1)*(1 + radius**2/x_**2 - 2*radius/x_*np.cos(t))**(-3/2) - return quad(f, 0, 2*np.pi)[0] - - x = np.concatenate( (np.linspace(0.25*self.radius, 0.75*self.radius, 10), np.linspace(1.25*self.radius, 2*self.radius, 10)) ) - correct = [get_exact(x_) for x_ in x] - - approx = np.array([self.field.current_field_at_point([x_, 0., 0.]) for x_ in x]) - - assert np.allclose(approx[:, 0], 0.0) - assert np.allclose(approx[:, 1], 0.0) - assert np.allclose(correct, approx[:, 2], rtol=1e-4) - - def test_field_with_radial_symmetric(self): - points = [ - [0., 0., 0.], # Bunch of arbitrary points - [1., 0., 0.], - [0., 0., 1.], - [1., 1., 1.], - [-1., 1., -1.], - [2., 2., 2.], - [2., 2., 0.], - [4., 4., 3.], - [-4., -4., -3.], - [np.sqrt(5), np.sqrt(5), 10], # Above the line element - [-np.sqrt(5), -np.sqrt(5), -10], - [100, 100, 100], # Very far away - [200, 200, -100], - ] - - for p in points: - assert np.allclose(self.field.current_field_at_point(p), self.axial_field.current_field_at_point(p), rtol=1e-4) - - -class TestCurrentLine(unittest.TestCase): - @classmethod - def setUpClass(cls): - line = G.Path.line([-50., 0., 0.], [50., 0., 0.]) - line.name = 'line' - - mesh = line.mesh(mesh_size_factor=50) - - exc = E.Excitation(mesh, E.Symmetry.THREE_D) - cls.current = 2.5 - exc.add_current(line=cls.current) - - cls.current_field = S.MagnetostaticSolver(exc).current_field - - def test_with_ampere_law(self): - def correct(point): - r = np.linalg.norm(point[1:]) - field_strength = self.current/(2*pi*r) - angle = atan2(point[2], point[1]) - return field_strength * np.array([ 0., -np.sin(angle), np.cos(angle)]) - - sample_points = [ - [0.2, 0.2, -0.2], - [0.2, 0.2, 0.2], - [0.2, -0.2, -0.2], - [0.2, -0.2, 0.2], - [-0.2, 0.2, -0.2], - [-0.2, 0.2, 0.2], - [-0.2, -0.2, -0.2], - [-0.2, -0.2, 0.2], - - [0.4, 0.6, 0.], - [0.4, 0.6, 0.2] - ] - - for p in sample_points: - assert np.allclose(correct(p), self.current_field.current_field_at_point(p), rtol=1e-4) - - - - - - -class TestFlatEinzelLens(unittest.TestCase): - - @classmethod - def setUpClass(cls): - ground_top = G.Path.line([0.25, 0.0, 0.5], [0.75, 0.0, 0.5]) - lens = G.Path.line([0.25, 0.0, 0.0], [0.75, 0.0, 0.0]) - ground_bottom = G.Path.line([0.25, 0.0, -0.5], [0.75, 0.0, -0.5]) - - ground_top.name = 'ground' - lens.name = 'lens' - ground_bottom.name = 'ground' - - # Solve radially symmetric - mesh = (ground_top + lens + ground_bottom).mesh(mesh_size_factor=40) - exc = E.Excitation(mesh, E.Symmetry.RADIAL) - exc.add_voltage(ground=0, lens=1000) - cls.field_radial = S.solve_direct(exc) - - # Solve three d - ground_top = ground_top.revolve_z() - lens = lens.revolve_z() - ground_bottom = ground_bottom.revolve_z() - - mesh = (ground_top + lens + ground_bottom).mesh(mesh_size=0.1) - exc = E.Excitation(mesh, E.Symmetry.THREE_D) - exc.add_voltage(ground=0, lens=1000) - cls.field = S.solve_direct(exc) - - cls.z = np.linspace(-0.85, 0.85, 250) - - def test_potential_close_to_axis(self): - r = 0.1 - z = np.array([-0.3, 0.0, 0.3]) - - pot = [self.field.potential_at_point([r, 0.0, z_]) for z_ in z] - pot_correct = [self.field_radial.potential_at_point([r, 0.0, z_]) for z_ in z] - assert np.allclose(pot, pot_correct, rtol=1.2e-2) - - def test_field_close_to_axis(self): - r = 0.05 - z = np.array([-0.3, 0.0, 0.3]) - - f = np.array([self.field.field_at_point([r, 0.0, z_]) for z_ in z]) - f_correct = np.array([self.field_radial.field_at_point([r, 0.0, z_]) for z_ in z]) - assert np.allclose(f, f_correct, rtol=5e-2) - - def test_trace_close_to_axis(self): - r = 0.05 - z = 0.85 - - tracer = self.field.get_tracer( [(-0.1, 0.1), (-0.1, 0.1), (-0.85, 0.85)] ) - tracer_radial = self.field_radial.get_tracer( [(-0.1, 0.1), (-0.1, 0.1), (-0.85, 0.85)] ) - - p0 = np.array([r, 0.0, z]) - v0 = T.velocity_vec(100, [0, 0, -1]) - - _, pos = tracer(p0, v0) - _, pos_radial = tracer_radial(p0, v0) - - intersection = T.xy_plane_intersection(pos, -0.8) - intersection_radial = T.xy_plane_intersection(pos_radial, -0.8) - - # We don't want to make our tests run too long, therefore the meshs size is relatively large - # therefore we can an accuracy of only 10% here. If the mesh size is decreased the correspondence - # is much better. - assert np.allclose(intersection, intersection_radial, rtol=10e-2) - - - - diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 7a959ec..20f3f7c 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -12,7 +12,7 @@ import traceon.backend as B import traceon.solver as S import traceon.tracing as T -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial from tests.test_radial_ring import biot_savart_loop from tests.test_radial import get_ring_effective_point_charges @@ -28,10 +28,10 @@ def acceleration(*_): def field(*_): acceleration_x = 3 # mm/ns - return acceleration() / EM + return acceleration() / EM, np.zeros(3) bounds = ((-2.0, 2.0), (-2.0, 2.0), (-2.0, np.sqrt(12)+1)) - times, positions = B.trace_particle(np.zeros( (3,) ), np.array([0., 0., 3.]), EM, field, bounds, 1e-10) + times, positions = B.trace_particle(np.zeros( (3,) ), np.array([0., 0., 3.]), EM, B.wrap_field_fun(field), bounds, 1e-10) correct_x = 3/2*times**2 correct_z = 3*times @@ -45,14 +45,14 @@ def acceleration(_, y): B = np.array([0, 0, 1]) return np.hstack( (v, np.cross(v, B)) ) - def traceon_acc(*y): - return acceleration(0., y)[3:] / EM - + def traceon_acc(pos, vel): + return np.zeros(3), np.array([0, 0, 1])/(mu_0*EM) + p0 = np.zeros(3) v0 = np.array([0., 1, -1.]) bounds = ((-5.0, 5.0), (-5.0, 5.0), (-40.0, 10.0)) - times, positions = B.trace_particle(p0, v0, EM, traceon_acc, bounds, 1e-10) + times, positions = B.trace_particle(p0, v0, EM, B.wrap_field_fun(traceon_acc), bounds, 1e-10) sol = solve_ivp(acceleration, (0, 30), np.hstack( (p0, v0) ), method='DOP853', rtol=1e-10, atol=1e-10) @@ -60,6 +60,44 @@ def traceon_acc(*y): assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T) + def test_tracing_helix_against_scipy_custom_field(self): + def acceleration(_, y): + v = y[3:] + B = np.array([0, 0, 1]) + return np.hstack( (v, np.cross(v, B)) ) + + class CustomField(S.Field): + def magnetostatic_field_at_point(self, point): + return np.array([0, 0, 1])/(mu_0*EM) + + def electrostatic_field_at_point(self, point): + return np.array([0, 0, 0]) + + def electrostatic_potential_at_point(self): + return 0.0 + + def is_magnetostatic(self): + return True + + def is_electrostatic(self): + return False + + p0 = np.zeros(3) + v0 = np.array([0., 1, -1.]) + + bounds = ((-5.0, 5.0), (-5.0, 5.0), (-40.0, 10.0)) + tracer = T.Tracer(CustomField(), bounds) + + # Note that we transform velocity to eV, since it's being converted back to m/s in the Tracer.__call__ function + times, positions = tracer(p0, v0*4.020347574230144e-12, atol=1e-10) + + sol = solve_ivp(acceleration, (0, 30), np.hstack( (p0, v0) ), method='DOP853', rtol=1e-10, atol=1e-10) + + interp = CubicSpline(positions[::-1, 2], np.array([positions[::-1, 0], positions[::-1, 1]]).T) + + assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T) + + def test_tracing_against_scipy_current_loop(self): # Constants current = 100 # Ampere on current loop @@ -119,64 +157,7 @@ def lorentz_force(_, y): assert np.allclose(interp(sol.y[2]), np.array([sol.y[0], sol.y[1]]).T, atol=1e-4, rtol=5e-5) - def test_magnetic_deflection_radial_three_d(self): - # Radial symmetric - circle1 = G.Surface.disk_xz(1.0, 3.0, 0.1) - circle1.name = 'circle1' - - circle2 = G.Surface.disk_xz(1.0, -3.0, 0.1) - circle2.name = 'circle2' - - mesh_radial = (circle1+circle2).mesh(mesh_size_factor=2) - - exc = E.Excitation(mesh_radial, E.Symmetry.RADIAL) - exc.add_current(circle1=2.0, circle2=2.0) - - field_radial = S.solve_direct(exc) - - # Three D - - circle1 = G.Path.circle_xy(0., 0., 1.0).move(dz=3.0) - circle1.name = 'circle1' - - circle2 = G.Path.circle_xy(0., 0., 1.0).move(dz=-3.0) - circle2.name = 'circle2' - - mesh_three_d = (circle1+circle2).mesh(mesh_size_factor=15) - - exc = E.Excitation(mesh_three_d, E.Symmetry.THREE_D) - exc.add_current(circle1=2.0, circle2=2.0) - - field_three_d = S.solve_direct(exc) - - x = np.linspace(-30, 30, 10) - H_radial = [field_radial.current_field_at_point([x_, 0., 0.])[2] for x_ in x] - H_three_d = [field_three_d.current_field_at_point([x_, 0., 0.])[2] for x_ in x] - - assert np.allclose(H_radial, H_three_d, rtol=1.5e-2) - - # Trace particles and compare - - bounds = [(-20, 2.5), (-10, 10), (-10, 10)] - tracer_radial = field_radial.get_tracer(bounds) - tracer_three_d = field_three_d.get_tracer(bounds) - - v0 = [100., 0., 0.] - x0s = [-10] - - positions_radial = [tracer_radial([x0, 0., 0.], v0, atol=1e-5)[1] for x0 in x0s][0] - positions_three_d = [tracer_three_d([x0, 0., 0.], v0, atol=1e-5)[1] for x0 in x0s][0] - - assert np.allclose(positions_radial[:, 2], 0.0) - assert np.allclose(positions_three_d[:, 2], 0.0) - - spline_radial = CubicSpline(positions_radial[:, 0], positions_radial[:, 1]) - spline_three_d = CubicSpline(positions_three_d[:, 0], positions_three_d[:, 1]) - - x = np.linspace(-1.5, 5.0, 10) - - assert np.allclose(spline_radial(x), spline_three_d(x), atol=0.0005) - + def test_plane_intersection(self): p = np.array([ [3, 0, 0, 0, 0, 0], @@ -256,12 +237,10 @@ def test_cyclotron_radius(self): v0 = np.array([0., 1., 0.]) bounds = ((-2., 2.), (0., 2.), (-2., 2.)) - def field(x, y, z, vx, vy, vz): - p, v = np.array([x,y,z]), np.array([vx, vy, vz]) - mag_field = np.array([0, 0, -1.]) - return np.cross(v, mag_field) / EM # Return acceleration + def field(pos, vel): + return np.zeros(3), np.array([0, 0, -1.])/(EM*mu_0) - times, positions = B.trace_particle(x0, v0, EM, field, bounds, 1e-10) + times, positions = B.trace_particle(x0, v0, EM, B.wrap_field_fun(field), bounds, 1e-10) # Map y-position to state interp = CubicSpline(positions[-10:, 1][::-1], positions[-10:][::-1]) diff --git a/tests/test_triangle.py b/tests/test_triangle.py deleted file mode 100644 index 7de5efb..0000000 --- a/tests/test_triangle.py +++ /dev/null @@ -1,386 +0,0 @@ -import unittest - -from scipy.integrate import dblquad -import numpy as np - -import traceon.backend as B - -def rand(*shape, min=-100, max=100): - return min + np.random.rand(*shape)*(max-min) - -def potential_exact(a, b, target, points): - v0, v1, v2 = points - p = v0 + a*(v1-v0) + b*(v2-v0) - - return 1/np.linalg.norm(p - target) - -def flux_exact(a, b, target, points, normal): - v0, v1, v2 = points - x, y, z = v0 + a*(v1-v0) + b*(v2-v0) - x0, y0, z0 = target - denominator = ((x-x0)**2 + (y-y0)**2 + (z-z0)**2)**(3/2) - dx = -(x-x0)/denominator - dy = -(y-y0)/denominator - dz = -(z-z0)/denominator - return np.dot(normal, [dx, dy, dz]) - -def potential_exact_integrated(v0, v1, v2, target): - area = np.linalg.norm(np.cross(v1-v0, v2-v0))/2 - return dblquad(potential_exact, 0, 1, 0, lambda x: 1-x, epsabs=1e-10, epsrel=1e-10, args=(target, (v0, v1, v2)))[0] * (2*area) - -def flux_exact_integrated(v0, v1, v2, target, normal, epsabs=1e-10, epsrel=1e-10): - area = np.linalg.norm(np.cross(v1-v0, v2-v0))/2 - return dblquad(flux_exact, 0, 1, 0, lambda x: 1-x, epsabs=epsabs, epsrel=epsrel, args=(target, (v0, v1, v2), normal))[0] * (2*area) - - -class TestTriangle(unittest.TestCase): - - def test_self_potential_quadrants(self): - def test(a, b, c): - v0, v1, v2 = np.array([ - [0., 0., 0.], - [a, 0., 0.], - [b, c, 0.]]) - - target = np.array([0., 0., 0.]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.self_potential_triangle_v0(v0, v1, v2) - assert np.isclose(approx, correct, atol=0., rtol=1e-15) - - test(1, 0, 1) # right triangle quadrant 1 - test(1, 1, 1) # right triangle quadrant 1 - test(-1, 0, 1) # right triangle quadrant 2 - test(-1, -1, 1) # right triangle quadrant 2 - test(-1, 0, -1) # right triangle quadrant 3 - test(-1, -1, -1) # right triangle quadrant 3 - test(1, 0, -1) # right triangle quadrant 4 - test(1, 1, -1) # right triangle quadrant 4 - - def test_self_potential_random(self): - def test(a, b, c): - v0, v1, v2 = np.array([ - [0., 0., 0.], - [a, 0., 0.], - [b, c, 0.]]) - - target = (v0+v1+v2)/3 - - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.self_potential_triangle(v0, v1, v2, target) - assert np.isclose(approx, correct, atol=0., rtol=5e-10), (a,b,c) - - for (a,b,c) in rand(3, 3): - test(a,b,c) - - def test_potential_triangle_close(self): - v0 = np.array([0.0, 0.0, 0.0]) - v1 = np.array([1.0, 0.0, 0.0]) - v2 = np.array([0.0, 1.0, 0.0]) - - range_ = [-20, -16, -4, -3, -2, -1, 0, 1, 2, 3, 4, 8, 20] - - for x in range_: - target = np.array([x, 0.0, 0.5]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for x in range_: - target = np.array([x, 0.0, -0.5]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for y in range_: - target = np.array([0.0, y, 0.5]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for y in range_: - target = np.array([0.0, y, -0.5]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for z in range_: - target = np.array([1.0, 1.0, z]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for z in range_: - target = np.array([-1.0, -1.0, z]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for z in range_: - target = np.array([-0.5, 0.5, z]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for z in range_: - target = np.array([0.5, -0.5, z]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - for k in range_: - target = np.array([k, k, 0.5]) - assert np.isclose(B.potential_triangle(v0, v1, v2, target), potential_exact_integrated(v0, v1, v2, target), atol=0.0, rtol=1e-9) - - def test_potential_triangle_edge_cases(self): - edge_cases = [ - # Note that the target lies in the same plane - # and in such a way that it's between the y-axis limits of the triangle - (-3, -0.5, 2.0, 1.0, 1.0, 0.0), - # Note that the target lies in the same plane - # and in such a way that it's between the y-axis limits of the triangle - (-3, -0.5, 2.0, 1.0, 1.0, 0.001), - - # Target point just outside the left edge of the triangle - (-1, -0.5, 4.0, 4.0, 1.5, 0.0), - # Target point just outside the right edge of the triangle - (-2, -0.5, 2.5, 1.0, 1.0, 0.0), - # Target point 'under' the triangle at the right side - (-3, -0.5, 1, 4.25, 1.0, 0.0), - # Target point 'under' the triangle at the left side - (1.0, -0.5, 1.0, -2.5, 2.0, 0.0)] - - for (x0, y0, a, b, c, z) in edge_cases: - v0, v1, v2 = np.array([ - [x0, y0, 0.], - [x0 + a, y0, 0.], - [x0 + b, y0 + c, 0.]]) - - target = np.array([0., 0., z]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(correct, approx) - - - def test_derivative_x_quadrants(self): - def test(a, b, c, z0): - x0 = 0. - v0, v1, v2 = np.array([ - [x0, 0., 0.], - [x0 + a, 0., 0.], - [x0 + b, c, 0.]]) - - target = np.array([0., 0., z0]) - normal = np.array([1., 0., 0.]) - - correct = flux_exact_integrated(v0, v1, v2, target, normal) - approx = B.flux_triangle(v0, v1, v2, target, normal) - assert np.isclose(correct, approx, atol=0., rtol=1e-12), (x0, a,b,c, z0) - - test(1, 0, 1, 0.1) # right triangle quadrant 1 - test(1, 1, 1, 0.1) # right triangle quadrant 1 - test(1, 0, -1, 0.1) # right triangle quadrant 4 - test(1, 1, -1, 0.1) # right triangle quadrant 4 - - def test_flux_x(self): - def test(x0, a, b, c, z0): - v0, v1, v2 = np.array([ - [x0, 0., 0.], - [x0 + a, 0., 0.], - [x0 + b, c, 0.]]) - - target = np.array([0., 0., z0]) - normal = np.array([1., 0., 0.]) - - correct = flux_exact_integrated(v0, v1, v2, target, [1., 0, 0], epsabs=0.0, epsrel=5e-10) - approx = B.flux_triangle(v0, v1, v2, target, normal) - assert np.isclose(correct, approx, atol=0., rtol=2e-9), (x0, a,b,c, z0) - - possible_x0 = [-10, -1, -0.1, 0.1, 1, 10] - possible_a = [0.1, 5, 10, 25] - possible_b = [-10, -1, -0.1, 0.1, 1, 10] - possible_c = [-10, -1, -0.1, 0.1, 1, 10] - possible_z0 = [-10, -1, -0.1, 0.1, 1, 10] - - for _ in range(10): - x0 = np.random.choice(possible_x0) - a = np.random.choice(possible_a) - b = np.random.choice(possible_b) - c = np.random.choice(possible_c) - z0 = np.random.choice(possible_z0) - test(x0, a, b, c, z0) - - def test_pot_quadrants(self): - z0 = 2 - - for (a, b, c) in ([2., 2., 2.], [2., -2, 2]): - v0, v1, v2 = np.array([ - [0., 0., 0.], - [a, 0., 0.], - [b, c, 0.]]) - - target = np.array([0., 0., z0]) - area = np.linalg.norm(np.cross(v1-v0, v2-v0))/2 - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(correct, approx) - - def test_flux_quadrants(self): - z0 = 2 - - for (a, b, c) in ([2., 2., 2.], [2., -2, 2]): - v0, v1, v2 = np.array([ - [0., 0., 0.], - [a, 0., 0.], - [b, c, 0.]]) - - normal = np.array([1., 1., 1.]) - target = np.array([0., 0., z0]) - - area = np.linalg.norm(np.cross(v1-v0, v2-v0))/2 - correct = flux_exact_integrated(v0, v1, v2, target, normal) - approx = B.flux_triangle(v0, v1, v2, target, normal) - assert np.isclose(correct, approx) - - def test_flux_triangle_edge_cases(self): - edge_cases = [ - # Note that the target lies in the same plane - # and in such a way that it's between the y-axis limits of the triangle - (-3, -0.5, 2.0, 1.0, 1.0, 0.0), - # Note that the target lies in the same plane - # and in such a way that it's between the y-axis limits of the triangle - (-3, -0.5, 2.0, 1.0, 1.0, 0.001), - - # Target point just outside the left edge of the triangle - (-1, -0.5, 4.0, 4.0, 1.5, 0.0), - # Target point just outside the right edge of the triangle - (-2, -0.5, 2.5, 1.0, 1.0, 0.0), - # Target point 'under' the triangle at the right side - (-3, -0.5, 1, 4.25, 1.0, 0.0), - # Target point 'under' the triangle at the left side - (1.0, -0.5, 1.0, -2.5, 2.0, 0.0)] - - normal = np.array([1., 1., 1.]) - - for (x0, y0, a, b, c, z) in edge_cases: - v0, v1, v2 = np.array([ - [x0, y0, 0.], - [x0 + a, y0, 0.], - [x0 + b, y0 + c, 0.]]) - - target = np.array([0., 0., z]) - correct = flux_exact_integrated(v0, v1, v2, target, normal) - approx = B.flux_triangle(v0, v1, v2, target, normal) - assert np.isclose(correct, approx) - - def test_pot_one_target_over_v0(self): - target = np.array([0., 0., -5]) - v0, v1, v2 = np.array([ - [0., 0, 0], - [3., 0., 0.], - [-2., 7., 0.]]) - - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - - assert np.isclose(correct, approx) - - def test_potential_singular(self): - v0, v1, v2 = np.array([ - [0., 0., 0.], - [1., 0., 0.], - [0., 1., 0.]]) - - target = np.array([2., 0., 0.]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(approx, correct) - - target = np.array([1. + 1e-5, 0., 0.]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(approx, correct) - - target = np.array([0., 1 + 1e-5, 0.]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(approx, correct) - - target = np.array([0., 0., 0.]) - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.isclose(approx, correct) - - def test_self_potential(self): - for _ in range(3): - v0, v1, v2 = rand(3,3) - target = v0 - - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.self_potential_triangle_v0(v0, v1, v2) - assert np.allclose(correct, approx) - - def test_potential(self): - for _ in range(10): - v0, v1, v2 = rand(3,3) - target = rand(3) - - correct = potential_exact_integrated(v0, v1, v2, target) - approx = B.potential_triangle(v0, v1, v2, target) - assert np.allclose(correct, approx) - - def test_flux_triangle_close(self): - v0 = np.array([0.0, 0.0, 0.0]) - v1 = np.array([1.0, 0.0, 0.0]) - v2 = np.array([0.0, 1.0, 0.0]) - - normals = [ - np.array([1.0, 0.0, 0.0]), - np.array([0.0, 1.0, 0.0]), - np.array([0.0, 0.0, 1.0]) - ] - - range_ = [-20, -16, -4, -3, -2, -1, 0, 1, 2, 3, 4, 16, 20] - - for normal in normals: - for x in range_: - target = np.array([x, 0.0, 0.5]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for x in range_: - target = np.array([x, 0.0, -0.5]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for y in range_: - target = np.array([0.0, y, 0.5]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for y in range_: - target = np.array([0.0, y, -0.5]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for z in range_: - target = np.array([1.0, 1.0, z]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for z in range_: - target = np.array([-1.0, -1.0, z]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for z in range_: - target = np.array([-0.5, 0.5, z]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for z in range_: - target = np.array([0.5, -0.5, z]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - for k in range_: - target = np.array([k, k, 0.5]) - assert np.isclose(B.flux_triangle(v0, v1, v2, target, normal), flux_exact_integrated(v0, v1, v2, target, normal), atol=0.0, rtol=1e-8) - - def test_flux(self): - for _ in range(10): - v0, v1, v2 = rand(3,3) - target = rand(3) - normal = rand(3) - - correct = flux_exact_integrated(v0, v1, v2, target, normal) - approx = B.flux_triangle(v0, v1, v2, target, normal) - assert np.allclose(correct, approx) - - def test_one_triangle_edwards(self): - v0, v1, v2 = np.array([ - [11.591110,3.105829,5.000000], - [8.626804,3.649622,5.000000], - [9.000000,0.000000,5.000000]]) - - target = np.array([9.739305,2.251817,5.000000]) - assert np.allclose(target, v0 + 1/3*(v1-v0) + 1/3*(v2-v0)) - assert np.isclose( - potential_exact_integrated(v0, v1, v2, target), - B.potential_triangle(v0, v1, v2, target)) - diff --git a/tests/test_utilities_2d.py b/tests/test_utilities_2d.py index 701dee2..2dfc671 100644 --- a/tests/test_utilities_2d.py +++ b/tests/test_utilities_2d.py @@ -31,6 +31,31 @@ def test_position_and_jacobian_radial(self): _, pos = B.position_and_jacobian_radial(1, *line) assert np.all(np.isclose(pos, line[3, :2])) + def test_delta_position_and_jacobian_radial(self): + line = np.array([ + [0.0, 0.0, 0.0], + [0.25, 0.0, 0.0], + [0.5, 0.0, 0.0], + [1.0, 0.0, 0.0]]) + + _, mid = B.position_and_jacobian_radial(0., *line) + + _, pos = B.position_and_jacobian_radial(-1.0, *line) + _, delta = B.delta_position_and_jacobian_radial(-1.0, *line) + assert np.allclose(pos, line[0, :2]) and np.allclose(pos-mid, delta) + + _, pos = B.position_and_jacobian_radial(-1.0 + 2/3, *line) + _, delta = B.delta_position_and_jacobian_radial(-1.0 + 2/3, *line) + assert np.allclose(pos, line[1, :2]) and np.allclose(pos-mid, delta) + + _, pos = B.position_and_jacobian_radial(-1.0 + 4/3, *line) + _, delta = B.delta_position_and_jacobian_radial(-1.0 + 4/3, *line) + assert np.allclose(pos, line[2, :2]) and np.allclose(pos-mid, delta) + + _, pos = B.position_and_jacobian_radial(1.0, *line) + _, delta = B.delta_position_and_jacobian_radial(1.0, *line) + assert np.allclose(pos, line[3, :2]) and np.allclose(pos-mid, delta) + def test_position_and_jacobian_radial_length(self): line = np.array([ [0.0, 0.0, 0.0], @@ -65,8 +90,8 @@ def field_dot_normal_integrand(x): jac, pos = B.position_and_jacobian_radial(x, *line) field = np.array([ - -B.dr1_potential_radial_ring(middle[0], middle[2], pos[0], pos[1]), - -B.dz1_potential_radial_ring(middle[0], middle[2], pos[0], pos[1]), + -B.dr1_potential_radial_ring(middle[0], middle[2], pos[0]-middle[0], pos[1]-middle[2]), + -B.dz1_potential_radial_ring(middle[0], middle[2], pos[0]-middle[0], pos[1]-middle[2]), 0.0]) return jac * np.dot(normal, field) diff --git a/tests/test_utilities_3d.py b/tests/test_utilities_3d.py index 5c92b3c..dc315e9 100644 --- a/tests/test_utilities_3d.py +++ b/tests/test_utilities_3d.py @@ -40,10 +40,3 @@ def test_position_and_jacobian(self): _, pos = B.position_and_jacobian_3d(alpha, beta, tri) assert np.allclose(pos, v0 + vec1*alpha + beta*vec2) - - def test_combine_elec_magnetic(self): - - for i in range(20): - vel, elec, mag, current = np.random.rand(4, 3) - result = B.combine_elec_magnetic_field(vel, elec, mag, current) - assert np.allclose(result, elec + mu_0*np.cross(vel, mag + current)) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 3befda7..7b8fa8a 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -5,6 +5,7 @@ import ctypes as C import os.path as path import platform +from typing import Callable, Optional, Tuple, List from numpy.ctypeslib import ndpointer from numpy.typing import DTypeLike @@ -78,7 +79,14 @@ def arr(ndim=None, shape=None, dtype:DTypeLike=np.float64): sz = C.c_size_t integration_cb_1d = C.CFUNCTYPE(dbl, dbl, vp) -field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), vp); + +# The low level field function has the following arguments: +# - A pointer to position (three doubles) +# - A pointer to velocity (three doubles) +# - A pointer to auxillary data the field function needs +# - A pointer to write the computed electric field (three doubles) +# - A pointer to write the computed magnetic field (three doubles) +field_fun = C.CFUNCTYPE(None, C.POINTER(dbl), C.POINTER(dbl), vp, C.POINTER(dbl), C.POINTER(dbl)); vertices = arr(ndim=3) lines = arr(ndim=3) @@ -123,7 +131,7 @@ class EffectivePointCharges3D(C.Structure): def __init__(self, eff, *args, **kwargs): assert eff.is_3d() - super(EffectivePointCharges3D, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.charges = ensure_contiguous_aligned(eff.charges).ctypes.data_as(dbl_p) self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) @@ -159,6 +167,69 @@ def __init__(self, eff, *args, **kwargs): self.N = N +class FieldEvaluationArgsRadial(C.Structure): + _fields_ = [ + ("elec_charges", C.c_void_p), + ("mag_charges", C.c_void_p), + ("current_charges", C.c_void_p), + ("bounds", C.POINTER(C.c_double)) + ] + + def __init__(self, elec, mag, current, bounds, *args, **kwargs): + super().__init__(*args, **kwargs) + assert bounds is None or bounds.shape == (3, 2) + + self.elec_charges = C.cast(C.pointer(EffectivePointCharges2D(elec)), C.c_void_p) + self.mag_charges = C.cast(C.pointer(EffectivePointCharges2D(mag)), C.c_void_p) + self.current_charges = C.cast(C.pointer(EffectivePointCharges3D(current)), C.c_void_p) + + if bounds is None: + self.bounds = None + else: + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) + +class FieldEvaluationArgs3D(C.Structure): + _fields_ = [ + ("elec_charges", C.c_void_p), + ("mag_charges", C.c_void_p), + ("current_charges", C.c_void_p), + ("bounds", C.POINTER(C.c_double)) + ] + + def __init__(self, elec, mag, currents, bounds, *args, **kwargs): + super().__init__(*args, **kwargs) + assert bounds is None or bounds.shape == (3, 2) + + self.elec_charges = C.cast(C.pointer(EffectivePointCharges3D(elec)), C.c_void_p) + self.mag_charges = C.cast(C.pointer(EffectivePointCharges3D(mag)), C.c_void_p) + self.current_charges = C.cast(C.pointer(EffectivePointCurrents3D(currents)), C.c_void_p) + + if bounds is None: + self.bounds = None + else: + self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) + + + +class FieldDerivsArgs(C.Structure): + _fields_ = [ + ("z_interpolation", dbl_p), + ("electrostatic_axial_coeffs", dbl_p), + ("magnetostatic_axial_coeffs", dbl_p), + ("N_z", C.c_size_t) + ] + + def __init__(self, z, elec, mag, *args, **kwargs): + super().__init__(*args, **kwargs) + assert z.shape == (len(z),) + assert elec.shape[0] == len(z)-1 + assert mag.shape[0] == len(z)-1 + + self.z_interpolation = ensure_contiguous_aligned(z).ctypes.data_as(dbl_p) + self.electrostatic_axial_coeffs = ensure_contiguous_aligned(elec).ctypes.data_as(dbl_p) + self.magnetostatic_axial_coeffs = ensure_contiguous_aligned(mag).ctypes.data_as(dbl_p) + self.N_z = len(z) + bounds = arr(shape=(3, 2)) times_block = arr(shape=(TRACING_BLOCK_SIZE,)) @@ -166,10 +237,6 @@ def __init__(self, eff, *args, **kwargs): backend_functions = { # triangle_contribution.c - 'potential_triangle': (dbl, v3, v3, v3, v3), - 'self_potential_triangle_v0': (dbl, v3, v3, v3), - 'self_potential_triangle': (dbl, v3, v3, v3, v3), - 'flux_triangle': (dbl, v3, v3, v3, v3, v3), 'kronrod_adaptive': (dbl, integration_cb_1d, dbl, dbl, vp, dbl, dbl), 'ellipkm1' : (dbl, dbl), @@ -181,6 +248,7 @@ def __init__(self, eff, *args, **kwargs): 'normal_3d': (None, arr(shape=(3,3)), v3), 'position_and_jacobian_3d': (None, dbl, dbl, arr(ndim=2), v3, dbl_p), 'position_and_jacobian_radial': (None, dbl, v2, v2, v2, v2, v2, dbl_p), + 'delta_position_and_jacobian_radial': (None, dbl, v2, v2, v2, v2, v2, dbl_p), 'trace_particle': (sz, times_block, tracing_block, dbl, field_fun, bounds, dbl, vp), 'potential_radial_ring': (dbl, dbl, dbl, dbl, dbl, vp), 'dr1_potential_radial_ring': (dbl, dbl, dbl, dbl, dbl, vp), @@ -191,10 +259,7 @@ def __init__(self, eff, *args, **kwargs): 'flux_density_to_charge_factor': (dbl, dbl), 'charge_radial': (dbl, arr(ndim=2), dbl), 'field_radial': (None, v3, v3, charges_2d, jac_buffer_2d, pos_buffer_2d, sz), - 'combine_elec_magnetic_field': (None, v3, v3, v3, v3, v3), - 'trace_particle_radial': (sz, times_block, tracing_block, dbl, bounds, dbl, dbl_p, EffectivePointCharges2D, EffectivePointCharges2D, EffectivePointCharges3D), 'field_radial_derivs': (None, v3, v3, z_values, arr(ndim=3), sz), - 'trace_particle_radial_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, radial_coeffs, radial_coeffs, sz), 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dy1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dz1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), @@ -205,9 +270,7 @@ def __init__(self, eff, *args, **kwargs): 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), - 'trace_particle_3d': (sz, times_block, tracing_block, dbl, bounds, dbl, EffectivePointCharges3D, EffectivePointCharges3D, EffectivePointCurrents3D, dbl_p), 'field_3d_derivs': (None, v3, v3, z_values, arr(ndim=5), sz), - 'trace_particle_3d_derivs': (sz, times_block, tracing_block, dbl, bounds, dbl, z_values, arr(ndim=5), arr(ndim=5), sz), 'current_potential_axial_radial_ring': (dbl, dbl, dbl, dbl), 'current_potential_axial': (dbl, dbl, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), 'current_field_radial_ring': (None, dbl, dbl, dbl, dbl, v2), @@ -218,7 +281,6 @@ def __init__(self, eff, *args, **kwargs): 'self_field_dot_normal_radial': (dbl, dbl, vp), 'fill_matrix_radial': (None, arr(ndim=2), lines, arr(dtype=np.uint8, ndim=1), arr(ndim=1), jac_buffer_2d, pos_buffer_2d, sz, sz, C.c_int, C.c_int), 'fill_jacobian_buffer_3d': (None, jac_buffer_3d, pos_buffer_3d, vertices, sz), - 'fill_matrix_3d': (None, arr(ndim=2), vertices, arr(dtype=np.uint8, ndim=1), arr(ndim=1), jac_buffer_3d, pos_buffer_3d, sz, sz, C.c_int, C.c_int), 'plane_intersection': (bool, v3, v3, arr(ndim=2), sz, arr(shape=(6,))), 'triangle_areas': (None, vertices, arr(ndim=1), sz) } @@ -247,50 +309,42 @@ def backend_check_numpy_requirements_wrapper(*args, _cfun_reference=libfun, _cfu libfun.restype = res libfun.argtypes = args -self_potential_triangle_v0 = backend_lib.self_potential_triangle_v0 -self_potential_triangle = backend_lib.self_potential_triangle -potential_triangle = backend_lib.potential_triangle -flux_triangle = backend_lib.flux_triangle ellipkm1 = np.vectorize(backend_lib.ellipkm1) ellipk = np.vectorize(backend_lib.ellipk) ellipem1 = np.vectorize(backend_lib.ellipem1) ellipe = np.vectorize(backend_lib.ellipe) -def kronrod_adaptive(fun, a, b, epsabs=1.49e-08, epsrel=1.49e-08): +def kronrod_adaptive(fun: Callable[[float], float], a: float, b: float, + epsabs: float=1.49e-08, epsrel: float=1.49e-08) -> float: callback = integration_cb_1d(lambda x, _: fun(x)) return backend_lib.kronrod_adaptive(callback, a, b, vp(None), epsabs, epsrel) -def higher_order_normal_radial(alpha, vertices): +def higher_order_normal_radial(alpha: float, vertices: np.ndarray) -> np.ndarray: normal = np.zeros(2) backend_lib.higher_order_normal_radial(alpha, vertices[0, :2], vertices[2, :2], vertices[3, :2], vertices[1, :2], normal) assert np.isclose(np.linalg.norm(normal), 1.0) return normal -def normal_2d(p1, p2): +def normal_2d(p1: np.ndarray, p2: np.ndarray) -> np.ndarray: normal = np.zeros( (2,) ) backend_lib.normal_2d(p1, p2, normal) return normal -# Remove the last argument, which is usually a void pointer to optional data -# passed to the function. In Python we don't need this functionality -# as we can simply use closures. -def remove_arg(fun): - return lambda *args: fun(*args[:-1]) - def normal_3d(tri: np.ndarray) -> np.ndarray: normal = np.zeros( (3,) ) backend_lib.normal_3d(tri, normal) return normal - -def trace_particle_wrapper(position_, velocity_, fill_positions_fun): + +def trace_particle_wrapper(position_: np.ndarray, velocity_: np.ndarray, + fill_positions_fun: Callable[[np.ndarray, np.ndarray], int]) -> Tuple[np.ndarray, np.ndarray]: position = np.array(position_) velocity = np.array(velocity_) assert position.shape == (3,) and velocity.shape == (3,) N = TRACING_BLOCK_SIZE - pos_blocks: list[np.ndarray] = [] - times_blocks: list[np.ndarray] = [] + pos_blocks: List[np.ndarray] = [] + times_blocks: List[np.ndarray] = [] times = np.zeros(TRACING_BLOCK_SIZE) positions = np.zeros( (TRACING_BLOCK_SIZE, 6) ) @@ -321,18 +375,9 @@ def trace_particle_wrapper(position_, velocity_, fill_positions_fun): else: return np.concatenate(times_blocks), np.concatenate(pos_blocks) -def wrap_field_fun(ff): - def wrapper(y, result, _): - field = ff(y[0], y[1], y[2], y[3], y[4], y[5]) - assert field.shape == (3,) - result[0] = field[0] - result[1] = field[1] - result[2] = field[2] - - return field_fun(wrapper) -def position_and_jacobian_3d(alpha, beta, triangle): +def position_and_jacobian_3d(alpha: float, beta: float, triangle: np.ndarray) -> Tuple[float, np.ndarray]: assert triangle.shape == (3, 3) pos = np.zeros(3) @@ -343,7 +388,7 @@ def position_and_jacobian_3d(alpha, beta, triangle): return jac.value, pos -def position_and_jacobian_radial(alpha, v1, v2, v3, v4): +def position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndarray, v3: np.ndarray, v4: np.ndarray) -> Tuple[float, np.ndarray]: assert v1.shape == (2,) or v1.shape == (3,) assert v2.shape == (2,) or v2.shape == (3,) assert v3.shape == (2,) or v3.shape == (3,) @@ -358,29 +403,45 @@ def position_and_jacobian_radial(alpha, v1, v2, v3, v4): return jac.value, pos - -def trace_particle(position, velocity, charge_over_mass, field, bounds, atol): - bounds = np.array(bounds) +def delta_position_and_jacobian_radial(alpha: float, v1: np.ndarray, v2: np.ndarray, v3: np.ndarray, v4: np.ndarray) -> Tuple[float, np.ndarray]: + assert v1.shape == (2,) or v1.shape == (3,) + assert v2.shape == (2,) or v2.shape == (3,) + assert v3.shape == (2,) or v3.shape == (3,) + assert v4.shape == (2,) or v4.shape == (3,) - return trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle(T, P, charge_over_mass, wrap_field_fun(field), bounds, atol, None)) - -def trace_particle_radial(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_current, field_bounds=None): + assert all([v.shape == (2,) or v[1] == 0. for v in [v1,v2,v3,v4]]) - eff_elec = EffectivePointCharges2D(eff_elec) - eff_mag = EffectivePointCharges2D(eff_mag) - eff_current = EffectivePointCharges3D(eff_current) + pos = np.zeros(2) + jac = C.c_double(0.0) + + backend_lib.delta_position_and_jacobian_radial(alpha, v1[:2], v2[:2], v3[:2], v4[:2], pos, C.pointer(jac)) + + return jac.value, pos + +def wrap_field_fun(ff: Callable) -> Callable: + def field_fun_wrapper(pos, vel, _, elec_out, mag_out): + elec, mag = ff(np.array([pos[0], pos[1], pos[2]]), np.array([vel[0], vel[1], vel[2]])) + assert elec.shape == (3,) and mag.shape == (3,) + + elec_out[0] = elec[0] + elec_out[1] = elec[1] + elec_out[2] = elec[2] + + mag_out[0] = mag[0] + mag_out[1] = mag[1] + mag_out[2] = mag[2] + return field_fun(field_fun_wrapper) + +def trace_particle(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, field, bounds: np.ndarray, atol: float, args =None) -> Tuple[np.ndarray, np.ndarray]: bounds = np.array(bounds) - field_bounds = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None - - times, positions = trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_radial(T, P, charge_over_mass, bounds, atol, field_bounds, eff_elec, eff_mag, eff_current)) - - return times, positions + return trace_particle_wrapper(position, velocity, + lambda T, P: backend_lib.trace_particle(T, P, charge_over_mass, field, bounds, atol, args)) -def trace_particle_radial_derivs(position, velocity, charge_over_mass, bounds, atol, z, elec_coeffs, mag_coeffs): +def trace_particle_radial_derivs(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, z: np.ndarray, + elec_coeffs: np.ndarray, mag_coeffs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert elec_coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) assert mag_coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) @@ -394,21 +455,9 @@ def trace_particle_radial_derivs(position, velocity, charge_over_mass, bounds, a return times, positions -def trace_particle_3d(position, velocity, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_currents, field_bounds=None): - assert field_bounds is None or field_bounds.shape == (3,2) - - bounds = np.array(bounds) - - field_bounds = field_bounds.ctypes.data_as(dbl_p) if field_bounds is not None else None - - eff_elec = EffectivePointCharges3D(eff_elec) - eff_mag = EffectivePointCharges3D(eff_mag) - eff_currents = EffectivePointCurrents3D(eff_currents) - - return trace_particle_wrapper(position, velocity, - lambda T, P: backend_lib.trace_particle_3d(T, P, charge_over_mass, bounds, atol, eff_elec, eff_mag, eff_currents, field_bounds)) - -def trace_particle_3d_derivs(position, velocity, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs): +def trace_particle_3d_derivs(position: np.ndarray, velocity: np.ndarray, charge_over_mass: float, + bounds: np.ndarray, atol: float, z: np.ndarray, + electrostatic_coeffs: np.ndarray, magnetostatic_coeffs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert electrostatic_coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert magnetostatic_coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) @@ -417,11 +466,16 @@ def trace_particle_3d_derivs(position, velocity, charge_over_mass, bounds, atol, return trace_particle_wrapper(position, velocity, lambda T, P: backend_lib.trace_particle_3d_derivs(T, P, charge_over_mass, bounds, atol, z, electrostatic_coeffs, magnetostatic_coeffs, len(z))) -potential_radial_ring = lambda *args: backend_lib.potential_radial_ring(*args, None) -dr1_potential_radial_ring = lambda *args: backend_lib.dr1_potential_radial_ring(*args, None) -dz1_potential_radial_ring = lambda *args: backend_lib.dz1_potential_radial_ring(*args, None) +def potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.potential_radial_ring(r0, z0, delta_r, delta_z, None) -def axial_derivatives_radial(z, charges, jac_buffer, pos_buffer): +def dr1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.dr1_potential_radial_ring(r0, z0, delta_r, delta_z, None) + +def dz1_potential_radial_ring(r0: float, z0: float, delta_r: float, delta_z: float) -> float: + return backend_lib.dz1_potential_radial_ring(r0, z0, delta_r, delta_z, None) + +def axial_derivatives_radial(z: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: derivs = np.zeros( (z.size, DERIV_2D_MAX) ) assert jac_buffer.shape == (len(charges), N_QUAD_2D) @@ -431,21 +485,21 @@ def axial_derivatives_radial(z, charges, jac_buffer, pos_buffer): backend_lib.axial_derivatives_radial(derivs,charges, jac_buffer, pos_buffer, len(charges), z, len(z)) return derivs -def potential_radial(point, charges, jac_buffer, pos_buffer): +def potential_radial(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: assert point.shape == (3,) assert jac_buffer.shape == (len(charges), N_QUAD_2D) assert pos_buffer.shape == (len(charges), N_QUAD_2D, 2) return backend_lib.potential_radial(point.astype(np.float64), charges, jac_buffer, pos_buffer, len(charges)) -def potential_radial_derivs(point, z, coeffs): +def potential_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) return backend_lib.potential_radial_derivs(point.astype(np.float64), z, coeffs, len(z)) -def charge_radial(vertices, charge): +def charge_radial(vertices: np.ndarray, charge: float) -> float: assert vertices.shape == (len(vertices), 3) return backend_lib.charge_radial(vertices, charge) -def field_radial(point, charges, jac_buffer, pos_buffer): +def field_radial(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert jac_buffer.shape == (len(charges), N_QUAD_2D) assert pos_buffer.shape == (len(charges), N_QUAD_2D, 2) @@ -455,25 +509,29 @@ def field_radial(point, charges, jac_buffer, pos_buffer): backend_lib.field_radial(point.astype(np.float64), field, charges, jac_buffer, pos_buffer, len(charges)) return field -def combine_elec_magnetic_field(vel, elec, mag, current): - result = np.zeros( (3,) ) - backend_lib.combine_elec_magnetic_field(vel, elec, mag, current, result) - return result - -def field_radial_derivs(point, z, coeffs): +def field_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, DERIV_2D_MAX, 6) field = np.zeros( (3,) ) backend_lib.field_radial_derivs(point.astype(np.float64), field, z, coeffs, len(z)) return field -dx1_potential_3d_point = remove_arg(backend_lib.dx1_potential_3d_point) -dy1_potential_3d_point = remove_arg(backend_lib.dy1_potential_3d_point) -dz1_potential_3d_point = remove_arg(backend_lib.dz1_potential_3d_point) -potential_3d_point = remove_arg(backend_lib.potential_3d_point) -flux_density_to_charge_factor = backend_lib.flux_density_to_charge_factor +def dx1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dx1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def dy1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dy1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def dz1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.dz1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) + +def potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: + return backend_lib.potential_3d_point(x0, y0, z0, x1, y1, z1, None) -def axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, z): +def flux_density_to_charge_factor(K: float) -> float: + return backend_lib.flux_density_to_charge_factor(K) + +def axial_coefficients_3d(charges: np.ndarray, jacobian_buffer: np.ndarray, pos_buffer: np.ndarray, z: np.ndarray) -> np.ndarray: assert jacobian_buffer.shape == (len(charges), N_TRIANGLE_QUAD) assert pos_buffer.shape == (len(charges), N_TRIANGLE_QUAD, 3) @@ -482,9 +540,8 @@ def axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, z): trig_cos_buffer = np.zeros( (len(charges), N_TRIANGLE_QUAD, M_MAX) ) trig_sin_buffer = np.zeros( (len(charges), N_TRIANGLE_QUAD, M_MAX) ) - backend_lib.axial_coefficients_3d(charges, - jacobian_buffer, pos_buffer, trig_cos_buffer, trig_sin_buffer, - len(charges), z, output_coeffs, len(z)) + backend_lib.axial_coefficients_3d(charges, jacobian_buffer, pos_buffer, trig_cos_buffer, trig_sin_buffer, + len(charges), z, output_coeffs, len(z)) return output_coeffs @@ -508,7 +565,7 @@ def current_field_3d(point, eff): backend_lib.current_field_3d(point, eff, result) return result -def potential_3d(point, charges, jac_buffer, pos_buffer): +def potential_3d(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: assert point.shape == (3,) N = len(charges) assert charges.shape == (N,) @@ -517,13 +574,13 @@ def potential_3d(point, charges, jac_buffer, pos_buffer): return backend_lib.potential_3d(point.astype(np.float64), charges, jac_buffer, pos_buffer, N) -def potential_3d_derivs(point, z, coeffs): +def potential_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert point.shape == (3,) return backend_lib.potential_3d_derivs(point.astype(np.float64), z, coeffs, len(z)) -def field_3d(point, charges, jacobian_buffer, position_buffer): +def field_3d(point: np.ndarray, charges: np.ndarray, jacobian_buffer: np.ndarray, position_buffer: np.ndarray) -> np.ndarray: N = len(charges) assert point.shape == (3,) assert jacobian_buffer.shape == (N, N_TRIANGLE_QUAD) @@ -533,16 +590,18 @@ def field_3d(point, charges, jacobian_buffer, position_buffer): backend_lib.field_3d(point.astype(np.float64), field, charges, jacobian_buffer, position_buffer, N) return field -def field_3d_derivs(point, z, coeffs): +def field_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) field = np.zeros( (3,) ) backend_lib.field_3d_derivs(point.astype(np.float64), field, z, coeffs, len(z)) + return field -current_potential_axial_radial_ring = backend_lib.current_potential_axial_radial_ring +def current_potential_axial_radial_ring(z0: float, r: float, z: float) -> float: + return backend_lib.current_potential_axial_radial_ring(z0, r, z) -def current_potential_axial(z, currents, jac_buffer, pos_buffer): +def current_potential_axial(z: float, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: N = len(currents) assert currents.shape == (N,) assert jac_buffer.shape == (N, N_TRIANGLE_QUAD) @@ -552,12 +611,12 @@ def current_potential_axial(z, currents, jac_buffer, pos_buffer): return backend_lib.current_potential_axial(z, currents, jac_buffer, pos_buffer, N) -def current_field_radial_ring(x0, y0, x, y): - res = np.zeros( (2,) ) +def current_field_radial_ring(x0: float, y0: float, x: float, y: float) -> np.ndarray: + res = np.zeros((2,)) backend_lib.current_field_radial_ring(x0, y0, x, y, res) return res -def current_field_radial(p0, currents, jac_buffer, pos_buffer): +def current_field_radial(p0: np.ndarray, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: assert p0.shape == (3,) N = len(currents) assert currents.shape == (N,) @@ -570,7 +629,7 @@ def current_field_radial(p0, currents, jac_buffer, pos_buffer): backend_lib.current_field_radial(p0, result, currents, jac_buffer, pos_buffer, N) return result -def current_axial_derivatives_radial(z, currents, jac_buffer, pos_buffer): +def current_axial_derivatives_radial(z: np.ndarray, currents: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> np.ndarray: N_z = len(z) N_vertices = len(currents) @@ -584,7 +643,7 @@ def current_axial_derivatives_radial(z, currents, jac_buffer, pos_buffer): return derivs -def fill_jacobian_buffer_radial(vertices): +def fill_jacobian_buffer_radial(vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: assert vertices.shape == (len(vertices), 4, 3) assert np.all(vertices[:, :, 1] == 0.) @@ -596,16 +655,16 @@ def fill_jacobian_buffer_radial(vertices): return jac_buffer, pos_buffer -def self_potential_radial(vertices): +def self_potential_radial(vertices: np.ndarray) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) low_level = LowLevelCallable(backend_lib.self_potential_radial, user_data=user_data) - return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] + return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] # type: ignore class SelfFieldDotNormalRadialArgs(C.Structure): _fields_ = [("line_points", C.POINTER(C.c_double)), ("K", C.c_double)] -def self_field_dot_normal_radial(vertices, K): +def self_field_dot_normal_radial(vertices: np.ndarray, K: float) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) @@ -620,7 +679,13 @@ def self_field_dot_normal_radial(vertices, K): -def fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_buffer, pos_buffer, start_index, end_index): +def fill_matrix_radial(matrix: np.ndarray, + lines: np.ndarray, + excitation_types: np.ndarray, + excitation_values: np.ndarray, + jac_buffer: np.ndarray, + pos_buffer: np.ndarray, + start_index: int, end_index: int): N = len(lines) assert np.all(lines[:, :, 1] == 0.0) assert matrix.shape[0] == N and matrix.shape[1] == N and matrix.shape[0] == matrix.shape[1] @@ -633,7 +698,7 @@ def fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_b backend_lib.fill_matrix_radial(matrix, lines, excitation_types, excitation_values, jac_buffer, pos_buffer, N, matrix.shape[0], start_index, end_index) -def fill_jacobian_buffer_3d(vertices): +def fill_jacobian_buffer_3d(vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: N = len(vertices) assert vertices.shape == (N, 3, 3) jac_buffer = np.zeros( (N, N_TRIANGLE_QUAD) ) @@ -644,8 +709,13 @@ def fill_jacobian_buffer_3d(vertices): return jac_buffer, pos_buffer - -def fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_buffer, pos_buffer, start_index, end_index): +def fill_matrix_3d(matrix: np.ndarray, + vertices: np.ndarray, + excitation_types: np.ndarray, + excitation_values: np.ndarray, + jac_buffer: np.ndarray, + pos_buffer: np.ndarray, + start_index: int, end_index: int): N = len(vertices) assert matrix.shape[0] == N and matrix.shape[1] == N and matrix.shape[0] == matrix.shape[1] assert vertices.shape == (N, 3, 3) @@ -657,7 +727,7 @@ def fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_bu backend_lib.fill_matrix_3d(matrix, vertices, excitation_types, excitation_values, jac_buffer, pos_buffer, N, matrix.shape[0], start_index, end_index) -def plane_intersection(positions, p0, normal): +def plane_intersection(positions: np.ndarray, p0: np.ndarray, normal: np.ndarray) -> np.ndarray: assert p0.shape == (3,) assert normal.shape == (3,) assert positions.shape == (len(positions), 6) @@ -670,7 +740,7 @@ def plane_intersection(positions, p0, normal): return result -def triangle_areas(triangles): +def triangle_areas(triangles: np.ndarray) -> np.ndarray: assert triangles.shape == (len(triangles), 3, 3) out = np.zeros(len(triangles)) backend_lib.triangle_areas(triangles, out, len(triangles)) diff --git a/traceon/backend/radial.c b/traceon/backend/radial.c index e7bc190..999cece 100644 --- a/traceon/backend/radial.c +++ b/traceon/backend/radial.c @@ -112,7 +112,7 @@ potential_radial(double point[3], double* charges, jacobian_buffer_2d jacobian_b for(int i = 0; i < N_vertices; i++) { for(int k = 0; k < N_QUAD_2D; k++) { double *pos = &position_buffer[i][k][0]; - double potential = potential_radial_ring(r0, z0, pos[0], pos[1], NULL); + double potential = potential_radial_ring(r0, z0, pos[0]-r0, pos[1]-z0, NULL); sum_ += charges[i] * jacobian_buffer[i][k] * potential; } } @@ -156,8 +156,8 @@ field_radial(double point[3], double result[3], double* charges, jacobian_buffer for(int i = 0; i < N_vertices; i++) { for(int k = 0; k < N_QUAD_2D; k++) { double *pos = &position_buffer[i][k][0]; - Er -= charges[i] * jacobian_buffer[i][k] * dr1_potential_radial_ring(r, point[2], pos[0], pos[1], NULL); - Ez -= charges[i] * jacobian_buffer[i][k] * dz1_potential_radial_ring(r, point[2], pos[0], pos[1], NULL); + Er -= charges[i] * jacobian_buffer[i][k] * dr1_potential_radial_ring(r, point[2], pos[0]-r, pos[1]-point[2], NULL); + Ez -= charges[i] * jacobian_buffer[i][k] * dz1_potential_radial_ring(r, point[2], pos[0]-r, pos[1]-point[2], NULL); } } @@ -186,13 +186,13 @@ EXPORT double self_potential_radial(double alpha, double line_points[4][3]) { double *v3 = line_points[3]; double *v4 = line_points[1]; - double pos[2], jac; - position_and_jacobian_radial(alpha, v1, v2, v3, v4, pos, &jac); + double delta_pos[2], jac; + delta_position_and_jacobian_radial(alpha, v1, v2, v3, v4, delta_pos, &jac); double target[2], jac2; position_and_jacobian_radial(0, v1, v2, v3, v4, target, &jac2); - return jac*potential_radial_ring(target[0], target[1], pos[0], pos[1], NULL); + return jac*potential_radial_ring(target[0], target[1], delta_pos[0], delta_pos[1], NULL); } struct self_field_dot_normal_radial_args { @@ -207,9 +207,9 @@ EXPORT double self_field_dot_normal_radial(double alpha, struct self_field_dot_n double *v3 = args->line_points[3]; double *v4 = args->line_points[1]; - double pos[2], jac; - position_and_jacobian_radial(alpha, v1, v2, v3, v4, pos, &jac); - + double delta_pos[2], jac; + delta_position_and_jacobian_radial(alpha, v1, v2, v3, v4, delta_pos, &jac); + double target[2], jac2; position_and_jacobian_radial(0, v1, v2, v3, v4, target, &jac2); @@ -217,8 +217,8 @@ EXPORT double self_field_dot_normal_radial(double alpha, struct self_field_dot_n higher_order_normal_radial(0.0, v1, v2, v3, v4, normal); struct {double *normal; double K;} cb_args = {normal, args->K}; - - return jac*field_dot_normal_radial(target[0], target[1], pos[0], pos[1], (void*) &cb_args); + + return jac*field_dot_normal_radial(target[0], target[1], delta_pos[0], delta_pos[1], (void*) &cb_args); } EXPORT void fill_jacobian_buffer_radial( @@ -280,7 +280,7 @@ EXPORT void fill_matrix_radial(double *matrix, double *pos = pos_buffer[j][k]; double jac = jacobian_buffer[j][k]; - matrix[i*N_matrix + j] += jac * potential_radial_ring(target[0], target[1], pos[0], pos[1], NULL); + matrix[i*N_matrix + j] += jac * potential_radial_ring(target[0], target[1], pos[0]-target[0], pos[1]-target[1], NULL); } } } @@ -298,7 +298,7 @@ EXPORT void fill_matrix_radial(double *matrix, double *pos = pos_buffer[j][k]; double jac = jacobian_buffer[j][k]; - matrix[i*N_matrix + j] += jac * field_dot_normal_radial(target[0], target[1], pos[0], pos[1], &args); + matrix[i*N_matrix + j] += jac * field_dot_normal_radial(target[0], target[1], pos[0]-target[0], pos[1]-target[1], &args); } } } diff --git a/traceon/backend/radial_ring.c b/traceon/backend/radial_ring.c index 8d1b6ef..cbccd81 100644 --- a/traceon/backend/radial_ring.c +++ b/traceon/backend/radial_ring.c @@ -18,14 +18,15 @@ INLINE double flux_density_to_charge_factor(double K) { } -EXPORT double dr1_potential_radial_ring(double r0, double z0, double r, double z, void *_) { +EXPORT double dr1_potential_radial_ring(double r0, double z0, double delta_r, double delta_z, void *_) { if(r0 < MIN_DISTANCE_AXIS) { return 0.0; } - double delta_r = r - r0; - double delta_z = z - z0; + double r = r0 + delta_r; + double z = z0 + delta_z; + double common_arg = (delta_z * delta_z + delta_r * delta_r) / (4 * r * r - 4 * delta_r * r + delta_z * delta_z + delta_r * delta_r); double denominator = ((-2 * delta_r * delta_r * r) + delta_z * delta_z * (2 * delta_r - 2 * r) + 2 * delta_r * delta_r * delta_r) * sqrt(4 * r * r - 4 * delta_r * r + delta_z * delta_z + delta_r * delta_r); double ellipkm1_term = (delta_z * delta_z * r + delta_r * delta_r * r) * ellipkm1(common_arg); @@ -33,16 +34,18 @@ EXPORT double dr1_potential_radial_ring(double r0, double z0, double r, double z return 1./M_PI * (ellipkm1_term + ellipem1_term) / denominator; } -EXPORT double potential_radial_ring(double r0, double z0, double r, double z, void *_) { - double delta_z = z - z0; - double delta_r = r - r0; +EXPORT double potential_radial_ring(double r0, double z0, double delta_r, double delta_z, void *_) { + double r = r0 + delta_r; + double z = z0 + delta_z; + double t = (pow(delta_z, 2) + pow(delta_r, 2)) / (pow(delta_z, 2) + pow(delta_r, 2) + 4 * r0 * delta_r + 4 * pow(r0, 2)); return 1./M_PI * ellipkm1(t) * (delta_r + r0) / sqrt(pow(delta_z, 2) + pow((delta_r + 2 * r0), 2)); } -EXPORT double dz1_potential_radial_ring(double r0, double z0, double r, double z, void *_) { - double delta_z = z - z0; - double delta_r = r - r0; +EXPORT double dz1_potential_radial_ring(double r0, double z0, double delta_r, double delta_z, void *_) { + double r = r0 + delta_r; + double z = z0 + delta_z; + double common_arg = (delta_z * delta_z + delta_r * delta_r) / (4 * r0 * r0 + 4 * delta_r * r0 + delta_z * delta_z + delta_r * delta_r); double denominator = (delta_z * delta_z + delta_r * delta_r) * sqrt(4 * r0 * r0 + 4 * delta_r * r0 + delta_z * delta_z + delta_r * delta_r); double ellipem1_term = -delta_z * (r0 + delta_r) * ellipem1(common_arg); @@ -50,7 +53,7 @@ EXPORT double dz1_potential_radial_ring(double r0, double z0, double r, double z } double -field_dot_normal_radial(double r0, double z0, double r, double z, void* args_p) { +field_dot_normal_radial(double r0, double z0, double delta_r, double delta_z, void* args_p) { struct {double *normal; double K;} *args = args_p; @@ -60,8 +63,8 @@ field_dot_normal_radial(double r0, double z0, double r, double z, void* args_p) double K = args->K; double factor = flux_density_to_charge_factor(K); - double Er = -dr1_potential_radial_ring(r0, z0, r, z, NULL); - double Ez = -dz1_potential_radial_ring(r0, z0, r, z, NULL); + double Er = -dr1_potential_radial_ring(r0, z0, delta_r, delta_z, NULL); + double Ez = -dz1_potential_radial_ring(r0, z0, delta_r, delta_z, NULL); return factor*(args->normal[0]*Er + args->normal[1]*Ez); diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 656be7f..e4331df 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -15,7 +15,7 @@ const double B2[] = {2./9.}; const double CH[] = {47./450., 0., 12./25., 32./225., 1./30., 6./25.}; const double CT[] = {-1./150., 0., 3./100., -16./75., -1./20., 6./25.}; -typedef void (*field_fun)(double pos[6], double field[3], void* args); +typedef void (*field_fun)(double position[3], double velocity[3], void* args, double elec_out[3], double mag_out[3]); void produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { @@ -33,9 +33,21 @@ produce_new_y(double y[6], double ys[6][6], double ks[6][6], size_t index) { void produce_new_k(double ys[6][6], double ks[6][6], size_t index, double h, double charge_over_mass, field_fun ff, void *args) { - double field[3] = { 0. }; - ff(ys[index], field, args); + double elec[3] = { 0. }; + double mag[3] = { 0. }; + ff(&ys[index][0], &ys[index][3], args, elec, mag); + + // Convert to acceleration using Lorentz force law + double cross[3] = { 0. }; + cross_product_3d(&ys[index][3], mag, cross); // Compute v x H + + double field[3] = { // Compute E + v x B + elec[0] + MU_0*cross[0], + elec[1] + MU_0*cross[1], + elec[2] + MU_0*cross[2] + }; + ks[index][0] = h*ys[index][3]; ks[index][1] = h*ys[index][4]; ks[index][2] = h*ys[index][5]; @@ -110,8 +122,8 @@ trace_particle(double *times_array, double *pos_array, double charge_over_mass, return N; } -void -field_radial_traceable(double point[6], double result[3], void *args_p) { +EXPORT void +field_radial_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_evaluation_args *args = (struct field_evaluation_args*) args_p; @@ -121,74 +133,48 @@ field_radial_traceable(double point[6], double result[3], void *args_p) { double (*bounds)[2] = (double (*)[2]) args->bounds; - if(args->bounds == NULL || ((bounds[0][0] < point[0]) && (point[0] < bounds[0][1]) - && (bounds[1][0] < point[1]) && (point[1] < bounds[1][1]))) { + if(args->bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) + && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]))) { - double elec_field[3] = {0.}; - double mag_field[3] = {0.}; - double curr_field[3] = {0.}; - field_radial(point, elec_field, + field_radial(position, elec_out, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_radial(point, mag_field, + field_radial(position, mag_out, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - current_field_radial(point, curr_field, + double curr_field[3] = {0.}; + + current_field_radial(position, curr_field, current_charges->charges, current_charges->jacobians, current_charges->positions, current_charges->N); - - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); + + mag_out[0] += curr_field[0]; + mag_out[1] += curr_field[1]; + mag_out[2] += curr_field[2]; } else { - result[0] = 0.; - result[1] = 0.; - result[2] = 0.; + elec_out[0] = 0.; + elec_out[1] = 0.; + elec_out[2] = 0.; + + mag_out[0] = 0.; + mag_out[1] = 0.; + mag_out[2] = 0.; } } - -EXPORT size_t -trace_particle_radial(double *times_array, double *pos_array, double charge_over_mass, double tracer_bounds[3][2], double atol, double *field_bounds, - struct effective_point_charges_2d eff_elec, - struct effective_point_charges_2d eff_mag, - struct effective_point_charges_3d eff_current) { - - struct field_evaluation_args args = { - .elec_charges = (void*) &eff_elec, - .mag_charges = (void*) &eff_mag, - .currents = (void*) &eff_current, - .bounds = field_bounds - }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_radial_traceable, tracer_bounds, atol, (void*) &args); -} - -void -field_radial_derivs_traceable(double point[6], double field[3], void *args_p) { +EXPORT void +field_radial_derivs_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; - - double elec_field[3]; - field_radial_derivs(point, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); - double mag_field[3]; - field_radial_derivs(point, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); - - double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); + field_radial_derivs(position, elec_out, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_radial_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } -EXPORT size_t -trace_particle_radial_derivs(double *times_array, double *pos_array, double charge_over_mass, double bounds[3][2], double atol, - double *z_interpolation, double *electrostatic_coeffs, double *magnetostatic_coeffs, size_t N_z) { - - struct field_derivs_args args = { z_interpolation, electrostatic_coeffs, magnetostatic_coeffs, N_z }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_radial_derivs_traceable, bounds, atol, (void*) &args); -} +EXPORT void +field_3d_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { -void -field_3d_traceable(double point[6], double result[3], void *args_p) { struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; struct effective_point_charges_3d *mag_charges = (struct effective_point_charges_3d*) args->mag_charges; @@ -196,72 +182,39 @@ field_3d_traceable(double point[6], double result[3], void *args_p) { double (*bounds)[2] = (double (*)[2]) args->bounds; - if( bounds == NULL || ((bounds[0][0] < point[0]) && (point[0] < bounds[0][1]) - && (bounds[1][0] < point[1]) && (point[1] < bounds[1][1]) - && (bounds[2][0] < point[2]) && (point[2] < bounds[2][1])) ) { + if( bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) + && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]) + && (bounds[2][0] < position[2]) && (position[2] < bounds[2][1])) ) { - double elec_field[3] = {0.}; - double mag_field[3] = {0.}; + field_3d(position, elec_out, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); + field_3d(position, mag_out, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); + double curr_field[3] = {0.}; - - field_3d(point, elec_field, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - current_field_3d(point, *currents, curr_field); - field_3d(point, mag_field, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, result); + current_field_3d(position, *currents, curr_field); + + mag_out[0] += curr_field[0]; + mag_out[1] += curr_field[1]; + mag_out[2] += curr_field[2]; } else { - result[0] = 0.0; - result[1] = 0.0; - result[2] = 0.0; + elec_out[0] = 0.0; + elec_out[1] = 0.0; + elec_out[2] = 0.0; + + mag_out[0] = 0.0; + mag_out[1] = 0.0; + mag_out[2] = 0.0; } } -EXPORT size_t -trace_particle_3d(double *times_array, double *pos_array, double charge_over_mass, - double tracer_bounds[3][2], - double atol, - struct effective_point_charges_3d eff_elec, - struct effective_point_charges_3d eff_mag, - struct effective_point_currents_3d eff_currents, - double *field_bounds) { - - struct field_evaluation_args args = { - .elec_charges = (void*) &eff_elec, - .mag_charges = (void*) &eff_mag, - .currents = (void*) &eff_currents, - .bounds = field_bounds - }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_3d_traceable, tracer_bounds, atol, (void*) &args); -} - - - void -field_3d_derivs_traceable(double point[6], double field[3], void *args_p) { +field_3d_derivs_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; - double elec_field[3]; - field_3d_derivs(point, elec_field, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); - - double mag_field[3]; - field_3d_derivs(point, mag_field, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); - - double curr_field[3] = {0., 0., 0.}; - combine_elec_magnetic_field(point + 3, elec_field, mag_field, curr_field, field); -} - -EXPORT size_t -trace_particle_3d_derivs(double *times_array, double *pos_array, double charge_over_mass, double bounds[3][2], double atol, - double *z_interpolation, double *electrostatic_coeffs, double *magnetostatic_coeffs, size_t N_z) { - - struct field_derivs_args args = { z_interpolation, electrostatic_coeffs, magnetostatic_coeffs, N_z }; - - return trace_particle(times_array, pos_array, charge_over_mass, field_3d_derivs_traceable, bounds, atol, (void*) &args); + field_3d_derivs(position, elec_out, args->z_interpolation, args->electrostatic_axial_coeffs, args->N_z); + field_3d_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } - EXPORT void fill_jacobian_buffer_3d( jacobian_buffer_3d jacobian_buffer, position_buffer_3d pos_buffer, @@ -292,95 +245,6 @@ EXPORT void fill_jacobian_buffer_3d( } } -EXPORT void fill_matrix_3d(double *restrict matrix, - vertices_3d triangle_points, - uint8_t *excitation_types, - double *excitation_values, - jacobian_buffer_3d jacobian_buffer, - position_buffer_3d pos_buffer, - size_t N_lines, - size_t N_matrix, - int lines_range_start, - int lines_range_end) { - - assert(lines_range_start < N_lines && lines_range_end < N_lines); - - for (int i = lines_range_start; i <= lines_range_end; i++) { - double target[3], jac; - position_and_jacobian_3d(1/3., 1/3., &triangle_points[i][0], target, &jac); - - enum ExcitationType type_ = excitation_types[i]; - - if (type_ == VOLTAGE_FIXED || type_ == VOLTAGE_FUN || type_ == MAGNETOSTATIC_POT) { - for (int j = 0; j < N_lines; j++) { - - // Position of first integration point. Check if - // close to the target triangle. - double distance = distance_3d(triangle_points[j][0], target); - double characteristic_length = distance_3d(triangle_points[j][0], triangle_points[j][1]); - - if(i == j) { - matrix[i*N_matrix + j] = self_potential_triangle(&triangle_points[j][0][0], &triangle_points[j][1][0], &triangle_points[j][2][0], target); - } - if(i != j && distance > 5*characteristic_length) { - for(int k = 0; k < N_TRIANGLE_QUAD; k++) { - - double *pos = pos_buffer[j][k]; - double jac = jacobian_buffer[j][k]; - matrix[i*N_matrix + j] += jac * potential_3d_point(target[0], target[1], target[2], pos[0], pos[1], pos[2], NULL); - } - - } - else { - matrix[i*N_matrix + j] = potential_triangle(triangle_points[j][0], triangle_points[j][1], triangle_points[j][2], target) / (4*M_PI); - } - - } - } - else if (type_ == DIELECTRIC || type_ == MAGNETIZABLE) { - - double normal[3]; - normal_3d(&triangle_points[i][0], normal); - double K = excitation_values[i]; - - // This factor is hard to derive. It takes into account that the field - // calculated at the edge of the dielectric is basically the average of the - // field at either side of the surface of the dielecric (the field makes a jump). - double factor = flux_density_to_charge_factor(K); - - for (int j = 0; j < N_lines; j++) { - - double distance = distance_3d(triangle_points[j][0], target); - double characteristic_length = distance_3d(triangle_points[j][0], triangle_points[j][1]); - - if(i == j) { - matrix[i*N_matrix + j] = -1.0; - } - else if(distance > 5*characteristic_length) { - for(int k = 0; k < N_TRIANGLE_QUAD; k++) { - double *pos = pos_buffer[j][k]; - double jac = jacobian_buffer[j][k]; - - matrix[i*N_matrix + j] += factor * jac * field_dot_normal_3d(target[0], target[1], target[2], pos[0], pos[1], pos[2], normal); - } - } - else { - double a = triangle_area(triangle_points[j][0], triangle_points[j][1], triangle_points[j][2]); - matrix[i*N_matrix + j] = factor * flux_triangle(triangle_points[j][0], triangle_points[j][1], triangle_points[j][2], target, normal) / (4*M_PI); - } - } - } - else { - printf("ExcitationType unknown\n"); - exit(1); - } - } -} - - - - - EXPORT bool plane_intersection(double p0[3], double normal[3], positions_3d positions, size_t N_p, double result[6]) { diff --git a/traceon/backend/triangle.c b/traceon/backend/triangle.c index f24dfd3..3f77989 100644 --- a/traceon/backend/triangle.c +++ b/traceon/backend/triangle.c @@ -2,185 +2,6 @@ #include #include -// To efficiently compute the double integrals we define -// a coordinate system as follows. -// Let v0, v1, v2 be the vertices of the source triangle -// Let p be the target point at which the potential (or flux) -// needs to be calculated. -// The x-axis and y-axis are orthogonal and lie in the plane -// of the triangle. The x-axis is aligned with v1-v0. -// The z-axis is perpendicular to the triangle and forms a right -// handed coordinate system with x,y. -// The origin of the coordinate system is the projection of the -// p on the plane of the triangle, and in the new coordinate -// system p can therefore be written as (0, 0, z). -// v0 can be written as (x0, y0, 0) -// v1 can be written as (x0 + a, y0, 0) -// v2 can be written as (x0 + b, y0 + c, 0) -// Therefore the whole problem can be expressed in x0,y0,a,b,c,z - -struct _normalized_triangle { - double x0; - double y0; - - double a; - double b; - double c; - double z; - - double *normal; -}; - -double _potential_integrand(double y, void *args_p) { - struct _normalized_triangle args = *(struct _normalized_triangle*)args_p; - double xmin = args.x0 + y/args.c*args.b; - double xmax = args.x0 + args.a + y/args.c*(args.b-args.a); - - double denom = sqrt((y+args.y0)*(y+args.y0) + args.z*args.z); - - if(denom < 1e-12) { - // The asinh(xmax/denom) - asinh(xmin/denom) is numerical - // unstable when denom is small. Taking the taylor expansion - // of denom -> 0 we find - return log(fabs(xmax)) - log(fabs(xmin)); - } - return asinh(xmax/denom) - asinh(xmin/denom); -} - -EXPORT double -potential_triangle(double v0[3], double v1[3], double v2[3], double target[3]) { - double vec1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]}; - double vec2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]}; - - double x_normal[3] = {vec1[0], vec1[1], vec1[2]}; - double a = norm_3d(x_normal[0], x_normal[1], x_normal[2]); - normalize_3d(x_normal); - - double z_normal[3]; - cross_product_3d(vec1, vec2, z_normal); - normalize_3d(z_normal); - - double y_normal[3]; - cross_product_3d(z_normal, x_normal, y_normal); - - double to_v0[3] = {v0[0]-target[0], v0[1]-target[1], v0[2]-target[2]}; - - double x0 = dot_3d(to_v0, x_normal); - double y0 = dot_3d(to_v0, y_normal); - double b = dot_3d(vec2, x_normal); - double c = dot_3d(vec2, y_normal); - double z = -dot_3d(z_normal, to_v0); - - struct _normalized_triangle tri = {x0, y0, a,b,c,z}; - - return kronrod_adaptive(_potential_integrand, 0, c, (void*) &tri, 1e-9, 1e-9); -} - -EXPORT double self_potential_triangle_v0(double v0[3], double v1[3], double v2[3]) { - double vec1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]}; - double vec2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]}; - - double x_normal[3] = {vec1[0], vec1[1], vec1[2]}; - double a = norm_3d(x_normal[0], x_normal[1], x_normal[2]); - normalize_3d(x_normal); - - double z_normal[3]; - cross_product_3d(vec1, vec2, z_normal); - normalize_3d(z_normal); - - double y_normal[3]; - cross_product_3d(z_normal, x_normal, y_normal); - - double b = dot_3d(vec2, x_normal); - double c = dot_3d(vec2, y_normal); - - double alpha = b / c; - double beta = (b - a) / c; - - return -((-a * asinh((beta * beta * c + c + beta * a) / a)) + - sqrt(beta * beta + 1) * (c * asinh((beta * c + a) / c) - asinh(alpha) * c) + - asinh(beta) * a) / sqrt(beta * beta + 1); -} - -EXPORT double self_potential_triangle(double v0[3], double v1[3], double v2[3], double target[3]) { - - return - self_potential_triangle_v0(target, v0, v1) + - self_potential_triangle_v0(target, v1, v2) + - self_potential_triangle_v0(target, v2, v0); -} - -double _flux_integrand(double y, void *args_p) { - struct _normalized_triangle args = *(struct _normalized_triangle*)args_p; - - double x0 = args.x0; - double y0 = args.y0; - double z = args.z; - - double xmin = x0 + y/args.c*args.b; - double xmax = x0 + args.a + y/args.c*(args.b-args.a); - - double z2 = z*z; - double xmin2 = xmin*xmin; - double yy02 = (y+y0)*(y+y0); - double xmax2 = xmax*xmax; - double r2 = z2 + yy02; - - double flux[3]; - - flux[0] = 1 / sqrt(r2 + xmax2) - 1 / sqrt(r2 + xmin2); - - // Singularity when r2 is small... - if (fabs(r2) < 1e-9) { - flux[1] = ((xmin2 - xmax2) * y0 + (xmin2 - xmax2) * y) / (2.0 * xmax2 * xmin2); - flux[2] = -((xmin2 - xmax2) * z) / (2.0 * xmax2 * xmin2); - } else { - double denom_max = r2 * sqrt(r2 + xmax2); - double denom_min = r2 * sqrt(r2 + xmin2); - - flux[1] = -((xmax * (y + y0)) / denom_max) + (xmin * (y + y0)) / denom_min; - flux[2] = (xmax * z) / denom_max - (xmin * z) / denom_min; - } - - return dot_3d(args.normal, flux); -} - - - -EXPORT double -flux_triangle(double v0[3], double v1[3], double v2[3], double target[3], double normal[3]) { - double vec1[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]}; - double vec2[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]}; - - double x_normal[3] = {vec1[0], vec1[1], vec1[2]}; - double a = norm_3d(x_normal[0], x_normal[1], x_normal[2]); - normalize_3d(x_normal); - - double z_normal[3]; - cross_product_3d(vec1, vec2, z_normal); - normalize_3d(z_normal); - - double y_normal[3]; - cross_product_3d(z_normal, x_normal, y_normal); - - double to_v0[3] = {v0[0]-target[0], v0[1]-target[1], v0[2]-target[2]}; - - double x0 = dot_3d(to_v0, x_normal); - double y0 = dot_3d(to_v0, y_normal); - double b = dot_3d(vec2, x_normal); - double c = dot_3d(vec2, y_normal); - double z = -dot_3d(z_normal, to_v0); - - // Express normal in new coordinate system - double new_normal[3] = {dot_3d(x_normal, normal), - dot_3d(y_normal, normal), - dot_3d(z_normal, normal)}; - - struct _normalized_triangle tri = {x0, y0, a, b, c, z, new_normal}; - - return kronrod_adaptive(_flux_integrand, 0, c, (void*) &tri, 1e-9, 1e-9); -} - EXPORT int triangle_orientation_is_equal(int triangle_index1, int triangle_index2, uint64_t triangles[][3], double points[][3]) { // Imagine standing at a common vertex, and looking along the common edge diff --git a/traceon/backend/utilities_2d.c b/traceon/backend/utilities_2d.c index 14e093e..a444969 100644 --- a/traceon/backend/utilities_2d.c +++ b/traceon/backend/utilities_2d.c @@ -64,3 +64,24 @@ INLINE void position_and_jacobian_radial(double alpha, double *v1, double *v2, d // Term following from the Jacobian *jac = 1/16. * sqrt(pow(2*alpha*(9*v4y-9*v3y-9*v2y+9*v1y)+3*a2*(9*v4y-27*v3y+27*v2y-9*v1y)-v4y+27*v3y-27*v2y+v1y, 2) +pow(2*alpha*(9*v4x-9*v3x-9*v2x+9*v1x)+3*a2*(9*v4x-27*v3x+27*v2x-9*v1x)-v4x+27*v3x-27*v2x+v1x, 2)); } + +INLINE void delta_position_and_jacobian_radial(double alpha, double *v1, double *v2, double *v3, double *v4, double *pos_out, double *jac) { + // Note that this function is the same as position_and_jacobian_radial, except + // it gives the delta with respect to alpha=0, this is useful when computing the singular + // self potentials + + double v1x = v1[0], v1y = v1[2]; + double v2x = v2[0], v2y = v2[2]; + double v3x = v3[0], v3y = v3[2]; + double v4x = v4[0], v4y = v4[2]; + + double a2 = pow(alpha, 2); + double a3 = pow(alpha, 3); + + // Higher order line element parametrization, relative to alpha=0 + pos_out[0] = (a2*(9*v4x-9*v3x-9*v2x+9*v1x)+a3*(9*v4x-27*v3x+27*v2x-9*v1x)+alpha*(-v4x+27*v3x-27*v2x+v1x))/16; + pos_out[1] = (a2*(9*v4y-9*v3y-9*v2y+9*v1y)+a3*(9*v4y-27*v3y+27*v2y-9*v1y)+alpha*(-v4y+27*v3y-27*v2y+v1y))/16; + + // Term following from the Jacobian + *jac = 1/16. * sqrt(pow(2*alpha*(9*v4y-9*v3y-9*v2y+9*v1y)+3*a2*(9*v4y-27*v3y+27*v2y-9*v1y)-v4y+27*v3y-27*v2y+v1y, 2) +pow(2*alpha*(9*v4x-9*v3x-9*v2x+9*v1x)+3*a2*(9*v4x-27*v3x+27*v2x-9*v1x)-v4x+27*v3x-27*v2x+v1x, 2)); +} diff --git a/traceon/backend/utilities_3d.c b/traceon/backend/utilities_3d.c index 2850213..7838da9 100644 --- a/traceon/backend/utilities_3d.c +++ b/traceon/backend/utilities_3d.c @@ -132,32 +132,3 @@ INLINE void position_and_jacobian_3d(double alpha, double beta, triangle t, doub pos_out[2] = z; *jac = 2*triangle_area(t[0], t[1], t[2]); } - -// Compute E + v x B, which is used in the Lorentz force law to calculate the force -// on the particle. The magnetic field produced by magnetiziation and the magnetic field -// produced by currents are passed in separately, but can simpy be summed to find the total -// magnetic field. -EXPORT void -combine_elec_magnetic_field(double velocity[3], double elec_field[3], - double mag_field[3], double current_field[3], double result[3]) { - - double total_mag[3] = {0.}; // Total magnetic field, produced by charges and currents - - // Important: Traceon always computes the H field - // Therefore when converting from H to B we need to multiply - // by mu_0. - total_mag[0] = MU_0*(mag_field[0] + current_field[0]); - total_mag[1] = MU_0*(mag_field[1] + current_field[1]); - total_mag[2] = MU_0*(mag_field[2] + current_field[2]); - - double cross[3] = {0.}; - - // Calculate v x B - cross_product_3d(velocity, total_mag, cross); - - result[0] = elec_field[0] + cross[0]; - result[1] = elec_field[1] + cross[1]; - result[2] = elec_field[2] + cross[2]; -} - - diff --git a/traceon/field.py b/traceon/field.py new file mode 100644 index 0000000..daa5e57 --- /dev/null +++ b/traceon/field.py @@ -0,0 +1,785 @@ +import time +from abc import ABC, abstractmethod + +import numpy as np +from scipy.interpolate import CubicSpline, BPoly, PPoly + +from . import tracing as T +from . import excitation as E +from . import util +from . import logging +from . import backend + +class EffectivePointCharges: + def __init__(self, charges, jacobians, positions, directions=None): + self.charges = np.array(charges, dtype=np.float64) + self.jacobians = np.array(jacobians, dtype=np.float64) + self.positions = np.array(positions, dtype=np.float64) + self.directions = directions # Current elements will have a direction + + N = len(self.charges) + N_QUAD = self.jacobians.shape[1] + assert self.charges.shape == (N,) and self.jacobians.shape == (N, N_QUAD) + assert self.positions.shape == (N, N_QUAD, 3) or self.positions.shape == (N, N_QUAD, 2) + assert self.directions is None or self.directions.shape == (len(self.charges), N_QUAD, 3) + + @staticmethod + def empty_2d(): + N_QUAD_2D = backend.N_QUAD_2D + return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0,N_QUAD_2D,2))) + + @staticmethod + def empty_3d(): + N_TRIANGLE_QUAD = backend.N_TRIANGLE_QUAD + return EffectivePointCharges(np.empty((0,)), np.empty((0, N_TRIANGLE_QUAD)), np.empty((0, N_TRIANGLE_QUAD, 3))) + + @staticmethod + def empty_line_3d(): + N_QUAD_2D = backend.N_QUAD_2D + return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0, N_QUAD_2D, 3)), np.empty((0, N_QUAD_2D, 3))) + + def is_2d(self): + return self.jacobians.shape[1] == backend.N_QUAD_2D + + def is_3d(self): + return self.jacobians.shape[1] == backend.N_TRIANGLE_QUAD + + def __len__(self): + return len(self.charges) + + def __add__(self, other): + if np.array_equal(self.positions, other.positions) and np.array_equal(self.jacobians, other.jacobians): + return EffectivePointCharges(self.charges + other.charges, self.jacobians, self.positions) + else: + return EffectivePointCharges( + np.concatenate([self.charges, other.charges]), + np.concatenate([self.jacobians, other.jacobians]), + np.concatenate([self.positions, other.positions])) + + def __mul__(self, other): + if isinstance(other, int) or isinstance(other, float): + return EffectivePointCharges(other*self.charges, self.jacobians, self.positions) + + return NotImplemented + + def __neg__(self): + return -1*self + + def __rmul__(self, other): + return self.__mul__(other) + + def __str__(self): + dim = '2D' if self.is_2d() else '3D' + return f'' + + +class Field(ABC): + 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 + +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'' + + @abstractmethod + def current_field_at_point(self, point_): + ... + + +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 + + + +class Field3D_BEM(FieldBEM): + """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`.""" + + 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_3d() + if magnetostatic_point_charges is None: + magnetostatic_point_charges = EffectivePointCharges.empty_3d() + if current_point_charges is None: + current_point_charges = EffectivePointCharges.empty_line_3d() + + super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) + + self.symmetry = E.Symmetry.THREE_D + + for eff in [electrostatic_point_charges, magnetostatic_point_charges]: + N = len(eff.charges) + assert eff.charges.shape == (N,) + assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD) + assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) + + def current_field_at_point(self, point_): + point = np.array(point_, dtype=np.double) + eff = self.current_point_charges + return backend.current_field_3d(point, eff) + + 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 + """ + 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_3d(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_3d(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_3d(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_3d(point, charges, jacobians, positions) + + def area_of_element(self, i): + jacobians = self.electrostatic_point_charges.jacobians + return np.sum(jacobians[i]) + + def get_tracer(self, bounds): + return T.Tracer(self, bounds) + + def get_low_level_trace_function(self): + args = backend.FieldEvaluationArgs3D(self.electrostatic_point_charges, self.magnetostatic_point_charges, self.current_point_charges, self.field_bounds) + return backend.field_fun(("field_3d_traceable", backend.backend_lib)), args + +FACTOR_AXIAL_DERIV_SAMPLING_2D = 0.2 + +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'' + + 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) + +def _get_one_dimensional_high_order_ppoly(z, y, dydz, dydz2): + bpoly = BPoly.from_derivatives(z, np.array([y, dydz, dydz2]).T) + return PPoly.from_bernstein_basis(bpoly) + +def _quintic_spline_coefficients(z, derivs): + # k is degree of polynomial + #assert derivs.shape == (z.size, backend.DERIV_2D_MAX) + c = np.zeros( (z.size-1, 9, 6) ) + + dz = z[1] - z[0] + assert np.all(np.isclose(np.diff(z), dz)) # Equally spaced + + for i, d in enumerate(derivs): + high_order = i + 2 < len(derivs) + + if high_order: + ppoly = _get_one_dimensional_high_order_ppoly(z, d, derivs[i+1], derivs[i+2]) + start_index = 0 + else: + ppoly = CubicSpline(z, d) + start_index = 2 + + c[:, i, start_index:], x, k = ppoly.c.T, ppoly.x, ppoly.c.shape[0]-1 + assert np.all(x == z) + assert (high_order and k == 5) or (not high_order and k == 3) + + return c + + +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 + + + + diff --git a/traceon/geometry.py b/traceon/geometry.py index d9de84b..999bd06 100644 --- a/traceon/geometry.py +++ b/traceon/geometry.py @@ -10,6 +10,7 @@ meshes generated by other programs (e.g. GMSH or Comsol). Traceon can import [meshio](https://github.com/nschloe/meshio) meshes or any file format supported by meshio.""" +import warnings from math import pi, sqrt, sin, cos, atan2, ceil import numpy as np @@ -30,6 +31,8 @@ __pdoc__['SurfaceCollection.__iadd__'] = True __pdoc__['Surface.__call__'] = True +__pdoc__['Path.line_to'] = False +__pdoc__['Path.arc_to'] = False def _points_close(p1, p2, tolerance=1e-8): return np.allclose(p1, p2, atol=tolerance) @@ -275,10 +278,32 @@ def line_to(self, point): 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): @@ -352,6 +377,28 @@ def f(u): 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 @@ -521,7 +568,7 @@ def close(self): Returns ------------------- Path""" - return self.line_to(self.starting_point()) + return self.extend_with_line(self.starting_point()) @staticmethod def ellipse(major, minor): @@ -599,7 +646,7 @@ def rectangle_xz(xmin, xmax, zmin, zmax): ----------------------- Path""" return Path.line([xmin, 0., zmin], [xmax, 0, zmin]) \ - .line_to([xmax, 0, zmax]).line_to([xmin, 0., zmax]).close() + .extend_with_line([xmax, 0, zmax]).extend_with_line([xmin, 0., zmax]).close() @staticmethod def rectangle_yz(ymin, ymax, zmin, zmax): @@ -622,7 +669,7 @@ def rectangle_yz(ymin, ymax, zmin, zmax): Path""" return Path.line([0., ymin, zmin], [0, ymin, zmax]) \ - .line_to([0., ymax, zmax]).line_to([0., ymax, zmin]).close() + .extend_with_line([0., ymax, zmax]).extend_with_line([0., ymax, zmin]).close() @staticmethod def rectangle_xy(xmin, xmax, ymin, ymax): @@ -644,7 +691,7 @@ def rectangle_xy(xmin, xmax, ymin, ymax): ----------------------- Path""" return Path.line([xmin, ymin, 0.], [xmin, ymax, 0.]) \ - .line_to([xmax, ymax, 0.]).line_to([xmax, ymin, 0.]).close() + .extend_with_line([xmax, ymax, 0.]).extend_with_line([xmax, ymin, 0.]).close() @staticmethod def aperture(height, radius, extent, z=0.): @@ -668,7 +715,118 @@ def aperture(height, radius, extent, z=0.): ------------------------ Path""" return Path.line([extent, 0., -height/2], [radius, 0., -height/2])\ - .line_to([radius, 0., height/2]).line_to([extent, 0., height/2]).move(dz=z) + .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): @@ -806,7 +964,20 @@ def __iadd__(self, other): 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): @@ -819,7 +990,7 @@ def extrude_by_path(self, p2): return self._map_to_surfaces(Path.extrude_by_path, p2) def __str__(self): - return f"" + return f"" class Surface(GeometricObject): @@ -951,8 +1122,8 @@ def box(p0, p1): path4 = Path.line([xmin, ymax, zmin], [xmax, ymax, zmin]) side_path = Path.line([xmin, ymin, zmin], [xmax, ymin, zmin])\ - .line_to([xmax, ymin, zmax])\ - .line_to([xmin, ymin, zmax])\ + .extend_with_line([xmax, ymin, zmax])\ + .extend_with_line([xmin, ymin, zmax])\ .close() side_surface = side_path.extrude([0.0, ymax-ymin, 0.0]) @@ -1370,6 +1541,19 @@ def __iadd__(self, other): 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"" diff --git a/traceon/interpolation.py b/traceon/interpolation.py deleted file mode 100644 index 05a264b..0000000 --- a/traceon/interpolation.py +++ /dev/null @@ -1,204 +0,0 @@ -import time -from abc import ABC, abstractmethod - -import numpy as np -from scipy.interpolate import CubicSpline, BPoly, PPoly - -from . import solver as S -from . import tracing as T -from . import util -from . import logging -from . import backend - -FACTOR_AXIAL_DERIV_SAMPLING_2D = 0.2 - -class FieldAxial(S.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'' - - 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) - -def _get_one_dimensional_high_order_ppoly(z, y, dydz, dydz2): - bpoly = BPoly.from_derivatives(z, np.array([y, dydz, dydz2]).T) - return PPoly.from_bernstein_basis(bpoly) - -def _quintic_spline_coefficients(z, derivs): - # k is degree of polynomial - #assert derivs.shape == (z.size, backend.DERIV_2D_MAX) - c = np.zeros( (z.size-1, 9, 6) ) - - dz = z[1] - z[0] - assert np.all(np.isclose(np.diff(z), dz)) # Equally spaced - - for i, d in enumerate(derivs): - high_order = i + 2 < len(derivs) - - if high_order: - ppoly = _get_one_dimensional_high_order_ppoly(z, d, derivs[i+1], derivs[i+2]) - start_index = 0 - else: - ppoly = CubicSpline(z, d) - start_index = 2 - - c[:, i, start_index:], x, k = ppoly.c.T, ppoly.x, ppoly.c.shape[0]-1 - assert np.all(x == z) - assert (high_order and k == 5) or (not high_order and k == 3) - - return c - - -class FieldRadialAxial(FieldAxial): - """ """ - def __init__(self, field, zmin, zmax, N=None): - assert isinstance(field, S.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: S.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.TracerRadialAxial(self, bounds) - - diff --git a/traceon/solver.py b/traceon/solver.py index 60afcdf..81db056 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -54,6 +54,7 @@ import numpy as np from scipy.special import legendre +from scipy.constants import e, mu_0, m_e from . import geometry as G from . import excitation as E @@ -61,7 +62,7 @@ from . import backend from . import util from . import tracing as T - +from .field import * class Solver(ABC): @@ -89,25 +90,22 @@ def __init__(self, excitation): self.excitation_types = excitation_types self.excitation_values = excitation_values - - is_radial = self.excitation.symmetry == E.Symmetry.RADIAL - higher_order = self.is_higher_order() - - assert not higher_order or is_radial, "Higher order not supported in 3D" - if is_radial and higher_order: - jac, pos = backend.fill_jacobian_buffer_radial(vertices) - elif not is_radial: - jac, pos = backend.fill_jacobian_buffer_3d(vertices) - else: - raise ValueError('Input excitation is 2D but not higher order, this solver input is currently not supported. Consider upgrading mesh to higher order.') - self.jac_buffer = jac - self.pos_buffer = pos + + self.jac_buffer, self.pos_buffer = self.get_jacobians_and_positions(self.vertices) + + @abstractmethod + def get_jacobians_and_positions(self, vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + ... @abstractmethod def get_active_elements(self) -> Tuple[np.ndarray, Dict[str, np.ndarray]]: ... + + @abstractmethod + def get_normal_vectors(self) -> np.ndarray: + ... def get_number_of_matrix_elements(self): return len(self.vertices) @@ -121,13 +119,14 @@ def is_3d(self): def is_2d(self): return self.excitation.mesh.is_2d() - @abstractmethod def get_flux_indices(self): """Get the indices of the vertices that are of type DIELECTRIC or MAGNETIZABLE. For these indices we don't compute the potential but the flux through the element (the inner product of the field with the normal of the vertex. The method is implemented in the derived classes.""" ... - + N = self.get_number_of_matrix_elements() + return np.arange(N)[ (self.excitation_types == int(E.ExcitationType.DIELECTRIC)) | (self.excitation_types == int(E.ExcitationType.MAGNETIZABLE)) ] + def get_center_of_element(self, index): two_d = self.is_2d() higher_order = self.is_higher_order() @@ -138,53 +137,56 @@ def get_center_of_element(self, index): v0, v1, v2, v3 = self.vertices[index] jac, pos = backend.position_and_jacobian_radial(0, v0, v2, v3, v1) return np.array([pos[0], 0.0, pos[1]]) - + @abstractmethod - def get_right_hand_side(self): + def get_preexisting_field(self, point: np.ndarray) -> np.ndarray: + """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.""" ... - - def get_matrix(self): - assert (self.is_3d() and not self.is_higher_order()) or \ - (self.is_2d() and self.is_higher_order()), "2D mesh needs to be higher order (consider upgrading mesh), 3D mesh needs to be simple (higher order not supported)." - - N_matrix = self.get_number_of_matrix_elements() - matrix = np.zeros( (N_matrix, N_matrix) ) - logging.log_info(f'Using matrix solver, number of elements: {N_matrix}, size of matrix: {N_matrix} ({matrix.nbytes/1e6:.0f} MB), symmetry: {self.excitation.symmetry}, higher order: {self.excitation.mesh.is_higher_order()}') - - fill_fun = backend.fill_matrix_3d if self.is_3d() else backend.fill_matrix_radial - - def fill_matrix_rows(rows): - fill_fun(matrix, - self.vertices, - self.excitation_types, - self.excitation_values, - self.jac_buffer, self.pos_buffer, rows[0], rows[-1]) - + + def get_right_hand_side(self) -> np.ndarray: st = time.time() - util.split_collect(fill_matrix_rows, np.arange(N_matrix)) - logging.log_info(f'Time for building matrix: {(time.time()-st)*1000:.0f} ms') + N = self.get_number_of_matrix_elements() + F = np.zeros((N,)) + + normals = self.get_normal_vectors() + + assert self.excitation_types.shape == (N,) and self.excitation_values.shape == (N,) + + # Loop over all excitation types/values + for i, (type_, value) in enumerate(zip(self.excitation_types, self.excitation_values)): + # Handle voltage-related excitations + if type_ in [E.ExcitationType.VOLTAGE_FIXED, E.ExcitationType.VOLTAGE_FUN, E.ExcitationType.MAGNETOSTATIC_POT]: + F[i] = value + elif type_ == E.ExcitationType.DIELECTRIC: + F[i] = 0 + elif type_ == E.ExcitationType.MAGNETIZABLE: + center = self.get_center_of_element(i) + field_at_center = self.get_preexisting_field(center) + + n = normals[i] + # Convert 2D normal to 3D if needed + if n.shape == (2,): + n = [n[0], 0.0, n[1]] - if not self.is_3d(): - # Technical detail: radial cannot compute their own self potential/field - # need to fill it in here - for i in range(N_matrix): - type_ = self.excitation_types[i] - val = self.excitation_values[i] - - if type_ == E.ExcitationType.DIELECTRIC or type_ == E.ExcitationType.MAGNETIZABLE: - # -1 follows from matrix equation - matrix[i, i] = backend.self_field_dot_normal_radial(self.vertices[i], self.excitation_values[i]) - 1 - else: - matrix[i, i] = backend.self_potential_radial(self.vertices[i]) + field_dotted = np.dot(field_at_center, n) + F[i] = -backend.flux_density_to_charge_factor(value) * field_dotted - assert np.all(np.isfinite(matrix)) - - return matrix - + assert np.all(np.isfinite(F)) + + logging.log_info(f"Computing right hand side of linear system took {(time.time() - st) * 1000:.0f} ms") + + return F + @abstractmethod def charges_to_field(self, charges): ... - + + @abstractmethod + def get_matrix(self) -> np.ndarray: + ... + def solve_matrix(self, right_hand_side=None): F = np.array([self.get_right_hand_side()]) if right_hand_side is None else right_hand_side @@ -207,78 +209,103 @@ def solve_matrix(self, right_hand_side=None): assert len(result) == len(F) return result -class ElectrostaticSolver(Solver): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def get_flux_indices(self): - N = self.get_number_of_matrix_elements() - return np.arange(N)[self.excitation_types == int(E.ExcitationType.DIELECTRIC)] - - def get_active_elements(self): - return self.excitation.get_electrostatic_active_elements() - - def get_right_hand_side(self): - N = self.get_number_of_matrix_elements() - F = np.zeros( (N,) ) - - assert self.excitation_types.shape ==(N,) and self.excitation_values.shape == (N,) - - # TODO: optimize in backend? - for i, (type_, value) in enumerate(zip(self.excitation_types, self.excitation_values)): - if type_ in [E.ExcitationType.VOLTAGE_FIXED, E.ExcitationType.VOLTAGE_FUN]: - F[i] = value - elif type_ == E.ExcitationType.DIELECTRIC: - F[i] = 0 - - assert np.all(np.isfinite(F)) - return F - def charges_to_field(self, charges): - if self.is_3d(): - return Field3D_BEM(electrostatic_point_charges=charges) - else: - return FieldRadialBEM(electrostatic_point_charges=charges) - -class MagnetostaticSolver(Solver): +class SolverRadial(Solver): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - # Field produced by the current excitations on the coils - self.current_field = self.get_current_field() - # TODO: optimize in backend? N = len(self.vertices) normals = np.zeros( (N, 2) if self.is_2d() else (N, 3) ) for i, v in enumerate(self.vertices): - if self.is_2d() and not self.is_higher_order(): + if not self.is_higher_order(): normals[i] = backend.normal_2d(v[0], v[1]) - elif self.is_2d() and self.is_higher_order(): + else: normals[i] = backend.higher_order_normal_radial(0.0, v[:, :2]) - elif self.is_3d() and not self.is_higher_order(): - normals[i] = backend.normal_3d(v) - + self.normals = normals + def get_normal_vectors(self): + return self.normals + + def get_jacobians_and_positions(self, vertices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + return backend.fill_jacobian_buffer_radial(vertices) + + def get_matrix(self): + # Sanity check: 2D must be higher-order + assert self.is_2d() and self.is_higher_order(), "2D mesh needs to be higher-order (consider upgrading mesh)." + + N_matrix = self.get_number_of_matrix_elements() + matrix = np.zeros((N_matrix, N_matrix)) + + logging.log_info( + f'Using matrix solver (radial). Number of elements: {N_matrix}, ' + f'size of matrix: {N_matrix} ({matrix.nbytes/1e6:.0f} MB), ' + f'symmetry: {self.excitation.symmetry}, ' + f'higher order: {self.excitation.mesh.is_higher_order()}') + + # For radial, use the radial fill function + fill_fun = backend.fill_matrix_radial + + # Wrapper for filling matrix rows + def fill_matrix_rows(rows): + fill_fun( + matrix, + self.vertices, + self.excitation_types, + self.excitation_values, + self.jac_buffer, + self.pos_buffer, + rows[0], + rows[-1]) + + # Fill the matrix + st = time.time() + util.split_collect(fill_matrix_rows, np.arange(N_matrix)) + + # For radial meshes, we add self-potential/field contributions + for i in range(N_matrix): + type_ = self.excitation_types[i] + val = self.excitation_values[i] + + if type_ in [E.ExcitationType.DIELECTRIC, E.ExcitationType.MAGNETIZABLE]: + # -1 follows from matrix equation + matrix[i, i] = (backend.self_field_dot_normal_radial(self.vertices[i], val) - 1) + else: + matrix[i, i] = backend.self_potential_radial(self.vertices[i]) + + logging.log_info(f'Time for building radial matrix: {(time.time()-st)*1000:.0f} ms') + + assert np.all(np.isfinite(matrix)), "Matrix contains non-finite values." + + return matrix + + +class ElectrostaticSolverRadial(SolverRadial): + def get_preexisting_field(self, point): + np.zeros(3) + def get_active_elements(self): - return self.excitation.get_magnetostatic_active_elements() + return self.excitation.get_electrostatic_active_elements() - def get_flux_indices(self): - N = self.get_number_of_matrix_elements() - return np.arange(N)[self.excitation_types == int(E.ExcitationType.MAGNETIZABLE)] + def charges_to_field(self, charges): + return FieldRadialBEM(electrostatic_point_charges=charges) - def get_current_field(self): - if self.excitation.symmetry == E.Symmetry.RADIAL: - return self.get_current_field_radial() - elif self.excitation.symmetry == E.Symmetry.THREE_D: - return self.get_current_field_three_d() - raise ValueError('Symmetry should be one of RADIAL or THREE_D') +class MagnetostaticSolverRadial(SolverRadial): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.current_field = self.get_current_field() - def get_current_field_radial(self): + def get_active_elements(self): + return self.excitation.get_magnetostatic_active_elements() + + def get_preexisting_field(self, point): + return self.current_field.current_field_at_point(point) + + def get_current_field(self) -> FieldBEM: currents: list[np.ndarray] = [] jacobians = [] positions = [] @@ -311,143 +338,9 @@ def get_current_field_radial(self): return FieldRadialBEM(current_point_charges=EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions))) - def get_current_field_three_d(self): - currents: list[np.ndarray] = [] - jacobians = [] - positions = [] - directions = [] - - mesh = self.excitation.mesh - - if not len(mesh.lines) or not self.excitation.has_current(): - return Field3D_BEM(current_point_charges=EffectivePointCharges.empty_3d()) - - jac, pos, dir_ = backend.fill_jacobian_buffer_current_three_d(mesh.points[mesh.lines[:, :2]]) - - for n, v in self.excitation.excitation_types.items(): - if not v[0] == E.ExcitationType.CURRENT or not n in mesh.physical_to_lines: - continue - - indices = mesh.physical_to_lines[n] - - if not len(indices): - continue - - # Current supplied is total current, not a current density, therefore - # divide by the length - length = np.sum(jac[indices]) - currents.extend(np.full(len(indices), v[1])) - jacobians.extend(jac[indices]) - positions.extend(pos[indices]) - directions.extend(dir_[indices]) - - if len(currents): - eff = EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions), directions=np.array(directions)) - else: - eff = EffectivePointCharges.empty_3d() - - return Field3D_BEM(current_point_charges=eff) - - def get_right_hand_side(self): - st = time.time() - N = self.get_number_of_matrix_elements() - F = np.zeros( (N,) ) - - assert self.excitation_types.shape ==(N,) and self.excitation_values.shape == (N,) - - # TODO: optimize in backend? - for i, (type_, value) in enumerate(zip(self.excitation_types, self.excitation_values)): - if type_ == E.ExcitationType.MAGNETOSTATIC_POT: - F[i] = value - elif type_ == E.ExcitationType.MAGNETIZABLE: - # Here we compute the inner product of the field generated by the current excitations - # and the normal vector of the vertex. - center = self.get_center_of_element(i) - field_at_center = self.current_field.current_field_at_point(center) - - n = self.normals[i] - if n.shape == (2,): - n = [n[0], 0., n[1]] - - field_dotted = np.dot(field_at_center, n) - F[i] = -backend.flux_density_to_charge_factor(value) * field_dotted - - assert np.all(np.isfinite(F)) - logging.log_info(f'Computing right hand side of linear system took {(time.time()-st)*1000:.0f} ms') - return F - def charges_to_field(self, charges): - if self.is_3d(): - return Field3D_BEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) - else: - return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) - -class EffectivePointCharges: - def __init__(self, charges, jacobians, positions, directions=None): - self.charges = np.array(charges, dtype=np.float64) - self.jacobians = np.array(jacobians, dtype=np.float64) - self.positions = np.array(positions, dtype=np.float64) - self.directions = directions # Current elements will have a direction - - N = len(self.charges) - N_QUAD = self.jacobians.shape[1] - assert self.charges.shape == (N,) and self.jacobians.shape == (N, N_QUAD) - assert self.positions.shape == (N, N_QUAD, 3) or self.positions.shape == (N, N_QUAD, 2) - assert self.directions is None or self.directions.shape == (len(self.charges), N_QUAD, 3) - - @staticmethod - def empty_2d(): - N_QUAD_2D = backend.N_QUAD_2D - return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0,N_QUAD_2D,2))) - - @staticmethod - def empty_3d(): - N_TRIANGLE_QUAD = backend.N_TRIANGLE_QUAD - return EffectivePointCharges(np.empty((0,)), np.empty((0, N_TRIANGLE_QUAD)), np.empty((0, N_TRIANGLE_QUAD, 3))) - - @staticmethod - def empty_line_3d(): - N_QUAD_2D = backend.N_QUAD_2D - return EffectivePointCharges(np.empty((0,)), np.empty((0, N_QUAD_2D)), np.empty((0, N_QUAD_2D, 3)), np.empty((0, N_QUAD_2D, 3))) + return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) - def is_2d(self): - return self.jacobians.shape[1] == backend.N_QUAD_2D - - def is_3d(self): - return self.jacobians.shape[1] == backend.N_TRIANGLE_QUAD - - def __len__(self): - return len(self.charges) - - def __add__(self, other): - if np.array_equal(self.positions, other.positions) and np.array_equal(self.jacobians, other.jacobians): - return EffectivePointCharges(self.charges + other.charges, self.jacobians, self.positions) - else: - return EffectivePointCharges( - np.concatenate([self.charges, other.charges]), - np.concatenate([self.jacobians, other.jacobians]), - np.concatenate([self.positions, other.positions])) - - def __mul__(self, other): - if isinstance(other, int) or isinstance(other, float): - return EffectivePointCharges(other*self.charges, self.jacobians, self.positions) - - return NotImplemented - - def __neg__(self): - return -1*self - - def __rmul__(self, other): - return self.__mul__(other) - - def __str__(self): - dim = '2D' if self.is_2d() else '3D' - return f'' - - def _excitation_to_higher_order(excitation): logging.log_info('Upgrading mesh to higher to be compatible with matrix solver') @@ -475,14 +368,14 @@ def solve_direct_superposition(excitation): # Solve for elec fields elec_names = [n for n, v in excitations.items() if v.is_electrostatic()] - right_hand_sides = np.array([ElectrostaticSolver(excitations[n]).get_right_hand_side() for n in elec_names]) - solutions = ElectrostaticSolver(excitation).solve_matrix(right_hand_sides) + 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([MagnetostaticSolver(excitations[n]).get_right_hand_side() for n in mag_names]) - solutions = MagnetostaticSolver(excitation).solve_matrix(right_hand_sides) + 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} @@ -499,8 +392,7 @@ def solve_direct(excitation): Returns ------- - A `FieldRadialBEM` if the geometry (contained in the given `excitation`) is radially symmetric. If the geometry is a three - dimensional geometry `Field3D_BEM` is returned. + `FieldRadialBEM` """ if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order(): excitation = _excitation_to_higher_order(excitation) @@ -508,653 +400,16 @@ def solve_direct(excitation): mag, elec = excitation.is_magnetostatic(), excitation.is_electrostatic() assert mag or elec, "Solving for an empty excitation" - + if mag and elec: - elec_field = ElectrostaticSolver(excitation).solve_matrix()[0] - mag_field = MagnetostaticSolver(excitation).solve_matrix()[0] + 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 ElectrostaticSolver(excitation).solve_matrix()[0] + return ElectrostaticSolverRadial(excitation).solve_matrix()[0] elif mag and not elec: - return MagnetostaticSolver(excitation).solve_matrix()[0] - - - -class Field(ABC): - 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) - - 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 magnetostatic_potential_at_point(self, point): - ... - - @abstractmethod - def electrostatic_potential_at_point(self, point): - ... - - @abstractmethod - def magnetostatic_field_at_point(self, point): - ... - - @abstractmethod - def electrostatic_field_at_point(self, point): - ... - - - - -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'' - - @abstractmethod - def current_field_at_point(self, point_): - ... - - -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.TracerRadialBEM(self, bounds) - - -class Field3D_BEM(FieldBEM): - """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`.""" - - 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_3d() - if magnetostatic_point_charges is None: - magnetostatic_point_charges = EffectivePointCharges.empty_3d() - if current_point_charges is None: - current_point_charges = EffectivePointCharges.empty_line_3d() - - super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) - - self.symmetry = E.Symmetry.THREE_D - - for eff in [electrostatic_point_charges, magnetostatic_point_charges]: - N = len(eff.charges) - assert eff.charges.shape == (N,) - assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD) - assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) - - def current_field_at_point(self, point_): - point = np.array(point_, dtype=np.double) - eff = self.current_point_charges - return backend.current_field_3d(point, eff) - - 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 - """ - 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_3d(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_3d(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 + return MagnetostaticSolverRadial(excitation).solve_matrix()[0] - mag_field = backend.field_3d(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_3d(point, charges, jacobians, positions) - - def area_of_element(self, i): - jacobians = self.electrostatic_point_charges.jacobians - return np.sum(jacobians[i]) - - def get_tracer(self, bounds): - return T.Tracer3D_BEM(self, bounds) - - -class FieldAxial(Field): - """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'' - - 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) - - -class FieldRadialAxial(FieldAxial): - """ """ - def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None): - 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) - self.symmetry = E.Symmetry.RADIAL - - 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" - 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: (3,) array of float64 - Position at which to compute the field. - - Returns - ------- - (3,) array of float64, containing the field strengths (units of A/m) - """ - 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: (3,) 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: (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" - return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs) - - def get_tracer(self, bounds): - return T.TracerRadialAxial(self, bounds) diff --git a/traceon/tracing.py b/traceon/tracing.py index d62dfaa..7d04f5d 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -7,14 +7,15 @@ [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.""" - +import ctypes from math import sqrt, cos, sin, atan2 import time from enum import Enum import numpy as np import scipy -from scipy.constants import m_e, e +from scipy.constants import m_e, e, mu_0 + from . import backend from . import logging @@ -116,7 +117,11 @@ def __init__(self, field, bounds): bounds = np.array(bounds).astype(np.float64) assert bounds.shape == (3,2) self.bounds = bounds - + + self.trace_fun, args = field.get_low_level_trace_function() + + self.trace_args = args if args is None else ctypes.cast(ctypes.pointer(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]) @@ -148,47 +153,17 @@ def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-8): 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. """ - raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance') - -class TracerRadialBEM(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): charge_over_mass = charge / mass velocity = _convert_velocity_to_SI(velocity, mass) - - return backend.trace_particle_radial( + + return backend.trace_particle( position, velocity, charge_over_mass, + self.trace_fun, self.bounds, - atol, - self.field.electrostatic_point_charges, - self.field.magnetostatic_point_charges, - self.field.current_point_charges, - field_bounds=self.field.field_bounds) - -class TracerRadialAxial(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - - elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs - - return backend.trace_particle_radial_derivs(position, velocity, charge_over_mass, self.bounds, atol, self.field.z, elec, mag) - -class Tracer3D_BEM(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges - currents = self.field.current_point_charges - return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, currents, field_bounds=self.field.field_bounds) - -class Tracer3DAxial(Tracer): - def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10): - charge_over_mass = charge / mass - velocity = _convert_velocity_to_SI(velocity, mass) - return backend.trace_particle_3d_derivs(position, velocity, charge_over_mass, self.bounds, atol, - self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs) + atol, + self.trace_args) def plane_intersection(positions, p0, normal): """Compute the intersection of a trajectory with a general plane in 3D. The plane is specified diff --git a/validation/capacitance_sphere.py b/validation/capacitance_sphere.py index f59b609..12ba8f7 100644 --- a/validation/capacitance_sphere.py +++ b/validation/capacitance_sphere.py @@ -27,7 +27,7 @@ def __init__(self): def create_mesh(self, MSF, symmetry, higher_order): def add_shell(radius, name, reverse=False): - arc = G.Path.arc([0., 0., 0.], [0, 0, -radius], [radius, 0, 0.]).arc_to([0., 0., 0.], [0., 0., radius]) + arc = G.Path.arc([0., 0., 0.], [0, 0, -radius], [radius, 0, 0.]).extend_with_arc([0., 0., 0.], [0., 0., radius]) arc.name = name if symmetry.is_3d(): diff --git a/validation/dohi.py b/validation/dohi.py index 43144d0..ef78cbf 100644 --- a/validation/dohi.py +++ b/validation/dohi.py @@ -6,12 +6,12 @@ import traceon.excitation as E import traceon.tracing as T import traceon.solver as S -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial from validation import Validation try: - from traceon_pro.interpolation import Field3DAxial + from traceon_pro.field import Field3DAxial except ImportError: Field3DAxial = None @@ -53,7 +53,7 @@ def create_mesh(self, MSF, symmetry, higher_order): ground.name = 'ground' boundary = G.Path.line([0., 0., 1.75], [rmax, 0., 1.75]) \ - .line_to([rmax, 0., -0.3]).line_to([0., 0., -0.3]) + .extend_with_line([rmax, 0., -0.3]).extend_with_line([0., 0., -0.3]) boundary.name = 'boundary' geom = mirror+mirror_line+lens+ground+boundary diff --git a/validation/edwards2007.py b/validation/edwards2007.py index cf659b6..97db368 100644 --- a/validation/edwards2007.py +++ b/validation/edwards2007.py @@ -34,10 +34,10 @@ def create_mesh(self, MSF, symmetry, higher_order): ] inner = G.Path.line(points[0], points[1])\ - .line_to(points[2]).line_to(points[3]) + .extend_with_line(points[2]).extend_with_line(points[3]) boundary = G.Path.line(points[4], points[5])\ - .line_to(points[6]).line_to(points[7]) + .extend_with_line(points[6]).extend_with_line(points[7]) if symmetry.is_3d(): inner = inner.revolve_z() diff --git a/validation/einzel_lens.py b/validation/einzel_lens.py index 344d9d3..b11f5b5 100644 --- a/validation/einzel_lens.py +++ b/validation/einzel_lens.py @@ -4,13 +4,13 @@ import traceon.solver as S import traceon.excitation as E import traceon.plotting as P -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial import traceon.tracing as T from validation import Validation try: - from traceon_pro.interpolation import Field3DAxial + from traceon_pro.field import Field3DAxial except ImportError: Field3DAxial = None @@ -28,7 +28,7 @@ def __init__(self): def create_mesh(self, MSF, symmetry, higher_order): boundary = G.Path.line([0., 0., 1.75], [2.0, 0., 1.75])\ - .line_to([2.0, 0., -1.75]).line_to([0., 0., -1.75]) + .extend_with_line([2.0, 0., -1.75]).extend_with_line([0., 0., -1.75]) margin_right = 0.1 diff --git a/validation/magnetic_einzel_lens.py b/validation/magnetic_einzel_lens.py index 63f3e36..5a32633 100644 --- a/validation/magnetic_einzel_lens.py +++ b/validation/magnetic_einzel_lens.py @@ -7,12 +7,12 @@ import traceon.excitation as E import traceon.plotting as P import traceon.tracing as T -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial from validation import Validation try: - from traceon_pro.interpolation import Field3DAxial + from traceon_pro.field import Field3DAxial except ImportError: Field3DAxial = None @@ -30,7 +30,7 @@ def __init__(self): def create_mesh(self, MSF, symmetry, higher_order): boundary = G.Path.line([0., 0., 1.75], [2.0, 0., 1.75])\ - .line_to([2.0, 0., -1.75]).line_to([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 diff --git a/validation/rectangular_coil.py b/validation/rectangular_coil.py index 0c1dd85..fff4c34 100644 --- a/validation/rectangular_coil.py +++ b/validation/rectangular_coil.py @@ -18,7 +18,7 @@ def __init__(self): self.plot_colors = dict(coil='red', boundary='purple') def create_mesh(self, MSF, symmetry, higher_order): - boundary = G.Path.line([0., 0., 5.], [5., 0., 5.]).line_to([5., 0., 0]).line_to([0., 0., 0.]); + boundary = G.Path.line([0., 0., 5.], [5., 0., 5.]).extend_with_line([5., 0., 0]).extend_with_line([0., 0., 0.]); boundary.name = 'boundary' mesh1 = boundary.mesh(mesh_size_factor=MSF) diff --git a/validation/rectangular_coil_with_circle.py b/validation/rectangular_coil_with_circle.py index ea2f3ed..a640420 100644 --- a/validation/rectangular_coil_with_circle.py +++ b/validation/rectangular_coil_with_circle.py @@ -18,7 +18,7 @@ def __init__(self): self.plot_colors = dict(coil='red', boundary='purple') def create_mesh(self, MSF, symmetry, higher_order): - boundary = G.Path.line([0., 0., 5.], [5., 0., 5.]).line_to([5., 0., 0]).line_to([0., 0., 0.]); + boundary = G.Path.line([0., 0., 5.], [5., 0., 5.]).extend_with_line([5., 0., 0]).extend_with_line([0., 0., 0.]); boundary.name = 'boundary' circle = G.Path.circle_xz(2.5, 4, 0.5) diff --git a/validation/simple_mirror.py b/validation/simple_mirror.py index badb738..33c65fe 100644 --- a/validation/simple_mirror.py +++ b/validation/simple_mirror.py @@ -7,12 +7,12 @@ import traceon.excitation as E import traceon.tracing as T import traceon.solver as S -from traceon.interpolation import FieldRadialAxial +from traceon.field import FieldRadialAxial from validation import Validation try: - from traceon_pro.interpolation import Field3DAxial + from traceon_pro.field import Field3DAxial except ImportError: Field3DAxial = None @@ -30,7 +30,7 @@ def default_MSF(self, symmetry): return [4, 8, 16, 32] def create_mesh(self, MSF, symmetry, higher_order): - boundary = G.Path.line([0, 0, -1], [2, 0, -1]).line_to([2, 0, 1]).line_to([0.3, 0., 1]) + boundary = G.Path.line([0, 0, -1], [2, 0, -1]).extend_with_line([2, 0, 1]).extend_with_line([0.3, 0., 1]) mirror = G.Path.line([0., 0., 0.], [1., 0., 0.]) boundary.name = 'boundary' diff --git a/validation/spherical_capacitor.py b/validation/spherical_capacitor.py index 120d8d6..327d6ae 100644 --- a/validation/spherical_capacitor.py +++ b/validation/spherical_capacitor.py @@ -32,7 +32,7 @@ def create_mesh(self, MSF, symmetry, higher_order): 1999. """ def add_shell(radius, name): - arc = G.Path.arc([0., 0., 0.], [0, 0, -radius], [radius, 0, 0.]).arc_to([0., 0., 0.], [0., 0., radius]) + arc = G.Path.arc([0., 0., 0.], [0, 0, -radius], [radius, 0, 0.]).extend_with_arc([0., 0., 0.], [0., 0., radius]) arc.name = name if symmetry.is_3d(): diff --git a/validation/two_current_coils.py b/validation/two_current_coils.py index 13af04d..f9ddc01 100644 --- a/validation/two_current_coils.py +++ b/validation/two_current_coils.py @@ -17,7 +17,7 @@ def __init__(self): self.plot_colors = dict(coil1='blue', coil2='red', block='orange') def create_mesh(self, MSF, symmetry, higher_order): - boundary = G.Path.line([0., 0., 50e-3], [100e-3, 0., 50e-3]).line_to([100e-3, 0., -50e-3]).line_to([0., 0., -50e-3]) + boundary = G.Path.line([0., 0., 50e-3], [100e-3, 0., 50e-3]).extend_with_line([100e-3, 0., -50e-3]).extend_with_line([0., 0., -50e-3]) boundary.name = 'boundary' coil1 = G.Surface.disk_xz(10e-3, 5e-3, 1e-3) diff --git a/validation/two_cylinder_edwards.py b/validation/two_cylinder_edwards.py index 2a94301..a550693 100644 --- a/validation/two_cylinder_edwards.py +++ b/validation/two_cylinder_edwards.py @@ -30,9 +30,9 @@ def __init__(self): def create_mesh(self, MSF, symmetry, higher_order): cylinder_length = (boundary_length - gap_size)/2 - bottom = G.Path.line([0., 0., 0.], [R, 0., 0.]).line_to([R, 0., cylinder_length]) + bottom = G.Path.line([0., 0., 0.], [R, 0., 0.]).extend_with_line([R, 0., cylinder_length]) gap = G.Path.line([R, 0., cylinder_length], [R, 0., cylinder_length+gap_size]) - top = G.Path.line([R, 0., cylinder_length+gap_size], [R, 0., boundary_length]).line_to([0., 0., boundary_length]) + top = G.Path.line([R, 0., cylinder_length+gap_size], [R, 0., boundary_length]).extend_with_line([0., 0., boundary_length]) bottom.name = 'v1' gap.name = 'gap' From 313136b08999d21dab151152a220a55d5a0d667b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Fri, 27 Dec 2024 16:22:07 +0100 Subject: [PATCH 45/85] Remove Field3D_BEM class and potential_3d function --- traceon/backend/__init__.py | 14 ---- traceon/backend/three_d.c | 29 -------- traceon/backend/three_d_point.c | 6 -- traceon/field.py | 125 -------------------------------- 4 files changed, 174 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 7b8fa8a..12249a3 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -263,11 +263,9 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dy1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'dz1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), - 'potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'axial_coefficients_3d': (None, charges_3d, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), arr(ndim=3), sz, z_values, arr(ndim=4), sz), 'fill_jacobian_buffer_current_three_d': (None, lines, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), sz), 'current_field_3d': (None, v3, EffectivePointCurrents3D, v3), - 'potential_3d': (dbl, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'field_3d_derivs': (None, v3, v3, z_values, arr(ndim=5), sz), @@ -525,9 +523,6 @@ def dy1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float def dz1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: return backend_lib.dz1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) -def potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: - return backend_lib.potential_3d_point(x0, y0, z0, x1, y1, z1, None) - def flux_density_to_charge_factor(K: float) -> float: return backend_lib.flux_density_to_charge_factor(K) @@ -565,15 +560,6 @@ def current_field_3d(point, eff): backend_lib.current_field_3d(point, eff, result) return result -def potential_3d(point: np.ndarray, charges: np.ndarray, jac_buffer: np.ndarray, pos_buffer: np.ndarray) -> float: - assert point.shape == (3,) - N = len(charges) - assert charges.shape == (N,) - assert jac_buffer.shape == (N, N_TRIANGLE_QUAD) - assert pos_buffer.shape == (N, N_TRIANGLE_QUAD, 3) - - return backend_lib.potential_3d(point.astype(np.float64), charges, jac_buffer, pos_buffer, N) - def potential_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert point.shape == (3,) diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index 761afef..024d14c 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -100,35 +100,6 @@ current_field_3d(double point[3], struct effective_point_currents_3d epc, double } -EXPORT double -potential_3d(double point[3], double *charges, jacobian_buffer_3d jacobian_buffer, position_buffer_3d position_buffer, size_t N_vertices) { - - double sum_ = 0.0; - - for(int i = 0; i < N_vertices; i++) { - for(int k = 0; k < N_TRIANGLE_QUAD; k++) { - double *pos = &position_buffer[i][k][0]; - double potential = potential_3d_point(point[0], point[1], point[2], pos[0], pos[1], pos[2], NULL); - - sum_ += charges[i] * jacobian_buffer[i][k] * potential; - } - } - - return sum_; -} - -double -field_dot_normal_3d(double x0, double y0, double z0, double x, double y, double z, void* normal_p) { - - double Ex = -dx1_potential_3d_point(x0, y0, z0, x, y, z, NULL); - double Ey = -dy1_potential_3d_point(x0, y0, z0, x, y, z, NULL); - double Ez = -dz1_potential_3d_point(x0, y0, z0, x, y, z, NULL); - - double *normal = (double *)normal_p; - - return normal[0]*Ex + normal[1]*Ey + normal[2]*Ez; -} - EXPORT void field_3d(double point[3], double result[3], double *charges, jacobian_buffer_3d jacobian_buffer, position_buffer_3d position_buffer, size_t N_vertices) { diff --git a/traceon/backend/three_d_point.c b/traceon/backend/three_d_point.c index 37aa84a..26ea4a8 100644 --- a/traceon/backend/three_d_point.c +++ b/traceon/backend/three_d_point.c @@ -1,10 +1,4 @@ - -INLINE double potential_3d_point(double x0, double y0, double z0, double x, double y, double z, void *_) { - double r = norm_3d(x-x0, y-y0, z-z0); - return 1/(4*M_PI*r); -} - EXPORT double dx1_potential_3d_point(double x0, double y0, double z0, double x, double y, double z, void *_) { double r = norm_3d(x-x0, y-y0, z-z0); return (x-x0)/(4*M_PI*pow(r, 3)); diff --git a/traceon/field.py b/traceon/field.py index daa5e57..6ce55b6 100644 --- a/traceon/field.py +++ b/traceon/field.py @@ -460,131 +460,6 @@ def get_low_level_trace_function(self): return backend.field_fun(("field_radial_traceable", backend.backend_lib)), args - -class Field3D_BEM(FieldBEM): - """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`.""" - - 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_3d() - if magnetostatic_point_charges is None: - magnetostatic_point_charges = EffectivePointCharges.empty_3d() - if current_point_charges is None: - current_point_charges = EffectivePointCharges.empty_line_3d() - - super().__init__(electrostatic_point_charges, magnetostatic_point_charges, current_point_charges) - - self.symmetry = E.Symmetry.THREE_D - - for eff in [electrostatic_point_charges, magnetostatic_point_charges]: - N = len(eff.charges) - assert eff.charges.shape == (N,) - assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD) - assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3) - - def current_field_at_point(self, point_): - point = np.array(point_, dtype=np.double) - eff = self.current_point_charges - return backend.current_field_3d(point, eff) - - 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 - """ - 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_3d(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_3d(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_3d(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_3d(point, charges, jacobians, positions) - - def area_of_element(self, i): - jacobians = self.electrostatic_point_charges.jacobians - return np.sum(jacobians[i]) - - def get_tracer(self, bounds): - return T.Tracer(self, bounds) - - def get_low_level_trace_function(self): - args = backend.FieldEvaluationArgs3D(self.electrostatic_point_charges, self.magnetostatic_point_charges, self.current_point_charges, self.field_bounds) - return backend.field_fun(("field_3d_traceable", backend.backend_lib)), args - FACTOR_AXIAL_DERIV_SAMPLING_2D = 0.2 class FieldAxial(Field, ABC): From 8faca5b15cacde02e9460cfdf147ee87e850cb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 29 Dec 2024 17:04:49 +0100 Subject: [PATCH 46/85] Prevent Numpy/ctypes memory reclamation by holding references in backend --- traceon/backend/__init__.py | 82 ++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 12249a3..621ee07 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -115,10 +115,16 @@ class EffectivePointCharges2D(C.Structure): def __init__(self, eff, *args, **kwargs): assert eff.is_2d() super(EffectivePointCharges2D, self).__init__(*args, **kwargs) + + # Beware, we need to keep references to the arrays pointed to by the C.Structure + # otherwise, they are garbage collected and bad things happen + self.charges_arr = ensure_contiguous_aligned(eff.charges) + self.jacobians_arr = ensure_contiguous_aligned(eff.jacobians) + self.positions_arr = ensure_contiguous_aligned(eff.positions) - self.charges = ensure_contiguous_aligned(eff.charges).ctypes.data_as(dbl_p) - self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) - self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) + self.charges = self.charges_arr.ctypes.data_as(dbl_p) + self.jacobians = self.jacobians_arr.ctypes.data_as(dbl_p) + self.positions = self.positions_arr.ctypes.data_as(dbl_p) self.N = len(eff) class EffectivePointCharges3D(C.Structure): @@ -132,10 +138,16 @@ class EffectivePointCharges3D(C.Structure): def __init__(self, eff, *args, **kwargs): assert eff.is_3d() super().__init__(*args, **kwargs) - - self.charges = ensure_contiguous_aligned(eff.charges).ctypes.data_as(dbl_p) - self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) - self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) + + # Beware, we need to keep references to the arrays pointed to by the C.Structure + # otherwise, they are garbage collected and bad things happen + self.charges_arr = ensure_contiguous_aligned(eff.charges) + self.jacobians_arr = ensure_contiguous_aligned(eff.jacobians) + self.positions_arr = ensure_contiguous_aligned(eff.positions) + + self.charges = self.charges_arr.ctypes.data_as(dbl_p) + self.jacobians = self.jacobians_arr.ctypes.data_as(dbl_p) + self.positions = self.positions_arr.ctypes.data_as(dbl_p) self.N = len(eff) class EffectivePointCurrents3D(C.Structure): @@ -159,11 +171,19 @@ def __init__(self, eff, *args, **kwargs): assert eff.jacobians.shape == (N, N_QUAD_2D) and eff.jacobians.dtype == np.double assert eff.positions.shape == (N, N_QUAD_2D, 3) and eff.positions.dtype == np.double assert eff.directions.shape == (N, N_QUAD_2D, 3) and eff.directions.dtype == np.double - - self.currents = ensure_contiguous_aligned(currents).ctypes.data_as(dbl_p) - self.jacobians = ensure_contiguous_aligned(eff.jacobians).ctypes.data_as(dbl_p) - self.positions = ensure_contiguous_aligned(eff.positions).ctypes.data_as(dbl_p) - self.directions = ensure_contiguous_aligned(eff.directions).ctypes.data_as(dbl_p) + + # Beware, we need to keep references to the arrays pointed to by the C.Structure + # otherwise, they are garbage collected and bad things happen + self.currents_arr = ensure_contiguous_aligned(currents) + self.jacobians_arr = ensure_contiguous_aligned(eff.jacobians) + self.positions_arr = ensure_contiguous_aligned(eff.positions) + self.directions_arr = ensure_contiguous_aligned(eff.directions) + + self.currents = self.currents_arr.ctypes.data_as(dbl_p) + self.jacobians = self.jacobians_arr.ctypes.data_as(dbl_p) + self.positions = self.positions_arr.ctypes.data_as(dbl_p) + self.directions = self.directions_arr.ctypes.data_as(dbl_p) + self.N = N @@ -179,14 +199,21 @@ def __init__(self, elec, mag, current, bounds, *args, **kwargs): super().__init__(*args, **kwargs) assert bounds is None or bounds.shape == (3, 2) - self.elec_charges = C.cast(C.pointer(EffectivePointCharges2D(elec)), C.c_void_p) - self.mag_charges = C.cast(C.pointer(EffectivePointCharges2D(mag)), C.c_void_p) - self.current_charges = C.cast(C.pointer(EffectivePointCharges3D(current)), C.c_void_p) + # Beware, we need to keep references to the arrays pointed to by the C.Structure + # otherwise, they are garbage collected and bad things happen + self.eff_elec = EffectivePointCharges2D(elec) + self.eff_mag = EffectivePointCharges2D(mag) + self.eff_current = EffectivePointCharges3D(current) + + self.elec_charges = C.cast(C.pointer(self.eff_elec), C.c_void_p) + self.mag_charges = C.cast(C.pointer(self.eff_mag), C.c_void_p) + self.current_charges = C.cast(C.pointer(self.eff_current), C.c_void_p) if bounds is None: self.bounds = None else: - self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) + self.bounds_arr = ensure_contiguous_aligned(bounds) + self.bounds = self.bounds_arr.ctypes.data_as(dbl_p) class FieldEvaluationArgs3D(C.Structure): _fields_ = [ @@ -200,14 +227,19 @@ def __init__(self, elec, mag, currents, bounds, *args, **kwargs): super().__init__(*args, **kwargs) assert bounds is None or bounds.shape == (3, 2) - self.elec_charges = C.cast(C.pointer(EffectivePointCharges3D(elec)), C.c_void_p) - self.mag_charges = C.cast(C.pointer(EffectivePointCharges3D(mag)), C.c_void_p) - self.current_charges = C.cast(C.pointer(EffectivePointCurrents3D(currents)), C.c_void_p) + self.eff_elec = EffectivePointCharges3D(elec) + self.eff_mag = EffectivePointCharges3D(mag) + self.eff_current = EffectivePointCurrents3D(currents) + + self.elec_charges = C.cast(C.pointer(self.eff_elec), C.c_void_p) + self.mag_charges = C.cast(C.pointer(self.eff_mag), C.c_void_p) + self.current_charges = C.cast(C.pointer(self.eff_current), C.c_void_p) if bounds is None: self.bounds = None else: - self.bounds = ensure_contiguous_aligned(bounds).ctypes.data_as(dbl_p) + self.bounds_arr = ensure_contiguous_aligned(bounds) + self.bounds = self.bounds_arr.ctypes.data_as(dbl_p) @@ -224,10 +256,14 @@ def __init__(self, z, elec, mag, *args, **kwargs): assert z.shape == (len(z),) assert elec.shape[0] == len(z)-1 assert mag.shape[0] == len(z)-1 + + self.z_arr = ensure_contiguous_aligned(z) + self.elec_arr = ensure_contiguous_aligned(elec) + self.mag_arr = ensure_contiguous_aligned(mag) - self.z_interpolation = ensure_contiguous_aligned(z).ctypes.data_as(dbl_p) - self.electrostatic_axial_coeffs = ensure_contiguous_aligned(elec).ctypes.data_as(dbl_p) - self.magnetostatic_axial_coeffs = ensure_contiguous_aligned(mag).ctypes.data_as(dbl_p) + self.z_interpolation = self.z_arr.ctypes.data_as(dbl_p) + self.electrostatic_axial_coeffs = self.elec_arr.ctypes.data_as(dbl_p) + self.magnetostatic_axial_coeffs = self.mag_arr.ctypes.data_as(dbl_p) self.N_z = len(z) bounds = arr(shape=(3, 2)) From 4969db2af496adb5b2e1ab6d8ddc4a09a945edbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 29 Dec 2024 17:38:09 +0100 Subject: [PATCH 47/85] Remove field_3d and current_3d from backend/__init__.py --- traceon/backend/__init__.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 621ee07..8694818 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -296,14 +296,9 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'charge_radial': (dbl, arr(ndim=2), dbl), 'field_radial': (None, v3, v3, charges_2d, jac_buffer_2d, pos_buffer_2d, sz), 'field_radial_derivs': (None, v3, v3, z_values, arr(ndim=3), sz), - 'dx1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), - 'dy1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), - 'dz1_potential_3d_point': (dbl, dbl, dbl, dbl, dbl, dbl, dbl, vp), 'axial_coefficients_3d': (None, charges_3d, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), arr(ndim=3), sz, z_values, arr(ndim=4), sz), 'fill_jacobian_buffer_current_three_d': (None, lines, jac_buffer_3d, pos_buffer_3d, arr(ndim=3), sz), - 'current_field_3d': (None, v3, EffectivePointCurrents3D, v3), 'potential_3d_derivs': (dbl, v3, z_values, arr(ndim=5), sz), - 'field_3d': (None, v3, v3, charges_3d, jac_buffer_3d, pos_buffer_3d, sz), 'field_3d_derivs': (None, v3, v3, z_values, arr(ndim=5), sz), 'current_potential_axial_radial_ring': (dbl, dbl, dbl, dbl), 'current_potential_axial': (dbl, dbl, currents_2d, jac_buffer_3d, pos_buffer_3d, sz), @@ -550,15 +545,6 @@ def field_radial_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> backend_lib.field_radial_derivs(point.astype(np.float64), field, z, coeffs, len(z)) return field -def dx1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: - return backend_lib.dx1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) - -def dy1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: - return backend_lib.dy1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) - -def dz1_potential_3d_point(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float) -> float: - return backend_lib.dz1_potential_3d_point(x0, y0, z0, x1, y1, z1, None) - def flux_density_to_charge_factor(K: float) -> float: return backend_lib.flux_density_to_charge_factor(K) @@ -587,31 +573,12 @@ def fill_jacobian_buffer_current_three_d(lines): return jacobians, positions, directions -def current_field_3d(point, eff): - assert point.shape == (3,) - - eff = EffectivePointCurrents3D(eff) - - result = np.zeros(3) - backend_lib.current_field_3d(point, eff, result) - return result - def potential_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> float: assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) assert point.shape == (3,) return backend_lib.potential_3d_derivs(point.astype(np.float64), z, coeffs, len(z)) -def field_3d(point: np.ndarray, charges: np.ndarray, jacobian_buffer: np.ndarray, position_buffer: np.ndarray) -> np.ndarray: - N = len(charges) - assert point.shape == (3,) - assert jacobian_buffer.shape == (N, N_TRIANGLE_QUAD) - assert position_buffer.shape == (N, N_TRIANGLE_QUAD, 3) - - field = np.zeros( (3,) ) - backend_lib.field_3d(point.astype(np.float64), field, charges, jacobian_buffer, position_buffer, N) - return field - def field_3d_derivs(point: np.ndarray, z: np.ndarray, coeffs: np.ndarray) -> np.ndarray: assert point.shape == (3,) assert coeffs.shape == (len(z)-1, 2, NU_MAX, M_MAX, 4) From 4b6f61cddca6ea0a8b9311f1e61607a179b6f43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 17:45:26 +0100 Subject: [PATCH 48/85] Small improvement of interface between Tracer and Field --- traceon/tracing.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/traceon/tracing.py b/traceon/tracing.py index 7d04f5d..da2ac3b 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -118,9 +118,19 @@ def __init__(self, field, bounds): assert bounds.shape == (3,2) self.bounds = bounds - self.trace_fun, args = field.get_low_level_trace_function() + 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 - self.trace_args = args if args is None else ctypes.cast(ctypes.pointer(args), ctypes.c_void_p) + 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__ From f9e65acfcdc267b06407d4cd734105895f92a919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 17:50:02 +0100 Subject: [PATCH 49/85] Remove field_3d and related functions --- traceon/backend/three_d.c | 66 ------------------------------- traceon/backend/three_d_point.c | 17 -------- traceon/backend/traceon-backend.c | 1 - traceon/backend/tracing.c | 35 ---------------- 4 files changed, 119 deletions(-) delete mode 100644 traceon/backend/three_d_point.c diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index 024d14c..4ecf22e 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -22,15 +22,6 @@ struct effective_point_charges_3d { size_t N; }; -struct effective_point_currents_3d { - double *currents; - double (*jacobians)[N_QUAD_2D]; - double (*positions)[N_QUAD_2D][3]; - double (*directions)[N_QUAD_2D][3]; - size_t N; -}; - - struct field_derivs_args { double *z_interpolation; @@ -69,63 +60,6 @@ fill_jacobian_buffer_current_three_d( } } -EXPORT void -current_field_3d(double point[3], struct effective_point_currents_3d epc, double field_out[3]) { - // Use Biot-Savart law - - double field_sum[3] = {0.0, 0.0, 0.0}; - - for (size_t i = 0; i < epc.N; i++) - for (int k = 0; k < N_QUAD_2D; k++) { - double r_prime[3]; - r_prime[0] = point[0] - epc.positions[i][k][0]; - r_prime[1] = point[1] - epc.positions[i][k][1]; - r_prime[2] = point[2] - epc.positions[i][k][2]; - - double r_norm = norm_3d(r_prime[0], r_prime[1], r_prime[2]); - - double cross[3]; - cross_product_3d(epc.directions[i][k], r_prime, cross); - - double factor = (epc.currents[i] * epc.jacobians[i][k]) / (4.0 * M_PI * (r_norm * r_norm * r_norm)); - - field_sum[0] += factor * cross[0]; - field_sum[1] += factor * cross[1]; - field_sum[2] += factor * cross[2]; - } - - field_out[0] = field_sum[0]; - field_out[1] = field_sum[1]; - field_out[2] = field_sum[2]; -} - - -EXPORT void -field_3d(double point[3], double result[3], double *charges, - jacobian_buffer_3d jacobian_buffer, position_buffer_3d position_buffer, size_t N_vertices) { - - double Ex = 0.0, Ey = 0.0, Ez = 0.0; - - for(int i = 0; i < N_vertices; i++) { - for(int k = 0; k < N_TRIANGLE_QUAD; k++) { - double *pos = &position_buffer[i][k][0]; - double field_x = dx1_potential_3d_point(point[0], point[1], point[2], pos[0], pos[1], pos[2], NULL); - double field_y = dy1_potential_3d_point(point[0], point[1], point[2], pos[0], pos[1], pos[2], NULL); - double field_z = dz1_potential_3d_point(point[0], point[1], point[2], pos[0], pos[1], pos[2], NULL); - - Ex -= charges[i] * jacobian_buffer[i][k] * field_x; - Ey -= charges[i] * jacobian_buffer[i][k] * field_y; - Ez -= charges[i] * jacobian_buffer[i][k] * field_z; - } - } - - result[0] = Ex; - result[1] = Ey; - result[2] = Ez; -} - - - EXPORT void axial_coefficients_3d(double *restrict charges, jacobian_buffer_3d restrict jacobian_buffer, diff --git a/traceon/backend/three_d_point.c b/traceon/backend/three_d_point.c deleted file mode 100644 index 26ea4a8..0000000 --- a/traceon/backend/three_d_point.c +++ /dev/null @@ -1,17 +0,0 @@ - -EXPORT double dx1_potential_3d_point(double x0, double y0, double z0, double x, double y, double z, void *_) { - double r = norm_3d(x-x0, y-y0, z-z0); - return (x-x0)/(4*M_PI*pow(r, 3)); -} - -EXPORT double dy1_potential_3d_point(double x0, double y0, double z0, double x, double y, double z, void *_) { - double r = norm_3d(x-x0, y-y0, z-z0); - return (y-y0)/(4*M_PI*pow(r, 3)); -} - -EXPORT double dz1_potential_3d_point(double x0, double y0, double z0, double x, double y, double z, void *_) { - double r = norm_3d(x-x0, y-y0, z-z0); - return (z-z0)/(4*M_PI*pow(r, 3)); -} - - diff --git a/traceon/backend/traceon-backend.c b/traceon/backend/traceon-backend.c index 1953147..906c7e9 100644 --- a/traceon/backend/traceon-backend.c +++ b/traceon/backend/traceon-backend.c @@ -13,7 +13,6 @@ #include "utilities_3d.c" #include "triangle.c" -#include "three_d_point.c" #include "three_d.c" #include "radial_ring.c" diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index e4331df..e7f9904 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -172,41 +172,6 @@ field_radial_derivs_traceable(double position[3], double velocity[3], void *args field_radial_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } -EXPORT void -field_3d_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { - - struct field_evaluation_args *args = (struct field_evaluation_args*)args_p; - struct effective_point_charges_3d *elec_charges = (struct effective_point_charges_3d*) args->elec_charges; - struct effective_point_charges_3d *mag_charges = (struct effective_point_charges_3d*) args->mag_charges; - struct effective_point_currents_3d *currents = (struct effective_point_currents_3d*) args->currents; - - double (*bounds)[2] = (double (*)[2]) args->bounds; - - if( bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) - && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]) - && (bounds[2][0] < position[2]) && (position[2] < bounds[2][1])) ) { - - field_3d(position, elec_out, elec_charges->charges, elec_charges->jacobians, elec_charges->positions, elec_charges->N); - field_3d(position, mag_out, mag_charges->charges, mag_charges->jacobians, mag_charges->positions, mag_charges->N); - - double curr_field[3] = {0.}; - current_field_3d(position, *currents, curr_field); - - mag_out[0] += curr_field[0]; - mag_out[1] += curr_field[1]; - mag_out[2] += curr_field[2]; - } - else { - elec_out[0] = 0.0; - elec_out[1] = 0.0; - elec_out[2] = 0.0; - - mag_out[0] = 0.0; - mag_out[1] = 0.0; - mag_out[2] = 0.0; - } -} - void field_3d_derivs_traceable(double position[3], double velocity[3], void *args_p, double elec_out[3], double mag_out[3]) { struct field_derivs_args *args = (struct field_derivs_args*) args_p; From bc7a3cfb1f3cacffcefcc086555f375f227e34ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 17:51:54 +0100 Subject: [PATCH 50/85] Move fill_jacobian_buffer_3d from tracing.c to three_d.c --- traceon/backend/three_d.c | 32 ++++++++++++++++++++++++++++++++ traceon/backend/tracing.c | 30 ------------------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/traceon/backend/three_d.c b/traceon/backend/three_d.c index 4ecf22e..3c4c76f 100644 --- a/traceon/backend/three_d.c +++ b/traceon/backend/three_d.c @@ -30,6 +30,38 @@ struct field_derivs_args { size_t N_z; }; + +EXPORT void fill_jacobian_buffer_3d( + jacobian_buffer_3d jacobian_buffer, + position_buffer_3d pos_buffer, + vertices_3d t, + size_t N_triangles) { + + for(int i = 0; i < N_triangles; i++) { + + double x1 = t[i][0][0], y1 = t[i][0][1], z1 = t[i][0][2]; + double x2 = t[i][1][0], y2 = t[i][1][1], z2 = t[i][1][2]; + double x3 = t[i][2][0], y3 = t[i][2][1], z3 = t[i][2][2]; + + double area = 0.5*sqrt( + pow((y2-y1)*(z3-z1)-(y3-y1)*(z2-z1), 2) + + pow((x3-x1)*(z2-z1)-(x2-x1)*(z3-z1), 2) + + pow((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1), 2)); + + for (int k=0; k < N_TRIANGLE_QUAD; k++) { + double b1_ = QUAD_B1[k]; + double b2_ = QUAD_B2[k]; + double w = QUAD_WEIGHTS[k]; + + jacobian_buffer[i][k] = 2 * w * area; + pos_buffer[i][k][0] = x1 + b1_*(x2 - x1) + b2_*(x3 - x1); + pos_buffer[i][k][1] = y1 + b1_*(y2 - y1) + b2_*(y3 - y1); + pos_buffer[i][k][2] = z1 + b1_*(z2 - z1) + b2_*(z3 - z1); + } + } +} + + EXPORT void fill_jacobian_buffer_current_three_d( double (*line_points)[2][3], diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index e7f9904..9b47acf 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -180,36 +180,6 @@ field_3d_derivs_traceable(double position[3], double velocity[3], void *args_p, field_3d_derivs(position, mag_out, args->z_interpolation, args->magnetostatic_axial_coeffs, args->N_z); } -EXPORT void fill_jacobian_buffer_3d( - jacobian_buffer_3d jacobian_buffer, - position_buffer_3d pos_buffer, - vertices_3d t, - size_t N_triangles) { - - for(int i = 0; i < N_triangles; i++) { - - double x1 = t[i][0][0], y1 = t[i][0][1], z1 = t[i][0][2]; - double x2 = t[i][1][0], y2 = t[i][1][1], z2 = t[i][1][2]; - double x3 = t[i][2][0], y3 = t[i][2][1], z3 = t[i][2][2]; - - double area = 0.5*sqrt( - pow((y2-y1)*(z3-z1)-(y3-y1)*(z2-z1), 2) + - pow((x3-x1)*(z2-z1)-(x2-x1)*(z3-z1), 2) + - pow((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1), 2)); - - for (int k=0; k < N_TRIANGLE_QUAD; k++) { - double b1_ = QUAD_B1[k]; - double b2_ = QUAD_B2[k]; - double w = QUAD_WEIGHTS[k]; - - jacobian_buffer[i][k] = 2 * w * area; - pos_buffer[i][k][0] = x1 + b1_*(x2 - x1) + b2_*(x3 - x1); - pos_buffer[i][k][1] = y1 + b1_*(y2 - y1) + b2_*(y3 - y1); - pos_buffer[i][k][2] = z1 + b1_*(z2 - z1) + b2_*(z3 - z1); - } - } -} - EXPORT bool plane_intersection(double p0[3], double normal[3], positions_3d positions, size_t N_p, double result[6]) { From 4a6fb41eaaa09c19cde7506ee21b1ac7c756f1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 18:58:23 +0100 Subject: [PATCH 51/85] Fix examples (import solve_direct from traceon_pro) --- traceon/backend/__init__.py | 3 +++ validation/validation.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 8694818..9ca7ff7 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -549,6 +549,9 @@ def flux_density_to_charge_factor(K: float) -> float: return backend_lib.flux_density_to_charge_factor(K) def axial_coefficients_3d(charges: np.ndarray, jacobian_buffer: np.ndarray, pos_buffer: np.ndarray, z: np.ndarray) -> np.ndarray: + if len(charges) == 0: + return np.zeros( (len(z), 2, NU_MAX, M_MAX) ) + assert jacobian_buffer.shape == (len(charges), N_TRIANGLE_QUAD) assert pos_buffer.shape == (len(charges), N_TRIANGLE_QUAD, 3) diff --git a/validation/validation.py b/validation/validation.py index 3f434db..63f4975 100644 --- a/validation/validation.py +++ b/validation/validation.py @@ -5,11 +5,12 @@ import traceon.geometry as G import traceon.excitation as E -import traceon.solver as S +from traceon.solver import solve_direct import traceon.plotting as P try: import traceon_pro.solver + from traceon_pro.solver import solve_direct except ImportError: traceon_pro = None @@ -159,7 +160,7 @@ def compute_field(self, geometry, symmetry, use_fmm): assert traceon_pro is not None, "traceon_pro should be installed to use fast multipole method" return exc, traceon_pro.solver.solve_fmm(exc, l_max=self.args.fmm_precision) else: - return exc, S.solve_direct(exc) + return exc, solve_direct(exc) def compute_value_of_interest(self, geometry, field): pass From 1451ba82bac222c213a43b3c1dc6f042617c426f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 20:19:23 +0100 Subject: [PATCH 52/85] Throw better errors when Excitation is applied to wrong mesh types (lines/triangles) --- traceon/excitation.py | 53 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index ea912ee..c67a257 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -19,6 +19,8 @@ import numpy as np from .backend import N_QUAD_2D +from .logging import log_error +from . import excitation as E class Symmetry(IntEnum): """Symmetry to be used for solver. Used when deciding which formulas to use in the Boundary Element Method. The currently @@ -93,6 +95,14 @@ def __str__(self): return f'' + + 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): """ @@ -108,7 +118,12 @@ def add_voltage(self, **kwargs): """ for name, voltage in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + + 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): @@ -130,11 +145,11 @@ def add_current(self, **kwargs): """ if self.symmetry == Symmetry.RADIAL: for name, current in kwargs.items(): - assert name in self.mesh.physical_to_triangles.keys(), "Current should be applied to triangles in radial symmetry" + 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(): - assert name in self.mesh.physical_to_lines.keys(), "Current should be applied to lines in 3D symmetry" + 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') @@ -162,7 +177,11 @@ def add_magnetostatic_potential(self, **kwargs): 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(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -178,7 +197,11 @@ def add_magnetizable(self, **kwargs): """ for name, permeability in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -193,7 +216,11 @@ def add_dielectric(self, **kwargs): """ for name, permittivity in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -212,6 +239,12 @@ def add_electrostatic_boundary(self, *args, ensure_inward_normals=True): 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): @@ -230,7 +263,13 @@ def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True): 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): From 339924f57d9597a926bc53ab5052e7b49bf086d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 20:29:20 +0100 Subject: [PATCH 53/85] Convert field bounds to float64 in field.py --- traceon/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traceon/field.py b/traceon/field.py index 6ce55b6..7ab047e 100644 --- a/traceon/field.py +++ b/traceon/field.py @@ -176,7 +176,7 @@ def set_bounds(self, bounds): 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) + self.field_bounds = np.array(bounds, dtype=np.float64) assert self.field_bounds.shape == (3,2) def is_electrostatic(self): From 32c35834eba671b78c4e9a2cdc194feab3111a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 20:29:34 +0100 Subject: [PATCH 54/85] Also check for Z field bounds in tracing.c --- traceon/backend/tracing.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/traceon/backend/tracing.c b/traceon/backend/tracing.c index 9b47acf..4bb9d97 100644 --- a/traceon/backend/tracing.c +++ b/traceon/backend/tracing.c @@ -134,7 +134,8 @@ field_radial_traceable(double position[3], double velocity[3], void *args_p, dou double (*bounds)[2] = (double (*)[2]) args->bounds; if(args->bounds == NULL || ((bounds[0][0] < position[0]) && (position[0] < bounds[0][1]) - && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]))) { + && (bounds[1][0] < position[1]) && (position[1] < bounds[1][1]) + && (bounds[2][0] < position[2]) && (position[2] < bounds[2][1]))) { field_radial(position, elec_out, From c281ad6579c6a2932b0e8b067893acc76f44d87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 20:48:41 +0100 Subject: [PATCH 55/85] Increase width of sidebar, to prevent wrapping of text --- generate_docs/templates/css.mako | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generate_docs/templates/css.mako b/generate_docs/templates/css.mako index e3d4bcc..25dd97e 100644 --- a/generate_docs/templates/css.mako +++ b/generate_docs/templates/css.mako @@ -309,7 +309,8 @@ <%def name="desktop()" filter="minify_css"> @media screen and (min-width: 700px) { #sidebar { - width: 25%; + width: 29%; + min-width:525px; height: 100vh; overflow: auto; position: sticky; From 6f5385f65a817a09f3b580698da0d33789499445 Mon Sep 17 00:00:00 2001 From: jobdewitte Date: Mon, 30 Dec 2024 21:10:11 +0100 Subject: [PATCH 56/85] use traceon_pro solver in 3d examples --- examples/deflector-3d.py | 9 ++++++++- examples/dohi-mirror.py | 9 +++++++-- examples/stigmator.py | 13 ++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/deflector-3d.py b/examples/deflector-3d.py index c4edd49..91ce220 100644 --- a/examples/deflector-3d.py +++ b/examples/deflector-3d.py @@ -2,11 +2,15 @@ import numpy as np import traceon.geometry as G -import traceon.solver as S import traceon.excitation as E import traceon.plotting as P import traceon.tracing as T +try: + import traceon_pro.solver as S +except ImportError: + S = None + # Dimensions used for the deflector. THICKNESS = 0.5 SPACING = 0.5 @@ -67,6 +71,9 @@ def rectangle_electrode(x, z, name): # Use the Boundary Element Method (BEM) to calculate the surface charges, # the surface charges gives rise to a electrostatic field. +assert S is not None, ("The 'traceon_pro' package is not installed or not found. " + "Traceon Pro is required to solve 3D geometries.\n" + "For more information, visit: https://www.traceon.org") field = S.solve_direct(excitation) # An instance of the tracer class allows us to easily find the trajectories of diff --git a/examples/dohi-mirror.py b/examples/dohi-mirror.py index 0aa13c1..0e23724 100644 --- a/examples/dohi-mirror.py +++ b/examples/dohi-mirror.py @@ -5,12 +5,14 @@ import traceon.geometry as G import traceon.excitation as E import traceon.tracing as T -import traceon.solver as S import traceon.plotting as P import traceon.focus as F from traceon.field import FieldRadialAxial - +try: + import traceon_pro.solver as S +except ImportError: + S = None PLOTTING = False MSF = 20 @@ -78,6 +80,9 @@ excitation.add_electrostatic_boundary('boundary') # Use the Boundary Element Method (BEM) to calculate the surface charges, # the surface charges gives rise to a electrostatic field. +assert S is not None, ("The 'traceon_pro' package is not installed or not found. " + "Traceon Pro is required to solve 3D geometries.\n" + "For more information, visit: https://www.traceon.org") field = S.solve_direct(excitation) tracer = field.get_tracer( [(-r/2, r/2), (-r/2, r/2), (-7, 15.1)] ) diff --git a/examples/stigmator.py b/examples/stigmator.py index c6b963d..53af856 100644 --- a/examples/stigmator.py +++ b/examples/stigmator.py @@ -1,16 +1,20 @@ # Simulation of a stimgator using the boundary sweeping functions introduced in v0.8.0. # Shows how particles of general mass and charge can be traced since v0.8.0 - import numpy as np import matplotlib.pyplot as plt from scipy.constants import m_p, e import traceon.geometry as G -import traceon.solver as S import traceon.excitation as E import traceon.plotting as P import traceon.tracing as T +try: + import traceon_pro.solver as S +except ImportError: + S = None + + # Multipole configuration ORDER = 2 # A second-order multipole (quadrupole) acts a stigmator THICKNESS = 0.1 @@ -56,6 +60,9 @@ excitation.add_voltage(positive_electrode=1, negative_electrode=-1, boundary=0) # Calculate field +assert S is not None, ("The 'traceon_pro' package is not installed or not found. " + "Traceon Pro is required to solve 3D geometries.\n" + "For more information, visit: https://www.traceon.org") field = S.solve_direct(excitation) # Plot mesh and equipotential lines @@ -89,7 +96,7 @@ start_point = np.array([x, y, z_start]) _, e_trace = tracer(start_point, e_start_velocity) # electrons are default - _, p_trace = tracer(start_point, a_start_velocity, mass=4*m_p, charge=2*e) # alpha-oarticle = two protons + two neutrons + _, p_trace = tracer(start_point, a_start_velocity, mass=4*m_p, charge=2*e) # alpha-particle = two protons + two neutrons e_trajectories.append(e_trace) a_trajectories.append(p_trace) From 96c3609e736241ded6b303bd1b578dde5e55be90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 21:42:17 +0100 Subject: [PATCH 57/85] Fix comment in einzel-lens.py --- examples/einzel-lens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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') From 1c406f2930f09ff35f73781665bbcd3b3d650eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 21:42:53 +0100 Subject: [PATCH 58/85] Improve pages/einzel-lens.md --- .../images/einzel lens electron traces.png | Bin 0 -> 359157 bytes .../einzel lens potential along axis.png | Bin 0 -> 38573 bytes generate_docs/images/einzel_lens_radial.png | Bin 0 -> 25656 bytes generate_docs/pages/einzel-lens.md | 132 +++++++++++++++++- generate_docs/templates/css.mako | 6 + 5 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 generate_docs/images/einzel lens electron traces.png create mode 100644 generate_docs/images/einzel lens potential along axis.png create mode 100644 generate_docs/images/einzel_lens_radial.png 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 0000000000000000000000000000000000000000..a01e97c433568898787620af1574544b6e921b0b GIT binary patch literal 359157 zcmdSAi9eTXzdf!=DN&Rp(I8YZXC5kLN(dntGG`t$50xYdNiu~bNfMHfDRVMqO6Dm( zAydfAZ{4-`Ip;a&`w#qjJ$pZU+aI6%y07a!thL_j@>f=r*}j!wD;XKtb~)LzDr96^ zILOF0T;D>DpA>S=58?kPY-KMykdbj7CjMIIk|^nfAMSNLr|GC_W8&znZ*NTI?Ci{G zW^LhMsBde`X=861Jub#TMs|Qq?(AtbmnT2FS{<}WuJcn9zq|M31q7WtvgHPg=7kXT z0OM+=qze_X3icsJtf5nDq0yZzg6}Kk7?t|Jo92cdV>v?iVEt*ws}0oxmJ?&n@xp9Z zcVAO|#3&xOm+E9=|4-owPLmml303}7^%UP7XSN)?{hxm;JO6wo-{#c+`T6Y~dJ+%Q z5B#rB<6rNFE7H*Y-yd(8YLqLv^`GDU=To{Oa+?2-AD5kHquBO;mvh@?m_`5e|6?^= zF)SSak019ZhTh)vzuyVZ&3F9&#p0X1y9NJqkMGvYBs4s2xQ73IG0z(5`Ljh_KL7jW z^;DQWCfBV^ORhu5%sWrPB&qsV@SK=K=T&J(|a*XDz@3q2yUyWO^BcdYnc(7cwV^-k-=tCnL~ zTH6bBgB6pc(pt}JY*}^o#P772?>?oIP#TwuhfbY4^yfs&9Apz!@|VW*EUl~>T3cm= z7+D#q46{2VC$7-`d*Eejc8MzfSy?BChK6!nr&*_5mOn{$bamA-uQV|8h=|;)CzW2~ z_qyY}*rrU?zh}>$w(f4$9b3M9{VIK6$AfQ_aV#8<sw7%JGT4x@83D@bDV1nacfoMYM)!v*~zG?syf2gHujs#$a2WoLLeH8eAd98ZQb=u?U>!-jM!yI-=I(R`CJr?0rd13$8t}|K{72!p&1R z@OA5Nl2Q2$L{e-U8ylmfr0jJRX&83on&Q-BG-j5Rl&sI2C_JB~Rc@W#)X}lz+S9)0 zR-?YYw|AVV^Z0{W6T4jPcUf5tZEgR2_0(B=6{KAM+B+Yg4MQZ-!myI}XYZ9(Z+(6S z9(u}a{836VR`W3v9)34(u2W!bVm2x{lhX1`H!W0wHP>@V!1v6}`1pf&{QU6657R%g z-3&St|8Z!Dwe^h-j?kgMjN+YE>VF6Pe982J#}kq3_lk>89-YcBxRuVhU|_DLX4~VHWS-U%V~c5_N{kuc66i9X)z{hreK+_ z+uQyvOnPcmem<+KUKN#kFT-b@|(Mr9eo)VTpq11pRZ5{nF z>$b3pYUdLFCymR(@m~{*isD*z-M1qEO8Z=fMny$Mr`!r6YKOud{kU{9+gPG&-@bcy zmN)WwLV}M1>$JOROSaaD*;8xO3(7?e*OWQK{VNA*>gpcWnh0MX*;Q^ZHTS_frA1|J zRZ8#he=Ss-+-F@<6FylyG3Ra4wQFA>`K()vqsRpgy2GJW59&!+9e?zN`_hL^GWQns z1h&K2Tt-GUEMiFKnL0P-{XPqv+Rgt;{^De|ljHxsN$b@$#}5 zv-dL3zJ2?eN3T0MJ4eRGk}J#H6k()lZ*NBqAKtNLM{s3ZTie0tT9srqdPc^wfvNPO zqBJLFGVVx)ciGuZJw3s?X-RFCG{Kcvr%~qRS=X@Z*=?3CVzHIdHcUb59#tEgn3}fr z^k~QgwU0S{9^`Gf_6X;mrFAZ?)sUZog4}1sC75*dDFR^E)=dwqScoM#k9YR<_TE4! z_V*tm_qn;U;yD{teDy(Honoi8AYFhjsugm1)@^@Ei@d2R8%_-$vCm~pRG~;|xmViv zG&3_)nf)wh_~Gc*OywUuM#8&ri73!D#_2C>baw9g<=%3OSXAkLX1Iv zUZ#)cUbT*v4cN^tU1s0)vc()_`ebss5pUo00#OaEvkf!bf|gwo;4Tq=(rLc&%QgOu z>(?Q~ui4mC4orzn&#I^Fj;>`*pp896p@y)#9G8T;!q2dM#%+b!GUdX*fw5jf_+u?; znuC+mer54G9UYyfj?N|O&Ms>~Y>@-0jwo)dtgNw->3D>uw)XeB7}1*A+MWJqKMmTW z=gca9!1nN&)=josr1hcxT;pHv-&549w>qdN%NmM8#w6x)iiM^xyl6Qws+n6z=u>&X zo?ORa8r`&TU*An$tJ86rIXRz-i`RKCWqVgAt0v{x4{k-hwY0HGXdN9M9_|Yi?0#BM zKpao1x}mjo&^|%y^mp&d+S?h*4MKo^nueceXNS|WDcWMOb+O`0WhD{XoRvi@9YuDt zBbQj&*!J$JS4@*ZEm1Cj7t4)Q*E#E$_ z@#cYIl9@pC@{nLrUq#?Pbki)p65=;HY>HByhFm5J=7@!&+XV$t70#yGTuf`t#Se}j zKmK!K0>NBgR<_ZmaDf$-x}ZRuZqJ@v?=^|@=g*fJ@H7td58ChbDZbHVZF=q6-Ki@d=NwC;)U^f!j1 z6x7UK^9Kd3JFhx8gyI=UO}7H3qMrK6N$x#dx<&>Dw~C94EBm!-O;j)NUo4D6LqP_k zy`va}SNpb(4*qP*)XsMau@&MVy6Tks16p!Kh8_bx<5zl`eb=*u1gYAes~n7P#6B7E zqU#+CyBF1v3V@-Nil}9zs#GP(9E+huI<#3n*5;InSAMFMs*={)l6zA+VP|O7ZB(2= z`*q43beDB>jJ>>KJtUNJwN222aQKW=ehfm&Z@Y9j_aG0?9y%=&^FouufS+Ob$B(Bi z=61HWr!q93YjY+v4SyMS#Gf5oHa73|A9jqg6i6ZGpfYW_lB)jGDQ~gnv#GtNK(w)^ zXN*?ri|&LrOV+shP{pL=7ISl&8&yUnqqF^vA}x;}*H14XdC`~y0s>yArKu*XA&g(A zrs`+5y;M&bSzPpxK#_OKi$V9{XSgKz3Xu|5FO#c1gnX7yd`7NmDX`DV%1Vfl>R$OF z-!rFEcPM{NYPj}tG}-9;@!flrv<{4_!i2(lu0rprD=P+yGNs^+pev80(jw-MkJK>%Iyu5&b>ZLH|6p~ zCAkdI7_TfFD9O$fM8Go=iTF=VO-(42p9kN52>v?ki24s`^sT!a$HR;YUp-hd>_|gN zel)xqpQcm2}RlBK0(dO<<0`$U1vp%7(S&e!V4 z=>j&PeCnnF_NEsW)&WE@d(P}cV&ec90`DwxgvtBP&R*w@yw8w^#{4WhdoF%nF`?lq zVxzqL96AxQTbAWafv!ee+nAFo`z~NY`ao&yPS1RW;G|oD4CgIngA0DG37Q;;I6EOm zbHR0kTEJ3xZt>NpZI<@7wgmn_WNLD}sL%nVLR<$|`jqy4yvF~mX&B1~mgV8&D_vX^ z8FthP%)87%H@l#&u70=N0Hq6sS6o~iy-Ct>0;{zYs74DR-eP)snuCi=HLaB=y4Jkr zY*JH5J*g$9_S2_NQmyRa)tyD&aaXdfGf>w?MvkSbpAQ_w-lC;!+qTVI@SaI#vXkZ| zFRu65+5Tq_<*%(Sb>=w-t0Zr{Q9Y=FJ{EY#Qs6v4gRzChwzzud?>$G6lEHuJP(R;O zMz5Yi&uC!!x4F4;{0xaI$uFaS+6vY9)3OP9Eeip}x{r{*YqNZ)n3U^0!IYVq`TF(i zm#I^D8P-t$_wNM&C{Z20Hz%v&{c+&)ebnBcY`mAQWL@f)VfHM;JFZQvtw_$UpmGX3 z3{jHmViHi~qXlj3N57s>*U&(t^&pA%#OtOxplKWm*@6g|_mF7s=vergU3l)m4&_ij z>z=~eni|TKloazDCD-^3vOD%NG8#ELK0=-5mXKI2Gu63rWgu>KI1a4heqbQMC^R(o zI5;?T;`IPb;*~!RJ2rNA*BwvGFTu&Jtd>4sJ^**m*nbHa4v9svf4d> z0EswK!Cbd0Ex7_Ktxf(Ed0PIjt%naE1{(4Dof*d?DtaDe$HgT=u*V~`w)TQ`XHIc< zHxqzkL&L?2ii#YM1zt~2PlQ9p+qWeM)+<-8u+aGX-QqG#foLG{5^$OI$TG^S{b1D2i_P^BG7iPDeh*_0nTeN?$VnahiN@{9sazc_W1oT3iS7<2-+_yjooDpm0R%|2k5xQ542!>^=hwdTnGdmw!bw)4@ece z_d=anEC6jSsYyJmWW}sCt#x#yc&01QIWi{ZQpyMj0njWU>#?vyfcv#3V3MeODPvQQ zKF@*#qe7zUdCWA(aGW@Sx{NY6qAgRPs{~xk5J!ZcO=C|!WL ziO8g~;BB{pI;0zl5o#kd=+%K8AFpzAOG+x?Oeb2+vtG6A3z5AIOh9n>&dw+8>y=%p z0;Q#ng&DZJU-4YK$~~`1?#3G0*InTL^XE?xmbQ)#)eg$>@$rZCB&5vG@$t7A8SxUq zZID3fEGuQz~ZYPNz=*u zdC`Z!U1V8GW=C5=mDEM;5OG;jzBE_ug&5~oys{2S|09Z(O_*9(1jMcUG)0sr8`3f* z{;d6wm*+ZND>ythCjM)7dO?hjj}QMbBQGzYXM#jj7X&-lRJlPh_L!HqgsCXd;lR7p=YV7+tHXrVoZQGH&Zil^YQsK4S(-14|pQ!9ScO7b^Z+0iSTM1wQa8U z%=8ROE6_cjp=}%|2sZ|Qk%+A^k z)$AqTyt!rWl8C0YjZJ2H`bVG5)ajZWPeh%AG<02kA!1w)WMgHGh>ji@X-Pqbym;{f zB!<+P>yXnmUMamB6&%|Ko(;zSvivxe-v$I7q8_D0yT~gBPyxrLdHHgDTH1u{{xc^T z0^42r7^yPRB(O@dA3uK*U1y*&IHl?^XZZO7r6Zj2WkiDa*+vk^`-w2JJ1>g=DsSZE z(h8^&Qi^%>=hHHiN9+9osvy{*+IHsH`JmFB)P3rq5n9F4nx;qkntBCbt)!1ToKwfZ z*7iYnw=Q}QQrJTxZPiXBhB~zxAy2?_ef^t=|KX;jT$iasSU2(tkoKk8F`|A(M(%y1 zTOg0nhRC3qs!mQ$&P`8=Zk=^lI*I2WI=uz$yxbXqGgM@tT3yaLE_aeq2E3ef4JVcd34r1 zJm7pdXM=fGj@O#EFZ^YFjq02iie5b1R4irNq*(k5FJ149Sw!u|>3{ZKuJk60b9@M0eO~$wm^( zKT3Xo8<8DXuDB0bJSzKL>i1D5tk#4O>#}Wnps7uMKAtw|>^a>aUf z_d)fP@+&q4iA>~dG!$8)J)b{s?OFVqJr8tO4#teFE~D+yJIbmIPIi@_VQRqx-4tpH z&@!MLkC4!OjVZ`8nfabuHXZ}}@H6q5D4;Gh{^pSNI6o1+wZ&GVh5=vJNTn43mjLb0 z^?1h(51S#Srd${k8s^JxZyLO@)X}plfD|i1;mn8(3E7E^EkzqfI|u7PYzB9XN!XX% zLfx5e2D$@&#RJ)-mbN>f z98@Mpn-ieYWi&NA$7QNN$%`Bi&&&CrOa_DHg?=kKk?+$nR$-DUK6PDwu(&FeE!VcM z#NoOo_zBwZRB!PH^s>reW>b_npj4y*Pox51P1Z9)vNW$28BEu9EjoLsxUrE2Ezi)- z4xGH~<3}>n`dGlAfw?6JHV7*u3;Geh;bbnL3xs-kF(yDLQIsKQ@C2{GFL-jKbobdp zh9zsZ-XK4F6V2v(Rj8wr6I4KyER;@=HlQQ(up-?wLf9Y*Jj7iD1`ZdZ+R2=FHEp0o z9$wyOCZCyKy!!=xM;{+Vb#_>uw>#O<1)@IXLa6gV11&9=h!hMPqaO@*<+;H@!=GLXDOt6qk&=%&r(RyhAV7<2i6 z@B~YlT<{1!vV;9m!B}r^Z^-fDu+lD2dkA5OQAm_fFUfsQr}9cJdtrcslB%RcjeUgV z!a+CC+e-!|19%D|j1jMYi@7m+77Bh*^N1tr4W>=V8IRYt8zkhuC`;R|#~1{$=F8Td z&@pCWZ~qV*_3IZeV9em4F~onvO$^crA%ORKdd$UFcleb~DTmzevS!=0wa+P!hanIw zzj1gb$!W7^1IAE_Nuh?7L4OK^0dr#YV1f?E$m|CORo+MiJO^st*(@z$Ulmv}I{?Xt zLIzIB5-Nv?N=Qhcz8*WzFVA+#`1W1(luO>sP@xvPB$369ZEaORH~8#xm7~z^(QgtO zURLC7^}QwH#TTPU)U*ISLMi3>M{$=*}Zw#`d}QdnPmlhvd}@-qMN01UZVN zN}SJZD>KO8=H~v03QO>wJ3SLJcy8(vQXI5+x~eE98Fuo{2;GDx7Q`?zF){n!Xy=}RYCs4=HGhL9jXh{%0`=0RYBv@87>axZvwGeu7pF4__43U^20+@|BUAlq?@0Aruq8~nIh`XLd}@?fx-TO?wXXe*{ApvKZ9q#=kdcwAmjneK#mcJ zzSOFFRKdU80N;U_$KbYNVlGAJL|EN+KSQJc*}SBxo{kR>7emyX{h}tUHa0pc`sWn*4dhP5kcJW`p zVugj}{*dtLg7C6WB4Dct>lYpQ!7oQXt(xXg~~# zaS|6~WGW5kgpgerKVXst2A`RoUAegUq1~>RJG1S6Jt+ay##TsyeU}QY1lm5%011tA z9vvOUsi-73hY0@q?8fd#LXEw^jfRJrz;&;|%s14$@OL_VlEY%YecFpy5s^FFCW6u#p}4h_MxDdz+=#c*wE;(ulzP z;t}74g@u0i_Aw=vZHc|aM66yyO-skokDu|*#h|kZ7AKRM#08V8)Xs_SkPGr zyTJwONlnnyvH1F*j90nop+Mmos75d-%(?#{LET3g4no zg#r3JG+X&ZXunA<=FnDgU{InqyrkGJFUA5+T5E!FC4PI^OA@2>PvugdqmWu4)kOVX zY%MZzb6ZSK;J;V?Oe8831GoX5<6&FTgAfb4-YeI;J?C^F^3GQ&d6lGI$p)Usa2L7~ zs2RGE>>fGvi_`x-p*6-JCCzip!n|XZ?IP8iI5z#$AquRJO%kDsf=A%p@d8+GqKfN} zEQB1GAWAaSRWv0~6r>w=2t(1!T3R@QCTN;a;~`3M&F=U%XwSpLQ#?62N$bM^r5<4m zhJ%?g$R^@rq0Hzm=1ZJ9AIgvzm^m>4reTLL_=k{1UGVkKXg@`riE*6jshTgNb$kSr z=-T1xXDnhUoQh9`;+-D3M6s=8n`FvCvCvHefrL2(-2(+43>1-_^%Uw2#DIVKS+!5A6XGvUpi zp_`m;lcfc0oF&y){p{yJtqC7pz@R@5vvIR;M0mH=v^Y8G<&4++FZk^*3aE zq!=8h&KSQd3Qu^|GICx?69K20`qh+`gWZrBM?lC6Kwpq+%e;3?p?hlX~F5J>< zD+EZbA;Zsf#I7(kH4wcLexctbG)KLd<=KkE+gkd#k9@U%G$)b&n=7F4X8+|3ws{uv z$F(M|b6w60B;kA*#JF^9(bhAxIZM%p2l5$IuQZekkzX*O5)d*=Q^T_{S(NJOfb zI^#4yFef2?mh(!)Q5WdeI~OhK5VR8_XkK2P`MOFZBSyJ|8H$k003-szol5#9x4E+( zygmPRVR?;jjGmuCPqd$AL^Z*=XeDssAjb5`-LYVtG}kPWENU@3KVbs-yAF;<2ao2&+}4`%YhPCw~e z%)_8wV!AbvKTQQ9fVmR58a+MzRS%CRp=?TxU0tbO!HG-%ItM*6h8;y_$vVulfN3Hk zB6=GW&j$zI_nNokSDeA+iH`_M(#ul0x7qmZq5z0Q2s(D89=j4_0~SH)IVR!~ZSGf|Y$ z+JL=4oFT0D#*d1Le17-GJrj4l&><({9B9Fm3+9mhW&X5`l3vTQt-vG@IPuF2fzp9K zu3}7}>rhe*F=U482oUJ9o}MX4BS7HDtcXYqRY3V4qa3E_f2@oBn}nY96|MnMAfnYpB9{^L7mGoX+cJ+{mYoAI$f z#?Xbgo{vXS>9J+t#%M9Z&f|=P^n(c{+(0ia5<8^`l<7i_Q8Om8!;S{r^f+16J>JNg zkbwj8eu{~FFKJSsE`dhxUtkDkjbg9b1?`SdIzD^=N4U#)AW3rND7Yzn8-&Fq zC9Tly`5at{Q$l}Ds=7M>O;!=@099bMo6s2Tp2y8Cc)$Rcyn9++J&$+a|oO|YH(J_fqjK;%Dt z+CnHy7*tjaOubB{$^J9nWkj}r_^?4 zLt3ULhxxh1ghzEZKT5eP!@*%=!v!=Az(6pg&dxH@w0M@wKtn;{+q6sF+3+p!gzmxk z31d6>6ENjKchuC=gM1uT282JGLNwCV?1VD|!@;9r z^2ku2Cn+f@AcI!}8es6541XiMrq3QzO~YvBL=I%4J(+K=AtviE6hQSz zRvYnsoeF>7<-fZ!wH^=SpbPk*kBJhX09=J3d><~bhH7Ogr>Y~909Sx>F)${~6-;8m zJs5dk)a0OhqR0l$hiJl?W@%lpnsw1kRy)KLtja;x(tD?R&~0RkfB0ag!Ya@JdLeRO zSV#yMWE!R?BAEzz_C2H8Eee!Hh)itOFSe6!fLj8#55=SewPQO0I!Mzph0KW}r{wNk zT(5ldRPU#rrqUieUzL*sm8ztn`8=Y{%wq0 zaS-xRN~!$X+S?6~)PRjZIiRcXps|f7!}$f21#1+5#au8`upcl9CXYb=04uiBLl@A@ z9KK6=#V95*8G-Y@YI()L<+@Xwa}P9m^d++NjEv&iXu%e1AHT#Vv+=IHO1J{T6@ncV-*x^`~$1%to{4BtV_ z_tOd&kHKLWS~XMSezAfFhzb)VAfJ8^ktbrV57FsrP29~(2RBsCEO zrWa+BFyBbT1z+QT#VUmEI^Xs0v9mkS@2A>~UJV6J1bw0bcBzKZ+4`Cv>K|y>BeBEq zdtv4X)$9gR2ox$t5p(famrrZY14(@fGo_dNxy(aH97Qn7z>Mr+^&rS9Dp9`fgyD%D ze3AFt#ld-AN-nv+msk6`Y&%#S(23W-fVv1MaV8!fBLhgV$mtzael2C|5`SH_vkL*I z$_h^EMgZY*;F9Mit*sx*b(_OcMH%w&5r`F|l)#W;nVCN)5+r#Ag zvaT)|K1Q~sY2GUqaIaJu(L~qYsWN(oz6s-$N#=A1Bgg@qC*Q}DhaN}!XhA@MPr#Izre1nYGf4AUTn zLvVHXBU+bdk|YyRiy__xc|b;is*YJ2#1IT{pm`=K#;COR7yE3ksjnZHUI19Y*PV1d zy%1DiTbq%Yx#5(OQa!2r5+tY9FaN~pE%t;1P?b<&O-)Shz=j3c9bi7M3y2@4eQ<6Z zoJr>80(GEKs0o;FJdkA}j(o=!ZXO=H=i3wQ(>;Vz3RRNxzr0=q<^s+kJRKl~7@fPh zyAz6%;pv-QT{?up1>Pr^iJ+N7?898vx3hZUOtRRQ(ODF1kmW)9%T_uI&$d8GKG-^yW;a`+=+Mm!Md*#Z$c;zinjRm3yVab3(sFwctOn1I#7rA7C_`X{q0XLO@8i^%^dWm8UDdJg{)Z(_ zEmi;V->m~6UaPsi2}cSM`{t=r;yJ7QAna&_7`R~vh`ur5y|zU3@83T!1NY#o>~j?) z;!NQegAxeU3}_F-h*uQb&}=Y@!N>zY!9E@LhI=4jQJ*9fk~rr*C%}$H4fUKg6dD#PiP<0fvWL0vY62=GA{{aU7~VI?Q((tHW#|WB zxribx0v0dC+km4;M0n=qW(S5QbW?MPf_$ODs=HVUkEJ(Zhogumef0%npw? zVbF&Aqsqu-X)K$VN&wn}dBE3KN$ou^g<1_g@*sGcuQ0$LstM5=3f$-L(>@r=Mx3>` z94woV{(BL^3@sQb<9~KNalgW^@$+5s#IWv6_RWn@kuiBdcSVgcf#yRD{vetlJ0aks z<{_;!Kk!9TqGI947|6HCFT8ofi8foevNR``sDd8#GK@SXCI+}5>-Z@!6R11{VY*P@ zoX~K$#XJ$Zkt_>wMDSmoiGQlj$~c(5L|mAFDGbyu^Zok@1jt`Efe>Q_dP*A!6S9Hd zV;#T&5N8S4o{+EsA>BE#^bCS6suBj(&_-d=1yDh$MZ&=)V?IZTc9z`ZBXe{NBL_B< z%t(~NiVAWtMEu%G|0D3CRvAUa#hK#QJS5gMdu8PW;BhboRf6?`a7WI}@MB7^d|dI- z_ic%T3B0;6`=bEf0(w9Rg7aZ=s**Vq&NO@wn}>Nc3U8=3k{H$0KKHdcGXjTOOpMZ_ zk=4%CH4>2n(1o`Pt$H4`5n>st7X+xPL3?^ia=;=IL;lXcDR_77JmfaQ)QII0lF9pF zYH}Z_K(V38q z=-NRooA8>;BRq`@3k0J2JCi^8HU6henE3ODE>Cwcfnhxn>~`_N2cV3?qL2LEggJJF zDjE;2Kh$T%%9DyQ7M$MWJ+r@04(#PH<;D;BlGMI#LEx{An+}q z!#2-rKrk1*2bz$z?|H+u=g^i<9hm%L`oeMJi7dI}g4ghhg~qVwZonFDUQd_N>qN@t-j3y-Z1T>ZDAt-S%4BeB$&~zR!B0YJWQ~C0wN&>Ymirn zrN5(w=G;Ks(!uw>Ncv+(g(U-45jm@_xpx=$a``~X!*F)@E&}snq6>&+efI(@E$$wm zZj!90h|$1;2hahAil`Msun6IMQ52iyZ(t#~ukfv>$J=CX8{`R~OTtyq61N0{1mR*v zdVwhdGis$i20ewbEmDCO69B?=`pSPj0%&=Pd5s793c?ULy95hoQ+%SFr;D^j+@jVZ zkNzn4kKvE)04jwMT%%}lx5gXoi?qrpCcyZiRc9sQ*ysZ6aC-x68b%-JQ@FT6n6)4~eq@WG z$B$tF7=&!&F`J?%Zr^&ZJ_;mcU}|zCB@H-zem2d!78?k>2tgG>V59>1---PA?M+7> z+api$AVS_C@D--7FI1ARW)KJhRQwk1jUn(L%o2$=+Zu31;ZtVk_c_rwUs$S_`Ce~z5(2b7_6FP5=yl1 z82|uSsfejd^S4LPlc1syDoIWb;Qql#g;zZ%&^<8k#>AiC=UB9^=WL4~jRWj7Vv>Kf zgsDVmRFHm1guy~I*A(2%*xvqE?%P__ezZ&USBU9tot-R9!I|&gJ+CuV`yh()3oMG3 zDV;F&<@zIYKSI>k&N-E(H3$d^TN5G~z+*(;8;M66pPiX`xw%_D9X?!GZ79%0p`lt@ zTmM>Jnn;-=yj8g3Ardp5E13M5qY5V9(CR?~2O?qtmQl|!3KO5H8?;BKBi=Xhd^jXj z@A0qqxuO8cXldZ=zO1QvAhe2bGA62=#{DUQXyW=9G|(bdKj5%863$R0Lu4UsE2OHI znq&<>Rcy??qHpQ`^iEeAP1>&VRY`rQu9; zTOG$xO=NjZh_p!5Fzb;_rr5b zkWV2k z#Kbf)TGz3x*7@wU-#xs7f|aDy~!Id)i>+6=I}0w=-w}XpHI#HRHzZjLZ4@ z^hhpDgKisN&Y$lOfJ$|z^q|~Bs2i9q^#g%@dtZw#H&0qyj+3H%b6s5^d6O9T z=lcx^VSYfo9y&tL+UgvhA#Q17`+$t7Ah5hcuG#0a8FdFO*oto6|aEeA6 zuY`mm{T)9Fv7!laoDwiGUUT3BCs)A?&7XUOx2-z-1|+xKo8Pn6uV*{nDO@;LFtNX` zbtTPta{2qy_Cc8ap)jHiK4hW412O+lTs`qjoREgreWv`1(=?UDC4q0>=gwynQ z%IrM84e^Fo?_Z6DXI-z&$+YfC@pYY8?^Rc5weWoP*~&6j`qld*#zI3w!Qq(IQ+MnP znNVD}?a-lL&36YP2%U6pT!*82(I;{Tp`Pk3bA1I&p;pGTF|J#eF`13fWZuIU=Sl(h8(mLl?eI%(6$ksKSxM z6yhy~s}s1LcLcJt*!})Fv0$$vn7Wq=sCO6o>(l!c99;XwwT6q2CG}pv`K-J1tDiA0>FB{Pqp;9OYm z1Gu0`u7rc-iG*jA-pYtZ{&bxvn653nE4L1i33<>%Tp(9xO=a**KA%NrS^WCG{ z3%Y@u3`~#9#awnEFMz2@xE;2*2#jfx6n**tnsf=k!uJ0@iDgX*$?AiWL)}sO#BG z12^%0O5Tf2l8Z}vpkAO~vlGi+o5kt@pdN!bN#H_v|c^x3nD?|h2;_knX7q8pAZu1{Eh!p)D+(7`qqsrVw zMQ7hM%nzXgaw6shm$kK*<=>Mnzn|zmJ?iK0?_b(|W5tWP`mL?bTzYCL5GSgcUezPn zq$XGjcUJO%bU|-M2S+Ou3Zk|tJfBDtOR_@<^$QLC8mv}OR9x+L<)a*!WNmrN0XYR@ z4IB#a;$A90p&%)E%5g~(eTcYjC2PI$ zLB^Lc!p5K!Y8;E)t{p9aDtG!=>ov*ab0yj z>}QaV!tUy~RC7~Kc^7tU^h>0{o@1dzC3#P=AXR+kZ|5NX{>zd3VNWaj<+EqMM+7{( z%YJW5>14WP4|DbVvC?}Kn-lw(LcKF$%HUrG!2I{)xCiyUUUXll>c4+RZ)H^1q7 zdcS+cdt9$Ze0~*2qDP9>t%fV6NsO^+rQmodi}3P-4~@%Q_GTeXGRb-I5^gaMt|tB| z&$aaM9zkJxnTITXlJ1jCn~j&o*Tg5{>eINigQX8IuB`Sw%Nr2#E2?iTxAVw3LFMJ# zdc8<>Z=q0-|Fa*q?p=~aANz!;D)w(I*7x07X;+wWsic+kqnCI9N3l!qRcosE1E=>Y zv|hJkBu%b)d)933m|Yc?jA->)>0cFId(oQXEm`25=h-ycxkl=pTYTZUJSyqER^QdL zN9Av(5`T}i-m1A{=YRYOsT}V}I&kmqfBq3Wu!-Y8e{%PeVbYCF|M9Ckce?)Em44vA zxBt&S1;?zX`|l_I`KRw0svmFtudfqx`hAi5)W0A6J}38Z-QV&$4R5Aqi{;U{kxt|4FLlYEOO~q`>NmGkd&A{_1$dO#&u{h zacj#Y!fjmgmV=|Z(eCh{#n|6|$Z}AQ244Y*i>R!GB)L?0j6-bf(e=L@@ykg5;cae8 ziqZ$rq>qI)u0(xe-m!7rm{YX(yq`Cw@JrrnWm!J5G_vznG!!3KCn+g6#6Ag;e|V}U zeBb?*F(djr??cEoSK6Pk&+(>@XczvzuB%9IERL2k_Ni(Fqv~S*!=Ay>CGt7X@oRfr z?LAhab_LA|ACjX%$;fKeHN;J@jEs!5cX#Hch<6T@K78xI9~)+jb9b>Fqw)T~?=5za zikF&9!S;4$P~vL zg*`b!VU-RwhIT5Fj~-Ib zag=9^iUCB?w*%@&_jJlIY}@CmV(8 z>*#XDbUt!_@mWs#a^QYWK~rCs`*kEN0fP zX6teB&0B`netqDdUm2_$(KndLoAl~(pOS?l27%LUmrSj<}P z;SOnvS>2DdV^lU>Z3(MwbS4(3o_utvGyD+IpT?>5D>Q)CNG!UUM@Xj3uEth~xSrSx z2vF=gpTJ4TP5%IW;W)S3~;uH?LtIX0+TXF7j1u^2IDE|pWrSC)G@iATZ3IJx9Z zd1=;#6L*={Roh)o-@h1A<>r2SR3Im-Nk3J*pCq`?CAL1I|!;E3?;kxkh!ZeHne)SQ7e0G4bbzM-FCA6@H9{)7F7OO`J5+)T(P8Kqa*)Re9*qqz8&=)6QR~G8IwNwUz8UW3Kf2~@zv#EJ+F*vzvJ7r zR(7^#x3he7bNCn+!)%!G@Zdn3rpPjLo^=8h}1!Yyl=|vfF zko5Q7;oT9(`@-zObt}5#W?x74T3cqD&765-^*-{`(vz4^OtU-AeLvbteOScjLG9NY zfzols>@TA1BDs=;FV#N0SRRz5;x1&6)lCYWWu4$^ZL{1?mLn87pi~!V=XFS?A~Rz* zi#N+H_Mssszu?xWd{>L``ma}hUf;kQTdOTxdw$yyFBhl#iRP$>;#pzeh;HGzq91P6 z;S&3Arw?JfZR|O#|Lhjs*`T6#7NJg7l$7#jey8bqw#d8|Byaw3kU8z--XMdwu@{8y zU4CuyX;I*f@d#fpk9;0oX5y|rtPWz58Z7r{wR|4Tn#A?78@zvH694eU!`V+uuhbUb zaZ_&aSy?g6h|yww?%<^oHs9DmEs+<0-5;<(Jb%NgbXWYik zExSj$Zlr#_F{d%IJfs*ytr2CJz4^O`U!r^zY3N@5TeTsa(dC{B{@8BQ4gbOim~=b4JZtGt`^m2qt=|3yqlBJ@?y_fOa7(h@bp)b3-Slr;T*-M) zrMXj>Aur}&-0Q4wX4E_ve9Y?%R=}nwb*j5Sq*b8f-Qq#v`k_X(*zi5)^PN6? z_Be8(J=3L~TJL`#J2946t3EB(q*r?z-<)$6D&eQJws!tDO5WK%e3jvWomG431GMp9 zg;Etwh94^X4p^iZ=`&%)b>%9m^t6OL1cyBvv*lU?10}p4LCl<{5+gMdy z<01|2o%yVvB>uFGy3Lxp?HXk|rH}kq(GmT7pHznqTyakl|K2pqDjIX}u|9X?oUr)g z_z3%q6JblXK?iB9h)xE4soC@>XKHnF!tT>ftKc9?CSan z31E4&c3(Ja1ZU#B60_B0pS+J7*LUOzpiZ9nN8UM~eK<9jw#RZ``Gh1R2htLbDj0^k zXECMBuEh2_4Dz?&Ddy7awkhqb$JU5+^Nbltyc7m=ZW=%pGv|)4*ev zrFGj7oo!as845m;D)$^hg-^&!8q*xONBh2GgO`_~Ur%T9#nDYJ@2gz8NuL#BnPXF4 zsWk8WX44chxgZ_jo%6odbo2y{h5r7bx3RU;dhc(zxMtK?9=*`sa&l;07lr>{KzH}9 z*x5V!SFe!2Fyp>7$#X+pl4ansNB&oXypvZ`PWC3Wu)a4|iRRi_PLX*3{OJIyU|%!( zh{we)b)?9>J}uJ0dG0z+?NYx*)-k6F$O+TM8b5Th((Wj8Yqyw{*hINOyZU*AljiH^ zZPwd=P|7raSQlsWF5=UYc7({t#e1KcUNB2Lk{^EGA@#mp>W=>Xi3o+=Dorkk9rn<8&B7oX!_y%?L~2Ua{hWcrc4j>qWzV8e4DVs?mP|BqDgeHden^1-CUKZ z1) zxHiQssU>N7l6>h>YoVatccuD3JF^cT)fy^!T1o~-{|A%$wT1r9&2>WW<#-0|-8ia) z>w{J0^YY!Z>$DQQBXVO{D6*_iNz(J^C|s<|@k=`x9AwO(`J&k1WGQvM+A#HDC((5) z2e8qkvAmNU)xGZ9j#M~Eyok55f4JYW&Go(AB%pTa2D>7Cz3UT(YUNtD`JhCF#iTO!pk(I_2t)#MneICQC~aRc_{%YN z&(QgrM=IY7EjPuFhRN9MDyNj;sdg8a(6#wh*Tv8(*un31@P)puK5u2>uvm}YD_i|z zN=HWf+dg>Bv;NO|dzBI?D*3}Ns|jnu zhG*%1RCNCchQs;NGvKZmx!n`)dvnLRyE@0;HZiT|4w=gp3QKxocinSx7lh-}^qH;u zaXKPJ^9JE)NKZ`pG!&{^`5|Hsr;OU$i5UtDi&#Qp!-6$79;n?Y5@ zOVjLVN5HOQEYr4_TGqb-p8UKo0S}UIj(UC0tv0~)>d&*)W%heFJN)mexI4CVKzuDM zFlQ_Wdw%9$Y*Fc`mZLem@;SA=9g}WRYiabEW}>7=__0VX+33$8o4O#c#>Je}L(L+y zYuk4y4}ONP`1{~j&Jj3z*2$FO84yBMX<$hyh*y7>AIB{9;K6%-RITD;ch`omI<8j! zN`t_F`eom)uZN`*3@9r*mr1}Et=u{j>V%3SQ7m_L*h4fwzcGqQS=A!C?u&lXc9ot-yB+2lMg)$5M&-Eps)ynjnBcCTzRK zTZhKcj*LazNlADE@Z;xPt+rkV>dslj9NI!=TQO-Rt}Bflpw*{Xv-e>efQOlpak=$F zVNypolR29Dyr(j_H+PgEvc7K0v~5pb;Ctd+ztAKLa`lzeMz44 zJiOO~GtyL9lDob(tOk2Y=l(p~y zURP_tKvgq@dsosK%#C#M#3A6uWcy z-B&j(@w;y2CTDz&PSB3hX8||~z#^b888GuOXQiwNW+4z~4!RG-W&`w@SxgWDsm;+x zKYTaSY>Nnzsu5aW6@F-znD~m$E&CskgzcyIh%UnCsu~I3K*3Hcz3%Gr8A=9-h3-FB zs3}4p0REoiCE?V$nL};JxanZ(A!s19o)Kru6^ifA`qIYrWMW7UD*wi9?6})mP7k>= zvz|COFepq`tLZI62j-UVj=iAf{@4-jF4L4dXeIE?_&i%z`nvk~${7>fw_lsKI;NWb zI=Q(2?;3jRqgivuFEj*q)?XLnG{CAzwo=Nuai7Y^zNnoDubW`N(?recPfUM3u7(-9 zs8l;7xaYwik8i;5ayp!4gC)*doyU=A`M&HLZJedAVXA5sGn53#Gr;8{e)w7w+2*6u zo5@J;)Ez7=7*O$;<=3XS zQGj+-O%Ma51r10nhieAYMv*fwn2)-{zBP8h!}vum-UHh6iDU>*I4pu1DxOLJbKyqn zzxWp$^$^t&xNso?;yYfCtk2Bc7#?~=FtA;R8W;2wnUP2CLTPY+)9?i{vRBolNzG(I zvP&|3o||D=?=h}4q$LBs-A(PK>;KYwSJ4a!Awt8f*X=Vdm8<`*#WA%^+l>)Ko>@m=)H6qVMCB?DbN=ct8uX-ocMj{zaX#!)56> zJC^nuxv#P&|0p~K#Zb*-Ww<|n>+<`TLf-qZodrX8j{FrDqZ&K@3s?M@BU@lYmYBkJ zyRRYNW;TDXT3S(!EO`~@a`bW6kO#g;!#{kx;Gl`+Wjh-YGI!~2?r+XLleSC(fZOyc zPCYI1A^kcph$gbD-fU%EfaZ>FKup$Z-vlKiytX-*?>)8DH7L>W#4B_+vBQnvnVR=h z%i?(140NjE9*4wHqerspMn5b zHaV3;sr^&`c`kSHZZhM^VA7b^o5J?6LoujIm^ancw3feEmj4epv}@)IExH1|%!5n7 zcoLqIOr%5dr%^;e+uG5!yyvA}N~e=72qm`}`6z2GqmPK?BTP^C101fY|MKQcz4@o{ zI#dFuH5`yUZ!@MVY7Mv7sf4fdhgt@yzY!G-*L?N6p?a>%m63@&q*o+{`Ww{JBh$8a zdmN?ydfoJX_LSq1s-J!F*OX!_lsETZ3NEaGuClOvRhz|d0yTW~PC>ENodeXzQo{I; z=V!JLkYABoLo@YU2QEjVr9Fh&X2BD$SJ{Ip`z1xXd?t;d|G&7nw*#U#eQ`EWi1BhRs@2Tg-?@J+XVKl6%SmwuXd7$2VIlI8^y zyXJCbFJR7C-J7<mmPwSJj^LKGnYs#Z(-kKb;IWa&kFE5b1|%F;ri zQNCuX|4mM-Xr_cH^iwPFC57(*@Q>aV*Zpoa_A|)q)8y);e3Cv(do2d2@IbVKO`0!j ziE8YC%)daA`d&cao*5J>Vp;46t-<(Q-++f!eCrPGjW3$XGPYXq6~bTEq)7-L*r;TjDDec8Fp-(|+1=V!=fG7Z7-k9p9@+|5G!ghPL45bA7S2T2t&emh+@{8+R6UR`c6iZ!pb zXRBX`Rkf3u^P!#xfFf2iQt+I3b@@A)puzj$_)cFD}Hx?z)+MPPWBz z>C{^;Hz211wOd%0oOmX>`WB-j*|Ub()rIGE5Hor-W)FY_cok&yegE}o7YgLIJL+B;b~@U0|^^^ zPni;t=sDK8QjWwgD)e71y-fRj%cWFYnF+?jHH?6G5th&si$}kjqpj0yfzbfeQA&?? zlw~UmFiknXS2?nB+QAlcTxiDcGIu?ztWLA=2<{;==aYvg_ZC1e$KLj#jEYZ=suO9x zrQ*Dub3{I#02wzPow+75anPc?Y8O}h5-taQ#yB)f%#dmv1U4DU`~Hp?YBV&KP$LkC(&lWh258f~ z>Sy1N5E-VPtJP$t^c}iB{mP~x&#op(a#W$f_ixzrFu(He+PG$7YYUs$N@{jOj znzw+aIvPdt(j_UlK7xvRBT<0N(XLo;c5pr897 zeFjLQOsU^}7i0;NgE;GEgg{nV&B^JJE&LWN#9beB!$Q(~pdMeH_2YmToflqgUEd@6 zFJ9g=uKdhzSJT8RO}TqHu7*aZ^m?6ZV<+10j%2*`u%=I%`O%J_miDpSBgMY3xcSsd zg3vR$%@15+ugm4m6lV&3KFCjf@5)QgA9k`}C{!^3VvTlyAu^m7#_~9PfV1J1XnD52>Wy^J5@!?aPH()6&HrmiMs^+=4NP zUo#9)ejCj^Z$W7Tn$UV-lvKBi6qz2Mv2p#J7fmcSF9wWclAlb8n6bFaC-5&dh1=|Y zRb9`a03anxLbuv3IyihIkZd4I8#}NKapDxX8P(ZTt4M1@a0N(j^sqTF##KM$X2Edb z6`4%+hD*7bMuN*A=hz|#jRdBf11#~*Z*JhXnS4P&o>Q3A4tL?PsYNN7^yE0d-k}t3 zou<8t6(cxH7pe6M1G<&jGnkcfe$w5hY`ZFb9wvNDYZ*9b-w6FnCeHtpaCiV`_18cn z!`_`|7!nTTCFTjzOk`_;3Mm7+)qMF|U(<8`DJ0(31`RB7&o)kG%)O;fQN5uo2q{XxTAa_r6Q@Qyzw9szbNhPbC*;nO zRrdVUrdh0-3?v{V{V6iRAJ{NE5eiTmY;L}6iF;B*3O(?mCH9w6P9BWKr`|6*p30aq zLrQ8~{6HoO9Z@JBaBqJd!sJ3G4~VoH(#vx+^gSO^olimX+#C`pJ`zxb7RbKe*rTge zn}qb;rL_K&H=5m!qyR3=YbBfU_PW%A#08h8>W=^X71%}a(pXM)k5=y*53?BXtvk8! z<-|IG%C3)9IfthC>V7iweS(40`dZG~;0p(FC7?GwcylDPe$k)I;a40Ge(Yzy>E#^@ zoi{uk5-9A5GxeYamlx^OKw>XW+PR75B)v*!t=at4ur)w&hD{-yn^zR)iv+0 ze8rUp7n+&jfY;WayMV+BGS^;5wOW2CI;y*(A&$x>N>l6T63lz?bEe%4r4RbM;R!ux z9o4pWk%JA7u)k|~bu*hNqO2FgZ(LH@jkRRQCf}`!?9IS`LS(2VTI-?YCbxR~5F#$P zET=}_o!vywO$Gq(v)=_jbT*G2e}Ba*rr5j(@MY@vJ2>G+wK^h;4LXQ7Z_EQcp2H~~ zj;)(Rwf1FT2Kj!hn*$@%GAZ7gL|q%gr$o&UgwZwas6Y1pmM^+%xSl1Nmc@5hV%X?u z2v+!^qXV>qROunq2~7)_0hoD;mtixuG_HyJeLCxvnl{&BRm%`iWxib%SzeAailHl? z7u&L|VieoI8hnEtFB(lcUfnij+EvlILL z^q4X^hx1f%Tf^2l)@az+F*hSE+0^zNnBwBCSrZRWpvuiy{F$-DJRMr#ncJp#rHfDN z>L92@UhEqX)Xpgfv0~5YH>94m@F!aPsR1-A&8KRESNuboqO2SGQ~9nljt{21VrF-`#)n418S)ZAx10o{N4 zH(%{VS+@@^^U%vP%2-rnV_cZ@X!%6bN(IQuINUeClEUN9oxy|_A@hlDD)V0MU1PrI zIv-+>>}6}5c%+e{Fs&ahH5onADxY?1#>=59V!@!doU`6!W&Y;^#Ib&FRt?W*o2(+m zL{}}%Q@efU-lo5`CBzQ|9CPAU+6Q74Al7Xm$;r8LO&1@w9)O@N#`HuN*Hcj9NAHJC zUODbRMi~L{T0K&u~UEeUa z4Ef>2s8}wSOj(*2bmhHy-55wd=#~xt><`$z#rKT@?Bn_eBri)Hg(V}z7E`Rs&+PfVIE3e>XG98@B8LDD8Sg( zX{rE@!R0_K>L;yf2ZAOQ`p`yZP76LU^;?KJ>olfc%~(Bb_ydhlHyH?$!p)z|UEPA~ z0Qd?-9i&{NymwXg`r9P-go-Pyi zSYJ?YZKmM{0bqz=TK|6`TzRZ978WKy;r=h2?9d|;!rvdUic^@<0owMthV<3<87lb) znHfy>c9aqVYfHS7N|=8&d3yVu7~`P%II~aulsO(1;h#1~0yzxwDs7fy@{_zm6FtWk zr_S8OJBBGJ##&Jgj;W5bU6ryRIO{(LVUTv07a(w`7FNuwSm4>Q6(d_t-qmuscq=ow z)oUDZfwaDVQU26kS=$-`?Q#U7=F+-idlGAo1KMy%0T=iq4LWINSu3ezEVZbVa4w#s zc|iv#*&wAOe#i%TJcly}3<>iwg9Blvz`^PBj{Tg0h4;NjYIQju=2HMhpc} zV-OOb%Y%b^$$oede6lyqZl5 zgtHU|`0XbM;m^09x8YuycUR$yt6#s{lWvL+|HvMIlTacvTVrt~!Qp|_3gbta0h`^1 zajJ|JYQtRarNao$2z>wtpfPg6po5FEfWS`dtsLrQ;I)5`gEoPDvtbycyGNZmh9=?4 z0B35it0^T@jcucop0@dY8e~1!~*b&#F^d1zs<02@oziZWjhrEc>)CIkdu0C0E?4LPhO zgbylm+i4%Xkr4H%xF_&?fFVBqi`ihd|L~o5cmTs&FEIp{InH&%rQ4_OqbZ4hUir#B(i2aI6UP|6n`b*u9{;( z1qMrHbaK8wE<7T@J&@81WSfjH!STiiTe5Gb0G>-REU}J*1;PuOOKIb3~L@)ETPcTE-}B%4S0|+aOnbV zFc>Q#57#U`Ch$KmMV`a7)xD2D_%hR;L@R7-v|k(AcW|m`_UbXpjy1F_83wj*&9c(1 zo~AN^q15#9;PPr05Bs_Wrg+Kov#n2ephA|}#vU<3D*7Q!uWPzMrgJCj96@$t-Lbazr7gEem?P<7BM?I? zc?Jb|?JxrOa|T6+(jc9D?-6wMt`zip4m~tR1wX@x|6az zw+$erW_t71bmEuS6kQvY7+vvxww4s8}}@tAmH=%V8&J0sBtVK}JBLM)*h7~*`-HJ18iuYa$N z-##V5N6}1%fW25-lhPe9DJS`Mj}l~dpbsEW=q3-c>PV9v*{|^e_zquJ?DLStQ?}tm z8TYt;WY@NyW=lNqAzmFChFf6|L1NmTjNVLRvRb${$B{yx2Xg1*f`BdKC!r64zce>9 z3x2L0f|)WHcZ0r)(c__nlpDYTabC;2k!J`=mJ;n1>v%Rfdq!>Q0FgtTniY&hwlfY7 z#lA*DLf4Z`sN})@-A4{Ivv=2m*K=>b(3X4l+(koUx9D{bCE+?HLw5KBstzzyElG z1b1|dtI5kh4$+FD9g-8!wf;tm1hEvTRB#yOOlv1sBYc7RIo51u7x2!+Jv<9WtuP66$2!4atfPM^xA96?KHlO}RsWo~WeOi59E**McJ`3^fd3xu%&`3>ctRUf?5(4D zZr1seCWEK?`B66QCR#y^dd0z65CaV#OLq(?($t#x|*O9 zLs3@CD%Ida4RabdTh<=c*=|-2v~g0!2e4vxi~e?DN$cki<^fnn3SwFJL+L})Q;@!8 zm4X|o(mZB4mJE$W@9CD#DC%TLCO2) zBsL<(JllKix~X{U?7Eu0j$V{+%L=RRbzY}=`|o|Yx2{EIvnECGr|Ld)KekKA!gRI3 zl-T+d*yLK?ukO_LonKI{eN>8@r;+|`K)H2cG`S^#>MZk<34w?fiH7Zi#Y+Z5oS3jQ zYFj;ZV^Z=Ww);#bMkq1?T=03nek+)D26~Q?@uXK-T%_Qir z{Dl?vRt!=!W;@OgyQXmbu@m4Vl#?(g389z5zIP)L-NDB#r)?lii$;Z$Ri$Xt>tRGo zkA(F-W`UB!&*#q=Nbm%MlY6z&(`65m3TRKRD)}sX+P&B>)7 zA-q14NyZlQiR)PcAQ}vbFb854kvM79mr^)lqA&iAc&`0$Nejc=1VmjqLA;cVKv#Rp zWYSO(y?i>uF=nYFppk`*yypMsGkZPi9*DnXM% zto52a;#Z^tht00} z)z|L*29|X3Jma3sWUn|vvx-NxrZHWHmgpaiqGucqlo?BXQsWzuDfIa-{J? zwAk9~;XNuVGvpAwodVSCSStq6%h7I=bXg}4R$c^C5^y3P`{%(?HAcO)gf8NO0_BVI zAi|n57`2kj+dSBuGlg`Pg9;^y^%)wY*F9DzVre&LRtX%T=5+__f{vOY+}dmhFG``o zamiVpD&E5{a5vZq(h5Jx^|L}z%g9zJL^!sNdMk5_pgPoZoFfj;&75A~i^)KQS5xABSUOTg3p z`d9blka3WnwqjTsgy#=Q&gR*jiv#sFr49SI52eN%Rs5Yk1=os-qQg?sGo>H+Q)FZh z%UkzC(FHgV;N^u(*>2K&f*;t4m8?FierEO_mQcY7l?qc)k#c-sz@rGwB2sk^2+-;g zd=~uMCdMe=c~cP)#nS)W` zL=D{P!F8jbMGR_>GL`{}+loXL7IW`l#V9KA3h<)f-7hkNXr#Q~#?cSaa?2qvZZ{_3v5)i-$ zRo6yDIBl2>?Vru=-_hu0nD6HZ$`}7Fz@L zC~4$R3@>>FT5fgO-o^QsO7ICSDMYZuRX-!PZ=&e;E;t7Lh3WI-s7HWcn;0*+$g3RU z*j4Cvl050c>&`U`|9pI{dsY@@YJs2Hlq}4**(j3bxyB6h;(#a}FwCmqQV_{Zt+F5Z z{>1RM(o2Hglh5({Zh7nALKu9JJDf!Zu7h&zTjuqnauY7}pA7p#L zZsd<~mNS|`0$;n(4J`b`bm@UA`A^Y0x!-GLg?zMtOAX%_qSt0SdNA~vjQV^MW^Tu% z0>YnIH~V}3njn6;=pYx0R8O1xCkFYp5H#}t&H|{%|Fw)PbTFa}ArqZva<;$(S6FZ0 zFBjz0+xY1j^2^DoC2V}xMr*&0gxrd=BvR4Xa2Eh-pAaciEl5eEVG`v;k!c92O=8+X zV*ZJ%RyijAZHk9dL2(Ze!-_LJmVe>>W0rRwQ^+Y{A{{1}-(hu5uo+yMw@KY48Dh_h zz^*gtKN`4Y_?s?y1Im^CzEH+r?Zs@~-}@cGUh^wZGF!Q{R`X67;S_t~S%(eDcS$Ka zTPUr)yWSr3F+C@ZL1++jI1YyT(Fu2BAWuSH1t-A2)M;5VG2S*hB*p8~;wq$6>7KO% z>5OH%_cLVsE5d>6>!Tl|q}Rfp=3i?pVo9lT-yFY{n*WV79CQ~O4iqG;NGNkOTNQPPv-REU$+pq{%;Rmq6ostN9pRA$;Y%L=vtEEM`jfMxLs+=+dpc#y?4t83A9^7w@moGDwH&AaR{2;yM6PW&t1&3+O&r*$n8_T0xL27rFG8@&Yg{ z=h|Yel!HQ%>HdGH<-6u6sX|2$Qv^I~)0!`92`Dw0J@&2Yv`KsEGy^=&7>b zuB00CV_h&J6{wL9IWQlvC}CQ}b?>JJl$LM-Srcmex`&$W%1#~x_;)$6(*sukBAgaQ zW38V)yyZLw<+Arsq&D>?C!zOj2MD9ADh1IMn!R1UohD*eK6qkK&-VnM59n&yf^x#* zoS>__9?-xc$fsn&_V`O&dCvM7$RN5Nn(Ik*127N1Y@M+fmqU{49D`BrUk3foKy>oy%ix zi<_#PJpAc-J+Umu`rr(0zl0$2#^kS?0EImCeex9Y|XF$M1tN;QbKWqSY0e-Nn*_ zVx32zX?YLdp{&)pdG%w98a|_gEQtz@)!z^C#`Z>(LN$mg{oU%Oi68O}0|zc?%XHLh zKL<@ni~<7-`#Yz65@eFo)m3nI)s!CnY(QqO2L2C=N4>OiT2c(1>=rEOnuRXznwzY@ zZ;VEM0rXm}bw@A3dit{L*oKHpyDFp@ig(wUmgWl4@WJNyOKk@HmS^4DH9BBNpJSEB+ zT++hn;6?{Gn^`af=CWl)7+Ns^vOf^n(eME6w`A&0y^m!3C_w-=Ep4pq2Sz}*k3wIz zeN)Ad1Aw}I-g9sQLFp+ub=2^3!_hfcic?_mt~VQZ=AOFAw}xG=z`#t!WLa(5d>caV zs7#`Y8RCk`pAV}!jPmF6@m933#jrs#AUG?T=$UK7xb$_W?d~h29<#iornc(UtZ*tM zIeb^M__(2J(!d?sXwNAvHyKxr($k=`&mh)5YU+4-S9HV;rTtI@=)#=|<|GY5WUdv; zlfe>)pZQgumRm4uxm`FhPncLw?VBpvE{Pf2g9pg#&Gms&iWQb5!H3t2{&X*0;<5L7 z73`XY;sTXZr8vS~8toQMB%;-VM!?cO zKLc(D477kIZqST4<>z8g6Zwa6Qb*5%jO*MCm$N30 zy+H1*KY8CxMmi2d6%Z}J_RaHEF1q?|ym9u31#oCl#Q5U>=vB)Tw&N%FbACRY!|m;Q z`%mgRVYc|i<;xQDsq!@pzWW!^%}?7MR?Oyts8h2*VX9$qPdq(qfF;8rSckB&r0kqn zXNqNo0r?ZwHqhYXA_tINtb=PNmMtp9=Eqa|rm1a=%hR3+h$!LUAkiay>3Ab4WI~VJ zVL$1lq$m@#4pgxibOK?%yBFjp1Y-};rpfz(W1S+NWRT=Z6z_#*s95a zak$bb{mrF;pVbt+G*-Fz6Sxz=?c~NlQ;X@w2xg~iS-EuYpP(1yD}Nw~`}%Di1QhY_ zRH#&-Fk(G__P|Bx#@+L2RS#lK7+&t4yAN|WbkrUozzN}G(^qw+U=81RiaKM1;ikAf zLUNXzDow$<_#i*JLQ<2Bsg6txyFND><~ojdG)@5+ZZ8Lid>Gp3qaGg4cekSv)2-b* zjRUh3F7y+(+^+K7c$jlGtL7%hpu#<1WhU<1>#n>3)HCii#>{MMIi)LXHI1OT|h}4(9!PBecSVk z7Jt)eX56ORot^xt0%L|yEZC31`k%=%=^R~n0&FclvI(4~lZWqhM5pP%%ykV?S(|hTf^)arBz{8U zX2>~IEW-_AtmHJ$S*{NN0XJEsTKFF<`cGJcKv0`~aA|xS_pSrZdv^TF8Dbcqhqthl zw;PjZ>ly7wITdl#q;I|HM)-IcGRxtMz1Gbt{Q*dc*$`FYu0s`TTykNbXq!NM2*F2m zoQE_HD!J^xEiolvMh>XY2VxQUfyDMox5~&W^mmO}Cqb3jD5FI{MFH>12P&FPnr8|(z@edfjZ=O&T`g5ab#xwJmz3R$P{weD*< zLsqi&a|tJ}T?=@pkL?3-;jPCl$w=&R{?2P$kUu8Aa++T`Qx|m~EsKw&cYS^*&T}6= zdgqn-217u77p4MEZzsmp-urGqN*=?;&5P@;i8895q$E@3tYpT_M01-oyE1W>X-+pIQ@Su4Dftgxb(@ju}A&X(ieeFM&!$(%7 zo{DJ>&x=AZ*#6SDn21S0(|T{&Bd(^~_bpy@L`oA&7Q6wD@sz9&WKm;+2h-SZQjs6p zQ=SIjB`JrFZ{{}Z**NCDUTn)FVXwCeSr19GqSpKrkk33QDhGaBVH&f&gBV88M99NY zh4=jKA7#oksN`;*wz<_9ceAoMZ};7bQoW0R3{?@E{aI#~%-By)s5_-UqA{n^@EiM~ z?y}0zl08=%7rc67^ol<8U0LCvnNfPOtl~~1u<#O9&p}fXlw4v%oQQJ#)uRV1Z6TBv z_r-0V1p$}So9bcXfLjxw8{QWmk=^gV?eoANyF;lBB-1d=U!cuf{^kQP5~CabJaUa& zFp0u&^MU4xKT9ZTZtx#_(bCO>E31qwy@mYQBOn3Pj2Wlt9cex2O+FkwMgq)l`S$au zEB4|8412-I9QJ8}E$i5}3)0aTthK2mBEih~xG_YSPw#;H>3x5wfl4%dG_(SJY+`H|mT>{`Wn3$4Rc+9OWyX72rOd_Jfe!ZF%=x!Al5i{Y3CIfgPDkjT|0+3~qZPup6RbWNZ=8~h6NPg)l zaY)?PB{j>F+GL-#a!Rp~JuI;)2fV-kKs`-)4iva5R==sJA3&vh?mAgQ@vF6QrEa-_ z#>i)?gyR5aamY8iJnw9|#PipW~fc|Du&iUy&!FB{{h{ZFpgVir^S zh66ehlr3soKk{(Jg#CMa(va?oAt8jngV{w;)uiUg&8!qM5fx)<0Rg%gdMc1UmA5@0 zebUwFF_%w<=tkBF6js5Zi3LATu9@VwIdsq^0_kBbazM4eJ2>s5GN*p< z&BviR5^n=2T&48@&exW)WjO(uhfLN;Gx6w87~F@B;EYKa5oJOW7sU;u4bZoOKph8R z(2&g*k$gaKrim5VV+L5os&x3g*xARSShjkX75}zuo=xPcF*0*Jk6HuX^q^K%9qVO8 z(^zJXd;!vu8;m3|1x|cnd3O$-8_*RlYK)F%yBsXfr=cJ6TaXZ;MfG<|7BZ7^xs6NA z5kY}e=w_9iMFGJRDd^7Ne>=W`qW>aSa3z6C3o@{hniyqm6bKwNW(SwK?l@xEySG8J zg_uAp*ZV>dki+<%$Hl5OpZn*$XrH8~VFZ{EP%o9c3)pY|scT=CTr6l0VM+m9itVV7 zFRHnh>+W4U<@;=3^vvBT;QPiLx9GFyc{b7Vr&M;6S-f~qxQfkzdObYV5E}TBRw^T z%V-dqv`eFk=zdFF2l`Wp{*XQ`-u691(C5*2IpH#4J!sPjruw7mKC4PcY)BG+0Udf`S&So6p<&kwx}tl}PV7)VZwj3iXo2`OsS6WR-+cOh z@etZY@w3c6Sh13uz}28mPdCcW@7w{=BS>-A1I`*?kb`Y?09oJ%n+#9{^!0log~o*q zG%XnWV6oxfwy6u8pM_^(m?48{&2fiH(SUi={u9>vQ7*T>PJhfN=R#SE7EW%X%y;Cd zd)`^#V0nP_U;^Vf_65)n4k_Z>&B zoo=!tp*J*@^%4+zUfvHh-OzQX^5(W3FVCD8-PcKc&c?Ek28st?d3WtMCz-~}Pk(r) z-#B-Q61)NyvH!bQ`lSO_pVH@n4}Q>{^STXwps?o+R+z&A7kmt`qy?3uY!i$N6T7;) zUR_)$;a~?+z!$;NGT3ghV25xg6~cPI2~)_hegjN1VQ(cYUBnZRfWsb5a7EA7RGJhXgL@LHjM#q1ISS|_4L+|UZTjAOt3M+EmHzV3|u80 zzz=m1mbfAY|Iu`-I-7FVv-{p62OjfW#0M*3PuX~R&$oHRiKPScjVu@r zweHAhY%~af)>1d0J?;X_BS%z&QwU4n4sUEwpVkMMehg|HefLkF)~0(Ix_>@cO52UE z^|Al}LDkM51s1GK+!ytIs>4A2kfgEcRX+(DNDwc2isURnS9?Q&4w<`JFj!)v68B4P zMJKMUo!N?m5DZKmVKD|cs3JGApUkyh>cAG(mWe}f(nf9qNA7T!AoGS;I2%Vq6+eYV zwtsal`Ts!%y7y0?r9f${G$++~Dw(GY*42#*%&_knGGj6-*x#6w)Y{&i=I)u zt~+45J%HR!3~srQ$-tQ(X|)c!-aDT9?1#~AgK_P6TpQAB9vOB?%z7)}RM&KIFaUey zFK)s*4Y{$C;lCFO;Dg%x^v$UZtRDeqQfNDUm!>g`b;Kx<$UFSo8pUb(y9h`e6#pW< zDCIerX@M@{w;>V1zH5rIV7%4jy9uDgvRZu;N(xv$EZ?E1Evt+U>zjTPMIT-lAlZX~ zTgA%x>*ksM3n!Kp=os|H4*@AirN;H2p1L46VnK(%f{L^E4q5{y0?*4TqAN_$?F8}N zuFsWH4CvVUxVB0+ARdU@+faU)GpFHZ&!EPui{?IN6+~^Q9$P!cw1Vdt0b+bEIW<`w z7k9$ykg@xs0Q(ow{%+3IIBBovt2PaHN$;S*hJt#pQ#RPq1xW`N504u*H#C-kQFzP# zp@dSW0n7Ut8)b$B*l>0+k5Iyar5r8W<&>S7(FwocvDB>!ixA(ciVMM7(Sp)aZ@&x+ zNv?IUz}hJzSSu5RN#;X4yz5R1-yZfAK*^mjtv>Nwdu5(!ePaI*WIYdO|OL(3u`>ceXpYEI>-KVM*V1yM~ z)6lc`!HWETC+Nft*QnL{H$OC!`$=F#DqD1nKPWFO&d%j&ZXW{-JLy1}x$kyt z7XbW^<4&$aK$?FSRG{^BcG(YGA}=Y(!zpu6N!2}}Ggbddc^%#|))@l0XHT8AG^iR1 zJx~C)@C`HlAKgE!m+^9?uBf;RrljDu4R?MIxq%6aHWEfS!pbxZ#9Y#~D%&(TFjnwI zCVJH*)cXDJI0B53-AVucdG)uu^=h-#r?^$-A8La9%jaTEMrB6atczn~Dksyas-G5>ceceFezCTee5i#7mzn7L0wqDDbidqzh|g zF7|>1SHYhY0sBpUHiAp}bgdk06arcR3)SGM!S>bZTBf(R_ao@RH;gq{=Pd3rRzgTJ zcUK9-cP3{r5c_z{#|D^5MRNKq`~Ff<=({3%!06OA8QFMy%i4aE2BPLaoj?qxF*6_n z2LI=J&p3=jRnOpqhliguvd7aAV1^lB!ikjLe3cv9FSk&E5291YtUwyvHc3suNZ z;MrWLl&i^)h95L&;==Y4BYYGnN8sXoae4@gFl^@XC#;mpr(j0{tVW539l+Y! z$kiN#5g*ly!QURd4Z%qnV1p7PuF}MQk&u`YtY=WzQG=(GJ7NG>5S*=Hn?_PCYba$q zi`T&;@BH}4b$76aen&o?An@QkEhk(fXV}d6;%G(+9W4yn0nm9l(}!kR(BES}Ieo3c zgEQsKzIZVK!)@p4<9O4LFz0A)K6-JKB8Ak~K))}a7t?se#v1p*mJjV|aI@hC!x%x( zOo>Q8EA(-og(y9RFwEAW{3|~ z@rP;OF9KUn(^{92>rOPawa= z9R@T&Ruov|;$~*FpcDd{Glc{R>mxbjs8VEMnJ99@8!s>KEuk0LipULjqLgpf-63hh zg4WD+*m4Ml?8xop4X*%o?7OAIsx;2Du3B3#6Hk!7J$%@r4D-GwunznmK5)|Vx3GtQ zxWiemabd@GQPAkG%A?yo{-&p`|DS@mW zmaB`P8QNEJLMofLvfe3Z|!)B?FUwRO@oZE_aH)`@l5TXXM(4U3-_boYS5-NTooY zl`ul!*nD=IQ}BFd7(O)YvCcH^i3FqVS9=$S!*8q-U{8}5kpga-08gIi2;$YsXL55Z ziq0%NY^hKwREkV)fFzqou$vSE#jjjfXesWs9&OS9Y>CNY9)F>RW`m*!Y<8kLpl}FTqQc5_oW1Spw z;6d!XP)@Ar_WyYL?s%%-|NXSgP-dB@?1Up^3rThy#|mXw7 zWUuV3NWbT+_vicj%Q;jxUTDdUbTD8(8wV+i%CgIY3`!P)MD^!^gJfM zj634`N-sUW7}Rr-6}{~wsuX}c1D8?2@2d8OkLU(yn$i4<>*0^$Y2gMi;{r8o_jdzy zD&Ho#oT@=N{`z_t~}IJ@LKIvaRU&4z1cx@pH1QA;3S76CVjY znq7&2QiKfA(((S1cA$YMQ7(XiYdT* zx;j6&sDk&A3f?$^2uhqpz+MN#q5bkSBXBeGK56rwkI~der6?O|*LaW>Pj5Pa%juN& z-{;^*0k$e9^Q;o-57|h=+rUH_f-{t>hZ296{=PN8_RpAWMbCPawZ#yP*pGk>5MqK{ zr@x5_KmObYR>}yfbOer^dqd;Q`>ZlJO4#_WR@dX7Oh>+Q-2>H_)ve<3Ih$&Q(Mv!_ z!V3f0-t9Oi!S2clXn=Y6{8HCS2kCjYq~OvpK*RE+GAGN%mnS0B`aGBo){^5NlxS=k zJtgQlMFcURfqY5zWR)Y^E0E*~YCqwA-`t3E5?ub^31(|WbS31Gl%O}y+wuBNXu#)X z4b>hj4uTkg3L0eql%l_8xN$;{AQR`Gey)4FK6Y9&Bg#pSr}KdD?$D_N;tI6Xgt8?k zKeS##c>?fjfcpf|wqwVe3PN@4^OARrZytEg0N)jFq77aVya}1$JJ?~&^q!HJKfx8j z7XelKX$A*o^8or|s2hq)N_NWAns!Cu0Dj(-(8m8vL$ApUll66LoDnnYnPc&gkdWHF zkw-B09s(>_by`_lgR$2(!{jHYkcmoL@Sl_Br1VfTmmF-p38UaVOT>Kg^V4bqwmaG! z_5OAq2r3R5gNH|69{VfCL*059*}wJ)K3(Z7;aLO_Z*L_U2n);R0AX+*dsW6(N2fei z75LfwcW?86su9fIpBEzbX{%ql*U4hDGWo%A3{ZZM4gCzS`aBFmb|3~h^=_;zF9&4W z?gNd6P76Ut(W_*}h{egbf#NUE(I12r*)|(axBUcv(TA&mJ^;`LXC3M5Z$X2Negl8< zIF99eaG!?=26r8QBSZo8W9uVlCi(#^cRhL#1~0qBj%A+7eu5r#@4tu9xS`~nv1Qb&~e zzm)(0TnLK=Bgn(3;4?j@yOt8NkUzkcB~vYrds1hn25bqS`$7C0ks2R@)lTQlqGwRw zgN@LqLU4E#vY*ETe>?C5{h%=i{&>hDDd*VH;S|+nfh;c;&mp%(jDv&NcPT4H+B+4%z|nLXrZek;RVUI=1#OYp%&6E_Dr( zzJ*xaCIjf#Lg+eUiW2hy45Xqg$t5`XITsOrM+e9%xYdZ{ns;A-5qDJBP!YEVusP%= zi2R{27UO<9cEIM+T7)1PD+syxjiiz7-4qQjB~0IXhnKBJX_kYTpWPAvelz0E zd4o51+?dttCIW>cL13p{4Y+!wxct-Udiw+=0&|`9;QE)VS#|SpC`(cg3T{=DT;k$1 zc7sFR7L3m_D3E>Lwi&~s>E^M#{W;vR`sSzS832yC1bqbg}KQb)n&bl>i%1aAff}fT}vp_a;=* zU=IVH%I1k|!BXThfFl)+W&j@h55NOAD5AN758Z0y3w={m_^O55P35$s)(?rUm6&dyS1K73gghZ@j7v)iVLmoKj5UC zYm+!dv#O+myV*lFa_}+s_|6GoXZqF8r`L%S*&^rm^V9^(JeWYjT)MbD>*FeV8d~WH zTJzB+0#)+o7K8yjL@y=8B#&j%ahiC*R2%%7IJ@4Bu6+bcNq3 z?GpgmkiI1}{Uigw^QAaH5V;tZ_a-#yWZxzR@G$`z1YjAZQ(b{^D?gbW+=(!nB*TY_ z2DBUTUCp4cZet5Xn(~xbq`2Xfo_mT8OV3lx<|}I&5<0%uiWU@$c{fhC*`RMhQvGpk zc2d#3D2c`Q%=&hyFQlACsgUp`P`M#`yI5Ei)xd4z4q2DlEM~GELN+N zZrC~hGcU#4PGR%j;b=a@oRKL>4vS;*h4^+zDcYCu-DYrY56WwBPXQRJxcqVfZ%E_)6e68iHm%|#6tuJ67f2tp`k(EE10NWfAa*| zRAb(b(^GK(@lIw)ynleLVlBX}x}QB0jCq6d^W(FjpIQP=!V}!}5Z|@K&wXH~yX{Jg z#U6oS=wXkomvrTX2{g4}U6q}k9VS*S9cY^oUnVC@bltyKy>WJlMBek`;M+-OyNlw( zy)Wd=?s6EuwY+SY=v7GnQc2GQO~Xtior?7+JC@csnD{wDce>Q{Tv2IZ-0=$NMHgfb zv*cx9wp`1Rs7Xcv0SF{We$$ry9;!K4g2sSjvY^cRX#^{-weu=*Dg#ek&@*e62K7d% zn7f;Ku2Vl9YzumQ>L~j-#27Q<%Z_H`W9#eGp0Jo_qzy(&iZ}&$JaY0X*I6tzbE55Q z6X07MxV*t-au|?w`~AETlNFtkSegS+ zULfiDR~AD!U<`i`+q7a^{)5?PAD=Ef?%(@r-k6d?=cxD3<_)}=k@B5-sYs<*kcLP} z218C+0_tjrpS*@!TW%ru`;2lnw#GE*Fc>!{R8LPelbz*|{H*9hYUW$>WEpDxsSN$A z9?^-ZZwsoigAz*O9}esn^ts%vOuHFYiC%`rZ(+)FW=WKs;K; zwC(WCM=x-Y(ibHyk@=OTSan0qVjqA9&&l~k-QYamTgUSXi$wm8lc;d{TT|X@2Bibc zc+wg>Xq%p_DuF0t0XmdnYFa0i?1xqP9&uPN6#+}n-|t(;B?CS$-Hs{N>S(Jfjb14C z;|m#M>u56e+vSy={Tb;C*M`P!T>e)TdlK%nGy2ApLp6o01yXbMi60ZS+2J)sARbi_ z-hW2D7ZAI-GYD7?XhVnbX)d$RoFK45fWSceKlAKxb}dJZ9g&5YnwlOS^?CQ2r9E#2 z-hmK~Vb~nBFaV1pe&UtWUP&%fZUIm~|NV*^=O^_N`6q~h(5Ql^+8h|@d~woi;rM9`Q6qX780C(HF;x@cV z8of%5jc?SOe{9%OVpUyBrqjRf<~reuPT9`k;!ao6XTiul3~Cu z;3q%bXRh)DNnYO-1!4q0Co@&w1e9a$<-`ft4-pZmjVBCcO#Z(ActEd?Fd!@%vP z7}~$<_umlu9kQv;l;LtVqyC*7v>NW+*LSq*3@6(YDdrxGd>}O(faH(7PJ{{tgTzt^ zd4WR=c=6r=cWWflpSA@Q+kQS``TI-dFIWy<87+F8^N_9f@V3%mV2dGuoZC|#n|8pl z!$Ap~gTf50L^e^8kdk8J<&_FBocdn4LKs82zZSS3n@JpB1((K5bq;8H45)$2fZ{{9 z&OOGMmwlL`IZs5s3e6Xz*gbE7ZjhKv&WaXk!+eG;(M*CU$u0)^jcvTb*)P{?Z7sZl9d+3-)^}W= zQUY)L9s)6L{Kp(|!-Ki@&I+>FM%YEn#q4l`asE)wQg zy1OT?tgLMA@gspHMa1RwXIfsqJku4RaZ-RyqrUG2+M$-L9a8o>Misr}hjKR^y zAbl3;Qo!x4v0Ue|{Lm|uoO)TLci361K{YCk`MX1PdAdur=3F1i(}RHb8!m^V0msog z&!wISn>$!?we*ZJX$A)k(*0Dh=VSFE1!>7hCrl7~mWErSL?1H_ilgo!c%k!caXTkZ zn7ib~FSh^DO%{uU3-hE=%BfT%zq;lBKeTtPV1v%#5Xs=(4V7t?O`BaZbmp+eMe#EYApIRjN zez+Y|)_zY(NaiFm^BoHn0Xi6;X5t|P$L!n2MPJP-XMoAtsUw|bUM2P%>7o-T(3?zG z5x22}+Cs52c0N7s{m{?dC%S1)ZWt9<`7D)vZH~Y|)yML-TsEenw$EGc zgt2+=Y=woGxNZTs$;%XMz6nj}&bl#~sIt8%PbrOIJ*B7cM;QoOVAVD+xvE=icgf;% z=QAaIf*)_^!PTQuq-5tL+%PL7KTkR*mudlLVKu4YEc*1z zY_@NoS{Dpo{l#Dslw=N{d}vhuo<3<;T|fAC_#oHuV(rKHMMIP_mrcrk@#A2{q4}5u z@|gV>A1va`hUv33(zsm`bJcuCUaMr@AQBtUBCXn4nYJsEXDh-YX1X@HKwNq8Wmp9i?V4V`2`Qov!T2v!-{| zE5N?(!VidD9-|?UlMO96@`aD4BYyB`&uEu-*^Y8dPFQT^4rNc#fWiz3Fyo1CuxyrqbiG{Om|5v~)>j&yt#gV>GQoOycJ{h` zOWNga?pe|4JQe2`C!AZaURbz%8T27$C>nJwo@m_mjCDY7{1?Y2LvEUYp&108&I%AR~&;hNDZ2dwhRdd%6PZWe9!=AgtV306b<|2hlU z&HtWsR#!;4AvGk)*pS7?;LP3Te!RvA$WwD_G*}#4YS<`o*|X$VD6B%C+VHolH+S2vAj)}agQG`d-pl-=3_;oRZ zgYo!-c=^T)-R^ss1rNq%J3^92q@S8ze9#Hsvj+rHESCDyrK*1L$6P2! zxB{K11f)gnPMMH!{~q1WN-C4Dnwq=B6q$m7@_k5A^$-M( zNty31+C5qtG;>PDx{Gp^!^tZ9O)sI_vAw@Q3r`}@AbAFhEhZ18DV0;l&OHA7&8TwY zp-m0^uScE*l0!poCIUeeaZirqf*%PFvWMhl!a$3GXQJ+feH!I$8WpnLL&cYl=<)2X zNWJw|H%On%y0g{6%2`NG#%X*#^8K?$sV6){kUqU?G-fI-vH2-DADc4YBss9_KY9=q zuHvfTx@(mKTyiI!v=LyGhC#jmXkg^wuNRRl* z@Z9EVl+b@y8D?okgrCP3I~m5d_EeK63jemv&fckADSqUlfASnu$igX4&@?Rr<*JrJ z_zL=<@gt9yA79RIbazuXFesXNxKTAZZG8pJ&#QW_t3Qx>JVc1BO<|P$U3RZyrzty? zk=DlRv$^H;yTe>v)od%v=HY6A9c2Ff&H>RRHxr>td&R>7am1+-+#w(J{p=;)2r~9&lWbMU*~&v)MXw_W6+LQ^?Vs zX;rAiT3wSoGkioo$exs&6}~Yi_4?|+3iFaZnHZY4ob{ei@}q>uXgTOTIMcRfyw zm66r9qXlqFhzhifa$3S;@g2x{DVsDYetbT)IP{TlY~jwh z;l(yWSMT!fIkgao-o7Z##dl0ME)L2)8o!dvh{eV$jX|(|xL);hh)&?9o+;+CS8=4+ z`V2n$%?VHDxZPOcUam9luK=X73%bU+@J^B0T5d?#pm1ro#o6bvI9I9a$}I8oD-{iX zQpsuhDYFCWw7axr%%DAjU42+tbN~}@q-rFXh@^SZA)2O`lkkb#lkswqAxZI{Skl4+ zKQcp(3>;)_K!eKqwy}h`zFO_z2gR-+y#O+DtO zgO{&Vpx@vV^2p<*D9D0B4}Ko*2l0YqI=B!xEt>cC41_|a>%xf~tRgC}-hFZG%-&>v zAfxEtusrVqpEO_x0gD<!l~Q0~4>ofl#QXR>jC~q-8StLe!HG zE!Z92rJN|ICn!!1+3sC93)|lK9p!IdR&%;CJ#mkbADRWYwe>ot%K^E;QfTvSsOY?F zg?c_z^IgUy9afVr`7ps9gkJg}Q7GjVZamtMi@GY#&RdEEG(mRAuI!}zPK&oy{k_`z zNvbNp|7frwGy73Gy1aWFdaa3Ze$P*pdp$Q#$(Y+(2^**_T)BjC^}6d#4Zm3lx)_a( zt)yXs-Y-idx&#;oW*f!TpwYp%XD&|I2KrZ`upR|4$1rs;HL*K-4#3j9iH05K?e>eWRZp z>=6!Eu&y=uqCQ~AVPHfbi=P(#qv5NQmVfJqBXYEYS>1pM#Jx4*Z@Iq?hv&`_Db2vy zl*S}PC`jA}nxB_1^0LpEqwkTT?hsJ{7qEekMXv@yGh=4;W4h< zDArz|#YOF|;8E?MW9?Nr^&)-3CRB?*4-ejtZilYdTow!^hkmz`J zubIvu6*^L^M!|FpxIpyCfgMl=$ZF6Qezgk#a7}!Q`p@no=uQ$9-h?GKzwh|{4U0pB zWu_`UObT&}CKdP8fu5@vx)Xqe7R;Qulv=dA@2hxJk#+PVfDx?L&r02EVQuc!UG`l5 zBEN9vK%U)vPw;8A=$I{L)9nl}AfAoy;>(~^V6XQ+3x{DjKy=b_do!F^>!!;?EQ~Lk zg~+jf#1|K0_ikh!i#`iw$Di1)raBg+L;0j_Rx@$~Pa578eECs>ee5XZARrcz)NCvR z#$rOqkO1zWvuteK`hM}}JD*DaHyH!erY%@S>0-m+Zm_=dU!9)&dj%GDBol5whh9_|5*)b`Y73kj8F3S-K zE6WZe&|2xz_LItvda?1GwcqejQUaYHs>z`+<6JPz5gng%a$HYk7@%{tQWviMo7q_R zK^TgvL&*n2L)B`ETTxvXrnKZ8j9Bm=m|Q?7Fd0bV*{#m+yW1PvJieGq*m0?CY@639JT8u7d~(SP`~t`pZ1n^=cq?b6=1hJ|2m`?zy=RVpz7DZ$_Wh%Ln?h<>BH(C1yF!D3;-X_8@Vn0VWDwr3-DldCnK1$ZjT4 zd8yF*iga2JPt;Zz)>L)v5`Z9Cy3vGw-t zBX?XD;QCYQ%Pu4rt~4w4qb7|M$nB|jexH|YhZ$6wR0EURom;;R{XHjz*HMIa?~DT| zqyq6-H!*&sv#%eX&SPo06aJm=WF9>X%#~WueVq^;`i`yaxAe%q2(0yc%}>!HT%WSv zagh3(FAk8R`T3l9-BT@LV9H2B*FAs;(>IUchr>F5_L?#kpo0^g3>b->LLB`>+XD=u zSWC!Mg07a|N#}XEj_nuIa#=1LV4I%qJmGA1;k8g8eQ2?#EwbG2C~Ad3)(cVwLt%IK zOXn7ja3EYFY{CeK=q(qxKjQbgWg$=XW?)h&yP{r7d^HIawdY-yG5?i}Pd;=)go89o zegW_~%|*3ZkMb|WD@;^`+k^BKq^!zFFP3A$OMkmJTjEZoK|9-X3`Dx1s9Oi~&mFwN z6JdNGEP67Z9`+$oD4cR=iVrkzE~`-NUwaO}ecDz%&jpvyoxbQ+HteK`V%8f5GY=*Q zs)bYD4kB^q%r|R6$h}A2;?oP>lc^>GCPvE~{OnjuvAT@y@=I3m>`c5 zfaFIVH8!gkNH%)lIsQBxPOJVPyiG;sbyYuDySU--;J z92#t-z=m#Q)tBieExZ`Ws`dJ}5Xb`JFI=b*2(;74i} zcHTw#!J#ZS#54ViV~#4yfyREwQ_rgB=D!MVv>G?%F#mc0@n!zh#)tKo4)&Gas4SM^ zgz+AReDSud$%(9FoSg)_#R>=DQwlBTU1EPjmiut$Er5C}quRYKTAlY3lw{#z*OQZ? zqcZkRHkmtyt8g-*m~*~H4D?WkR{;FDR0c)kXDWel=?kt zurYDM*;LS!Wo528w_!bOE#+Y)TeyNjXP82*NeC^t3@eTuc9NES$m^8+@+8y8Om?Yd$njqX8vzV-)AzI zR7MU9bEQIglG?Bj}A%P7Y3y7@(7@!y3J05HF>Ynz<^Z<)V>xnMh)LT z`N0^vCML>sFfy&@ElCNxkeI@tPg>Yj) zfEH+c_A9SlvTLEiCQ5!n215ZlNGd`!ykn!rvr9QZPMt7jj?=;JZh(^uaZZ<;k)aa$ zruh#cLX-|)gDe_mT*Z5zw9ly}747Ssc$Kz}W(i1=Mix5(ictGGez7Np=;o2&O5jT_ zOXcL-QM+W^|8n)`9MvFIWSQZ7SLU1n?o8R2h4ac~pZF}O*2dY+RE)k`C(d)iJ(;&p zwR-=;`YBHtxTTf_4x=qyv* z!@a#dzd%Bw8)c=st=-X5#UD2t+S0vDFC;l0)@IW*Gg@GBx-@#ME^>~xkCg%qr+jzX zwr8tVEipmeH$F>&pTv%<0|T*u zTY2hPTt@T5J{b@Qj89BfKTRdSFGtuB*r3KG4|Vy{@1alU=*4V-B?2Z%^xsO;_T>yR z#H>yuE%cd*hY$jQKnk3=a{u^qSiY9yca$YA-G((k0P2`EV$g`ic)m{4?R2mfmgMCX z!oN=E@*v~Rj~V@tzDgR;is2_Y8$du0%7y&btUFkWw*WNS+@D$UO0d#6he9fuUDhDr z;Vem;^s5yp(oEZ9J(HzQbxZz1%CPm5te@(#vw*M=Mlz~5zx{pyjPrXs$7dH2rpvpQ zzsa$)b%Zs8+r4?H*fB}dG|KU}v{#)@_K-cm=z}zeuz1~>*U6(QV9BEZ5%@^DViywZ40{OxiP5>{l8tBdd&TZRH62Z&Q*uih3wA{wm%WMm#CJnFjqR0L4O6 z+oF7kJIV>Bcz(oGGsWdxiT@-#^3rp!n7>(9xDgrz=)jhg0`B)^z>K?J+H@GrqpvNZ z!1N^w=if!0y31PLa*i)XD%L#xoOO4rQCj(1aj|m-SM$&7_#INNe#e1e3;&AISGOA) z5`bq7)~>CdXNpM?=p$k0r=u6Q-5$0H>9L3dGBGObnG1W<8lm})T}WVF-M6_`?XGRt zDaS+m%8r~QCjjohJ`;t?-_O-oMB(a`R`;{#!jb|O?=15t$y%yek%q)B%r^j6jhS`+ z&@Uk8xrt4S;2W&RjznrzQH-Vk?)BPv(WQ`vCSG$$z_TqhhG9=Yd2CpZUj2=e zWtw)N*#&wE$Wpo~%5eR^8%@G{Gq|yQg(;QcmD;-nS0sz>(K8fc@q8jC2+aoqo_Dg& z@U5HoWBuZ@q97HSW(cOL)QNzU04n?W@owY2S5&iA7bM$x@ba8-MTMS1jm7pwv2zim zx{5>5e^xvQ8Y7&&+x5TCAdS#iE}Uv$T)0$Ps!O9y*lD1teHgu6$KV^loRK6JUTz-% z0uIj-RU@Z*ACX_cg~9;?E@x-a-XBCjtp->>yE;=;SknKp^Zzz@Gk!5LAvcivn!^en z%m$v`D4kx#thpnJz_~6UpABxcK)aZk|2EW|bna<4RSqHthSdf5WMTisQmASc2vidK z?gb`Y`+Z(LM(|c*{uO?#JvyqzDjK9EyTupUAY32$b^tr|+LjbROFkkg2xSA#W4Vr? zSX9Rv2%8{5N|*y*Vp^8k|2)hVzx~6!x~c91faKTC=*9;-xCk}TgqSb#UN#(Y_2&q` zLh*|9qI4QizpOM);#0XhV;Vk%BKJm-Mh{UybS#{e_!KX#8bL`abrQ5adBK;IOhz*Q z42ZiDP)Q}!m=ddhxLWL(|9vhSI~O78<{_l_fKLOK3e+%b!@Pj})>lW*tvbHa?Y>s7 zM2-0b%$d^$rwR2T4ufbObjKwahCQCg9c_40THP{P)(r>Iuc;v-B4H4z=rer%An5s` z9w;gXN)&xZs@HFp`*w9_Fqoi3;TGQ($4(aEY{_4xhSa_gywj|QkM=AQ)^$jvG3-p7 zHj1pS&rv<{;{JKzF0(qM{!kP@-rxhyq=6xISi8s(N>acP3kL1rC%HNZb$PM~=QG)j z`%umjMbZJ5goH2jNHkSFLI_DED`gi#B{6Ci*=_dj415;E=sy~1(8+XKQ(+`HFYBqd zw{}zg6}NFzVuq1O@k&svc!Yj+8u2(cLM-fAfo&VW+S`#SsrC8m;B7 z-mQ_CcN?4m7KEWlbEQi9U2JPho?zu720%&Pq_E_jr}as2xfw?S4`O&Kw#Kteb>V=9 zAGi!q6~t%RnRE04UxE}T!$)OhFGy$W84)-U!(6!(C%vc*70Eq()X8G~P0%4B@4@bV zLZUpVMSK=Gt8+Vw($o))Gwwq{J!y9Y5DDl)Mq*IFD8Yd-dSot%Wdz8#v!$vg+JuJ) z5c*7DMz-BANn>G`YqxV`oG#>u{XkPxIsyD|swPm8{*{ghHZs)lHJ1XUCa+V({3w7o z04l60+tP_=0207RzD35#O!Y8vsH);7Soir!DY=z_;9F?|XL}H}UrRRQ*0i(I?LyCPV)p*@>HG_A+ z=-1smHUd=`sP_W;nBS=?S+}8HO#}d$J5&38BZoVuGHRBH&G^M%bS0#fshj8ZF z^8#X%S3#d?RzMa4WH#S9ZvYdc0i^}H7dk{JJ0pr+O$21bZNi52Jw_n-fzkW2TgdbW z&9VG_yjKy1t+Np7CU(uC9E98%yZaLt)x`LAh<5!WY>T6(8TYH#XOD0I{z@v9L*5aH zZelz=e@^J*ZS($%;>kBr>Ao-&eVS3E5&!pK2VMQaj9C(=KC;l%;UwDwouNzkKp^?&?;tQ%r~-wY0>OnyHL6;xTJMP|zJ7X)>Rc zf{8+xDdVTssg*1=V{wXdMp)_jtx=l^;@v z{bPP%CWBIb{u4HyqzXU|d!Gd6b7)OmO2vKx@IdM=mi%JV8SFv}zB_f1JRx#7Baw5- z5YAD<)z`|?6~CqFV|1YTfc4;9%IMC%|EIfc<+pOuJ2o}~2>K8_)N3FbsDw&zm4iCN zUG;sJ4-TP_opyrfaw*+A!Bv3S_(RA8x3AalB%l)h_+cXW=+G&8)=jSM) zJF)4@0V1D?j=#T^Cvfu!XM^WS>A(Vnd|4W-_(VPwhfg^;%?zOj-5&9yh3^G)>OIT9V z)W2~)Wxowwo4q_s?erbBw!j6s@0g7N0w|5IEeUnl(&qd883D!ws_AZTrH0 zg17#W0_WyY4`GF`jK%!-go19eIp3X}$gM`y{idD-rWXJx2kh$ctvJa0j70cNsx5v1 z>8_}smoeZxYbiTwzIv@CvuD`w6-Xt>WoDr4S1avgb*r7}7j+cyUk# zLkg)HxRRMEjTF$Mq~4(eq!Rd&o2BfKlH%pYz8PuRec8Kzi!n7;MD&~W_O^RyTb2L{ zil;(F)+({M*$uVWu9ov1fnizU(@q3-5a2pO~- zX*eLho*tS7VzWb>wHRWFzxmA|-AiYPluOV}4R-JlZ&^PY`gU>K<6K&uf79luEGId!;8U(dE^;k1~r7J-m`8}m!*<<7s4|L2bLbaMr(imzgFW^ zN0tIlozY?yxI!R{GV%#NWp7mtnF7R7xxw@69H(B>-q}KBR$zp_s+(~BetEN#t%J{W zh10~SmN&^bBXY(rugAHrZCnzBI56Q?caq8C>A>IPB4|&u3qg(pwTY|F#(X(NAx5X}rFnU5UJlcy;K4Z#sb0NtEa0b&HMN z+Lec!mp~*0(9eGA4iuV8w1h@B8tVb0G8&wofE{}plB>-oDLu)|7@5guFJ*L z-kMd?6j>EpyRHW%IT4N@ik ze&)xL$v0$QE`MGsg^FHvWq^hVX0K5Z+QA#()RT_7mud?bn$+8MV(ss?4uGNoqpsuf zqA(940D>Fwqy-soqlDJbV*TEMjZ(L91RxIITVDRzpl z8-zi3_dr$&0L5xWUw4-mYZsuXd>2hgHM`m`-0ykEHD;Rh2C;dfx+L1F!L zPYf6dU{^+|22P$aw?S$DDgVI&11fCn@F(^Eg*$juJiFd|N-4Q{Miv}|32X*AB`y7R`?3G=Q>)SyJdjyJ!da`M#I=TNbPK1>=l*P|Te4U@ zS-oZy9bjjns3$W@4hX2i7+WgCERiQD>;yXXD0x+Rzp4TQqR${j8^?3rg}Ptxy?~m< z-qqizmB0oN^Z_A5u6fuM#&Nl->4jR?%(hojU&S>zau3P;z;VI%@Tq8N0)H z6WMIm&Z`%ILjg{deVdU8WokZ8MhUMnAzp~GVXyQFwJ%baxa-XMl=CiBC^YxS%YTFR z&(4Lt%TPdAAngGzipXC)&35$2s-OXIRge}y-es}a0&Vae%GJ^v` z)$+NChkd`yA1cA5;!r|A>ezHxBZTAJ@UYVO1orWa78Xo5K;M#X8GN3q-fyMJYmFqW z(#^i#w0;=K*Nm?;AoBx#TDEF3uCxTAL#><4S|Iw$4ceAd4?N|hR6a^OMmx`*+O2|(id!)ka75r- zdWZ0O>(%5HX!_liV^rbh?vaKM!Vykdf>s<37Vvfq?~SWV|A{dP69V6pq4dup%a4Z& zlXN|-Y3>EWYvF$ped*0Rw&g@AMQoK;U{vv?q%Jlyt{4ddg%wxesG*yLw6ZIc{?a-Z5Q2Bw1i|*sMGKrTFA~(5dA>?4 z76XnK`8?>MG~Z2NgBf50%m$K*g)sNUCt~W0SafBbXE-LqD84#GMRe^Xy_ zKx5Av^Wy^{%nnp6jgQf|CsoV&Uq%Z&?-G{qDmfM*hyF!44x;tqfkvnSmc-TCk7*nTqNZS?5pn%YKMncue#@GM$vEx%z^Ph5Y!-hVB=|8-k|v3A{q zx{q?s4y+%!{l+UNcTZKBzTYZm*1rDm!jB`zGd~%rzvN+peC~r@gngdew9FwbF;(#k?^sbO(%ng2wvPgV)X@RxIWIhpJK|145ck+J&#r z63$jm*pO7$eV>7AqLNAa{IkNWgT(NckKBJ40r6zTYq|5C^yJjD&nU^|ism4&8?IX!KVs`-8tMUgUFr_%1FrwSS9y z=X}Jy;ve=r(oOAxJM%Ax&EwwZW#mQGg!$UNVra1v@SmJm+iA!Uz9aDH*9rBDQj9iN z&%EoJRYDvUf#MQpNo=%=pP>8>9qYe`{ofyL(b;T@HI&|-UWxgZGt?XmMdgQZ2*1+W z_Y*F=qt*@DUHY(Wriv_8h38)wS$JAj`{ot=tdRBF|ID!cc4Us#<=|y>^{deA&Nmn9 zgR(g|V`;g1KTS@8?G0_Q#%8HL+@85U%O?|8{7Nu=fX^gn-Oi;*!Gjun8wQ0!j^0TN zSw@!s3VKY~X8og$_Lryl#MA9T5B^X34_b$+gwvMRODTF)Y80zS?gdahAs;U|I4l1B z>CRlAi>oKkllnEIm1{C)L)tUAaRr<)w-pzd+OIbY{^y(m1e<);xfjU7`jWPND^1z< z&sOWsO-`}LpLKLJepOhk!2mW@J5TCmqhH&c*6|paa5Opg-cKq1zSr0KF7II{xTAFL zw8|&&C|6exgEMzk-S_lao#Z^@1a4vYMe(v?)qGCmrb3wBhrNl^`JFt0c?V}J5~T^Q zM&-Q^;%c)DBx6y+MDm${58j4y+z4ay0Do-f)n4_Isu5$C^VF40azTU9vbbX6?gR5( zFx^e^h2;uo-0j*5t@&ZUvoY`e(v@uNBm%E|(r?#!{w1`BlA(ydx_;V-y4hGUrK?m( zKecanWQ=zypQ|v-XyuA8nISD=W*E=L9wEQ|(=TdbGXoV@C}?w^m9jm?gD-mNZx9vh zpH2$S+lVv<4+kR@VmR_P8vV>V#k=17i?T<%^Tga+s{Vr;t72&qHNpxj<~Sa&b_n{| z;pV8i1Ld@sCMX;W_~*AJ)26(_ zlH^uVGPm7bb(m!ej#3Tq6RFYDX;L+N5tBz`mQyO2t^2E9E;-v*wJ}aa?{uS8z=MwD z1^ei1Stn1u8ORU20&t%-V7>(5HH^?aKxxL|YgA4CBsg&JLl>xkRV23%aa}R8;{oC%TZc_<&ag>JRZwl*5 zV5G3*R?^_Z?J{JYE_D7r-PJ^&7jcV(-Sf8`m1I)h&Nr5}!S-^v5YScXR+z?~&fdBF zz^C;^+EhV$9*4t)q}O>f@jGev5@?jEYD&$_eY_HQhDTL<`8s!+}G z9eT;ItlJ(BPow@)&1xN61Sn|5z^^hI!P5$ue7Rj~l+Um}>RXd6AsC z9X?T7U3@9@b_~%{ww^Og=jNV0)en!6k7ZL%rD8XM$(djGu2+mpTj-_j18Y6B)OM9K zhL=pwilLH838tJK#)%wzejd&i!t0n2#%PD!(U3o>48hW3lB)J*$An|g>Qk3$Fl=?P zUOFA^(W4)BXIjZ6{A$=SGXV_y?4FFSg7;~uG#*+X3Fv{C`}-N61=S}0jp2s3fbNjL z8U<(MqU60PfhxGeuKGdNgY=nMWmmZE1~@6&|4~dBdlhQ*l+5kkz21>(;m6bzs7KPN zw7<-W!O+?1sHU&taH@sFwlKP1ox=KTn}khvVTo=>@p$LX%Kec0-e15d$HwD<8TBl6 zBKfnHppt1swJ2Rj_12OAb_axCg0Po`>}1uwO0V{Lj3EXrY6MJ7(CycV=bzQ2BB*t! z=swC1vCbu%2xXL4gJEJDn+Jzmsy<0cw}{=vuPT@cn{DrSzO3wxtrC9BXk@WW_FK?b zFcdlAq&b1U6#o~-0|ztW(d?#*oO(u|e?(-mhkjNuquyRN|62c*q|!Robq zUhSH7J%uHy;jGO7QSs#&KlBHK_Bow24T&ehW)60(f5vUbqyDebfgsMRg*JN2p*>ZnX41)j^h4F>_*n0^ zWcT;)Tz{v{9$8iRpL{)HUnCH4Ym2W6dlh`vK_-AfxA5KD;)#7~fApT$m(Wm3w?|8R z3s;u%H>v`J?f7IG${jfhTi`(o_%p{C=t%6%Dnm@3lG92%r-T@B%c11d_J!%ext$Le z$3MGR-MQ+albtnt(DNup1-G%I=&qirRf6ra-G=iH!CLOI49>dyV6>pQUu z@$a9`o*O@%AtI@b7-v6`psOLzOjX5Rcr?srLPV)Ryml!GVzc`-t=vUfI*tK#VNBrM ztw6GiUd>)@Mj2?>h6G{TWle!1A1@r($qh!6FCU}9H}~<+vLmaE7D?_eL+ZWCCPy1b z1XQN>>bjPbO@9*qQhVGpqqc)bt^Y3aqbr3NoRzc{%swtZv(9UuFuPe>u9lt5ugdb? zCxX=Qo`xB!`5XIm#8_Mgp+0T(A7QB#%_*qmHQ65@OP{szx_mc}$)?k5de~<~DIvFoync1k zN>o2GDk|;MZ@VFfiM@FhB0(u;Ea+yT(yP}(323yTyRlsk{NJqYX5fMFcPu-=~`$-cM*5gpZ47fRc{8AY!U+YxSUA>~}a;bwI z4_*CVj#gWPP-Uma{y_4N%yD`DE5%}@RB85>S0(3^mL63vyym8>0I42E$oc%Cx8|O2 zy-#!Df`X=-Njeyq8h5L@_{+V`C&1*&6266lP^u zawMwHq;8+1A_r*C|8(*c z*SFlVJ+QFi3;5~soI^V*%ijNvW@oKkc6Us5#~LvZD^5tTRt&`dw03#6{$8P;U5O`V zlm-=cq1y2u6XC_0e>N=rqgLluj;2{W`?r%pDv!PpPnUyFe80tU#nPDkhVwW>7P_kf z{_w*tlf=N8yZd2jcf~#A*hA08wi18NHzBzt+EVoy5g(Rdt549s6?V}7``_^_Ey=M*_-UWvsd;egiwU+y~*f*zIuQE>+0(5?Zx~3JfG(| z=f3ZAKj*w;km&rzw}J%cxDp~-mm#gSQp^`GzD@u-h?p!umY>e1URukQm=PCV+RD4? zat|i9J429yP4NQDaGi)CpXKe*9(sYwR{MZh@^jlqD5t%!A z47KDTo<^Qg&tzwfKeNvX>3@U#JbE0>ZPMvSp6g$o^Z=DdO1Sp&l-9)G{rs}&(a+<8 z^Xx1N7)#( zyb1mgU(F}|VcrM!8X!PS@`=-qh1>PXO8#-a5p6OweSpbkbQmf$Z0U{Zw&~F*+j%3u zAcRO6+Z{i+Zjya)npk+H!DGGTVCc==X@=L_7VBWL2lLI|xYf%yk1x;Y#U_VJSzyik zvQNYW4&6kHe3@pZVvi1u$YA?IA0Dl?kH_R=A0nb;#?zzN6}i;BCWGrN2d?tGIgMmeYpT>q|3wI8cq(0FGL}#$C|HBEEMd=p$avGX|JJ%^Lz;WPwkTOSVav_O^qG> zCZbuU!%O8?;sKG&7rlvqTiRkh7aI{LA1)_IDm~F1CsAn|d0pJUxSMfnVg#<<|E%iL z>slCpo=7c^l5vYQ7{~`M$-?O}Ay}r^971B3_CGhml6?8u$l~@o3@|oKgG0FU@|f?d z=%KznN?LyH4KJ~AFt-n>G;~=r0`w z$@DF&Po0Z;ii$8QQ=TQX!ge^lAM5m-siEOxPEuvJf<}$Po!#jkr%g)e?ty5sa(L)a zRb368dhF|URn;U%xuEkgv{gg&9)KPnwzVWNG(}aA8fqfbGx;2qpQbLxb^aC1{kK*x z(QzNC;Ni>O&(;l+8E+p^6Vb9z-bNNGJp8`>$KL-QDeL?hK~B%)?KPisj5NkK4xZoY z8pd~Bl(#wW7y{Osanq0jGplV3z;G&4Vx6R(vghQV3Q!OA-jUO8I0&axORB$^$0#P4 z=Y>NAfMwF$jhf-NWwLLC1h=VDe;iJQUWhj8L1O@5V|@|xotZX)3ySoF3PcPz-IY3G`&r3=1F9wY=EKk)(8g7 zB0zNIMs&V_(JIh8W7CmXe)OwDvTbcY^rmT>!+Hk`gKlV#|FuS?2e!;58@3i~A)D|s zGuS@j0kd5d6A$j*MVB{qaJ8ofx(*{$fjEi{Enj-ZQp-a^@1*!8KIWl!hh)Mc0Xl+Z zGZbfWs+YQ4$bAsQvN6n%x5Ik#wnVmtU4zj$ATr8)LVO}q-nsLe2E8$Grr)o`EJIO(0&7o#1@LYu^>1$Skc(IW zcWVoK85r79YaYAKL{r?q)HC(xald9#=4Zd_(+|U*O(eS4jsI@(3`-#hXXk#NQ;wj) zA<-O6(agodYZ%l%d@0nWZ|*?*F=)q=X^!X&B?FDI5(h+@-#5MeD*~;1&JS`#7keV< z&KLDBX-?f?UJy+wg+i8o{aV)iSu1^Dlr=`ASAPi~e~NCOEfZB}kba}?N7*Yg(|m^p zGe3Zdy-=~i@l#+X8&$$ECULFt-8jW3VRQzei<sAK?g&Egaa&0{Z_?q7TjKWZ9n)s@{%H>?Z!VtJpT-Da5N_K+ZRCWERS zkNWbys!nZas`LmhYcK*`{jII+^Bj^=!p(O{l&#k1o3I@*O$iw zFoomuPR6aSZww~r#Sddo3f6=A@ksVUqoA_C^}GHAYo+c|DD%x+E7H>Pa^CX-$RdP( z(0g#%1<|)~U_gGxb^Rf<6?wN_m%C>D?47Xe-Rx&L1I>(A!Ad%U;PShV^Kewoc9SbH zwTmx-CC3&!+%x$Wxp=a}z|nUVrkblutTL5x<5&e6YL%Z{hgS;x{iu#i`(YS#K)t4; z&9b-lGF-W{{cGo_@X_RiDqdW#R#Hh_Lz50w05%!c`d{c z6|hWJVNcd1$%~|~`>p^ctgM~lY#)MJvOt!N)vSg^W1r%PJh}bn!26E8TJF+AmVqUD zW17;P{tT@!ZSB`h-&YmMZ(ZyE{ko}9YYx(IbRqw}7rsC>E!+T*xS!Mv=B&#@w1*4; zgy%));y^TZ2@8JS60@b9s*2!%MI9F1`&;-JE%WnKh)ES9ucVgh_(HvnHDMJasNI(Z zXVaAPN~^28+WavmfBu9y2rm+7{a&0a=r0tc`RL!Jy(RW;`|PojJt5}R%&e=`0*u!> z@cVVP9loYYMnvVEg<&PE!=`6Lm_P~a8{XhPSH#;3ZK<&Ou|Q8^{tRs=@J0!1EYKBD z-k#faOFbhpk`H0B8qWvCN*I3XtqP^&*_Iw~Z>GeS{SgDD@M9Xg8x* zw9^^Sao@sStk{&;Zh?y?Sa}@qqOSFBiPI7EfFna%=D6Kw?7wa*bG|a>0tgW%iuMxG zw11#@?<)bl1$&nFXHUwwFv zLeT#a(~}u4Xg!&N6WAUeVf@ckNMef0mRwO=_X(>V1q7qM+XNnr7p-g#x za}2d3_`PB9aX%73F0RD3pqGGVfmjmW63RP1J)5paZ!?$Cgefb-3NFb7aqP81u?sNf z8zx2lxgNF-eH~K)(XBCdnx|5;54-AhUtEwShEOUcDZ)x1=md;%rcF2eQZwcz0y77A ztEYawlb;qS zy{K|p{xhd_M>pj~Qg6FamI@?dS@rw3*r#8IT@*jF!x9o+lW@!p>e;uH@|*~AE}bxh z$%w%!CaAOorgr_mE&Ekwbb%w!a~W4}?)JC_0Tw2kK!$~~Y+2Hlm7LLh$f^FO^kDba zK#FjfXBHK?Q#%G;#iJ zHhOH>WLMmJ%qp+a8}~bT*Y`RuQg3mO)6lc`>5ALP8;(Lm7av)x1kV=;++ZgzRc6h9 z@8m;)o;y+Oc}te~fdGBN06su#(Ajkxa~`Sj2yCx7Ih%+PGC!f*!|#@Nft}Ij3i74i6ZHxbd3$lwr7vv??Cq_lm zMaza}jG(u{1gibPNmTV^G*ulOwK|hKHdH-7JMRT8#A#Ps?ZPCFA?Sz}6B`?!nwq1W zcRompZ#lgL&7h%|;LcY4HukNS2@x_sN-?aR>bGarh{+>tWPVuwv>umwGFbD@_jN1t zK9^#3DKM@V@%fFEG~fLA{Nq(T^M9&_ry~n)T@IK_^>y{^UVeJ%FkvYSeOy1yT{D}w zJk9Tup$W<>kZS5Xoh+VC%Cdun7z(Rto}dZFsnU`lBRx=5qc=hyRONv2wcJZyKWm^4 z58Wr1nV)BK+_Gt{5=Dq*Km_0@>Ad)?so=;NB#lkO;31kmLr&`SmBfc0RjxJz15IUX z^)7Sf>PmFVIK}T5MO^?iczLI(SIs<{_!1Y2DvUv^U@++~3oToggrg4!`Fx}0x|3ra z2(;nV4l%tLB@gkP_bub$SOb#8l^my{RA5AGJb+)&y0JTinD_hS@QLgqwAG?I9sN$hxV2J!?_F=(J>W zWVfc}GeXPLc$pZd5><9CU)R~?PXuh}f}o-H6Oax*FYm`oE(^9%maC;@9U=gb%9EJ! zr8W5{2IRC(VJ^z1aPm$VOJ>RAZ{HBWCEpV_MenZCqey7zA8R8!s`P20M=Z6uxv~$+ z3Z79=z3xFJl$Vql4p9BE`m;z&`F9MdZ#yT%RNoi>hm-BXn z3f8<447<5k=XT5@dbt~R3X^{R>!M74u_C3)82xu|SsB57n%P%&3RS$blLw7TvE~bK zaWg~hG;k%Co+k;}%9B$`J~u)f&O0^@d4XP87OFZyg;^Q&YWdDlm`w}1suKlKc zii>`aRs&ZokW0*WuA&Bw?4M@P2yd}pjl`}Gjw5Mfd(p^l&?sW?OA0y}S+9|i+1AyG9PBdT;{ z^`v38vW zV<~AoBFa)m89gtdne^1|OEe{q9p{4uxn-31KS|rB7ubWLSyIUoJ>TuI25tfW1`z`& zs2NraSWRbe-;s`pLgTD`fXOTblUYIhgdpNLB_ZOP{*~x6#O{n`$tX@**~Q_|jZq>E zdQ*8I{_%rjFfRM-b>rUB|GWU~fE@HDGKQggvy_)i%pUJ21_&#a?(n)^6l6%9gdTSF zH(ym3RhxCxw~^d;atG-w68e*jOcDUB*=v507-ti<9~jr z|Cf8w_Wp!Jmyp)gF8S}blb^!tdWvuCIZW_bgHh6bh>0KKX33ip?wx#k=2;}2l!@*Y zC;MzD`t9>~0{H{a>+;hR8G~s2tB0P$%hxWVyeVqZ$o^}8fM>C=hbc%h=I-4?+?4RG zIIy&_MOH(ln$8B92I68hNn zKP%xae)GV*c}q#of^ZKiqoarT$471l=K+vp;5kc}aW#@{%g_(Im+7Qcyy(-o??5SU z5x$CC*c?icU-vU|q+E<#OexVDe)F*M%)(d3bc7%U0#y$nXH zVS4V|M1c9gj`_E>pd*Vn(63tmrJFydwqpuS`fuyZXDTD6)72=FZdw)_ zJp=#9Q zfmxiRM;B-P6aiqSSq3eG7ZNVM`n0io}&vqLD8UG1{eDM=-@^!<~J-HYwSdiREL zqa0!C0Bn2tMG!1RT^&Nv#z;(a%2(DHJg|NFIuK+qogPNbAR@Ks#i*za%-T9SIy#v- zZtBv%7}Hl`iS03|xYlkHycgX*Yc6+*vRg2+yxkrYb=jLb-w|2+|K}=!zBl)zIla2^kTTll2hFO|d(Vpg1?E-H-nyEpyb-w^KThpmXE1FbFR_aK12HuisZOEdIE- zczQ#9$yRRJ^78|X8&D8yQ^+9LIW-9yLP&PdJ@D|UvIe86vpLHUF%ykmLLcX%mZ6}? z)2k{O-z~u3?t49Sh*q`k2g6cw&z|ighPOgr`n#iC&|gZg1UcAh2fgjlgaem+B(aDM zBg4=vd;W>cm^uk@xk(Vt<2g|l^V-iH?E4#f;#OH#l5MBNb?~E$k1bl#$j(49C|Y00 z{Ebs}AHh|vYe4LgXaED>?NFD45?@Btq=Gs~^jdI-Z8~}Q-n{`z*!|F>*f-c^?FQNq zkhkJNyqbzpSQwr}iQY*vQdVjOmCrfX@hCfK0 z$=j?0i>H0-Xz-DMkLTMz2c9e7QE+?@y8WM@FN-cW9?vWcCJWXS_W>g`PuPP=4<#4c zqg?SRDcR62v#yu~hf*O~|DsW~?pnZtlvQR2%7fU?eA&N8u67n-s%gtjc@H9;xSq5; zR83i^(mAWHkM2@!Z61Q}k6ytU%0>j8$~BOH>jE*gI=jw+2yz-IPz)=WBYVICjtPGyd3KXmQ&xt@;E{F zUH79&QxXyqgXyCZBy5oJX8&Fu*Lq$E5U}b+fdV}Lrn@l(uFOPpLAOW*3d(7zcsV+m z$BZV4JTcOfAfQKR=T_eq=?aD7%;9|?AQdjex+-Ppcq0c-1J$lZ214F{gC4R(@#{s= zkcq|92oM-Sr+V`cE@Lzw)hN`xG*RVBvXuu^Iux{mE>V?mvugUjZ7Ah?BzAmrfvK4# zNp^7AgkiX%dj16sOB=>K>BzV(TCWe@2e`Am51A7vi4KEGPlD7` z2%EIZ2rev+U_I&O;g*PK{o6;lIdOF|3>JGM?kRtFr-TShb#(gp_A=luSftbqH2*JE zub~lcjq~pO#HC26p{zZ~2bYGtDiU04?VSLgVzcstKGs_MHaa~mdfsN^nLTD`7Z zMVA1LLd5;smoRKSeU{xnw)eWeu^(nIY2k6+hVZAO%{W5jBP6ggy8A>h8V@Nj^nIFf zPn>|^N~Q>YAei0C742*IB7yLBu?2%T#R+~ZmatY5ags_d-(vEz+1<(Q6RxxCN=Xsb zcL(L`nQ^7&az?3xRhoXR?f`{5I}@I2yQB7g(bmt2RlU|T`*G*D(DRjcZ|K-&(d{2b zd_HgE4KA!qwneZqaDoSG2|NfL>E5qNE>2(pq+tPm!Ayqpf!XuB7oLBsFX6=GsWL)K zTb&7Eui>lff`~l;8=)OKjri7jNwlzc#|ac?jUGRM4*^8WS^G8UNkGU4w1H#>YTi`i zPZ(cxnf}7grKd;#_3*)MMAa8V-_q%;sxOrMY(od##U#}fq;im8wg$KFY0i6Jts&E# zMbLChHP-gWWgbUl(HxqsP=8GF91{kCZ zvnrrK4Dv6Sk#rIUUaTcRND&u;QOFX|@SQ$dW&s$MxVtXuNY^4(R`;zSLn)TYw&?$t z|8t`ALZ%{bLU%cxmZi3?yAUKVLWf|*0l#l273>_n@+}0*iPV01Wj;N~IZ~T_^&&-L zkL<8YNB0nB3Ll$ROcr$pWBJc2%AY2plnmy8^G>-<4Quy)~-1SR-hY zIZq8!dVX)|g$=T`^Ke1>mR&^WpZ@YqVS>AMcGubd>+=2pzW$cI8K#8HeQ(4E*tEkl zLF0)@vx#zfcM;kJvPrZ1$S-dx7=k6Ch`WS_v7BAuzI=`gg&XE52wAR~hB)GKB3E<4 z&8Z|EDBDXu(#*Z-Isa!wA61x8?bXETQnYzU+VvI6y5MsI*^g}F`1@-&U`e1t!#$se zFm~Q6o6IW6N$oxOHS+QxEUIjuf?GF@|IUehqp!s(Xp8XW!~9crzq%yVaP#?tphmb@ z_#vMjvNF^}v?ff`2$e_VzNvgp)D2co$bKx{3)))aPnJ#CC|Tug${j70hbnd?kCQSV zOSb~=fucQ>(Z8Ex_Gk$NEuvc&M!|c{OyXugY)g(bO~yN?Pvbe z^w&6Bd5|O#rW~k)!JibzGFHb-=5NQ1v?_ z1cxEc$gh|qy29B3CS;o-4p{1spE;A5<{09Ldh#bw)>zeC+$!bx4d&Z5pR-Gx4rhfrjiI%~i{@N+r{(uwR5LB_{#D|xQeZo~*(BvPq zj4onR8NmLWk=5)N{DnJm5!o9KLL!Wi%fb{weM%1nj02FcxQ}$378ESCdRzmi#ffxsxek zP_SzA^)A>U>$2b2&~_>FdmEbhKPP$XSXG~AkrdwxascKr0$g9g@OMyv`K2WQuFH;K z>2ZNc);y*_FJm*PS3reDe7FV8(ABfO?LuT9$$!ioQ4!4PrU*7axCz*+o-~zS0sL>A z(ab?v=;QbBmb)0_RYne0%g~^lY*h&_8T$HyN~OMprlZdSrmXJC&hTJktjFK#pvX9l zpnOG}JeU?9(D^7UVy?1-3S3kBYs%sKW5Wq)-9fqH0e{plxx+prg!Y2>*11q#?L{O1{z zmJ-Ir2K!RWInc^FlmJS&;5k+7(PCET&?#I}a5tE{FQKl%6Be{69)>1bZTdMffxpcRkx4N5F)lO^m}Cdbx6-bDI^$u=8wQ=RF=2A+^ZH zn#X*=1lTa?jWPueG^iOpiPWGlGlraqHkMcj1LB3yY{7EHlgL3hzo$#U-?YU#WJL z8GNtbTtx&oDFt7L9kEfEu4~jW3TAht6*z#i3hZ|`aB;zs;ES-FhO;{;v`9bveET|l zIe2>kI*%SwG3zdfb9Vvzg0u*#+96gm`6w5S!pJhI86;*zts7Qo!%3(-W)QFo7{g8m zYf%djs_EI+PB`eA|NKe~*BBT+bwAO!X~<;A{A+V@740C|DZxE~!u(x$&m9qMqMl{` zVA`$p0~?-*v#Kc!N5N`ux9MH1CCRoTTZ@AcgH;B0E<8g=y&tKiPus1_X9&SLwO7>= zJxSuy#kcz&+9GJu^A{fJ*j6dv=77EhKD~=#QPht$G3T@l7(cg0#XT~}VJ?>}jlbQf zrtSFZ9j0r*C@wekLWX#!Hyk|B)zUy5hgxK66BiUBMZUp17GOG2YK3~U{`SWHWyb9= zOxMrIl+p>dj*NYfTVOAAkDcDAvMHZd(o48Be3jQjqw<5b zJ}$oY4$j;E6OWC$1BN+}O}UUMEe@|YJOB8CX$ZC^cx9ZYf6cD{O?Nsj$oI;61*K<1 z;6PN9Gj4bkt%Dt6KyGFp%%HKRcJzU^E0V1?U}l;aiMM^Ygo?4afvC+5V*Y&-$-J7@-DlL%&x z;7jf;ogAMf5`(zp;jdH3cNZ1>hpMOCKaYdkH7tc30x29Su#L`#a2kVjc&f7V13GXq zBeEN<0%LTAL?o4iG-bOrU9)o2qt)DSU+3N5o>wa}Z$wCXFKuT{{5^!&37&w6C@>1a z5GX`ZU9Za!tKG*&{1Pk7&pG=SA;3Ehgi3zA(;By#(@`geH5W&;oTF z-`jds=xa39Msz`8QY7+g$t2NyVqRy=xcFDYPKhW=%0PkPtE3_$7>73b3UonAPzT}l zK9=?hXDpA@A_+7RHOS!qG=7LO?-QKHxv2$|lCjufr6tQIaDG^;`uX;L#Tl}D(`5E` zmufjJnxsFtig53aM ztCk`7xOome0kvU0M%kAnmRC|t9cR`z8rD(5#YF-cIxw_{E#SnxqC-sd(=LAK zHHbKtGoEw$ZzI6Y)TQ9)1vrgRLtmm=>lRJRv3u9z2#xt7{0e z!eoQw)rExxvYE7}sa0m^3sHk?^HBW*d|_H7LJ^DuxNlZOeJ(nO)EvW&M3?m?Nbfnv z7Uvg5y0F2LGC}19(s4dp(ht%7DUDnCO=|G0(80lP*XSwX^p_P|g1bkw{)OFFuU-6{ z1*S>uS(3@Cb!!lT8V|{2_`ctd$<=fwMsK#IXx|&Kh-UeLj(4i3;YMsF7ex1yidfUr z#YkGP9a&c0CR|5F>p(~90#|ByMF@fDJCuK%tV7w)D???S(fLKc*zB@&>I&Jepxk4GWo<90>n>D}fBB+-#sa2Wu>7z0Ndkqot)kQgo9)}u$j0*jx@lg; zHy=LyCRd^M+I-m#ZlGSFX~T!&pmRcLd-aU{OcxMEJnIyAr-JiDUvr(#qnaYxWiT!m zz?J{6`wzA@?7@k4a1jTXL#3F1t;%4HzPD%nGlc4~B=DkUC$X0uP=cEpa9{3t7mqy( za{vM3e=L;MQQ+H|NJ4(8qHPUV~=4r;<(jg2e_#xgrQMI$YymNuWD6W_PLwa7Rz6xFRI;76N`cwQ)J zAtE_zK$043C~@t7Ey1xO``Ni+Rk+kz#<{@^xCfO1h?6Uyx`wt2K=89pE!tUAz9*~B zj*&JdKEtDtSJP0gXi&chu0g}*1v|qEB)H%3dqc@=o~`M!CNE!nkVm7a4obV2uY`#j z-$C&Zytp0lKWhBx6|h8up)z(x8V2e+MF5mP5S%^VE&V2%hv*Urh+sYg?{Ln#B+#YP zS25&z$%|{^*VWW8H*ftqVgrf)S7asrwy40bIhfBaoo6Hu%w<+sv;$Nz7FAsmmBn{GpUASW*|_H1SWU#esh z6L@!6bf+z(~$VjcYPUV^d43y_8P3P@?g>WU{N3D$oq8%7*;;w>~`BfCL1eoBA%zOpkPxAeVtK@bz0@z_TI1x zx4rd+_Ptfovs*-W9NCzdG8hN=|L&i0?ub9vHmq_ZJ3jUU7gS94r(WznZvPJrABubF zyZCA~%N+LndSxpl9o@)Mt&b;HYk&OQG|c7bQ$yBP&*i)Qhas_{+QIj>rDNK~)cCH~5cUmcxUKg-Eio(~m9;Rsu@0JNSnx0K zD=I!0NSN^jz)+2-`xN;#( zoJe}Q^mb_}IVrp!!}v9+q|=uD+-s)hyvKM_E$t^Q>z$Qa-6!X#EW>$+R9*Jvd;=pJ zn+Qj-Zp}?oq2k{Xb6#p^JhV>bYw)7DGnB$dfCm4BU#+|u3>*nmoa$xtQ$$vT3x4dF zL=xlh&IMQuMd6i!60x`{G%ATUtr(g$TSqP3ohe5kkE=N^_CVHA<8_GHfrrS?m6z+N zoHRHniAiZ>XVeQ7>D|~d#flF0_yrh^HcyA{nxnfnecw{)yPuRNyxxebP);FJ{yVg3 zNMXWwManF&-KgcajmYg! zScFOJWByhKs`(yY9BKDA&2IBkbcn}?U$X&ejJZ|)uuGPhc>X%tA@c^9g_WZm{B0OD z$XTBAjV!&kk4LN~+SGniLM@kHpoCTJ+R0ndu-aKdTT`cZSxn4ouT?coTw29Mj4`iC zYA=karYwCm&EigH-#KwF=o*V}t(f7S^C3oJPb7^^Zh{iy(-B-*l{jWX<_Qw!&65%3 z6lC)R>~@sY*N&&hLryN=NWvqO9nlBJNM|&P-xz1V+1M78QT}7qT_2{^uwUAd?0rNG zcT9EcoX>QkCt?`S4btR>M?w!Pk;Jeru@U?G-;@3C_awqV=Cp3?$|SKtMHjKN^+QWh3#v8_|`Y&|)K9?v|&n>7~0YI=}v@bB;j z`e@KTOnNe}^6c&V^Qi5$<@PVKUXVf|-eh<42-E-hGiUf1jN;DhG}ib^ zVdj_e;WV^px39rQ~2{ z@RGzD}pL?01mv{ZSu;K*=l0g`=9+pp0e?VFXY6oH8iP$0-e&3 zm)vW}a;J4)7MB_EXss#{nHymgN%$kNo~T`qK2QG_zI?2s5eM}5UA!!6kl*lBR8Gu_ z+AbQe7b$&tWzlX;&UOC}yA@W2hC|##P2?dM&UXGl8atm#>2n3%FkEgXLlUw?zfU69 zaJgpXP0k#BdMSQib4j%jYeF^ND@$pJxIg;!kt=k6dYQ9q)6D)GK3WpP%iq3TvD6Go z5hM4Jgr>;LruPFzqdqjWR8CUixGa35j=Fa66GHc|piL3-V#n`WwcQczS$RUD8q9W0 zDH$=GU&j`tO?EQ~S>$YuCKwH{WVH*8-n(q3f1v#-;D#Itqt@z=D7Zt)I&W~I_ZdPxC&`XQaVCX#=8TKwhu`y zvYbe?ww)C-9fo5LH@A3i!*xO!%z+_|ilhRSQf$#$RnxpBgZ19B#uRdsuA|0i{N{6` zpg}%^Uy>RLt*R}*>0)85q+E1uxvybv&KwubomLd;t8k9te#rAwPK2a{m0Z`x5>CF4 z%Hpq9NBbh22TcDCK5J4NUblHdIwJ>-_QGKYM^(0B6}Qzkx`uVwliU6?$uBe$ zozyZGe?ynopfbKFu(@J-*==7kf@^@tJSAOix3$`oq}|f7)6!n2Q!JRa4_D3?(qF#W zJYkL@5mNwm1KZNp*1Nyk<77_9w=)^~V;>HV+36%RKmhN#zgFh%)G5~fB?u8Zkqbyg zrZ*r%QiwGg>|C63l(0lOyA*dDwR};GRtdlH;7)6gy};lB;z*>@kX~G+aAfTH&*z&T zF>c$p593{>b!R1T>D~GrY?);X8N{3;ka_r6{0Ld3D1=3VHB)#G?l|YAe@N>!3v~Y+ zlH;n2lFy%9J@_21a0u4lB-_F?54~ZveZbC*Bp)5ms@+{~&q#|@5IPtge*^Q_{~FhA zGFORoVbp+mT(y|T#PV}x!EJG4;{hQ@gsj2rs`0LapY+(`x{2v7&myulvl_vB;3rb; zbo+cc`Swj#aW(1~3k+*!^%@~vgDT6n3lp*S`t$)<3h6xaAX z_3Z42H9B04>hbFF?ID?1LIspU)i=B0(J3v1MYC}%3@6ihTPf(ThZ}Suy3Q8P$;Uaj z(`JWrzT$p&T9Hk4k#8m)f!Aoa2w@3|Gm%BxZzaHAI4v_R|A-XEplEp1-uUn2d+qR{ z#&j;O-Uwt4=j9U7vDj~UYhXGHFO z$Wh#My$2cB69A-GIShXJ@F5h=jnbH}FvZ+EyP2AtBISnrk($9+YGK|!r6+y2&}moWv($B7UN?{_Uo>uV>{V0|sGFjm zhs9>|sSiEqn-!9MNn+$np8EReQnzDVjY~4y;8o@$Q5hhtJ5Po(&b9;+^q$#ssR2KpreF3L(?SE%2;EQB=}qfx*nunNSWCC!~|& z)oC3SOtO!{xD3Bxjw+o%g;7Fr=WR5htN^}wg30oaHuJ-b$nqU|@P-l6J(hMHqLBW? zFv1)9755+SQ7G}sE7!4mIs%D%(0Yse^dbWvI7k-v{M(;XhSl4 zkkts^;@`d9~V0I~TH>ov#9#s$RL*AX&Sc7fseJK?9^oNve?9tU+@>{o?|Y7Y=XIdpHDxm|D*53`_A8fU;K#+R>Hy;7rbaz@ zAx6n4CrG5i3CDas4bC;_RFkTN1)q^cVadgzsog@>xOfn^LehIaUK3XBTTden_N_Tp zHr4osymTfG=>(2F!#{dY3F(GhClrLFZ|FutI+Pp?Eg@)kej!Iig&!)3uNVGvC5ItX z`RL2X2(rQxJk0B@`si((h^buz%Ctp$^V@qB)5j;1V+<_v1aaLx zE6;3jSi>KF;}?x@Q#8FZ9|d_gjl67zY=2h^Sw#g(-TcEyXqrilJ&$KH=gsW9C}CG; zY?jXuy&(VGjpf5CJIZ|DporTfi{FH#9by~-ZG{)o;w@1=I`dw>fbVNH=UZZtn9rvG z!oC{(wI=s|djJpqXLQy=7sn#fEtrN@R!7dqNT~Gr8BA zQ3iQReg1)^5FF(lgOqMI_ubeV83kyEj54!k>o_j6yi5K}RI9qAwD@e$iW{X;#3u$u z7nP20#tM#~ivMN*o!6td_knRRP5-_d(nL&k-3ewZ9qK^B#x_`o4}nk%v<4|mMKn`S z+a%HProX7-6%XJ~uGR^?3WbZ^Wyvw7 zjpCH?GVu7G5q{2a!+f)pZZF>h1D&sTv9~twBKSxn zf{(l_?qPoZ8n$SSklH6B^W0~<9?v2yNlY7s;a$xCjOHOPa`VUz=6fQ8xupS zH9XozWv|Hb&_DR9K`0#h7YbEdM&cf7|CWp9lQs{z0aBGNZ#&vT;m}W%1~Ct^r`UXc z;=|)#RpVC-xL*yS-M@X$6E}k?-Fk8~!Wcy&lASBQ&8O~wBHf+j@#MB@nb#v{5mJ15 zxIEn*gtCCEa#Ka6^pH7P(pkX&CazKv$V`w>^axeT98hDP%qVv_Y!W{mJus-0>3?nt zk-jYqn_2footaXVec7wW|F*L;b5-`IWn)kG7hbjH1Ap!8h_{u-2MvmZvpF$97@_{v zD|b&@{bOqSL_=DSS$-$eKFzhu%_WTbMjnu2uEpf@AgLJEE+ut)Azjn-0yvhndIR{J zt6RU8UB9JG`?~CK#f{J`8{Rn6=R zU=I7ET-y~xi!3ytlr_GWqKBJP^TJ)R6j6*yU{w0dy_fCy5?L=LtBop=8i`eICG;kb zAWOY)EStg``{?*|>B(WqzPta$>9X!}A6l`Fo5$(2bxS&*(50inY3A~LBH$H`U zUdosjjkYpr*Qn%yH1JaUVQPgyAfND(L%hcp$qOA6@$Ppi#>G6eR})dnvy7mo%wkjH zs)d9Uv`L0}j6-xYwWwIu4c7=wP*qY04dqcxT=h=}m(QY^+rBG-{q{XZ*_KMBTewZeIbd7!p!n>YjXD_@p((0$;QywtI=A&`d-({ z*ZpT~`w+%=(^#7ixe_4F4cWHEwTMm0BxOH+YF6%3#Kh-E`1ntDl^ctb;~!{vpp^89 z0oz5zkjfOer4rU&R0LPnw&af=8@rb>i35XtvdBt!ulSQs13lQjGlv2wK_IoA6Ld8b zD(jtU0=l>ojc#nPg#mbm@a{N~#f|#(z5253Go8{mxiOrjccWvcY3dd}hK&nYI_b`M ze5aCD0bMLG#xL6}>GO9sX|Gi;X|`y^Du;CHEbhI@Ef+5T>k_Quc1F1Db#&KWB*FW( zv$lRT8&Z1LH!HtQyrXkGweYZapISy4=s2ip@s(bLw^st&b$WnnuQdp%Q(NahQ!^r` zWq2Su4+ulwv%CWi)$lQ(2LLtJgKDR9wL}E8`>J+-J_!4HQ8d*2_a5>XqG&!$Msz3- zya2ef{RGl1Ts;d!#%Yq2ZTn`##a&Q}7kN+rSp>Dc8m09skWsUP@Wr^&IWbNwE2LU_ zM;`W8g{B4K2GP+-Yu*bhjd+W-}*EP<({s@JKsWF2x-><@%o{4i3?cjlu~7v=^SKb8iBur3w97 z!rq=6>UkWF<;-}3K~3p#&M4vvcXa0*co)Vmf~OAhENv>mMBCU}xWr zehvS9w}&pZw6<0{XOuE@LDYp*el#vQ+1SP=Jcxi*xnv^MH5G7G6Po`51*(BL=*U6j z$*G^rUI7?JtRwr5^EZ;+R41YXOl+oQCZ18reWvLXfpTU@q!7fQ58u$;`$+fnuqtMV z_7l(K&*|qXvjAKR*evL0gW}TvXZZQ^e(bT?d!T-lt5(Jrd^Za;Gu)wu!#QIAeu?c9Cx%(r4UyTkv;vOMVp|Zy zYCj~!eR7xazexmI@|M{7*+3E5p;yZyLSq&dJE=JG7vbeS+c*@?^uF`0wp?;hHN3s{ zSKcP3`}p(&gjimUnXqy2!`tpnN~}rHq){DT!!v66iiNr{Qag+Kp`v6FsPtHV?gC{D z#HUR=wWel8TGL$m`D7bUZmMc{_ki?*0Y7%B*@ixXTclB`n&;uTPZhFC|*1=QEDI;$|8V=Bu`X?a`-y z8R0bJ;vXH2*UI+zGUK{UmyPp62=B>m*ju?;9l%Lm6iI|Y#27)sZV4M4i(^T68u2(m zd!*W2zox{?X=Qkmv{!-&AUTwj#V4x**e{>yX4>7B$8y6%n~$W+X2F9!e> zI@WC|4Q&?{OIhHSPVcYOQWhztG!ZC$<|2`3`BpBP&rI488-$!9YWxb-l+G4#ZCHW? zh{%pZ60>VBUZLs{KDaKaon!FIEYOFMafUq6Fc)eKkSLi3?rkc4+59Q8yv8rEaBcmU z9pjxLZ#IQH={`lFZc*1LWU(WWBJFVa17m1k-b{Uzl-Vxb+C8LY`QsL8jk`EI!d^D8y&!oE^ki%)z#k@->TSP)J@qB*DR%C6 z0ni&7%5*@@774--7W1xyM4%r7Cy+#i9=FDJ5j8S0l9`zq?iWjzOq_L2lcV28Tm*4H z-|ce$vzl}5{9^X6aOlFPb%BiGfEI&C(1}9-a#8>3C@zGPgoJbrW=~`7u~*T`dF`#O zH)2_I|Huq`6|YO7prvboHX11tK?B7_V6EZ2YJpXqKO3*ZQY3D&H_NLuuw@Hl)Ntq; z6kCcawbwIC<1$CD_u-9jEsAb)0R`B=gfch=$P@SzCP6xJ{i!ubg85XO`eik2@fqwp zE5Q_TA8sTi$GRJ3ZtkC5vBA4H~#VUi?FK>TL z8CG(Os~h7p!jF`dTD~LUR$ewvA+u6P35>a+eq_F4fGk98JR#fCqM=F^#^-_x&8oyo zsPS?`3CjmZ;cC0|OFx=Xj4%0q{(FSlSZcZYI1%KTc|R1UqIDxuSz|%LcQ^^((C)C3n)h5&G#Rt ze-vYo_7ZA!k2~&-ojikf3h-^oKSg-Q>p?Pl)6Aa9e-KUs2N(pEy*-@|KvQjs{t&%L zA|3i!-1Wq*Kf_zx_ag0?-N&q3cQ5W}dc{ZdB%0>KA}p<0hJWyKnZI2H6ue1J30H2^ z=+!IKQLfeFv(MA7=lMipynKhv+Tlv<$;I~Dp`|6-<-z2IF40SN)1j0s4d(L3#(@l} zu#*l}&z5b^V?lg;{J7NAE}JA#1`Y7O%gM>@w2@zaOug7JHJc)*O{JC=ALZhbZ(~a- z?y3i!t-}8MWtruA6oEZzc3^SuQ*02nNO_Ikt}OVYfR3wfgqrR5ntr8sqI$YN!lGPy z3aDcEJLC|`fftWv@49>^+$$byRW*hpV>WF@$|q+hwZW;Tl!E0G%YrUP*81vM%X;9C z^a@JPwDA1iSdQQld8D4XNeEY*Dn0y<^qUuI*hIlpI5CaWHRL$5(3D11zB{Gxg}oq+?SXl>otj;c2Ij+cP+ zZ0t!SKxVTP)*iOXEq|(a4Of%O`W4iMnwmAJXnb*b1PmSB$Rt=_=EMr~g`UR&CbA#S zG&BD#cSzrHLvLf@BvzxYRcG?0OY4iWSNpmA0D)?&c|RFhfJ-imfzOXI0^i3JsuK-t z>p_23O~Qt%T`e7S0W_8yKw-Dbx97(PA`)l(d1|dQa$4uL~%zE_y1_R3aBX8u4{rI zpdy`ulsM8YAs`?v(%p?9-C$6X0@5Wd-6-81BcP;ocbC+E=KKEdF0X6dbw`|e=RD`^ zv-dvd@rA+vt;re*rkLl^oBsN*(g*Z-$Vy@I@won2pg%_@J2Y-gS{^TRf zMb5fXJb(T?@wxMsd?jrUTYMJm=cv-c(H^;4IQI93z?Q52{IO|W8ec{>)y&+ZattmS z6?`rb!y{RSG5P@sEDga^oLq#!uxA58dbGM*YivnknI1X>(9FukuczcYKbhd$$4?k5 z6%)8RiA+Mqr@fLPC_B1GD=iwWN7h;{E(o|GF^4Uj`{mOggi2D~PRK>*Mz(lbe;a{X zZ%RkD@$g6{CPH9-$_DGSug=r@?eWgMPi-ojYNgBDl3sXg-SVSZQ~?p6eCa&B+#9-< zCp-akfQDFFwm&ejY=NQ)OAjB8h=`(yGG2An>+q>K#iAL;_z0EaEaV`?P^Z`&o*LA2 zz#rwCe{XxgY?y3l*ZLKsZ~H~lOcQtSv!j^$YuYgnDqRgQ#LW_ zY^(ME_dQq>l}#QJxyS;HDp7Le(R6k(uk7@7OU_A?)KOZy6sx9IEOq>kx?<|-B{a6O zSIn9rfcZ+XxIF)Ds}gLnyMy#2Z3R*nD%Fk+x3ik1u_yI%4;{ctNb{`TH{8X%M$&jv z1#r|XLs7>Rm2_Lj#}$j4YWwN}6NDB&D3m23Opz z9NP1LwC7iJfKCWvQ~D!5Mw53fL}GBUsPElFGF1~$0G`^_C!12+yk_Ctt-=Ssje#IY zSCZ`u&F6~?Pnmrex||3a*iw?lkoKB^g_TvfLL*a)>2VL6pZ=Gg#qUatqfXH{OJ{<% zWLI64X$%G&Bm-7l>YAD@ELcYB?HsZ)L#?)C9J20n2FpTtm6;M(m-EN>J9bkplHl0e zKIiwxf0mRMC5yR)jgJ9U4Df@L{mc9G2etR56@M*Q@%IcX8?6>G|NBCVp{V$s5DSh{ zdl)VkpZdTSsL2JJ6!ymJ44cB?84m|8Wx{E?EBhS=(xSQFVMW!Gf@qgtAr4L+{Ein6 z)SkO!OxH$N-S|f-2kk|%lzF8F9kCW)KKr`Y9Q%RL*7OJRs;U8Ljr_y`HUT@wtVXaD z^^e$xUDU}A;&BI2NE{qGKNy<)E72R0I8ncU!~)*oWfRI3|Q$12Oa>%+V?Spmg~LwXRM^iScJ z@mY9pH(QlM?$WCg(SqDjF&rSzU0vHSzT{>2iYQx&EU9Pc_7tC{an90AfYWW=aM|ca zmH;9*6mkP*7}DD?2`~hmkh7dd#KLLF6nPn9*};a?2_hLGeOVgwswLV}`E*#`d=E_v z$obANB8qe3pAQPc88#1q0i@VecrkrfAcX*k0Ai3aAISj| z0ki}JneI$OPZ?SLT)my~H*r^XCH--qb`uESYH8m0 z8QM0|z72GZK7*Y~_{Cs;H#}ZS-GH&JYVeBvohBud#d}NscjljhjOo|59m~Mr)b7!0 zkvcs#-4gZhqX;Dg{Du3*aHWyXC}~X^SR$$$Wd`Q{__BfR3q?J z{)59LkZyUcDxG4()Pk)tKVTtH=uHnX04P{>0JTo@xu@IK7t|X*68Ou4CJ7zsY^#6| z27L@^J81SE<SDm%8Orr;)d>5d1& zhyGXGc8ti|pMY8cAq^F@d*9xENM&TUDiwhplnTvL!hy}=6UIQXrwbV5t+O4JsoYmc zyibRrpBO}vD=}Ly2@aYo`T2#PKDA#zVHF6O-?XTatw6s)0P6$ERu}Cy_ zgDAsFi#8DON09Y2rOPebV8XW1co8C6gi@)bm~8l^#cpX<=hwGMnely=LB<~$c-3TZ z_u8Do;J@wq^mCS26uWT=la~$5B)jR%p2_N$tQ9MfmQC&xxlkRGib6>e{;FrOYJa8U zGylg~l#7okM7UC|dl0`{j%$lYokd>8PKSsdBo7PwUiUB!=(v74iqh?)1t}*0qABdd z2cN`9Xc7g1sndTfg&~Jbl!X^v`0)bh-Ys!cp%HiV+M5t6*KzEv#ShT#uBuD0Ht(l(h zb`IS#&28=glpCT{&tQFQEnw(K{b6$5BPp{Gk6{f%L9IXR! zOMy!Eehe?DgLGwv4pyoMR!|%WR}B-K-9IK=174$RnO2s+_s}9;BdY@24BD2!&a!_9 zm((QxYtCr-5Je+PjL!O4$YjR%pzcdD=>RGE!QU^#EPCE6LuCIr(62-7FeSTBJ9f=@ zN-1YaCQ!vmn@ODn7}YvG;Dh6yXvpCAr3?O`r$SQ&*yH*Yx$L^%$NdBk6E51@ocM8i ziu9I?p!uFFg(r4WkNlITdfMTAE}`ST$q8%lSh41xPW*IO#XWQ*AL1x^OWI+(a_9z4 z4T@-S->#DYM$XjK-}}Vh+a>Lz@2*R4Ujn!5)x|OD-?6c=)9!5;1CxvpI^Bk@dO9<( zfU>n%8hosm^G=`Sxofcg20)+J?$s+dJ#VU-1+6L`VzVTIfHAaxy=RO%AZi%p-s@+W z*#{y6_i8@#NfzeP!-7$-Xe?QMfMSz>t)*$Hi9u^^o&`zW!L|O844^czlA4CxE8*b(!SendiP}`c2`U$RYWL zg3Ax3?@mgZYa<1Z!(iSU4Pj9@{8pWxR)cQ>C_M|I3N3tH z3KrsJlssBdMID-I@cmoj7qe!YNSda(rd*I0cjjXg59FPuS^Bw(^-`ff3PeNgjnS-{3a>K=J9EWTe=EF1wJ}L2E zj^t3T=VKq;SvfufR?lNvV(9Wqvl4=!3K$&W`!hWK3Qq%ciX_U$VC9r}`UBVsLnGPz zw5YZj7F1Lh3aMD$QIBTD`XrM2qCx{Aw&+|?5*Ogw$hyEER>E7tJUD*Qv|dR`?}{CK zQz@yWcc3?Xj^pyHvpt1OdvRPVTsqQqFUv2d%iFO;gy78DPM;A;DvfVM1K8CX`B?4^ zK^_+n$Zk|mk36SV1A)UdDzpODtg=S^=^)rs<&dI^M!D#3rG1Asi$^`ZQ30H?#PUMG zte~2I_H{X@(&DMS>Z^#aEM0GI3F&OZe)O1%fUmSmJp_Q)&-F@rWmeaRsBtu3Z{72B zs%E0huSB8J$;sRJlta;Hd1^K!mZmA@ z<~L4dzJLkfS4ixQgDmZDgW>3iA;Xpfm7Lw7a$Um=6MVUF2PElAha=Gt{Zrid8^)Rvs>W~hsBPXES2vn;7%zWoIMG4& z1itVSbavKX5d$^%q&!Ez%AS1dR7nGrg+)oWFP3MjCn(UzI9*~Z2-aMw;t_^xn6&FN z@fOA-zgsLV+Xl+YleMDXD>*tKWQ8x{SG3?En?8}mwVteQv++LDobtZ%+?sB% zI-hcNnsVAD?ZUKt_)O9LmKU4$8CJ zMKaK1pEL+f%npGs5!N|xy&qmTJWcU zKDp*>Qh~j!_QS>Q&kSm>@jqM02@%n=6(6#CGE0}a7jrib;wcpG+>lf>!{VZ8BlVzh@ zgk{VyJ(-QN*Gq=GRon!}DR;p^AL}7SgbvOhgY0$VE6TA?shM41u9H^wFMEgsY<6PY zyI&|@Uc(#y-Nm!kA(kA2HPi*3eW+@0v>>r@RH}joRL9S7r?IW7U>xgyN=^w7Ckwlw zp+=F4l!$1B#T&HGrQ1&SZ27P|R&2@Yk?aq7p$bH2rFF6?{GB_pPMLUqGrr~h2f|qi z4E&D9-=)zc{*nF6-6go89ka`Hvf%BY-~fT}dz;PHCfL+PZh(*k!ztiPW4yY^d;&(N z9db`wK}-LRCuh#X&UP;M7f*KGVn|8J1=Y8q%WW6-_$7}IBg{sYT8X{;dmiENzSIl( z@a_fH%0(8DrGKJ)GFF0_W<}ct>L(P4d!&!yd*G@u6!!|IG|tn@{6y)Wf|z)n!^_tL z4~EVzF^3nt$0j`l6^S3HgShSiq~njEQ9uyHB_%C3``%THK5U)%KC%w0Od)qiH`J|n z>67|;H6ZCzJemQ8#Kp$Wz{|86ypmQ|&8;S~CN}MkHq>EZF@&v^mDRiXcI>@V{t6B3 zYqCT25kKE`vS5Kq0_b|zCD>RUUh!+y0W$xBT~$$e^ocZ{SgIN#qGvHSG4Z_H5#!OF z8~n(0jRFc}6H19yTm4S%SY z;gpfw%24}H22M$!4@@j_WtNPGOR*-jVz2J_NLA|QOe%{G(K}a`61jjT9qPO*oymS5MX)NHR??Yub+30Z5ClJ_1Q>SWi7~ofPv|`mQ$yqM@**xSFOyN%>omjB^#TY|V5v*v? zT0plCBvr&Muh<<3I4}($WbsRun-wJ$fBxW}*_wI}M4?A@Tm@SvF(Y&nF(!d9Fb<*& z#hG-QDsu!)pPQ^x`*o;1FegF!HYQ0YIs;6U2X*%`7-AXijT24-+dRfh3awa`%VQbi zWeBjy6a}I@A4lSWh_dYqwh17+?PrEa&^Y`r6*Lq^`XYkdtOH0}GoJ8NOSKJr=Mj&N zefK5twQcF|w6p?+($Zl$F=<*;Gxe*a>DRCaAf{2MdOC8h%*`?f45J@{LBkp;_cV+E_i#` zc9x}670Hp!(KHR?Zy^XzoKWV8ktGX zsQPIPYBq}}Dat0DMlGIiIJ75XK`;CAK3!rk9U1mua^r!Id9z0oT$^B*6VH35ff!R_ z<0BTBtmV6iFn--XNMy2oBpp3ijV-FoVCqmf{IHp4>D%(|hFK?}y8wsS()Vp!w)kJ9 z=9$a$B5$@9K2QN$T3y|&1wqvAhAB|b? zf?_5zFxUU)!@c3B9>;cz8=Sjo-rh+%2|TB+hb=V0JQOmctHD(bLyH4V?@j$hs|~LZ z#uAD~d(l!|!&PNUGX;y(KM3~nM@or?d|T9ygGQDP96DAuup@w#iGrmz(BEkWkGhc*&B)Rai=e`%jg$J z2q0-9KkuDwRlM61l?uv-%n`-+{ix=5W4qu2xpgBpV$`lYZ{E;}QUq2@qocOg2yI~6 z^w||VMcC@0n+)AUV2W9=uv$*)Ql^XQRnM!v3AgE?u+V28UjTnz?pQcncHY5F@|B%0 z<*hR>iOa#{`wsM**QPX|vL8JragE%w`7Zi}2186LkI&{A-sooN*m@{0Nk)a`vGd>| zk!&~%C$YA2|02ub>bj%2s(O(zJgd`}k;JkhPR|WP(I`Z@8-GW|jTRHs*N4vUFV4PS z(i4ezVI1G6+C>zbYq$0Axb~4j4g1oib&RQmX#CAEo)1x_eLuiBSf}>L#@wu7abFIv z0}OlC&b^VJuBTWzyV$XO+dQG6(MsvrZTHZ)xw~ZW!L7eB_QMA_G(JMKtf&D{?!dy% zSAMNhZCQx}Rd0}N2RG2H-3-bF_IQb9c_=cn-zI!IXx;3*yBY$JC~N-2ibDcCh93(Q z`d8q*941N%*!=2DfWXZR4yBeRG|;eq$ndlT)#zsM40vj*YZ#An4j4*(;dAZ9gTO# ztU>_Jl~C?(S$uXL=^hFhu<8EupuW(@f$!>c;0wCDfZ*8HUYkzJu-V6gr+>9N?M-t! ze=tQ=jdzB#_Io2&Q#3|)c`33u(KO$h#($p~Ady|+5p0os&oyZ!KhIE>FB1vYtWO&K z-#7}qzA*oS zL;QQ6?$4^b&eULM0wb>?t)<0+4F3GbKPEuiy{N{puO=3{3x1G5YScv|^V6s6^4xI| zG^bMCO*TW@$L3rKu=F3%qZaM^c7h>L?H%E*S%XMp3`G?+CezrbvAv7f=PY!AFc{Ye zIS3i%7hHQ^@}skgRD-SFtdjXZ$ZX4l8}1&}L-XqPofk<={WLS+cT6=)?cA#(Q{yov z#f-^S*Gb|b%9XLd504%KCz$IO;ISM%PA0=T zaVhHwihPh{`#!ZZe}wIPnR{6+2b7C_8+GHD+{5ih6Sm@mkxbaOp5PWThpC(>k4Z)UKHnUxE^ zi)zMn*@tL)m2x~Oawp5Z<@B~WU%gxeH{3TIwJElaQMzpstN%s0^JejO$s`42$3vLa z5|5q;5>#DR|NDsA^82WGbzElNUOBUuQ(+gSh_Vr!;{4RKJT^bZc^Q#pa_m(eL3tRT zBBy+B(f=)(ox#|_NV58MLPm2+ySeuqoQWE^aO?PVnK{hWb*^}XJd#r>x|?e||Le(c zCfkpgz~6RdC8>LkN!D<6Vm$5)+Ji#M(h2giKP3C_8jz7Kum>n+Yg9tj2n#M3GKV7s z2V&ixcMCxP*+DJOD9fC;>}>xyND@;R@8L^UFCun~Ec1s$skKRR@jT^2#AVa=@D6qE zr*^OA6by~zC&mq*L3-*j4vMn#gWO=(Wm2g3x_pRAU^ku?Y&C~)>%n^UNj(gClTlJa z)rXb#41s`SVIkx-bJ~w@Cy4xjC^p4FyGOx410A6eAYRLnLMXP-`~hdjm0+x|{~t1AwKk6fm2TYkD8Sx^@IJ$?++rA8V)-Th)M>m6c zH{7kr?tPB%x#THOt5NEEeL?=j#ZozeQmY6m*;~_(gsq9e9)fOTbdoa{(#C7N4<-3h zEE#hV;jFMqDk(D}WM0A{soZXBz$=Ax-*qqACFN&;T;w_ABxIO>x!*$?nx!B*o8D!H z@#V8^fxw63{XQ`rkGHlDTs(f6uH61@kLDbWSzDt4vR?7QbZr}fIBRGMxB{yu7cb?) zHdQq8EX`MiBWrrZwhE%v3dW7R!!$4;%LYkdp(a(os62vv@z^GUoikdRz&R#}C-Tb_ z&o}lX-nAOCBYqt8DRunj&bv?yJ{C%k?7G_WQyXSvOW%!d_h>RIF)L9jRYQ!yn-J4{ z9)z9fD5fc@YTQknG!Y~g`j5hWl9jaMu})GjJSf|92g6=$YNzK+fAZx7OzkSd7cCz5 zWLo%TrX^)5X!!)~X%bmfzq8e-Qb0PM{D^srrqif{TuhI7TOEpQs8N}pSjWebC7{Rl zDdIuV-xPZTJzV#k-1&LQW@JQUWaFkb;O=pa1t|gP{&N$Lzd?~cTcf7g>`QIC0Z^83 z{#m=(nI>kf{Pglk?M6fm&XztsB+J83XiF30%!4Xg>gT4f6Ba>JAYu_hH#B1E93RY{ z#Vt!7Y$d= zC^F=lgLbw?L7F+qm{l~~)8pjq?0R|5tAMA#fJcOdhKG3@44+_6Kt=Q9r1sX|s+Pm` z5x6x2IBYWx13=YC!80t`p`RY7Y`!AbdrHkJG|0%wche~^0_24*zDi{_!|Tnbf7;l0MtN#mAM!|rLb1zF5W@kE+VT!(hS=j1ULh5pMDGR=2n!@Ecr;~DMUmaph)DHodY7UVaWcj+|4H85YjkF5KH>wP>d5+-<`-@rbLG2>Jaory>N0t>Pe__Lyqyq~gvt$p{8=q+l14NxhA z+)9XN{3k?6t>m+8#Xb_U!@6=6L)^VNmsTgu=0SX%vn_RmyA9dva+pN$D|kxCWh<<_ z0NY2yS+hw!9y2eT?tV%C(2*DUU~}M>qAIj|042qz_}2po>&GH!c%O&Zj~<^`Wp{*e zY5W(WE0v)Ii3J@GcY74;%)Z%N&UR#qFMH|B({%V)Jo3xssOgXO(gCvFg?Ei`&vTtE zV1*KF-{8VGJtUCA*DJ78*UTjr%r9E3LKb~nD(=9MglTHM3?A~jlbv0& z1f^%|d`Fn;=^pK-Qv#@g0|O62=Xd0<4&)&S;Ggpyf1!nfA5#X_);P+tF-}?|nSU$> zwvTuD!&tD8m**m>ji`0xfo;^s+tb<8+xuZxCt;m-B>H3>tolR-L}=do+}dQ%I28=SG#VAnCbJ96Ci{tvC03BE7OdQHAeurjnN`uMHUJ$7@@G{EV zGgAzY9uA)7GygE1O6XJ!v)RF31%`&r%p21>MLU3yt3J9G7Wu*x*9~-`g|WaBax7W# zC{8TSp8qsIy`y17vN!lbk;2INZ!yVgliQgsScQ~uNh^9Chu68QeZbrA3$mOcLrj^X zSb$sFBHBErng^qF-R=3FE|?nwJNMNpMDwoecqr1^w71M;eB|WZ#fKe(_$QV8J#;`X zP;{V8t*ugn51iaHBs(1Bp)vB>7*gSOespIXBW0b{u6I4Z$&4S_J^J$#v7n{Kc^m8g z-OWQxbw#moQ+tC6k-h(U0oavKhCa`gZ_Pk{_I4XzfF(;I^A92)f$4He=gHpWQ{^cw zhGLx_trOBI4uMA@Djr@O3r#vC`MC%VpGG0xMTF;7(subFED`Y?<4L-b&w-2_G?dHKPu>yZ9E1d+f^|8(7_LvbWFwYzB5 zmlJ1WV8mz29a8FprT`W*DBso&7ykHxa4;zgOXlO9k+2fS+sffx3FhO?%oH==3X%=4 z(m)z8uuW|4j%f@zx%bjfXoGW2fo0%p`S2z`4&gE^t~o?#K0}ucUk$mbLl(wEEEjug zpuG9;T)=vEozw+IFW{U}2*SXE>C>bj^x${WVl{?m zmOPSwLr#0|x7ifK(^9VmBlDL1A{piam-C4%}huY&=w!%jNZoRU_ zoi3*s`O4~zL#;%~POT}UDy!YgOt$TUsJNa4yv=>r{i_bJKhRw7koQjpMAwqs(G&Rq znE-c3d{boQXKkVTax$7nDkrl$fcW?jATcNrqeip`I$>@(&Uam9c7y}#}fyIsG@ z{B0YJ1N|XRwmD|@;XQ$oj+A7RE+TVkx#4qH#{sP-{ydLY4KjgFBu}13Xv2fPv;#T& zbfV!x6B4CF(a2-q8crHyhb`K;q_nj8 zp7)99l>4>-h=%ps55*qj<^oR1eDm3^N~S_X;H)k;H=@s^%61;DN^8ri6WU5J{F0i$ zLF`;uDqHt2oARwHK+A{}{tsmKz{dUdgpC&_ zhVTgj>1_N~(FSRPp^eYEoAB1Z@1cJ|WsA;8+g$@R>-U5&Ap#)!_02KWMU6|0Y^$PC zs273;=nkF^$~kXBa2ujF(~0q>@3(s-Lu{9E&h?~I>24gk{G2x3kSI^YNZEX8MHZH2 zyQw~<{>08QwegQzbZ}~N4^z8PbSk5gJual*@5?f94-lOvckB`GKaYbnv5wu-B4Go5W-vfs3_}n>5g(nTL4*kYx^& z;Mk)B6#67x-o~1<6n3t4E^oE(kAwr~cSB5$yJgQrNazqD=~JA$8^Gc#U<1B&`HjrTC+YXNZ}=v;0Ki!3u@gk&^22v~bt(~uk1wj`b{|V#R*)5okiZWmI zWS!J8x5^=DhA^iT*kqWCsGPvDz7K%fcGUSa#iJYuUMP!CGB(nY-@0^oZ zZEPNBobS?4(dgH!vTE4!e{zoDqwtX(!wj2#!?DAGEQLq%=^JaNg(s%SZM|;bKtD}V zgx&ks@C8eM2awTpjYekjRUdvOx&BfeDL;8?+>YR z&=XZPwef@r{8{9tho>C~ct5YvJefpvuQ1-d4NFYUdbETL8kS3kDE8-!I)mmPFw2u^ zv|!9XO5=USy^%tJ{mUq>+hJukr0T$D=dg;bhc+RYs;{-NxhR6>HSK5dsf-9#z_^AB z==@WZJHP|cYidIpwT0WOtj9&KPTn<&G|f#`;7&KF7*<&6e*fUB2vpY-K1e=l1*1eM zU1Ucv=hX>aTuaD9Kb=>@ub+&O_+>o$NKj8a$CEmLa(-eQ1mJdzhhkPn1lD#z{9G+h z;l|_=FWp`8B{zUC8F;^;J}x1z0fRgNP!t}#WVMa>)`feR*!KLX*j6E4RgG3!#6TAj z<9Io`qe_uXgp9mS2pxcVSFVEk(e-LbUUd30M6^D(GI;cW!TaB+=Vjp&!KoW&BnG+; zwHXaxNgY=D;F)txOaFgp=&Klqa2!6PY8o`6(+{SW+;Gwk^?&&Hk%$O&`qRI$J@z@# zO|PTM`eelb<&ppw_9Hq77X`b*73c-z< z;hgUF5I~C!@_UY%t)(CmS^!m2^-@2l%A*JxjBB3U-{w#L0;K9v@kg%_sC;g?m(~5I zYpEdf#__Ne$Uhn3+zQ5$9$|o~&PfPuMkF9+IQcFbt~UK2tn1QUVF>Q!#G|0+)I-WY3vOX+GTIRcv`} zyj2hdTPEax)IdFhoh8y@5fgT6qJek`cSh&@y>-uEux*X~ZYKRzw z%2VQD%hmSt!E#EKxF)gpG?MQ=1q{$+j2-ikWCZa8`fKsx%Xs(ecqQznpU#Wn-`|%V z073wv8o>&yoYr_@ng_Vm924wrVr*^Y*144b&PG3~H7UJSVKuI8V~%O>8tLug(F%tR zX!zwC0Xe^imuup%4CuQtw!>kJe1<7@6fkeE^s)s^7JMOn0UdUEISUts7OQkUk{+ze z8$pbba4+xIT7-OgKcDJPBkaBajOw_w_e+W+8MCv9<-_^G61d(CWl#)3YD`V?X)p9qAA&x|kG&d-wKT z?f_7pY|p>+MYl9qmj^tIWsU{%pt+3`!;pd!>tr{GK>i*Y$Js+A+K`iTW}AL{dn)<# zFLKMeG?F(Iz7|iD?-w!4ofzfyK$FwVP!Od+^SL(DHa&cPvVv_}m$6KvOV7OfAftKR z3r$xy;ywFko()%z>dD!%B#ywn;_V0GtB)iiGKx}gUw5r&6&nAncs(;L;&#I_RWAwK zUh-PR9V1A(sAA%?51093$)++$=c|@o1I9Elb+4AUd3UI9Dj&6s#fFK%#gNILQ`Ypo z!{#@ub&x4)f2e?10v$BqC7|vB%QLC6b6|o|WS1{xOLajeBq~rAXjoRIW}d9|6KHSx zK1k4}M{+XvzqsV?TK!NkK(nw|(qauBLIdy_j*#+_xbFWYuMC*;c@zj-#%Wg%JRbD& zr1y5>hEO<`L_Tm6?kklE2{G7dp#LS29o?>i54)IM#D_X6Jc5UbU3O!0Qx_l;VYxh9}7yTGnLV$X@fxIG3B=CYx05q z-iSkg#TTpYJ)GImu+hz*2!dw>(gaXjqU^>M)w&a>nSSazxM|b8h!BqWS|)jxm{X!r z;pz&tzboJ(+1=^cV8B5D^z`svwQ`mj)EI|(*GtO0eXn(> z15R}b-(#Q0+9IlW>4 zg|DD(ICX3|wF}9|#nCYW?19~Z2cThu)bd`inVpM7KxYANL@ZgFC(zWOel@#hcjLAN zw_|h4cfpXU+Q}>m7=sv#basax_;P=f7xMJqgZkyK)Y(x9-i~!RjVe8}AOcO_buFJp z4NVp6@H*eTORU|*puu5BC#IP$gabSLKh^h`^uTJY(kz~DtUcm7jY0GJ@x}Ms;KTkP zxXXdLwC-i5`#_x4P-wx`zVph%q16j{TQAkj);Xs$A;^_!r1X0Oi&;3$>bDj+)j~pV z?Y!as>5J=;EF$5waNyyuU7Cn_3iXPG!#L}n3VfLJ%-+kbQ20#25g7*uc?Eee$>!1Y zvF(Nz5i5ovSG;I4d8X$gN~hnhhTa*VWJ`SqKwVl*!yF@!`D3jOhD+7l;(vaVNxw4D zA6hH#VX?H(f;-t6iBas%!Mn~d-NcY|W?kTI% zKh1-UV2VbdV@GoT;UH;Pw_{7($>%ky2iG}fRIt~uG4EY(Wb<}3jd@_>Y&qHzNGL!d z1tLmRV9)FyM(DQHVbxc^{`Ncy3)|i>6q<8knGLEFyqXUZ1y#a~SMXXjEY@YIVvU)H z%;@S|@v>>S*!10N@F?OAHhpD~gwqNlMv9f=iA!Ef-b4S3WPe1aBWo!loKtCy_pz*^ z8sZ&rc%=5cb}(DoJH`1QtO*&yh7TGBS`1D4f(uOq2y7LT6d+bWG%Wp^FWY32ykS#> zS5@))+t-0Ex$WyRvb;6&|McpbKh;qn6CR_ox7%2nUUpQ5tKwsOxh+y7b&*IK(1O=P z$e@G(U5&0C7P9Kr;nCzac~9|#1Qw*h{ebuAIs8|f@`j8WEkh6$8>hT_j`0z{$Xq;1 zMwTz-Vm>8H;L$@2{~*BhIF^cvD)n@F%^-)H#xv_Ovs=qM2YC?N?#DXYktm2z&aBK@X2OoB-D<*Ye3y4KS!&{LXQaD_t)Mw{h)d}YkF5>Y~5V2g? zFCwLf)OV~R{a2&2>`m9}pO2I=N^iMJ8)hcX0``n#nOmLxQaHcA>co99_1)+f9}N`VAm3H-2V%E! z9N@jaIh*R5y7e=>pKp+MnaRc_X5l}|?Fng5&oI+KtQcJncJ<<4Sp3}5vr4UPKg0U& zCI_u{qb7q|zVh2>DXx8A4zbjDPB6j1*PqqD^uMqF7FA{^uDT+s*Y_9z!inhTbutT%6bLG4RkBfo8$1SFmkQ`!2-8I6|vg; z^0~rj_Ma7BvX}pD9BV0Df*W9LW>#3-7a(%Cd!k#DJjg zx}$AG@wEMEcD5|V<}tC$_1n_v$=oQj%h`y9slU%7ayy=auoK;|d#4_7o#_O)DX$fB z+nw*2e0nau`p=i>4SdZfEr}`ejF8-Je3{B;EEN#|f+pM6To`YNL~e97QM}a>K(^(= zyzhgAXa1nFCFA?$e|=j$k7;{j|8}HBf1}dSPlwNAv1&ZIz8W0qSsEv_};wwOD$qI=q7=~!pFDyA`A)371$CiMM zVU`=j!`2RmRiSp`%XZItpnI}mCPbN1REvwVb4bemRnz^i3NSyvuqt}OrY^eJt`*wg zZutSwu1D!4C*N>g6WtsybrHxVCHVgRz8n^*QNO<8qPb9H|u+~@KF@JI0k8z*U zngax(YR0QKn8z$)B3 za{D{Iy0@YSpP5Q>26}H`F(7w>dj}|H1*6?M)aE^Z-2YIt>@UJ;Fe}?>X+@icT>!Mh zfdU)Z09nbH-KbRvk;`@HTi_seHNA(FXx+Q)YAA>l8F(4F7`GwYvx2!V7P+krdkia2 z9r9Y1p9?s-GkJ2)H0e-wgss$E2lW|DGP4+b^p98dSmAOi{3kjJEdkU+{>GL!H|f<*xg89j%H zeb-1-U;L*lfyD2X;{S5{-SvZ+cDH9OQe&`_Dfl7)+IBSS#>kd~MnB5-FYi;Im_~h5 z7p1lQUY^8{E^v&fyi~V;)v>FN7zZW{mbiC=xon-{W1*3wb+_v!=ZMISYU@TX7`W#Ial`Q&#uz*5OpF0&A%4xOFnyRNRFZIxI6mUFkzS{V1nBU zd!hPH$*vZU$Y9POoEG;{fT5ptBPfekCqKI&=YW~uT67kWV94pZibl6*JyNbmIO-6w ziG+lvf6mD4wFx_HiE#XRLPhb!Xe2*ona5^HroMg@*#+A?0;vT*gPTnmMYo&Ri3*sO z>VyDD->ejUZ1bkuj41b|DVa?AO$k&>hr>S}8b;YJ^N}nnUvBuI!6MyeJ}Ni z5q8v}2AXu_03Na7IMRhbktN>8$9A0FbR82E{i^y^1)HR&Z>#`kXagGqK4Eyjt;yqa-dUcZHmG(5IJZRGO|WA8|`Y z+=6(LQA87*vf3{zz(M!C2L?-oI=p1R;E-*)K_lJFQZ1g%UZ}-f%jhHFOCV_gf+C%& z`A0W*+aMb6AqfaAl|NaAkb(1}s!nMJ&hp31KBLUA|L+0s=%yf~zc)gZG(;}P^^gL) zR#zx(vjP5@Fv3=>+V>OEV&Ka&MdtzzOz5Xd%RR#*KEuC{1V(pPozS(TPLAB7q$_%> z?wD~X`F_L>sckYCfH|62A697hAnHd*?c(X4^!hMJu0Q)RVdykr2p+pCBm25=!&Ecs zl*ltzp}^H(50E1uw=9CeiY7pvNfUDA0OGP7q zf`2ui?Hx&d?y1_SY>%U5B;dz%C}W4c;t2~s7xyC-}?;7uwg41op$qZ+|7Yl= z1Gf5ZBM6D*!7XA4wGBj3XbHVWeBY4dF!a(1$@qWa(!@c*slV4mBVFyOCVv(!VlCWd z$-ibk-Y7RQyW8~sUP{W&sWnJOV10rX6?_B>?y9-@%IsgQ$YqoImdWYOdlz&}d*!(% zV4Yj>q?4H(dcKutYB>APsxQl1cH!VuYD2Bu0>)>YH?Zgb*RTSOC>l*;BxaClRi;$> z3okDAmRK~%H4u(afoX!_3xr*QuTYmO^pL>#X?lG}jQ>rbXhP9fzqx#+LEt@GbG<3; zezAKFA_y^ebW_`YuaGr$pDt@gR9Jmoq}q^g$(*7@=kp&{XNqMddOtY(R%mp_T|J|%=Z zuwmM4h`ZAV7Wov7+|=Wz*az(g{8q1(m1~Edt=o?H9w zos0wCMH$Yzt_sITai5n@^5iJXGIR2k%fLJZ{Un$f8nMB)>gvYvLe-eHn5D%Hb;G=I z%_aa(Rt{gc_Jx01b~V+(f;&efLW$VHzWD5N*(6L>uII;NEPQV*uaih)%&8!HP~vy3$7#epq4O_EN#vmAfwyR)QjF6FHW=R*Ret^U zGZ3Os+QzJSQ3Z9yI6>mf^DlUU=YQD`VP8xH-3IHKi%kJoA{2CwlUhbVK3k7czEkr* zFF*wX*ug>kz36rc<_W<6K=XXb;64Em0*)>bMUxsPVPzWFmkmA#x5pig*e{>B$Y zPuMI#80W6SA1_zL{KZRRq0`APm=EqHmMzixl zFy-fD9b(esyzN*n-i8WuPg50s7MmYfoQnjyio)4Z9VnQemiv5`^qS>5GMBUPf!AXf zW#5>7)#RWD!WzpvsLg2+f6N!Kfu-EAlo6E8dP1&G2I@8Hp?m!3*Lg2<%4iDD&I8KpL3VsQ^aqvE(DmTma}>1z%4t|Byi4&Ck&eSd&9BS}(8==fto+ z4jhz7@T>)<6<1caJJ;`%=l)@gHii+VlD7${`R19$o|syf-H&c{{d;?*-~E7Fz6ZC- z1m?e1!I!EGEqMBM!{KIr;=rhN81IUExr88CP;LudRV(!sYgK|Z!%+O+br6dpUU8o) z#esZ71KBg|=nUjvoeJ?zHilA}ex}q~qDdhQ64%pssDg(Q&X7)SU{*AeG{`a9hmU^x(Ur=T2A4hiOQs4~Bt zJaUPXDlGf}1-hDMwz@ql`0##AO65*Ekx(GmI5!;IJ?cm1o2*kpmY{G-^#4|4(gd{( z%q8&h&P)w(Dmrp~XOTw20QrAuzy>n0XhPn?Z~BSOt@1?eXLfTJ@6*D*sRhk- z<9g>z7==}0LvxOi{^`})P-j&JvR6;~bs#C!%3;ln_*AP$pJOU)&HE!BDkUx<(j4al zO%W8mr|B4f@;|VCiprX%fvaS|R69V%8t-H381~ed^nxs(oi1bDH^tiVbRdWTz$=d& zv=XZ8yk74L+W&~-D2JQl47&gGc6Dl&VAdo%5J;neJO#D*~>Jj^WZpdPc2S5PcKf@xtm7agROwz8-%@FqksP{jFp*!OQ7r) z8iY(y9!FAi(Xy;HGychH9^7%{=<4t*`#H`DAnLAn%|IVd*Jv#IbHn16rCx|GYSAe5 z0wWfs+ZY(4$|vvUeU3U!IadGUy4#)7aLqy|ki$oWK$`=x;Po$8)^dUUh#WP7;7<{i zPlZNc@`|Vp(Dp@gPfMNPP3Bl8abw_`tM5;jeHpIs;4Kh80l%~Dr#sr=RfC;-ml|=~ zI)!$b;cf9vBahxo60yTAsS{g!-pMfbVhdW4i?B~tCv*;52z)pUjH=l=n9+sb0+pa9 z{`c$k3L47reM2VzYcN*%Ufplw7ku^f2nGZaC2zfeIf*0v{LL)Yc z*W90^h%VGyhlTj5or9L7-}>VjAwPI(XpTD%=^sz_$HJ&f|DcIo!CC^`~ZW_M%M2-_kDl9zsI9T{}ktaUhiu>ujloAT^B*0ix=dB`q9l@ zxI)k~%4+%^?n^+C^QF9sj>cfYeQA5%=nGu9@u`@YX`i`SwAw{uiGKxD6Bgwrrtd=^ z;Z`mjT*rm?OV+;VMk8J$TiY=y- z865mEyo-PIBLFp*7ebGq=d^HFC7uc7Mi?A@!5VI=FFC*5A9b>k*d6cBl}$nBp_GM5 zT~Bj|txZ@Y&?%f0HzXC8BE0u%xUaY#ftjYHKBnqvNW1ZI$Z<63RM{r<;n;T__HoEH z-;&Bs)a=)SiGN15;d@K`w1UF`cYlv1iB}TZ{1E5yyr}ERm{{2Bl)#Zx^&X~o_x5n< z=^pKl(TBFVCJ7n1TSUf<5`bR?b*$t`hgvUYX=OFi@ZW_bS1%e@ScP^ujHkCyn-gzYu-=FM9@+=Z%Q>pMe^G`FJ3Gw$M(wbd z2IRGKkt@Te)QZ#0{K0V~fzd61_nzB)SneliK%RG(Ql26aS;HxgMcc7`3d?Tn1$p)6f^A(T4FQnzd$$pRcNR0muJ z*=D)R7t6#y!dPM$n_X{&&Q3Fh7Q)Y$;Ml6d;~Hhw%Bj3JK*tEC4ffy2mSc5;iyRr> zNlC;^WF926>6`kNs&vBj)OT-VO`&JEdiQvLBH?Gk4=Qq@^B0Aq$jkLA)?DS~m7SI4 zyM7|v{^4sfg|`>aUA_5II%AJ{zsv3E;%w1*=FW=BzaPgWGwHbma|A!e_t%mru{?1XyRqibaV0R*Tknj zKFyM_YF+qs;zL--@z{lUvc7M+P3y-`?ZVQ>PJM#rD^Jz`-D%uB_81jdnw?e9G2j%* zt5Q{de;VC3XGG0Qa9tnp530BarbvwGCl^f1v-_m%9DYr#llqc6af?x6`7Ksd3(KMX zXDmElonpaStGb}foK*hYQC3u2DnTa+mG52VPDgU{>c;h{OCF@D3g2Ri+xkUrE0^AW zq|V(Vh+)2tykAr7XPC&lWeTIUJ)YM!Y4+af4ciSFNw9$xo7s!hcE4d+66xW@3lOg1 z+c}HV7Jt>PY3_~_dtM~u?p@WY*d#kZuHnH*c&j-t>PsX0qk-6)moNLh6p83}Nwp~t zHTUixKAgY8g&crcjc~Ib6u@cN6NSHKo)Y=Jl#gWC{z~Q%AiQy<}TN=0O^f=jdtqZDQh<{9H&qqm!H4qgZl*QQ;bf1j|mY!O+8% zfxbm$6uAa60|g`R^5mk-!kYr{@l7j^K#LgJNB*dkZoJQ`^3npk@-l33&wNm0=jc?W zG8N|TEHY!I|L*Hlb>EjTGGb&)ll^Elm38;$Cw=%V45Z)`?VAMgd`Fa%T3 z=`x2!XQx}U`qeX*p%F7#MG~9Of5BFUAlsciU4KUXXC>dAX%6`synilyUNxB1+vFz> zmHwVx-DKM-c>3oL7uW)u&YwX_U0C<|>0c`#pV}vAib5d43ZCTsl7AI=JwM-MTqxfB zTF|Zf&9kvD($qmX!E5^2G4}WvD~avTl0RD3)Q7~8<6Cc<0z$KujArOMcTrtJlb(aOy7^&2v>39!TH{WLfW+t$ z|F7!|^btt~y80M)v~^CGbhkuIpI<)Pr%uWOWP(L|;~>gUBwOJ1=*AI3iE_P^ovIc4 zh_6`8GI!JI`G?C~qd^@u1Loz+PfC9YaQY~I4;SgKY>=Ftz^7+eqcWA#WsWIuGB-3R z!|qo6Hjre0r zTvNpj8u$I&)X3v|sN}O9_B5rRBk(FVE+>EVl71>@Rvr;N`1bN%6n-!??~zN&w_$ry zyW{AZdu^4jWsyOq$&JK6zmCxodL;)NEtk+3KEmr2FBs&rqV)7G{N+QNYh~k~Z#u1P zyiitD@QIU>NHDmiy8D#ZjIPxVHrP%uY<<@cG*_2c>@7e?eVo`ITLWnzHQxIlVni;< zN&@CKJ-=MeUS>8nhCN}R3qMRa3ikTgZ%)n>tDIVGyVau`lWv$iBRzh9HQR?-htww) zmL?romG$P1Ux4QvT{~>HZj~OIm9A;#3_}^}6{5|Hm51K;Mg8ZyzY?nz3(U4#Lt7o2 zGIaQtm8JJCo%7~doCv1JecJQq7ROtP$-?ogvC4HKj5JhV4{K`fSa8Zpb$Sgga*+|` zpYM)thaW`=p^)&P%q!fG$XW_{djKTg3ss41@IrW#3&0s@~(nQj_(-sAMw*^v_z{3}BR z`qj>_nG{avr`}|SK$vfOe4a=;L^30f5R;G2K+Q@QG(TxFF19gUfvt*g#>0@`iDlB9 z#T}XDN66!EaHHBDsOOp*>Xn&&=SsSWmg^1sYf0rc16_nD<{?5qMsv)h2-Wy~Fi%NI zFDS^|ifho0`;%e-y7f!s*vjG0AGCcOI$T5aM%8H}VH@)Tp3dUMW?8Fz1nSPnY!tsv zp=hDdMBV|-kqm1ur!qgO{H^qL(!<$ri!y)w4N{(Nl>3piKjYI>se7k4mTST7 z5xoR+;r?o0ENf&=w|gWQpZ|vS&^nhQGSKLp=6#+DcdSW_+c2gj1jEj-{GZ>7oTvRCsiih3QqYuil!wUrs`kN)<7(S(YW4?g4(M&y-!ch<3ub9WH2plcNup<~29QsND4Vd2R~Cpw4_ic;e_vtC2T ztCb=&^_Vr2x%X(^DeZ`};xo93OZ->^FM3%IyVf|?OS@M)1&e#)(<35IrqPjY^j9QR zbl(5aPrAFCT7GG>P*BIZTDz^+n6`%-{8jcQNc8@;rme6y9Tx(1HWNWidjK!-*}QET zm#3ISV`*XTS#N>|v8-AjVqNt-5i4V6H8; zfOnobSf;1?e7R?fEbc~W+lZ(OhIyAS?4&>I0>gk6A_5WA_yi3Go6+w?eLYP{$KTIV z|InzQvSW-4f`7PS5D7nAw0`ab_t2vd$dCbFF2x!(6VlphqZIYio9fO z7k0A+M+zT#`S4oMu~1_=R>C{=3SwgZQM{#i(U`BqLsMgeto2YgU*S1h{@ zZE)7hqO{&q-g!uP3~nrJUvM7(v>3h-pn((PFa2!q3@R$?A4{dOtCys29(2XDiPEq; zc;qy1+%zI*5*MLHml)huX<)k)OEh05Oi1R6e=ayHbDdtrxYDC_Q=wwN-RIIJ~F)gq4qcUDXu$L!9oyEAZsR=X~ zzYnoT!9OQxq8CMr^8~c+HcI>M?$9Mzc5n@+3MBWl2mBkHF*!!bw9mOlN}S`S#8-BX zSno%$T$7QUfe)dJkml}<{YV@Kxlh@^*z)SumP6N+cWviXt+;%dnej45s}uJM@9*0m z9U$dy3LiPM;2>dYJ&edlBk7eAN<6V&PkE>JdS3Q0JscsYTp^_zXSRuWm66&_9oU5a z_NPA}QW(FG5lMZCprROUn$ZEc35?3Cb_hFHbS`)MYa1Jm&d$wHx8r7`@a8x@E5B+|9am^l z`{k)&G62A>jix&iW3(TIFN;tI9`Ag;Hlgs)jcn)7lT@vQe09Eb1GNY0abuo>*kCD`PPR2ogH!PhY!6oiDCn;Q`%0*;w}!ZVf&7Fxs=6lzX0$G_&!6LuXCQI z{pN2e;q8VI#IAqN9W`Z|JXABPFxmd`II3Y{Bqu7HMA!zfKyp0C-V&QOmA1UVCGfxGd`Le}StL z2Gw8kKexKN3L{bHjwIkhB-h=$#u&#ErU3U-E`z2)jpA;t#3Jo^kBH)Ym?l~}whM^n zB3d!g%#23EKGn4)(^Y(YtJanIajXebqjY!93^hReh}LWSdqH5rdV_hkd!<|a;pIc&s@;RvA*OolVHM!9xE`gqGRc_iMZxBQ8rdT7b{_0krT zZi@oT@5v8;og{jC-l3D>?D^a5$iEZEOj|2qbUv<ly+va4>HzsSc(P%^jRC@4efLP^6G+@KHsmMUgdrdadJB#^3A9zL%0jOqW5!Cp< zl*hkb+>7dc@p|^kfm+90M4QD3t1`b$#I|$FOWS4j{_^=N**ZP0z1ZQNsCzXsLe&h$ zDNs!`tA6Ok(HTY2fugeZ_x6L&rLRXEwD{lg`8^$yMrCTnL8Z(6fqVF%KaO&QD!&V5 zXD1WWL-hvAvp2^Dj5T)S1Mt zSBMDp&bJp&FKNoppH8GD88NDw*?MOyKzjX;A&kPh=7dTvaH1GIKBnxhUCLx?3L^t^ zO*Tju>9&ej)}(!=cG*m#VKqm*x7`uSSSauhoISs!rrw(4WuKI1j!7zQS)aIdfCjJ60RnPR{N`9d{ff4 zTl;(R^YeiF>{@abX*Je&Pij`pn(8&&9>GcgQsIBY!-EcDYqI5=hZEF|$WtIKHho-Im-s6!w=oeX&@ z!6r9rx93G$+-@_laIfuLQ8K3sc-fEGESkJ()i~ zUGTR}g~|}0gEGBL7|kVIeJU|Y``8T$EJo>|L)vOJz51L+8$|m4S%cpEukFm(e0yH4 zawvcsCYBr#q}!R~ZCpKKXDit{IR0$KE(v4%cYD3z?1Y9YZ5LRJt$d-3H0$Bx9 z93_v&dV#4#Alc&-fxg~}Nk0z2%^52%7o=;GO7)ZRzSaC0R$zFz3!BsW$Nays zKmKeb+r%M@d$kTG=D2m@okSwszwgpq;!}vvDBw^>EYAi~JY-M459s!R^H1ZM4!fkP z{QE8jcL%uI!pJr@A`aGHBs9VFQ5=odGfsukw@a|DTpSEnqQ>J>t!7{pU*3&$+o!=J zz+?hGn_VYzIi2kw7N#tSy-RcMe}%}DO5;$qZuqytQL(Bg&O^$jDfsxN)pH?i?Yt59 zX2*i$uIA$Mz#1u3l%)pYJ)fK@PDD`|_kvn=$IT0{J2ThcOft#+JDq8OA=+EITG*bS0R)>W-~9t!brSt^2y$5&i5U^^ua>j zjC-#|Fb~i=SxcRvCF~T#E$4uX&yNd+`VfbvN|(`lY0vBg1M{;=e>yzEkux;G1mMnVBsk&< z9WG;+^@u(B$L{zSA9|Qf&pu5{3z!Qq;mOIh;H)Tx*wTUh!(*d|?Hpov6V;vBbt;@G zS4l}mn!lr>5vJtLzv>d-GODAA5hw}M9>p}7SPLKvH<(P8+TMu*L?F5gWJ*f4Xo>-k zN!A=x5h8Sf^YfoLS)fR%EP0&0L95P3c)QaHGE6Sx)~Y-`&;tOn2I>8Wa-h6$srM~u zSr~h=Ep}@{@xM$r(87l(?xi@l7Fx7h^CZEI(UlsH;08VPe8V2gqzMp`F))O`9taP_ zbSsaU6kDkK7`_4`yc}gxzN*GANE$>lq0s!5&FuNrzuAP4VEeCoL}meTLL$y*;~KUz z#C#C{{IT)1cyJokE&xBDlaEc4yf?e~6@wIG&BT{qTvU*YHT%>dI|BVizBacJ43{7s z`@^dcxF4M->iv3rEMBA;hIa?l92KAwB*bJ!Er?d< zJ0K!Pmy|_l0}BxbkcZSueml`?i)7dmhPfmIp)|@}GPFCkeB|7EE&bUQNOqkh_%XX@ z`YM$$0XPq#4+W4w1X(6i7PiH?j(WWBqnV7aMX7jOr=me17HSlGjmw*x_W4cZf8@QE%cK$_H)tj$~1Cq+u?> z$(=4f?o};rJVM!i@;Gi)`ZAx(>BpPdYW)3zc!4Ih2Go|gsxWsm2K#qDq3GXvgpi*U z+UDkpBq-XZ?TcPz7g*){z331gsLp@lMr^?;REA1!Qv3Er6}~zIT~iuBmoE9XSADs< z9izGC9LODgeIHtqL5&ED9}^Zi2KDEv@Qz}jo*_8&@@-~Kz5uohcB~U0vlx{_BNaVM zO$+$t-^y}(&&D5qCb73fF9hBS50u`$uZ*c-#L;TCWgYpgfCF6wN=jXD(N^er@gR*m zAB12)4X>Ua@3ZQ815VVjS80Yk4qu2QvrT~tB%mFC40N$B_HM9FoGCjB#JFb45cTTMj_^%j5~5mPnqhN z%YdJ)EA>;0`ndH*gMZY10lN?=4WR{G>gYs65A7I$f?xRA_7GFt~xvC@T~2YyMpakj2h!>!T>LaYZt zbnFof0-ahyKlg1u1?vwLs8xuK#)|5Y0<)H##1Ew*7j$d5#iCXEbpo$SdyOl%6-DEm z0C<@}En=~`5CCJ*T$a>Kg^Vh=2py#NNhg`II1lYo+%>~CUsiVwKrsH1DUhVm@5Xd^ zHVf3r^DBDnhRmp&A1cUAg)N86WNI!KYV8-rTZnqY_k0~PK&;M5`jo?!Pso^7xXZIC zs@GNiS1nHMjZlB_;xQkx*K)cAnCBhfTl-Y|hB9}ShFUm$J?t$wnwb8DSC_Y+!*Pua zCKdd!3YCst1R~P1=8l9UbqrS2IxXgcN4yh))Wd;y(fq1d(lDJg0fOrl zo)S>t;1KGG@q&BEv z!afp{jHE;eZG~n=VWFBIa7J=Xv_-178aIk4KPqJ>HmZH_y>L`J(l3#zJ54hz)}9`f zRNQgG)cI&2Ws=FvZF)|Y-CW5sZ^I!2)hJk==Pq%$eM`SLtg=kNC~NLoE)8clKd>kI zR&fzcOdT5HjQ^$4gHnBU)4UlY8V;a|(7g8( z8{(07pR^2D@#`&azYQ*S0kW{)u;c4{+31BO4)KJ*%RrBu)bV-xM{)5OCIdNb*OIw` z>0L1YR+_TGG~3^wctsd);=faM*0+mYcKB%Em%-`N>340L!LgsATtf1jkl9j#qBcg-e5Z`cgwPusAPe^-Cjxj283*xCZGvG`HU_$+P?a zy`=cqB|MLS>j64(8uMd7$Zd?tgp3(V_xUSs&u6PTcT&&?Ew;cM3XZ(9TUOV)2E?3H z3KURlS7iPIn!lyG$!4G>DLOqV<@TfRyP+JZtOk*YPMv(w=4I{xJa|T!BL#Sr(OXig;>mG9vBCT|4-Yb1z~T?B?H|f zYK(L|6+V$+Y_y`$jIz1Msr-k)V)j@hNn2k|@l7*2ptmo`851g^Lo}R7z%kmt=PpJ| zh6_Zr-l{oy`RYs-8_e7YDev4nuHMbZY3?h4++mk@Hrr(Z7|&y zufH1cJ-p8NW%&pG24mA3v_+*ZE3d%kKavCoVvyZJORBe9jXMAXoLKo~$*q9OmjhU_N2|=bY??hj`t+y0|boY=_g^(!`nVlLOLT;^LND| zmsI?BLS1~JVZ-p_(%r^xkGnpOuOC5T8*u1ezRbl&h&f-8$ERLy6_32+10FcGfzKuU zv&?W-%VL$oT&ZnvkcYdy>EU@IORi3p#yenAH)xraBuTlHM<`#=G1>k0mXivXzGh@o z$wHx*j@cmmODY9m*_eb6w`fcFAnfx9)FeZg17|8Ns@rEO|cnyAo~hhl;6DqdE0S>p%Jx%r{$vp zjkSs9<=4?`)>>gr=nyJatNcd0p~dVfLYEPdp|;-L6!GAi8X2Gc%FdG$KEqfEde3|| zUiFGfiKjD*HvVqh2yNShzQNmty!P0;(wDdGSzh6+{pmP(>QFWz>v~Z)!wYJptSnpddHFS0%1)Qg2~ul}~5) zFOs=cFQ^RskRbG=9r6sM)hSw~8s*%iV=c^ddiqo=%kMV}Lc)qK`JLI6m6_Jl@-dT* z?ZJd#6)wR+r-YAw00jc|^*ai>5t1I(L7#aLaB8Xf(uBAJiu%{-*39mV7Hi{l z4r9BO4e4c)TyoMWpR}}43mP$dy`k9!U7BHE{EG9GGXqlVRi`-fi*sL_zi+#9OQV#o zR^+})y6U)qN6B&fqsyqIAEZQCH;oIh_er=AN-VpAo;IX-xE2VDS|iXJn!T*brMVQh zcLi@ClOm=J{+BAbmh+`sgbpUNDN*CoP~SbXzcR{rNjWofpwXgt4z_qQhY0+jC|KHg zpFVk-H$w~TjxqRDJ{$B~fM9`w>6oo$5JimKd!B2&)b?|Vv21Ktp)^G~|(+2zAE-&wt zHJUkj!=MvCkowSW-y8C=LhLj~Y=zv8lFPun!la)&8SX?-<*X5hR_zV64h!a2yvp-+lN4;TIl`KI*_H{(PpT&84qU5By z{QddH??*CqTUE;T`Bs=1r_EBgh;)ZUhsy_>`W#M2fwuuWLJ0f6Tt?=h4rY96zdLN> zpB?0H+kr~BDy=MV$OOHN?Q)b|-DqMwQZZg7df``Hh(E5)!lYaZAt%9PnL~gg%%WXd z>z@vH1oa<2V}65#lZp(r5*K15`*|XCC>2YywOl+_RXBC_uw$q8G8bJWwr|LUDZtiB z{krzI-4w1~D5u9yc;uuJ1~G>#znrxnm8Pg&1?U8faQHDb5@i*u450HP?X6kCS*BITtog0Dex z?si0=-gb@(JibUt#xjF8l2T_MzZ8+D+P(CO?pp-UksJOwpV&-WjyKV2;Q@)!q(tcG zl6#j&*C-5b|D~9tQUDzvxA&s0G#?)S7m|7+GRd)*Dl*V{(8ph8(h153pbCZ#F%i$csG%&z9?p!p>hdC>?~s%?T3BuX3rgsi{t>0TNrag|!>N{g?wObWs5S;< zp_^TMbFnRKihQ(tKfD4pm!08NW3Dx`L*sC&$PI4b_BJ)O5A;Cs84TwBPsuloK`1Q_ zlB>dl9YiAA*28cJUqPLMT-Ln82AEC|TYz~1-v*tGPJsoSePtF97H6xu5a+A#soV+? z0IQy1dx(ky7?p?#wOvW94$Xg%=36pDH1}!r8?qhmFzjaU?%c?9<+yAtw4Peg!FX+h zIV=yzm}v}S$)(cfs@@jkxGr60B3s4tM8+6gO}ympt`-Vy?O}UJViZb}LlyD=#VkG~ zIi5|#S4ci=dScc2Tm2U4z-2eWTe|LZR2fEywJlr0l#5js&UD0EX(W1-;4g-K0 zG*2x(?@VgvNgm-J#S_iOrq(}o)*`+R1vOu&NqV{F@}S@89ItV~>h|dzw`5ISuVycP zs8da-{ni|(z0V8pc+f>)nAP_-!2{1HMT}EHEXT&5hprs7bH~>40pe6dT!0~_qN@wu z?4eJX_LOqu;O$%9c&O0K_GonFurgky#;baLBMsgsEMQdU;kY`B@Wh1gXOgRrM5%c6 zYGJU=)G6S#N>#M}F=qk0P%iYR|bh#P{cdQQ7KDAbUkI zpH>;5XoUW5Qlt?CE_C8YAH-r4t2bzF&`{K0wR}%lSfLLGf>ZoJ{ml+?Cvqgtz8vST z>w|tmSV+C3QcNpn2J_BC#geQ9v?n(%{$ja?#-#oxJCg~0VwXv;ZdqZY?na_PAqTZ{ zuc?Ua?o|pr1PX{D#hjqxe25oJ9Qj9O1GF6Mr%2JOt?Y)eSD9&aNHI@7@jEM%C`qPv z#0-Ft3%YIIk}UWg@jc389Qk&iGPBw|;ul1HV*7qe3yCo#-@kIk^pV$-O#5-@Uhcq7 z=70X#?DU;0*=ye<3<`}Vy#f*l^ZEJYGl9lm8h%R7o_YDk^hVV1n2nX)_XR)GzS>d6YezwJ)CdGXV_-Wb;RDr#{p|Qo5(11NDQ+7M8Q4E8GrFX$zKZ5#($fOz1dMf-M5F8PF0W!t&j zpG9QNU;t#rRlzb#A_MjQw6BJpLI_aMjfHmWcjs)j;Bbu(-%hWP^G|jftcM|}!Y~UE zir@T~mIn<$NJT+uvj?mYwyg!B7Turp5SqgvENz1>=-}t@XAlABHrpAa>Hz*Zm6&ZisE?y5Im^slFP*#3vj~mW${e{CA>Q96{x8xv6O`h6 z6k)T$%ta&$BsLrCZz`*vuqu>UPWEi++&i4)V5`hg84{xYLjWSHEmOJriiN_kB^{o=#kG58wR`u*2R}RRwf);hN z$Zd=`Qziyd7|(i4k`ab9lJ&u76T5JkIGWIi`(W@_?|r{XL$&$e{D@K zWJuq-DiCn9>9qsEniqbu$!ZJy5&SF+qx9 z>+VAZ!g7okwAvzPo2)OnI+r8vKv#C`hSxQ1ZQCl1wcR7^yu#@1UPoxr3=hA2Dk(ne zKn~i+PLg1d1XSfChlG0E%m2#~=u;X@Xht56YfFEcG^ce%jJP(Dt^0Cx_=)k$Ob7jk z*LGK)yup50=d=@T%cRnU938F zmRU1v*J6@kpo~-Feogqx&WxLfXF~2h@n4NJ}3%Fbkj(KqxNi@N7@TEkwPe zadQj)`GWzMR8xJ_Zt*`Y_W%(XG?BjxY5eE$^_h?b7i~!mV|PGTmBDyDmwk?R0s11- zEkH$Z$+VOslnp#tQYkG8C_lgk%+JENRRlm;>@s3R-E5{4kpIAw!m@(<7lmYbOv1nb*-*L`*VHmv` zk!MUp3%_VAG?0pg4tbk_LFJ%3i?l*bUuYHc$GObeaQHMBfdWZl(uW@+K|Nn~$8H3G zFt3p*Ce$B0{m$lQJ5DS&UYGgg%ODEA;d*HxTE+h!tBQpdLxc>JPaqP_5g<7CepC!T zrn3xWGWX`Nj1 zhpS^el*8sqGvzI`XTN{T`qWG=#H>qrkU~|B>wu^nq^EB{-{}zAY%SHuZoURJhm0AF zzD)uMkGKr(s5pT%KxGQ}IUEmN+IFPdC<6PIW@cs*6=K1UK?SD+((CF!Z+hOAaxK>?$2T~iODzyNL`C~=gng3g)DYL!A)u(no&e?%E zjWt*9!71$podal|0h%Q9k`CbN<5#$e1doz>>OCnn@ZwiUp%A`gpuQ}=^3fyZmn)Lq z0CWR!J-_uIqSO-ew4p_!7%z9#a_tus&1Th-BIgVlE~Xn6?L;G7Iveu5)L zB_IeW>-7%C115^*qE}`AT>1vRgRif0$|9Tq?&Ec90Py6R3cSEZsCE-JKIjF|dT zJ0LQ4u_J`j5T zK95Q6X4%lLRh%*)?+*0my4_EERP7?l(6obwI1t?m@#;b@@wBA7Lz!YT;)j`>53hZK z3bKTBh!K9}73eRV4`$|iOY7reL%mSn!YRdz3hi32G%-4V^iH)tv zEbcj8fVTM+UjuVG?@=Y!mJOSgEPfOD&kF($E-A+M=y%V8(5s#i4R;!FfPBLZjVbF- z-I9E^juRb!U5H%>YVsO7)s4($`@4Vxgw6?z9pd8D*PGcRBUykyjZY^1ZFzXH0dI#+ zuFaAfzbUGi>a7J0N*=qToRnR?Ti#Zaqoa~SIic-)_O2~h^@X@dA}{f5?$a!&bn?(1 z6d6VImyEey6A_#%IoJNgrwL88rY+hfV8E6WSm%rXBB|RaiZD0LUKM!XQa_!=)3+G#A+ISz0 zE>k%PNlH?e^n+~T4_)|$KR)3(ppu7?4ogB2B}MN*M0d0zWm z@du%FNz0<6Ys8M=Zi?Sc6%cB89^JuFsFODzpg{2`1RLo-HY#Alc5n7N`BhqVyF)wb z`)?4=MS^1N=Y5}s1g_Lh0cJoB9`<>yP{u0{r(+KXo)P-Q@)KN#g6NcOe!D*a+IP=3 zNyiv3IE#L=LP`ZoPsNl!hG;h{p7nLm@m^Qi=8SW(-lpLQ2l=jspTY|y^99IOX5)RPhyJA)QJLC|{do|U;xsXERt zr%p{R+aIpjOj%W)yEHdJ>$tBnzx@8r9cRM8+rOSFUVFku?#8s~9>lrf&~_v7T%XmH zfk8b--Ov@CFI|ok?%{{^w{2d2MdX*|^Jig`r?7|Oo)^;R6u#Z=5TVBVsa>@FBb!oq z#8!Vnf;*7^F(Jl`nh|~sf7P`_x|c9dU}I651+EH2<6uUz|loTvE_x*viNlL zDY12sv9<4Vv|DbGj0hGRawuL59!t<;j z)fLA+#?e{q{6V_Ra{65`M7a_1C<%|7}6&31l@#*+fNBAiG=d z;sLtnOPB{@>+rXsAZ_Lx^sL(PcYJm{mQ{;R zV3tamYeC&=6wiNhlY8;n5p)|&eYAZpDj${x%E)~!t&t_z3)QseB1FWy*TpN!@8!u_k(lg zPZw)lv3$0FE>WwI)m+EDP+{2l{gz2{mBmGAPPcdAsex*hn*_FJ%J?2~hf%5;URemETto!nwf?@0_| zZyR{2km#`K)uY+DxUhcd!)C?UvvJirqZxGepJ#20Z#wP2*G_lKoAxYToAkd0Z+P<4 z=(&-jU--orLn{A=7HiRqcHT1!fHN!J4MfrW1EAeXe<<5b%Y_H4w2CTnL_F=yUwlzr! z>RNmZc?|^k-H`8eFl=YcP6uA$_S%b7y;Ya$VyW@|^zg)M`}hC7F^{u#pUAKuf_Oq0 zN^^oM75UreuW*a|nN>YgH%G|cQ6$<6k_hFxmW@Y77D~)nEY&?2Pk-R+evVavEGXyk zXM|9E7SSxI=J??>;a4PR(WN|J81iihv~C?$UFt1Ze2L(BPeWT{5L^@=nBh|&&{)fM zdB88sqq%Mu@3{o+?&4(P+w>?3y!_kXD;{cpp-LL%24)wESa_Zb>lEEkeIf+U7{JI_ zau04h!}d2&rJYwsejOyY3DUhQjR>qT{Ww9C&(NU<^6}DTA7XEB`Vk?X!-Yilbsb!*q z>T|3K*P#6n#bn9E;E>Z4YPwVzOL{6?*GRo73tcl?-m z5)ar(7k{;xn0_)olO&ix^%=9~N(ayO2V1MKEgNU9 zpr)C3B~qQ-!Wbs~Q+pERmW9CU-)1V5le|bE6+J`CA*gPeYd?T?(X_dwhfJO*?#_;A z%8qzb#BOrz|G#Nijh`RIn2Qznq#LZ?JU4LD+;_e#L_jQ}^~HciWr(P5@pi4qogT+l zsV<)4A_r5HtlGO?!7eFvrV2gEkr&)29$n(fV>jH_Tcs70SL|%y<5S*bo1p7#+p4}==tdHkY%92vId7td z>hx4=^?%iSD1M$mb9GUD;_;b;QmLOp)YfT#?AdPW^&X3BI%?lxwdbC`PwDt4qk1$QU z_7fP6Hw(AbzD$0Zi#heHd(}JnrA?K8w(w?WfoP(K8tfFT!?Z6HHx>OB>n$9HKxHjT zo1;sCZykDElr8n&P(z4)ojDYFHpdVZw`No1@1!GmL{ep&b)=#f2bJSi?r0=7?M>K-(60Ta3#W0pX;_tE~@{COW9hRHi$#=cS-n^ zIlO*WDk3=_KQ0Aleg8M{vTgln=ijO<6c(~y#k)k31HBx?7uNmR8PmEF4+yF`=4d%w z)Qh4~G34fB#Ll*LJvVCXUGI^SC>^;P7UD)4r7{!ZrdkI)ADU~%GeGsllIdwV5K|!E zzSpBIaEVE_b@{U!5+>XHO^N!DAj=&80P(GzC{doBYBvO*ilI(7&ZG}rQ1$z+e6IN_ zf{O(C_U+lj*lFzjXQ=4M@5l_^s+qkm4XDm59s4xKct{uN&mr9#CQ6HcprDetKo@ZV z-hIV5>aE;(KOc(bw;m>&O$pW5V$nH^)v9^)graIRU0&HQc0V%beQ8&3@h1;-!Kp6 zME;%Ytv+!%{J%`NDfv!3KGmL@28F>GGhU4(!-2n7X`Sg(Rl1Feo|R6%C$xuTS7#pG zK7y{p%?tnc&(kyxCDgUE(5BuGuI!45K^G|1T74)Toa%adD`;yyMHaGRXihxB89@Qj z+9JOAA^mBSVUe*FVF#nmWL_xj(lwc_8AQf$@xV=U>@G)3+4_=I=&5}ATwCGA9?NT= zRk5ms#I8Stw5myq^Sp~oLaj9&#R=RutvaTByn0g0@WR-d_{fK8St5$Tex;%Tdz2`C zu-N+ZEbOV_=J!v(pC7y6VB(Z_rV$QcxUV2&Gqr?u@{+`B3|YI0{6w&3WDcJjruUXp z#x0#9SmAbzz}|JhR86lUxlk0pk$Q7BpB42hOEj^%y|P+nW-`*J7_FRW4GIqU4OjpZ zs(=ujgs|Op*EZ=?@=GX02a)~f)?9pxH|Ho$cVB9$(n-*{D=ZjR?L`QK_2+m!Nv0zHS$X>X_q$hD4$)soC zsApI?rKAOjMtIoflRZ5$&i6TA9hX97kYd}Aj&0&6`c&&EeiE|1ONN4qgWn(5=ZW-e zmejI6+Kt}zXl{Dd%cac6K*d#9DN?U5DSqbS;!0IYS04Lvcf<|^0Aeb>Ft#}(@~f73*xf{>V4(ytp?m8q zYM(!uTOJ6PJ@EAEBD|SLmY}77nu{HNpG2!;s3}a88UA^Xm0Y$x-Cz|gCZev;kIF~S zFqTQ@>&z$?pq|gJRo60CCYvIrqP5^2@^sAxhtNf;LwD|S)pwRMbr;+YoI`)H@-S!i)q+}tkn{OYD z{d(JCsf9S<>{LXSxx*1Q0NAfO+lq0Pa|q6Co0=zOJ~n7W6=FZfJ-PjVX}}9zoUzlD zuas!EcEx+P(1;;h6~l(JI;x5n$H9?3)OVBBj|WmRw6Zrv<>LO*(VCyScV6Q>F)GJ+ z)vnx52y@OD12xaaZ>qv!BqXz6zZl46DT_ zh|_BB%bu>*opbA11Q;|hJZ6v+UOYmwk#ZL}sCOQMJ9F7ZmwZo-#q{ppFeqHc?a^UY zgZf=X+1uTIr`+rP*cRbbo z`+vI-m25)x%;wm)glsa8nK(H1v1L`FvPbp`A!KK-?CcPd?Ck99(eFC$&*$^~JsuwY zb3bm*dB3mMbv?)Rx~|vh9O`B`S`pvvMh#Yle;VXF0Y$MJk&Wi$mod`D=nx;Ij;o&1 zC3XcGXr0CP4x{RjjEP=Drq9^$(>NsW9EFo=5iRs1rcV9a$4iCxxWs@8 z&U~Wn2%M}wRnkhc%K_eN_i?DQN_%!RM8vx5zP}YoF5<@j#$wfTy+^Ul1FG&TbYp!U z5epQ=RC3yeWFUXZV5RDlO-Pu^LXrx-tWOi*pzEu_Iu(gP+upB|A=>QSP9#t3GFbtF zhB9e2o{A||P3~3xaOv23F>`}~|2R(h_+>NZ^W$4l^OwuBiXQRl<|jHE%@Y{CpWV)N8Ln5eqYs8z6B!2dyhtD?=V00e31 zRZI2t3+$(#_H6!U_9L{nbj1a#8kemA`GHo&E)*y)YK%ml&rbSV1}-vp9(kj$sjXfP zycw&O&ITD2-fBWfL%V?TJ9hxGJASN9WBWwq4jd7*gvUfhpm`uZkf7-$+kmqe?YpbD zoz%ByhW5-6$Fg_XxhZOJ*EeqY!YFv6%3`BgUr(2yzfXPtIehmy%&E<%)3-sgCCtgk zqpoWwCCK)JDlVx{w*qZ1Zg1AzzkeKB?04I#g7fY@PsrJt(e{JVwl$!ATTw?%vahJU zkd{jUNTDU6Bli}dRS*6@!pSd_dhW~m1eYjr=-TTU=wqsCXNtx!->r}zg*d5ZSA_cp zYRxE^g6qsY?#L$f(FX**1R;%lmKXm~`X}JE%Q0bSNixjAg9sDs_ znPEE1sMsXXwAVjdW>9FirKnH;&{0Ax9>d=oe~I<#>^TjMeVsn%gKg_Ok+o*O83AZ` zo?K=geD)^wYl#cLOxpmlS?45@J|UsoZe-qld1GSwZ7H(lB9`dI=D4c?v0eupVPj^l#>US}v_mTIFV#F#p4qnxeVri_A@A-d5hN_CSXIrrv zm6ey5$im4iIRLoYm*WM^e;rKy^(l7>zag+J@0k0|f8W0v@pXuBG!Tt*kZXRmCMnTV z8@T^m_Y|wt{hHdY)cg{`PWBXMp>$FHI4Pgq0Ewxro^i}y0PJ5hUV{EXQJwpab%6J| z)cuGC-KJ*+or}3jJ>M!PoT_wNl9cX4aJKO<{l+J{M{;YHcKXS!&C|``)$&$|xHeHv zmbBGne%m|J(J%rTdHD1;0d-h2(0HICFZdUpP;Xl| z0M`EZ$LBSX{Cypwg8a(k9=w&~!REUSPr0A`X8ajTVpGOpV>lf$E?Ae)h>dI2`;i}~ zdPkG~&L*2qe7D_>>awjC@QXPaqg=-78%2${29&jN{O0~c z2tpmC>kj@IXFZqXY!9jMUvM7OjYI@bq*Si3)QK@n`50|xI&+7Ll+w01wvV;V=lvjq1 z|Ivs(I1(kTjSsmNnN*7W;#=?3D5i7}lqe1yCp7Kd@k61A!Ho@Ncww_CsQ@@`;3X}`P6zV^uUITl;r9keYm%Pi>$xJV9 zS1<&mF_kFm&$6tGgiq_Qt6U240j$h}Mb-68cNa=?uUQbj?{0pJ)FsyVCG#e&)7db2 zN7lvAmOr1l1O7;c2^#c|5lx@j( zSbkpmW0M>fz^rCPudBQTL4W;F(xTm!i%X)k+A$(wm-M=nimSKMTVz4f%efaa%hil? zhS5vaS0K}Sm{#;e6UWZVe?F4a01WVEAZaMH*@@C6WzxpMy6*IkQ6(UTjbx^OVdHvc z`}jy8!!#LhXj;jmqfdBV)LGDf0kdN=Oo*c!B)`Q{=8&r=@cl7W$-0X|YRwjYbH2y9 zuxhxZomjgPlG`~s0Ewnemxe1+JR(TC z4eLWe)oCSds75ug2Rn3^r&LK4Ui5%I-jw994lLEVHT70zI(%ci4~oAsm$#1N;8__02>%;1UUrdtSX_-9ULk5v#2o z{8g6BWH>Ulc8~-RE=ftcB)vsI6As9J-okKYJ==sY7C+p$eHjZUnr=j9=6xJyv6`f21efc#OP7)%?7>O`)51#g@aE~}(JjJ^XEj?uvLL)06 z&leAKLz__{pM#W^nSt~iXoTN5qyO%!r$WBlm+w9Oau)j>N(U7c+M||wh=9&8RFH5F zUMy^b`y*;R0(0z)vHy(*NV$?>a^6PkRSZa} zxOpfGAkFaiRh7&X4S#5(aHUG-?f|#uYL%StEetO{i=rCw8`5n;X|1A9R!Lb5QaxXx zKsyNH3F}R)S2>kWJuQ?qdi#*jSU;+9Z)i3m6!g#*Rd~yw)=Y3iSn4L+N5()hBej6? zuuh{4sn(mI?K}X7>dfN7au4r{R;*Xm&y8#$2gXtB1bcND7qKU4rYc0l{&#s!e6Ho0 zsdGyEFUvT7CuVfKmZIJq#R) z4<#?t*la5DdPb5%n_cq)OV<%1p=mHdIJDrgC2pJJ^W)LkeGb@>6YHC;BLwdd5N&mV zO7rk{n!}+NA`i)~vihN%r@sb~AL5uwnpBud#*gUoY4OkHG2a8a!_o2EHTUJS;@I$C z!l{46nS7eyg+rXVI?sHZS`G|K>-^9u#CJGV^G-K~%VIFRF0D`h$aF#?jyvoTzl?c> z1|wN5rmAO%I5r_C{fqI*2}@^WItNG-XR;BTbg-3vf>OF*)64&mwTrM_t?C;_*FMgM zU&-jF6*jx~#K?@~r{9ybPrsiK%YY0`{k4w?NHyzz;Vi7pIOFhJvdWx9eP@z0U*QJ& zht_%3am^*fl`-3()pD$`EIVkHZyN|ufGws!FPKp{?0GeM39mH<@!5T>24r>4Kw}!58V9P4;l`JvCFH-n zsBdKoSJTnwNv^ppeOImYE}!xuIsX;o3Fl{R`Rfb^PnEwP?<+m-|rd* ztHNLz0n)RpD8Q_!(7X)q=gMA+8t|mQ^{~;cV_`oo7AjtC=L5t~ioyzjsWP>02$O8n z?VqxK3xdm_RArl>A?uee4r=)qWNZhm7$$@gtfy{GcU+h3tr{`Di4xvs$wXT_Q_WsA zED9%PL~CyVX#DHE@WOKe5BPEJLmPgM({>A;gN`|pSdrclkt^vexs|PQ(y28TT5o6k zp0|HdjPoCQeN!t2v{eA&SXcOIVH?Cxb><^J0j1R-?|Y0>?O;;P_kV%Qj2o7}oJ8=+ zIqlE+qgCE&(%%Y`GQn?R{!c_`^D|v>6gGd-_AMTNv6ybFq*W* zpjuni%*-&RXKn-igGOx8+sN3cEFJrr%m1=EQ$2Rn8!LEZOmof?@c|vc`75|8}i8 zYUMn_wJ|iwg#U>UoW7{e87Oj28T$ap8ec2MN6jB>dSVr( z^I-uJP|dJC0Cb(OX=0w--(CPh(5sIbg_fn(2ap0M0c-RSTw z(2#esWX5t5lt5C2d+^qtJZ`-UKXvo^i^{T3N>UR_j%^z6zq|yg2W%!la!4b1cVNUE z)QovcAW|^=1P`|_$mzIaiP?m~++l8)c2`@_qfn;1*oSBB+l_c=IVMtQY`6U*lL>_S z>P$MjTi9~w8~C4u@r?n;#t@ak3Vi3MK_Qr>CJ7jkd}x{^5Zk@ywJRrKQTm#t>&?7y z6zz1>=>_G8oOm`{Ig4ytkj>_-`sO-$AE_E^C;er`S8N{E0PU~LJ zNi9IOwwTF&*+G$rk2j{~qk9=oMm~}h%eek}xwrY%LygX0B?Lc-D|Klr$l4Ih&gHDY zEn?A2frMM>Qy{ZIcCNX4dqOgWcQ-GQO+DeF)?NpwCE$^PWHU9dXB$tF{w}X`!RWXh zITV7GHr%H-9i~c!Hed`w6z$!P+!p~1>|eSFaX#n|L7U-!a{7asfkAHIh|OjCRp5H~NPNm|2)`O;)_1xUfERXX`6E+9_K>z6y~a6>in( zK7gK?mP^VJy0&p37pSA+w~=4Qw=aV&;C}SDX_MhSN8T-Jkf?p0RyD8Ga8kqsWIgP9 z;?Dyv-t{^%#k@7c+Umhykk*v-+ts48PX@3G=KA^nfD21;BU*Ms7RTt?bE&SQBg!EK z)DZ!d?2+Qvf@VpC2a%0g1kXzXT5SGMs6}K zn}$QKNK#Fi`&ysBnZZY3>(Uac$%Je*ZAs`Lf+L(L2UZP-Qb*tgu3q6pe=#z5sGY-< z0Eyq(<$f_P8au%ZyvSsQUkrJH4~G!St=}xURFb$nb&+%Qg3z*oalTgjsd$Rq{yv{% zN`TjZ@y2`x4>&p9%|u!;pd~JVP*Z3lRQ1!vL3u}!*y=4x9_JFK4e@ZX&r~J*F8Buk zMJ7I*`Vy(dFa~mzjuj*Ab#2Nype&Dg-UB_o?|MF)@_}~_ZWtcdF~XGMeV0?2!XnvI zo)cut=vIHK+&)G&iC7iaUawr%DgH;l3qBa_kg>}GWb8+C(mM$|_xAZI1(ij;d= zF%J0A;Ff@!N891C&N6RF_=M7_{0B)chm& z^Zz*rquKLVc--_gEF#=emjXHx4iZU+u~1h}h_#cDtJi(Op=okr%}EUBE0O=dVojk< z1<=NKe;>BnV?)|F>E9gj(pTyNv>mN#no>Yh!Bz=FFK#W%95il#JQMKUPeAAFIVm^=9MCC?M~I+HAB3cBACM7+b2uYG_J+FOR&#_QJA3 zx48wm9t120Do5TBfEm#4h3(=31-iC!yk+f*Afosor12a(?U1ZN>8j3lFmv9McYD0x%VZx1_FRnH7yTsZO- z2ErN9!duf_bw2+M2`~zkVi*99WS}E|;qP9N2x6IYC&UQm%i_mb3CKD@oy9nVGOlKZ zgOjtoMf2)9DGoFcy?q9r3kTl1&I(g52T#x=u-YIb=oEM~=j1Bl>t8>jHgX4YABk#i zdOCPtg8_Iea7?UN4>svVt~NDPxgJTMJ-@qmMNes0@vQ*1QlXE8Que96;3=EQ$d3$x z$k&<|`{SdKvW=mQI1a=?YU+N@8KeH&WY5S&`}~ouC1vi59PPw%e8oZ%yRPp|&|9`e z2*LWLFp|XFB)a-0(`A!LC&8dfP)^{Bxa7&T4=o{v3W0W5b=rITA}=VbhchPUiIZyR zEDk9X$V$ALeKw1MP7qp{7=!Kwo^B4FL>h?OKwFl3*N2p#%_YXiSMGG(^G5FGOLK-Q zNvVjb;irDF<&=;J%}UYJ6k1lTq#iyWWO=UF#ht<}87gQ!M37yf3ibv-gwck*Fg1oC zxAmxu>4lm&jN0kL3pU6|INIluY+&emQ<^UF_gJ;BahlG2s?~tw2|RER1jLj!XQ=db zEOkZh$Ysw`bQ$N(Bt+{)zx$f6wM47M6Zog9m-7E2VV{lxha#poQ?S-GM@t~6RIwj19O+sxf-I z&5qJmzfCQVhlqD>y|!HP0jLu)^KDikLkVwkI-w;31W$U&=@CzQNc9lk{hz|Ghch0i zN~{m=R))W00TY{`{rum}42g9bRSIrwo#5J)Tjm*juK6mB7WPGjsrKRx;8%XSj9V6HI6o2;{OjisP@esafYggV6F{X z{YE8_cE5NFij!}po7CI6Z`}j|OtWNcu_6SK4A36wqBu7JYlq%CA_!6#E~WI*iG+qe zXrjw~bZvc49FoR|c*#)Zo%W?b0BzHsXT3-wNTJN7x-bjwTA z?64V5D+UcZy{CYPia-;H7s1DIjmLuod*Jr|*1;TabEABTxWk~qPV5w|>Kj@8H81|p zvR15TyFd{??A?^u7Ed!lLph%&V;j-228IeH?ub4$N?waoa_h;2aFQcWn8#mtkiO;} zM@&jlr4!u!#0O4cf?q*p$=qzx_G6fw0Rwx&i=VCoPsQTa-__G_?jEcHJ6GVB`-Nq4 zvkYw%bA6r%Yz%BKZQUs_EvV0+aA99LXj&hK8{FTTAe&n0(j~)2DWAfXR5&sM!vs1$n6~&=dihdy!E85%gOFHGt>|S zJ7_>z`J8?1e$ffYOyN2n|j1X&B)7A1-)FLm(XuMS@mw!*Y=i|IsmG z;qh<2ut!RDKge{5@ns@(^C|glLjp2Ow1uukfm)b-z4j7U3TRU~`(2ZNq+ zAV#?nWP7Il4q6Qns^j!4+=>K+?eDQ=_jlC~i4>gr*(TPL9q;|y zmo9mz&oB!^b@}V+`&snrZG!f@=b)b!@ZsOnOnm729RES1s=aTcCvEf=+xV!RdIBv! z00%bvmx9sZ&{UsGXNtz<)R=A43mI|C>-*)Of923Z)=Esp7g8Q{unr`_ITXY3a45!_ z+~CsyojeeG8kzF$3t=vYi50JlhG~rmyR^8Y-HYpRJ1@}H=Z zQ;pWtVv1h#9&TIJPJ>(S>z3UI%{`E8&co4+PUtA5#6K13LE6SaJx9$jgJR9nQ(Sgk zvS8rq(e`WT=P~+j_2{iu_1q0_Ln;R0_OAIiAZWhNMVS4<`t_NTVd0E#lFM7mQo2@Z zY>j?c6&=2hNLO(EGxK)L0`Ih|4iETV2?u=4>4TmtLfR|f%Ys?d*nnL{*d+8H+p0+j zN=@*bO>4Wlf1#lj&+hFrc^*^l1@C}fk_qJcpg0CD5C*V+69vD-8T5h`EI96X+MI5J zj!SrC`-r|6+8edviAR17C6jnche5L-PNVt`Y}uh(jMaW! zb-$+(AoT)xDv^<(BVaAbopF-H{N>W4+AOL2zD6NepfdoG@{&q5Zak@E{#^4eEXc%o;PtbX{jXccS=o}%UEzG|BQ5@1=-tDC|EyO z`yd87_iXr;p*d;Dd@r!eYWzJ49asIL;0swox5}7#jV*L#y7gh%I%M{}qo7FzgYT64 zh=3+1N__aFrRpIZT+k$`uhT!o=5pSrVh|Q>I%{FEL0PqYxEbK6!>;~I_N!qykx4Om zrk1>SMn0>}WlxlCjGFm-#5r>RIRNVkd*G)L(3=3rVms00bZeA47KGK2{kryJbozby z$k-ONO1K$70?1L|w+(Aily|yz&1YgS7zn!ilu8W_sF}U_$bvJN zgkXPI0@amw_ck>?H7U5ZDi;8eVPY~K@+PpQv1uazaX{~$igQGy(7PQa*otwjl z(gKMG!-M_OwJhgemnNOv8$l!oI2&FZIX1D6z;=9`zVJ&^jpV#dRhpE33CBHaVGH`> zQw{94p=%?$dh@+*8Vh)5yH}|HF7;miuYU(1$Q_;62-jPFgq~^|!-BOJ8z+0sr|5r; z=({DFt;B-1$t!`~dgGAcm`Z6)2;W142WarjQOhGx&Z)Bi+Ay$_d0<%xwBd&4mFn@G zIR+d+sS=g(q_w_k#gB(7HxB^ufo9UK&GUHB=FA<$Tg$SrHu|EGZr*Q}6%;u@e`2Nc zpusL7+GtjJ0SL76>;sxAFW9=VcW-~ny_2J}(zkfZUUH2O?~#v(xBpP}n=G-D82^%& zv^^e24o#Ah6x$R1ZwJv2!Ev@4JDxejEi|^mRIPk1`XqVa1qu?hHh>Qf?(AbC2QhrI zdxWw+_tj${{&tK)R`Tv)n~%R0Bcb1iD!Cfz9fn|EYzLo3kM)lRNE<%=0fYPODP^vJk=Cim;2Z{0DpQalD9P(eRq8Sbb+3x@^ zkE(B9;{OHG`H{2A2#*@-O{l4th{&MYC|cB+>Y|(u@&PL$UMSOf4^}B;|$a)8BFXYz6mQ%kF{6TvPw!yU&OVz@oMg!5>Ax21Dv7@;*F5`tos6vum%W<_ z2x+f9CqgF(u16E;(Dy~fUo(DX{A@Di_cb|py0)~hUm`goh#Hj{{`tM=o#)-*op^O< zoZ-PEhP(I9xk%i)bEzh;lsAvYEXC<`VGooJ_Y3loFLNJ$2n;s;(5<9@4~GB2a2o7j zpUWTJ#NlEuj0EuO|D**oPp*f$o^M&=$4^^Y{`k#So07m9|J6`{H{p>z8dpo-G|Q~$ zheKRm9KT_5WEu=SS$fKo!9Bfu8ke7|B8xuvhPd0UwzdqbQQRPQKsa z+|;a=BTSJW**NFFM0+~p^+vk`q40y{JB`0m(@S+&5Om}F;sN$s*9;00igU?oWTlp7 zvVNmVzHFFgx6-Y1J!WOMf71J~qQZq(QKx>W4}0(DKR@+20ZfBzb^;lobaShzQZRdRW)> zO|#H-;dN$pMz`Whdb);|1#GVx$6=F29rO)jKjXA$77Jm_`y$=455gh~=7un~`KHnl zzuk@(%yFYM{LA8T7iKgP$ll7Jos$}C7SsgGp#BN3aj~xL_d$|d0*Qh77@zW%Cqpy- z(IPiQZ`;8@?_#0(jB!Y>gPTtZ?Eh4a?8X(weH5T7;Lt)y>OaY}o26K`V7dqP^8KNi zvTe@Xm^R-Du_^dp47ui;$L|VX_M@F{C$;!6dMKaw#$E$MWU}Ab8$Pn`=3!CVCu>jP zkI>;c*z_m&snlL*GI$eUO6%Jz%!KTnW_eOKyLr0%tOoXoADvH@Z{dE3T@@1ppB0`z z23_BDP(_HZlWm1w%mv%)_n(X9hjPAs^?kjxNlYv)gTsVANw*luOBlo-IrDO=6vwm1 zf4g&MZ_(av3Ael;g(&*}szKABiiR0@p83-oTR9?9NyS4q$S$)AxymNlRdZ-^-s_I$ z7PyQQvFc-0U67?H2<4F9$F#kw;WbIPok@<2fkAPY>(LhTUEMlL2GZi_%zeZ1(qj=K zxei=78*7a(^*!m~p&KQgS1aRBC}=E;W|OSuZq{oyFE*ES4Cd#8kw+l~?KIT8`<8An zT?tKPn^aY+JAE*>{OR(d#;FHZ=TW`#A7Ver13;e`M<)Uk_Tv(qnz&zZrZN*+9mmZx zB{+2<&aWTCU<4jvEAp=?4<5+(y2@v3@s6$?*$msnSdo+xMB6>l3l{NAxgc9X_??QBVro#t)QA`AB?%ksL|S=-HI2<01P#d15-Yi&dNKU z5qJ_3+Bc1$8>3dyC;L8lKR7cXgif{aAkj4`82&6wpPd+8+#Y_K3RCGK=Q*@FQ}lB; zn#)A`?!84I($;BNMpGt4|@(CrYzs}50yuRTk>s7?}Yi*iLQf2hF zISf0bvAGeS7bnK2Gjlahptq;;>NCbVFJUu zoUQCo7%&9q*53Zl{$rSSz%}JSHQmaJNzhQ^Hsclwe zdDtRk}x;S)`e(OZ>P*r$|`B(C+}VUE3HGHZy`HKkkw3yPP_+WJd=OgN~4QAIjanW?T_v=AW_R?U~UxM(GVx z$uN3FKZ-oe>s#>h1XAL;1|3|wGt6K(>$REST?SBWVQQ1^`r*$Bn(cRdY3p!-QDuA> z&41lo;R+whuWIX0Y1{*x*x zD9!CmVW}jHwS6~(5)3|2kK5>6VserD+Z_|9T77}cAG_qi_@~&^&Q(KLVT4JDBK_)! zZzQ6%@B^eym~rX#6wT6u2V+;7dtXAaJDA0;g%logc3;v=UWm=((BkA#J=1Si|MTg@ zWQdSW5_&ButmFoIEI@J8!?~{!uH8|o^hH1oiF;)T`*&Ix67HqAwcV=NYqt!9oz+lq zVwMH1@e`w~dk*3WW3n8ad|N_bG?y*Gck-9lU%XUD%ousz?-GEb4RN-RsR7*qg5`GS z-*plS$j#RN2KR+g!<6hd78NbgteRaTJe}U63ev)pIhfazCb|sEUr!tKU)7e5F3hjJ z{>b9s98IcYUF%xk3b75d&~|R4Z=9;9H9{y%czrjmn=Kx{@j5K><~bOT-de@m$Eoe^ zQ}v>t^G_|Bu=!~0qF}W|qB!hx0bz!p9acPWaD9zN&$K@3nfYn+rj+jGSXjU1gBmu^(vbi50*q&ktn{z%!IC9skvn32^w*_J zI5JppHL zg;ds0ESQWn2l3{Xx5bmk#ttth*;aiIVAGSrW&z`x_ItW4fiJ}RV4v!yyVA`|57ZyT zpkO6b7J9KdbQXaKbt3FI! zC;&E?nipH~=%+AU0et5;K7TM5tRB8GG5ib~#bAqozK2wEKhL)%hR+J)!ap`QVsE%Y zdkBoU4JqiO8TPEL8+a$K zPZUe$^zygWr3DRM0k)Vn+SeX#PH>0gkKaq!y#VNh@6FQF!IwB1QSlf^%PKwnXoN`q z0Kc+Hfeo*QU_3h|teMDQS8aU&8!AB*r5xbT9Jh1YTQe*$Zs@&AG(cU{Z&D00%Xt%DLq# zTEl87kv_rRlJh3*v#}Kfzy$=GY41Md4a|6QSvdj_4jd5OoUv37LwRk$LPiaf(?V_{ zpP28)FqJ;4sNy%CZ!M!x9+_I&e*m@XWpA1VQGb zHwOnE^Yil`hOGW87lFHC`hl0x>4#(5xfy=Q7oF@S3In5XmP9sas^Yr_dtC#hUT`P^k@>r_d?84S@)!Cjs1P<8{~B@oaXf7j zwpa1$Ju;N7ORPe7+E-k(i*FkhV=WkY$tS5lzQwDO@0Z)2h}S2fnZ}NLFE1c37{(KP z?dywy0tGt1dEbJOXH#=I%=vj;x>Vqg_FY92WuQ8tCX&u8U0@z$zIQTnyFicAon6 zR0b}Vz^2|_0;!u0>l5O=DdJtNL8NHGL`^ngb(XUOry4Q4=`<+tm>@;};{de`B!j0$ z-8K3bowd@e&~RfTdCT3zD3XB%mn4@YmZTF3Uow>t7p;O59E`ACgo5Z>ck}x0DkLe~ z9+oUN`6bW0kxj`oF-En~Yo2LVcfM2|-10(^CE<~twl|hzKAs-)K^UMrgP*4zTql5irxw(Pz z7tS;aygQSM*?4~keY+wb45gGu6gm3?+t^kl{t@+h7XfqMfb@&Zz78e5-9jD(Ls`iP z8OUGFUNekmDnr7ev*}+5^}YYTcWIU#F)dvw?cauwP3Ld!*UBvqU{lrS2PD6Gaw}1s=GpVq zuY0Cfq+rB!S({u&=7^e+!`xa6_j}x>0FYN4yDMhV)Xm>sfjvIul?y)KPxeQ zu=CI8=@R4VCe7an#n^l5Ihyse?8IkC6r&_0`^CU8rB~pA8$nf>68^h5oW?%Mjuy?Q2$(ivxgs232fZyoSAuq zT;-0JckebFFFs>PEO&KqzAO_HdYbQ8y;L;dP{cpSHzo|7^H)VLp02niUbVd;*~>!7 zmYqN#S&e9}iXX7Z(#9serja>3JQ9FZ${Jpi7H)2_`W1GY^NIFbr$_5f3%z#&n(um~ zWUPo}p;I34UXg<4#`owzjxNnSEo~j0D%VvCuKIntge)FdiDx^;66mOTmlgM{)n>Ojg`-#vv z5+<@KrC;Le1WG=3@@&N_6~eEzim))n*3l!a3Chyw0WV5JI`VPd3Lk+6%xtA=WKk;O z_m7W!p4CZp!?&TEuMv}W|Gb$;V5d~=7v75=+JE^#(8$= zyAQv*iqlXtId_tOevd$0)`ZvF_@BskX1N^09L_Sb_n%~}U>fd3nq5X3(b81Rhcnoi z{s^6Q9-MA6h!1jN?Cnz-Lw~z-qG%j;hD`yN?mSK2Ug&dL{UNZtb13g<3Yb(c|51)6 zFp>kytG|lH`XW%|gZvo7*s3bw-X!7X(^Clmviyn4{K}#p0cX0?^b);1xK*XYMR24} z)W5LA_4573m-g|jkWOiyOtDp~U?e)2b-4;m@P~*KD(WCmep2_Q*wf=;f1(TB%YyoYWERSw732X~&pJj-=E0(X#?lKpsqp1I*GWObfl-t=?r zYQlW8C66llwGK44Zyz;>15*c-vq!)j-265ne-rpTS86Vwz94k+TU#%%Vgo1@8$Sd5 zbJ$}u?ly_Cwhr=X2uo)%sC0M^yS|`UMJrtHD*5Vi2N@HAisx5`G}K|%WX9XLE%t); z^S0ROQN!Ov>;gAD(t8`tG^gRMtulJ~F_qI;6{M-+A01+=&|YNDgF%5xgmKoo>L3Y# z6?iJJw)Zj7z@8~wYX*q6>#gaLaQ!9&>$w=g5ZUW9!5mzRDo+4-0h0ri244#KL9hGm z_1?bd{RZEnFExqYrdB1xgodB{OmwISHc#DZVfzAYtdpZ2QFl5BKSyjB-aU|Mi z9+&}FtVE$N2n767C?WB3sLFVMjNkt%?(I$S@)e_Ya(DmYfG`uI_RTTHK9v_6zaklR3&tt~I-icf{d2-h$iU%$ z>&K*`8L=`1KCy=OI~HgNkk(+)XWJ%@YE6)NJ4bX`&X%6B#4BOqJGETxPEmWE9}bM< zG!}$UluJir>%8we!lNQ+Lj*%~U|-P64bS7fI)cC6g^DqxP?3N3i?xpX@m!Q-;=+ZH ziI3@07=QXcQ(B$q*JYa<7beCj98B$7WJlzp8HW=D;k0fI=p_eOzt+BqeUCzG5GExY zv%9Nf$IChm>67ajxoolb3}BO+rI_Iji z4!C2=+E^ayuJ6N6&1URnnRNc_C%KgYfwmvKrgbah2);G*L!;srlx`uKGo^z;iHAQP zIC@z$1s|gyKYL}B&`ffV#NG|&-(l%;1Qxl2D_~8hv%vgbOk6q}Zvsae+n^yCNbBWh zj;_OVHeGYSKX%QiOSNGVuc|*gJ5yn*F({Q}5t)qscSBa*Y%6ns1XQ=AMIBkkP~_izWya=1OoAy`#O*MS*le%_h#uI?PyLL?;qJ4 znPnp4FuOK%l*S)Guj`dYb}%{TfL2`RpRu2uwvKAtBM#)!lzo$Q2rh|CdU6@Pj5>`UkbMr^Fn(z7&wV z(QQDk98=|sNE62uPI>?XQ+<4m;X-xqCj*77rvtrG5hxOC5>Wo_eusjS@`?GDwQ^#t z>d+`<03ZZdo&buBU-Df_aT;U{;C6NwN*O^lm{~=cf`{;FlP|phBH`j;txrSOPKbn% z>nk&l=NYQ{617Eo&1$F@8M|?j49PatmQ`0!ilV~i=Rs%&s&{ov8nj;eyZz2C_qa6O zG5Kv)%~@6}lZ!iN8M~N^Ll=?45zg=i;7{?VPk%CJ$r$Bf$)M8Ud%gUU%F1ic7Cinu z6>pp6Nm(#S$cjT?zwpcTa0dCfgcmuinzkoqYE%s5-dyIMH#drAH;crlrJzyn&5|dN zNhApH=VT%@9QfbG(5g7i@Mo_x|FrjnR}*7R5+o25A1OcjsGm4q9hya`134l{FKa2v zMKE^8v^kTVe7Ha+$C^Pq4aMGGV$spb9=penjThQq+??S;J#aRuF$i?uUKg6ZhPN;U zN?L@Bbr%?X>FZn2;iRYq8d~V4e;2!NF$b3=mZSY~cXRp- zTm(WAtqrmq)fBP^qV% zN5KD}qywBOVDfyS2Q9k233tk`p6@;cS5Wyykz?7H7en7K*m4>#}5^Bel5(A;Js zPn3)B59HtpB{VX~-zML|eUI#w?OOo7YjSdZYkDo*B>eFQUKcrLd8dhq#VGU3n3!3a zC5seQqi1aLWVuSsL!j1hjM&7ikZcnNRh2xqwt^-m(+`O17iny-<<5o*?(fcdsKlW5 z>-!uK4A-S+5E2K6gD?DN|Hd-B(aUdJUS^9yd6Q<}J9~dZ4fWzu1{Au^XR`#>G9 z^VX_-%c>s_UnZKO$!7FHG`PKohX;W&T>{WFt{%rrpivzacM#axTFR&?^Moi%%u*F5 zm;-yZRHbuvsg~FR7E=}YCbNUZ_=(ekjyh;SJmc2}cIfQZRTm_|w@m6z1{MpFLal2- zE8jLF(C|X|^6yr8a5a>aQ|y{7MZTw6noP@Jw}JMI-)*RogjZd=y7@?gUC?>ree?)S$- zCXh&fuMW&-|6cvVI#-i3WVEEi3QVi%qDK|+oRqNFt>odqpO6`b3@I_)wi~Z*T`6qn zJv74PY|J>Z1{r1~`i(iaopwgfKLR4dBZqOxs%5U{9 ztV#P@J>E8;rBY!4mQsgi*!&fum=(c1<;0^`)U#mCO~2IcnED;u1{Zh->-8gt9Qk@VAdV{EfC937Y<0A zh&71Vv4J;PT_`@n+{GKoa8T;aaJGrH_}sshjTP?I^T!8fVtj^^+n8lxE>Sq_e|c*0 zBfunMrGM}CF=8%i=i+5pURaA@?7$7BA%_b96Vqmg2^YXn!|Y=2{!)>gI(!<2g|Bd= zF#DLNurv&oY3w@Sjcc(HiIQv|x?5l*6Z6vtV9beMRlr ze2yz2i;!XTlfe9=p>!KGi`;D--;Qdz1Z5#lu<-2G!`x>Z`H^W0?Uq~o0@Lsw8brr_ zfts9|&@;_qmbdPNARw1xjmd&$gMYmP!)Q>SVaVp5LpSA(YLFnWMFk%OHv3l*f*OaA z)M--tyYTvxEr;`7e*Dv68bfI#v6nTUmd* zlMKJ}NsoDsIggGYx>}^XfbI?dzuEZ7>iYV2>feJ@toGp3(6_b4OUh-`;xf)No))j{;Kq^Pn!Kit#lM%~32^C~9&LBVYJozJ!e58kY}dQ-q2zm!Ldr zSuY*I_%tdGEJnyCj;3xcv=75kZ@Pj|rz54MP%3%&$&uYbXJ_Vld%Yi%e3K>rjNc83 zsn~{N#tQpMb0?>5zFD#`VwvX*^2dvEl21%bq?E|cUeMm_+%IRlh>Hy>fJ-BKmXw~r zsDA)}H-6j(o7Vm7CIo}n(eIfylW+$B1SzB~OU~^Vlx~y4kd^ zIcCi{R?jKn5wS64Wj+2dNCc9u8VcER2TMhoZm%Is(k9oP=DW`p$q(h}U<^qI`JeZ3 ziE%_Lq%&q>Ps%YdBKD?of-AE9UUZpTjq3`k`DHL zDetABw2|ZKd$P$F|wRwSH(|t!q7kMK!kB}dpHsVch>v^ zJ94t5TL&VZ(e{2{KB_I7m~C5o-OG?}X7y3P<{qbJWvyD;G{uLlk;xzT&!A~AcomDT zH3khn$!-%KuWH=Z2?OqKZ*K!!KfWwf9vw}5z1jU|h0hZdQkF>O&ylyQtLx&<(_q<* zD7w&DZL%KoDDAw~rlt$P=Hw$=LRGMxEIBS-H=@oRFnCPQzRwKIS9f#KV}uU69)Kq` z&Eeox>^%_yZ-W#Dw>O#TC56M-n&YMPmn_M$^gfYU58o<<GRp;OYz=Cl8}_DgB36 z$rFO53ch{}16&>8PFsf^)^U3kSHL6<_1 zlw3gZl3pbx1VKc)L|UXBpVlyBNP$)R@U4$~IAq|lU~~E{CP9dAeLAIS5i;PMT&e zZGwm=srDw_zs!%!dlnp`_vOe2gM6w>JN*RW^1D?*;h|yvo!>vWu!99m+7v@jMs~HQ zA37&8FAr$k>SyhCXf&?LQ5z+`SEwj|TEsB7uA>ju(XCJ~9j{nMiCB|@Qpq5n@?D1q zM0sejKqhcIET*RArO0(NSp4P-c)3Xz6T}@!8aje!V&b_^pK0ep|6RiX+Jn}YB6|7s zk2z8@|KT5hp<%(V`@GDTJ_ooen%bLy3;o=eNGq85SKeeWr7sh?Je%FBP^J~8r=lnU z8Q_-_Kue-S8KC_z`hXq6LU)kO1Tu!Y`hl6{USSZ|VFN}=n-I_vm<`cecX^N02e6?IQQu$o;1;1>45V3#LV!eOlcLwY@*VA(NATqGC&u4o zvjWi6r1u~a0~E;m;bwo@zn;Gw?)_O=*?^3iJbAR@1U2a8^MG~#r_neZdJ`|f^BWOW zs$j!v3Y1HD{6!xo&l|a~pY_GOPVZvo3_~z&}4D-jF=d?K*0 z3&t&<^cqxKmp!d?(Nd>g&!{}STXy?}2dJq-c8Ho)+n zj$8vDRKwt+GvdcZEXmw0Qh&>kj0yT>AxoG?&qO+T*qLGRp(jSZut24{;^_?hprbnO zvGJ@(!Y^3TLcbgaCmr>mWk_J?-(10aZ~|8Knt|&T`sGaIgVK%>7=N*sM)?$ear@T> z*Cm3}DvC*H+iaYJ$i&pN#OlB(<7bhFlaF~KmYv;DlOjJqAF!(%Aj805a$_AjGb~ z=4)f4y*Rg276QoT;gt6FPBqHY&=3$0zgzs>6HN^Jt`j1gC`fuq|^P)^5`s`77px;Q*??7aMULs?rWLYRY6{|4>6fVGPY4< zxb-nPGD|U;Mpq;lR8=Af@HZ%z${3^UVd+^#IK63InE;|sfhIsHQi~;dK+%aazzhyGH?=yMOPQ`_~%DWgjZV+uD&`sHbebN0J-Gsqs4_N>of~5qo zCHte3M+PQm@mEi@Jkk%g4c%KXmH8Fyy+Pg2D}F{(>2t!PF##$_3LQu-0bOIee)AN| zEJ$Q&ByqYQH0$cr7W{^;k;{rv7FXpZFBit~9g+ZR) z>x$Ir%gL+zM??Vxji5=c*YB9P4t`Bgd_aN}5iIvzmMI!+H(RK`abfBB-Gwb2DxkygLO=6ifNTL0sWbs}0<`pr9ag+lkk`o$JzH zI{7stG1L6>^v6^54O{mAX_Pww^aAArjdof=0|H51oCySF<*ay9Laa|#CkUh93fsje zzo!I=`QYt~yMMU@vTk92!)|a6bzV*$zau3nQ#3SnE}Q!8z2jH%7~akehSBLHmrkq) zeo2NjpMjB7#X#)?-Q4bh{cWB5qe3HQ>zZGLnB};DDmt(tWtV|Te?T8(#^FG`IMC~n zbi28I0okVTv~rK0(!}{?L#RdG2i@XDmD6V?4_O0rWK}Qb2xivfCy%!)nRCySm6bs# zH_|*Fd+!IA;*L(;uDW_L&=q!QZZruz53@vx@~7g%TSe~4KyWz=nF!Hc#jW2IQH=Cu z35J4McVY1~>;wt110%rS4GjsV8-F@96&dY;Ycd6V_wkf3eBb^MX_1_vmi66Qmh4>o zfAWxFQB9r8IeVQu#HteN#;`JW@6r3|br#EtOZ z?sD@(Ll7dVQfn)HR>c>OearY3@wx+Tad1nEnCE(kE>i2sWRVp$?6c)4q(C^98$2v) zbld);3G8L1L-uJ36AFTq0*S$eGo>F6jAwGmz}DW;(H(BaD8D@M{*aMBdk##TeBopi z4S@x$J*wG@m%;De-p_bXlx_>)_zhc`%-rme4^-y~5mo8h>3H>YhC8l~(FbB_*-8#0m!} zeL%|#41E$Hzo1iILCXPB58$tF!L7$AFpl32YQ7|$NO9|*^OCis%^1_Nh0X2uS9w_j zwvGs%a5o9)?h0}D@23k6iYPQ=o1(V%sufZH~fy>9JnQQZbXjLh77#?}Q+ z`KT9rA(S-mfMDXpGf|is1&!P@F?|s04G}^n{X;4?F+pYP**-V+QFibyOv6C$yK37! z#gCEW;t`in6+Uoj`3yVTrV?BL$rM*Y!RvMNd9{sU5P5#_?YUX7R|L5SprM2E0db0V z5s4#x;gE1;+YobDc%PLQD$)qnXu;^nKS&T1rn0l}^2JEQ5P@g$v$?J(F?)H^org<5 z9G5DgFlj~NZ_=Yop#|-K;dKhc$HIm>ZADp4b zLB#=KF-#&N`5-Q9=^k{RUzfI`cuCV74o56)Z0rxVt-cKp<9olFn=?-JI(Eq$`mHX! zz(GV!5&+^DsKbP}Hih~VcQW!c2m$S+q2@t|nd zW@iX2i~8PN7f;d^GCebWMm!eQekd!$fK>*IRM}bSv+}W^kpZA9#NsY&iLX6+HT$a? zCSum7pQqHlV(Fv~z&F>qF8eNb5Fe)UrM@I%*rm!MMis#JF46drxq=f})cm0KC1Yjd zLW9z8wi*`q;C-2dPcz(>Z~8)xm2diNqykY#N&2RI$8>GK7HqxKin3x`+JXFX7K?3 zVrC$)*Mmd3-@SxcN4}!2g||*7TYt87MD!v+Zc_OV@&qU*vL-;^w9zaR2ApD#>PPmJ z4gR&OfWpCc$yq$Wv4MPoGzlhzmJa8Pq%%~xsyWoBfrD5u)s~YNB0U{t$OgoKUr1>7 ze~1lqHBg4PX355}#K>sG;< zT#UL-)6S8kXKFU99eT6qWWVU}a6#mxYxpMeoN%RepO6LBAQejME$lbxhpm&g{K0F} zD3QLk2m`CrV{yI({h}Z7nc{vblThp?3O1s-zl=1(TspvbhC3 zOym<)iANHQZ`Os@J%n`=GSV5OUGhY!a$2R^mDXMt+=`Zi9rEI^Ow}s<`*K*&U}#3} zoKhgOMV+U0q!ZoC-EkjYo%lCr>|^TQOnvw{)vlJ7GC{>I^?@9vAkS~~07o1_FL|Jf zc}z|%ss7lAnlJRyOz3O+tF3gsP`-|zsVIERPQ^oTfzUCjEwT4uuStc05)Z7&BU(eC zkf{iHphr}*RM_mMTk)Zw&jr2Vf8B~N4zQ>^2K^;llRT+NWcmBDKT$j9=s0JRc0@Y8 z5(f&lc2c#XiOzoeaVL?@%e=`uy8~ndU-Xy!0&d4eIPE(t9VSfPPGHMnCQMFZec_t7 ztI>8|GDPN{PRH-2z@v1LtzP{IPg&}Q%!K}bR<*||VmR=5P+iCWI5bH8eHTpWX3d3+4)Dcs4wDv@^M|dUCufJs6#p#pt;| zn6|OMROB_Mb~)`mOV`c*8Ku&;ojEj8ktqgThYICpyr|P!&z~h7h4O|;(fl`N#$f_bWU24REJf&ESokL7#WRqpv z^jF`OPW%Ant0Y&&eKK+xBu?PVc&{y&SS6ims3;(gV(!;b*r6}Ba5Qi{9N{1eZkvv) z2uJeB`KbWK*bTMlFz=>_`L1213EmA)`&OMv=3>YByl?B5PxPp4-JCl7Mz}ilWxFJ` zom!=3;@^V;VTDi9@@2idCq`amv$o~>yl8jU^snfT?gQezombIx3 zONgAT`dYJZZ(n>NO{57ma~^Mh$_HDJhd=CX|2m7a<~wyVn)G9)E3inE$W_UB%%Z1B z5o@eJx~)a+Ohc5RHkqdn*P7WDDm{iEbo@?1!sV`NeNX_QPIA&H0*M6w6o}N zFImJZ6~WW~l=X9Qb8og%;!75}eO-zOrf&FX3pkGG?qKwLYP&?Rn)mmV^Fj~MOFE3b z`o>~ZDPoedP&PWCeEPEk(I_V?G^9P5Tt6yMh-B9}Ee<<47^;t4xA{DSw> zl&A*L7ec+`iOh)4t;l?)!^4;pl!k5unjc+PSd6$i;qiS+oqn}sgnAI|?~;CQt+Ukc z=ygBMPNGBIW%f{jMY5h-=sNf7p<2gm!oUA^ePkD$+ZBDAm_5|I_KZ^UG&tr^<{yrX zNGp7`l#>^wC_jxrJ7ZTPzTNIN@bpZ@c0-p0Ek5Yl8#)aSG6x)K}3!#zO$r>t=84VfWgTwH&1&4}%NP?8Ky?49&KJP0vwm_wVDt zgNrvSTfPkxR9p8=lks%q|^6UTu{RNN#{EkhT` zz=B_+th!ER2DGNo*Y7GlvBRgLj49v3T zc4CKUQxjO9l=5KZ=kzUFTPnKMoaF4G%#{QSpXCWE`L{~OL3x?n+PngHr)*tIKW!Mj zo(npQr%Zn8O6|Qy40=+43~ZtF1z9e1Jn$3&S(|&Y=lty01zOdP+1c5h(z~Z5)TK#c z@mQ8bwoTQfTOoHd_*)1^iAX3NA4!85%z3+qIN}kSSwgDkcs}0ii=%9!$uPh|$D?h% z@^^3ChosUe05=MbI@s>3WZ-UV`TV`6M1Ke@1b@U}v-8H7L%k%PEkPIHv|Dd*pt zE3`x-zr%wfiKM+*I;kY_b>cZxc{Hq1Rs8DnJt}X5v*xULPYVsVl^{Y2G)~{#JgB!< ze!^|_K6G?|&%UU3cNaTW;+wM?%~s1#U)sM)zsoll626%aHl`dzNDhZ8aR&a)99=Fg zF8&mAOP$|ipC{WKVD!|5GcK|O@`J51q)5P{i8FC2Y2ugr<#L0T{r!@}zvHjAS!N;5 zajcEd>_VkdqZ3He`rTiq^9k@ds#-Y)7NAICkTs%46M1;Ev_+7?OBEAg=;Q*{BioWo z3!ssw*l$*`%!G0%A{9-8vYGY1rvX|4ZD$!}4fBj(xcHiS>~pLV=<9DhUEeo0z?~XBu|sD0Co?uTtmslK(FhX_epKouvX9{XBC|0>p0jy zR26ka%Mp)uOl~fAGTe&7o5rNc!5PwK&k!Mj>kH5I?@d^n42OQX{zjCzPY@Cy?0KNj z7#d)ua=GFY&w)gJ-=g`SKVO}e(!YoDef6cvMvS1smeo>_iEPz98q!_=VNt|+_wJUY zkiluYk$PdvyH8P_>7S}eG$ecp(NN6mY$=R%JHO1xa)it07XvDL+<1hZV3_{ZyMK!;knV#Q4|3l{2vbac1kQLXdKgA!}9Vn z;t;!)wSo?=OEb}hlrL8GI>zg-IBf?ASyo#*)Xngcn8&Iw6&X}6G>!;n>s-5S4depv|d#+ zF$aR^gN)xhne<;WDQuCTD2iAwD?G{geShfDyRcd%JJJA$K%IjL!IAB~W$aD_o-O4t}V}Eb23x#4|8?q!@ z@*{&710%njVc3vYkWM-=ot4Wox|B;*ufgC(_uh19I=wx|ngG=WqC1a&FQ*2$!Jslv~KI0R^8F|=$}7-KCkY;1WdFHBY|HtWjkjB z=u71k6_v3RX}g~qBfPKcnVJTam)}L^VN20O`s)MoE9kxX<%57xp!q_F_pij6UL`3Ds?S1)tKT@@-R+8^0Q>F2E|gVFkoS5} zK;NULcV_c(mqe4-`dUb=_>8<@k8doCsAQ2!>P z0{HeH^FK`D(&<5J2s-y$-B;_}*%Z}257p&Nt4rU%cRY?2i($+TzQCULF_Vr-^kcd9 z_P%iCj{k#+r7QSMXrIz7yF4Y()<8m{v5MFVcjK4lUh}M)Ns8q{-pgA>pHkw1d zo4%Sy#l=4VclNOla6r`Z?O> z&XzQC20YEw)YNv1PPQ6_?Pw%Rq!5ei%f9_LWnr=a=8;51_{0-U#ssOZJaVa(`)ASi z3NEGo22^_J#&3jID{AOgg)BEk3YSay*6?SdT}4f^EjLU}ncxnDoh%($y>vAMQuCXt z!d>({8-;0ha9!m$eVdWv1`;-U#1l5vF{nodIhWW$2&1ZGaO8-shN6eW;3g4uUrc?cTpae1==kG5x z?^D^C+I*__xdOK}oF6cllkd4IM&pxkhFDiDjT7!k32yZ&zRMWaB5-UpswCK%?hgO~;P6X>aU%%3d} zwWLg;CIg!eli{0kV1U=Qbnxe|`;+iU7L7yNo2mDqqty3rP~#x; zWExU|9wjvx%cpG zI8Esf^g3J@zKR1VB9)b)cBm_UJk^$UjdDRI8cVDlBT~}n-lOa20KXoPu&+kK6V$dA zPEw=9)#ZGjsjilwHo8`7}lU%dJ4=^f9%2L^bmZ$V^3SZ3_$S<&>OMO10S1*tGZ zofC!Znwq=12u|i24=J~Ib{fAdf&9tbeDUIl;Fk(37{&pX5}8Gqvfa%p0;D5Z&}k*Y z)&Z$0NkwBtBTfDu1%Bhu^ZHrlwd>B&aj{36Pf-bqiOlPwvBtr!ba(kxxf1Rq$i-}s zA791u$tzH<;F-o4*DsfjtbhL#zPWvAoKx^VJ-ZydrMq-7>Us>%gthj$X6jsBVIBnY_T2mSG$)2*u?;neXkHok96a+EBpITW^y@ zF=E*&`k<1qCr_fJ$a87-qbZ&wg*_90b~akp|r*vZ&jw)M!Dgz;MxF;mFi zB=r4Unmk}OlgO6&KRqyitTDBDqUZ0uZglxa6dsbN0o4u8x&@T=$Re2)AArggZidqs zU6wDK8A6UPewK~yxO_(2Io5ZdI4!-Ze@*)7S)0LJae3>>b|$rhYWtR9bbs{))`V+N z<}k@Jg=N49spZT2W7o4q!l6YvYKTbhf=9NpLaJ#|yQ}0{M=Tb@s&x&iviG=^VNW7< z{xm-2e7;JFP3E`rKKYw=JjmyQRlE-_z>)y-u0C@Mq)rO-K305vx7eDEV-75oU#=3 zxA2pdI~VFUi(3lD&(N~pvp)Lyi$Gj?=1}e}JY5J+^OE+)D&oQ`A@Ro4(utfKhTlf% zhIwlbyHn0p)EYj~%eJ(CS<@lQYys`jh2O(+YP({R`3y`17aVcd<+;b8;n?_wvapJq z=lH}4CO)EXyTPnAPYNX;mXT|hVPY>(@S-sxhq115LiFrC9VsvL=DWp>cZ*LrRAQMd z!MbN};90^ZHzS1C*zC(ptfJ9y@9Y`{rlIHxu!0AbaY9O{E}A|mXX5{brgF+DRu zVKEY~m7_=qRcUaK!D3w%dJtHYxbYY7(Z(svkkh#-R5N}D1XYkhR}WxK!&=Am|ClkP zelVoVJMBvq(GP%rCkfaa!@cUl1p1 zzv3ZcFpI-z`Wt>g$;VU&x5Ixn1TI-qBN>DAM8 zUGdy2_je6KQ?quqvUZ4usv@+=QIhUO{UH#gnZz<>XFh>7TcFg#E$?SYQtKb%2Mf zZ{#2f$_V5SmySI2hf!k~p$53=s?7<@TLuKJ7o6xEclSmv_-6}kpFIUSRUB0HD-2-Pl9uoNvOjE(ln@13L6xZ7 z5;N0~*pr8pW`Wm*e&n}F{vMrx*Y&N`IR_gc^;r!V_)5Z<;QU&UBZNAw*4nh2^k9kx>pKi)_-_83r%#cqQ)?iM@$)Ga*KlbIWIFaI`%h->{VVz+&jHI(3huo zQRR6TcDa=JRk5B{3=bS@wS|v&4G|-TRnHwkFmtiX2T9c5ul^Y6p8kH|&*|6A8fyU4 z!qI#6{P=MV`Z++VA?`$C^ff6?)*{B-{0P`nKH9x# z7NAS>QpHm?R;X}ZcJQgm+?;iu_ z+Ql4n5$5Gj;WL-i)`l#&;PPqcjP>PDIQf-@3U=y%0gQG0j!2yw z$MpQmwDOiChcU1Zip+8`J^@Yzitgm3JEa;<@z`rBv?-Cf9xDW|d3>7q5l=S7N)qVo zlc3)efBQl3UVqw$wK&Rb{`;DrK1VBNak(dLRN&h}QP}ht>Kc|#K>q&L1wyAvweJ$y ziYFr%Cr1P}jATZv>_$)!!;#}w7Ke&pWreu9Ok{R@<;@De(J$QC58BdU(yEFP(+YzZ zb`?D!2x88o*zyi9SO_A<6Pp99`3Sd1ezdFWo9`X=!Wcso<2jMO1+Q;U>29A4`o*bgAt1wCixDnUE#)*-|LZz^ z<-l188EPmgX-W}v0-am>&~sWzs^-iC$^C|Y@VrQK7(V@3m3uNz0;58&E37piLZKod z+G;&%TFYmWxeO8R`%0KO$!2O#Fcv-?_&Y+(sJIK$E$%}mP!(ywjPfhiCR!}rU=ezn+{jLDZgG$U^YcYbwhpWAy;btp zPv#;qR3nvu;&*+*H9+v-rV%Usm~Q+nd*j?LLS{A<%Ofotyme`uG$B%8Ljr5Yz?3fh z3Co=gj8AaEf>Or|9X)-sF1s{o#5XS}F;ltxu7b+#Sf&kz@SqIQkuTr>gqnw)b)5;k zSa1b0;tq*Qjm{$+p{6;CU#Tqj``)BUh8c%8@0qnsU;WEv-;UQj?Ek1MKJL6gI%oN1Q#-9Na16;@vs!VYy8Bvb630pa_z+Ly$` zj(eLTC-!}5BFMAmB$|;8xzaVq1(J;jhxdx}EcNf6-9d!F954%;GdKl)5`R`#y_T1E zT<;$*rX2_EOt?iT=Cbn0iz#h* z_nDM+CfPJ}3a-|*gwgQ9l<804yai&7VL%Isk4-)o54#%=31Q6YFyLg3{(sx5zD63u z6$YNr(cz1d)a8XM7ya|VzM+~!Yd&TUAK)*}a+_>MpMrcj3dZAc8PJ5SPZlkIrt@>( zPzcHG1R7afS&V20sT=6Mft3X~zwYGt06}OD_lFxn5CsZQe|H^5(N8$RbLjgv_%AY^bLy69CfaIz9O+TNofjU?)NOE93~6qEi^+mQr>Z?nYqdbb@G6hYnId=g@8Qnx!D0*J_4*Lv`x0x?(F$SmUZ;OIy`$fK#L zIR%HZ#V%Pfivtme*4zPE6#)78gFKBp_>@RgvBgMpNAhF}T!H`sz#hiD-89Zb25cCI zzOiVsb@BcCKup;Pb6@e31JRa!6sn#>KS#u*#*|CxGpp7;JZ1Oc(?nUsd`z*l<%%4s zv1IzifA+*9tveQou6310(%oMbt+0EAA-3dMF_u8kJaQ=5NHcHcyhRo`e2xF zbRgt70!Qd~lap=ZVJoemOg6MM(Q*lqxuDiCkLBFB?W)sZIbx7*VxOl3oZ=uF z4>|!c5rbd2fKdhML97k)i|=aFqC*B1$&}^Mq*1iN*{(uU=^bD6yNQ%Kk)loLK1LdN zS`HBr#d;mdSTFu{T$qGU+TkoSxNTv(Atuo^P8Qwl=7L-bqdoxDgCcY;PIsPJWDlQN zF}2kWZKMek9>Cy_jE>F#PjBkskpi|ecI(sP*lq?QvBX{`p!h(S@3~!;w*Ezy-ERNy z;!MV=96%@RWY@ej z<2&w|8tu$M1o%?)?d7*2^bPx*M$kfStUL{v=h&4iX_dguoYa@d;)p?Ev&k*4)-%ml zYI7RG@3#&--H&&i8YH4EgvP;gy%-y1gV@0mD0Q{WD=1t)VI)H%sw+iTh z8-e^c8Uz;@v>=fCUs%_)hA`(Rr}kW>blBP%Rv-gcR(~dCdm>2c5V+Fm2hez+g<*(3 zkdHg)1&3gEUpSj^Oo!$#iN%!sJIQPhbO5yZE=z9`!?IW>HK+Yfj~M=Fh33)-z>NtZ!oPfh34_6dCX4+)#z?Y)c+)izZnl-QS1c zE(bgTxAA6`%;dMt&Rr3-?Y@#XB@=*sa8^(GgYNO8D@(WIpf|{Vd~24Kcn}=PTs=4U zRa&OLK02cu$N3eP^rt_H{0XR6svAaTe8T7ege=8!YI4D(;9QqM45V5>t`Zq;dXZk} zlw6QM0}<=t_&knz=~UWpNt>H>PH#FxjEvtxnpY~20@f3CK{Nay{R$KaWVdtJ6peE9Qawq>;@Z|D*yudr!0W!RCy=kCf|w2T47#i6s#7B0_8I=(W}#-PlRG38XWN)LUc8@9moGQ-(Q?bHGbl>wB6sIbl=wZ38;M z)`0tzjPG=t&A!Jw6T?2-yCExQ~rH2 zL>&I^JWaHXFB(BQqa}X;K~M1%%kA%foE_=|C-@PVGYIJmr$KxS?kJjhg8c0ZtBUGI zY!wQfU!W-s@VKj-*z)Q~|5N=D*j@QU?k=6!Cef0d%@^jaF+}ACq<6tcC&(kKL4T3g z(Pflcr}qI&lqaJe{C9SupO<$5eTs<7tH2e_rtyduhRz_xB2UFyYg{ep%hA_Ba5)Dj!g}LBLW%`9a~nvT`RWDG3H48z*xnSFef>tb3CW zJ4z3tw1-&{y{uzhTIRz`r$E^t&$>t2l&usPB{+U;SS^FB*xr5tzzh(@DIN)h8$#;c z-vVnE0WzqGj{g;xZurMIbH>gvzu#?1du1hp3Xc;8BB3G$H`xV~U=&dd#AzDZ2o~w} zX5WI9cnDa9g-%EVFzdVdXy*5InYEc{t=e-FRl`OF%hQrB>pDux0z)jeCZ(Sss~x?L zfK`!BLJ&-xcEJ2mA|3-~+kD7LWn4fY3Yq*V8JVG$AG);B!X9?EO(#m{xN4aR& zi^DGTv;mctlqLY2Yhx4R7oJpPX;XWnSO0+ph%|`!srL80A~uA5f-wUu*YvT8Ss)&1QV!RPV=9w=xn2=X2Ie9jn~)5y!e6Zr zT*@)PuSS043yGG9(DA#wGEcl&=K6GmFL~M2cQy=nT@0?Kd15j|8Z>v}7Fs$rM zkKcTb&g?fz4c4P!!xJ+ue7+9Z-$(y}8|w2lKS-h5Ebo^6&%-RL44_hgjF0YAmkYjw zhrx194&?wzzW$x^eQY>~3N=1w=c)5>B`AUb#(*virhE_mY51(yB2IQAzCOQuEeuRC zg-Xq)zGTS)0(RI*fklP1&CL1$S&m3?PcvUFY282W->G!@{(^d!6U50CJjisFmzTAFVhX$SZtrtKS!MXb zES*3j?2t&**;V>6$R>Z#WZ6(}l;jF01MzS`67czHL)hvLJ8PN~0^MKf zUefY7p88F3;6cwPWriX$xy4kJV!lSXz+B=$u6^Xmbjv*wBmE(=Rb^myVZos*Rt87B zp6UCVetlj4Zw996S=*yKTDnh**8)a|QTV-))p{`keA>-~hP;A?6Q1Aa$ID5qFM|ek z+vgSZm$eq!^K48Zj!AH;ftIILt!OM}#Cy>6l_mZfxA9U{F;gIDpIm4HwcO5Gug<3p z`RGqX&i*W}nxD6BxDE7MBAaPWaB{+(7oOP=967y6OZ^!_Six2G{8a7GqC=Ja0tpfl zkqJ+JbjH%X#B4w!G-GMKNIg^8_ea!YF!+es8{oFkL4LO6ZQS4mcm!!jZ{51ZAHG>o zQ2`Uv5MwQh>Y*+Jt;gC*?P@8sv+0i?sH#32Hs+xErx@lMOEeC@@(QmW3je;Qw*M$3 zx9G)${NU{0#le5TX>4HmrO4K1rrMRpqh*_Fm(6)Y8+D{v6jr3l0Xar_Y`5DIt?>6> zCF3+#bEs9dW)c#}*s&_r|7rm7X<)vW{OMuuJQPA8d0h}UvK=#ck@2BSqbwm;Lk<;+ zyBE&W4H*;2KpT-iKvD;SjE?vp%qPxh_t!1H@&i3Z9y<3lfgi$Z8^aPqsK3huFJMrb z*-PSZV7OGd-Wo5u4HPOPpNMV_Xyc8M@OMlRd!MnEBdtxZTgU5Q^# z4OAXKU;uoLf9F*cPYbcZ4{PB-9gEtJm76Ml1%;AsC{XK$Wi&Z-8ZC0j>!(}WXP9uS zm)H``1=;Z9q=EB*=4rk5B5h%z(;M{MSglr&Ln#wj+9w2HKnL{ z;-#jAi{(x`Xp4xB>Mj~5@`CD#^eQDr&7c{9O?Gbr>W&Py zp2Aa(jxo>zjE$jT>KA9%uqN2a(`)mupK)R~0ymuOiJUOo)U98rv>xB@8U-TxkrL`! zSi9X+TjkUTXJ|P#%F>QP!Ck`Zz~&^3RCxE5?$K{`U;RLAHx`R6E-m%WHisc-uixbn zFC2Og<+VvQV%z)(DfwZB%MR_8{rv`!nET{{&GEd=|M0k0z%XvwxKxkRZXQd-ogD1n zfshB{)wXXSGdSE;NjaJMf;Lue5w;wsYGsA%JE)8YOy7O>v+qR0e?{B`RO`vOL@eX) zzL~5ujOG%jN|cQV`W0J_zY&yKQnU6HkXoiV!Ui6bTZ$Q%Ml(CAfk%b+@b(BBHk8b2&6ayCA`VH{W!I<=py_T;=@EJ-> zKnVjQC7+;y-Bi6*WDjS!QnjO8e z14NmKu;zl_Rr0P1e?y`5;luTRCPd_-cb#+kp)&><1}BjL6T!W~Iro1~hD8!#=~yUp z)!P&8?oq5%kBS^qFt}g>uVv&W2I46gje)iX#F|sTdQ`1o?z*d!suWhDkU@PN@s>i zh#*Yl0!n3_|H!``Kv9d&?{3~cYO;gY0h~1<^dW=<)bx~%yLb>UlLhSOtFoFX&3UfD zzA?8LCw`bts%gNaUvoql0d)TIX zhzDIaOK?YNC>W-F{fcVb>$RzJ-2y>z0t_G{vlyWWg^Tw9CBECBh!Rg@{kNl(XmV%W zgC40%4@yGBG1Ab!BWQHAB-6b>wfD$J9e#VNSjq2&2x+!CWfE~D`U)}pO9?4s^n}>a zHUirT#12|B&S<{{EFKVfz;sU%tZZN_!7%RN4a6gNBZFz?WzZasQ3EW2YvTA6@kB+F zq4RgX_51(V0&wvN&_}Tk(Xz#cXLq>2s2XRvCd8+ELz){J(XeHNNbXhoBsuEABW_T+ zYHyz2pr+J_OPdQ?Rk=8MrVvmEJ`Bn?^t}ob319PzX-S`b+bW>TdT+0(0pIib{aaaZd zlFhwCY`NR*X3WVm5cgNozXTb!h~VJQ>1PSvds|N}PbX19fexBxmoHPeoZn+2RGpNSu(#m#lY0&3vH^`EV-kDm3Yw!W*wF6S_TN4f_t)gUvB zqr4&~d;Qf8i)5z#sYjV(e@?_CgU(;QdKDpVef4)^D$~ucort*Yj2Lw$q&gj^P0+1& zB^C0TRf(RX(hGg`1}*)*>I%)l!zR*qiaC7wfp;^rI&Ng$_+V#CJbb~b>tIHEzsKWk z&huAOdatbiJ-2qqK!EqIriM##NI-K>XvbsxH14N%T&T;-1H>E(5CF1o1jAan>*0pr zyvjiYg)mrTBQazMgEP65RWzf&#d2B&s5x2DH5o?mDJGPWwkh6&zD{GQJlfp0qlI+zRgGVy(P-| z5BxtqKKQxAH=f8)_P9bLUl3K`?moRB$%iKOxHt2vI#PfR*omv`HH6XQ2Tp*+Y|~AU(-&p$q}b}w4O&xy7ou=j!aSb;06&EMjYJ6P+@gW z*K;6$=y`-AOhZ5vI!rH18Z0~S$03bBb7=KT5KV#w{{g;}Dak>!HdDxHC@SI7_FTv3p&VBr<9Z ztc_3YKYVwoX>S>uUFbv;*faXUBhHHL#z<8)jONV$=5K%F#!NQIG39_N9^R}y9FaD_;G4{fj|Rn&sCjF7@|%xh!7qsg+|H2_%FG914w|*gNhI*4YkxBcH~! zL_D!ZhwnBP(yx`}-@a4+tkg@p=u<~TwG-ET)X|w?r+qY$&~+Q$H>QoSa=hnxuSw?O zap#5CdHMF@cP=yG-&J3z+^HQCOyT%zXpJUSwwD2c{c0MVh~&D7u&^f8SAGApAH)OVQ06<}KRWxo|}|562Qn zMsh%R;;`g9Y4vjCw_M;f3isH>rKGjLkk`etAQab5eNx{;Ccw4Wb?J!2F-HfCf=zS@O?VtC};-iJ|{Cq`oM%@b28FF#UpIGzLznTsh6 zHCbL>YTCib-p+vsoV*IFnVLHY8|?DdAQH3N;B_3I61FZk7|kCpaPOW0X!XIR0v5ZB zSeBNQh^9a0SM)LCxdw1W2FrAN7!8WRZ__^-z_iIpWQ-S9LBXK%h~<~(<$V#zYrmZO zZ`pjzx@ExvoVv#9$tj-{VXYMWio#Nj3TKzx|0C+X!>RuN|M6D?*(EE9h|H``b`&9- zj2xltne07ENY=6UNJ7YtY-O)Q$V!q;89AqLtbX^W_vd^4y8P9ZtLJ&1$GG2b_uFVs z|8*mvQM+K{=fkgI;G>gNnjz-ePp6)LTQ1EKNTjTsVDzP7-OCY%=6(QFWI+`fPc`rB z>ekGk(-r+1Wq*8M4dzhiN#V6ki?z5AemW_?+^7OwCL!LLEEYlA-&iN7!%-A{`)uB6 z=d_s-YQH^`U{+2b7jpAd8dZaT{zFdDNxSd`o{B0M*@dsv>1JHx#ixa-}6VyzgZ6L3uKI(-;C93hpOUiIek&3*KlpV7#n zR=P!Nfkf3ZiC5I}LAnBYfU9*6*=^8rrsH~!VrIT?{@yxzsvM(Lc|+fxaON57lP_;2 zIz8FZv(%ZK*Z6h$Vb2_)1TE_PQD$Y!&dU~&tekM6=*3Rh)WYlg%N9>R0C}WEALW2r zX;GpAEHD4Y{kP&1Xd(qMX$!LARbLsA(jA z1>2b@u4v*m!p&Hf9TMq$)>Rrt9guPdh8k+Nw77-X9M*Xy(o?c?UJK{BCLO*PbQ)q! zs~xLkoDBFMybr%x3X=x!`saDe5$x6-0_^U{D%q6;RP0GJ;p&SQFA&K9j3%K0tmKzX ztT>A?q*VZ0TE7{tGbc4PG=R$Cm8BHN0opn$YU*cx8FzBDK34KWZbM5_z@H(dgYkcR z59|C0BEYaPhh8!ygy{0aw#lzw&BU3_(yqdC7pKZvhVUxF^kzWU)RPwVmYHS3a`tlF za+dky$G{O89Jvfo9FnMkfFbK2X09sJz&YmqwW9^-8ov?L4xD9x!VX0~%&}t2kB4P9 zE)badr{lq?ea9=XpsQ>Dlu!Jb2lwkFAO1*O6nSFu*12x=-uw4u)dP^7Ztwo$u(`{0 z#+>OYjGXej4bs$7IG8afWxlPS=i+&B>feEDJj$G@)4?;}uYAP1>L#1oy`QB6W|MPH zKU^irK>1ghC;V`rD<)vnK!ubWE&0p+W#B)*2>qdj!0$4P13hO1)oR^Qtr(gBw@=H0 zS@t6LQG+84$M=}6?@GhoV3)9dF#tAnyfLu}F>Fk@bD9rQw`1=sVglfu`Z zZ`z9Lk6>&S58=Vl%13D&$cun{Tk{#x13PYRKft zW~PAudTw9~a%$elQlvffwa#i!n6_9SCQR7W)2h+^J$J~$5sPuOp;!BEpi?{HCc&gzHdDDTy+#i8ppaD!tbO>`0@1+Gt3ZD^FE?UI)9!t)&B3qT z!xK4#psaISIDeIA-TCO-*Pm)$*2PLax`h*x5s3#9)%V;D3cr3a${aqGe;g(@^8`#% z(Dd;=!TUR#V5=5ts>M$)td1vFJ{1TTeMN0;m~EQo?%S5;Ew6q^S%4}5d{I0+Jz=TN zEi8bl5*bSTn-4sozAT#!z8dA_<+p(Unx&w>3Ao!%;J+QXdSOUkZMc50iFq9sAAcTx zn>6S@bP|wv2l0p*1Gv!wSr{0WNZSaAz)|aRagj>y(2p0g2W;phXQ(KFw&Z@a+gI6- zxbA@WONo9&@%^$&0P%9Pyeg@WRn5Qm^COQ?3l_A$k2JDBY`(7xlw5kXspE`F;u9?^ z(I?Crh9uLgqR0qKhvSp;z{{yl!v+i@M}52ytXyVe7$k^L^Ivx!y4bwagO+53LJ2Cehj6aRx{{$fh4fqXQM>tlc!^}JB zjnI9RHr*;_K;WPrfFOI%A?nTPv@p0b9`g^814vQT$ zGh6Hm=-!d*OMUc2zyzTEp+G7|^@5p7T6F3>(3>`Xw%c|t>az(N?uIIp3q_>r;|3_e z^OPYHpWP8xILw`bA}8i*d%A&obG_$0!vMV*u}OcA(4!oOAzr40$Cs>=?0&n48x2JN zTVpiR$-RAKq&gD0E&juTv0OK^@?K`;I9{CDi=rtd_TT>Uk1g(7NgwagE!AIzdM)#3 z!eY33mCZd*cO&_Z%seUByjyh@ysyt9{*JqlTrl{51^^N=GD`rF9xREqt)h$!qg1Rk9Nkghdw zPxBwC@i=T4>D6Jpv}7h#+w9qyotN{w2b-F4rw8$%9b9UomR;E0O%b91ry}EV&F90iDcE3t?9x`(tf&Pe8 z+69(T(C;!%v|@sVpNI5PV1P#}*S@?X3)PmD`USyjiC(THozSz* zpcM0NMl>8wa%Z`!2>zrOz~LXy8J}2$4XJN4Jlrv!i7qv>BMVJ>l4Tcua7!#SP%UZk zB{eK4>YY8L(i)qMX?r%4Rh@_f?(B~3MYJA=kP%#?1?)EUPjxhLfuc zL~`>&Rt-{%HY@m{=Agn}-ONVU1-UlUrmU80Nx-ywExUgKaRmd%^4e10 zBVkAuIrg!EVO{y-i!(bAlnY8Ih%+-cf1YgO^Ae`dM_{%9q#Gln_8or!p`oCLZ(j+r_oqKT#W6PL(H=EC({5=WpimZJQw;>b57e94-rcTh6MLj=vXxj~dNome& zUYaugWaaCinTXz6XRSK6*_FEahYr>vC_91RieS{48e7tsdxemT5GWE|G3nq!z*(N7 zL;yeO7;X*H_=Fq=;onz6P1FLJhA+He#^6%+o;P;|%;btiG45~i6Km$_i9nsIpqgv; z3)(|^6?!PH331BB7IRFz;rut_{yY^|emR@~b^>n9P7@$6(jcsPbvGd;HY6)NvLz#5 zY%~w3v#Zi~h5q@+z2aLPW85hiK~Y(R7-?R0b9Q?uY{$dro6LaqMS6P&Zb*-|T0;jFH%{ zsxNv~{Nwe2nJ6fw@-`UZvnD}IntF0uvywHlC{6m z8`&B-b3VjkC@xR?J#J^|o!4Cs#3|CRrLMFO2nqG2kXedOI^maNJJ(lA2PXw%XVuMI zZ}9wRIr`OtjJR5N4)OSXbCi0nE-!FXyz5VX#031g0@f8_NVe~O$T`NE$9OR5MK}%* z0ir&uaLzUjNR5^J*#$QyYNB2rY|Y(<2xYy*(NR4BIuMXfbh^!j*5(p4XdIMVI%>!SD3TeyQj;xeETuJBX#i8 zghW;a=+qs(o8cdfhjcNZ^{KmjbP}Z3A%P-GlR>% z&7D9RVNx?gh4}h6vctgMc>}}A2W>Z0jO^;+B}c!IX_;Qb9eH4&!ij$2{j7iJ^YWe} zh7i{=N??bb?rq+rlKbe@1I!YmrE(hGcLP)e@%`n9V9;T4F;n~1NCN5@jUVD#Ck;Sx zhT)fQ&yd4TjLg3aLTv01y2t=3ZX*YJsLyTbDJb*;Il7JsSiXsUmPiI0jN@2#U6+@Z zvIvfWz+cvS^jipUV0iDC05xq|qwzzzpRLa&`EHf-|4I#%KAr+3b70TR1?N5+M2alO zon5CVAA5u@Yv}0Pi6*BJ0md-Dm?we_dbUp~x{R=R$?|(cLi=Vfw~(-Uo!imu0xV_$ zF0=~B<0VoHO1Z2d!Kb7)99duNYFXN?*ak)+LE@H73s?AlmVPoCdZUM8kqprSDjF$} z*Y2yE7;v9!=b?iv(7lX-&cuM)aW3WSS8oR~7*^G3A^7jxI^so=$UWZ)bM@g!07wDx^R}+)YZ?&_ zNB}mepkShxdtPfq1m}|K)tXYj%{V%lO$RnP9h^2Xyxi~g-#(7fexF$#$_!?v@&~lI z!ac;EkIux9KW9)eT-wh1C3#V+cbfdrFcdNRLl(uETwZHCH#Mv~q~T=Nj$6#V=)=ON zCzY*(uCbxR;Q1x05;Da4lD`5be}9rbm{fSZq@{2_1 z^W)LH%s;g!=ExGu@tOAJXqtM}BP~ygN(z~;te-O%)ES|btw3NC*?KK0!Q=8Sm}@Zy z@#2|n4IyU?-3oczT2|Fl+1Pl{-*v3m(A3OWn*O!#j~R?s{3gTV498%=%)HZ&L9-RS z`Vd9TiSaKff@mWLaic6gr@BN>>*RNS=71G*kXqn&FotnfUfQktzO4-pjNyxO_WuAd zc(8J_x4n(X4W;FuezAZtdzcHFjH8MWX=okOr4jEe;UgwjJx9D%Tk%}&jek4 z12*iU{xd#TqO5ej^6yTumz0l(;(jEDv)sW|H;7>=xalGyWf-b!BD(M^(O{^zw(S+z zFbKC3aX@fsH~fE4gKq8FB}Rp+Zk?OiHv}tqAn;qvrSXpZen+_aS}ZDsFSW5*pRF-@ zICF`J9K=~~;k^0Ok>B?jQ`bB)Z^IaK0vpr=2*?ekKo0B`>lRhp@JKrZeBeMQiJe># zflg~cd|_!QhNBNymfulOk>)wr-wk_njN5Gat{8{G!9g9kE1%`NQAcw04Md4iW@nC% zzfc%B#M26@4UC-E=(>HhIsbG-`EpVSY~3Q?dlfLFlou${=N%2tKm>U`rvW{x%(`d!>!90)sq$qa!DLNOP!nl zbIF;loQ8h3MoQ=F{8MPNS~fu>51CvA`||qsqI@^f7sE3#5n7gCp2Xa8$!ZkZf6?kt zrJVFxH+8f~{gW0xmRofZzr4Ggz29Ve=LwC-pE>lbW-nZ5owGuLrGr`j{!Zdsgd_e) z^JyXU%>Vr~Pk~TIiWC6&xnjb&AO zC`qqVT_7PajeB^ico5RYoyPc3SAUTExQ2c5ma%g(M|WcV}qmxKg&<8$?jsH=7t!9 z>MmE^+|w5u>Zt=SKJ5*FWKt!N&L4v%uG4{ z=E|VussG0Xuz9)LA_)ClQfc|UszPz5n$q%_9hgey!z*qcvHoyfL5DM`SQK*pf0fXr0!PQeVEQ2%8rcbBxCt7pgKTwX%AvO5&Q zRL{G-g)x)~m|F{D?Wo`b1AD&yME)fe!rCts_Do#DfcTG%o)oDrg8e5z-N6b^fmB}B z>PN>ni3{xfa{X1Jx2*M;!tAKbMeXDnm zKvEU!>s26FSd05|AMR9BmXdb6g~%+x5hW$pU`<N1~aXW`k<~H(~gon`Zf5=>#0u zGmajFM-5#b)?~Lv$DjoA_Alv$&LK_VylS8{k2(#(-*s}VcGzvJEqK2w>L12qp99^= z5hjsAcjZ>I=yfuhZ66lawLkFsW^EXSD9vfX?-x}&%&S$wx>?`v(y>&)@Cz#DjiWRV?m3Q7)*N^(KyGS z8ys)MqEH7gfv#v|Ec;$9NOM%d5G)WOCmUY9Iy%Rgh2LPr{#$sfwj&vhD81z%(N@lvK(Xvl!kH zQr~AdZCc`}Q{nEWx8!`Oj^o)?RWP%NpiEB7b@eX2<;gsp8k`Y%QX^ERtL?!33TxZB z>wnlithTZNdN|k%SeCZUk?_UZ&{G;JoY&D2x^eCb^jW_jL&V8uBc3X7$1|`?GXQrU zB=PPLVm4}*gA9i=7z8r7_U;1TGP5PmRwXT8tw2XZQaay*!W7T06V(!7Qp-%)lzHxUqV+ICESYew_`TM$e54(fKv0?FU zqFsV!4c8aHuZnNJkwH!l8;rb#vRo{Ot^!n)0$W8TK(qix0`wvdC}UA3ftpg8nxDI} z9s=5NGZdOZDI?k+|4ttfrati-K1{-$Ir91RC_ps#cT_d6{f!qPF5i`p-jpF@GvIdM zt^l`#ko`du0bNaJy1rPzfXtrH@@PH+4?2UirI&MXSea<4IjEnO^07RsD&#j~aJmLAk`08!B z1dA`snE=&P#V}0s%{)?@e{R5yt|ETr*S-MnkcvbGMgcn?eA|_K?ThKY8H8%!;}^OuRm07`=o?guqG^6sS#)%C#yD zxYPTSufco&=}=W|QAn#!*`+l&|CjXx2lno8WXVAA{yM2=G(Keyz)(PQu2X?Aeq|oq z9Cd&wArRumXWleG;)(F9(faTk@unZ2h)Ok9O)dhqbcB+Noh1<1W$O%$zloBk$7)|QXcVi9$H^?1O%&=6E29@>SAoz-cc*d`% z{e~{|#ARjrD;l_kkhV$tKIj`P7(cU+>}t2Jas!i(Y7g$0Z5}ieHb}YHrd+EA_aRPU zbV#_GE87RrFdW#1zjgv~Gl%jV;wA67`8{vi5u~P&0RZz*yK1J=UuIL=3}p0vh;(LT z-9th`B4=tH{PI)M)8|2@40F~fMB-&uvHM>0m}wncw)*Y@crPAs&u*-y?9}+eG5dnr zbN^VJT=#|d_nu+m2LUl?#omS=OvKS0X?^-)P$t!%sD@IoV(N^1zI!ka&`%GIGfb8U zK#o3(pWF&V^(kTW9Wwh$z>Q|>&PE-Z;E%F8Vg=PUEjqINP=>|a3;QEs4PEBI0}|3G zlq7VISbe$`Ynj0HUKAPo`^iGpC6|_u@Z14i2Lm3M<^{B9LI!x{JA8Xcl|oRJKy?)= zj4Ux0RuTTw;^u)5dO5%Ws0{!ZQ z17>pFn|b@@F#k*#N*BV8-!>4}l!`nQL6;i58x%bl|2079%KD=I8U0#Lyn&R(=bXIY zTznf__8RrBSKvp0U!Bv~*kA~8?SXZvjf;D8cX2TteCzu5+d7%+o*wFy@d?nYR?VMl zX&#^5bIfi-oV)mwuE7blB$y48VG)JORxz->D~eW2|!^4;+H`>GCDu80%_BF{mdc?i9jl=%aK0A^c427rgLlKF!{XbXP zwBzo1Qi5zhZ`qul<7HRnnK#`uWHfY21#=6s+2|zC{K$i_lI?}Y6dRa1mn{SG8^mqN z)cGf$u+^!Ygh_2)KxyIgkQ?yPuk#o(lv*Zrac9 z3m#}82-KO@=Tn>N1h^|UYw8;tMqvsEl(%BN^kTe1fKSChM3n`RLV!pQnKcyDKUcmm zgV#l@6=8zpKlI55_o1`bjsbrlB6&{iBj)xuRW~x~i}43?&+7LV#;u`DL!)BeuveaT zRd|jxx8>OwOFy6rOkfB-9g=|_b)bTCrWAf`1htu$TQjccH8+`H%Yn%4v*oa=1_O^= z|04@QrU~y2ufZk@kvlO}12Y74(my@VwyiB;E-tQ&j12IkfWa0bkw!i!{2|e>N1B?f zW&|K>jQ1`Rv;44JA;za>gE`fCyMF?cdKykZHjV2>@_9d3`1SZ`%4)^G~QOymQ z_ubHCmd||5B2$O&5xH9#IiE8KxR4mN(l=>F7ipZtC89q~Vcb1VfBZ%Wgb?=ohC~7= zqa_uXgcdxKfpNbqXO9S+v}F)4hdrc%Wov^udD%R7_V-xg)p2Be#P1OM?)P1!yBda( z1=zV0UZi&+``gra}U;C0T4*R*_4ph z<|euL`*%!c=B15qb#0&*Bq1RIRtmT$5tcLQ_#iI|EVRK7-xP%P;D`?DI$;X=HJ71J3ROfcIf2@g<$V^?|&%0PGQ~m1wt;jfevVuRS5w&Nu@%hX1d-V zfz{EHEM~C~sIe&5W?5~QTTGBZ4y9JHztv>)Kn{Q#jK2?Nuo+odS%Or|EG+l9X@%7B zekX3ly}?_A{4YicL}saKpe2LQP@uj*!C(PnE3*pj`UzxlnL=8O)!9yE7D|RCqBGD| ztthPh$zSsTAOWx5nwv8?fdS(>;7GXxoij8K60kD?2h6jpGOR)uO7iRWPhOpkH@+j& zK!}=4a7HGAJ|i*M5mI<5em+7AowJbO0u}P>JmLE&pdh{*XErh9bprD@8&GBlvyGG{Sgp8;XXCQ&I0{L_WRok!)hM%*Ygo~4@#v}4S~lC)$)eJ zL5~O5%x;UbI0yFC661gTdJb!7xlbc|bv-&XxZVEIG_2pGSu^4V1bs!5iHRTT4?I~| z1DBHs#fsWVxHyhbZ$x3xlR|Lab8%TkMJUG~RhVH-A{_LKzQ6P0)if!02aOQx6?9U&s{-PR2r+PVbrL}K zwYRtT%6hTv4OxiHU4%_nGypze;a2)9CI6)*dfApd)HLiW#Pz^9KB?y^3OljxbKqFq zwm*fCcURS~&??W#wJj#*iO7C^i))pOEi#v`_N{^FjKQ()&Z;2q|BMrbs~gYU%AD3= zD(d%}WDh&9(Rso;{z_=o-{+%gc-?^$xFI?^1R4~U9f<1?bY!WT*@4>u-n957qJu?O z_4j;~Or>lzQbt2NJo&0b-_mI>gocMEnyZ+szw1Y7t`{W zBTj=B^?#^19B_#16R=%M^bt`iRJ?lUSYc%Jn_smmQb>7KO>Wyq2jHNZuknrXb(_MJ zW>-yv!2eAf+|Rfx?@W%R3Z_FbKW0Y-?SSH^S3eh=abxf7rnC|Zl2l^yFZc`gHuO@5 z`ZbPQmu+TxT51NJ2G!uCq@ZAu7f_V)^q=L0tM~NAUw&uJopTurVHtpkY_)7u6P!Fh z&VJ<5fZR=@qv_GBWu^L1gV3t|eW}P1@DQBUe)b+%Ny((LO>S* zV+G_!mz$K+vB-M#<75m5Q;yJ z-=Lem>UMPMK#DD{8h;0kw_i_xgmBiMmx#v|bOZC^;?eTCLtbBB97pi9nkR{2EhrjO z+M2uerEUhem$d<&x_>h}Jh$RrwfE!dR+v9!J>-T$_bJy5h11xXkqa-rNw8E*X7$r3 zj!bspWw~yDf9JFvpuh5ukoy>e&?eBu5)LRMX5)_G>@}ZwepQ3Ypr1_ep7@oaF~#hV zz_4s2^N!oqLupdESYXO6n2mfI(u=gVd8~#kagP{@lPtAM^b5y-Av&+d#(r=n8MV`M6Nh_U`ZPSJ;9v8>>Czh`piEGP4yn;yQzung7*5;`h7jC$V8*(89A1vnSi z<|Em`xOmJ(BsTc?XSY=4`kDBIrj3M$-=vqGLyvB#t_Yl^XFweHw#@4Xpl0Cw_b>k% zt8V0HZUp}em=A**|FpA@$=kD>Jm_u*+5$Bd(8odPsU4SvdH$a`%N|8jSY9~RrNaz9 z`s$dDgrGt!WN-t{(d&^V%EWmA*jt&--Ehur`?CByBG6Z!ws%+hKh<0JDW!`m^%?-u z-79ng<^)7Ch9mK=%5ivcJLR~6D;I94%a5Qa+CBJq!tLQ*7%I`)hn@{4-?xApw7cLQbBH zo=U&-t_{~`N1n4GK%K8h)?<@SFYTcG?%L4aQNqcx(gbhE=I%VJ6J6a`=G^s`&<&A! z7dUKtxkye0c<(1*cJ|lYpd&D^v%@bZUu^Ww>4ldF$lc$~ww-5>xnUf2wC3_WJQpAM zQ1(Y|lMG;7aAufhC@GE^of=IV%BtA#e{dg)moLW8lj-}`{^rA0!++Aicz*B&%Ka7D zxv`Peeg!1|yyMW6~Lf4hQXH5a_q zpx>D>Ru{nQG#laVyZV<6`cB;hUK_E=o#`6}rH1ekJ1_py0}NPic{Tb9;~*J9R)W!G z{!#aq(5)m+hnuXYgtyHP#M#tv?I9>El%WG09LjjSJ4Hw(P*rhqdkBpC|{{;X@dWk4lIW+$+SpNd3 z(bL=T_98M=Dd2&I6h@@L`AB`dA**g(c+Ow&wG^oY6Eg0EkGp{t<<(R=0d8tcyfC>4 ziR%CwmD$LmS)ZltnCUg;!S59NRd{GYpgHvREbe>Z>#q@dqLLyREOh}E{b?5hXT^}u z-c0-^gtlvOk}Ag=@GoCFx32v(|L!+QX5l~V8yS&My}mAl0YhvA_GD3pl90^0If)jB zmIaY~qICqV2$4>nO(9?$fO2hHEAzo9<^AS$oH-aTSkRVFXO-(#zy%6fqiOc9x;3{Y zIQnQrtb51_|5S$~)6%s(-C^y!j4t7fp2g=N2#6T=AvuXj`^N}I&69wdL3aw6Gobsf z(pDfz@pK1JZF1ltc|?+8&zBfWWm=-+Tq^`)k_K21w4~D;3IC2(&jE3IV%-~d$!S1) zf8Q6Z@d^-V2@IeQKk^-k@q~8l!zeJgIwAm4&NmUVsU-&NX!xN0+)HRBALtY`N$pd*HFhcUvP!M&#q;~0G_ns( zvKmv~@Bq8-|9Bi)?V}({p7rHT*DLkxJn;a5d%FBQ;s&RteyNkrIV8P`$!tLbAU+I5 zB+&P2zqkQF(f@khJz?_d^kn`*Rr$kYOK<1StS{V(L|S) z8&(r$eGtH>u~zHNBRfVg&c0GN8Oi(k zlY4N*+wXo44Pj1BZ^`=p0|;9O%03L0R=IxwKO=_$NCMklHSG)FA%F#iv&M7HcWn6rpQuBxAukzmA4HL6o({St4O5 z`ewvC@}BZt*90-}nkpBXNuYC_SjYcxYe)1YsYVaD@AcR&xF;%~brtyo)|m!LcZVHV zxFU7j^zsvQ>W2A$-@m`KYty3X8ONEP6)i3E?Uzoau8C1$xgqut{q@?B3;?Y*wq*6} z&gO-8Z^G+6lUv9GPg74f`tdM3~1g53;D8FV&7jwK&JD@YQ$NdhJ;Iy>4=Ly0BR`QvBuA==c8QmwvUx_IL1*7pVew+l=! zqQb4V3m#{EP)Q*Iy-!>R&b@Qeapf?p&%?Uaqsz45=B&*ROCrX!);XiGsersV^OB@_ zv!;(O9j_fPuBeRdnbMQow_f)C?CI?#xg>^>^m{*3a092!@v@j)R4iGgvC?qX>?Y>1 z?c2(``n#@D7r&*a_4^Gi?A?2rbsk_^85u?)96L*O7mRT_KGT*8(oWjYS6V#1;5~Eb zI44)5l|4c$Fos`nTl5p_b{sctS+1P%WUN02pXa7xt!b;t`0Pj<8{a5%A6-P~V=e42 z3}-bkEje!KznLK@OlVo+Lc{geJoHMg&2bE;VEXlAO&q`ync%Pu}fvyt_|fA?O>u5g3#i+}#h!5>&^U}-wX z#f;EprGFrFB`4PozlSddb2IVm$Lwg=O=imyMbCOFTQUYaiKF!>ZwkGuD)(5bx%^I^ zb`GMoUM7n<3FV^Tigb&?!e;i9*L*b88j4UZ!4P1_(`Q#(?b?%aqna4n)r!YYVmM>y zQj75&{7oZY!gO|RWT#O0S(EJC=B{pPnDV_Y%wpg_MxVrlFBYpJ&zh}9#$nsqj+yu+ zAuWEvb;qmwF#a61MyFa3f11Ey>x@K%pFO=gJb7J>)1wDZDp(;HkAH88n=2Usq7Q$f<6`7(*-!?v_>4y8}(wX*)Pnc-_@B|6+als zJRi_7vs{&{U6VU=&)dDEb;iWLa)?{V`XXjuE880)69;S^-imrdpxRqzN+S$_(@M8D;e4wwi0Q@0xRvrYXc*!3r^Z8Oz|Z?9@%->mSfG34=kk8On+E`ZW(@ z$4%ae{9bASUBXr#lOTUGuw{y*{VOg2P6LUlC_Pv(Bfi!uPiS03pnO6tY;45<&1KEw z(gC;Wff^n<4_!0n5Q&s0nT`Ir4YuzDA0m1paSE-ECwT|)IH^}^m<3Gwq0mu^uMqgQo6 zJ$9)VsY-tag>>*F+c6C>i(NS_OR(HM^XZt7s=7^-pP_m7fVisuidtm?5m#7jo=Oz-0~Y0z4fyQcI>%X-f*i_Tj1lzeO|ALBOz-mT3;BFFVVyN^Bq;4`ry z-uSm%JtM!doR!Lx{Qq$QjOJW&g&voPJdkY>Y|b7=*%z9v^4kBE&Mz%D>ZzE@ET8t6 zSe0CJyXs|HQ~PgnC?SO*>7ciAm7|C)#Y@IB$~XHW+RfeMGM zIhBy@U4HLR%Wk{ga#0uCf#1~gxhwmK&@&sehsv>XOj8-*w&I}}ZHz7*IPyRCCq=rJ z+5Ln!Jho)hF9+L3g#Y<%COnk3nSTK)@axvUj2@H|cElL!L>|6M@NF!NwRQ!x9R^7r z4OA_YKb?FFl+G5s8(j3Z&Fd%c;~ZNi0~tSSNB^WGk+J2P(jDbJsWLR`hG-g*KCgtE z!V)e+y-if&5&^z-i#eq=PuK_BkgaZKQea_=ao_sKuYemi^7!Tr0~^am?pcUd$HogE zDu4Xg6G#8E=2;QrN)d%EZ)X#7Lfof0wvy9)RrTziivD!wo3mWdoSeNwx_|QpOsPn^ z%;5;|nqYDjF@CQ(Ol!%c3O;T%Irn;P9Z+OOwqM}~p4^sRT_uUQ|GV7RNkVp}CnJ{| z*9mG3CBNG7Ww;6)Jelj``L%B+a^fJ4ZMe2hcdnY8&cr~V%49H?SETp+*}~rh3CQPc zH5r^urvBR4GBUrhb>?Yfj*DwudkYYYqpY5FRQ?sGM(@(3@#<*1m-aK1s}m{2PX4OT zptwo3CjeF7iaL<(ta8BGhb%2ly|@4C@>!WDWsmI8IY6sS=wlE+Aa%6N18?npxJ zGo=~Q$yI>iYjU(dxCeMubn@9-4zJo@D2n7&ZNaDdWh`G)rjan_EGYic5~Dju3{;Vi z8rH3ctNz>HU#i)!ja(vuQ`FnEVLnD`DoZ}O z;OwziwR@f6|L<;n;-5mz`-H_Em@;jL@xs~}_uixQizEUXP3H3wMT&1kWRwovVGE3F z?T$eH37jO%+s2^XakPaalL2l~D- zg0%6_u$OaPe|J=E^@t^GS-cw(O_+EMrI&AdG3D$0lMkNm@-Sw=Eyh#&w>XOrGqq9u zNN`B=`Oltm#Ey64Ky(ME9pN?TODu96hRdsrTM{!gI16+JY-z7i+7lDY2C8Me$%yu& z+82O(PTEd$VzZ9xJA7te+UlIV+((m_F)$j!p4d209l}0!=|#zQ%8L?GDAS$OV21ha z>nhY2wM`?|B}wMFwpE7M3Ih@08-cPOCY!H{5*5g1p(2c0r9ZrB!={Y3ABI~c zIjQde>k2gYDT8{PK|I~So2M7sVM{n}{@KOqzZmQ8(xTHZmdJX@kHe{?N=%7se%7%_ z4XNxH%~!Y1z)s&DqtlMJR7vVIyrz_E1AZ4E;s+xNT{N_t6PuRvD(E2cYU!H7xbdne zN{m#F7DV~+Nfdcs(uwc6$P2DHn_lfb-EE}iP@Ot`WilTSl{3|RRm)R zbjJPVf>(L&I45zO?IZ;vux8-a%w`>^o~0$R^d~<^o)6+?Q@ICVflifiUt)&h&=*kn zz8j>gMI~RG6HHJ`s}YWJPsg5Un{*G$w z2IW-h)_9rLwL^hCoy)vv#HtWfdS1jy&O?f7>JrHmW92=pvn1{UKRqdXx}gb|HFyt1 z%9!(< zEJ~d;=qV`H0{#{wfr-26Erg3d|L&QKyBvdU1Ay~ASQp-ObSM06qk=OZ6(aFqS?=cG zLz7aZtQobV($TZBGL_P}$C42Z`(gfQRb2ItnrWfKR#~a&?kB^LAVR}63C>437A}u- zx-)ZU2`%GvsV*A1N#n3W>e>HkRQQy@?Sh4y%}3)Se8%SUxOAn=wHD0>kVjxF>e`Ks^(vA@r-5v6w)9G81Ki&!v$97t+k*d#xRPvR{ z&<+Q;2N%mXhnWa}GT@FE!O1C_1T%ELtyCM*Mf0X3W4B&7NFuC+f2EqBZaAodwLk2Y zcK(3BneW&St8&?PqLtSEGQ)`k8mMk^WgVLd;^ZYVsH93!M=bxByW zP^?FA#z%!>T_Z$`6PZ;Lznza2i5=Jb@;Wk)@7)-#v4(!wQ{GTMdp@V@_6)LOW?V05 zY>t=2VlH;Vu_`n13sTF9yfZs0d+u>|xk#wCr&dMT(*B&6QH6EW1!L>2rSvD@vVhDN zQA`lR@;`rh3$)C?%x!*Sgoh|L;}`GKH1MmeMc}xcJUu(O_O5mcL(>`1zO zG|{B(1|s^n)2>9%BmPQv2=a;(8p@qBBVs(pQr#M{tQW-By0`mCO4#Ir7uD~g@&vgab+)57Zn#w5MsMpVUWiMPa zL}p2{m?sd;lB#A?ZFomJ*pv}`TR{hX6c?6{Qdf>UJ??J%VTwCk4p(XZlDzM)5f#I-h^C7t67YtS3wGwK7T8>X+?%P z3?nO1u%%ZbPZ2wAUw{Moo0u*XQa8n3jZiQW&gI$WBo_9rT=$?Hh@M|tM!$cLA9Om) z33Z*)sm+6ek=jiC-<#iX#mVJHGTlR`u=0G`nOK%jPL~WZ!( znKF1BPMM`{OI8v)sI-5|aor8UGq*lo0UtvL=R8~dKciCoPs0$ap*x9rsk~!PFsEv! z%w@h^{5>0xopVw9Jqk*@UQK~fxKa%2_wp`U;6l@)=Bo$C)o9U@tbP8YwndfwiTP3d zpL!++zcy<;xN~r@!2=Z;_Loh#%yotJP~Iwo8W?S7E#=t6iRt1(Lz!o6fNXep$g zcsGuAO1EzKy|?4{C7@Q=S2I`3jWLwj6kQ+s^}FGBYYI{pLt|^r$EWWU6qaZg&1&)v zx7Wc7xJ`m<;O+rk7Bg_F?^^`;6^7>VHF}qIz;_W%%%V(|#(SNLJ;0?g$U4>f&w;N>NjF?yCiia# zZJ}&UKg^fUxY}U+$ws0Sk}nb^@k*%a9X(G#hV-+O_!4q@u+`se+F=3G5{x#19S7}A zVj>+7lYqh|{g_Q%=Qa21X-C&zW7ptq1RjnD9{Oh@`|EhYG&sBLMon`pDzrp+QtY-Z;j5!l)v>~*AqXt%*!VaBslr@g zqFfg6mk)7U!}oAy|7JaE3;cn^Mu7qG!}?e`WXu^XTgIVm8|4R(*3y_pGW8BI0AQvW zPKB|{iKArD)8L@|ip#?qkq=((r;(NYx`}U*kywHObFoPBGFfA#KC+ipEy;Csn*VGT z-14L;pK@m;3=vu|alx_4kMF7sm+_*z!|B75dVet#1Eax{*=f__jy0_lz<_09?yqYV zISa1vRk^MMEUo_;zz4M(W%!8wYe)B;)MiKl)@=E{=JeT>@ZusKmSe8XqX>y>$gTMO zgMfjPaWd4LU>w~CbU7f3DPdT)Jb|%(9MfG?bfodAm0_Oo<{*#)HrElJUDlo337=W^ z*LIj|zM_ktuWttraJP3KK2H7gRy z&ws_EsUO#xdRYVDE4Z}~WC)6*2IJ)B`09YLI9k0VI0U=`hL%FW=Pz9}akbU*yVek` zx7FP5<_K}+Nrx!!C&hh81K^5kCuN70jJ0GO7F%NSeZDW`d;NdhX`35VOxnJXL8{;< zt=-9KoedVs$zKv2w&DDrIKsTl&8cqgEKsk|p93{120q(*^`+iRG!hHu!Z=I z7YT;3_s4k;yqY>;OLn-7eRt6hFBw5`s?R$8aM`hGhxxHb{QzNZ54%WHR;mX!Y70?i zCML={;ui?g;+L+wwHTDg0A{|Y(cK;KyVWZ#Y>xo!_)EHUNqzK=XRGS&0q+OA7@(f` z0IGoePIw0Z0ti{>HU+V(a#zZ=iFW_uHDLs$B0SSKSr*H6^ zQJD_Gr$#m($aV~nouz2|(VNHzSw>4sS3~5Uq15YK7ReX{jBbtp#gjPDD%6%j(jJF0 zt|>DZe(RlNIyaX7wOJ@ovnemsvQ59#v|j*$p!iAy^WU3PBmx>|&)*?MCYVM1iB;b> z7c&snwn0t)!>j63Zyc?qO0wUhh`LJ$(XH*P-OFA-fC~8gw_9sd{5g`F$))EX6^Z-} z0P1(!QVZ(WH5*IV|(}^N}(~G=t%mexnez4%b%_e0<3c+V_ zoDhrbF2yHPsU9UE)i85u+gKx%1t6)}kCqDx@4F^XcMW}9G`luMRA zDlg-KXk|SPT~5`9PgmPb;zX=3!qA^ym7d|3H+(su?)VP7FqC3CCMS9N`U7)Ty(C-A zspvfcsmBo={~t|P9ToN3b+3gN5K&q{P)cG*>6BDj+CjQoxMC7#pbic&X}mZw=QkgUxPpV>D%ux z0}oVRp>v%MV59vp^Yw!q;u~zkxH?uuW?2TaAF?`$Oy84gb;9g*evthDUOeE|zF}jA z;OkdN`QAm)RXk&V=Z~2i%*Wh(gOXb~e{XK?Dqrj=V>D=()vb%Y)N?}c&;_p!L$c|% zV(2@BBL=ulSEK=dfjbKN^HzGkQ?bp_AAmu5#UMGAd`|?#~TL;GWxyb@Uyxu zSFgsU)pr1^;XgNK@QvyGGU33#Uj6yR=Y~cDuSP;y$D#~|b^%~JhNUulKZWVvMl{G` ziKNq>A&ihDMRmu_C^`p7G~?ytJ35>XyK^%TllnL~s7Bf5@iXu**#$Y8iV#FEd&^OeP!k1S zVj5}jV7&bSnKo@|4?zA0=S(UIsw^uHOVb|5d|Ji2h9|G)Zd-8q+Y2zhapg8ykhUqm zJLNkRUlthhtcWxYji*w9QMBAR?!AV4Lec#4Jxr1&jxXH%D zl3;RqAO?OVPo`%A-jK9)FFAkIvu}G<>GBU)(TxCBW!`F(03H ztio72oTP-dfIR?95Gn=5rpeC;G-(8LG&#KP^P)HZ8q-EB9T#)tjn z)!XFc1HdpOSo@B@;2`ScK;m5;uH`$zO{Ro(*Zy)|6LF7uVP>gkI)riw-7w3R?cN-V6zDN zkk$dqF{Gp9ZAmta6kkp=Z?h96J)VY6E8e$XyD|O zvboM4zF=IgPDoie2m_JU+vzYlO^s-X5Ow_XO9LMN#cAI~H@Nxru={S~PPxzHscV>W zlZ&*{{^wL0NNsAnK2KhyYG0`oPFsD z1aH9;wjer{uj+bw1zSa_FI3AzifpCHTd?)O?{Ld~h@7z1Z!Fhp23BwGJTy-PCZ+%A z*B@90gkoaf6!$tMn3{h18h$~svOu;IXaV|%{@z~9@}H|30i$Q*!y)>Ruh`tA$y>iY z(!~4t3MsqXlfis`$-l}qVXr!;hDIdiJV+Re>dytm&Q_K0+`#hQYPgPZdpeE1*wO#) zlI&S~?n7AwRa0R%yLN=SwDqS5xxz0O*FCV%a$_3%Q*!mVtkvK&X_oOXS??(O3MW|rZfqc=YUWWR>s z#~6fTRS=CV*c>3BgIqHxA0N#aFCxxy>k_Z^p{5sBSnSZ zS8#&u!04Y}9ALp{rxAlp?LzB#>|j9u{H3Ozs+hi~kf-kyNCvhKei0CYyiV!b z{tj&zpvWlhMUw@zwn%mC!m~n?_qYN+WyhZ9ob}a_WDAz#3C!7GnVn&gZjA$Dz_HG& zQJLuV@rN6hKT|8Z|I9-%H`iJHg`4Qvo?0UZ(E((zgYcP<|G7cxA!nOptiGtl?x1q$ zovQodNl!V{N31-~dqQA9VI4%oKa3~waHTS4H_G1FT5Sji)ya+8f3Yimd_5W+T?W*M zNqai~UY*E3IPX01qJ-Bp0dc=0yp?J2MQrBP6p)-h0}vDyRW$igwP&@aOtS^s2#V{L zKj6MQtFz#uq57A)R!7Rj7wh{GAtho=^mLet_4(NL1tOw@jg%?;^S7rQ&3g)^er# zzdtKjtbm+~Qse?i!digZQt0xG?7-z(EJXnmt71%lhqU8nPS#Y z_rgRduw+u8J`sYu^!VT%wt-P=rv9w_LZo-O-B~y-&H!5F2v36(|PbDZX2; zslTuNsqj4$Y%$vAyj%HKnwi|{D>aSn;g{Vd6jt?9fShZe)){?)ZSx^Xr?RDz3&|2iKOb_ zc-1ItJisp1ZoZj;M*(K;uftCl4LMhqK=HQAt^|_;fxg#%nmO!FurEv`-B4zWiMa>UYgk!@ybkZ!7fjH{nJ))m+uc&6?G%Q|EL-xz`@qT# z)e_;B{7+lhKL8X#VljjrBe^Zdu<#~6YPKH-rvABAu1KsAks73lZ$8J}vW>nps%hPr zv5$EUXN66M)1XOy@=B&3$ThgwmyJaFIi0`FB7^Mrft0M#diG+;c!2A+XjXivw|4l!{{s2yS84-Sl{&)kPp}u>zJ`-kC|sO)69WMj9Tb>Ty69#@ zz$tU|ckT`0nA;_Rt-pRT!)6L(R={@VU*leIEltRZ=v!8a8b6_zhKY1y-0lD@3Q+(s zzeh!Z`Xc$NO^VrB1D|pm%ou`%sqd-5B?TgAJGtIwJ>>?Y2L@6r4?r6*nw`(f$ed@; zcjpT0;%=`xbC6^+iAT*`y$x>{aL{1n1k@B@9sYz;fJ7H|s_!Dbk3bRtVy)SLOfEo5 z&hH@q6 zva0(V+qWd6-$xQDMldQ9$_*jAUY@43qu!)3lU)nYZZI%TsMvz03wAI>1<62$WGt4x zd=)@Z>fZ-Y)C*Pf9Nf4093Yt=$NEPWMUaUNpTn+g z(U8R}#lJ-NrGVIed_5E;G}IQJ}|)6 z&U5A>2Hl!$e%n`qf!=4MbAnRUfeb~W zW6qJb58CTRxe+zr-d-$~D#7Y2W1AD8Vcjr0o1cc+>wNXyJvq=A4w?jzi|G|XdL#%- z1MaAoaHl-~vYipfuXtop1eF2O~kOMjtq}%{NXR_+Vms^f8(90)Qil$CS)4lSC#bT9TJ02&2 z@WMGsgkGm=y8>34fV+X6Hzzhtt8ZDg>B?K1lu(a}Um&gqtugqD!nTu9$0rr;FN%0- zge~nDZYn4DI5P)FA09DCd;rU=tgIBA`aVpg|&_ zg@C2G9n#n}zi(^I$<s%g_+&tVcsccM7ZSI-CPJ51H{y;LgmA=08frfE6Wj@3 zv|+=5h0hZh6F}PcH-3v75A@Kor%nc@Z%g%ao zuVE&r7y1g|tBuf}#E;xft^iEpF-DBV8TNw^;1zgx)Ut^C6C@hcbF6Vp<&D8WOtTBr zgJ{jZxmMh_utD~ahwM<4UREwCt3tfnB;hlL1&71`c(BeX`F&VnJFL0|}&9Kql4YNO3xZ7j}Vf`3^lbwf6N^Yve7TT;z}NRh2~@y^L@mg|zaNBQnH zyUxP3edg7#e%}on%U>GPw}(Fi02bbnvYd#)=-q{+2a`yXmGKj^x+vmeDZtMjth?yK z=>;_!{0L31S1w=W)FZ>VJ#1E9b%5P=aMgf+0hFTSPDKeBW)u+0Alc)JcPGIyrEcr1 z=ylA*i5WVPF!KS6>ET!223nLd!JEEO99kUDm=kLpOhch>1)PDS)aI3MjDUD|a?>S{ z82~0Y=poksaI@OinZTuh zuk>Kh=rxQQr0fbD+;@&BGzVgx$YT>h`cH{L(+W7VB~$Uif{H1s+lF>{oXkyg5idag z&*c)j2tIYd>~;XYAQZFSJq-3@{gLsKkM~#2SM7!YB4-!ZK8(ipF!p5R8m_s+2Na#p zJikm=38aU>;wNfOwvp+ zIz8CBfH54omLqzUYbNy{&S zY74&xA^Tb6CeR!}v_b0;cqMH9RUkD+wgEX~(@vGOgCKtzvVDETE=;YJixKiocpnq6 zV9v_l;}Vg@6i&~wHyK_q^5&qd`sa_|+Y@J(9Fkz6MaPl^qjm!>vF59)jK)Gd8~D;V zYbR}N_n*IQM|q@v{X0dT{A?DM$@db@O`ZW_o8|M!pK96VI@KacPj;<1hmM zWm@MD4p!y8ECa@`ZJY(g0ENKjtV-8fRQ)VgYEmI;Py-wnxa=hx`64~4>z2xdO@YfYLMlrfD)9{H*p&`W?nH%%1W2te?gHx3epN`KDXO&-jLO2nL; zpmcBBv_{AsECx%szwLS~on={`NRWg7>L|Bt+Cyv*7p%K7fb;@BNOG}q)F;@d%4CXi z67-O~pPh$@QGm9=dRREGl#+!w*t4PCGgWM{IyREKI#lTvr!!hAATq5jIpaPx^^Gwr z#j(w5GAq}~-XH-Wo^@cYpi;*y#|AbZl+Q|!xnxmX*fdIS#|;ia>;|GvfN^cqOfytw z0^#9SsP-~o9g7RA zhWu(0q&YIkEvuHkYO)~SXIixOWzb=4Q(*l-_pO~~H(WZ&vN8FQpP1;?K-mf8Ec_tX zs=0$1O8u3l+8Mq(4t~X7WRflXdwMWjiFiWe_mIO4yVU1GB7p6cOt{#;jsx`>OqTkf z)QOEN~?K^mm5&MWofywt_z1wpbjLy%G z7}$%{85g;Uj>ac$fdTmz_c0tFD9+0M9J26XV6y|Hq^=L1dkd@$ZUk`Jh0na@^d(Du ztk*}X2*NbBA=rgLUyDOU(=$Q7K_=6(MlJfh>-^w;5ZWjJ20K_-2RZ+a%>Y^zRBJS{R~@(%@!WYyg9sulj%#0!cdvX z*$dECn5{nsrtG!vh^UeIF`@@Y__B=e8&cD1e^C)iqg9;Y8l@)Q5_3z|zJMRgtdZ*T z7v`IFu?vEOLVbQ=e5<;q9@pi+znuCUQkD50FMx)3NofhSbkkK!h$5)}33-UNtwlml z)}?>79;JC}czV^0iz#XHjGHCg`^JFdE_Qp^fY3m5q(drYRCsgKLv%V2z7x!kJrzc{ z1Iq*3-;MKlW}7ysQaA7_rc=Tp!>P>OGN<|Z8gXG!)VR$;L}vEx-gyqUA`DGA)R1b_ z+=kY?MaZ6Ivj%HNyT?}X!A>5IZ;zi`w}S-5QQ84(mt{m%1lt3!~M(&4dh0 zKR7q8=R$mSwlboZ;2-#%)}_r8StR*B@!|24SHW9)^LV^y(h^jw#bysg^8hqWL-A=? zc_asq8@PRrV2#bhS_wMsCMJ_by`g(`pM@;f zF{b8L3$|fW(j>kXGcN^CY(|>A_4>n|60hH5!7yLKw*`9v9RvJiuqUlc<(iF%wFTNI zw;+;F11AgkqGIQ3ioW}W+5a7!dr4Zd30(UUqEoBe=VxsCls5#(D%`~zt$LOU{@`rd z47t0ZVO@ovUD87=XjSowsPEXa-=yVW{NVy#085WBtlw8wJvFH_5!qQPCzPVRp%eO; zjh2s2Ru$k7fTmUYR5gu8 zNQCUfk)-0e4Qs~Y?p1%u>J)6*1Vuq;T)=1HdNLFE81H)mp&gq;4RQzZBv|W#f#?ar za8j@1xDh(S935c{KZOP<8VqZ>Ng5(~eOgj}DAb=_3J>opzM0*#`(V&S8CJsVz#ar7 ztCf;*rO|e)c7PTz=UTQ2(pTiWc&mqYxs1I<2 zJ-OG5xo1C#n*NX*Tl7Eyo^u;iCr-Lie+3ufyEn0J>c!ns@P2F8{(AqaZcos8g7j+` z&jLK{TsM2rPq{ko3^+N7{D0?cN&J9GoM`vs!^Ph1IDD$T!?}Ax1x1})?1%+-u+fJ% z0I~((3`aSAAK_9|LlIGZ&GxPnn*n>i{jtX?bTMXY+M}^uL%Xfzg6fpOat(;)e;|A?i3mK? zyT^jzMjJ>u^e*G0!!#p`F~y8=Kh%=R^4bZ4^1n02!FclfYtYr7!j#(|MGR-I{uCsS zO!u}Bw=u;mNyq_3O!?s_LfmlW?OrlM_P+dOdjWD!%A2}S zaHqH*Y0%k-tl?>uz=k12Z2#qsUxUqnLOo9qCQ zWF(S#Vrb`Bt@hEj?vd`C|A)-Hu`0Qa-XCYs8vpSO-Pn(sk8mq{%Iz$f&TqP4%9#u& zr3(}E5lw8l3cJ-}XurVvm>Q1fHS&W|97>i%H{{ASRJPY}dBUZGIs~_9Apec*cQAWz^mcPg(=0(($lGThmT?-UB z-_Q((cd#JhF{)8Uqz(2MJiIbG!Idj0#?@}5cYp{oo|hWJFV0&y@dee==`vAWOD{KV zG(9|W-kh;2ui{U>6lyaDt&(hlYTsH7hyF)Bj;AK^V!WADSFzN41l1ewQi;JUINnfV5H zT~adFxs6}Dt9cMbt9cGl_UQJXXs%7&&p-x&>xT;Q9mN=_qyw!z2ITq_I~cu{3$fO4 zfG^0174M5ejlZ&Xc{X z<2!anB^ljMlng?IOk~_2N|z&x>NW_92I>CIKxBX0?DxgUNl{$sU{ccHnlkOBjS%LZ z^!H{ReUpnrMDNpcA|3)yCXfz#5YVoW1Xc}qf}gcg>-xdnB#0UfP&m!sXkD!>x3EIQ zpg#`{dhKOa3QNmN@-#bx<<{SMs?It)=M!9tKl#aa`d!0LHkfGH>BJIQYDr(2Q1&ar`vva6bO1Lq0K3b$!h-h0IU5|zIbCZ$uGBHd?W6!noWFV^2 zzavCO$xYM_1+Y)Acz#fF%MSKAzZ2z(ODfcD4pK838>SR2Mc`FK2!D6v-{isvVX`~ za)vJoR%lu&tcgiFmyq2b`9%=IOmKnb5tu%o&upOqCai@Cz6r{kI&g!C)sNo)KcP1l7J%rh?b9aU&4v~zqW}^j-S==;epb79%&`b z!IB_!$Kz*wCI}VAwRo~qeWDSp^6j@cQ~%e-ZZ(_CZ-cefNf~(^K!2$O~~Xpp>IPN5AC#`AN$GIb7@pczM>C4GRV6d`jdtVBxJI}SFMut4?L;= z_OS$<&Sgk}uH{2<(|;fU3Ia~yZ_2PrmnaPhv2_Jmv$N%aqEo(@tfxJz&}9DSNcL?w zpfR8LO>aJj);;TjS!qu2VxhJ@C{Yiny#(D#>$pZKcRbW{2yYOp8JdCrv*w|YSG%xC zO8%B#<<)osKHl)O-lKq009v4?3|5b{mfRh{e%**LV7Fs{2>v@M^sJvg-AH-~y?Fn3 z+7E;~Z^0Zmdb!SE_F%lqzkr1smjNypesWJ?3+2NW%|K<>6R<@4hJDA25Nb9=G|lOx ztf557ig7J&J*ATU z7o%Eo@Ihw5*LRnmP*S6JI4w9Lu{69EQp;Xn*M)kJK(jli`krr;S;u05DehOfoiq&U zCujNJCh?46(h#Vb_KSx`dl?uI~Ou2h2Anp4=$ zPo)c+si&27s*PcD=(hc)H(mwU7tZtV%15nhD)q!;QqFn3QFMR`1tHYRT&BkJL7n=? zHd*Kak|h)#D0`Wp!t1#Aq>gD3Kn!**#VQ~5VJvU|Yep}xB1C(_2&x8O_BLnpbn>!k z8$+{y;+DONBgsc6r|S4tV(O_Sk-}0CR){y~)!0o|u}f=Gd=^X2=XRV#A1MwXRKP!h zat|~gH^I`*3vCiGAMyB>IX3YklKS8eWJ$0J3Dl^T;)OQ+vkWNE#U7_@xCIPfXd?7r-1>3T4a6u;&lII z1jXE2u<{0PukCqxJYV;mUY!Oyw@?)E05|3IK~dJ?5i( zvOYfNL`i0g!`5o2?{o_;#s(>(FQ|MuXq@uI@A;QUBPp{G+XA(!SK!DvU!w4XtJgRmiWuf6Z4B{jvL?rA7ko( zCQ6~m-n^hY(@ibMr~=}ODYidGCO@n75}x(-YHTWO8pDMS5Qz#m#g;~W!3*%50*gJz zO5_a4H!{sqAg^P5q%VCx)jWGtQ1QMJ+W*!ASRGP2+UR`=FSmEep!xdaHxB;HT`TDK z!=95%wYWUsl=?*voim1flYE$eEC~)cpT<}JoNQoIi>>eW>8H#DfRjV<(-L>EpVRNr z7H==1;~jdl`BO_-UFSK%$coE1E1It!PFq$KL2g(V}p4|$(fegMHK z4y46=z7$j-%nNwo8~ePCOGVi9SKCh~^hqoNBw*@u$8zt>NAh3?vVZ7{^EHi_h}1!U zcEyN(u|1CzdWGx z3ZRT%9#_0HzlPU_iAP8|#QpCqodRN&Ty-&L&|Xv~_*0mi(^(R>y^-ARZW*o5DKzwS z5UO2wR+bTS2szanj-}p?H-1$P-39ZU|Hu;u!#@g!sm0^%5q#7yyiDe#VJ?HR?avQ8 zF^;yy0A2XZw=pJ=Cx%e=7>!i%$`L>-0guFB23pwZ+|Lr1iIAZF*dAnYvIy#~pAt0u z)drKl9p#5-2)7C@eS?e?zlF97QCU^jikX5x%wQ5@VF~RM#=o$meSQ@GM2hpqYAwz{Djy(Z9LI867QzOp$ z;%PBYXPMIY2&hzZY2K!0r_-7KdW=kvFz^_cP=q(ttqMi#!i4DnUyo|)!p#U%{_kW3 zxZkntzS;G`Iv17e-56@fMq7ZXYW(x^=Ot%nX%btPb{q+WBB*M>Dg@#yVAQHo1KxGU zWWhmr?Xjou9+?N|g5b#+3^vwN{o2Y zVxf4{?F;wf1TmtU06SM8nQw%dADF9oPW&HgP-kGAgsU@+36{wq1tL22yfJEUE#aX$ z_=PaWF;^vIZiLi?_wrk_QnoT(;ggCqp2z_A#JBJ2*t!vbxWis>$|y2#se(Ti@RoTAT4h*;DXH_ZnLw{_@36fifZ1wJ zI_ZN*-5B-G_<4BWJ{F8)uyh79xNc^|D6BTt6zk3QU}7mwYz7#xw*0h5jA%2(@Wur1 z>~`3t>f5-G@!u~&6sz-k0NgpTMm(im6l8L3*3048t%Cmq5#1hSqxzB0+7t!|IKeT6 zH1Y&M=(2DHJ<&eI=z2?B+VmVgjpVny}pwKtJT!4@#b1ZulL)^LW4{VPX$I@RGu~054mJnK7{E>VW zzR_uH*mIksqra3});fUjm^GqplcDYVN)q{y$gxT=_)_$Dv(1g=KM5F=9e(=CZ4`TF z;rv}Vl=nasIxQbq9x#{n;iHu`=4j~tv2(Wfi!x?vvFSR-TL!2_t?rv|d4iXajlVopi(Q~C&d9@|m!Ibo z0zb$N00g0$(21MR?fy1PX4ws}shvNqEx;Tok931s_L(2 z=_AG}e%RZBZa4vDiv0?}{BZawZ;Bibz61{gSK3y+_U5oBYVCJ`?KTHbOr7Fq%+I-l z$7L^IB{02G1E|a2b1UZrD56Gw!|F)e9q`aiwcUZH{LJ#7NByeEqQ#1;rwbAy@T6n^ zT<)V-=u=T{+*JF!!L3jE;V`va=@4o_dk7o|c)Pt4U3@)D8yuzSSpHQG3n*y`W9Ngo zOAaNE@A3A)LChJvBQ)7C{0E>bX_Z_1=HnQXgT?_OUJ3yisNnRMhn9?BffQs;8mw&l zsZQY1F3~Kl6xN@a(|IM2fz>_PNH5jPS7Cg)cz+@D0!c~afK2#J_mO8GP8|l!#$wwj z?rRSL#?zrMjSsKFjVsH-640CgRENa4QKb!N$Dc9AN7>*hk}$!n@wbQGrIkY=S^kOb z16Jkh2$=yNy(W9Y&^s&K$+zR?Wi66%Cv zA=heRW4q{NgXny!)2*dr;IC(vv`nmNz#gqD*o~3f*Yhm4HiW~Zp2tGqAt^2J;JTes zQGo5|F`1Z>a6TeBY#BTEnDsn- znv3%jNG-{fk6roW8K&`NtGaG)bzIC(gR%`QuTgZ<$6o(7W*q*?@fG{v&~%mWL;Ue= zXhVFb1*~Pij5>|2RbN$+kOS*GkSXxI0Gm21fn)&YGE$6dQ;gw%3oV`&yKpa_Dn|rwA9{dZf4evI7M=>fzEas>Bu$q5I!~+sFQ#y>3_=j>WdjqHW z(OI9|Kom#kaqhGz)pmzcs=nB|1J_Q3>3b+LUGnr(&+@i@$Vn@_e3RX{I=wMqLpX{{ z_>)6Ch7N!~^SOunG;$;Px|hb&-{y3L)rRjP{@Sp>1{=j6N^73P9+}V}!0*-f%{Say z^_tKA2B*Mfuha@|wJ#~8x``E-1(E@5-i{*ZJ2QWq+YYKVFows$3isI$tqq^zo-rJq zH8k&R>Ww<9llR&`cWH`?ARQq0qQn052@%bT5rdUdTBBngI|bzP_l;;MD7Fp}-CzyA zMcnbf3#k#^jteFB!;$lgZ|voQKSknUKMUd*VGZg!>DuXn-0RGefE=b}S0yNLqSF*x z6O)%MycgVb1>PEEJe4n7cgaI! z);BkumV57FmUt6i)OA#E*2(7$&#kPi6jPmiWxO9cFUF|-um3-gmY%*+To-uHTTK>6 z7Bv8^(_a*&S|ku2sPzKp4h@|9p*~(HhB8{FHoB&jM!*aKPy&nM*v#$C8AU)w2Ik}T;)pxG4MG%d z@{zPK0mrp553XvvanrwV0=uN3n1$r_u+Heba43NJy4R?!C6hQs0t)21iQla@@URB_ zrG9*sb6;YBTc{SU&yG8A6(=%7(ijGT(9vhw{UT%vbm8vVB`|+_MTo)HOD4=APzr`@ z$h^^c-mY|nF(YIudB%N2OZRO=LmkQR;;Ds$5l2yoN=z@R~i^Mq=^lq-am5BZVPF;P0RPm z#MJgO5JB$ni2xvW))0)9UQjL0O$ zIrcFvPP3@q$Fn1KaA$(-8;Q`EWr+OFWdB#5!QB3Ipl9v zdhn+ktvykVse%=hViygf(6bjwdRh?UrXF9CS*%kQ$3g~a#?oq;$RrcXe~eS>2vcnH z-5y*zvmE&i&Z9ZYcvc>-{p0EXX`klW-$l~O5+u;{v=TChjMpES=is!_rbeeUS@ju~ zTBQ>IRy?!G=h~))FM>|C*d=-r)ug)Vb`2LqA0?S;U$}RwBziIArkX~b%UZ~{C1&3sxLykhmharSnQ zv3~#{f2&+;8O~aI!eDB94X-NIktJ zv5V!4ZV*i;WZf4ti_Ts4_*vr-i7$Hi5;F|Ge*L=W%okSSWhJU7ee*YI|E*_Vfsws$wa%{|By!PTsZW-!r3l2y>(H8V-Ow$8zyn;PYv@1kcEv!r*O;bRF)r4$7?G zAL_xynr83;wKHJ5L&%HjQO(ZNvo75HQKx6i{GuxEzE23?4mZQSeM@Q^VaK%)dmw~l zSCf{(0yFbY<5DyAR^+WTZ-X){zZ)>E(_#g32G_ani`$C z#Y_SK{E#_gVRuiC@&}0^};IIXy4_#+Z~Vlgknb7%l5)M zfGIbQpj|0J-!`1kBQt515d#(sg9viQ5_i1+2#q(pwDG04r#8*4-KcFqm@+~GzW;!Q zW7ybA&Dmqe_|n{voYX!^Rsh^vna(V~EuUNw9bZLHyTRBUpt|o=SEp%Mnjon~Eha!C zYdB}x;qid>V*R6L%ZQm})>641zN#s<%Seo2M#{Xnc@P{Z-~_&A=d0UTRkoeqE*Wvp zZ7c;am~!L@G)8bKLC*-6V@QHx|BwYq4_eZ6`F&P9Knrp$ts}%TcK?2xjKl_3>-DQ( zge)*&V_^UeKVi)_rpSHPMF^@ws^og1UbS^@g52rNU5KtLDJ_*w$b#et`?F&gL>JAC zrZ%cWrXY#5rys{{9A_J^?Hrws`ijXCfc?LsnrH2lu@C#~(cbVF|ATk&-88X}?b^X^ zM8R80h6=QIpcU$GUvyuI$Qp0X0<&~daLIIy`Se$T`5Z$du#jjxPfeZ?@rz8W|1>BS zz^HVm>JqeX9qs0T)Ypp`LWTM*t9h6D0d{l}OlWOJ0z#~w4JU?H&c2tED0t$mqjfNbQ--qIAJx1xl6{74GciH@vUOi|p@ zY9_#C-l@Frrqmi`8*)$S5h{*_e4*v)d7ofy89}|g4kMBr)c*YgpPvbqCiN04AI#?8TX&@?l*xpw9Hw~HAYUALj#ZGvqgJqMVmf6gq{Z2()~{eKO%AFkKorVvUjbwYlzXEryv zUgqnV*sBjMfkT-<-Ee|^eX@@WgD9aG!#e`783-b5q-z-=f)*7xQd}nwwjOZi9S2Y( zDCTWpyG1T605F!kA-m%H&@$nV&cv!9hN0YU5(VKH9KNBjt&9bmjHQ48niu&Rn0IJRlvtrMHJ+I>;3y_bB8?(?EE%u(BJ>^)AI$d48t!ZhLUd9urM4=;N=qw?=y_kyK5U% zJ`X0O>V1TPwcJ_@^oi*rfnNj@nJh0a{k7%;v#4?1ZFw!uuL#=}RrDh=dVX;!>vgzf z#;labtwP4F6akY5HIaEza=7RBlyoMpm>A+j=-h00cQB~f=VdT5A~M1IQ?s=AKQ5;4 z)8Bm>pkN`#Pt^VUV(@El`3gidK+Fa#Q`FR|U=qYb{NHDq%X%&s+&b9%5>49rQ}}dJ zdweYT3wJ<>c4+;N{M|+gP8NPmDly$l_db*?1f8HaO+N#V6WM~2br>0#oe=cFY~ceJ z^izFN zDHgGe=;Au^rkW_r(abZy@>R`{Y|eu!x-&ATiYUc4dy^V&Ja_Op1s9*atoyY&%HWAh zegx1J+DY8?sgRTbU12pELCaq$E&`q)8~LC z!+~=*?Z^}XKYnk|!7Up~Zf6NfF{2q1AL9w*RLD<(Moic5wH2FuaPES0P?lW7P`LEv zxBvBVTOzPAC8khOEVe)`Dq@hmX3uXQLHBtX4?w=1reGI6?amm=r-ZQsTnKi+j4Inmn9)CNvawg z?WK1p7j}bq8XT2Y43>HN#_2+B#JG-eDNzcpe zM0Si3?CiVx=UslY8|Bkx*4DwWNItN6fYjGto^+cwg%wk2j}o6$Q>09csrny$eFlw$~NUL4%Rw`_j>-Hj>b7@Ak?>gJuef zM&v_vXP${~f}4bW*{>*?LFoyD_0F`YhzZ7%TdrxK0?;inU28|5%nt=(dM?%Xyr=1~ zZ6p^#l@?MHD#SA4@SGF&5%$NLu;*=5jD4_noH4v%Y=b9Q;BF7ROvQpHymgMbI%hB>brsq4EIY1t34C z13K$U?j4CN@edPDW1HqaN<7j4mV&O5lpL(6mn(r%DTqmI$&`B+EA?MFJ(S0pzbYn& z!H!qYfe*uiVOimnOkN8!7%XPeyeJy280t1(4PW*K=_2^=*{@kN7i8RqM)RH==-n_n z3xJrd1kT>ue$BR&OZrqH-t0RK-vYF2-NblM?BT6&miDD->B=euaA_$R>{p#MrS5LS z#9eITD9!#+Oa|4IvtF46mTl>unjy_$26D261&vdIBY1>UmR0f;^?hhpi^I?Lg4^z17Z>a$*X;HR zLC*b@=UTyI-w$$`koQ~jGB=l!n%e2&+{e|;4N~LWQy!;1{pZ`RFlE~;x=$3sJ$?7l zsgNjF>3y_=Tzh9#dTd@lol0?-^hZh5{K~(x)h?b@9PbEQ(R2a5V|_{vO5X5W?~fOa z*HiFcVlw!Q^wtzfrDq&6tLNRp3h{Sv9ryxZ`CA%KL9z6~QL}}&&J35ld;&3sD}9FL zLJ*+|eNeN599W;_K7*S^GmjT6G_8wmOjYdQrY-TEIx_q~*oqNjW{fY*w&Qa01Z37U zR40O?l;@-7-A zf&qE|gv~NM{@(v<0RW<6lm)LyQjGZ@EBBucZ+1xypeMmsJUU2`OcAs&*Eg_(O(RPa z6sOwDT&Uba@5>T6Nk;EXAnyB>1+WFq;pE4*qXN`0f zj>G;;!Jc$kE*6RS!`ohGHg{du1Ho!yLLEd&QAL!^LNCP7p!D#d#UU|?@99wlqlOpt zXS{WrVmrnb2{NVHiwg^H^orX43!VL0=@h7c_DjoeYdM3FCe#>n7`Km>Q+Iz@7i~wT zp1y&F=oji=?)~f6t2OZ^DP=@?Fp68z;q)C0@2`8KCLU+N8Vc+zZO(nt%(v6(MUV@HE zh&Ulpx~v|iI=Ib>3QcN<)uq4sxT`AZQ$RQl7#c&P=I2w=Hx>-OUNYjQP@#*8y0@py z@Fb&E`Wz4`_q?O&WS>0h7;!g8%K?Aq3FKnwjUT@i#y5pNL2QgssTEwXDxdb_l!{J% z<_~{^xsFf_!;IEX-rdSO(FMQ)>$hd_y;x=N;xK$Y>vf0vWa7Om z3m@|DVbrswE<+EVQNpAIm8q*oI9Yb24j{+xV&=QeJ`G8sEJVX;5k8 zGp+m_Ad|{1n-8i~k%enu9AcT!yqz(9=4kGpxUaIZU-ZO z3~B=N#rDYa7Y;B0v+37$ms*cPKDzSWGQk^=tk_G!LQN#tgtjLplO_GJa=mtt^t2_Y*Ej+H$kyUZwA z+4Iqq29llW_|D?$h`6`={6Id0vkm=X2hl`+eQlecjikjak##bhutn z9HwvT|G@FeC>pP1`q!0!`1uPPo4_kzwI_ zbG?KM6da*|@OprDKYr%)sUuoICrD9lK`H4czW2L(JE>~=iXBy((EYRja*=T$5hV(S zh;ksJpiRTHU$v_risl;!<*nl6h_%*HTl{rpRR7B=12WZqgTGMsdWJ(7LpBE?@d*Ye zHPK4IQGsj*%5Ykw^twnIE>2-`*$V6&ty2_B>~GslJG?zY48abQdj7F!SX6=k5Cu@UV(geaT?ck&xrgUAlC6? zl`+M{&bZenfh+G^;#{Q%zV+;9{71-X2nDIt=!RjgPaFjb5=~s}Nr2UJ~7kVrjG@aQoSf9JH8BVw=ydC8617xlT0*9 z%t^tdQKHN@6hQ8&B!5{}CFOzuSX=QU5o!|l?Kj5SVcywHKXmf5f=zj~KB{y*`b571 zf&IN+<_6YUzB3%kab+i`p3?c_>%(jA6H`-5U-cd>ZT~&pMuGRZqhl1LxK9kpc{ZnK zUXZ>0mKh&I(wP(F(zXi&(plHC#C`MW$7&VvFHPyVB@#RvO|I{B&}czc zbQ_4H#vV^9*4h5qv4jkUxHe1lojpKDe_gQ2pvns+2lXm(r1bWav7_EZ2@<5A`SU9T zb9Ss4mdo}M88u@(ux#%{7n6t5nTIh`Q>GggU%l3l*#MDe$MgB!^!!^iMKCUfA>SSp zu(631C~~m+5Cf7#)frL=22a>dfMgWdNE`D`9M|IsbWt%Vho-M2r0BS`Rt-^l>{Sr1 z4>|%{tCHlA$@$d@VhILsSl+Q&C>Bt(Z~IJ|awGtq^>*M7d$s@I2Wft_8AXM=5#alN z(5oiiLKrJ(=T+_|Bb3vpr#sgn1s8t^kGaee0Di#q8;kW}l#zuT?mn_Xq(erHSP{N$ z;K4xP{ma6PZSCEHHu2WWkT6Sv+ZKz0nJ`$)*(1|Hhtv2`$GpkfVol?8MQtRZ{bfIB zG2}r&Kq>lDlz#t*$KJ`}r&@k9ze!S!I`oW zG^+61{49u@+B{b4`1C#FC=_mF4P~*5_&$v%|5SkhC;LAlSwq?-x?D;9i&!s;rAZoT zB7ZM%UT~FHSDbva4~I*5XH^{#T{gYoUd55QSZGY1!&#w6g5X>X)?=yc5Z@s;ImY!J zHMA7>Hcm*#Qh>&X1qAydd*U>;p#8*}Qi(ki{$(2}{exdIksZwg#e)Gb2k0~Plbd9| zrbp%9AL_`PwY$d7rG5ocxCUBgp6r4YXiNA`Y~|@Q{JGj)x&St!BwG9|(i|1q7w}>6 zEk1Uc6(v_Yxj0y1da%>04juQ=_hL1;g)}RwY;KzWvbzy`OD7Q#Sxig-OJAU4m)=mX zo5}p}HMQbv>Xo5yM9&*XZCW5nAwve1A7I`MLi+Hf_mS~Oi{8?4=ONkqk#O%CQ9orG zkZB~44>bX6xFE(Jq}qq9uY^2!!gE?}U~o{`bcRw&wZs4(NCE=7i|{1`Wv*$MCCUG8 z%(-aQ5eh!Y%mbguXxx!mG)Pil2RZLfrR@jZ{#{@pTj~Ur26cLGpL6>-Dsm7TPkDJp zoB@6>xL3i%oL3BsSKxy*Cf8JE7nlVuSdA9{1+)=p9vzI$U{u|d$-Z!VEx-a5P*DOp zJX;D9(DC;4hvgo6lVKI&D@t-@vNr=qUlDuBF9C67mJSbr|KKjP3NTJ!mRU8kBkHf5 zKN{_LiCz7*-#6AG;*NyPf|v?4s|BBAn!S`0FhKRZ=IXk1m$ z`tV+MYV>{_oA1nD1L81W84cc~cHyjJQx(sHyuY$Ohx=^3UembKwCO~T&8eeSQ`1XV z*x18Z-m(ViHn3j z(;D{_VxGTB?d$7<$c4@A373^hz|se8Kr+*{j< z;dqiXEzF`U1Y$5rK8m_Z{rQrwf`__&KTwFAaK}^Pcq>|E^xOr3i_fEn4nvMN<9RQh zi{@7c*BA#8hUOOJLbn(ljCc-+W?~52TDnk4Ub&{4RU#uKzejzKKe0tjuKT!i+@Mq~ zuzhFbM5(DNScG`(!Zib~I;`D2#Fvu#s1DH}E@HCp7Df7m<}VV`$UAlQa?YB@c$~b~ zy(I})jb{!6HJ7nlQ$U13{vPVIIODl)ebJj@A|!`~jp}HGaKCNC4USyXX0o&oQ)#Z^ zJMcJ0Q2}DAG@e&0#T62<<;?aX;U$VMOW>L0V?aBn#m_H-I~KPqin!UoqChgA{J;3n z#^xNhCFfP_W_dp!Qx)!U=6k4YRd+LE4sARPjO?j6QF`|0QRvpn!GtVX2kh558Yi?S zzlbjP&26VIN5fT;`)$mbZ;ppxfYvJ9G9#H9Jq5d<)j!G7_QRF+Ur6tM%A{6t5>59y zBR3E~N(eI;OfWX1zy|ENhczePNgW?4o>M>{h1lHw=J=`1MGRMRH~swlBG2ZqSx^;xoO&T!H*DEtom*VNDGaUDQPgX&sr3wEvkRub=guKS z_M7R?x#*MTp*3+0ZJLW$})@g8a|*T!9&eXbGAlhG^g4qm#!pRv(Ejh*1;$FyVByDeo|!%tH#k)Bol* zQ12`dhbT*cbA@x7$%KRTV!`YVdMR;Z=KH%nYNwC2(+JbYVfS^fn5g2TPR7&7{;qsi zO+X|n)2y^u$PJWb9@6}|>hI-rRV}t~f-WENFqgM&vK$mX*o)JmvJ*wvqc2Olh5fnO zNhf~IFRRB7!4Wfr!$NntQe*p8(w@tCr0Q|zb0sz|jn;SHzTu8S8dMd2T(_P*vDS8j z5NiG%ak}50mFM|XCbs`TT5+_7F)V>%#_TA5VOd71QZaYAH1q}Um8bvX+1l*6`UM3) z?XyBaFPwVcda2pbrZY$T@3xmnZaa-}raqp&<)`q#h7itdd%OT_D@f2S zy(R}s0FN>5AI7}pT5h}>Og4*33?$N$j-m8j`l)v@!>CgIk198R6A060P?n|Dw|U=k z%9mgwfcSEo48kU}5(EaxHk+z1t%cgy9 zD^>Z}w|c*)_cq*klHto^Jr@?NBFMbrBS;3WB-rKF^M=|f>|D^;bc0V}c@i&d{29Z> zU($Uf645_DR$_R(sw8V}VR7zzwHDHaCeWW-{b3GUd$f5Y|I9=te&2t(wdQ+T#W)s1 zblnSXI9|!}*jV~nLq)*KypCUgDV}ur@?>n8fLKBMT6ht0%+RRn-c^R_h^Cj_>f;Oj zuWrmRH2k`G0}PS)bwJ&knp>&m;b~FWG4+<*2|?*X^3GuFFCvIq=P%PW;FD#%H29Yci9E~@iSuC;?+$vh(kO2Z95QO-S0PJ zLD#inVeiOCWk*FKtUzJ(e0(rUVR%64A{aHy3xh-2Vvu9CI>u#9&H7h?P*lhnj@%a@ zWLnVr^f}F?UNdhSGpPL>Va&#Re1naPIn8PfWRVZu_Us>|E@kL*EHs*$)vT0{!-F_n z)v$?pmAr=N%3>$^BNQOsIbx}j-{p<5ZgParreCO!NFeFcm2rg^32Q%s`W+|k_zzuO z(lbo8-Z+qM7N^;*(UA0gDsH~2keoW8I5mF~N$3Bppi{R!+p_4L9zvpOeL0Bk5mUs( zng(Gm1uT&JlngwYHZ;w(&gG;Q8?3%fc62cDRyHHMj~tUT__#A?d3B=RHT$rso&}UP zb<>C)nEP$9SvE*PEn65ek0VwY`S3IdiXU6(F+c?;}INi`>XM&OHwa7X_1u(N8RR(fm zV%XkEO2SJL>CHr0ZBV7pYe?}RF(4bV`tnOni*mG&bMPiz;)~xG1hTeCCpR@FnU5k) zLX~Oa^Jo>cmk#d6Hh`4Oh^x}162GX4Rx~ro;4ylScy(Px0wznx-$h%#ZIyq|A|Hw# zO5?FQ{e8+JW~e|E%soggcYXUKIEBE^7exi1ED1PX8BQBqRkJxB;$RH+3q&Y`Y`b%8 zxU?s;_^sA|OBxplxA|>!%gdSvT<=aaw|iSE1q<=U={=PXSj5Bk`_a0?a3njr<6hI_qJuQA^;_ zH9n~b9ZfJrb8tho36algYVOXxCg!iX82qb=7966oEk_VPVJXt_7DZi?{^Ho(SjE;W zxy-!1s0x4yo+Hn2=KdW(?POMQ48$c^x6W=oe0{n4F?_B0lG`-YVvGShX|d z=~38@WEdAfql^=$4F1BHA&#l7F@l$HwaE8Ino^rnlydI~XDWAOPOEEYo`W6y7vS00 zvf}dd0@NP$t~2MHx>`TWGNV3xb=EJN1Yy(_nTo2eaIj`cVHgte|P{_S>ij&2;vlZ z&P!#I_<8JWk8y60dsQF=4zM{Txn`gI{~RP_Q)}^m@fryTejmzQ*Z-!E9P$ab%wvJ$ z`jaN|%v-zp2lMXNxzLLttoddS#xr)6lRa!obP({S;~~`YKQ)bG_)K&7_1G*~E5mkC z3N#OuQ^}G-@hED*PgcXob2_79Dx>0inqacxgZ5;Y&qSYNVu%DXuBb$dkH*Me+{j+i z$cak7c=8K_@jp*+C^C(0BM8=h!Kh%I_(b%x-$0Al)U3>5%;7H|Pa|kp{~qE6kZ*5<$fg4fbka}Mr5;N|w>Fk8iV`p$z|u5Oq+$LRA`8JIvZWEc`FoGx0w zdgOG7vvj)S!?5>4Anj?0^{{E7!^85jE_vvIc?<881EN6%DAO#^aL+I-1@XWO4}^|w z#ekGOB@~>YcsDrofq!q$Q|nyeRYqIgQ&zmPQrV&eO3gHZ9hUm|m;>=e+eJ5NB5|hQ z5y#SY*{1K{0_t>~7C=>DFU{if@p>%_{eiR!L_Im87Sn2din?m(s%%~Ux2mah6JTzjPUe=i;O%S(s!aemHQCtxC76Dq!4+T z@AMVlvB{lWK+2`@u={;Lu56iu(KlCp8^N5vTwESj;9e>>wpEu@@W|b%$7H5oj5kUV zt_Lt9J1~IUm)D^k#5a|31G=bKaE>w5!`@Yt?;O*COFJ?ZGxvGLb8$3d0A2{W1gB?e z?SXLg6fv0v!YG|5o6M1&SG%__yB*-{R=PR=`ovjxC^zFKxl`VcIXG4mrAM3vu$!G! zMyur4>?h4QMC0jY-Zg1GIXW2F8K+SBMw@392;pO4is&Up6^?gkcFXfxtdeKm1%>E$phoMSEgee*j6b{!eI>QjTo0|Z? za?=v|puB+~FX;5pKdpoZUgoG^ARDu? znRj|9RX89YrxsR>my7n=;FYQat_FEAf>hbG9{_tMxW$eZKZJm9B z(8_=X2D8jiabrz`w)ZG)B^$e%(DhuTFkBVOO&D%KwS^Os>_S;j3b^C$H+h}`X8}@H zzow}bSsC#9T+E^-cry0n^yf6o_Io67=p<@v@t4|K*tVDTaC5rg=%Y+i5OLgi$*Dl9 zGlP1%O2unDyfh5^Q&5?FHEiJS@Ehy(#&VqwQj2C8y4~Kb7kr0G5w9A3{K>l*BG1Z9 zG6Hx9RI{iwPWQtEVgALE0=FkRZ=zIjzCLv*{|Oa|BOe9WBJuZKdkK{(vzkNNP6LYV zl`gMx<16goUt6$ASj1vhL=+h?sM0y#b@ydj2@6~%2sGqoQ!8`)5AyPryEk*x&Kw@W z;qPuZ*DT54CP~&KSX-yLIA|Wr?C`a=C9Yso_}{b{o^Gx)$~?Swr&l((_vM!z58)9hll*BQh_it$~Ps5fqLyd z*Bw2}qNpy6ZfuLE#Y@Fje5?0Ftb^2?X`tJp5ei^N70k3+Ts!IEV+}LH=*ADk^JFtX7*>JM5)~;@&wQil=KeDZGB-YRr+!qqP?7`2 zcc4PnK{dNpu9@hbB3jcr8AyD|F39|O9-U#rH15m4L+dHTMu_gescMC79PzH4`+%we z^(IZK?+r1swibl?!+GI_v;~1j8<(L-ewzEtp|Y12>M>pnH+f%45X*kRjW{rgI%HEO zCjuW@Ao*mQhd*gr`85g6+O7)iu)vYPW-&2frxha)MT@-3rzj^IJts4sAPU0*i(cfb zOa!Z>T8r2B8DN&DRbIb(IW+faC~M{5Tae&Gtxs*?wTs%TtR@< z0e0kPluPp&1ilaX^w@>F#(W5RexK(u+GXU719uJ<4@+yxOhP0z`QZHxmoP`TLalln9 zdRN~6I>vWVGQs2CxNf4V3BMTAM&Hjqg0Mj60lVfkodRJ6 zhNK~H!@OkjX-d^^M6#$(cu#M9)E7h{Qame@QTQ|iph&zt1+B@uk{_SaCi<^5>`6vOb4U zmNJbHnfbxRQ{1<5OFQ7R#0&|x5Q(+$!J(fED+veQ3a?2Cq(-DrlWgxx<7di~(=i1Z zgr&^cT!M#a)y67=<)+BS;du|{KofSk1VB5mBKe}5gZGKpb&R0sI>G6p?kiwkl2cvh z)NZ2C%t?c6i*v74Jp|=r+0jePchmk~3&2C=fi@Ws(8T-pg9!-`jQ~wQhp(#11He7= zR<%ygo`MA5VaoP&(`7}R&94l6v`q#(HGdaM0|BuV3+mmPRYSwr-@0;cXZ$b;9Ebrr z!SdedQ~Y0XjeV_=_6@v3`CFlRcr-GDk4z7gy_0G+FrQ#YGnx|U{sU)!+Ts&0jm#EH z8BPlXhdzY-)aJ=+_sPMDpO`JM|VSyIorM|^F)kIUFc_2 z>!BD_isG)4P8J1Kg+z9j#`ANJ);7{(d-z3yzzsEP#u$R*kJ~Gm7{x5@nqeu{u?{>MuZi9gGvPtZ6fP$(6L?OuYcisz`Eh+&tsmgC3aul0Cad~c=k?o)-bmfKAPN^A*UV%+H+Pu+7GSlmobI1M?JwugjS$zhlK$ z|G+;~_(tUC^1#2J-0DW+5b)7>118KU0%x?*R+fuGOCtz>8aj5ac>es`WS%3LJ9(RX ztL_ls3U&fl*=YsIE2Os_64?9D=xCC6lRpd)u5Q^q07tBcKMhk-l*{?9`Td#bvg?|N z1}q3xcDRt0+f13Y(h?chRdJ*NqR}}G!`wRukAv`+@G!S#`92o+vMt<=YKpoGUN>O2 zll}j8Ud4kgv0U?Ygr3^zk>abYX~&08-5rdL$Oz^x$9mA+2hi*1TB+Qs34H*BRSJ?p z6LVFj)>twjJ_ zLnNt<*^A27n{X%#>2i-9e@l)oBoFeY)`-RKTbkIKx`wmB^`nU6tN@M_fLUgGJv4y8 zpa2^Otk3G7je|z))1kY=mWAf7#mL12^OtGF3cAHyR;t|qZC@_X8zheiWfwE)#iw(i3(_c<=+^R(#D@4 zaB+-f9_FNJY|x#9whbVERJMuA*-i0Kuo(mh-6qbb3)@J*>@QUWm9*5(|H&zIam$8Q z=7a3cnz}WxN6QE1yANl&BXwU!kjR2L;@vjRB~OZ-{sQAT;f3f@F@bFpN~_Je zGdN4)`QP-2bKe#kbDzvERld4t5=eiqLs^FPw}V`R{-BM~tpt*vLB4*@U&nxKhXz@P zvYhZZfX%MUoobXRM0y=sYkV^avlI!YCAWz)2H40Gdd%7JR5BR!LX`j)b5L%ZpSG*s zz(6J%wBx=-1Vo^M!hzD#s@#-EvvWYyIbBt874B% zOz}K^53}#~FX;gBrQyPU>nXG1IpD$Lz)xrXM3j{GeN|h8VxvY<)21|m@0%SaO zSNu*Y!FbWja$Wq5Y>L2+b*I|#wyq!(knmAi1xv+v_Qy_{2R=VN}D@OMxwpR$N;L{8p1j9Ou;#8|B zg@bXEYwitLWJ-y?=tzmdx?3V+lLyJYhe`5s89BSu&W)rVRJZLFcEAzM5_G%RkSCcQ1tj@GU{SV#2v_(z#+9t+agckx z@lplCtOo|t;bA4<#PLLIad$27)k~oEl15$b%jp5xPo;8!(vUg>$CnH`lcIo9Zh(OH zn6vTx^IkT69ME%AE8c%NZMS#CE)i`^ldw*UT zixEJ^x{xF;g%Q954Ag4deyl4lAKm= z%(8d!r+w>}>zKw%umT848m0HKk(#LV27Hyde(A4@jon#zvMrup=HjQ$G(3-H!*Hh1 zLDPG@ws{knBYi}QKHrD5zk&nv(p($Rdi*{4E~?c2pSJeoyYD|>Gj&-dP~&a5kPF= zulq&acriU7kK`F-qSH;aNI9 zbBMHW@C@Rr2(*9|UPhwef1PIIGIr6u4aRsrC(>3$LcQI7cmxiN4ax&rRy-vf6el-1 zqG|FgYTEumEIWx>?=^DwATY!`6lJ+E3jU&t&PU(Zjk18eq$z;mYk*hYdeTK!knk5L zbMP*%8-*{0DOB2qy8UmLY3>m;f3LZ>)-jBQcBH`kZReZhdYAf01i#9qWy)xiWPUOh_3kqbx1%#MyVdzL9>NrMMVSbl zZ)#5@BE@$f=DUEstLex9zG8v_{&Lb3>H9XGj}w++KH(=c|K`{Of)nhktCz`?;8vpX zzFN->%avdS0JLJH^~)Mvp2h}~?8oZ~PljXvR!B%CouKZ1C-rm|U*66Zw2z(aNYJY*vo^(r*1WNv^tPuGDIMb3!hdsVqG zj#y4J11O_>rP8SUog|$|_mZ7bCn?JhoX9>@YITpk-kxq!bNKc9Dc5~(+uGQ8k|wT}GDr^G=Tx)O zOj_(2fG9rLz#wWh^c|!nT$lU0XPnTp7p}qSDZAVaP#U@ku5AStW-S-ddRAqj(=AkV zR2ub1!_+p#DLFJdmCAA=J?U-SWtk# zKq%=_i@#4_Pk1srS9kl1zqs1|iD+E77!`D@ZJ#Z5c4M$_bBl53w#Cq@ zc=aW!*j0SMW7KVyksQJS~E9Yk>j-m|E z*}%`juEghl;%A3{n1KCh0+EKaayzNHwQ`>+-R|m_PJu7jfKQ2GPn&O=lzdCLMJgYE z)u!U#ty0!dN%7LU)Tf!Jr;2Blj~m4+Q7;okT!>6MU73mX62SSg@SvOEqrvi ztFM4QgXv|nY~JPtxZ2^$@)+X*@~=W)kgsZW66QWiUVU3V;XQ)zzLey+obvxxh-V{I z2nZ{2sYJfnTwYKBe%MK)3uFyohxV4@%1SN)bRb9w_L)aGEsd%?#fl&s92@>YLyG?A|d;a zo8=lb1>t6qzV!Tc9`;@kPz}FWB^^Jli7(=bGUiB7Nr^KN&^(se_NsKO)EBKAbFOXD zoW~>zLS-n*4WBgUD+D{i>nh@O7w{+!2oFFDz}phYTQynl0#gQ!k}YOP8&1iPQykR; z7(@2-Gl$6lF++y~_yd%_h>{Z8v~o^UP=o@-cB}CLL>wkUI1hZ&Fi)^<&o;M*K^Zao ztCQyLPvIcN;aG@?*Yca%ggg6$mWML!t!rj2pjdeCc^f%xR^9@Z**tbqPNm*GC=epM z;8ot{H-o=*1<9OWUE+I?94G*@2-L<6<v8RR%KTCFbCw_|&AM*nFKR zjO|9aNaFv_O!h+d5cNg*(*xzC=xW$oi(pw%6LKm`93rmesgBBTW(vyc&V2!s6lrm~ z^6%K5iU`Hw*^A?kkALZgcg6)38Ex6Xi>;x~0fc1^8zhGSXf|Mv-;=(&W2IH~bc;7} zVoNX=tAp$%m|MGQ9}CefPeU;I^0{yD|FmidCcsFP z^cTP04!dC4@X0kbnEdvLbz3ywrW5*wJ$ek`Fl@FIq@^MZ)5b6ZEblP5rJ>_YLTTK~ z8X1(Nmud~k6#kxLOK`g!Eqgw2l6*ztC`^3(H7)$dqg#~^%ZFBa2m_pP?#OIJKXAuy zO+Mo;KVlI0#R7;2&?J2FzE5epPTPEDqLd_Rb$c$cWrC_h+Ii_1x*F=R73@RV@AP}1 zGyrvq)|IQ(9H6@b63{IdISa!^54i2hjdCpQz`cZ>Wk~9|L6|Z`KumK(;M!Iry>rbr zB-8H!eydp@P6-4^Hppg4ac|?7^n+V`zn)N#07y(DO#>l_;*?pF^@_bU%4_(q&i`mP zVh{BD&uIg2oWWImHaFu8Fh?-&j|_k<=J~08?is@}XCJ&1+ndG5&iJ1k7@PH{H6G1gKR!*wv2U?20mtpS#Hi5MEb>^#YS$61O{YTZfkhw;It|0h|F| zB7vY41DtZW1C^Ej&3jn}iJ~;a`DjMpN zwzlT70$oKqhX*KuVHyFAr1JO9F>p_spLR`=3Rghfnz@L#B*}om@tjKUvNBO6n-w`| z3_2#FvB?5X@PD55q;Q4?m+p6Z{S6?HZmQB0-G$M83OBoI?((WI@dYRjK%&BS%3Je? z{363|PssZ!BE&H})`U&Ukbdeay$1v_Kv=5={O(pZ7cTg?Dy$uB(Q_T$O0;HS$gyAl z+$4gf@ds2jm50xKm_Rgwv-J2>!(s7zls2l=xdb%}UC;J=J^d2>i6qZlo=a`Ai5~7N zZalY)=PW5e1$9}&2mlmJlfZS?gQQJ-NwN4&tInfv(gwX@Cl20(75jYPqWE>F`ft)_ z5O%lwCVcwG`arZ(I)?)T={YpdO;yV9-@bY1{LuL!YOZK>qUK=K`ti0|dbEN)1y*Di zni$&~Zy*Fr-TEJl7a%i-0hn`wEAR-6E4?H>&!d<20^f!ye;vaPUk4QbkZW+Ip|66i zbogJBV+kX@xriYUjsGl*%Wm&}LDsqscKx?;PypHvsOeD6 zCb!gF@cdF}+_9NW`ZDE5n_MQxk6}M-scMH^^-J}I!!DrF9#l zbIv_osqZ=5%b=3kbO{wxy%Z14cAOPmFX@FF?zWQ5Kfh|mLZ%UJ~&=5eAJ zw|LRDEf0ZE=Y~1I3ObCXe*2MRDobvvAUgkpLHX;kJ+_KgJbMCayd9t}P&RF?pK=!l z;r8)IJ4+KC?cM3pw0fXs4YvMTH893^ZbHHRw@B|6H(+0G|8KRWEiuW8)(*Y2o_7E{ z7x>wpMZt;y>9w&UC`2KbMePB+o8>xc^J6!0#U}4E@^B^_fU2R(^Kuq$Bei&34Kv%t zQ`gr%{p>&aOoir+qk(NEiZO>eO6_WL}KK0wYE9`{Zyk@Fzq zLE5#UB)$1yRsF8a6#dR>2e?RqAXL$q94{$otnMhw+UZlNj9|lvKRx$sx%_Xm29~UB z)P+fek;BW%?bN)vt_TR?Pn$t3aCSirjEFlBh0EM#Ql#ngZJ0px2aXP-K@zmcObg*l8$GUCCVi6@2qo&zpwMa0?t8B zAr8KiD{XM5G@B-81Xd-cj-@X+>Fqw4r7tt_0YORt~ zt$2O2zwGOUt>SxU9gHEjkB$I8m>oed_lqPCPOPbU`Tk>mKq~T`bhm&oL;1dwC6n=xti+|poAuEp&0~9jXxK`rTVsz8i0Q8_ zW9EGm9Q+|C3pROytTcxux5cE2tIO{{)NHK+&YD;Dxr>%_BCi-v8%YcHtfVoHtZj=qEhg*qP;BCs}X9i8uLJ{9NHmX`q9N!z^ZxEohoeLlcgacRVel zSe_8!S}n+Ish1KzAeC~pdZcRT2w43ov2~iTN?2_AkY|zcAUKt8`X0x4KacW?*bB!` z!~9_1Up{^~v8gt>=>xJF*w`AQq*HhWax(*rb*?ptl_wk~be=G351jW|N(6E2>aM$+!m&F1*BTs%yu=#^^#xNsu}DJu6Ud)qy;y9?nqc zuAsH-7^a*yflb86v|Z<{_Kr#XPI)Z7>4%)==I zN6QnPK%g`*(Zj+gIJkpNo_TgLQT^||C*;kggM((uXK^(u=O|Hk6S1qKV5K(cd^D|? zxaAu;efv@S5Y2C)HqN(Fq{_WC1hCVH-%tVA7CG-bW%3zfCW@-ndv%7u^ygnyF#-N6 z_KZs$X1wCs=^wEI-)h-K=hb*C(;^~|q%i#05Cr6PV+fF?J}<05XxzWe@cVe)B?QEt z5yzo%h2@%pp7w7CqMd|4f9coIm*UA!4(|0bu%}@M(iw&$s#e#GpVw{6+eZyG;>oG9Nu$SqgEIsvW zaD%jfY{5NNP~X(P?0(gL6)=lSYIZLy(3m|Y3Oh~{NYOhHB8S660N=}k zL+nx7-^;YvkS}&XAC$vft9Em5XJSeD`_kH4ASzB!RS>4H+RAaCDN}L^!J`AO|AC%D zDx4kK0%G=aD5~L+vVh9%dy1#eFD;z+NAJA9y|j=aleIy11nCe7i*|Hz2_BoG;TI0k z{l`bvui%ZYrV|bR?TvU*mRbq6e^wpJUBJ!%-RZol@$K3(Z>IV<4xZy-NT>5|=vBl( zuo~ZyF>%1opO~e_b`aelp1iDhi*Hn-S$@aUf}Ulq{)RHzB^UYW%N_JRm$~9DQ5^?) ztL%gVMn*DUn8E-Dm89WuaU+kmsW$@VfJ0K(C<^VCEkUm%Ekzw@DV0`;4i^5XD0*DS*{dT0#_lPYw( z4?S|)xY!xU`LA9l2RvPJ9oWGHKt2GuR@L@mo8VeH{`D*#9-S~!(M!(0jt%479tx%} zO)r&b3UoH56&wm3Seh0mGr*bh=^LaV>{*eVgredqWOuy2WkLsb)J9jc_oay?!v*Al zu+BHk6yp1o|86YX>E1L8Y=mgDe;K#D#ivRhT$Jv=E$cnKqBrCNv%nwMmq(}0jPmD; zk=C^sm=&98lPlwIsGE4w_F->voc0Bidrstl`R$_5sr&QX&X&y>P78)BfnQn6DkTiC zIPJ%hnPd5JZc4irlLR;8V~d3-7eRSHICu>G^|`QsTdXXyN$0|TZTr-R-w zKjIn@IzLC5$2sC_q#f&zT?WUc*0LJB>-4Vp?6}l(z%-KetgjK~Bn({3F(M>rqfZ&6KTZ!z5dpHHpo$XVIb`<5=9Y{dK4xu5#YB8Je6-$v&*U z)u`kZk4i+@xySz3GdVTMSSkNsk+Z=mdM0Em zY5yoaB0i;lXi2Dm)-SSr9T3JQklEquHnA|-8&i_6@|js=leV58n_~FYWBxGc}Yu^TQN=!hO3=uUyftoBe^t&Q7<4r<)~PWvBoa*p#_5!4+HBDZB`Hg`H5VZb8k zDL25TJWNU3t(iQShR|d5t9ZB!PPqNZkem!;R@mXGw`2?TO_x4Ta_okC&tFg9z8dh! zG?b|>)pR4BHsZ!U+EOpqsV=PTVt-TxrW<5PbZ?3v5Avdt9@r4XPlsZHm3(OSzWPOv z@q;=3vI>=id}sZQ@>o7P<-Ug^6%>fHi*5oJjkCJDC*Nf$bw58~%bF86uavN?l(>hU zJvDlH(GAGru>X3}=m1}H?J3;Luca>K1rt$`pg0R7-qM6J!VQs31JtPEy2X;T@f(btT(|gUaceP` zYrgsVOxRF#Area7?^5Au95&s!A`Q*>9u4^L{493|%=@1FGRH~nKVkw)& z629_xWFZI0h4ondyWfhcW{v7+}n z89a{|_Og`0WSf0!aIrVzEkQ$YA67fMnt5+u?y_tIZ3Ksng<$yNgZ*zmqM;i5lYs}n zQK~Rz`Ega#i`?nt_N`F(Gk!X`nt5cGasA@3t=M@9#gYTGo6@3Q-P+Z6dkcms&*@4(o&WE^JDhuC%=NHgiXdjGye+WHua_iHwc1AP zn^n%ad}GG0zKdp29iT#}L`B)ZTm6YSdJhgJEUgb(ExB!gXbwZ%P$iF!s>qX&jY#h3 z?c7EO#i{iUYk{Ez?4$=$_wM7yC`+p#6J!)J4k@}KI0zRw({`UtVCp$*Qc-L>avt}t zu(R2X$(0ogN@%wWQ0(P*`SzMup`YrNj4}L_h%d6YbaUvW-mi+!=UPLZ|L(Q4>f$sY zcJX<$%`T-NKwcCG5MP#Ek0#D|dYN!1syd-Fpq-7cR zwy;!>7~LoByg+H{Fg*V?=xj^9wne#vHf1kSz|LJw%9M1LN~5PANE6xABz{ItZb#UX zool|mAwa^yAfIiyp5U-K|DW>lR!gQ1q&xL4swH+-V7#BV$$4}9Ql^1TJSIT?!K7ai1<~KC@A}=c$S7cY+@U}gtO4Q zw?(tSRvQ2EJE{5J-WM6@ZWEh(_B12=@)zU+#iDqZ$BM%4O@B`2GZ;ujQ1ziw+uU38 zOxG44B&HXdFsO%budeebYqxP)6|e0cyZ0>Q%FsvN;5d1)Jb7|P^Mj6V5gFgmGE~|3 zYIQmc85I_@bn{G}!yg-CG(04IKYm>fZ{mgbR+(sBTLS+!0fAbKTrog40DA5Q)33tx z*7b09NF(zYN<03o{A!xhJXc^*Y$z4>CZ%e)-)k(z3^8dEk7t$e3{c-PVwK|j@#K#~ zjW3)d?9n?7Ct&N zeD2Kx79$s3cDbTF(*5AC3mLHN z#A>l+$g3YMioOVdi*{d>xhQ01N|vm3srmL+RYJZ!#uVuHI7~JIUjsc*1C?$Ei<;95*dD zuhg#w|7Z9O@?CER-wYqDvqz|DKQyK2@-(|Qp7j|9jxtO;tG1s!LTZUqx!mn0_VSjl<dY5th;o(DJ?>xVN9lrnJtkNq%!zbGPwlvEOu_J&-pB$H(Mq{j zi|$U3=p(-4&oK{~f-0+15rexD!(rF2?yC(?WdGRVgoDpKk+tN{)Er9FVK{gA{sZzo zWogLv_8CIsL>G+1pc@re`UpdCI@#H0sZ3J0qBE+8T$~<^XYrG;9P{UI25orwS}H?s zJyoBmy~stiM9%Nt=-L+m`S4|9ZfNx2E?gn_Q~XF);_ZUXf_;B=*QR%rf2}O0y_2OX z{q)@vdaXO+U^qP|#?Yd*>qNHb10A zUhhw#tNDp)3;1m@q;W)CSP}TP5Wc{%$Gr3f@?G)CoB3dmZ&7Ngb*JHry}LVsh|}J0 z^H>)OY=ey&+_|{rHz91TcYE7JkUzb!BO}-G+5iZ0XE{zUeCR@Qic@|S-E2T&`NasdCq`SMjLmDNd zyIX1~>29SvL_oTv8>A7xHJ z0-1u%=y!A$+M*+5TqftT$SCbm-d?KrmbZ$tjc?QFK$0H zB2bFA*_qGbLi=H5w22xKe(Cu80W=kF_TlXiwSVQOM&-yhqjd61K5(*lUhJwvgZTmYPyyCmzVT|$fOvnM|6}74b}8>x0r$| zE0=2USnx$HIbgosCWM*e5%Lwema_|N?j$#u6?990q158%+R^|Ql} z!Gcu5`EQYouD58fSBJ20e8eQZ2}e&~!$&M$Vb>e5m ziYJeY*b+J`!kpM|gl4fA!$W$$kmWL?xOj0d#-#21v-s)N>)Xl}45f^?7ro|?B9!QD zaXsF-vqZryqptdy^GCz$cd>(I0s@L|j^8Qo2FJgNMnIj~sjPc<$YoozFMGUtop{NlW1`k#w=RvU)6o}?+$ij;=sxAH%%OfX79&y7=Gzt(RAO8k;lPt}1d?GG@ObKwN#6u}$m_o&RO4m#1Sq?^C!*6;h1o{&TZMXC(U4#Qg7`WIUh0&wM?=|DjO|M}jz9 zX;K{0tubZRsVH;8kw~bd(iM55kYl0JOLMI!mi&!K%Ec{}oW`SOY$=~($QV7~$%>eL z4a%6SXL%1`CtyQKd@0C`)5l~SS`|jylf%y*gCnXK;H}|hVT11O(J-dhx-v!}1=Tk7v>PCP?#@P2&B5;m{Q}Eu8WvL9rOOyH zCMW*}eR=x|chW#iD61=f3D{hXj5n`d^B#*9(`e_5^uOs7Z@=TN;CWNveCyl4iGlIC zIs&cnhgtKR)FA;9xPz6hAJ zW9J`c8)0x2$|+(I(OCKF@xd;ZDO?}g4yX!wl>g2zSb|OgEKDa9qh3s8y@E_HAC+9x zf_D_IwZh`SU(A*GbML4wT_ZX%L6E}wdm`dsC*IvBy=*)<>=)#RZgX9;g(vC zO)>)8DlHi9hdhGu&n<`$!KJe_#v4kz*T`=Fk?^E@r`nU9K#2Vr@v!v$bEGPf-_Rv; z*!Q}3*_p3feb6Y=Qm)K#^ufjL{x$h_JDU#Yv6YxK^o)HcuAOjM!=M?b1P(}5*K4kw zDu(-z(|o+UL?Ms!RO!P-1>8=9FKicGG4*TX@`X(JedK66c~@78VfoIlbsqwrF;03$9YD%JOD>f!}q5wDTSSO;B+Q)V;@x_B!|xpSJo7EQ);vC6}H$hT6rvU9yr>oPm= zoNj8jc$KA9z4UO+9xj5&TA#l*0HVEccwyh#e?>r_#o0_ne6wi|dkBTTek+lz^}x>C z$-tuvB@M3WG9Nc8(AU`^FMSzASI-`5MN<-uR%9RB7TlqRC|tCoRgtp40G*h^8@&PN{uSiAYh68M=~C77 zew54@Gk2p9nkl?FhZFWm?v<|AGOJ85q$)@$J&darTylEW(+Duizi{?W=QPF?d36P&Ji9Q+mdYt%^ zo*5s1x2K3E)k^YJSYo6UldW<>QrjsCsWAf2p8t9DB>PPT%S}|5MZXzHtZH4oxH}$_ zB989SDg{88zEGH0}Yrzw%S_kCOkVm|&p^~4m>pq6f zzcBc$rHvDbmFCu^nw~EYuEO+&gse22bq?>q)%7iM?&PQVW~fCnEH7rt~{VZY>P)8P|0jzPV59nv%F?ZxaFdybqt zVX{U+8Vau3X^aOGY89_V+XpoQBLoTf~1M@j8Cipj7sBb?NVY*5f6zr7EAUPHE)YQ{|8?r$J5tHZKjR4U{ zx7_lTP7QK15ZAv_L&TOL?rUzBzkl9+up3<}io{HQ+e^IEtYR-h^`$Qjyskd?-cO-V z=cv%Xs|Moh54@MOIy!V(M@l*+B#0vPZkShddS0qS!i+BptO0N&2#!|{FavOiTxWNG zJAyRa%|QD8EiG@K!nbuF;W~zKiK$lT^Sbb%$#&$7hZ;}^?YU9#Ln(n3(J5KBQPsQr zVQ^S*Nbl0LbM$IZhaGM;8pMNo8NNe-qN1K#7nYwpCBMF=XfD5E6;fhS^uK>B0=zdo z69gZiP4F!EXD!Rej{dtXkuZvVVR;?=V+M9bH2>WeeWQ?L)+gpYz!AVVKVy2>sht2& z42|>~wxdYf1Wr5+yMP+?v2A$=uwXQLjkiIoV}q45rx$aTFo~_^+`d!-_51NX8#>&J z&fRAKe+ufmjC2ssQB(ry1P9q{9dSgWZ6C!Asonps{PM3@ESXA17iVQC*70TG4yJ4s zD{6d~`rxX{Hg!bAeLRb3xgVJv$s>dxp|~y({sMs(UbKu7C6_u?QX6xshQMQ-zxbst zh{8^M65~R@wBMwW2&8BG9|X)?67QQhQq}=qa>R=d%#T>_d-qHCugC1710RN`C6Z<7OHbXQA@98F zCUorn)r|#I+vtrB71+%ass-HFmeoEq5+3iquh(&`HLueXB9|Q~RIwdR3^Irf1zRX; zYfwR!io~+|+%iuB0R4)L%b9CTy1)_~y*e-xrmq^UDPA?S;8` ze9v2vf$vqH)oDMTscXh4*^oyA5@lu7~TqVoi)I3s(+fIduh zj$_e_1-X6pB-=ZcT(MA9gcgW3wWyNW6gJh4_PyD476p0ij6>ts z8?4Lh2WM+X&;b&Y->QSyvE^>p5_DJayy&<+5q^oe_+fysJiC_szecA9K2u5Cp#cbj zH(0NDtwOOpiC*pdF@>vwQ1j~)oTct@!h1+V>xb!KipeAN#+QZm%o|1e0UQb*W#pn3>*5aIAu{puOjToz3V!ju~gcnXI!H!xltZew4qNudFg4z1;`%bS$CDpFYG+11mee1aOOS2=0kb z5J?yOtniA%$`fm;dd*yu9|)RP3T3LLqD?i6SZ|oE7=PArAUKlCREJJYs#66IEm=FH z=qXeTAKVQB0?cQk%^KK?X<2)189~sIV;B}tgZS87$ol`Y0J8l_bqG{5Hv=6Gh@tU= zjssIy?9=ljZ_A~!1YrTi0i{1XN=eomMA}b0S*!l}6@Dds*p^{{882T)A-4lTImxul zE@BDxsAi{Wi=Yg~g6wzc?(v~>N<{4poMj;;GVz+B{#UMEc13+iUA$<>NT&pNL|_TD z032Wqag_|`z^H*v6(a%=J;HV{2;C_xq{OwZK7fED2uA6@FA*QWu2}9(KpewqjEK1O zs9%7kCFqS)R7LO+TUU;Ms^%JQLu1Jj2*#JPA4rf#D)z$)crAat|97tlWio`M%kesL zfzRS7CGSy=D=Yl_{5BP%2QX@F zwGd=s^1VUa{8hYqZ7988hN>h0lD)}?JeGY0@WlSgj`#PE|Vsjssr!NQs3Xq-L;@xsx3Fl zkL^&fuuLkM4NLi!>k`OmLMfS(>B?2hL3N)kX`D^APZ~;*T$L1;X8$8axVB&k$3JZB z1&LMl#d_W&_%dS26^eFs0ve0!7E0P016}^`t6g&u3K`|DL4Hdt`lwerVAz?5@T?p5 zJ!kd`i?z`@+>NKnV%kDP_cX`>caeDITFB+AvKDMv%ZoJfoLNA_iKLP?Ll*);33^y% zeGN_kQp{7K7LZ}}cBYV+Z{wBt3~5Nn zoYC)KQ}bolZ7=Iewr&bZfOKB4}Nw_EiN8E#i2DPb382p{TMHYxAO6AdfQu5dSSh3Zf#I$kartdS16*7Q-1|lZld?eiP2FGQ`7-DI zW=CH-XQD;~mk7U6{9u!j28s8rzkJxL!RIID5l5 zRI1!uLbJ(TSdt_|ES5ZYq*1!=Vn#9CoWu(cmdzxxmQ_;e4X&K)Z8(YmmXONQqzr*2 z+ec8kV132BC0sBoyf&Y$_d$+h zO#(Mq_3D2mO;auVFKB?o0^?aD3}jgwl_OD#r=H?|XJQY9`R8CR%k~%-*q~vY9s(>A z03K5SpPZfV<%5gLxF+XZba2XG@Ll$dTbH zsdr@q_2cRCpc~c=uUskK$s5}R~&He8Qro*dX>s|82qqk+J9{|K% z`}C?Z?cvNmOjrnT2uJgP^xpCHLJWCObhbh8eks8=uiaNRB~69gwP(WR5>w1@7I8#P zXiP>3?|_3j!So5#`R#M?gSc<}R&Elyxu!Id4t9O!xALc7>IUIiS;DWyvuXAZtoxa ze!OO+vrK~BIfWlNCU`pHOxOvQI=J@D=`U?s>ZDks@4$NbDpr6IV5NHDgP7jJv!!6? zXg%qW;Fs_J(Dv*x*${2E8?5p|s#*YfgbsAnzBKpN4_|Iji%#6G6nM z4Z|RsmE5Ed+Np~x`>^i?DJa6h*)gw_2!%wR+&^l=fD;4S(pVb$pX;2jF)Ofnsso|{ zarL*6LeWrhA&kwwvs8*$-cWV>z$6Y5AO^dJhtr5bc9c_pQH3xzC9ft8@|u$msl?mp z4*O0-{Hr>koS=-%`0_6ho?e`mb&CmnT9*MCQ@3{aSZy9wY#sqd-VPC23t=DgFLKxY zzt)45O8IYSTOznyl-;nbb?*Se4ZcGtQgMllYE9faKVdHq=si&NVJs2BA6e|Yzv$y0 zWV{UEoxRI^()1O48Vuav0bD7%-&+W5w5}(Rd5ZCRkM{iNxEYo#@NJs2rFfa_;g4;w>wc`o#c1f)uoqhd^u}QBuS9f=RlY z$ZI~cOzUx-THdb>@NGwWaf@W6!F+u-LU<1YBQYHXs@6SEaNhPPpPj1^Sh*cl@Po;D18o?7m;w1H`8B#HbZ_PJfdhUAlxd&?`dqXw`E}gkCEBwZD!Y$mk?iQslE@^9%=y}ZQ{h0k7!r^*OLg9KYjT%bLt0L1>gArU$eSdnrCGk z#w~L?5zp996-X7>j1ZHDqxI@8_)ucK6E?!hA1(4BoUr3uLH0eIV@Ygwy*LijE}1lD z*S93gZ9ZptYp1&Mc-tgTNcz43|Fkt$*Mnp75cQ!RRGE^Eg(Vi`Rhsdy6H3OYxPNvV zvBKTe+PcbCj<>rXG}34MSvlcCjeqlXLRM#Xnh1dQ10>MqfGr-Z0Z7#vS-2sZv5V+t z3ItR4TWqD8GW*&35sWm1)1CcuqwZsHCQZJ*awaW`05&BxJQq;M&j-g~i0VEws)0F| z?^UI%SB?5oLT!NKvi0o>!BuLfxT&^D@qRcc_~L-1vVaSEWFnq2jp=<4UP; z^skTZwEWt}#@h6ppSB78ht-9mW?Ggf+ZmnOH=j004!ns;#0IU-MG)y8fP(oCD8WC6 zqcuy9exDmekv~w2qMCk28{ICW(oe1J$6$(R zxOH1chAeN|wqnW40d}}a<3x5NMCCK5Q|4~G0wF%;X;$38%U_9vfEogjs(P(V9{6G+ zKIt>@^MAnxUkQM3;88b@Z2Kuv5meHN)XB48P!AQ;Krw*_L~1Q#`5c)`?AL!p4!rI^ zai*Xs@*n%*0In`9YbD%H_luQ@nE%Mv+1?Q4ienZs7!;zGWa^uX)qHW*;tHsyF}S zHWy9FkXQFQIJ%$}f8tpN0o(8aZr_9mWU=|Wd%C!08rmSj`A30AxAaw(K(g%*f>BSx zim_8roKll={|!bC%VX#t*HMD88+fp^4JxGa*vjuP%j}xb1AvQm(Mstt2t2I=n^Hv= zgL>iKiQc5GPBf-+Pplh0$RXLFyIr5E`C_ooNG|(DXFKGjuTawB+!!}^Sqk!^VBcBl zP7sM#hsMV#1+e6Bj6!4Zv%)-vfujn~4l#Mx<(s^^JWH?hFcP9F@pE&EsA@oV*Al@gQ3_h&}_-ynoU z$wLIR{=g?NxyYe0ePtZh4KpS{_(gwUlY}V<3#6pVH1t-_3B%G?uN4f9gF$G3H8HY= zx#l_-QEreauqCtuVfpDD7ePN#2{`JY?x9YMd;Irj@_CfgES?qQM8XfrjI);@*aK%d z*U*AaB@M8&cS7OYpJ-tm34Or2z>YfQehHtug=Ys+cqrMi{uN;*!NaGmhJfrQ7fp(A zU_{u;E;@W_NLEAC^v~(sgF(Tb8CvrAA_g24Jbyp~*Bi)Y)AatDsSQK_l$#$)9)x0~ z`wjA7mKMS2A#(b9i7Asxl`7Q?uz`45_J=qrZu1?AakQ-$j~R5jkxm7mp2HiYu`G%T z*_^%a4j`+@68&Dj6}F)bJpke-n89Rv1T?jI_BqskcEP!{U|oX-$Ui&4{ZUa( zzhIe3gSB&?X6Y<-0V6oJGrErjW{mgY+GB4s$%kVYC9C<_@|+j_;X^ZBLq@N z)?Dy6wL~?8*O|Gj4V%%Sww)jUwnY<$WF5#D-d@wXMVq6CuUVChOAHUe%CezPaCOQI z%nhMg^`@3mc-}3Z^cic<7iWBl;cP(|v16RCQo-nHcchC^ z%X>r*p)-i!qN(Wrrl+GNP3^)0f$>y3=Dzd78!ZOkzr#GYLvllwW8suRRAJX21iCCP zv=PMzB2mn2PbYhxJ?-44LTQT-LC)!Xxagq}72R3$$k!5m3S!aV4+5k9vNZLXU9Hc{ zyyWYpV|Z=`6v8!L7iBjhL2L-%<8)?5K)LMK_ zrT6P>CP!&A`i7k}o-8ps5j6I>KZhgPFzo=6DU^xHp=4q(0UmYvEdC&%pdyr}UA|MV zU>Wqyj^~L7qsA!_HQXL8lY01Qo1qi{u0^~56|d=%?_7gK;QnkO=yK&wch%NNd&OrR z{$5T&@lF;?g*|5^Hq{q~dF=^TS|DX)GXWxWamoa|*BQcmFPU^g<4-yg-%VQ|0}5vf zt@xN1JqW=;Xn6JPy?2^N$#^h!Lt)5mR`x3urlh{7i48t93r!RNtnBjA;WY;S2@F2_ z77*qQI@bdlZ~mbW23%Tpcn?rk{A@7X!5@vNQ3t6NGP+0uqSGbNI|PI>`ml^rB?ubq zTd_jON(RzFJK-1IFpf9i#cx=kz?N`P;ZJj#4`=A@-!t?Na@CmrY2!;EQ2?lCq#}Ix z0s%DMaJI=BZMnukA2c7Y-#AF2odz*SPK)7M47wM5EYQD^@pi~w!5H=!4p4or2i@X8 zFzOpR`OyPLlf#dOwz*Bh9BR2#$d*SyfJ5)y0Hn(x?i`HvkUS+R%L$1Guo`G$gjQCk zZj)=QuTJqoz775oQVeB0`n*B#;e(>t!43hJCby#9d{D1`3&a&dAczqD}s$w%76_67r@1VoahuX174}@AcymGQc>>bPV1AFJZiXB zW@p1N?D&;T920C_XiYI*UlQEsLJq0zqG6H<#G=0on}bT}?WrF}figLAVcEhd_Fx4x z^BiUrsNmfJDOTyJXg@fKvU)Is>Gbkh6kL6I#*i6ChP4wME3Ht^$2Vl~bC6TR!QY33 zN!8R^25|;yKV}WWuoqQ=9BBsRZZ-w#Vu6+icK&#BNG`49G9CR|6Uuhw9D2o_ctm85 zti7Y6S_8h>OBC-wL=TL^EB<@efhmx46J{&$NN7ZoBkS3{Cuu7XCA)+$Hf}%CF2o|I zuDlBf^*5Tjkg-(Plcm z%(Z8b#F2FZ#imKZAfLskTbITJLNVY(fUN+Kh?kGJj6bct`JM5F)3_r6D#5+F%Z2~o9MRfEFZ)jko`e6A680kHQAGQEV2sy)lNtZW_CJOA8JGw~ z3JmbVPxkVYD-0H_k0sGx;4mLx_fWEYA?CkwNyW#o;=odjq69GJC zNH|;!R^HQo3?$Dum||OjK|rJE0Nh}Z>RRq$=rKxQQ^NHUuOcP{!m8jY6HSKw zfKCgH9NDt(bY}9=|M>us4O58V6k~igiCiUb@l$Gdc^KJQhbfdTd~6@3Qql<9`3MAU zW;pp+vwlRXoriWA>y$ziV!?a#)UcXL7~Kv7hwz9yk7~hArRcv3OUhCD8Y1sIEaXZkctuf9(s=te;R&)t5NZN=oSj| zB9#4-pg6$>A|HhDXxeh45^P?`XujHsUf~{Y{!^+sJ~v)ZX@Gg}!&|Z!@1IkPXVGgM zaUUI~tQ(x?wuZI(G!Ln-bM~=VspWJ?KIR(IJ^CfNu3Eq4I-2;zY?SoB_jm69Xs!Mv zQqlWbsz9#VV&7e-7QRWy&?AH7-=lAWb>^m51wH?>SOmibXJ&DC1!vynhqou4MWEqm z>C&AiVJ*X9#ptaq3b5=G6fMdwE07xP_1h4sT)dN`hc9c?TWz8U$R9vB#h8FjQCQ>g zdwunFFf2})76mP<8jRy9+VaMRrkyMJaL+I0Q~LaI~{hRX347Yo#*GA ztM*T|CP!I!CAv*(XFivfj#<|}MGxq(yT0EVkwpb7I)h;mG#1v6n!v|U(>9?(W|CBl zI@W3J!Xj>pjM7>=Uwow~HL5%=E@pK@Z&wP&Z-p&yQbV0(_MNp#texvx=8USD;j(d; zWsHJ?8p8ly(vXH~fa&qr2!@&k`jjOX+HZiT_`}aaO&+0A6RAn6%e{@57gIL>m+XQ1 zzZunCk(XagHUc(+ubZty>U27+%uz*AsT*Q$l@j)1bv`(AcDJl1QnivVf3()0{f=qo zd&^orWBTuH(|nc~F7Wn(mZ7)=#`uLHgsQi*_8jeeQPiR#NfeYS#%vWhXb#(dodQg% zUZ5~0Qb`9c`=t|%*xf#LV@RP7ut*`Y-utkUgJ9Zb1Sva;loia;m(vZD1 zg(!1wj6`#toJm_u$nqV=(0u1N3(XNKDVp=9WAB8%eeBcc8oa2#y>9Zbi8YEyX)5?@ z;d7LLFE`CJR{1$$a(A*wx_U4jQ{T4`*gND&41L3h#$i<>HOeP76Lg{bJ9_oao6Oj! z)!W?c@p_~t@^8%)oka^C5>bBI{y00cef)`Ew`jL)lISE(D?cY`Kn7UEej5^(~c7zj?ldflT@9MpNb6Z$g zI5ukW4jWinh9)IFo%g!3ns?i#Kf1Uk!KSmcv^+TT{&VJWbFpq|ZM}STLvqbupcq_f zGHidiEaG)JmhqPmr$6hC_+iQA(KP*Y!CKK(mgJlBZA+ot$cc-?LsV}!`_SH0G5*}_ zVE;`4nsBTpCgXbcMV*p;+q~(Qp_mjE+!6*)hX#Lg@uanX!%!I&T?kS2`mr^PVbEpi z8-u0RW~Xy8MP; zac9OVARLS7*UJ(sr^JDo7DZIiS)jzhyZJ!;RmBv#wwC|WhF%VwN5Id6GkD{R+a zZ}uMg>br>y+ybi%8@ci5JpHI$ZuNX{mtnG&XSU?jMRt44i!+Ju;I*Wb9*c>+{_NW7 zOV>3CGh@_={JhxQpdibopbNbL?a9?k@4dPkGKnW~z4ME6HS|T{ z_q&3e-zgj@Z0O_U3?DFt)>qCLRP!%(IR~T@JZGLZW73gUc-;BE4QoWnKb71LCM!yQ zUB3R@uk5af?_$u{ukP@l!{f3kds7TgYbAF=>KxT){kCr0{ZpHSxm6#GgY43u^UClJ zXOEhWSl2#ikE9AlX^&Jc>YSLsEmqzpAmR|i)Qw%VUG2ju(QfV=FH$k9R{v?wWnWJy zhyA3R`NS=)KY{TTK3bjQCMIL^FG$RGdo8#$gyVc!uo)E#iZc6G4N1hPQB)v)=pIKFP{*=h8r1X(4`sa~q(88!C7O6925C1A1Iyv(cGzJOJ&Rx{gW#9tk_> z!VC`|G3&h@XDu|P7Y1+e#|$VaAg+FMB0Nb%k=Cxw2Hs3L*(VY*h3RU1>C1b^l*9!4?n7_oy<#|@b=E2 z73{(md)USSVtnvtUDNCifBP<7L?o$&c`VDlezuUXwaBh7d9Tn+(e8b}B0gymb=Vl& za2>!_kX#kDYCVdFJ;(w2c(E`exspsQFe^}#L9|@e(AfB+bK@Yq5o%gmp35t>Ggdb% z0yne`v1GsMSchq~i}#*jnE%VKZgo_Y(#UIyW_ukd zufG0vbQ!|vUUtsWsku=sild05C%aBs^NlL&W8=8h=>c?NhEj2rV_Y}a-_J8?l>d2) zdElFPI%3kiDZ5*DM^2Kt*umUfvFv#$#6q&D6)k-`?UUm!H#`lBc5Q8HyIm#>cOLV# zJyJrXGIKIpkEzqd{KKP_Fn$?1@m!bGqC!^sRXm%>*S42Ed92bht8WQtRztt?)B=hi zO!j@BPOyB=+(?w5lbX`O&PJyt_-57xbZvX2Hrql8MEUTgGJ#NuWmSF4+)|fv0<@B| z*RX^-nyl}sXO$|i&SHn(VAShi4{?Efd>o6ewz0=u4+cLBr{)LFzcp?un(Oe!z)*l)ehl3I${3XEp!$99d}TOTeq)kA8qz^5&wE0>jq)g4O( zZ#QWgNMo2oQd0-yck6ibPc9srJE3kBg}uZ6t#{xI_eGe&*`An~P}R_I&su>5@e1d6bGG<|1T*#&%{to;5Ahgm*9M-7 z=VBHX>Z{@Uzy9ETndhzV>Y4;S2L3E5JslxS<*}KVjL@55H!v`uV_@(q&=mwME+Qz0 zt*9i)^}y%g{1odSA1dR<-3ra^<6iR++7v5x4WYNX6gGC|H2u&p8Ha!3_lG-Q1YZP| zuHSj$u9c9&qXk7=)#edseg+0lwUg*=jcG)=3_cN8gi&kbm(SV#Fbz|`{m)OyFyLkyCT@*Sd}yjR zXl9Q*)rB&OQiv=SR!_3Y>E?*ZZxi}tY^%u`Jyi+_N(~|vgAIuk60`HWm8Z5s4}nH3f1Tvz$S>tO?)C_r*2s+LPJ4=ay9-*HoF?_p0vQf?Ki* zcS8zNJ5?v1pOw)kCemU0wdcwg$kN22-`v2)+Vd6}iWt|dl`cEU(#dS)qVno$5&Vx$ zH<#OgciHOe-)_gp*^Cn>A4=otPUCrZep2c^I6eKW)_RUKP5O@@Op5C_P5x-4X2|l? zQL9QE6F}v$Phw*upO<^<*TysR&+PX)9)Q^r9%kaQt(w%z$@js^+%y>fjJNOup8dPp z;kYpN+QPYjO;lUU*|k#bbp4vdHcr3Bk>=;d&0L1n%s>oG3ni(GUl|jGPTflq8d+X! zR4#usH1HEDCuJyBjXL^Mq1;z6(EQG7v;;(3o)MWOPlTEEQguyO!?EcgGIh9R`JrAuIM^!G*@fm? zCIK(%f{h)^mO-67!9m8m)AQZm+4D&|Li8zXGH;9YVTkN?8X1}~c<00+wt=G{m@fwU4ePlZ;0DTX_U+H0mV+FR23S`|`NP?_TRwHpc~>1NRN%M>$lh8wgo zk%fyEcs7(|Z*y1BkyN>#>(ls{dyP##TY6Hb zuzPrD9s&pO*}oZ8nf5t}%L9t>A;%)x$pg{s--QzH9DMeZlU#VHR*Z3RAQ%IG=w{uM zXl3t6H&~xU2(Ygr-kGX2kKoN`wtJr^?sEzXPkiTO=bzrmfA?kzYm%e-o7W37)TgH0yLQPx$h3mq=ch8$uw>b03l{U=}0CT&(3on5lybuVN z(ERq_9x=&%eQ4m!&A>wx0B!kXE%N@!t?j`VfKaL6F`!~iu>wAv(hsiJ5AGEII3*wa zTAY%;(8u5A>z9X{+_y45vtMio#_)_23kkHbD*@hwcj(Bso)n8KB({Vw*_0EmBXpO0 z>%3BY%9ZQjOdV-S%^_A=qYQm+_?K3%8aK68u6{}m+Uio1 zZjbVeUgK^g`}cOGvV#L{CJjku>y+#&I*iZ=S zy#MgO+n>)J+Z@?++&H_sBWP<&H&er9w|u9@iiC_U5fQZAfEFNBQ(JqnksX14a~Z}l zuWN0+7d7m}Noaq(CS^U>&^egGVaEDQ=hCA!fB9&&UuO3b4A$Re3+yM;$^o>1mNje~ zfKgfC>!#2!N*Qx;#H=j0QAzcE>x?~J0q0}l%gXOn?$%}I8)=+^V zp|3U>q=v3ln)5}Ncjs~)JXlF=%C)nkYd0Kv?Raoy9(|{fjyZ{qi`lwTo?y}ciAC6p z{_3y!ICfhH)t>Tkqkoo6AlqT%9elPoUSBh=m$w3}z58`N+%hJlM)V|3zv7>1e_324 zxh8q@^Cy01=+J#yslEME{CiJmPj3C$?_-D4&JiA;IHaLyxuTW%{#;l0%OXSb-5)_8_PJrF1Vvv{GKDyUF+X@=>S z(4}>cC#Sn9r|1rZ0LsrFXNQDvtlQbdp91V&i=N+1d(Vqw{OM*_LLA{H;EF@Jm`D+tc$R&* zsyvgqeFv!8Oe3G0cF*tW_fV+n0}T3;vivh?HnJjup7TbLiP;j~pL9Cp-IoZtj1ZT@ zu9%{WH}WiCYagTNXW4X2mM`cS7(g30Klv0U#GhdTSV3#D-f0^*flA(7N{OJ8~GnFu?Eo4~MiU7yI6%csG^u**CG88IXMQ zpP`Zl&Rs~9puUj3?vGz6`J$MpY8*$*KnJ$bkv*lb&-U?O`(o1kv~2WQ?yZar`p1;L znJM=bVI-Hb>a$_pO;mTSh`7u5B?M)f+YUyRLS5f`)Sn>`p8{)GbR?q5B&~iC*9(a5 z1Tbqq_M|ofN+I9ynvgOF4M!+yw4*WaIZDJ^q*QkPT>Ez!@W-?R;*LAu9zW~n{y-XB{h z0)fydax+CUfuz-4EQ8dvp-ftqBR#rWuQaEbS9XW{y#lSzPwTyTzj)1)auv9nb*^l2wYL?ktIrC>~7HDc#Xi+LK zpXv=?5L^fxWlL(xjbc{97Q+-N_c?gfX*W=;QCnxfippU#Z|LA~Tpl4;pm^TjdMz=O zAt1w^(#h1)tYsQaCI^?wHf6lU84^~et1UdvmjY(%?Ecl7l*3+KOeicQtEiP zs*DvUXV7ly(PcXSwyuYddFR%j1B^dk8uweS6p^#ihTq^LLp>+O==+Qubx;QC`M(?r z0b_vSVO~rsKgXwdLsro?g|KD*crFDzQQ_e6IRErj+{)4F5$xPcU6zp!Jv@ry{pjpc zYVo)edNT-lqwj>ksbH~SPLqkMW<`s_ysQS?mxrhTrh$Z|&NF(&%%4{7+|lEmZgNCn z%(60<#9NGe>DnOurMeisx}|%4+X@DL53n<_`Zwif23K{6lHU4=V2Wb97>hAbWVzQ` zud+UG9pScMk~^_ZQql!OKxdisOJa=?k!}-t^eAmF{svpA#qn`yzPn{>CD5Mm_q%)6 zsAv26H>XSBQ|36uy;e6Vh}O%DHqMOIM?~tg*vLz6>qnA#P|iL_?E8Dv%rA4ivzYy{ zw;aIyRLF?b;%H?Bhk+h9saGRE*pW&{8VuQVT(M}X9H)6s^|tRgkV3xE&Tv~;f&cyE zZWL1c(TLTxDut3U8rN_CKbXbPZzd^)l)K#wstC5cie;)-z9E%aVik6tofBRLn<_QB zJTCQKCLM8Rt+EkpwNSphOU%3bM+?)_$TBh8>iwrYf|-wRkie$t!~PX$+VrWbfn;IM zkEUw|FOJtS)H!FHAoZ6<Wl>%FZZBW)r?1jwSrbxb zv)>iU07aj&i2i2flo)2Do-%!k9fby8S0%fA()o?*gJNhY$qbyUGd|$B=Np0{yEVRk z9g6eIxxI7%{^3uF8QzkV94_7<%AW^MnLerq1!&zr$1x6YVv>)@v@FK-zaZjNSlL!Z zXU0xIU??0b6qDAC7?k43Zy2lhD8RtP4oaY?gpoJh4@}WL*)`BVBFa>yW4};T0L@iB7N34BWKMp%>uj(6}t<3Dpd4#jArx#n-1;(Zf zG9bYF;rCZNY|3-F&=t{N&5?0#K%lc*+%s&d?5+c!i^J%=)LiCtHZ`i;`a0R41CH?^ z$b4o?{_+IFYH0?S^1)$yPZPjlnMw8Qtu+Se$1lW~(%Crq(pCp^znO9%NJ)0SV8a%H zCt}}Bj{x}si-VB|C0LKiA4oqgpK+Sn<%TQvTvosZDROD4D(bvZ7l_58dlH{LRWP8j zv2&x*FIu&IS1W3*v~nsP0pPZTfY*|USvYzTJf_N5TyECs5NcL?W+v1r8z=XHRG*gy zMMpH=D4dutSPwsy+bP(&@%NAJ9z&K?KKFP{hO@UJ7c$~XN}z$P52YD+c%&*9iL@?I zqTF8Qq)6yTR;O<7Ytebvbs|8!^_u8f-c@&Bi$$YH<9^#@)x-a`>%8gQ7%QGD$}giv zOrPvoF1s9(aZBeS3E+UlUJNj-8;^w*=0jbG-<*L#G+hgDnxl4zh z-dTDe0`nFgxyX`~HE}EsSto=c+JSD3p5eQ|2C0}zY-wpjE`C@ABj%73sZVDi`*vHE z^qcfK`o$8J+R}7;&wt*xtIi-47IIRN+HL$x2F8_*NZ7$z;o~3UCkCa7o2!D z4CT9I?)!iKey^$Z+v1PJLFnStXpA z8fK|u-4{1AIaGauuiJk9B-6u(^A3W!$f%%1;JbO(08mb6X9*P)6jb9+dRha>#O~j{ z&m^uDn6>d~wb3LNCfmiN}66qS%x!dL7!62K5u`?f0elJw`swA0PT8 zZu)e3P!Hz%V?tn++60X`{pQ{^Q`f_nd%IyJhez5OEqL6l4)D zyBDsA0#D%~N@y+-ecTB$slR>lW|Py1waWR5qf!?;8+e&OGVh-xejurk-+mRx0WVVz z;Ws0)AUT>ed2u^yZqZ>lj}+i*x;J_rQHSMeB#GVBQNGd8Hy-ipcC`7>xvS zy<+jgrPM07K zG+#v;k#beCG+Z3x0Fh|2j$Tt)-UA`%V$$79U8ni_7t_W8U0>W2^Pem9ZG=uy#MH}` zE`i#M+h^8gi6w@KVY1YSMfpH3b>!JOsA}hy&7C&;lc3v?z2V!|PObu@^=F&MEv+ay zjKh#3u&KvtGO8ja%u-n#{&Af9Ma z?^S21DPU13<5cgpty*S^dQ~pv57KZkk`7OqBE8DlS!JM20G|0cbW36pk>ARt;$lmX z*DB6mI%rXkU7)7LS2SCQJ&t!PPYtV)U%OW&Ho~^p#OnbzQg?7OB!8B`qzD zfOLa&gLH#*gNk%YH`1Y$w3JeZ5Rgu3IE1uFD@nJbv{jLaS^^oI|2U|J`5sgj2XT+^ z!mKhPvYkA0`$3ztiM(!yuviGH4!-Wdk|-T*5z~2|PjS}}?rrx8|N4uCgoH$|*((9; zCDl)3mNhze9^H(Kiz5~GOV7IeZIRCFxTKv`wimKQ4T1?7=h^K1HW0o(r_w=#!Cbz* z4c_k}wtt4Tf}W}JC3bNzMVis)xd6reUFN}pK3~HoyWVi!-Oc;rz$@XC?&H*(GzR^U3@B}>e=Dnb75g<#=&X;CH3%dxLbh<&a`}-UOL-kwfx^O%jVz&# zicI6gnYFn;&Hn`Ezo|mxqh;=`eq~PNQ7H>gt$<5LObfwln?eLc6b>V~t2%K}e3Z|~ z*{6-x#@aah1x`y_ow6;C#DkXIM?sXzxPTbIS--T76d)cc-u!x91}_4_vysB4q8 zzOU^s{j%H|BXj6YtF+;Z)z$ST%z#mYwWGPG5=NbU9*;B^=8bFU+Ugl214}%qClvX1B_hRU zPhe@GTw0#E!dP~;>ULk>IBKKgMV6iM*VQ5w>Fmk9y0{~tt4O#Y&%Mq~?y4t31tBG= zYN-5WRwyWoJ!dcA5_CPk+K(Q2|JYwh;w}5kH_Nz{d!U?ss0buB&rNqB!c=!tJV zY%cIUG7+!X7OpPOeGYo)m{?h>Dl5OgVuNRhD?xepMruE8q~y)P!^|TFbbs+!YU*!r z^|&`>|68fjUB|lVWyPAo$P6r9=3olhpJPKg;*jj5a$bPZGDIxm?w~2zN_3^ee!e0ITki7@wR;<95Y}s};5s z`&R+GBq&HW3nQc_oWVk>eBmnQqtnzjCAoRL?1^~q9VBpW{qbrJ@C;~f7St)lk6MNm zxb2n(DEC$TPRt;($CSe|)$Jv(Bdm|BzYhNrlMMY*z@kII7QFcQ>N_kmO-=B7@>Cdr zd#Ckt5MmUKSzk=}UlO!ltg^Bz#Hki4Iv%XZ;N#=}F!(9?Da3b-`P&I2}OC2&gs7=kW0bRy^ zj`p!VvzC?G50)bxAP0HDwgl3~zw=Z4H0C>R*maQ|;z63xE1+L>dSL0$blm%0{n%d9 z8%hndA?V6No+gf6ys~Xs#Wb|INy37O!oup7qo~P(^7hx!^SPqZ%ADwuV{FQMKh;v$ zXI4s0%uKCDYeBLzZ5S^EOwOQ{E&x-*JZ^xh>LZKb=5k8x@;=^z$_dI$8JKxg*1pkM z8L1R@MV4QfxLqN{`U0vN`q`z>@$Njb`FL&_E@8@Izz$GSQk?BB`l2Jqv_KXY1Q`n+fx#qA{$`;Gf3GsQ=?eHgE+ik1M0&NOib;ivsm z$+X5sUw*|GrAYQaqB|337xtc`%O}5oSlW6!wMnX)ntrGUO*;&71T{7j9(xPdb#-;Y zF?4ft1IMu4kyPkp;ildM+DA3U{b`%>%IfMr`S0GCSM>n12lib(v(0K31x7O*cma2? zizc>iiM%N(fj^dcfl++`*~j;c(_%1lb+XbVx!~T@xM`*Tmj!r17}Vh?UpKP~TN;8Y zWrnSOF%=7*bKlt+6YhhijC$aZTj5mrlS%NNKpU41UK%8L2FG>v3>;9Yu5aj|kA)|2 zvXb{WgFYt8Cnl>Y9`TQ-Vm{TfZp+D`d~mEE>(ddX2yPq~*ML0oWO0Z?DvUv8@cX$m zAp%gE8^R1$$yQXDA*k;FNL0R3aYqQ!YQKxfR`IL92Qe=zf$Q5w-vYd{jJ-y=cPeV- zC+A&WS>K1+1i>eb&70Q%j{^wZoh#2RhHTO!In7Ee&-*>@8?6NWgGs7)(zu{40kREn zafr%pTnr>xwzCHz0U;_YrAobT0mxUmVvlnh8*A%`pWOd~{dnh>*a+tu64~&2@y5u= zNJM1h2kLu3G3@JPVpl$Ri{77)yc9>9h=wp*Al(U!u|Nny1ow%|Lp<;J zBDRF8WY$7Yt6?E&HMX&#rE-sj`-7sf)VGvH5&I1_BOoo&f{X!4v{>pzY4%P~%Rn4X zPp+ustUQF9bQc|eD|wlgDmNCw!X-MROq!UPo!uBFJDV+WBdWrTy*2Q-2Xs~Tj!TUb ztV_QGxHuY*Bd9moE{0nTfJ1uzvy3`mGim~;h(amP*_gW+*lz|CW}gI2XKRa0F`-ip z>=dtLisuqFI+4fo{#nO!-qEOh3Mp3T^2tZ7g$(z(MV{7EOg358m$-pGLeX|JghAIM zRB$(s_%jqilf2iyw{YS}y3C6OL8ptqb**N0c9Gv-y(zE)8#0Pqc;HicOYe+BYk!(U zi|5PF6-vsuaEO8?Ny1|f;-jN}62)DhfKb|^9t(7Vc5vFGcGceL0Obk7O~?J=c9BrL!<`-rGk))0}>9=D98q~X)e&w zF;1=>DvRSw-isoKjjdgL1X*#tIesvYB$nnQvU^RLBR85lUFO;_sX(7>{w1`m9T(_C z3|Ga{T7`-8*;T0ZfRHJT(>Z!B1S+%Sd?Dl;1QX1Da6_2k5n+`=uT?-bJ7e-cqzz~% zdLW_*^5k*t6(0)@2g%B6zBI*CCV(jcl>Qw)|2iCh032<-Ix64y&ElE!2#`x+aoiZD zVd@iA9}8#;s;p$cR=K?3O&|JbS}Y3+hS0n%*!XeQ6F~1!x3z^;x&%5+FI@P%O|o!G z&+LV!P8lM`AVAtw!_pqLOSjS~j6ZE3kk`4N{&EViM!;~AFXKgRSN{Zr+w~Yg&JgdY zf``gOEl2S`Q)5*DTo0~4#cqPczFCIzr9*@#(72J$5Rbd@b4UI`CN`KunOqD7u2GRw zoVUq<<>}2>qml(6ER;Q}7>C9!*M_fClnz$l^)G-zS6KMv$JYH4rtnZ25f53YAPz03 zw7}Y$j>dI??FnBs=BtE4*a2WuD`(c;=BM%l9{)ogCMKrWXMe1ER}a2h6j(X^o@95? zx3IU5()bjJ^eid~E87CI^BD+EOG{KK@@sOP(qTCp!6rdW#-H@ES z?Q9iH;qWAB>FBie^r+MN!!&T-?Y*1MI-a$}MERQ@H9f0G+$8;!(z&(q2*fjxsv&WR z&~mG0qvyWxan|QCty@OXe^4SDe<_!&_1sHCKa_}yoS7^LD(D{UFW^h zbQVmoXS-p+o^c0P>sCBr*O_p#QyvgRnM`eZ85@y(zD78(n?3 z#$4&i6yq>Wb0<9=CKPI@jX_`UgN)-G4;u=n{-@9g)%W;ag*%h|RT<>QQ%$$77j5T8+qAnw7lOJU6i zP#j?)^`SDcEX=V2G&8^k@aD7D3Co-R62tFlO}|9hd|q%p=#(;jelM4l$U;)HBlHo9 z4}}iEcn##l)rC&%i}((dX5rpCu$xFSJG(JA79VK#dG0*r-KcULxeoh`q?sOK@L4t zSWbXP_dh?fOr$TvmGa4QY_P!4MB2?V+uGAt4Lwe!!m?_2pXBhugnX5tUdRYS?AQZ_ za1LwT7KT+@&zLw5TSpgcK-GW?lTzA31{_}$xN2#CJ({jsmcd977j_B zNqfH8&wqg%^aMXufX^>`^xC@+0S{GM)r7;r$*QFrd+mOD<^ZtGSnBY7$YMd$;>U-Q zhacg5hTxBoft{mgbH6>JbJP?-hoXXc1FtZ#z=}#L{hY6;NPGm<`SBSn%&t=FBnq;c zYl7UFi@+v&(j|2KTN1Cid82qL{0~#9$ZhGX`jJ~mAAA=Hb1Dm`JWEhOYG*)ig<5J` z9R$Pts!CZ^N<%KXMx3gFqA8?9^@3qqNo(D$@s8*Uz6nS2Q*K=sLs}-cn5f5f0iGpsxyGv{o7B>wshkeE4~qBtdUvO&}HAeMbLW%Pl9G{N{}7OgjgbWp1%W}$;( zh6PHq&!3My_7f5xUbV!pArOq|>D9%PUEoL;(kVV%qBbhuf(;L*HzYkE$zg(xj^E+F z*#mMvP}>0%VgJfFax4l#rLnGSGinS4M`DI~P5%TT@tTCs1#k?4gj=PQN6^f%jdcz9 zD5C}3Y#w%E@B8En^#?94mDB^ap}|*#;$V%TaHj7!2vZP>uc#7BJ+;D6AgdwAO@q1D zfd_2_Ouy^YPD;k_M~fwzwXx+*Yz^%mLxX}>^T7FT>!6a~!6U!@a6>k3Zb|O6)nD%O ztCz?BqgEqI!e(&To}PM|YWwBALmND-2k&^d=@1L``{~oChNruoju+Ah?*~5n|7LDU zFDaW|Db6geDJC87Of-BQDiGQuI(zUC=22V0rDo|%!^R&DrS1bHc%dp43fE5fI?MEpuy| z=E37Xr8KD_PYpT@5+X<7?W|~cXHvmw z?!5jKpA7DlQdQ;Uwm%Gaj=NYhh;-i_gA4DaXY`>kfm96Ik@4B-+2J4kxYc?8@~ep{ z8Coa&)<#1MVAl&0vy+=kXAc}mnfKR*hKr6~N$NR^czkHuQ`+Z^LL~s)cV>+sMQUAh z238C5u<7#e40yGR=U3m`7NJRnfJJWs(juso>SS$XffWj;sWIYR?d)Df9UZxXV4-bb zum;-0ic>@RO%SZ#0l8DA)MwY8ZKb554uY-@yaA9mvm-o|!el*TFRnn0^CJ zJmj$=$74zp#b)kNyp@%-09O=#Wv8z}7Fn9I(_7LMm2@MNB;2hvYJOn3BdNXr-L(G8k;Cu+->Hw(MkL zuj!mAzk`w0lJBO>HUXPOg!1sT#*A4NSDqjSr1dH}%^!8L$5IOuEPG2N!dRRqfw*6( zl*v87nHoA49Z$Vk=HL!VY9@V3lZg9YvVybDwIOvYr%9+O|0PjUN0Z2l^5H|Un!$0% zh`RX(wCmJfxK_F-qkxecf-&I>f&O81GHq*XwB&v#l&zQB4h_ENe@)0utgXZL_Z=Z$ zqKtbfq*A1^*cOPMF6^HPT~E1I#4@oji`9}-Qk0A07D!e1fwRt_~~h%tnys<>7_p@C9x>QO4uygTH0gHs#s zTZT-|2jeg}D-2Tl4`FtbsE(MXY2_3769WkD-Xc7sEJbzS`NS^E?)v;w$kNuHt3JlW z#-X?y+fL*pNs_8B%ME9$DT^$b@Scc3-9?=-?Q_auap~Q+|*CnO-#kr$bngYLQtS8F>$k$x|PLyGd4H5gfhTVJlxJ(c(p^)}ze!gosa zF8g0xiSIvg?=!?l8gW5RE)z9qQB3C`?%l`tDr2BJ7hIQtcemL*{UDY zYZVn$^Pwza|DEw^9cklG<9#zu@p0&oZ`0%XtV3Ll)TIF|BPUnd@N5PxI&_7vB~Gco z83rdr?^1TmJPnH@U>>~M-(YvIOM5Q#`{(1J^^&&!spsS@1PxqS$YIlm(<$C*RM9d#-pUhZR3kIBet^}`OqthA1_Fx zLzbrBbX;hw4_~zDHu6eFSs52fin4kFv@fWXYW!;#eiGNSH|M>pOpm+n!Vf|Dl9I_< zn0^V-q>J!QUUhYLi~xCBG1|EA$EawM5#d#%e_=tS*JGR1zpc1T=~pcJ!p9D*zQQX- z&o(QMRz9|`$^84(UW;%9VF7~l7V-^X_Bh*t5n>k>@ht2W_|a|1u!-+ zZj$}31CG73!=3V!S15Qmy~1Ed+&GB4{lELLo8>s;LoKa>!)gs zWWjdPxdSn>(^Of+S$|m)%ZxUN3FwC5dXsr{K1yO#Ydz0ZcV!-M}=V7jD) zn@8ui?lx@N3V?_9G>7OPp8oYFcN9}AO%P`a? zWcg(N=v;D0J~wN>^$~(S)y)sT(U6*vEllF@-HAkHn^z5@`4_&S1Lvd-W|7--d0$*Q zHAP*uVSFB>bJMf5<%|Z>Dmpq+uHs%ihYFZ3s+dC%Jq<#J&P99flc;I3o5IG77Vy4K zdz?OG>C?lu+&6*Q{&q7re1 zFrLAUrD4-?b1D-@M-H*(t;4;K3N&^`TZ{sc*P*23?pDnc!~T(DF?0G!JWO`Wr8(wQ zG%P-UnUJ{PO36eYKND6!2+zEw7{khxa+0Co^OeSJ+O!&>cDA-*^*I<=-1iiZMafLN zIM2|QEtH%!#JJYlw`JZA0!)HlzXC!MzVxw{E55d1R~jE;uip3WkGp04IQovaT@*iA z$&C@d;67Dtr>K30f7kZOjW-!rt9wkx3({+g+>`~!dE z9~By<%tJt{&||xG1j>y(?UT^EtRgsFq^{3vw|sYYYdqa%%^K&BMa#-t&`TdtR?Gxp z5w&5xl!4bqRQ=*z1?iRac^DX%S;p3Z9;%)9PLzKfF0-`yxpcVCDE!qGgPe!mmQTIj ziV}-@Jx2m1l912@cZBvmj>8jm^$LuH)dTv!6)_yAjd#GoRSKJ$t zy@wdh3yk(ywh9v+XRqhTfb^BKjSvD>U0geh5|+SozTY^K_iCaLnk1OqCBEhd+;V0~ zHX~C6q3Bk+Ya*yxp2nj1Fi1qa8F_f}vAs?gvt`=0LVtHTnElbz(D%@2jWC}IwVZfv zvnQuCGO^I>T72vS|Larh+yRYdB^(-gBx#wvg{QmFpm9$pal4q70pZB8=U#pcNopof^;!>8soXkIA z;mmpay5Z+BPp?t4#v!dzT6?JXV@S$q`_N2lzV5)e!=7|^OuRK)30YZN2faW}x#s=a+Z zFkWPUU5lwfjRQZGnmL4Ll>P{l6s}0*V%$)s-qoY*GkXn?AtPB{c`&RvKRpy zYqn9SQxc9t+wUUeRSJhy@och_vo=^KPPqexgErN{loAswe?>C|!qF1@SkNX&`^2;a zSxxKs7Cs8*abZXWyD~)<&lcfu(}d=f%a3!{^QJ7;FIWCrSKaoyfI{Evvi6yEJ3$$* zo;!oaE>gz`j&_WTkW#NKVWCeVH$0`rbq(6_j_BP%k&y-*VkL)%w$47-N-C|aMk1c%pjo!@jd8P zOe}A`N-ZsAww*DzvI-p=LxnW7|J`%Qhq!(#AC$4%0Df%8&Pi(pbpqZP;XFXxQ{_wR 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͎>0z&f}a=*`qXuI5J*F;h~-x8BQ|LG1)z6NLp8XxoDU6qL*9-dh5 zmz+CW%abUOw`s+Nvrw0IaY9t|3A16d>OJQY^K|j1cElhe(=9v*gTE@!3Hq%PS>OMh zxcuKqUUWsm$B8w}@4U2|1uq+u9U)!tIxrl>(s! zTmkKQg_J8`x!&M@m&pQ(`Mh+=z(zdt9X9b_d8|QutE2FzV1g6%)FJGQCIeC``e{#x zKFE2KO4)G)p0Y(&+ehvb3u*uJ0&FS|5?r2c=C z6rf^MEjw%`Ri-Xw&lsvxM*xfb7vlbWFNoSxN%g`8zPu1rA3XL;6bYiovUB$ByR7~( zj$XyiYJG{obsm(GJYckylAFo1t?>cYU3c@{$ftv?*9FIk0^tfzt&e;C#|Z1~|Jf1e z=WmGQ_lLNfCgI*(`|L&!3%ga3lt;5HTNuXg5g~Cx?>IU+`^(oYh%l&ObThyl6Z^L# z>m4oCIVwMt*<4kHsqhPwLq)J{qj^)m4r1izn@@B@QU@36C~)!Gf4>^{C@nF=?uKnt z{;sW)fpCITo%4OeA0A@}c{}=puwcq-Zpv*~8psi@g4zPQ3qK5E3+UXAUO(U?D`YsA z{>(V@lWC%m#oQ;FU4RlSyR39i>RTXW?E{s=flM>TUblL5^`cmX;PN$s>6P zX#}Makdm%%9q)VZyZ}d#$;E1)sMcAJ{PtGm7FicPU2A+F#UcX}h zTy-6-;Ka1=gq+eSOC~5kI=&y)E=(3fU>G1hhBY~1jtyjK$Tuu@{J9zJ zBLwgVrvWLr$ZDRv1+Jb-x7S?SSbFB_`eW=RK)K}BgjN(%)U4cHYp9gvWK zIE8h+Vm+xGrT}4PQTMR&6SR6Q(S`Bj?|)a@UBnN63Q+5NURT`*%s@ytX~V3O`}-CS zn`aY&BO?0faNi^FAAqIl72b7872rJT@hrc$K$kV=1Fc+lh#w;pBzm~J+w(lIl_1{%5fuPDU}9w^ zkp(rC;iyJL2Ww261Ob!y0K?c6wN>xo1Mdu2H$u3Gg=lJEPeTjj?~KOM>XjQrPjHYF zA^ITZBHsAi{CfF>aW?lC4tC>RtTM7LRQkqhrbm4D4Yw3jH{i*x6?yXP0sRAl>nMOV zwfCKDqJ28f+?eXI=@omx>?G7+7C%kk1@uHeObRLB!BI+Czg)Wwa&{kp<^UrUJ0FIC zgIU)!L#b!>x5FYa$?@*W{W;Ew5@$83WZW2PJ>1Ab7Fu*~!r^rf=$70S&aUI1dIfls zf`kUh_`Ok7Kh#2HfZ=pU&+3~8R|r^z0rCH|qey|Ii)O+1 zFIz-#eefwTD7l}c8I>k}!F~vEB>jWPlKyu(ydQ>0T%qCyA5lr#5Ar*pLyr{){dr)d z$=YmgbyGJZpv)XgF)tim*YXVZ!sx zu!5R)T3^Mh`_fB6ZI6N|0lvvy1Jp8m=HRuQ?oa6FddgF41I}G|B_K~uERiuT`p@+1 z^eF>?P`w((`$0a2`oM#;V+`a~=z4z4X_6|*9sV5oixdIk&^TdCz4@LN9qv#*O~RAO zp+in1DknEak`Vd_o%7~kGfCP;4t>0%&Nn@rk&$vzYAY4C3oev^P@lhkgX%(ks?r2j zKw1iMb~j)ikDtdclyqDnSjGTRw7YqpeE;mq1eomZ<^U8ZsKCtg@MAZm)ZRGdnj5<@ zDH`nVq&6&ZpbDWHK)=xerR-}9Y%g`Mj3Nu^QeLy2qZl-x4M9sQ$${%>(h}q!K)-`R z&Qd7GJZ!FRof*g%w_QICqYiUG(7Xv1#MDz$y6k_7ML-=DX0^UM6EZ72ivgJBxcP19 zody9oK!_ZN3BYK!PTFM-U`d2 zRm)5DYPjGtIU_wSJjFUSJRot~IGaL`6$`mV9*o(mYrm3OU&yfGhej@$#q@^cEsu?GnqX_C;yZ4?_-PQ+QiV1pB~?^b*A6d6|TP49C;V!M4aO8`2&x;Qh8(0&TD^) zc?$esZP=mP8(bw!_q4Q={6ytcmXwq%>4x5QmM~Zpv=M`fag8-SG7z9eE~y4|CUvpz zcGz7~RC#pmm}|jWI4B=rzBxkoQlyN8q!ipGit?bct`l{J=x6g=UzJ-6 zznO(1XyOP1R#S2{f>4PF?nv;>7y$@RsnjC~VEP z%{TZ38}Y)es8zih^(bh=J{*Xg9`;@%LXc-X9*|C&_aH$PA#@;LE{C&yB(f`4_R}2s zFvI}D)n*#~_OYO^zy6!5M`twuEaD7fBKnS;j{1BFDyDFqnF27|?HRzXYn)xYI5JOA zFZ=3|PYr+vBi%M5Fr>o+i)K>%?%a3~$&kXtKV@p`l`mbr3~HECCJ(feklT}<>6Ho~ zhfJ)ckW=x-l@X{@)oXTMwQVX>R`gofk_z0<;+Fbyuh8_+h0r;WSjQTyiMhFwnqhnW zCUKlFyL~GgM(RPqoSO8lGR*HXLPmod4WAtyFEWc^24dffjq#GKM$z%zPeenqXbF=A zCpoyD#dYop?ej2aljqE%O_%&R+-tn=jT=*aTS`kfX6I~90|=*x0q_qVj-gY$4xK5h zwPNLCXj0ZtzvwUK~uU-j@hcul` zzJEM5LuU84L@-7r5vU)^X#WkIbF#=0Dw9^|(CMXD9vxl1}m)J^I#O`q_rZmr1LrN=Fl8p-G4F!)^T?W}R%Qh_1PogToZX{{ zeVV?c7+T&Vrb^LxwNscyG8h%Vi-DlDmEXX?sU1_b{M0Rd98Bl>*+k1Uh z0}LnTduE{%4ZFph3Z6I6=aoZptDailzJp>yn2&*pWbM!6#WZ>XW46>JiAbG?V}jdE z5OPcDsQTk!ib0~FRbHK_U}X7vb~fw zcrb1e$-n?4U6p9iwqAVDFKy!LQ1G>f#vZnM4FwGYD!;k8?MkYw{&f;Mnwf>7G>&ZYE4t;0 z3*Y1Ho@Inq5)ttd%ueXyR?IrrHBBuM)H1lXd?GtR1=BQUJ7Wpzs>siBFoG4yweks) zSTB|rDtOfL_$K*7Y@@X8weK(a$9C1NVgi`w?)AkRvDm2{h*6nj(&#ql0e!9+b>>U9 z9?g;{z3t=a=`|3#$;;zKXS})1g((d}CgO=|d6iiA=wd{C+RxNcRGr}yY3p@o!ea_> zHp@FTR_l`X`BlVw{ zug$JRp|{G2A9K3sPsbL~(yTpYP|po&K6Mo%Im_2iJC0CF$&xCpxqotT1bLtS9jXVK zLiKkJ6;2J$@{>&aG~ulL_l8E_qT?7^aVwB!sZFV9<$+s&ciOGgr8 zN38Hrbit7gWiuWH)}T*G@y7|mtt0ms*)ihvYE%!Y93Uyf}}HZzXpUM_x$Mz>&kwF0R}q@kNel0LmwO z?io7Mg(kWB)s(^=c zOI(KIPF`#^6u$lsQSAz8J?iDHnS#68IQAyZZQclj#@`NBwbFD(ppXQ9idp1H#9TRH za!6)DYP!Ue6xnwbn9#wjHqZDz`mU^Is`$BSj}cfF6avQ~88+t~Xqegs>63>&gljq9 zIObQWRWDXPxhD74=!0fDa{IhaTt+&NHs4B|2jg$?Ap`=aW*WugMafL=%Kd~n)f-6wPD=GdpY1}~aFaw@aq)ic660Ws?F9oH`A-+LbCCu1=!n>Ys_fn&aG40L z1ibLs)0TuOrqWI4PdaNtm}XPDltxj~gj9w)p__p2-lc{U2~3#C%0`Y{OTf52>pA-2 zAAu2n7k6Riz9lX>Td}Fm%9-`$5YL^rT-Q*}QQ_x7)#j1N(G<-I%kmMzAIB7-x!3Y( zh8vnxe@ug6P%Cf%;-LJ3QvoXhISCB_>Q}qi zE4hJH2jv=5gh3V|h-E@L;eiVF7Vrh~n~|?x>vu1-N(CGszb>_m?uO= zj$30Lx|Z};r*Ukw1X;G~QZL)uarC4}f^Tq)GLEPws`U=m==w$6?v) zqbC(+f!HVZ*wf|}L@KI3MkR;qFkpAou|c0<#SUKB9lJv8c#we}1tyrlhqIipq?CkC^R?o|dVYAmZ?Gm0$$!rrpPae@*W24vgy%#?upN6(e3O6lbDb(TH?%|tklo%Us9=^oSr03sE_bfSY&_Br?S-Q zh$cBUu;A{q;3m+n-D6bGn)3t`CL`V!$VntI;@-38zccrYKA(xX-*~m|a$G6G+2Ko? zA2aNEr2B9?1?X65C-=yMu3{PXxEU7YwWZe8LB7q6TtH#aH>Z~mdgVy~+wQ%+9yQpR zqcXRe(((yHz+pU_2EmhY6;pJ6`E>!NnMU3(0k|Dc7Hk+t4&-;i{tTJf+^^A36>m_6 z5O-5E`nUGweHCKp>?7#TO8>5~_IvrX=*d^iK*@)M8MzD;0+!m&Toz)lNR5-Nwfd*0 zkLOyY;vvMuf$+#24yY%y+?@Du01y6W+sJ(#%NoK6K|sK%Ep{QV1^oY`rUV-W)5DGf zupslsk=tKD zEqzD6Ovu|H?I9P*UFUIU&Ld<#(b%Uwu;?kIpAHsfl1PjJMSn1UyFm0*rY1|-QD(S~ zrqKU=blQ*OOZ8W&%lelSySOh+7ooV>d@e6=l+12J*cG5N(xXLhNJMtR`ZE1oiWC@| zCvYh|J+=-nS<)p@9#B%Ij&AUAj!uv@R7(2ufNYWsfv_L@sMl`{K zZ+bA?w9|VaH?+efLgQy9jc04tSz{$cX@kSd1P`*$d0$IXac1RUZrd3hQjm}7 zesI*fe>FqUUUNr_#48lUg`D)zk&zI`fFK0x(1chTq~049az!DHXu=S_EG@^ynFd?{ z80)yDusms~Vekw%OIftt z7%7R|g>$acm4%zvQFXFYQAb=K{!}=4w6W@l*_tu;Xh;excZ=_cE9{s5lPv!to$;fV zO6$S|&ce&f{IjO^uxTwG&+U6BTzpc?TEQ(S$8^I z@b+7)x4HzmPggK5ygbMwzYM*MuF93f6WLBf5uLyh%69oZ0ofa}hk$-U9Mh15 z8H_w}jesf|J~UWAVfrZ9b+0eR20`EwWXuRZOzPpb$EImNfDuziC74ME=~^D^7baZR z1+~NXfk|&EbOl3nqHWiJ-z>Vy5#qswDds1Y)r#5ePnENv7H{mpVWvT|Mws|{-C_F> zSJ;;e8u;L;<&B$JKp%3YcAp1aRb3ePpgs#hgi7OwblqTDbfaL76ZAg+uFPYk3@G@$^YywZTqwQ?{`W?_@M{*>4+SYb%_%WwtMjDtFOXVDb4G6>& zIAIU8n~ZLpDD1$CDpqv98yHr|ATE0Kq$)%|6^l?$Bt zK#(aj@kgs=z!?4xU=;Rr&q?Q8c}XWok1aWGtk(p%dcPcH<$4ll%(3~^O*m*1q+}I^ z4d@Ljd8718COrn`%)XfCBBVwsXoO=UuS+XIqa?IaO;PFRMbqat?J1+O4ZuWbc5VT| zux>T%8h-nMvH5*Cofk-A!b)Of2~U@_!zIS%`vtH$gFw{L+td9`Yn?ch(O#ENkY5>( zIAL-RVW+C5mZ{2!B3*)~&BXNdX-rx8eB#CixYg@i#HDcBz}OI%&pEHpE~k$Wt}qND zflG3R_c0yJ$hwgduxJ~)yC2gvJh4-SG;eIm3XX(B%f>GEMx-q+TT*#l!@K3Z4qlUd zwpjT*Ufh4C6PHE0N0Yj^!S@wXuv6!f7WHb`M$Wlo>lEleu0(!nNCXCw_E*Jb)P_hW z7+8<|R6h2z+c<_HMVjN+)w|&71jHk$0F|ZP2KEGlrgs3~CMn|1PPF;6dlOd5Z8})x z_w_Ym)t69!re}hat3mmf+zF8SlWvU-j-Q9FB*4=fOYb_rAg~|ajTb=~9#?jCHKyRq zh8EJ&(u4PDzf>Nay%4S87X&#sCw^I61g+SIlMf1HRf4I#svjf*M$9AqB6_T+a)18D)@mzT@wgF8Yn9Wfy2`DUF|NZ+svdoK6Yp$QBKE+z21K^#EdZDhS3|RR zzcY7|(vKppLFL)gC=prG`MRr07`Ydmb?Be`g7-H(+=8M8drE{Y?hc&(zm+^Rznf=X z0?LsZLYMMPUz#M8rj3I=UQ(Q`Muu4roP$qK_Ppq|BEvf`p*+wr52qxDV~6l>-DBVD zNHU21(n=I22odZ_i0_&!?%&MJpjK?ZNNaX5`EaDa_TKexE&i z0IPoiTob{rbJ?xoY=7#l-dEThFrl~Dcrf!2Q;2<|gL^BYkSpNX4Xsk^r$-CkY0qx( zeYSo9rnO7*zMS|NP^+=UmFHqZ1&cyfnCZ}gm+%7$9;>lyV1jnA|?S&zwJobhdP(=E$+JCHy-y*hNQK)o9hBuGyUwjBpqx`bTqh z@hv@;K<*kO@wTuvO3s)8%j;xyMY;XqV}OhDSFZVy{kvSe{4kDM#QMWJR}^_$Eb)G5 z$2ABrBx+|2?U|r>3bizp+(HT{7a5;+>nS6SO*t|3#Lu2H5x^n)=olU+A59+i!I2MY z-3Smk1UX6b+d*y?bz=QQW$zN!3J*)c9+{0DT|87FhE)LfUI*)#^W^l*CdkUV3PfV7 ztbyf6#{?$BJvvLRwt>hC&+KdX=db_T6P?a9s}uo~>nMhX61hA6{kxHiO9BjBz=-S4 zVh@PE#fX6ywEttwI@_ODK!{^xLdV6oNI%5lcbBW$v;c`oli%NO)0A8XLD zL3kd_rSP6UaQXfkrgMun@X4l2>}rRyXLbQ4hEvkbqu`4Lh8t|u=y*?*X(hDecE#mz zp;47t(7>Aj2jQd;84Dry0B>;>sD$Rmc3JWZuo?-H^!x||F&6?Tppcx&o}ZNG+eUc4 zXUY-^LHBmZGUEAjm8G;e$|VftekEQv2vaH?DVkFow#}2Ga=Of$sN&a#g@8=DB6z+7 z`(X{KqAJj|fX~?dGUVuu-_aE*!<*YxxOb`B=YIet@z0l(+gShQ0*F#lP)Bdl|Zmzpc6y%J>Q=?jn$n!=M>pdVpOK zmJnLus2ZL*ThO^ECEkK675n$-xD^(wQ+S+xG~};<7D?qWN=e5V{3e94s&s-^tDi_5 zH9;KUc^E`cD7D}UlX&swa0&|F@D zh(D67^H8r>-D%o;c}5^X=X=2pWDTKDh%oCqLpEVGv0sO@O=|S#p8pkebd}m<%?b0L zqIW!)65zt%wI>klrqbtkjUSg=3$d7t2;pA43^OXQ^4XF+ag`6tD6WC&r6HS?j0|YX zWDf>`v|{blqYR`^QO5iUf1HHBd9_5YeCF)t#h^311lSZC>Y8aai8ZDuL-Gq1}Mfk!y8}sD)GMX3eVm<>V||%8Vc1V;;?@F+<3^{O(q8)gVwx_UpN>)nw`s-;ZFm-j1ze2XJ?!RMg?|Ty{0j+MeB` zlAMBUyt~*}o*;IoVX#yPH-G}p=0Vf9R#W3OXW=)(&5~it1hb7igfgIw0%Fc%%;piK zomK*jcx`PX$aV)s$;vA$ci|+RPoxd`pj;HV)1>WahbhPV`yS_?hpn`o14_(&#z!p@ z!9AgF@gd@tLZq1ggqcAgFoUv<{{&{p6*`S&pII*Q2t*tK$(< zw^2D;)ZAhnBVfQeMje~wpo0fXZt6r!U5HFlC8>Kd5??mybBH1x!JO@CVYlTBbaIo| zJRUetG1oICK=Kr^rhqPmsWmr^2+j)gLtQLL>C>Xm$Lo|Lp`Ep783?4(xEpa+Z@C!r zNapWF+`_*7iXp>qSF4$@%gfP9S9%k_WgL*QX+`vs2K8#iuz zTiZi;Hx?GAK68lxe_WWjV{i6A=Z-LVLk>?JYP8`h8%#!!%PL)?4cS?FI{R(%XLGjM zeb80{)g&07gfqSQV2?H=D9cn6@BQFm|E6dd9Xjm`ZmVrTOKvF`FlYQoi@RIK6gKrD zXZ_)Y9ICG~1b2(j}FgGkl6#TbmXKS|Qc^ zwE_I~8x))e&tfa_KNmhuDhEQg^pO4>&R?@YNVPtyN`QDL@JS(i7eLJaQjU=r6IOs> zBd8G*Ndoh}8L+={STIo=8y!77Li+Th)LB83lk{XS&um8)BpgrYpPxU>m0$I^j-QS{ zRf9T;L9D0Y;{4>W>B2+jz>5sId${a#Zt{D&Za1uE$5jr`AF!!P?!Z2gc)th)fTqy) z$b67-56n)$jZW4C3R9rrq5;AQo3Z<OGo5oBob)t1SqgCuDAL#3Kgag{Y;A%_WL@yXwFK`LvVT z@^Vt29;UXE;Ke-P)UBR`p;b6g6*Hd&V0w9m^Qp<~_DkH6R;a5PMj^HdwMA5z;m{X` zmt`dxYHQ~T!whD>8k!Pm5*{D^u#Q7=ssntqHxq>YJA#1;P_boWHb%S{RRoMJL^Wr3 z8COD*Gw(*4B(+qoV?&4r4;EBR^SqiL6rqL?VSGG$@FsG%socEPlwJR?np^f5Nb(&s z-8(ESjFzIdBMJ26zYu*6QXQIOj}m&k$_^fOdXKe0%8UAK|yhwJYBHk9JjZl(j%*)UOW;=DS-IpQ9=Q*03gva3-=ghbGVdbq8>Xb z3a7_){2Wli)AC;o##JCX5eV&1B=&evb6*Gw2~bHc%);qpjW3mx2yatB==Cmr&5&7M zUIzF2hW$wk&{Kh+ltl|iZZvXvaBuTqZGinYXrf5pL+*l74sFQ!$_Y$s&u+p2Tv#~A zg%$jAuKdcq!osY=U!2yvZ9m!$ZtDg;&!dH^WZ`b!pMZPNKoTAsz$Q~v{;=KiOlhM&IVG zaE$YxbA5CCejX@=P%f6MO5A-pTiQ{}l?+<5N2{1H1d+}Ef_o)mYIofwsI2|;tY=ut z#0o5H66mMraLiDXVhv62@?+Vc8brZd&7=9F#M?jB!}kaB*U%taUL3N-KO?LP&TIC7fV~QxVbSIBHC36=mmWCiJlVp7HAZ&P7iy zm5x>IxW$R((;Ms6nx9J}4L;0=IPR$NIx|8iqMTbv#=(sOhO>c;2?@t61_YNR7OJHF zN!;l9?rD3Hpf+F*2cUt;0ujvEQz{AzsNnq`LxU^^NNFJt-_U|l8}%@Uf2>;$0~5*7 zmp6KZv+h|JQ?9D^$3LHE(;hd*W7v^gXpg+T_&hH??5S7+>7PQ`m>EY(tOQ`FL60%H zxwv$lobbk+Ad?46Y5ThAgVHT%&7jIq)IXZ)TWH?{t^rM#X02J1vW+2!|KKk9%hZsm ze-We*=Qq=bM&tIius1=_`TZ^t2??ue$v(Jx-1BF*VO)Y{pjTC)0*Osr1S0%9$6ixl zW&t!m0Zq=a5(1e%-s^d#_LLdA%?r^Ljxc;XNDS#?fips&!n4(LG=(nxeL-=bY=11I zIndl&(tYXhV|US+c7Il_8NUm_7J@cJtm}DZj=>K1HY0WJzj}5eA*O z@tQtxZn5ec0+40D#<> zGY{UD*&=UxK+A zY@8h|1tMZ4r3lTBa{PQgUk#XY)br0t~Tng$4cr(Nj~~ z?NUXVDDVjg)YR1UK)eR~`6;}KL8k|^2dki+(dZrrLNf0Gw?SaB#f8?EGjmI2KH4@@qs685f|gz!%e#GPu+r}tgs!& zt!_h)X;inpuaUVUwxj!{gt{1+L2{uk!+p>mJ!P7=*lUzXFdu^l1BV;vQ8+vSiSr;D zAd3O21DXavHFmG=EDLy=e0E2R6Pm>P>desB2oW4=eXE0id&9-MgYbkTrR?_V{p^W(CMWU}+EKomkqh*nt0#>t6ze%kij7DVow zj(;(f>NNL*PRZ-p>SUD6_FpZF`EX8<2SM&I{q1W1U6>8;0gHF$$mXzp25PAGR?Olw;2iPW`}+>SvsQ~ z_$50_&)pshzEck zz|En6Q523Vq(OXZGXRzw7hjUGR~{cqVGk20aaOTFrd;y>^=VYN$%Nm$%w%%|-;n0K~EA*l@HVuc) zASu;KQf9yDei?u(x$ma-UbWJ*7!Vj9r&T8nF#cVQR%@y0QZY5RyNObu-=x=!nu!OO z9paakPtl;YY}`e2J32lhk2Jhn^RDXQRS*#)J6jP&)myNmP1%oannCCfoEMUklPROb zc#k&&^xnzj*MSllC@xsn_UJx-A&0A|^eCk-x@QqUpLZWoK8646-gbo@+eat1xD|th zC={0llJd~tF1_EdcW7>dPJht|zaJnQ zolDh6ypX-aBo2vC3|<3E5cpl}eaE_?MS$lI2L#Xxj^4T%Dodc-rH%)5!h2xe1m_EY zAOX7BaEIFjQ`VhZyJ|Lhu5T2DFJA}vW=0!qZAD-@vm{ftWO=*mjOTOVmS2ppu{}NL z1V7av;Zc%%_0(Umb-|Fk!xk6%EMv}7zKQJW| zQ+ZX@pR^0II<%f)IO;KRiBv@~7do#WO2!ud+^rg%dz>ml)Ji zptDde=Xr2m;RiNuC_(}ei?l)GrkIE3MSXC~DdCqvFEYi&^+GcV{`61;^M;uA zCG5yxKx4xj?fk(SWHq&IqJX{bZPm3qb7~(k`4}h8V&rI$S}75F0?AS_b|}JK<@?9l6RA#^P+bG%1o(v`=?GXib#LLj_JHfh z`v@lRys6u0!(PSN;W)1ZLN5m(_1iEB%ANp5#oQXaYJdRH&u{s~QHo2>SME`dxjcjE zMu1P7XSsk$;`&db!=a~Zid^Q)1y;xZ?z6odj&0V*t{?C2godMlW8m6?C7s^tJR8!6 z8PC7yYlm2frU6 zIRs=AfCFq53+UI@WMev~VqDo1^3A5mPUL0|1Nxy=!oiOKC;i+BTdBfJwIP-x5)&23sj3JkVl*?FNXI zPqR00XW$3HxxrMB#GG+IQ-SH7|1DfWl4n376lMjY93bGsduVV6M)^;3=8u8C5Eo3f zS&Uo9o3Z>y5s2nOMsr;u^L7_MN>TVQI4;iB9uNZ`74Z#V0)R7nKuqBYx9DVa134)v zK}icL$nOJ5ju1{s7{7sC7XMb|t?N?f*<4KT0*tM=}D6%>=F-yg#UqIWGek7t9rNjB@3NPS2)mQC28yKI!pYki%9fmMgZoA3$A zb=W2_{{SD9M+Hg6+IL@!DLPt_s3)>e0VxtF1Hhpf^-j?5o_k5L{hv?- zgOJou=J5^MpKW-u|KcaHfp|3=y_h)K=7HJi3Py{}+tja&a_}=kWbG($DU?aM^u5B9Y-2 z5tv60lw;JG0ED-EYN*y9SfXw=OWmd&GFM=|!s~qe=OMJy*D%~r^Z=Oi02%6T&rS?b zo`&$TUX5U~$a3Wkeg*A;1bM2G<$S|RoGUOUSvhQW-|r6b9zQ(awfYDBGDg5#4>o~5 zJT!uDW(W0kzV;=n7_+a_S=+5FI{-#rhyGm!6}hK%UqWeYPNxmFe|J&l#)@~39@*D- zeD@=L)<@x0CgJg8*x{;nQct)>3h%s-0*dkW-#TFK>u%f4G3*Iza5bI%IbY2Km^YjV zO1>TPZz^NVX4L7ob~GcvDMzn+Akmcv09xFaD0UKnfe>o?d}ZmZ$`vK-fueJigb7p4 zr4m6)!l0@IC!!^1AXF3)na~80%CiD7qzyGBCD@|KGV9qp)AyaJlPSrW?u){w73}e% zW!q)AYQ;Dlg$ke zaKB7$Qtvn+Tg#r{FJSA^^VOsXL6D%df+W7puu~+#o>-9A1rFZf7&D-5K#reI@Nvw( zz>yaQ^)+EUv=!q6NMoAhijlO5cztnil(krKa43@;;`xD;k^M^oXQu`K1!@}>JX)&T z@_C(b=JQW&O<2Jo6S(8PTheWhl#zXBhshpmB5#Fa{r&5e-oJk5!I%k5B=nSUph^<2 z0V6JSarOHqc|#~ZbSH`eXolh@pB)5EV3qK{zd~8fc)HXRiU@J=x>W zufi3V%$wE5#YPUJhPcYPpZpzbNzSu{F(PY^ugk=+{(G11!EPc#xOQqVMbrl(atjI) z#baPRf%b~Df*kHYl*l@j)t`ZPKIi?o4oq39dvqXz{VHRQFDRAu?|jbRH`^Yyc%5Pj zyn9MG;jMZUj~ru)PylCv4fcdYaI=paC^C{85P#J-+Gg*t3nS|+9y>W*t47V#KmY}f zgd1f?kAoT>We0{I zc+>_DbKKL~xX~C_hiZQ*e=kHXD_omX3jv>?z@&5@&Zo>t!3^RWQF3K%y58Q7k{Y>J zg_Uk~Iuv)Z@nTSYa8@_G`0o4X^KuJgAD;8fl)_aQ1EBX79=?9R!TA?2$Np%Y(U&)W zVv1~&?Is#NSyFdz7?;ZWtA-n?N*V?m?c~@Ck8(J8ek{YU?RzVnznsrlO@0-hN<8|1 zzjA72GIMYu^3)<0>zWDm9JxCs*!>Cx!+W|(b%<^dHxvVC!6T{xai#$;^MYhOR zvSB!pG8ie>d9uL0I*WMb4)><>5Nu0b_F?QX=k58X{>JOJHLj8{B83}6+xPhR!ifnXR=#Zk4$*~|D98b4{6ZoC?;YUeX6bM zip#g?EvAwCw!G-9LI3OSkB$Fw0oGG3%NCYz{GzQSV(oFr$>pSq?PX)HZ~5*|>+6c2 zhDCyZm2c1byxFU|!tf9)fhkrsEJrPbw+z#OZKc3AxxiViz)o$El_I*TLn&TurNHIM zJ&8ow0QpYAuw_C6hCk5Wum&3i$mrTa9##hOFj_=(=Y+Q7sA`--W= zY3b;nWKwaOw1yok@Ty7Pkyx-z3_s*mhULRGB;OC~5G(Xfwl+&8ac+iY-YZH%B-`&< z!(K8I?&Q1=w2mXOgR|>KI*r8n9YI~>Q_C{({RxQ}kCJA^V(4PG#-$v`V&#@=x)9}J#$*G(>i|2_E|VbsY-$x*SCd7@ys7dr0+>D ziZ4s>YKG{u{^A}{6IYR**uAaCP4Lc}QvDH5*>@=qywr`ttX_RX$4C91T*8zZy5>wR z9f-?swlM>f8^3)j=IZSvs~&peDKGKwS8EdADF7tVZJ!7CiS$4mbfr}Lp zVv63?MCGY=y&7Yo+2YZoRYcxSL*t`LVdK$zyxPI6MC0&+^}el{7F9WT%ib=D!;0c# znOg48u8vVG{VK@SF(eYAnfZ3gBD*LLGh>S0p2+CLwY~tk$?;*bZ290%htAMcQLlNs zF~_S#b^}ggv1JKMWAtnKM5lxB#xJK7$5t4`J(F_XupZ^eAj4y8jMvr6{*w4yhDRr7 z#vr%FqRp61H+tktPKIjIh*;7{P{)V-8G~$9`3bRwAQN=Pw+fb24lm`oyNgAn&>m1X zV#UZ^g#Sox{>DZ9e_wkJldJ}&f?{090QH1F4zU?teaj1vxU|MSYb;KWl@B%guR<5> z3mH2Y&lV)QccoX7UPY(K`c3PERU}C$=sg{h+c1<3Pg4)P99sI4BgJbtfuWJ&kOQa zJ;?(M9h_dlLl?e42hZT{GMMO_`gz+Y@VDNTxIfoyVF-KEQhDHsWAi@ycm9@&Oiav;8$ zzrA3_Q#IeWt*U-=zZNg@`{~eS5C)F!Gwxd6h1Ryojrp$E*Z&p#sLUi4l$01>(}lE} z+op|InmLNx!!zP&&}MmR9=wzF=Dy@dT#sQD#tlVb;YacPyHp>$zGf?>quq;@pkIu` zDvaeFjcr|0kBDVFXwJsFP~`B|f7(CPkP%*e%{?t5`k5r1IeC^ym-W};^$qeUri!LH znnqIUgx;9`6g8c$N-^9=OJC@bWwZE0nK%2V(E{43S6w62o`&#MM%1cGe{Ma`W07@l zvlsR^&wcx(HiDUyDvl*;boEop?Wo)Ja`Q^;aXrqIjogLk>7KsI3IPS4gmbJzNmm>! z=+Ce7MZLbkVPRTe`|3mx@58cA5wzLV)YU1|WDGb`>Ko~aK9c|YX;_(otg4+cYDo#N(K^zPM=22kSlpLvEJ!fPz%Wk&d2yb->Vwl zo@y-e0KYqp#UxMsStR;GDEgpdy1#95pVVbX+k+_o@D!|d6{RSn0(lxsROS6!lYL=GIex!3=7={`V}Xh-_QE4l}@CS z`Ry_xrlGx{Ury-Y;Na-wBu9%&iyz9SE8wnHph%g4%#Mj6`|o4O;iS(%#vqsJ;v`tk z>Uf(~qnp6!K`4J%PD7*T+>6iW*y~)@C&f=NL=*SQAg6oMtb~#hXr%hSfZp!*ktY`k zR-H`cRmH0#&OoFyW$*qoOm4wUz@LHb*Frw^1uHX71nb%rR1<*$N92o4;L3YXc{~J1eD%ClTZF567~n%vjn-E-Prh`{%nsS;OlFDE3uSo z78u$c`f{nk`LsI?|37Kq1a4F)qumcA|EyfJ^Zm++u%5-aR)37Dsw!QM6xRE-=N0jV zh0qfN^8X=t&`JZt(B^!}X65(qPD>7(f)4RjgdBYwEiU?WgIkQw#qk&kzh`3e`C4=5 zs-r$z(#)Sl@#nta%*LOw3}Wt!z)tksYPrz>>a>Y{#+Ybs>ut{Y;a4#*Fg%Y}^K*vH z;Iq#IfC%4XryB?l-jg2*hAW3-K4&WK9v(+i&1V|j-Q5dZAKT!XI5?q@1z+53(a6I^ z7N4%J7#h};KFT2J$)>98d!S^C`f2#^>O$GYahaK!S&JcChewQ1yWlc@?P%Tfx$uDRK07=6^XCuz?HcJXZ?wx0*0s&fFXB`0 zSP7{w)D#vHhKmFQ1Q6c8@4i^3gnPYFn}zJ1Mq*(hzCgoXIqA02<0NYfBx6ak_-!n>oHwazngnYXU?l6)YWa@klb)i zw;{e!b;5%1O%u)^)+<+S_c#xo_r=tmI@k7O0f$={aACv`btP@My~!~v3Y&TJ(4muK z;%W^=<%E=)8qsFcNomlxv&_J>_{Ase?CsFYBqAi#)zy8WY&5Q~sFbV9SW!{Y3jK~t zD60SM+qZXmiSsH>~bf!i)TRp_V4YH6jdD`>Qb5`9l#)|!J?2WGDL zaJTUMvi2S|&m!TvAb6p^js5AgKn zd<97{RpXTnBGbo&Glk`nQ;7rk8&%hNH)bg~3u~S$DK*#L7Msd&ZU$S7!$(tJ`ZH-m zT+uww&U;?$z@WU+o#GvR;&%r=%STPlO6|F7w z0wYMcTW2rMW(`1``LN2LHbjskWpSp_1A0}kZV!u^j~VtiXIT*WOoAzEt9$OHV9}H* z6xjX=9^`0dxNLTIHm}Ei7{n14?`@3Zlas|mc#`u9)G?^5C*gDd6NjK->S4}6D!^&N zz|aO=oY%Pj-S-eEovDaEev}50bGzStMK9Mae9DycAvk35rcG;p=c($DE;-HJpPkAz z>~-GJ@{))@nYs^5y!>IWY{nwz$c2D`(QPX)agQZ|mu<|GM8RH7y$<=%G?=Po>FTZUTs%2bxRE__>dPi}~^UjnZn;u)z0_8q_ zR@yTw_WL^OR3pFRG}~v?C32%-O~q!>(uPoWTdX;LPB@NcY^km`HSzOXm(&;u^^#z) zpoZ@g?cb|uB3O1;--;Yf(8g4as|f%HzFy&BaX zo&)bB67kS)z!@h^Ca^{KeN9a~yuHDR2|D^1@n?R{{7xPo&uC;)-#0ap{^!iRPMHuF zS8g-Q4+pJwnaZ^j$*BwutZ(1Gp?tJHfBtOv;utG;Y^_|C(b2^vMv5A|TcYM$u(HB$ zXqP4U-N2r6P>A{P;n4=B`j1I)U4x7=HX)(sSNgLcxRj9*I|$2@lamWkKF?zD zKYlbpl3H>Q^JmER-*O@JYkA@3#vO=F)@fcnXJ5Ba`9s$5%bWk5rl{YXiJ=C%YE$4s z6qWFjs)zcw=0rwThx51H8`*UA0tbIPsi+XZ?F0m%)d^I$na+QKi-3)ex`?pJ2McSL zF|2Df&=}WWftN|qNOwA)$A_}A{L)gHh#Sv>@V^#cXM_kcZC#70dvXu*zRMR=a`g1{ za3JSZRLILD*U*9+<9F-)^71>E>c2D7aB938&8KHzScN~3m4%90A`mt!Crm9Zsgk7T zY`Gw0?vPrsvh3$-)jPu~^@p{-?)*$zS{hDhOO6zhjGA`@Iw7Cy>kA+Shw$cZSE|B@ zzo=;w8a*FFB@RF(0yd5TGM&%n*gl8z#ngww($c)opO2+=BL1L3IR4|#$Hc@0DMiC- zPn5U*B-al|o(X91r!_OUP9E~szelxXC>JzqZ88xIF^)!-+k}B_F8N2JN?psoy^hsQB5ta-oNrnj)|=_)IsFv z28^z`U1Ldpf2V^%h+7gsV^RY}sq17)sg6M~qC{7iPo^4gGCHd;zjFM%BNZ0L=Z)a| zegCF(>Hm&Ge`Ejo-xcY9?f8|Nno70*{^9@q-~PX2`ahr8*9Ex!fByBqe(S-rq^Ef3 z|NS@r^UtG)L7@E4zy9}=P+qCo+xvIh|BhGjn*V~+rFGxu85w#eCWkp_)0#&c$L6tG z{d3E|$H$*Ne@+Rvy7A^8k2CNj@=`}B-NsGqa7*jr$75@8uOQ;e?5vB#>C@M2Z7cr` zzQ!w@L9zjc0@b)Z3Kz8Bt~5hYg7XS^W^aG&#*G^VB$B$3QTV?V#}-%wLaPUH73c9& zrxdxO4kTQ{#dcX)<|KVYv$R*QB34#B@nCU?R>OQh8yMjxs-y;7)=&xGRln0?Tzm%i z!PR_qEiL)Wml*(CAI8L(39)_}S>=PLNS*OfY%FdpzhZA6uEu*iD+?zM=vF!@^3^MW ztn6$+Vpf_vv1N(^u?OSpPKbz9jgRx^>E6TrYPjfQ)K$a4;5>Gqy}i8{M?`Gr88HqX zM(X~>RXqcPgK-y7bE`9MLnlRLKM~pF}Ewpd z0xbAQL=k%Va$w!nN^5Tq14Z`z1tV>rKcktoW5R7_?a%o5r+%l>g*n?2+yjq2hzF%- zY|MjIZ5YYxnh;C4L~JU1`>J6Vv%)Tu<<&e>b%#l{d=dZZrq5GO+l6OX~ONY zy82l>SX4#$6=!=K7ChJkyG^l*O5C!-Y$!V~^E2ZWQD@uO*r0kzD=yv}Sv~h%EH9cd ztSa_%pp)1I=_Ho9l+T|Puu4Y+1*?Z$-nUvYhAUJ_8S3gDh_BP&WyJGB$wPc5YwP6F z(lvG;U0Vv6!ObPN2Up@clm8@RoUh~B9d>VHKl90}v$C@fq9ppyl_&r)xP_A~9FGEL z2073Cm35x(#)7 z!^6W|_xrUnvs5xQ&2b`ei!xDUphOL+9>fRIH!wgZ{)OAFQr^9Lf#hk73D*#SDpWZyUcLnM3ftC-Te81?r8e=IXG-$h5ce%lQsc$V zmarY+^P^TVKUq29HdW+L986rlG}&F^y|Fw3b2i@U)}@RzJZPWQ1!r8+lUK3!F1{rz z3wJHy{+NC`zaaPsOpaJ?&IdC-X6K;6Qzo(`~ z)|ThSy9((dtMk*ync{+(klSBB%7)^f@+m_HvQr zq2T{C*x>^Xp@fq3{2P*H{&ui@NJ~nA~{(SBE2C{#f#1N2YhGR`;`#w9@wf{$Snomxb;dI2<*kr9kee|0@Qyu8Q6+mh~2gU-0NvdCkO2W9E* z0JKbN0ld!C98L;p7`;Kxj4Kmy9K9ANiLdCgx#5%A#OwJ!@$Ow};tMqRE}R6P0!-Sw za_=HHy`=ZjeefPmBlY{%d}ZP;-1+(y<+lD(Um)GI=X(TmAKQwLmBDnyH>WsNlccZU zvLQsEOwEs`LemYKYYit%`&ALI4jw${J>Q~(RmXCHWx;ZJdU`G`4MyUK>GD5nqt(T= zt5>Y8Lx@|#zBM#3gewp!5Sx;?S&I1I(ecupg%pr{!*iFc9Cxi@SV$d{EOdW;8EbZ8LFT!GfYmoRW%) z7-fNjg+qTlNdor{mM>Ka{{HhP_2tWPl60C9_s8hNszkw#TAYh5{DDe3IKAYwv_LxP zC4Yv)m%MEoqpsrGL6rJ<4EuO^c+g}B$ci-*^Ia1oUJay9uX|#w${}>~R66>t8ffe2 z>KxX0$SWOx_x|0$TU-9J}3fYqy z4wLV~%b-Ld_6bho%F4?0vUl#xZ<31a$JmiH{Rqg_&4iATN+-plFm^}q@)yc>LV+0o1ov>L<9}&CZrUu zsNL~Gl!0fr%33qATzV?@fpDa2X)y`Jjwvpe7Cu2Hyb~Nu>|cCR96xXA{i5-^w6t~v zS8gTWvM||wG;s?~i%(9{%+ibnr;P76d?{csY~ekusifnP)#s4UP|o&Y{>kGDJWXNrYIRm6s=hzQcBvH!xtIE|1p(M#M~lAT;ga*dIN5lvvtd zzpgG!bX5~=W4OC;V<2fN{%u)V{hvR4L?=C_7+|kNN#$Ot!k&71>2P(#zQMFe>D1KJ z0d_5cGJ)ut-Qfy*_wMb#S-!fj#*+SD>h|NPD}A^Ezy=-xPD7IS><%hAPDUnSGV%a2 zTsHNy$T=`uCs#vD>yYo;R9;*O2M!&3XBZ1lG^Qbhyl1@cf-R24s#;Rh(Fv{|1UrQE z1fnY|i?7KI17S>T8Qe;60I+bJ(%xTKOuWyEaeSU|8*~yw&XP-0|JW~dDI*#`F1_;m z;GXSE^9Q1G!xYl+b~CHJn~5nY|A3);@fy$?;nPrnsv`4p1`#EYf=?}uTl215xneG`Csd9Y zkb|d644}Z)zHx(O?FTf6&1Yk4+d;BrVG%GiZF^ClI{m?xlXv9Mro_5dtD*{$7LFw zwf#yY$AAh1*omv0JKS>Oo`R zz!0r9jEpXV*x-o{l})L@jmHuR5!lfL^+qI$J!{XJN0G#d*Bk@``4J_MFk6)vu2w}^ zvo-MS*VdN*XlIZ2j3lERycTEUhbTAHD zc<@k(dwY;9o-2LmZQg?~VlHK6W_}o)F%@PjpAiDjK5&JXgBH-U%0CCRkbX|;T14$| zr*>3Qr@5kNJg)sUlGA0P-sUQiz+0&c$a&+2i0Ata(!oxW7kGBL9j{#_j;WLbV}k5H z@Qi!$|9JsgT3XD7Hr|RN>|n|ZH)kY6SQV~=hAyD~O0A>~$rPJzGhI6M*g17Z5K8Rq z>|AUURb@l267nW4VjO;MCMrD9g7fJhyjLEejfSE*`yo4jj0&LKqeqW)jf}t|o9F3T zJ33}$W<eQ8h?t%_w+apr6DSqf#+f6USVYb!G3mxQ~igKnwFt6fs#@ckP~|%tXb8 zFDiI!?b@N!2cs|>^C>7R>7)!zK16qUaUSg2wlSvRq8^1#awPz1WUEUV@{EO5<|&|e zO=Z_6mO*?y@eXmciV(l;BSJb)3Ux8!=a}Nd!7=eQ} z_zO=Hb9X^(fV<=+z8J39S_BmTk?Z8~b$38QA+gWS&N|i!cR3WqhOn$Y(B7ys&r>rt z7Wn0y+ZFqLwBYyoD5W8GpUl)$^2qAI%6|8BQ5*yLnsETZCTqq+w@Ds-ZjUbTnEbii zDQQ^y6xAoa&z~F8VX`R)$`+BRMqTwEuC$!+B1z91?Ug|+_Ldf0xpE?o(SpUl!|t;~ z;o!dlUHc=SY)!sBVO5``FaRJ`Fs|;G7t}J{k<)*1rPc< z+iVsU;!8@!Om3ERpH3B2{vQ8qU(4(N$O5{$x~RNdemDLjy*$M5uNpVg)w}TsHyZ=$ z%Sj|WPe}=Q^5n^W{`73Ao2O2lD)w3wICt(G^5gfGmMY}V%-q~+8xjE;1&m9-w70Xu z9FGe2wb!D<%p|HBT_^=}4Kjxrsnv~)>H;XJiqJa@R0f>lw^r>Zbm9aTvnJ{0fivH{ zNMu{>>&|*9T#Fwq$K|PjSHvUDvn!$Ew{93FLKnfFrw&v!r74b1O)07~0!!iteJj6x zqlr@5`rfz!Y})YN}{brEAn+-HY&Y%V8l)+{X*=@z0?WSaOVy5@Cp@sY24 z3JVHC>L#H~5ox2Z??79w2~~1NhN4}mgO%W`>pjKSgNf|K7ek;W+)|nW>R4XA$ah@= z3q%xLra~dL!{&&Ixtf;@R`-H(9E|i^JBa2qBs>s)iV++ipon~B|4+M`1xa7rWhg|q zvax3mlq{4PSzn%%V=ch|;9^ulAH&zgJ+}e8GVA7~t@cKZ2tFs`fU&-P`?fo>+F2q& z1_4bW2bl|H0s-xQ|K=@UX~}gTNw^7K_AnR~02&CE#cmU9*nJxt8?j$={9-TgRH94+ z)F+y`Ksyq(48rb3?-Bb%61RrU9; zFrIDThYFN5WjE@4OKL4o045W4z#cI-R7(UlId?8_J6{P95%y-m+qZ83e{`NiDst_? z4k^avuMj(bRno}}A+Cex2pw6(69eS_U@GL)SFt6pjHaPCCrR4pEB-o?Q|hC6-gJJ_ z`aRTbD8+!a4NKg@M@O#_4OswN{~^uqx@DnWj=P|#si|gSqAHQV!xnx*LPF)h?xiM~ z-omro^k%rz9iQohq~y2NsqzMba|MTwaD*(`L9NKG;yL@or_Jwp39~WTU&qU$e!1TNaT^Lzgqz% z&J$pSUW+dVWCA#**Egq(L8#+rz<*$Z)Y62b-_fkoTZBbLCs9{}1Uk-%-SMxybvfgb zx`~M(d1JvcJiYyR5avE}tGE#vNr1RMnUMB?tDlNrzXLK3%Ye`Iv)nJ~sLm@PaDhOS zNHykyTXwfJ63-ZXUoh>yf$p2VIK=dk)y3HPM9KR#=IGFz2caY*)z}6KYPU4?XaN%< zPXk(TN_l=7N%E`5fd;k(H~~cd)K+%QrNx4sj~U(|aRo(1q5ztkn@hc&03r{CO;3+8 ze$3jz;WI?>1)l7;|9H;*x`>hzmzUCy2tZnI{=cNUiAhrXnuoeX!c_op;?anSfnDO} zprr`8&Vm+4ag!W(5LKW@!UW6u@*AWN#;6d1mgpf2`F_1?;)>j93Kzscg1kXBhBpDg#&-ct zN_-b-pC@?m(BL*+N~g!zm*T7d;HN%&QahYlTpUO2GaNDJ#N<|HtfRbm5k3dXxQ_OA zkXGixY|j5m1dYwj=Mb{qzklyIaVJV!b`cSpxJVgA0`g+JoiqqL3JMBy^@SV6M$E{N z|6Kd)7tg=4+c{g?qqs)h9K{`WtoN8~SSl|ebE0ch2q)s>NAiDlepxI&UY`(7251Tk z=Q-EztML+Clw9Z;`{=wSh#!m;SLspx?b-;RoNV}aj*rMy(jEmlRX;V~s_-TOQ#;&gO7cbV?`Y>aYfJG$^ z2P!#Gf(ZC8wU2_!Kd2bt1d$K5d)Y}-s3HuZa_(zO#{&Zc5e`6ngWx42xal81KB0)@ zRgrZlv@%IiGe!MRe52lS!Fq;PmGu1*u1cj0*Wx2`ZNRkR68oq_CHN8&3CxFDR1+i! z{RyH~uPzx7AAlt|16EGXtGNFh4;XmQ|FHpwn3F#R742)s-ZK00&;!bj8h96+8suS| zWmHhOaJ{jujo5b6YsKEPL&_i+iFg3g164xU%(7NbbAd~LidqPc905rF1@_I69OAd? zt`34Ihcy)8n6vOnZrc{)|OC`B_t$}ND#ac2#~&CzI^Glx?oSsDarfbP(gSTH~{QPh#bL{ z0a;l%{Zpi#%^n3TEB1RTNPCsazCI#`>s($$0AfM*g^YwM z>C^MHu8HTFnabSp*sqYN7IPW|)0)uEzw~*o)>_zM_2yVE#*docv$wG^2jqg~qq(us zzj^eNwJ-=DC2slwyVcOp9UxIw0~ODqOm+r=00I&}%OUP^8|yqUc}m_P7*2mzS<7!@kEiB5sSdyINu9kZhv;@1;8CxRpziD;CBSW z4!ebnmEVQ;5SaJdMblj}Z&o8^k6=mw|tSBriZJ#?0qlG-KNJAT?@_L;j!5B0v0P1$&F>$7AC z)UY=Ie*w|~L$|j-{!(v)Dmp!VnB=PfRm;rm>;*X{`8^DFT3S_B4kzM1as3VxY`>cd z8b{aI_^PGlZ4mMRPpG1~gaB6ioWwp!bw_|XBA)%t$)9F+b~JtIWd(2E$fl@O*pMK` z;S2fYoRp5Pc^2}n1iQ1}X(>HF3>7hOY^tP@s%itB03!rE0+;K>gOTJq=u^^j*gkp< zQ4<`_?(*Kq4m)NazPJm3vv*NfLvMgq*|}37J3E^os4uqUE@ncrh>G(Bcm^vD4f;hE zA35`_56NFXfruaQMVJMLjvAbw)mlrWhgqL;4DvEi_*JB1;5G>|&0rQcVjWS+;5(8bKJ+7!GKb-tJM@-agz0Fg zCK{!}hUw>^N6xIJ$s5oQPFC-?v*Q(c;0*Sw6V>8EUJBcB}LKM|_ zY9J{tUV}^oBnHhm_KgkLAP|&zk_5CtorUKMQ<}Vdko(*Bx~;Ay-AA>wAku&qBdSHz z#PXxIAVX4l?Hn9Vh>I)3K#PhXHLHtmJ{=Zqz8FIFvTb{X5CWb*bygR;ArcA79pqOi zHgDg)ja~V^vhwb6wv78U*_nrjT6=;)uo70(VI_7pHt4@y0@Iog-rj=A4TK1CCbSvk zVjP3#K>FZuW(=ps#whww@R&D?MJXY+mYp&^yVEy9jth_GJ_c2m+!Q3DWpT0I%XB0e z)JX1se@F33?ccujQ`8=)jF97z!UzhJcv+Z>kg*`tsqqqk3j_gBG(%j0{{2@A=M5PB zqO|)3qifWFslg@^ROO+r$%1E3K)>SOVUGY$_y`Q+jK{SYcTkfU5{sZ4+%@&Nzs+&%RLi*6Hp@7E6h9vvmhw}E!% zGfy{5i;BLW76aw2%fFza7Ym)l@HaWl?ruaS6rN^vJ?DgGT?F|$66OCtzPw0FS{kC|=>!PA1l>i;b zqd6ay5A5y(6$1?hg%T%`VtTNZsz&BOT7kUd!r8+1fxX0QVSKq~IaV}-CP)@O;(V}1 zgavX1&N;~PMD;wdxC-7J<58RuYoZugse@Se@3#VNLX=?ue^27$14brZLBp3OjLp}%pkR6fCS8ka!GDt90VhgH0mUVkKP;i zvMAcx-V`Vt`Vx$yggX&5!NAGOS}`Cg;Q;{AWQBqRvI21L==k_rk5qx3D6lIH@(m0G zphK#VvS1=0OPo4_k}BjjRnGTHJPvsc3t_flq|+B!E8&*(T3Vol@Cyzf}0eUU`We1XLc0rDUCTtKLg zykR051FStgpFqFew8XIjIsx5M3y1EfGYe3SPl$^0sF2$ZkDF+Tas2vK7mwUQRF`;1 z!Wb}J8w>$;#4uYL%1T&%;4*en^I8g^cAcS_hwl*T;H{l-yP+hh(ursR82lNf? zK8f2^=KrwLxM123=$Yx&b*Q?)*~Xq91Y%UUEXMIk<+%2sAgMu!&V(?EG{n!0?1uIN zXP#efCowLF5NP51!J81pF}&0o52xQ_j0^HT#6fIHFoY>84_A z9t=his1#6UM8gA$lc$$P=o|`EFI}3A?_IltM|b1Ke1xtczAYg9Y>SQwAU60!!m^EO z3;UR$)liy&>BE0n8j4mmK5YsVs$nTnWq#Z_F}up#3>5`3!pu+&11wXp_#-BSZKJTS ze5T0&0jCs#NS4V;Am9w@$1*SytYgEP3#I(4Vu#R6nF>Gi*t37H2;lkm1pxXqzpA}UpUX~?xe=+2tqZ(krv zeL)dTNb*QlJ6Qx8dwV0hCV1#Wh2U+$i3Na1c}?i^3GoGv*#IlpB%kpsghz#-OfW$Q zF9ujw%W#J2wKUCtuUpbAp9|ttT7d*%rc@l{ihXeM8Ga1dH z53G2il}jH6SDJzUp`ztTM#1t>pnli|o06bN32!+7xtvh1+cR7rTAl>dejPgm3n+$> zMOY3fx$sskPW4)$21j~@S^5k0N=g}qz!5+n0N8J}(2rr~Q#j$kPll8UgV+W2gO;Dz z+WCYz!DC;p;M*gHeC&TMCSY@VK4B`AG=WvbC@LAJ>AthD^+)37TKH+F`_KJfcLs>8 zCGWD>2#ZyL&RfI4#}Ic>iMYVCqH5CCxX*F2i@l7BYc2_#fKpX`^-{`e8ub z<*~N?$JvDhbnzw5er&zSPuc=9qa5KW#9kB&!xaG(_-v&V<3=@%jE;`( z(R1oO9HSDNoa(o^=$BDa5)bMFs0&+E1Z3JUXcjJA!vC1twyI%*q%a84VFZ5*rNU)n z%_CyJi~u<^j)}+L-+OSvp+|(upTxd{1#?spNpm}8AlK5j!I_Z!a*@uxAN56YQI?}x z$FTx-3=RL~2>cf!#|7axlur<5Fk&fAJeh!c{05d}hB?!uhQ;s2RlsDWUS&(yNw#({ z!`YCaVardyTi{qOv%LoeD_)p6tf{qC0lZFx*q~wdtNRytk6BZBVS7ADVf16pPIYanOR-kvkD*>EWUG)WQTx)SI&= zrFd8hd3q`PhO7TW{&yL{DGhdMLtOOEXVci_xaX{?h_Le<`$QC3cuCm zmnmX?w(Th5Eg57fIfa+Ms?z?VfJC2_rY>^H?vYpV7kJbY*Tw=U!8?JX1Tzlsh;FjY zh8{1OY7gVKK`XCHQM+npvap^l^9CFec|(_HqY{rXv?SVHSHTp zE>Xv#QhDja<)-Xeu57XUuwYy!Gwqe@79ju6p?L0)lY=wuhWy5*Q(j3U(u;{+5xrBe z<+~qBydt6&ednPo{676|C91j!_f0%NsiPtV9~}`9QNO<7DzPi6A=p(Sd||;2T}%uf zBqi=!EjB@r$NT&LiT?Pe17apCKqpK@Fk;D+&yJ;rIs(Wc!=<;J9g(0K4 z$siMcT9IF+%`rKB`ZUI$uSu6vql~aqj#J`B*M=!cU-c83;`&(YhF#jbx-1SA?H4<<1GwvEjEO?sJ*90!N8#FRO!iF zO6y}#ClO__=wf*+6sEP4phW7^UR=Gw>{85uuZqu(6ahbjj!5tcws1g0_f=SOE$cEFLXkhW*WodZTUL|&FMs$xT0aIOI38kv7%Buq3C8EAu@ot*pw z0shuA8?CVuX0g80_);)UTt}5C~(tUMlHJqSTsxR z#(`VU6w(6fu3CK5=Vg?Lbv-lIi-pEuF{A`IzI8_@B$D2Hx2SU5|<6S|rWQiAg8yXr0=i5w1?tG9p54C*aBfTHR zT?eiI-C+>;=^8sABN{eNj*t8C1or(uFMu4vO6V@6uNL3daGVmF5VYw|(SoKAsM#ewey-vIgrM??>OIqVAZHaa?*>A1YF zAKbxThFv%=qmagTggZU1j&)SlvN7)O~N%oc@WU&d*#4HUulipiv77O3%m?^aU1~@VIsSU>Q!H z8|-i) zMBe_S%YO$z6Zg!RgXp)QcK`kqB330BX~s=qW z!Dva`^L!7f#oN+4Rzn(uW@4piMHUc>duYjxCCuU5DNqo8%pCT$W2~I+Lz} z-eG`euIL)4gac7Zga=PQ=g}eG+FbRFW@zVUc%ren58zdp!_H0zr8QO7BGCLOV;tlj`1M!G(^V-h=DWKc zR|>nkje5cwDE6e{?u@q1h`@)fHzf9f7h2{&tGx?Wr8)KZI_=}u#%24lb%9ojPz!446O^2O z;65iOG7uW@gh2SDvIk_j+2av>IMV-cD5r8}WJCnqzFgSsQA(wMtA2n*Oms&^m07M) zoqj*%LkAJ8RQ#?S7xvoP)aHiZDl=wvO21AF0^R{=LJGyj3TwX=Wj-#JZ~82JqDU;I zaLt|SEr-FK0)Jvuw49UCn^U#4B(~ANe`8DUYR_V0XD2n{Qz-kPaqRXW{U2q!kP<-{ zMx=xFh_ncOlFbvn1L+-!op92Gf_qE!7Ml`Rza@9#Z`=h+;zgXG)}zU2hUW_PnpH6C>&SOukbLj?2k82J(79QD$XNkJpq`A~{u}yFYul0v;`42YRmn z8Bmjt35o|IB*TN>{%4&n3%fk@@38SyR#y78dM(;xY>35`E89?l;=zDr`+%xY-@xD> zcz)q71>nS%CuFow$iAT0Q#3N`5AolZ^ATo~M{xw#4k4J81%vr!3%}!|&%(?MVhy;y ze%tm3+T|$4iQDrwOt`1UTqW?LgoPc&04f*+3ax~D?s|Y!LgM0~2!`lzfm_q_+bZU(Jv6p+txTJTB0mNI$I`^>NQ0)slDbP#zQS>f38zV6Z1#VJ1Un zeMfL`FebzHg~sLgEv_CH6}^K`hQtEEGeAI>Rr~x7G?k#_4uL?laLvxqQAYj6rHp12 zTWDQ@BEq^cvww3B))xVQg|E|u5-Ui2ycm@f7>_OP&T=Dc!VU~s3#=XiUeJQ@DPT?v z0UHp*V;}UCk+m}bJPw7#lnCi0I@FtM9MBhzMnY`57anegtOXd1_aNHZO@&zb2EtxO z8#Cs9bYwt61qX=k1kf;Nf!W~{ZtCjV39=NY42uOK01_M((g4U5fSwV0W&m)I3cI&& zZG>(}aNTvMdWTl6N&0{ouhBb%tJ=VjRhSATr>6%g9~wG2?pD%-Ab^UgzGX{=c^! z)NqcQUdzy;cwKLKzNz7EmAQjU($z61*r5JxwLu#)z%~9K$s8sWP!F)y-NGr+krjA) zw+-KvFXqn=4s?dg@-yd4ZDopSppdG)z(avAm#OJ_%SmJ7Gyoj57nyj(=Qdq_X0(G{ zj!{BL(H$`2Wps&N;A%s9Cp_ga7vzP@p#llIQE^2+Q*(foDXi*#jir`t9Su=`@+T8R;Vs6%SDLfh^soOknhh4-^UAT6JCrarx_bDR-($aI%*?-#)=Ni=Z z#SRTL=$C(C252%|nkwN>4tS0%0|{XqrzsK<*-aikf>CH8St7rRP>> z+L$A&qiToI^o2aAO0r2=ylv+sC}Fgj75F3dWE>6HncUcniHz#Sfv^j&n-`;*%t#`} zd@{oYQOIYI2UpLK7Nck1502I7CEo-Ms4(BiMeO^7~4QYcbx5UPWe7RpNRE1 z0AOdWNuAdZxth2(%6;nT;l$QD$)|kv{^Wb(Tt#rYr#?X*SXfwqCRtCKJm9v7*Tktr z(N-*`zd^hEyeg_+HC|Ik$H>F8$WD*d&dyrm>*yL7oI{3`P6{>@2-eP)NBzEfDq*B! z=|q6#N~k@qgJ^1ORCC^saEyHnjvEh0{E=)*W3kGCW$!YsM+nmBWzyxl>-%V26K@BM z2b(;|LOeGy|lE4<5oCBw#lHTF2 zE$Jkyz4DzuF`7*^bi(>XoNh#D84NNNcB1Hx1vn#IPPm^GNsKm_f-CB`VQLJT)F9hs zmdA!ZmRHsiV!hovr3foz^lRS!w2z$2Bw#2seDBnIbiXtO*#G+VxgB@Jl`T&vTte(E z!(ML)-Cy%3>bBLgFnf(O#1=qGoJk8u$0^zGk%AOTvjR~{6(e(Jc^Om7$`U7haH3fh zc<(^(_9{x$sW>qI93=qS(8Lnzv$mc_!|(IwJop*k1=rsaf(pN{%dNn@KawT;{vL>m zQW*|T&eZhuZOH$UAGUe#JO6MHAOz%;mis^gC6@v|F7$m+C0ThxPGasdr^Uox@H%|o z;^XVf33d}Nfvt=|0}T5g3!XMhF;sB5d6PvVffh|Y$Q8Y-v*u|q=sdl}lT=TxCyV$^ zd@_!RtYA%86_5;gvL31W9g0@QJDU0V?Fa5hJL6LKlrJu=i3HIC5P(7tBLq;Y_LK|k z{2KETWDddr0<4~W@@HpX70cn<2I=sqb)yf zMW_~HO&mmxJ}rtQHOyHB$@a%B(+x}f2!ZKPa<2|ABhZ=Zx?Y~{HxK&tZ`azC{C2L} zKWvNssD)ubz3W`5KTia_#MMZyWd~zaJ1bRfwJA)d{(&wRPcsrQTPmAE7I3DbSN&L0 z`e^as6l)YS*;CKfjHL9jv?THDCIto`&<9HxiSjx>KM;%o(9nlb*Tly^e51~|Q31t$ zFrAJoO9Ph#Pt%x@E1hadc|}{e3ETL)?J4&YyvF$82ZsXE6Jt*-Dz8k z%J0QTQ{jJ?N5To9O}>X2C}nl;z9hpKm2@~>(Xpe(%czvmY)Er@GRd6-8v@x41o{YW0L$iaH?OMI z!+^tix{sA)ca^{&c$X&w2EXfoH^0aI&B)iQ9Jd_t5-cjwGVc}ln<#h*tQJOp9^U$zjAiU73b={74y_wW1}H<;X0 z)nvJuZsDbny2Ue49H3At>Ylt_p2YV0p(MH3_0$pht%o$9C8C`ZvHz0NZ}Ps+#;6o2 zyF8Mgn<7x)?DCb~-huB3q>E(&_izsNBn*M4@4G0!y<;JLt9rdW#QB2lKrO9`r!voX zl%&HhdFHfgM?4fcBZdOqjQb09`DZ;_o0_&{H0#}&C=TQ)|L>!vc!<>|Jh+-`w_e=jGfC3L)S^@d0fBx(CEd6>yfglG5hpCs>>c_H&_?-*ZIn3vuJ-C8-Wim2!<-I0( ziQl~jXWZLc`538>$GLlXvBU8Pu#5r(mKprN;Jn^%YMrIGkV%Nvbx1||-+wlL9Cqm^ zuGdDQ6c85o=paf|!urU+xaGBA|D}-+j0)Zu2&A z*M!c4KRQJSzD_~sp@vg{x@wTwK}ZImQ69;d)mB= zDNG*bnE{m8Qna+R#I-p2!6G680wet?dec7%PeSimLoW+eBOrQd>D0as%zQcI$=H)lNDjCg^ z!=8IG%bmo$Q#b<5MPpR*EXS@LPuI@Y>x|+#sq{-Yy2cb300;8r53J^AYAXa%LLLDr z$`$30>hXiAm`>f;6|w{=UI8Wb0pYl6n+LU40kd+mk8|H9KC1PIt(ByQaz*r@U^*93 zml3Xy>EBHUH(pr_W8`KS9zU$Dsi+1O(q_KzAz}YHk|X^x8>4|Xu0935vTWhcZ_y|> zD^!mi{99g|VRneVCi5+Ab*>gy)awgk7e$)(4lXr}xQK%l#o9l*moaw_V+}>)rEWZl z@O@jl;iBJTDH@OZzWvlGRvO?p6iGmAX7JSPcsT6)N01tLOqgvKKXYxS<+nMI8bHE; zB)U8av%dxpyb-^6GFB!1vaNabS#2(1b>&|52|Hzm{OaC3m!o%^Uq#iQ5q35(wiS!O zW~7cc@t%H1Xt9Hk6a<#W7)$)!*`EpE($K5q^cP6BlVituS+!W%)d$bIU6+a zyL7$4Td?fQV(qZYD;;ewX4gc?vfzi+d=nJUc)U3NCW2_=g=ZS1Pq8w#Y zrjhvx(HW!&Bn1L*Xk;3Pcs3&AJv|#rt~@2PGcBIl{)aH|-OnO%+9yx>VjpAwW(Xt? zC*O{HB4(cVn5EyQJebX^AoRiCeI8t8%fI4bgdjA3yeL%LEs}aaTWmxK`RM!iTZjwR z!cRt8w>J#Cta&H4Fx-{Qdr{%jnsO&`&mnQ)yz#I#+|^(Aql5iN4Ds{C|BlJZw@fWw4QwJKFYW#E zAOdg}7W3A`IS^^MOkQV%A9-LdA^04~T_f{HoFDr>Mih2<2(RD1y@T8@eCm{iscD~K zOa#jgIq$Spt^p*X4#-3--G)Ci;y~Mw^oxGdpxH5ii7Y4Jy^aRTS;h5i1(j5Up<+xe@ zn}A7=x_ zZmh*pK6F%jb(Z@_^Y)#tGD)J(BUnyGo&zca=-1?%NxXdN09Q~P(h*5Nyv^8a7bI{f zt^P0}!w}}X+SO|gzR0a4A%?^FHpQP)+lJ4x=&fSe6STfOw-Ss|IofJU$jRE-RUbZV zfy#-bOP1SKTa^XI2t^EG!9mBLKdMaqoHE8weMb%ZyKbSCo={A7sY~@=BWmKpF33J{ zMQW2feK`vDYixP2U42A^d>i=|po}ClYgMrvvx`F~-#q@OL-%WtYKekGLu}jDhbqIF z3s2;-(vnE}IcPis?N5+sP<8NI#(wMb8ShDcdyt>eeMD$%j@Ys%6OMszKpjmsE#bvn z{HUJQbyM}=t9?{b=WRr-^7YUSomI>^aS{zCQr|Mm&b288@>QRmKiHXp00zvPl_dh^ z1==Ti9R>y?WfV{vqmd*812&RNO4w~kD?vO*$yB!=CLj(*+Yk)4O!G~+>Zp(EXB9qe z{hL05NT-|rhwGv}S3FoN9!U_yoUeJQ_QYxNG2tzi_N9pg+px8~it>H?w8*2$w)ACk zKCcKQB>tmI`xKXx8}%qIRF@}xF?krA^j12fd6Zh?#5#Xe6tY|PIZi66MFhz}N;%7a zNCo+Ku192hJt{ZICFhmN_-rArUfQ`g+@^BaYsMql{h&d8e$R0>?&Pc2Jzi?0{Vn*1 zo=riGFDBR9TV~e_PVGw?QsD)crmAP&FY!O5<8PE!pIB~U(<}>Ct5+xwZk?TLW#}x) z8cr4dbOS9RYkzdgF=$i~%x>9g_Y`I~{MP93GGY9on7*=W_-BiGOyZVZjYgL5pUp#% z)gG@J=JhMIzL+YRyS9iW_%t|^_3VL-SBsvAF;@;c$6!@OTVARfZy$!sRsnDUPL8{+ z?o_9H$Hgyc1wjS!DdzEWLIrK$HfsZ3#YB%jO}B}#tv287QCk4a22>9QRQ6Y3G7FwL zi#{55;ka}{7bdvSwLU5t)7)R>!hoy<%LKz0N(HHCxuq`0@^EMgBk56()6U^@R6yfR=hEF%F2@Q2SBrEMZ!XKNx&MScI=P-7$#@UV^JuR_L?{uBxiCniWKi^NG+8 z$lUvVe!>Edw$OWcBS<{>a^|L{WN5f}&)B?BLJ~($_xNr4z=bxK9-27_ zg$e8yfv|8L<2_JWV7doa=Ev-kt_K}0{R&2cKtqTaq3~j&kW9(XXTT&>0c6Ld#o_lZ z2-5^&($yvWccCD^iMclok~MBYGW&n;rj|S+D=IA9hwoN&V~iPy>W7U8iZP^ACwKQR z8@-s60bab}Sfp3W>%zid*rTBvd>ol0-llhb@}|eFxV83-=g$GakW~l;B)`L3HduXUE(=j1&7=#26}vm4%uv zSyv9tqr8_6gA#AhKJlgwuEYrk>5TDWKMgT50?z=!QcOa^oW$~kkvpt9Az>c?3SJw? z9$ek!8m-(vhj;xyFF>_DU8wF^>Ragl3CcMc)r~enj6(8$J_eH*Ha)B?ND+wMfcIIi znYhdic%s2Lna69Qg(tg3pOoj$feH@HIk+A?W!Om2C(l&xpS!c&@>*=|Fq+=NKoUCG zblQocrv*h#z9)mnYYNjY9gSl-nVDwW4)IyCgvrBjkN5$Jg$4dGI~idyvF(LjKK;Lb z&fNh!h9Vow51e$;hf3rP6bnFe$ao<1I#RPs_p(M-L-WADVL(HQcD5NwUvOyi;>?9` zDt*f%sU=EP~H$rBCz@en7 z!hO}+HY~n10X6k*x0^Si7ofTq;Eq>)TLT#tRF+nYBSYv`m_nj3^l5Yl*;I#|UFu?} z-J3117bTDEH^|X$w|mQyTC;S*zm;6$H#zdo&Wz@oq?GnLOLVe|Vcgx-l3#Nx8dw9k z3Gjq*48$ZQU#c8{Vdx_SN$|$y2693_Y)I%mB5Y;gb>Y{Ta{QrlKBKu*5si~Gp0Wq# zgP^w%Q)%I(wLgF>C}|73)@Avu)(@STBAW;=_|?|dHfQyFU$X8o&|_zuIFW!bGX{J) zLJJ<2syBw`^-oXV&oLe4he)5&V&fr$Qwg{F)7mX2(HZ!fsFXWw3^SUw4)JStmE#2% zNTG5@(jiNwGXqvhu^fMIy?XhQ@I` z9Z&RVF~<|A>O@eKk%(3NE z8c{lojB@$Yu<%N%06R+n5JyYaa>aoeFYEzbnA0}@&M zPYyM0Y-MxSIa4d2|XfQ9+lByl>|hK9T6-Yr&)Hk6er z!2*qL@L;0Q$ELV;+uN$r%Udzj{iozRC5yk*I_@#k_D9x` zu;H%4Fv`0MXE2?J#T5;f*wXzo?z_XPAi-neyeea8b^y0tChxv*g*@mgNext(Vi2wQ z;~sU8tfad9MOToMVO8Ye3Y8ZhfpR2*>2r;Pp0>7OSDx*nr1*@J7{1SHS#g?QyxuzWq;A`4nu)hggDqdFjn!k$YqbT zgfA!XPAO{J!G}V~K)WLQUOX9uGz`5{K00i*3cDm&lJTi23bIqywPE8(0ay<-TKsD+ zhk!5uwf9KbT*4GkUx`iAs(g?x|AzJgTEn(uo+cg+&qMH7DBCn&m??1W0mU)vfA3O8 zV7Ej0ZnG2^bAYH9J-Ncw0;%0kw4=QZ0;-d%>nCawvJ2uNv>GtmIj*4v8i|F?B}}xz zP<=3db7`L^O2BxSV7eU?VZQ=(3T2Z|T8A&D=qxuQbyIUQ#d~8v2T9mxz@b8~9UmXB zm|9HIJw3N935hEY#TC{VuBhl-xiXFH7UX0oP}yqT+*fX6E(_2FkS^Nv>UW};gN&YQ zf~KQ3LH+%gGZ}Mt$=yl6Ii^KE!H#vqNk%LJt0&W>33onp24ooI&_UQ;8lPZLUml61 z)gp>E)){I0!Y=U*;9G2H9Cf?{_C{Vw$D zb!ZdjYh~aPRIe;~{`^iG14fI>W3UlOy^V?9jqsd&f@m&XU+5Cls$1@&oS?$GV`4q9 z2xlY}n&O#~j>f2<8xQ2*tq!|8&7-A_V6XWw%9B{Z$m+{Ck{>bF(b3T%KcnYLw<%6( zC%lkA1<*Q~-Zohth9>|&T0N8xCw#`&?yt57ty)}?sEoVqhPMT1WDZe$zy!gQ%k>8Q z%xFp>ELjj9A!#qY{~r zUMyF&gxQ@^vYZIDSGg$ zABQK$+{-II<0#?v#QcSz1*o$K+!zpqS-6`UQ=6aBodpl0kdTl+Jg6gXLYSsngfR>t zxxg;wSgl0Bg%@!oNLJk})|sQ);_lE-0j@ZOH}TwEN^k0i_8ZWuu(jCCXKJWxk{JQ> zI{MDBU!66ErO~l`U6g~i`h43D@T5$O(U#V#(Nbycr7!G^ic8J-nNMH7G$wrQlS`%` z-=BR{9uHWkw*f|;KNzlS{DmMEZYCZ+dh^k3PjMyl3s8vV8ucCok=6ffMxKX>ml)wi zQ0UXsyD^UT((QxWrQ^Z){(%$a-ecbARqAK87k-hPjPP^*5m%Q{f7*@ z43R_#ZBj&J$dHf)NG2Wd06yOSszgrG4wL0gyzUqc&@#rxB`MI@(xlj#vTobSv}f5dGhhYhk5PnQ(6+t=nEt< zLd&9V!=#1ZL{JJmhDt1WklED2{y4@q6L%pPaLo|C0L;S6#TK9iGg`TVR5oUov^+s% zo}Z*C-f`uSjzo_~Sy_Js-%B=lo0RGJ!gWjQ(xRl8FU@xhILlQTHL8JhmBT#6#19lg z03htisFaWdj)E9jZ5cagK&#bMbA9{P!;%k)AXFAUSoD#I$VjoNVY)0@A&d;5e$gS2 z_Lu>-;*QaXaUY=S73qr3p0PR^mRk(?VbWRUydOSow;<(~3iXM|NkPhK=ssp&S5?cR zb&&E&N3JOc-%|`tz4g_#lTS@wb8~ZvHOGkFd3oFNx}M{VfMk zA(%x#^uu4PErklQ9!TOFvWwwP+@y2>si^w+>cxvdfI5ZQmD&^@GV-E=mP^e1mu@$2 zSzAbCU;xrkK`$3)o{Y~FqLe7)Not3i1qEH9NeOLKS6y9QModgyaVm#KLF2G&0)Cbl ztTdYKUDb`j5#)3sfy!B(e-Xvvw2ue-XVpdQX#3996-hg&J`XWUbr1I)`P;q*&?b%w zMrr{)T@29p)TfQG3r||uzrMRc2iiRU@!A{kZjrCZ)66H<*w%=VmcSi^Px@0qq~R1+ zU4cD}>IJhcz(%pL$k{Mf&4t@1hRc9w&?=Z!HroH75yb}w32rXGp};#13NXD9J(mha zSmR8csQYQ}iu&o-x4*yjv)Xz-!7CR8IU~@l^t<;6LoQH9>Y@2X!7FDQX-4iHa#;H> zNRLAfLI^BSuZ)|3jpv2&d&YEiFz4?V+}0hijPi=Ei;p`_gTLf@@)$}k;klwTVf>`F zF@RG^j&EoTD8ld%1F|Bm46F81GriqpG+{oMe7+IP5~{wg!S|GMEqYi^?l)39c-HiU zZEY+V=`B{H&_Z{ve7N9F=$Nil=HITKTz6v9sp%1|P9Avj=c8noSdFAbs=hwD1h21} z|7bnRSYJ?1fXQ94t8nX`Hyof2<1XDDebkx{C|@w?V9Y($CA*rRg*-x;#)G9c$(u<; zt{?ohkM3NzhWb<{M<(8NURr-=2RKtO?g`8wOVfA$W32z^2!-lFi$;T6be;7No+y1! z6amNd*ww52sFwaD99jr}%4#|9Enwdx>fk*WBLn)hgjo-f5|S>(&?xvq z?(S_7Cvd>8sm!Z3Yv6H)3XWF~*{<*Ewa4mVb*(oq9r)KE!D^5?S8H9ews_oM! zPfCr=`c8<7?q4;l_nI-P_hH>twxfz()b?LfQwV}JhS+5g=6^IDhPPBwK(DigMQYto zT{-b|G023K(VF^wlNNcN^0Mj~Ix{;rSDXV7yNYNE1fP8MGGXoh{pUl~p3FIZvx%B= zYCyx4q93yQWY<3g#`unF z=I(uw+i0W7T(j{EP8I8~Qwr}|8&4YLDJ8ZS9@pRXCr9tldB8H86@MexN^#zOapIdT z=e9NJbE5Fuou|`!E06hik5~4h$7hU+25F9?vtcS|Qr+hkw$23glN#epC+hipW!{r! zL6<~UVv&L=>Si8~o>Nk&0JsIjZY!%P`=6mzI9FQidweFYrQE6WadBc(1)@Kt|ME^m z5^fH;kZMyLW9*5TKEyO2EzWLx)BMSNettAE?v zM~`s%VKd1VHX*fTy6{I&QKx?^?Ly#@oI|raa&JYI7uF0d#dAO^$?DuXW2V~}e0Ij? zBEa<9p+}d(ERJ36zu;)b2vbYixR~r_q$xi?Kc!>A%v*qe^TaceP)r5QCYGw4iM3d} zH4_u-&S)F>I>Gm@*wSpIKY#Ae(G*{2F-rSO=4@0+Vcy?$Y0dT-jhtM*a+9_}DgsHa z8H^5Cp~(Dk+Yr7hK6jmv(z+L4&g5OJ4jkdZCJA#ge7l>rDW4n!iG~LO+#*;nrTdrJ zBCU!0`eKPBH>QvP(}0&ATHY&Hia(IR!W!Gyaw9btV>eW(WpzJt>o%aHyP1dLqwtF@ zd>0gGAg)V{k@=2>n@n*gNPEA28&FbFxyY~pON_?-e7oOtqZGMj@dsp^9N!cxod@PK5+SU^sZaW#uDiaMzWy8TCQ%G24mH`;DH}Ab8ffVO*@FHt zpF~QCKID4dGPJlF#9zb+sON;TIb5F`AzR##>%q9;{iSTG28r$qPaK2^%-6`Xq6w#h zBrnN~a|!DNUVjiJ84U3odCN-IaHG=!&{1si904CBMLijULNJBaiFFI# zz^>(+m5sLH^RdXs-Cfwk1<8;^6s(?k(B2P-Nwu8b8Iuf#1viU6_x&RsDtz8DsIAKq z)SEVv`%kOJMUy+fWQnpAKZY@dZXONi9zmQ(XNjH8b#6aaUqm@du=%CYA<8DOOLI{c^l@H2c`i`rYXW<$dgyLFiDtH5Lo@4L&?p+f+(UM8I+%{4!Iy8aVAz>lsl1&Zc zyK*(1ZkPa@bK(UUyLodI%ypmZQxU@_zW#v9GZ`4SKX_jN>P1#o30SAr!86w;M^X9%`@7UgTN9u6$k zuixACX=S&16O12seRQ!kZsA?5>ILssx#zz#v(?xnNSYyAth9p>60ykR+M^q$Td9eX z{Z>LP=Fe8&lT?)(?&>@K*d%GFW}G$B4M{DW9#Y$XVE@{gg9GwB)+xoi#n$ZmL$&`p z(0e|;Y>C9e)ZM)awE$KP58&vDM3)i=22!|a83)f{!nnS56)~E zZ+L0&zbl8}E&}#{l!bW1wZ~4WHz)BQ8gY{LP?#?`u%;Or^ zC3?sAs0aG3LIL#+)hW5(Upm^z#2-U&Sn%tnTxZvLlkw7!wEENwjaLo%$M5Fckfr_i zjrZyVEeD{0k%xj;uk=+dgJLoq+m9`-ub|3YK|wuo`?wD~y=y>1A>C?YX`D?eHTrdj89<0a!qPc1qR$oS4a%n4>)OE>PR({ur zc?iBHHa9*pvJron?SGdW&8X&);|-`w9WFc%Fk08uRe0-Nm)jni?4gH$elOD=luerb2_wJjm{}1Msc6n)y@fYgE z^p%qiCLr|B9~j&2c>|kZx$3fI%XZWTt&1Fco=H-%hFh%Nt?#F&p6-ib!?yd-KFwTd zbScUM*AasVBIm9cryP~dqKyREwzHF90q*jx5>FnPlOjhElgO~y?===Zeg-ORGowny z>XWtJWKY?g|Ww1^x?lCkPThD7WZT#hIU8r9STd1EFS zA}m8^xo6_>Vj` zDR1-enl4l|jQa-N&UoJ+^?r-2J;4>wD!3i2^RPWA=mqElWICfUY%TDE0FG7El;>3n zzE2n5V*TdyUX^49`IuSSz4whbAw5$3a~|mGyFa|cWb4~|E1#4G29yeSfKg^z(9Yl? z_4;zA{ixNeEB#tC?;<==LNHXx>;nb6FT1lQO0}*CsylQjxE=%6C{Ce`Em%oPTZ0mN8$T?qx+aS+K+AU%P&x><$aP@rs`n5vhk1vs3c8+mxn~gP}Oi1k46b}H! zm^v9>cIs+{WhVD_etiH!1B8wEW9keFOX%saA&jn!QPM436AxLVy=&a{+6+`ZKOG`_ z4&<9t#)!!ckR;>*wW%cpB@UJlCJG7)0yOaY^W|26DL`)PYh>g#_ru3T#onWB{_SQS zYglR6aKN8;)e9VsDg4I;(5I-UR55|-+{v30D7FcgE-BK)BCjJOD=Bw1i;j=q#)puZ z((%cEW-jEUF6*orG!F&2Q-uE?SPb4MA5(B=wEd9&HY1{&$miL=e*;d@x{G%8oB@ES z*9L{JlbV0g^@=0S^7g~F0^tYqU{$dcTuk|-FzJ4p-hr8c91FS;o&iGy0)wm7f}i*x zW4Nu+a`s)RJ1;1tUaM!Xn1J1PWKW4{62dKd6l#W!ZQBlk@<$V zc7gPAJgr_^&vRkIz;npZyu-MPj$CtQ4Q$is?zIWo6d(;`r9J}&us}OdhQ7(Yd)%rk zGKo#hH>@|dG9$hPloMOqb)X_#WneyjMi_iBnK|83oY8?%dVlg_YF5K;;)0F??Aj^{%O@YNi7wGn_ec=Jgj(LYNtpQ{2-m8O<%ZE`7yy>)OG@$6l$2ju?b*o4i+&TZO1rG583b;>&FL`XUQR0TG_Ble`#Nl*XcKG)UgY}lklwFI z&^ugd8JQ^(RB|3^d1Gp!n7|a>2j`{47XO#tp&RJ#*h${#DYOfYN6%>!GY~RSC<$E; z8g$$zoV|!GG_7!81jT1K$Lq7*DPnI5l#RUAIx0;thid?a{&Cv4e>+l#P+p0@gfRUd zRCL{GA>AGjC(|V~x7xD0>d^XG1W7D(Aj&WqPrdx9xEOiyd7CX+&Tp%avO_W_hcz{^ zsu$Slz3=c9YA((=(k%&*JGY#93S#4-5psN*?(jcPnQQF}4t?(wc#O)%Oi`^4K!==x zCq|Dm?FGB%Ts}BdZr2~|I~ zS7d&wh0=<(+@lMj_s|@5vz>unsG7lWL>dY(hGu~O?JSi7c%T_yHn9h*r&wj&hwcT*C zKlzZ6`UZS*0hoNU7c?%bp`Y*SqkX%R>E%7>QUNx==tTjd|JU$DU9LTNKOGS0IR0H? zwc_*_8y$VAVI(^<%i67Gxvp->VC{&!vz;_tvMct{JCe&v<51A59?tAh8kgFZaC(wozwUe^uXIz7;9}$V>4@9WeQiUhwIA*bx9?x>bz5WYru^U zUt9)nC%{sILv9gASHmd}aSG$Bl)tR>jHgaFXFH#=O%LyxgdsxUzP6&D|6G(d55L#> zPYeeR%*X3YFd&`QyRMIOIAa!j_flc7Mi{0+Pv2n{(CQze}OZlhvyjv$3JX zM#mBNS+eCgfK{OScXDxFW4GatW|`|j2{_yi7p`>aQjY!$ruvWQY6QN*3-!pl_H7zV zh;Uj)TQ5$+Y*`S=54bGo8O-miF4uwIeQ7wjO!bhjz0JRB*17 zlbLo9aJZb+6DLMb{cfwT(&X-f{^{nQSQQnu))ilT0+1|_q~Ufwc|gC1OJKoPo;X1I zEYFnr7w@HXpnH?GE91XcUv@Vv89#LQ!j8Boc1um8!1dYLtFdY1Kb^e4)q{jXMl)x% zhhDcc=?fj(-2^{+gBJRx%thI$HrS(AgdS4KJXXOC`s?4;%%$V(BR_ur(=wS1M#*J+ zdtzJ%RO)aOQ#I{m>>*~wIXS(@oeRUL9rRpyN1athI5`!DgkF!?xp(C`mNs$vm$T8N zY7So0+h9#%68QQFU;sTm99iS$mhVr=UE)SML;ooB&9rk4bS`Sms`{&tg(y zGmL?#HZYY#2<=N87ntqT6pKdL{^gd~%kEKKmv8rY@#UyqDHtc0Ni4RS|6I-)GdDLu@W`2os|b=S164U=9LPX%_1x(Opu-j$3g6lMzTbgJBbSu99$z>#1fID!d_-Y}F+d$P>gJ~FNAVFRK8y~DFLyU` z>%4r?qOls9)Vi3ucMo7y-q5@}=MWlR2M+4oUbUHKv?{LPYCr}8A+Clboe0H6u zk%d`hWp_Hs{#eg=`{pkcf*!Wqx#x<9f_rx$MBel!PFYwPZfBNit7QO0?c&m0rmdQq z<~{Z>&X9>Ez%)^xBet1?D>KJ7Tk?oC?5M6-)H?G5{g&<5XSjdjkH(K0oV<({8sy$! zjHO+RPND0J4Gm+u=%g{jFZ#Sx=dY{5w7~SvZ>mcY@8GBEs2e}0PZQcKUI#18=1e%i zHQHO$vT?l`o8{^@0LXDe*omIvE6bM21OhLx_vH#++;Sw*eY+d=-r%Wkcg2^iBzE!h zli9b?fTD~}a}dYk(u}iu%X_8xZ#kSLd0>aWwikTl_oKt-3u~JId_g;C|jke_IlNxJ$jTPi4bfSf8D57tXB?JQ&~DNJG!I+ zDUt{N40jg}V_fSxWzzpAz$DsDmOC{ozg!>vKmE7&wjx2C1GMo!c#c zu=%C2B%G*j2h$f)ZtfauvOfstb`r{sPCrT;vu3jiEpU77tG+BsJ{=hunpK0onMfNw zLsfOs{EHsh9}@04uBT}rTY#=!K<~r$i8^mHzaz!-x^w&)LJs9RXt9r{fzS7hu!0q}Zv&jl5)#=;f%ZTMxA1`N zdP7URyL?o7565duxyMZOWhId;=iQg5dwY!OvFr!Mv!uiF`L@|7i5&GKxwgv;J(qLe zzDBF6-oP5$O>4B`fq;}Ny+fDWT>b(2L~cpwI^}GqKk--C*XtT@*KW5~{Uq3~HQsfq z8#bElSh9L`jI*uo_Siw4I%Uqc?CQ&DK6>@)jIVtfcg}rddgFC=|D1{l|EZdk>Zpp4 z`I#M=xFf(Y|CP&HzZaNy3F$woVN4UvnO4B!OMHkFShL)}p}xY9>){epePP~3kRVzt z5DPJxLFos>-f_QXS>CU@6KcQC9PAZ-i# zzt%3MG@Cc?sIztHK&z<0>moWMa-po3U6(A3x-9=i8*EAC*3J)1tFHnA6gqiU$X9B(XY8cjtw8 zkQks?w20K2FhexT!~rh5i~BeH!0g9Ivj9^ohqh;3D|FrA^$`~P-q3) zOdwhIOg&?(*9NC!=WmuUw10v5G`OC{)fHv|I;FHd11NG`QQ0;@J%2 z50q;w!yfV3p?+GIT8JL+)uRP=yWf9$_b!Rz4JrI`*7)!qNx&b`y5T`*?hcwU`=LeC zl)zT|^Aj#6#P6i0CzBVBNVZ&Kt9rcyV}BC$XqeMj1~Y(0nIT47)U9H49TA~KRc_#J zv3t+VS+o3T?I}v_B2V_ofI0<~2nDbKG%Mi-6{d#5wn4lfL^%#vxN_x6d_r(6N&dfxs9Z zy#pR^lU$C6A77PYq^vNA-9}Ix8t#9foz^YAvCV;TS`Ef2PL@z8Jp05st0T7E?S$qqllsEvUiV;sDFPI zbC7LEPahmA(x}lZ+I}*rG_JD6g;@#%r`WHt&;J2WB<9#Lm8DJ>rXQ-dXPilw{yEvn zw9p3C>&wiUWve^4I$>X+G52DY<7Ep^14`;U9l~sWS~z;-8$hs~+9CI4@5)8VxxUY% zi@Ka+5wZ>H5BGDqYt;h+ki~6l9ViO`>d^YYGsMNwC?%$=!ErU8z z)HG*!$5MhjM6q*rvFl+Mw}E;F?~s}amB%3VI}tGkHeD8Y$d4e%`Py-8*|W-d<(}~d zsXD>;Uel(1OP&2g>*!ym(Y4^`o3{2#QYM4ko}ud$)W{U^o_);ZR*U z^KQPDQ`Wg>ldDKaQ2XEPcTKp?BwaeJarXn9?@&C{yfsYB+I3IIvn>bh&;1WvvQ@2i z*UNan5+@+BxR;Wdgtew)<(DschKW_%?th(WW!;*ioH{05x*~SLz=7SwEofO7u4qF) zxP71EZ2dM{?lo4le*1%R)8Hn%5RRaEB`KLNS@L1i-7)FICym#0&b;}AMVlN3_`4_< zuecQ|37OGj{H=e|DXdsqu}fwAjJI!YE^ox%A50 zvn5}y5@|siD(k+dsL_uwjfwKyq;pT(;E42uL}cOAl(~D28UPC3PJ$1;7M_T^YNJ@z zcDdWTWS1D6+w;SIybsHX@NJmhX2c~Qm0`nuzQ6BYRd*}5nnmW1oom*or;FhVIc9m&YtWO_e2MfiV^EEa=yexhgd<+tRSWB9hK)NlW${hI<MCM(||9C2?Z5C@&pAwJW7gHFJnuiAaZ4ekKFyH7W*|R=yX~aT>QwD6Qcff zz35{l$L_wJ5%wiqM~Q|D&Lyqr@~T;V@Cbbg;7QThC0Kp zy-GM%5(2H1(gii9eDIAXN1Ca=2)7tPSt|qxQixOmjRsO%0!RSREY+Q>9e&Tu$_>IU z6a`Q!u>GnM%M+8oHfI$V%-yWM4c-npXlbb=UfEGmt$14|FHzmyKzsqWy^M1=tsA?| zya85_z!IVypxlM}m_5nC@=`s@2Sh8kVLL&kIug-?Lk#qpUv}l=o=YCNi~DQPurWCt zJYq<{et!n-M82Q-Vb3s+)r(^nSg*HMd@f9UiZ_LVt_%8W>?z#vtWyK)HVdg9iEq*o zu=}J>thPlX@}unkpsS)#G{M0xA_BzOAjQh@=~oUFBH4)x|FwQkDX~ z0GgeQKwjLG=_DhhWQPmAf(t=JqPjn!9j{9+(XvR3U)oLGOgr#y>*yJ04jPR7NPgx%5OKi8HLG zv{|%t>7HYsir&8E&|{d%kP#KRV_o@|r|niL?d;X2`R2ZT`gAV25Z?Oy!uA;q8{CZ& zjzn0au@k|z^FZY-x1x^RSl^`or7UMLSKx>;`~kXP8K`q(H85uCroh0!W<`hjE83}_ zdfLil3Fn(dpSjs##EKb?{k}~2k^>1d+hq~!57Dld6t3TkoTY$6nIRXIkRW=oxskz zWt!bS-D7`WdxN&OV$n?&tQd(t`KL8vXxcpje|hYj&#{h|XB^Q{0_~zHp{&#&*D5sC z$#z#f#S?KE-rmqpZCn2S^~c}kZ{HSGJX75~r6M}l%ipZsjW5A{^L@5Bu}~-0E+oEb z%uI8=n5qTYYj3{|2>X(K#rRE)$Fs8N8HtgHzO-GMQ~9AX`}?7l*}W>SeA$xws^;SC ztJ{X&)6WV@ThlxKer?>VRj!q*qbAqPczCM#-02UYK8Lc;?rrLkZB$%s{K;r~V$G`R z@BCNrn%-!bJ+LbgU`#WVPE1r4;25{zUuD=i zdA=AJnOHRJ)92NHt7nBd3_m;jlhc98X&0BKUELn@>U+tLGj|qVTN^#A#k7D<3onL>3XKt^j{QvnN6)*bES@*wR&EJ0L z|Kr1*hSa~?>s+LSVk!&!&Bi9T;%&>8=|g@0_jlY+#1hY4h^{mUP#q?= zZm6;C?F82sF|3!0{a)ZPlYeFo#!SaS!QB}JtrLyajcNJwg9z6VSqP&4?W&dv2)z35k%xldyc9i0(ih@esQ}<{ zH2vFNv7bS%W{Cjr)~Oq zKPx5)TPR^|d&aLip{Yb;wzjQLE6TPG{zfPIOAO19fg3Enze3WRvX3WAEPkE#if9q8 z@I3((8zGB6U0--s0M%q6WJA|h6+>SwR?XalV)p9QKuTC4u;wkJ2lgIi!b(I_Sc*U& z49pavcoLK4=jnmD(G=KRN^Un3woUl1;>Qo%LF)^hWg{-;h*`+PfM>#-0BW?@!}pUB z9TR)TLlg!bns2T-07Sjbd;_z^DK<7ou3pvP3rPb?&j>Sy{+$6O?(8HzQ7SiWWazcP z%$hQ-B?H?F(<$`ISmw>N5NrTEGp=TXxv8n*&!;$jpy~Tjd1I|B9I_C}02B$TBwjtH zS|?AQ^hLRHEc^S_MVDtVUn7IDe=;8~mCS0m%>;ScP+Yh7=Zyb##=}>q2+T!0!2&#X z1|X|5IsH`ZOs=rt3;d7CJT2cLjq_sG1fYNvzyXJHs%e3KUyLLiFs_wix2{~jZavrX zr_>IA7a72c>Ivk7PbNY|S^abv0z;N_8`%#g-YFnelJP*)>BWSX1vB+9qqw9SNR8yF z!LNx$nZjToAF;-xgd-23<^0FkWCujd0~gZ|^X2_Uzmc z--ZV*8=uHvw*}j&{9yYKLEtAq59ue-(!SlVOB+qBina588(5D|$ib8W0FWeViMa#! z&n}yqFc=UPgji0W)$rPl^!rnKbelQqcjQn1VADHPjtO`I1rOsv+CPy|4BS6qsB((6=PdMt}sGKAK zGvCo8m4HA&>&%~lcEDrqey(R|s)+d5uq7p#Ajp(&UFIB_uE*R&xQI-(dE$Y6WSreE zwNN&d5Qr$LTbr$mh@erv&4d&5pC<|^iU9x}5dQ!`ze9h1M(W?6#W6cE_+7ptrO<$% zg!}?W30M$URQ$LA^&UY(5`6??D~Tnrt$at@DAFQK&fGa(n|sx}L7OvEKTPh~l)65T21@W%nZnQ$Q*3&b z-BOonF(hb?GX0mZU%;#WRB6Jwe8ZrZ$IDRi93wl5)L!+JId5DOP%7djlQJl-cerwx z5tOoKpq&iIMV3MJFHB07xkX=$!{bAq57eUJ#1~Hby6oZ{gH3oP61OkV;ByKYiy8bp z;JQB#c>K|G_VIy{3EzcQ$`eeWC3^`n%hjg+6ML;|%pwBnDU&9!nBZ2y_9uEXE+Rg^ zD1E#?weUP46{ficrhyGvhqoT=+AiMHCS-&jTQ=N{IHNacV)@$m28dLHMRU9sIt(;f zAb)LSyn~zdcHu}lKy}GvNtA?#bj?|n8BWH zMbIgZpn|4NOM<7>tj%NYlZK+M-pF8<1UA@0ZammF@thGn7`Ra8ST!0>h9rVX&jwMX zLl8?FWg-f_OrA(bgIK59U+D|q0*OATiP_^vWsP)02D}KXQcVL{roD`fl(VGnqjbNM1Z^~ps zC$K&N?(Ec_}%YshZ?)0^;)beC}T}Hbhf0 zXGRZ6FG#Dx$rSPR7PBf3q5Kj^APn#*!dc`w@*TaewWKp56#X#ZKZ%TEAx?taC4Liy z5f6<0>KW%#XG}#_U_cfS;EI68XX26^&ivrCrRhP-?lI|@)#3vI;wx(;-}(QpebO@+ z_j4S`v*Cnt7BR+ViUlpFkC1u#X^)w)mvwWFPALRynz|ZvCUWS2Vne?446F||jGtua!Qrf@zyacGLQlrw;`&P}ZfU_s-o3Jps^Jb_1=OE=(oOtmIqbU3OF>3?kZTy$#A&U}7 zRFd?M?@PEOz~m%Zk?JswiOS6=1 zAL1&rQlAp%8St@%rnpH!1vKoz7yUy9FwJpGA3l$u^+*7wajoLZs&7 z7t_wS-zv5&8%zh@eDkQOf4_YBFJab%f8%iBuw8HPz@^z~l(Pl_W|yQ0_uTWVU81~| zkvGp7BFe4jF+{cMwMWO^Ei^Qg& zfhp&}E8rpVJRi{sJo8xmm>}zHFk3PipzQ%?b}48FwPvIeLLo zfAm9mgai8_(N2|=Lcli6iv+W}>|6Rug7^w{0FKj!$ zK&rpb>+~(}Goo7Z!i;2yHBKHk9_;+FpIT9#8n)+3u%+JNI+ECdJ*1Y|qOOm_NQx8G zWRe+jWuboJSSOPvlw@#avdY_mrXx;ZE-QjMaydW#GnbFVAchA)0q<>wnVozp6{=mw4HIrv5tZmX`%n%|H}%(45~Bf+2HM5bsR}!L|WJX%$RdHvYm4=@kz(abdVTl2ku4E7~oax+}ZVCQ`awZcE z>L2Ph2f3n1AlY*k#sVNT09Y}SZLqIkhpY3ux+WvO`oRC7>B?^O2Y-zodg-_kSVHl? zafPC&Wv%<2uRfQi>k+Je{xfs`&dn5<;r$Of;h75FM-t_A>8g-;CcdBGxs)u!ttMDX z2y~16`CLb~{{304C%g{bZ2{qnhhU=3mkaT23|aNYg|Z9#}=;J^3W;`>TLoaq3-WD&JIN8v!(R;NE+VZrLeY zhEm$gu>g#qs3qUg+|*x}GlCf5?f!Druv0Zhr>A9Dm>Qp*vGHU%1ahEi)VyI=jDDxD+N-g4z2Q0n zl%OgBw2+Ij|3Sk032U58Q`5+ijDCOVO}O?o;a%0q9w+Ah#C>YDs3EA&I9JqX0j3V4 z7j$qAc`#q>*SVW>`dm4-w&CyhiJwJ2rR3q;!0iI>mkm&u6&S`Zio{bViRP7#6Udr> zLn5R$Y$uRbWb2<5?>qJM#g~Ejeii)G-R5xLE=>1w@bil~xKLFVEE*a%wy_~zGp@|7 zIR0O9)~EWXg!oM%BvVk>(09ej*Q2h$m4oHi%`rM60>g!q@-w~Xo!g^0n-yC?jyOtm z#som&YzsxVYb$sPJQd(4v;VN}u^hnw6|tBI8fk9E*e+u~EwKEm5E`tg9%)Fime|C| zQzvE1gmwO3cTjUxe=WW#i4IbOG+z^S*p)ESx;6Y*rXR1v*bv6!9YD$lu#eTGXyWrR zU$3dntsACrm=UCBy!R;Ol)ykGDbN;Q7E}g3VRHvp&y! zf<~Q}0}K9E`=eLxX$VSn--7C7XDDBtN1@6UzKZmd5CDj;#0?6<*;o8@z_%_s{J!Os zb6^+b6s`kTL)6Tm`LrwBo8j)NBwY_}5e?0+D$GbTZGTSx|C)G9C|RyP)!Cs}A96hu zlCb1h&N+}26qW{7MUEXsOH2%=i_HF!K-GZ-QCiWc1q+Ji4ew4XtkT5!+SVr1-CLPi zAG-3hK<8r)tQkM$&V5G%QGSb}2TTbnf%6|*Nok&Hzmjtz6e~0-!o~nx=IJ8fODV}I z?Q(HWcUve6l!;)|^w3C8r0>E)>$u^z-z9~;h@JpSsKMY4-*kzQ1!z#7{7xIzXrS9+ zFgKxMuwx!F=+mL@aic7vGoRdhZ%)jj8n|T)4!z29LLNWPjqIG{yb@< zW_Y3DU{r)@9xXUD;4!FHzrEhjLJSg!*YWoHncY%!8NhtZP3#0Gf~emyT?h>%FJh+IuMh}H)q0D0EfPCHSj;^)Q7 zY58~5$x3;kN_I23q>Lf=uvU`w+gl>K;20+8TG7LUH+@)VeKb70zL4e~J$ZuRNswFY zR?tWSkOUeKmW?r!s=MxU11L8IRBYmj{MF4HMsVgh3?>UvyKG}@{|h|^|a5`PmB5gs0HqV0$& zx?!B3sp+MR3pdE}=|fYBS0z6A+7R+16&&CkrQ;@sK*nL1EBS@%07MGYH6nse{b}?E z3KFUphYLmncg^N5Qh))egoit%CgO%FXokya_;g)cdHlON;^fKa(_GzjusT^_$wM-^gxCf&7!u}R!6+EHvOIt*H- zz%4Iyvz_1hzF!rv2b4pr*-E0tNo>*So=-D>qQq`~Cjj|NRj^;QyB?$F;h~C6>w+wcEGWLzbv8 N*38oMoXMQ^{|CDRyRXbSbHHNS8q=rGRuO2uOFQL6@|WDyejLgEWYAH%NEao{R7Q z_c?ov^Y3vO;~Ssv!(y#<-}j7bUh|rd?+aN8?CT`gQ79DlGf7c-6bhXJg+e>NiUFTI zVR}Xf|99E?$uq^P@Z)mztq=S+v5lCDje@zpjr|)dJ(Pi&xv3trwXT((o|*MKbDMSa zYGJsD8M)}GmEId0LvypciiW0oC>i(%C)-_7J>9z;Y#a~ovhne=bMbTV-F+c*_o`S{QwwyBFD9-okwlBpyKY zxX};)nUs|7SedPK40j-<`$>q9^WKXeqP~I&3t@7?-)$EE?)>XcoUA*WYNB+Xoe37H zGTXGAYsDuc+mZ7lj^;LPjbK!^u(IM1fbEf4)V_v`i&tPe66v(Ngtl?A-8C~aQ=jxK zOr_u*uAJ~;Qp=)Qg1^825UwAyfPjGO8I?RuT6%6J$li73`6Eoj_UCZrsSJ`+VbG{=+eV?9|mi=`N>JmD-rJWre z4b7)q-KNm6FkMqTQd0j$oi}x#uC;$*uXn0E+Fj1KT~;zKrx0@5*leOiAGE76 z3?di!EyY?X`tIHRtgNhqgM&ZGQiM9Xy6gY856c%~MBIhph)T-J+36cypkWzqDNN2@VKIQv4|& z%Wr$Vh{RP|yrjS2^Lxr8Vm$;W^2aBxGOOg4v5qu~8f z$_XFZx&0dbex?YtLD5$Lxs`H%||F})sBtD*^fJ7dT{8qqCQ7Q zKhM|i;C0-haXsEke&TuEWH9G_?MF;JO3JR;mN3(-^FC#5%f-L6M|-QtwvLuq3LgDS zBolJFBOxIHAGZBeNL8tF;j^A^FP%Fz87qsxCguKmLF5jOj;bU21_pS<#LqrrV1HqI zbBR$Y1m{^y zJt?zUd;<2(=X&%o!R_b)JW+R|%JuT)%P2Oz);mg>YGV6qV?!mD)O&k-E=P;0OzK4e zumF{j(^ul+7$G4cNvWyZt&G`|>+4-DVf3nbZ?8Rj_RPV_X?|Ci5Vu9!%g1Mwi_@U< zAxak>^Lr@YAaJHBm~?x-gVV*u1xs03d8WY^Ph3$k9F3?;T82iJ~(b0g9m^i~G?#HXe*v`(*)GREWY*_U^zP{|J`lhBPUiY(#!m%G> z{#Z2!>+a{qV|!a|tfV|g2Iy?#&g(+U$GyG18m8Q4qwi3^qqN-&$BLYGjnLVYvb9{t zw$H}4_1?ZEa&vPN&r~Z6U{uah8DUn+9FAG<0;gZ{Pk96-8JLtI-sg z08it!p2uRX+zIXA_1Rk;QGnf8c8fec+Rb-F{ywH_Y9Yb>cXxLPL{F9Cp`oEo*yV*q zt=PD@-2C#RE^|9OZg6h!d^{48=w&Smr+@blX8$vTs+Jc>OPo@ z(w3Bw=}@4<4Jm)DnJFqNjl>;=LZeq?WMuahQn0>%|K87<2pd%2*%|ct^Q{=&lhf19 zt*z<7TwPc;oXudNF(Cw&fid^_cH)?Qdx*4k8l`m*oWJr})6vVt zH?UzXR|yasSfnnL@2PrvdCdgc+NNOeqnwN9g7#o5uabo4{t zQtO4kuw(poE6to;f{A8IR5}buE}|=ez0S;68*5I$G@r-xeP0avVE_)Kutb zZf@>AX6JtP!CgShaSmUYEu=mMMEhZ(~ z+vZ!4P8uP;ZSL*;hRs4l$I#fTk<2p?uJj;r znG6^BZq2nV91HpS%z8ow4tYX%|Gp2r)=hGL(cPuK%n`W+VHtV(+m9bV?lYM-BG*$1tZF6Ej;VJ>!cmRpevrw--3LB|o?K=q``Eu8L8bScQJ5ZIBM3a}7XC@F86BDz0pq^V? z+_hWqcB%JgX-aZOx`2>&rPJ=l_O|ZP&LX!0?)grS+{#4t#C(2xdULZR2Pfy@e)YNO zSR^FVII{z#EPK}Ng|05HO4b^6^GY!d4YGWLE`6{8NXk8x&h|0Rhv~Dkvl=23u!5D9 z6*H$;$V@#KnJGm!jvv`WVov?f;o+i?X$JH3ZouECD5T03yz8zT=BA?h09kbtq7EMT zY(fGhj_|=<^NLLje?r#zDQ`+7;R0&1INZ{+TOaR0_SMA1Bx@pT;<>2kWfa&b0|e~B zk&&KE_2T6s*MlkVubc)q!KRR14kYEt1LIu|deiJLAe4}pNUhrxlnh7k(6~QJ`(K0vr7ats!ww_RD!uxE;JffKykzkl|op%1c$Yf2rLg7u>nXMQz({~pYQnZyZ2y`De1Lq*SL>GMQ0fTHvy%W%4An;U02T1 z`2I3c9Ki+kI3hW3>M?{5CeiW9si=6q(Su`B6)&b|SLpywArp4_Qc&;^4g!HEixbt= zL*1gH*O`$R2|x0a1wr1BXnlO{@c!?Q9+n8=SXo)|+AiIppr9z$AOh2d7tqot!iZwg zq`G?-B~V@=()3~~1T$4Oj*5-#&kE7S-8_H8nLg02a)}>F-)Kh1!NHka@>b7#`z);I z;u&A)`S`xp)I3_YpRJIGzaA)4j$l%i_q=$6tD}3jE@QJ0{*RRi$;>@?ioGV}KFF&2 zO}9dpEo^KW#>a&xy3#MIn>CVpHc3gzyAK|;tlX!v!qd{yIygRFSSGQ%D0ODDN|kI5 z4i8`c5WU9S&M4nZdnk7RrY~d`k527tbId4WGNL~4|w3n%xkKx}KYJ7c(((T}V2(?6X_)_OPB}Sz3Q+HU&ZE9|A ze!aIc1Tn-HVtu~V-1FObc5e|pAgngWnsP!77CTb;LzOpdfn>k z>gN7_*Z%f=R6qb$Y;5e>frGjEx{I{6{XbviaGPalXlXY`%&Xb$R#emq-aUpCg@iVU z5h)M>Yj(?b;et20Dk@N3Zd6ZbAXxxxZgOR%-TiEv5;74gP4VY#s5}P;Rf5R)>ZpSS zp8!y|u(wZjJ3Y!!$*rSPc5>o3uiPbo9YzYa+qdfgM463M8+1_&7n%A)ZKT&4J~d>J zFbC+3r*dF-d7!7l(fWD#{l`D$lYIgLX6rm}^q=za@l_no$2d4U&j8*A-!chGcxZV0 z&6_tRrKPw;M9oN%_WZe@?KxDt8(UjCo70UD+tCdS45p{2ePHLIbV2I0Tph8hqb0@< zA3vIw#nHn0kt%d~S*j(J)^Vd2a|oad_WE45NSJZLITWuym9r^mpND^g2rVihp`MRT zDI6OoV6T&_0x_{Ynv1b(!0r5We{*{q;@3x5T72<@+sORlGe8^vAXOxa2ZC#Q{Q2_& zHQ)aADUwt-Hfkm&5>n%xTFbn|q4;4|D?ojNat`1P1V6OdH6z=px-0R{YkUA*#lV{1 zjTA9^-z0C{9XbcVshF)rZf|ctTwwUr#pMKBr2Zu3!p)?0i7O@|8r zTi$GFY*g*q9<}b;+#S%~h8&yce(olGx}yML|9!f4IHk+R)0atSGb zFL>JV@$tm~V*PxXh(XF@_7xw+ZU)U@4zCpWt|48dOyY$kwQAQIB1!uKavQedcYnT(9G#rqM9QC#C-u!OQ_q)Z<-@`x<)rR4cE>2z zdj@2Xk!8i!a~-A?W`4|P4C>6{EiIc_nGTAHV$D*c{Lx$*`c)?Id-dq<@){}bvumjp zuGm%<7O(p=RCgA8yrF(pC^plCic-ATY%EQ)e9u(dxxojwz-(;z=xk*uA9Zkaq*jj6 z_fx?e7z>q=05ZPND>n#o95!{1_Sd_h);|OTQiT#8pzozDVfNDb#9(16LBUu+Vl`!} z{;XAZ!IB_y$QK&*X_P|tl>l2u@J_~vQGW)YH@ns03ds2=uyoI0;Uga)hSb& z1?R|xed*8EmIL(kKE3h9>(_S?@BsL#zsn5(GoTG(4`5U{bsBsG-R<0?kfc#+EFa2h z@sK@Ew}p?a`m>+$3Io;c`Kd@pwEY!-2a=ZXoVQk)E)Mr*xFY9zF(==Y2fEBY-1S;; zqt7Rt7!mqpe~o@gfHKkkx3Y%ijhYRtx?CID=NO+}uq1KQjtZ^U#kE$>15iwaIEOep zQriHp;R}Il6QZlCyFi@sK$XS>QohuCVe3Ni43-}TAQKKELr(C=j~@Uvi{0Lzr9*tn z1Abr`@3V-2)oiovjo`Gj`^$vAq=&}eUlp0C#?a#-d~36GSXhT6)IOFL7T=(L?>D`7?_M~=tJZlpd3kxOOb2(sQjjaGdfIu)y^VE^I%D~s zdS1uR8WCn42nq_CPma>c8*&HtMj}CsDMa|v-?kfvHW6>yiBrAIp1?4n2 zE4AEy0R$$fnZb$P+r|V3<3QnzlqzMJqQ1BLf6NS@p6|BEKfZtgSEz)TBkD(#LtNf+ z${N_uMBSDtjdeJS;PdI@n?l(X_$R0K?46VEG&#K*^73RXnVi#+b2LznC0p5O-kMf% zIuwZ}<|X>dOS)J>v{>?4_Q%W6f_5@lO=;>L7O!Dh^?TTom(LttgzBpB>T3t-ow>6A zDqN_lksl@e=Nb0Vi6W-4uRGqAno6d0ZPm)s^1JPFKe%A@y{BFX8M+NzO*aIM<;i~{ z_ETlueqCl}X2li#-yVNN$TK`T>AN&Emw`B1}g6K2N@}88yIstcxZFl%fyYnJ$(p}J8NSx01Aw!rk0kRM*Mq{ zlagM*$u2v56)Epl@TeHAFC-gS`~EHabRgjr0-tX{01XF6C?v;e$dv#(pq%PYuyrdN z?^?GjBR|tbpd5`hkyq^$=Yy4zA}_!4)LT}1gj~$-i#X=*Zw*LkFlhQcTEE?UAGalv z$vw8RRlY7A%PhHd^3^cqz-@|>ksjFb|KBveO%fvWU1>3j!}NO%@gwcX@dag0|*75fAwiS)nBWmqR_e1xZ%5 z*mSIHH*!G0ViE-uQUEch7a(#Jq>o5Oyw8jS39quX;^5ZY=Hn)-MiF1WNZQ!g#3w|L zVvB8?mqDFCvd|U#b|_1?eK@S~{U@p3f`;5)M@E%NGOdQvfqP>-SjFGU!qdk+>|4Xn z+laF!Ud`I6tgKkrTh@&=`og)~BD#q?RXQ9eG z?wGqyOx`O`d9@2~^r`JO^rW^=bkd1w>*!Evmf3t)&erP9(jX5^BNfBX%;o!dlg~uEW1vd%gvuRI4t=E93c=`D` zEG8et&{M;QKcJEWni7ehFM*T;j-v5zveY7g)cCUOIWaH^c6N4AF|n6bE{;%OZ`rIW zYdLztZC_+-36b!awew84)MsgyoBSTrkc0RJO%NSmS9X>Mf}w{ps}@6j|Nd3~#=1~i zsdoceakfmY39leCN>Dg{f}3nCrY1~JP1OQ8mnpTH%Qz1tz;G&Jf~uyYx#dllx&I)0 z7C8l__teJnyPGr`buHsTfA8U%q-|laYer6SKJ+U4NYImZeBX%29VP9K1A43s3?foUZWt- z@@Q}-8uk4`NOy2hD@X@D5bU9J4_~0Cvt82d;Gj5Nk9KP7>(L1b2`87ra*^&8tl@9If!r^p%;$iEcpSg%qzudpM4 z8pt?cp68act2=uNA$uBvUA05_Mfe&xZpe7`(dXhWuC8CXj0u47N)-g@><(wbA&aP#E_T!r9t`> z6cQ?R*rcMBihL*#3Dg02hP}ae-FkQfKNU&{CgSp77ANpp3b7C93P+Aj@k)#y@4;7KR37W z;Pqv^rfTN@2H)*R0U{sVQov{Qi5d7_FwpJxVzFV)JFEjXwobKQO_(O0~{*bUv!&Lxl zx~}U}Q*TbKp=!qw+PMX;uLGa1j9110?3|jLvsK>~^7px8VEA=NTFvcq9;t9jYP}r? zwzC`SDcj>XufKQM8de;xq*$%~R?HgvQsn#7rDW;W-pljvj;-=}RmZ+EG0wL)N8gdf z#dZWeu##&Agqv`FLJ!nbBb1B?#cRJd`lL_hpMI8Y6A72m9XP#DUS3;(iM8z~-7WRi zrn-wv)uCE;u0M6xD!KBy`-d3tqErKSnMbX64{ z;7DrY@dzDU&sc88%hr0DT|RV_&#Zq$qL`s?S5ThvLGBuz&CmMHz7%(R%iNH2Qf zg^fWjUP0lHlS_x3X|Ixa%ej+HUd}Bo8EL-PGRu&}ViAFk89?1+pw>(G0*N^>fRceq zkXXb`P*qj6-xbQfBse7Q8-=3c;hT3XA{&cn;RR=eYDF6mp@+Nn4wh}UFCYb1s#En`51GxeR>+2+33Z|;p0!AK9Opf zgYQCbPEC7Y{uKf4gPU0zRSj$ILQfKPdkjtf(rVHCw-%r{isxg{Mw>@U!LRQ>_0Xt> zcj=Zru~%#=DGj9PuyM})3l)w(huF?sX%-bt^0`By?Xr1APUO@k3wIYO?^n_zU!lj zs_f~RtXwR2CH}iU!{#S-^Cat5;~THr7ms$L3nRjJGSn%VsN|*bkLqTCXP}0v6ex$Y zNK?{>@1QdtX{#p(Oq6VbuxZcy@G)uN4K(P$rMztk#ee5qhk#Ih zeSE$_CM-4~#mD!IizA1gAqv8pPoeQZC@|5c`7Vc6Po6$~@amW1dp+1@qoCD^pt)`D ztDStE#1+q0%6v$$N4}7!OFDeh7pq$9xkbrCA1h}%_jeW}Rthvl9U%`W?|P^YvvQEi@WuSN?|FkYNhpUA4%B7=a6-qHF*#yl#^J`e`F3 zdS>x}IZP|RC~wD#3&Sa(X*)S09Q!(#fKrHJ_dwj=F4oCUjR~k3tOXJ0aKWWA;yjkOsMZLKS}2hoTP zCN%vdAb@Tr1OIGL8Yo{LcyYUP#bWsSZ%lK1j1H&Es7M_OS$n`mQuIc-;XaClu3c~S zN0+oL&EZdI6jU57rT?G&V8-#sj~~(g0So{F_lN?CnZ23nkgo_CUj1qS{AfxbQl!F! z=vqMIF%7&FR7VKW2`h*0hJ3l5sfLEeJAkjwdn>suyAX{P;AElKd+p{;ePEP$DuV&( z;1Upc!3F@kS+Vn1Dk&)mm80K5;Y$^i-d@xK!+(npdCxw(b&Y5;@z%fp&LOYJyuUa<*JX;T@&k*rejXa1B*D z^8k;NXXZW=$n%BQk_zaU#({w-fCwrHK1fv#EBOmT4y5e{tSk}7+pEyPE1MDmI(-vi z4MC9uFVg{-FXIL~kJ%_Em+@?Pr^D!6cgjC#!(OqWbKJ5$cK34mnLfe$0GyV6n%n7pEO7qALC38ejV>3y|0x zBbH)_T^>9ik?pq78sAf~Cv!(i+w_Bf;%{e-0ZcTWapk#t4o+w1g=Qh;iKfJx;Mz^AW3Uo)#0 zy@l!rrI@J}0lW>n+ld2Ez?Ngq2iq#az*vpDpSwb}(gZQ)Ls%HzMGrhOCd;)5x&W}_ zGSbpmV0kDPsE&FwRKwuycuCIFo;CsbBeUc<)LJ3l)`0R@wQw9!&dp-o)_ zj53Has@4vbGE^P|J&a&wcsu0fK!`E?wlo4dCU`_d@0j^fM!jk8qoSg?NHv&Evhc}2 z7k5dm2{}!JJ4mmn@Fg6xGSN2We7?iy=@F_XxodWRZoc6_wl61UATXu7d?GKltzigdxT1@bh1VLQqJxK6EGdlhB67fNXI>H zF}4_JZs5T1=w%z$g6R_`+>DP!J6l>7z2u~_eri?nHw9BJ9Bj_?r&1j6tx`XD;0v&F zwA;0{QmSiEOmxlC>&-^z2O+X*lddS?^T{eL%4qVRD!=-y8R(hyHWrwU4XRA+$R50g zun(pa`sveUAl*Y5$1#k)-5R=vzt0@kfA%a2RxwrN|}3FH-nxW^E z3hd&^vi3RE{rmS}HJtib=Lbz9P}ttRckliA*?u)IA0L`>LW&CzJ;(IN!i+DikuMtOQWIu!jOU8roqQ!W6?+yX%-&qPy|iD$Vy`wU-w$=muoWSR8Yc)Wto z>rGlr#2v?rhdlPS&!V5MlS2`i3jcZX-ZS81BK7&oiyIz%q`{W6CVnlx&*THSo#*c)|6V`V9*7{AQ=?}95C8v~cxy!wR zl4n%6UzEItojK%TeTY?XaMdoFPPF@30_M5J!F#VL+U7!6aUW=8 zoeG^U|JruUC^`FmnTv_+yHNzIt{N z5f?do|NP?OrnWYJ5OSbE6nl+{z`y0E=xi5O{ar4u2>5hzZqBo^(zPm19eU2qLbD5% z*U0#szlO4O+ARI)U*QeKd{rJqvpYkp*dpAXRd^$9?1Vj^8vj0Ec$D$xJu1&Ih=aT9 z#%>50unK0gvKrjpUf@*-qrg$Dt*1Sw2$rhd++%D?;fQFZaQ@VgnkJAmAbp z5fK5IPDc)Fo5)IWVB*>z@TCkr;UQL!w?casG*WMpZI>$5e)><$Xh z?;3YpM)U-Cur3V4!n5Gy3iPn9X4Su|e1AA48U+qFZr@ z=F0g|=kO*<^w92`&7;pPhO@Ha>56gKh2pn#!{=uDypa`zt|PVV9ZO7i?WZb7H=+1u zuyGh!;oah?=&j(eu(iv3dd+F+9@Rbp@^rD$IecBT!KvL_dWB{ZAxg;sm|cn;I({<} zvFAruKt13y%h+KAI+{=9K_U`c$#?QrnGT8vdV-uE@0X1$`i?wO3MId0>AjKtqH#r% zit6x*95{zx4r?*ZAE9m2@te^Sp1=0N!L}Yp97pFg9SV~%@im+X_92pyLvc83NnBfi z_Upw7sBa2APIRduC*d#m9Z|pH)E*5-w9%#F;xm{lO;o-C#u}I3em&k?U8i`uD$7G5HRTNsv7_F}-+$Z|#8z zmvRP*nb&BgzQ&4Q|4^*W=f-$?IgvAAw(Z@iH8Yi1!kHW$m3%`3rGLua95(8p*r{>$ zq5NkTe%G4RA@T#eF1_0df01L_D!xGOIy&x$SDpQ9DHe{e{!$uP;g$DTyRq{_)=`cl z&-zR8ZV9rgv;Q2GBFrEe zA1^5}&5P(90=X~b5^Epq3QvgEc4(QcxO1I%i}jnJ$EJI+rq6~FF7LhwM-=YM)YNvj ztngY9mXwrPelw9kt=PF3T9M#}2q2>RBp)9k`>)Ko3(s;(dXmIQ^Jkhh6|=-BQAWvT zQ=>n1k%KDbytt(gZOk6(Pm&+2o{%Q;o(fS3*@xVuu_N>|^^i*Wu ztwt`~!-}vgAz5jQALSCg!_61?<-ze3jw8pmrm(O^G$ZlQ#Rf5_;8Q>wBN>{QAaw>S zg4W3Qjx&(G!@o_mOos#KD77yg-^GGIN(8od{O-D=Hn9>1I{~ai8Sy)(rOyn>d zExA<91ENEl*N23>>KuGVgwk_4zmWxVrL9POD`tGKsCrVtGmdygabrQ<3`i||p}`ys zI$keO86f(FirrlR{39#wAg~@Pw`XmOVl{xQjIfY|S`M{kA|Orjrf~B4_U$gT_B#R7 z5ObRlfso;T&hc&B7V~k;1#}C#SxY86rx%*BLqn<#6=rvnOPe@0WJD8GphoJ)qeBkAT2%p%;F+K zn)Izc7rj2(c*DRlP6L}>iU1l~FLU+Lp(4Fk$h;>QEH~j(o{v5!-`-+hngRq>Mtt&XLkq4AANuJr9lm{dSQ`E}xfk2g?~SDglD zY2<;WP<6=N;v?Y2Zaj}7ya=ZMxhkZGP+=%c&Cg%_+J|Uc9)X}cXo1EUcxLurqN8}M zH&0YAw(>KQnx4#7m)~{}$j5PQb$R;8A`SBG)ilK!x1ixGGjCg{oOU(-U)R3f>{LJr zl4;e#)5BxW64kYQMamX^eeQ@~yqs@|u0#u8DY~=3J3=LZi1MEkJzkj(Bp#*YzfVIA82 zVpmFR5ZZ(6Ua0@YS|<;V(#Lf7%10$0Xa}x;9#Kwtf}avaUhAGuy1hMq8CmJ<4po2>HIa%nNi1@OHe|bFwa$J9QK-GnDe@{g=lk?NBG=fLDnG6 zz4zoseCq-Iwf)ojmHSpWCAUtRR+Jdq{clK((^trMr_}5fz)=Ki-Gpv|@N#{rSE`>3_}5)Q$`nm+|7F z>)PkHpOVdQsNg&2Cjz)Xp&ZK`#Ysorh~nanthWBSk?5QK@ILK&n4a=@F4UdjWI_R@ zC^aEAy@+=aAfChV_&f{BE-!lbn%t!|oQE%PRO-@)Y`fy;^}cDD)d&i>`~umnXnD(O zltpPMMugPah?2RWZI6gv|Kf9f30zz>B>8`tZf_xalK%rOt#|fXSWk0h+RMtLq?v}_ z)V> z-gf+D6qT{z>dgl#1OfunCu)~I=i7&X-6xeGR%1n|S!ENlQ*WF@AH!XE0{+_QVKCN- zQj{5gB<>WAT*v#L>%_9PYsij6m{{0Djm{__jp=+d5|!$l5_`siSGgzwb}Gq^2q$M* zT8H+vQ7ngjO**+>V#WQ?oiQ~{9GR7v~d9_@`2A)LCMN(++>8X39R@U#J8A8O9 z8p9D!DyK2s?^~1(K6+T+IX(`6I+mAjusbiBa-&+c54MdPu`>I|vUc1No#z;jeEH)5 z{58IOi!EZL9`u62h+kYb2z)$=$cjzC!$%V2R!!N8)V@+B@|kbc^JgU4dkcP$;_wI7 z@NtIec8^{AMe)Xt2KRyfP6M)N6`THqphfD#tQAsT{!)O`e)o8ASZ{`3#hmPoq=Y9? zHHKWgapUl`84H!N<=Q)W>gM&dh=BdL@dqh zH6FS-iT<9Z+kCtu*z zgT8evkMlWnjc09ae1Sf9=r6+BpW59U7CE!GbP1&&H5!4Heb)&*UR>BgJC73fw3R6I zEA<%_y14T5X)Pt12?>8C-g(+jRc@O4is-@Hpp5U z%V5spf572?03)zteGHa;m2CUlfu)!XLS%?Wz@H*ixQvRP74&;q$lA*+Mv6=gM~YT$ z_ZubB&pdorx99^R|jG2FeeZPBHy} zrvG$e0CEqRPy(?hEG!KCA183_AYI8ghN(zN0AiL_R(WRr{|&v(1U+AaaJ{9a1?ilQ z0N4%nWX+7u>-;JeFQIickjf)Mj-Uzn<+2w`p`o6=1lrqXodMGq7VY#!3t*99oi~ z#sTT>NGu4|R2#j0W``E>B(AmyKeiCh#S1f8W&2od?#-A0hU< z`0h4snfVV9$UsF_>^N*GgiZ4BE+eBq@ZuNP+;!N_GRb(|{2|L_n1A>RMCxz_KyU`T z`h66c&95N2uUnAeN^?E@`UY2C1oI7L4q31vrJz+XBVhf)A2Yxv!+^jgO3DOv3cUXx z?PS7`>5koh9H`Js>R)3;XZzjK&#%?zm*vxl-?-){S<8{R0kHZWOe6xvMj>d_&yR|Q zC8)h!8qpmgvNS{|(b-7{gf1}Jpr7wmFqp9hO{y}`VqXm{l@8a~gcSjT9G31=!tTe2-fjJ{E{VQjX7 zQV&PRNp$2-Ln8*pxPEp{#(kmMQTxX)YOyWQ-)eUrp4K*CnSQvm&*wcbQ`Twg*m;PO z%WAvfkg$C?x=&NjMMcQXpF6+vtI#m@e)cbJ4Qi^a_CYQR^&*ouAnPvNuO`O;LLJ0& z*2u^s(yO%UwEqE~b(bmfkVi7RxzuUiZE{+tl!o(vo z!>#sQSu|SuXyC@8?$7Bo+7D?5@hR4#ENKSEs5(lG7~ zFbxRhNnKqtJKALEtRE@HH*`BiHIL-|u?X}jVK~*lgKK!%E+(*HRn98T7JcUSC3x;b zl~*VDsLPbvZ%RZ(H6kz|eT#pNtNQ5sN_#7|X&K8G`(YgQ-`8sGh z@KH^FxOl%0coQzyYtRzL3$_L@lBZpUy$a|%@)=Ry^1__R$u$pJl~GY;(QN#Cj(V>9 zn}`Ls)WL9!a(=2e>{I-@MBXA1t|gyS6Qu3i_^~at-*qTaFF$6%pIU9+#thWU7V^UO z?tCf#QjcFkR3iX+IVl^qInGfgyC9XstHEm~{1UT28odjCJSC-M_+?_!&Z~QIr7Vma zlqxPtq45&)2L$O$iL&@M>P+>gSzpHQXmpi%NORU<6Y5iPs}e@MtgihswQId0hj#1F zHUrpSl$k|ITtaCkHT(LFRS1EB&p!ezq#d-}AuA3Ss*^l>{G|FNVkxS1HJjmsl~<=S z?AQrI#FJ~0Wn}Pw#(bpnkOI&d3=5S#Y2LUm>SyT#nKFDS5Hon!;GHXV}Dy>TW6GOeP8eUA{nkFE(ritme7SM zd{nHd$f${rBpxrha4dR}q+J3NdmI%N%ae;o-e+V^vEEcdeK^+8EfG)JJPbG#$x1uG zbMIIb6N`xK!w->4MHWbvWUlrhJ2_Tx`0r0RKe7<<0qQ*F$C(wp+FFf~)CMvOK0Y@t z-VXQqzT35$5N=5DyNQY#!F@(=L>FFHq`h9)GlzGDtIVZvLoo72Ol){7DNw;#GuQ(@ z596NIHVsbM@+hw!7cSnxwM0YSpX5I0y5MK@kMmF3%a7m}GAR-6>VGSpFIk+BGN&_AMTx_LCDAkCplW&cpeUZQ zxrtlAOv!(d2G{Zl;wwq<9Cl{k$gkWPa+M@v>$C}y%wHVd=Mdf~2{NUvT@j2l2Oo$H zN4&*}KmXJk^;dnjzNc-(B2aRB(;}aP=^l|s-??g^jA+t#5|>|nFtH8eeL|pc3l9&! zMMfsQK3)l;q~9az)jnbkwpZdb&>z{Rvgs-1|ksY?(UWuVxs!HA{*CwuWaj<%3b{p zQ8=IuGZks$ti1!wek*~}CVjl&?_tPVId=K`enDFrQJ=s()JLGp;44U4V5}b*5Qpw> zVouIyL?#{6Ar8}t2M063F=gtaNvfHhzml{MC%*O^S$?Sdfw)CEcP(y`vmM%`WNRvT zWdm{FGI_8;f@`GZjN8eR=a4oKy#pv`9G8Bk@>hAjb@P6vU+`6BHea_T^?g(e75M>JNta(t5rhP`0gCUbz^WFU(iWDb={_i=G0AYSR7*7l&lw$utf zyA53WQ|iE|M=ORrdZ^d=Rl1MQRYd0ntZ_`9glNN5QmR{vz~1?c){mISWq~;b&y$`ZVrmrJkKma+==6Qrq!Bw$x!kz)A&moz? zx_!S5|9()-P|6#@h9T1l<^Ou-#upe4LZ-c_sXxNV)OM%9`Yl4jrZ4Qe2OIS`Khx6Q zwaVh+>Qc#OqSp)`UI#0kceEuB?lAj4yYg(st{e#^RM_K>*nsIn05+(Bk%=)N_DW9o zMy}!DEWF30c;_b$_XwgKmI`0xjj-dH#?*#z+)(nVc7QGEh|7%e@CxR(O`Nw+?*#3t zhOpDlyZNq!uJZM&8#RpC6@Tx)q@oI%{+H5)lH6bNa(^uV!Fc}g;HbM)feDpf;9u(V zutp4r0_+qLxi?5>&B_+(K+swb-;+U_FC*H(JCPBAiVY?)ED>$6bSW~!6IyI8%v6HISgaNfY{6GvaKH#66G7$% z1aZT*_w{#1LVc`7#5A6S!q$0r-+So85WMX$!Y~xGMeIsHYRLagH1<)YT;VX-a2B6t&!t~Tx}3&Z;&Qn7)}P48{$)Hf7|w#};pq=e_=JQ#@U;+>n%9;~ z7vg7N_-Pu{jJi;ez(_PZeD_0ZYpYRk>0G4x;D5D8V6YV#Q3r!4-#e_Ud-6MoTnfex zL6|ky<$lHqV}(=DQpq0Jr=nt3cG9OC_w?S}w9tK#p_G67Fzb_B;{Apw?pS)=CK4nc zTwbybYy#aZpyg>VQnSgW^z0NE#Jh|+pdqRB>Qy8B3Wk-@VIms&wwl7W<+E_$+o5}2Ym6&SFqqv zJ+@v{juRR8(7<3)~frM%l_LBTkpbDTaAp%$5rZ zRu1+Vp?rb01<3bhh5oonDJ%{8nM8^6eQ%XVk5KSs4lwC_#RJHCkh2?m-%J})ZZIm$0~zzM zt`|0`S8A%kDm(OTsF|4qfd)VQHvr)ED-bh35Ao z_V>{#+X&`RSjQb^u+KJw-E)iwSfJ*-&&*5ik1gOWGNshofn1|yiHu(>9jAxMpROEzk-d$J+5gQ|5S5|C2$cX0ZRy(IP*CYj zfA^;NRjI9_BKh`hQY{s(p)4^e;omNuh0lf5xgnMpcN_V z8a}?sf_|50#j5@uRnY(H?5(4!j=FZyEhr#LYz&kZ5KvkvB}EVwL6L3+QMx+>MFdG{ zX;h>`x&%Zd1nHFS?rzRpyx(`nH_o~D{Bg%{3>EhN#frIRJkK-DUQ9*ik||XK8&!az~KS=o9>bhrxv_zV083ZF#zW8?CgMORQ~2C5Q%x7`|px~`LhLX1jvHX z-24R4#&%$)(F!*{lu#>3ttT99PQqzP>{bM`PBrAkaYe#BlKAqHyq1yIHd(_89_A427M;pH| zgUL;2)j-mDC=d;-R^f6N+j?x3ZkKCNxxoi?ay%m{ulbq{MJW#uTk04*vc2?4#N{@^ zHy2hMuvb@C_k-HZ_|()*z#WCLslF{Nv|FeJ?W8#L5WrS?p09>5g}{M&y08YdBE+2m zf=64SnKrC5M1sf<$@JP!=6vkAdUXADV=@aXe<|f2_Sz^9n-Z(4NzmzATc@g>%HK6) z@y9UI6W8k$_AMkbP-#AYeg!Uf{`~oAM-nW#UG91GXD3_lP^ij^LWPn0vilBsqZ-dGjCylLk!^lUA zle>;u+qgil6FuUa-&~XoST%|6*^dB6Zs*Phh0x&Zy@kX@QY<$I-2%g+-Q%mB74G07 zC&^O@6Sft$-Zr|r{0WV=*zMEu5+_+$uYIxl!-o;y)if%)_j||s(v#fT7sflUo8mZm zAU5-#{t{*k8J))n0161kM5yI8S;4L}3gN;APKFfBusW(|zR2FpO_lE=eB<-L$weEr1TwG>1P(td!v(vSVs2C#0ceM5&~u<+M@;*vah z@}%XwA3eQHy=gAi=A|%3OLGIqyRMqz-_?_Dodym)#%dYb60?TO!ywGnI9%y|VtHj{ z1pEOE!rws-uEJtYLL%~xZ7<)ig(Q|jr;l9I57E4)xz-l+PvyUJRT6?@W&_sk?@Au~ zWr8zj#t>&5@cDs}k%T|NHEz{YHQq48^G8T*^I@u#rvt$2uw3_FaL?>~G< z4}4Ql_X-LM0^F9U{gvWsozEpDC9MpS5|M8+QoW1bD)g!l&QP_tTKju7=y&fK3hd&Y zmW)ba;Y8sIIs)AsiD-ap3iPa^%^~othKK!atP?+qJ3p8f?M2`6n&S;gm&DFz+t4u{ z5F9v7?_R^EObuhHsWtbOAYuTPn{TPp07*t_stz1-HvX_nUrgDu;n96nbltUgyqt-( zpjP7N*OGOPdt5+0)x<`5;Pi=u3@g<#JYg;6dr7V|p!VoG3(GS|KKYSdrEjAu*IdlK zZJG`fM$VIC#DYm{TSal)SN-MT=Y@Tc%^q^v2L~0L9Nvc^Uiak%UFdy^W*-^bp2_~x z9nk~{kGo^;=7n!3tZz#N=tVVQvG`AxCal5cI>t}ptip;Jp4b>_CreFBLkV$k*-ZDv zv4KqHEEDo?-OdKslZgv{LYFxlr1?v<&3so{IorU6CXm0G4{Y1}PN!v17{wS4uILYx z@Pgl$-wcWA8~QDDizRcJHJp$H!jW$F$WAj9;*JTr&qd8J`lY{b>#eWiHjV0O$WIu| zfP?!3yn2lFd`}=G&Q+2WKX|oY&q|$ipBM8^OI+RY`iy2>$^K}LZY;~1qfc^W$9Z23 zKT?(q>rUb9SF0N!ocQ^!M0D?D)f7u+GE-4L`2hVsSdpT1$g)utz8f`2p%>Q625ZHg zUg<7nYRUnESXb%kaX|h9&fM942??r!m5zYRIs=W<+hXZmOPpa>=>4j6@-5jQQF?vL zRj=rt#G|yW6s4g^5V--xy8RInS2`h~eSGC2GMHm`lCs}2vCKV-w0P?`!4kJQmuj9i z68Stj@Tb6tyHwm{XeL zr(m6mXtVaxGYG6%m=8s0378sAZ>w=J1gW0JMo^+Ijq4WNsp;%je;|5E#*h~ zFehrh*qFPR9t)nho01cGMiVw~0!XpS9mADlKm^wJCWd7c9-H$?B+SrX{g;>T63V1h z6P#I|oE;V-YtCRaaS?H^9nt{{a2A#`H_%KIry;j!Sv@m{5xD4lc_E)Z?tX6MXhrb& zT3oJ;HWsayKn_EhQbjs00ciEReR45 z#)~bDxZ{VQAP)xtryV~8FYEzPrGgQMxF=Y8@_Qgl`U$R}3!Dd_zOOh1c)ojQ4)Nu^ z4`f&a<3CZ0V-5kz#Y{Q<*53CUIvuDmBgJ!Pp~`)lh(-u}>st=1<6AUPp%8>a&@6-n z9@(02Nx-Fn->R$+506O#HmyPSXmHloded)T*`v-$wZTx_zi9*m4eCq?Cj$^2FqJ*V zeC3C`MhK)2tllnk{)IDIjftmU6pS#< z5(yE!_Ov9AA3sJuJ+KDk5Dfz^fY#Px97^;NQZl_`^!?_g5!-Iqlft9orJ3f%Vy6@2mGsbb_T~_5hVad z?$~&%l>af4F;eFVd&l}H$*azl(b|`hQINJTg6O}D0`YZv+2lcYDc~!>Oh6zF`UbOS zX*)YR&tJG;0V+K_4-&lUss$Vi-c7A}u6)_BEbzO85@Cm5*Wy{PY0f?PG{NJ!WyX}) zCkE$m+gW5wK_;tb!Pd0mz!ueOp#EtXw+3+`V39zBlK=REDltmA>JCHGd?&XZDKfEv zW)6D=I!R_`j-jUXuNvr~a=;DT6X6?i(p5!KI0HnRa6ou0m=ZEp+u11lif ziZ~x#nVRZT#X!ZK`mxz-l)nWSr()pysFpeM{uc>YXzlOKqB#vSY1U2G!9v+C_h*Eff0RyS1VGsf^ z?NGv6uzjnZ+G+gbxswqdGq%7F+az$wi-rfgDJ&^oEkf8dPTIop7rX(UY&`A8(7?bu zh+@QNLUHCI+q25Ir}`b|G7z~8^mXp}ogpS>Vq;6+EC3lB&{_CB5q@NMj!Rp6=yoln zJGw;*0nddPVL)&M4@GBxdj(ZBf50kLP*C78O!E>5gM{01fr4dM+xt&FO<+^U1%G`R zy=ClI)2K5wT+@8KXTdnGfO`rgLS}y&;d&^+A>}+cg@BvB48P!!WWa!26rxiAi>j>2 zV*x1j%n%tb4wT+B>SBBN@S$Ox^n(YVp+E|R=q?c`)d~=vIRijZv*3JhFNcY}avZu+ z@RvU^jK<%fXI^paJgF2-bOTLFd}Wea1{g~)YPY4Q3XHn_Q7RLZR(K5ikJLa91Y$}^ zn10>N26;MmLG*F7>3IzE;e#CqX4wip4jWJxB8Xw=E?!FEYKO_`3UqgO@ z;WaaTTgN;fW$PSf-4k=AYXNpCiY1w+>fU0Zjm2 z&UInWTMD;1SBtKo``U>Lvm zEx@jNDlZS&C8r|rkt4srPGAEXRwXE7?F82nQK{roX@jCM23~7?auPCMeS6QJi@wJ8 z0T~e3Fv(@q!j9`tS0xi7CX7{2iba~4{cusIgzZN=w)j#{AzHL5UGP>=&QD~HvcVi7 z5ssMKHuSe{T?RQg7-ZfsP-#b$dl=td z-7rpV|B4Dxu*;&m4)BxZt*tDpbJ(esh1F<1?p~fJzguMt%3|z!n#5a}-6yaZ5?i=0}2%eDrkArAr=-bZmFWliO@ z^rLEBiE*PJ*{K3rPSW`O*fuJYNN_lb=V|pb_l)NCt>Dm-kKgM&WtczQKZlay9rBO4 zfn^0c8Gay;%92^i{LFQjABzCj=7KWUJZroUPDh)gO2!yn92s0D!pTpwk?TR&~ zgfir*7kjnWnTg`~&)3c^6N*8(CM|V~mu9Jaj|0i8f*^q2$S)7)!<{KeNzg_$c7?dm z({CtgDppV9x(?qf?DX{};etxf#`*rFmll!lMomIOFQQeH_;jg~K~OM?HEVwQ<8cXasPZ}FAuY9ldnLvBbfv^iXiJW zI5=oSv$nh(4@sfcuW%066jR6HejE`cunS1+Q@$@j5ngRLalbE8#%qm4W1GhiYazQ) zUQ=aScoP_t&Rn zOL*hBwOAY2+$?92>HAk+^0$Zn+GDJ;{8Hr@2#1)~6GQdZ(z;!o-i;^Al(FCpFT5eq z_}olWe2;Cqv}koIt^*YNG@iKIfhebi#Gq$5?vtM92_sO-Rq3{2nYs0#Y6Z zI7EZR8WO)-&xb2M)p+XNsB^{HO*WUzLJ4>5mwpAeN9t2nkDa6ylCw7;B=Wnz1hw?` zz1wSN<*&bP&M#jg4Q=UzJ*|p^68oxdd(l zsPiU;6+1CVvzm=jXNlg7*QJE(TYezh4Hyv;$@})vy)Qo^-ix&r3c0ci>jll`g+0q= z6Q1FoT@9obwluL?RK~m*H!H#8l~j`eVRf`x%ZE;kf(?ZclpR_m{@mT1!MC&~#u(W-{NZaBI%=uxFz)?jTU zS4fUGlrz0*ri0-hGOIb;9Kr%-1|CpSb{}GcNDUqjcA&tw3;1WY;~g0(OOE8w&FRvj zC_QoTai8Q(ok4GQX}wQPBHDkz^wq?h$TPB1Hm#-;^qKWv=PqEZ1@Z^MyXCarD)^FB04n365xY>{p}-n&d>z{_{%3MEeCYtUVAc?=zCi0Q30?wz2c>-N)mk;T zd_*lCR6(qI|K<#NyT8`r+DdM?h56RK(O{07uujPD9IumYp+feu2JKg-f3?l6y62ea z**%g9NoF#3PnbLQO~Gm(*EwG8&kFlg%Q?e!oTvL=p**C)OUP~EkXk!`JKn*twSQlG zF&Q&e7eOX8kbYJgB}nNT*=*|=AvLnnd3l_s0iY&uS2X<|^3ri*R?S0e&$c--#yHz> z^3N=YMZQwT_&XnbfVV5YbbP(MMlD&AIqdHH+1jxC(u$hq@VMUz% z@bLcVZr759boG*|)3PsmA38Mm%Rg>}VayY&SfE6z(;IJUfngg;()=qK?T40-{#D6S z*4O?Kim4pUL9NWoK=<6;6FnAm?)8K*rdLNFp_|^*) zl!r41WNGtufd$7Ya{~;zSxZ-(wXU4ajmyuKb+Tur;1#D{<`2{MT;%tO`JxF4)ZY=) zg+*Qh>8Xa4R=Mu>@vG$azK8Dcuok4T1DSYqyB!qsd{Ho7DV*qf-wX7ntTGdV#I#9v z?TisrhDu_pQd!<`i!oZBs&~&1okGmVpCnFvE&FzYh2h*=3%m9JR&2`1Sa&2(MWj*r zDjz2ou$ z%>e^uVNFj@?{D<2VDL~{Qx#c3=t#oUbik$zLKGw&3K0W{+AK#6?Y~b=O`SYcf*Hzb zFKDlP9Yk0vs2P-nmIyJ^e-@2JXT&&FPW%{ z)UmiB$=?sciyEMo;tYfm48$`xpZ_`ul^qb=W&otz3n+)!+uJXL3^4y5)E77GMx#zi z;O@CNwl`>Z+d-mxL0oV-WvIDFrRBYdRDIE{HtRqy4X?WuV<&js@2RM$EU<&>ncLg9 zI7s3(*7eZgorlh1`P~8HFA(#ycK~@y9<3f)`b{LVsHUtYy73|nz zH$EvNEpwJRYczTm3J$ohRxCEm%O7!~aBO3E*}WEmc|cgu@$rSJn-+V3qlGCj9eAjx zcNP4LOyL6XR#5UfC!YzPH z2MG%(3^8(XeQ&CTI3_4K7#9>QWn(^%^-Wc{EY_kh6UcZSgY*m=hZ*;+Zs(1hgER&@ zb)>A1r!rOEoHeNL0=FM$PwsP+5G=-cme=S)V3vQh_%D|k)h_|K3$m&NfTv{IEBT+* zz2uR*371t#GH`|;dCH+mf2;6th5z`dgYiEHvU`uw4MZzH53Oux0-hKmc@UhYV`KC6 z_9k3iTbrW_j*60ovK$HzhSVo!`}T%t*6#j4UU}f8oQ78Q`mfRmtrdbRcN32@wcUwZ zHWO6U;+f z&Kt1LMXNt&tsuB8M0N1J=qsSp1ErlH=$r$>E*RH?Iqht<4AdF}02c6QwP{)9YI`g2 zbV%ULX{jt6h5&eisN(lnXysjs*#n8!;);5w+BXLtymb;P8_VnV_v#3ERhTw1$N51d z|6*z5`|8=2tTgXDm5ZeVru@_+(-0b;CLoB9j~DB9^5XgIa!ot1$oj@k-j~iJ>Y9P7 zc2p~P(>A)ctidJ{MtCFZ$XiwzKT4L&!hn3QFk6d^6zeHhGF>35j;ut+RaWrg}w1<2i+RxyIQHd+{ z^4;#SO8#CN%(bls_IvaN-o}h%N_A8jGvTdRZ9}lDuiembK)@_7iubJIv6X*S6{XHYne?Pl}8!>DvHy{CJ_m>u{!wrkgI9dvglEYPG8xfUMj&K04gT zavopEN%_HWYm@J{rtPtTe|k`AccnE_GJI7y`j0RQ*UIn0$$6>J`pDogYnGVC*FLV` z$^snkUcuheJrVAvD@CgFFEWqn!0mGOgvaAvOcQ24QS?Qs2-zCjq^~tN!!aw!@a%BF zW9cYpEm{cd5?wU*ox6yxAFZ)BPI+Qltt95HRT7+Tbw_RByxybMTBAAhV{-ZPQNz1a z_Gp(dlZSr%{oLq0O_*U@_lz|wR_Y6koA0XN`UOlu@(c8q*DV=pFvGD0J>WI%qq9lp zZ?Qb{%+rE1ZgrKx=;%x%ps+r7CJldWcjtw7LmH%A>Ri!K=-%jZeOFq2FHxOsLANv# zZb#ml(uavPJ5VoAov-R$mq&X>6rHr(vG_)^?saoq2(nN_29lK9i@ZLH&iTx>F!s*U zXt?A)xxUZ5+K2JFWc?KxK;E$ukG6k7GQSR$ud%Wt7X{t)Zd>```Nz^1RJ({J>=uk* zFf<-L^~4OWIr?j>h>rfOX@+qoKuhRjVbi+K(;Z!7+3jQe;vFb15##$|;w`%o^z0;h zt`m|Iis=tU8KMn-c=Wwo&lB0$+C9R^JtxIAkiUQ3gqS7%49wODijeoAx{yML|)t z@ZiZ3R4+`%Js;k=&_7xp6u5E%M(KC;sm+ShZXBm5^GZU!o*#ecTL$2l#>C&^en}vM^_iqd*Y-GB=gb5&?N9gt@9rJs(RY^ z+(_z6={lshA=~-%1>FL!3rk9}PF3t(cvv+ySS4-7KiZ7Ka{qq#@e8Ijj}a6{@V^k} zcCFp%&1g&fMxO|i75LI3GdX&rnp(i{Ok!2bHTZ6V$4u}iu{_>@%qe-6YuCYJXrn(K zx$GC(9Ilx^0^G!l0$9Fn zWC&qbWhA?nH?OV_a9KUH6J!<@7Uh1Ba8|`!XIdxVM{l7}7yk!i&HC^g*OBiCMPQf| zx>E7qP^+D!TJ7z+Bgrp)J5Uk~ym-A}kIr9(VI@Z)sK785-Sj^nwT<9S;#IIc4*Aq> z_;l9)nKr^K3onBY{9ji~NyqK;%4&*lTfDdLm2&(BK9?;hmz4d6|9mmJ6^P7Hf+Pwv zWoCcx+MVc`>qB+{w%C1X@{1Rmf7MCd&$pS#rDleOlgEMCNz@kE^K3&(uVY9xUC8U&lz6f9)-=H0&iHP^05A+mzalZPDoga;vtN^4IRFKEae zvMq)SBTqbmSUM}x?z!NEv!sn;D0*!aAn<(PHE;kQC2;8W8?CZEWOY)$_DzT~#3`M$ z@^_)EIG$s)8kn;6`>>cy8SkHshTN2(^WZO*$!GXZ>7(HT%tpP3OBjwgjSTXdXO*;8O)DG0en__&gCQ%s}B|8%2 z^yd4;G;EkI*xKC=C~il$ee7MnTX;4D^r|N|$68WQT7skTGk(CUScmtZyxtDrF9f@U zYAz^~r&;|4*eVpAZA<~L_c%ru+GPB1e)M(r80I#{rgSGgA(IO9{Bj& zJXg-T1r$&L-hQ-~%5hnx)RrBNct((T0sMu$s_GxepIzhN$h7L$v>T`fG?0-QWRWx$ zte_t9=o8l)aHJq{T_?EoqY4-(4uHdn+UtWiyISqR7^_9-*6nurrLKg;tih@qOMmj@ z_+)c(Ity%!(VTu6{QVGcC_9QpLE{$Qr>De*6~x=qT>3$S4bW$_`&1yNh5FW^d?G;a zz+tvu{ln}4>dT-0)8I&ER~SryQ&>eUM*3PU`)5Yt@_HD+`(?iKWWVT_ZdvE*<(Sx< zbg7ZyjbR)OqS~bnSb|DECf=m4q`?ZP_i>=TfSMGdZ1A;fDJ~pWuX> zQUDKvBIZG5mb{jhEO?1AQC=E7l(kK*Ev$jiw#|Fu_|4a{vP~qf zGtex%xw-YKL$ddrvlM;iRfZ*@h5$ADMqBoQ%v}$(;P7U+W=%0A;l@l;`tRY;>K<=N zQai{3L{tv+9sxnwCsQlOAj%=+|ASu&`PsmThzHPMf@9VQSZ6*UJdupD{h!~H1fJP8 zUz3ZU4Dop4Y6do%p6DFV$jf4dXOyyisv5MJ=QpP~_V3WQ(aEm(*Me4HGe{GlN)q*L z>+L_f_{3BMV)yPfrd{|SNp?|NO)rqwI6OSGF{NJ}yI%kBa^8-zhMvsGWH}ZY3XX>c zTaht;b#us{WOQ;$QpDUPp|LHEL2xfn91Q_196;u*Dg#j04$R01Z-YG(Gjp!fmg(y1 zVJUYD0H>k<3o|!&w!tq+wNC?SBG1l8+5feo3AZCgN4BM0NkDn1_&&kdc_)5nn4{kl z;6pOYC`7%1^vwAr%|@-@zOKbIO8jNV{1LvFWz}$E zxl}`X+S|*hNLRm+_nUOV{FL+aa$~D8mrgR{H#dd>q%QTkU53+-G?n{OSnviy^ zI4?2&g>BFw$m6PTQTN{C^^#Fyu#xnBsZ*h%%yCM2g;P03o%pcKG~R-jYrq94XmxE` zyk}~bQbPR$XUQQo$U$4tH6QTlq}xE;q5=ZUi9JW0|<=!4&IXu zR#)6o}rpgyfxgy-hA5)kPJe$T&2eqX37pg4fX=QVYorK&TjBA?(|SUMW|JaqRms@~iW zB9cbg|JYB;ZlmZT@FnCLSO!<^{XN?g&G%chV1Q+j8`RMdH5C2w&c>61-2gg13%BFX zb|%RS+<8r7uabHzd;0W$e^jGo@Z71tk3F@O2c(AC(HZY`X++qT5989rKA~S@3bv*R zIXN0FKCHwz=8DY(qg6PF-1I3B-dxZHZ}??)@LKWU2B`rHy!~o0v8i_LwtE1Xk?I&*CGVnrXK-rYFXnwh~L-hzDqBug&a%?ey9P-wb>If2^{QH_*HN;P*A;e$ZME zYgc?D@}SDT*Sht72tBn0OSgs-!xa6N;B%rADKbx)bXz*wuct{t9zG!0{r< zW6ml`AVgI6Rr%Fs&c3JY3~@b3<=`PtK|I2zP=RA!N_&;9HgCK_D^qyo+#_aqkc8F` zpZA5w*%`lxH7pDbd_guV0|C<{YXl z$Qp!rVzdPuZBJvLcC`QB) zQ#Ih09&`*^^(MrtE|@5|!VQK-B3gV%Jr?bGjQ6drL7vp>tnhRKjOYtLoA_NTUA|?l zlxz*zTUeU)r(Xn|1ifr!djn;~HIRf}yxU#xVjR0i2|p%AOYXbLjMG`612fE9j*4J( zM!~2weRDBZZo?NjC6=YTlxxs^0)FUUJ7DeR^~vT2t6x!w6y`or)US9fKLrQGwz4i+ zKit2m?m38*o_lqXdMxeJrMK!vw=4tAW~fO5FQ%s_c>UT+9C^$QiK+GCg{gQIZ_3{rY>1_jUGc)>U?R2X+NO!QgAfN=&8Vo*$J?Qy;8qmrhcF^gp2)3I8GA zH+~TsQw8eYpOfKc-%3*Jt;4+Tg2UW~^Uv7!aA?nGMjm z$>TNa@A<$leDv$%Th5-dehVg)ZzYe=V{>7~^GG`t6%!5*XcCQMCCnP`#1<6@b~EMk$3URohBvru^b~lR z&z?;BBpj|?K^3kviND7$bRHtgX6?*pPZsx1s(J6o=$w$pmyBwHst7hHN%+A-&%P6v ztmkD0_iA%P6sF%=892b*ii*ZOH!T;xHOwd$M8G(x;|nzprvyZxugL{1?XwQ8I|f}S zC>IJGaHrIg9+7yRGQ$@55^UOUj|H`Z)32BOulCx82=!X7mz-KJTO~##$lm7mfrQk& zulD5b4g!jGSNwQCkishS>H`lYOc}>?$Bw0z!y1ueW|_N~_kFv^KP|TmN1aG+#xT*h zec1>wsQndjcbcr%TRZH?okluyDvI1RarjZ@3{M~v@E(7A^cdTUY%@i*HfK$6;#c*& zm=iawlaUb4iyZdyPh10pg_rSMLxt;LT+b6%4BoiwgN=rRo}6jkg{>-0f>-U^*?+~o%92iyQV?u+0ICQBv9a3wtv1hTeL=#XTFtRN zoKBxi?`GEMbk0?ce&X0P(h|wKdf;wJZuzCeAWRh0_O`Bk3BZ_kgZHOYYtAt@7(>SUXZyz^C-jh?81x zZT9-!9ZjyGv!$clv+-MNjo`|HE_@P7#4&##G8nB`SvRTIC$EZHU3ArP-`{rFcX+?) zwRf~I;G&?gQG&m`6npvQ`xAH)&oGkX)oubT*DK$YK7DwmUC7d8<4Y_ftCOJ3hFW5* z>^G&t4ka7j=C)GV$&Ajn)o6d)$K`BS8`*yGKa!NZ!zhP$;p2!6-piv!+Uc&qJ8z;d z2QIzJP+jd?+uwZ67tr@Fz{yOkV*2Fcr28k`{5++zSLhy}6OUgP$F&TItqQ}f-u_~k zvs$6UyIhrBcdclRBzJhIdwk^42e0YE67%efX={z!Y)j|a8Z)+^mXaEz>aw)`{>-#X z;9uvV*qbfle7{g(uzea zmh~_1k;?8R#q(~ye)me~y_X2yV34F~Rn6FKfGbD?XyXF(fDnV8Un0BH3_y}1mXwqv zIDNW!#F>v~{JO1b{Fj2$6-vBcc6u&uNB80#_3B=F#`@+?&h6j+`a72n=<%P=4|Dm- zoV}whYDS_5h@eH=%6-;{1+K&u!FDHDYB87#T@NJgC{O~{gD~R-KE^mNY1AVuV!7Sm zBk8RBQOFUwp*+sn=kBEEu>l`nw?0Yfq-lyZx#jh{oBH$ByLuQaqYYH zD6g^Bp_CIdOw{qUrY~PNwM=&t(fwMm4^N>dbR!xln&2^q~hdtYYBgY<7>Z z8H|n#){X(v`2Lw(bt} zCck@7FX)!u^_h?HLb#LsmsQ<$3YxPM zXIK9mXtErX(L%`TEP?ARiMv?n;%13Gi}{qM>ORGDG-X|Zq0`_S`{GDSzkBJ~O4HBg z+47ck%D2Nk=huB?#D9brR254|bO>H{J_zKor024vZ#(hBINV~@8TX;I|LBx^@Uvv$ zH)3;_KaE?pJj}R67k484)%OEyMx`)NK$4JtwGy)!YCBhOns@dpt?@F2PnPo$LCigm zXMf&?vn^F+w%t+Eb~v3+{KUY#OW@?UI}uOP7^fv~zE?TKN!GLh|O zHF9nt<&n6&7b}VhE&Jz?t`@L6JkFZk=^1ZmU`@f=p_NQRljuv-DMj~({d>fF9s3uw zNmizcg)U1`9q73xBlU4y zD8SwFJX7DGT#cyyD>`wTL4*0z#k5R4VJu_py~^06>vk0C9B)6GXr}sh8y&r^ObwRR zP4nQ)$vFDz+ihRwcYXt#lr1Cfb8^m5Z~sF#neS6_0l!mjZyL3SS1WL6oeubE6sGJh zIlWrD&Ld8OQTd|!#*D6W=I%qoYvrzpm%=tV!*19KEIu(j8Q_&lNnrxGgB^0&Gj}3;yt`@DSd9(isWa~1o`*$d{GSDCElK|+P%-` zd%GU}HF3cc>zkUmb1+Y=TaleKCaR`|gVAsC);U@-~m7o4;x7p$8yW(&-OO2z=4SpD|Y z8KIc>^w;a~FXSJz&~a$j2T?h0Q^*M5oyr@?WO(3K{8?0Og0*nmOPYCs%x#V0+$ior z$W{}-gqZG@<@6#aiD80B(aje79Ua`%Sq3>{J7@ES)e_39v}cLSSKZdCci#vM_7c8Q zW8`G?JjpV)yZeU4(7>`p>%Q2-`|J*bvWnr`@Z1^yYg?F9nIZ%UQw< zUeX|noP_sogyK(Gvz&BxKUX&pVAW~Hc)lRzA!_Rp zvpfBsvjpo2ls=Say^PiUk<-|l_(bdy*LSn#(u3Us*P5!Axs}hjh5;ch2>I902}mmy zLE9_H#I>0E=d#iOC)>Hk9S%k>$FfC{F^fJ?;e<{@Bq#&0)awl|RPO-;2D<6QG_}rH zb5Zyaa~`+1o-Nn1Nb7{`B*1mufxruGV@H9{4H^r$fPbBw=|ETXa`ez@wMT+B0e5LN9g}9ZX%iB&u$6x7PzD#ob^AbJqm5%=vt~+)I_@I!Q ztNqWDdTIFk;lqQ2Oe+=Te?Dz_ix2gpht}Md#{aoc-9(Q)Gzo`>&JF*U3(1bnoOHyw zauV}XC)-}&-0}UIOPpn$cc_YUd+e>nH8rU*@EE_UB91RMKruTz3oi1|jQs=ae8m}$ z7pgU&us3N^n8SM1x8_!fP89Z zW(}}&pYc#&p|2TqUM57c!!f}T5&sr@96@HSKg41Qsgj+g5+HRfLsDKnK&{U3 z+}^gI-f%>Xuz?z9S|8$;*=YnyN(=rBWm-!$#!iLMpAJFfK~B|^@d1#e4v3}zPk;+~ zdUx5RyU^fSDNAc%S0be$zc*V~s#I4xh!GYazh1wCv5=`8Ulb9vJLCc^%nnB*B@iKj z6CctIO}sQhu4rZmZm9jR=MLl0^aj_KPE?c@sA_ff_2O1mxo2*E1LS)A_wS^rCksl~ z&i5CeYX&e0(UeS_$iPSsd^MXgXF(G=$IzkKqmu%bAE<_f5n zo;_%K0FTrSpfb>BeGJMarcFQWW(~{S3Wjo=H!KvClqPF@oeJN-$AfJBNpSL)6r2qV zu0a97(`m67A2QrO5lkAOLAp?q;AjddQA=w8&&hEF~)DN)PHbdC))IzbA&578{jH4r@T{{36RQxCv1$uP8*dWP11O&x|Hor2?* zoRVT8uQX80N>dmCByl=uziMG&VGkf@IYq^ZnUJ~xkWkoM?SxV{4m|GVjKhRRD~k z@<06_pW4p;5nSljZ(L~Wbur)y|FmowCceBJk?FuV{d{-&+!Ga*X@Bb!N_*!GAwbW= zqOSsexF-O)TH9v?I;~!JUKwZs=@KrBQ3(J$eNIR?2~FGM5F>+*6VA^3-u9VRMUEjm zgBuZ4VNs6~)mLNx_Idm>Hnp2k{n`rcoR>?6wxFEFaN`Eq$jFF*{W2W_sR4ljp~Es| zi@S@=M{q#)q+NFuN?@{6Fc36%Y1(2Ofvf%;diPy|mS=`WMk|MVOAHJQxE>xJ(?b;% zf^CT$)6?5IDs1|OhCpI3^t|!a2!8h*`cQ#_+nY|q>Y>9;egNvz!+M8Zv}Wx{++$zV z3dOT*{njsYHB`cm=b_8?fZ7qFP&&tiguDa17XuR$o}lggPhd}!T^c{4V5h9-EuW#}zA0MB) z8XD0zn(FGr9y~ZLvcG&6VcTi`rVI1Yn65(`lrd;eRAlQ-q;@Fs0mP{GoacoXvJKXlOg<+Bdyb_j}6cH~$XBW&c>>hWvhKDy>FTVGIFAfvQG&|DZBNeW)Cx@l|l6By7O677$RVETo+Ha3CZ$c5ty) zer?1#H-tygUVH$0h@1r$=Wgc<%G?^E(;xJNjjINfTUwK1;%2;pxDopd+Rc zVqtppi4K?-67urF%(kDqK{wv_5_E2>Yi=e7st*}JzsQ1_WS$O!mIpcPYgljE1XmBLxVKcZR-@!wj)vrpp>Qrwhod(T>R4$p<7%22;iM*_ zPq%=5PtFwlVA}dNH?&)qQ&1Q`9CwOHO})--(8f42H^=IG?<>-!yz8!_rZ)Se6r|`C zp$C~bv<)DPk!9A=)vX5o07(;*4A0Hs%>rPXaf2Mf=T1XOL&MbVovFCcEx?6d1KcFl zXvOXeq^8&lqAo?xGvlA+`S zMjG{Vr}-y>l{2eEo~dahu*dEMCD>*G;g5IGbz z-9`P6;jwC=WR0bu@P_012S{HL&jm4_1~Hc3?j#}v?v-w!nF%M-=c1xZpgjr#5_iCA z0z;2#5_-eZK>s45$jC_3*)6zv<}>6a^CLzba%Pn8+&Qtkx!?n;Ce{K-V-_&C4@)QD z4SMnn&Ot|OUtXiGVW=!uLf_~oT3Y_l>xl*?>6iB5DmmCG!M{$R5yS=HOfVc6F#Ml{ z9B!cV9EgJ?fIRRZ#Q{Fkm#S!R{__e73e?FV_a#K}m|09hqL3tC&F|P4TUp(Ns3Q=J z9SE`62_&VbpM^thA*>&CMisgAUB#3(^3G6}fs(QWU&=e5$@k`h0iUvxQ!X z*VZ7Ocn=MlPhz-$N(DU=#$Y2z!TF4N273!F_^N`lc(~BIKu$?%Q@!F8B!Et!U|^gK zA(+8kM{N?46{xkJnHd^d7B$Z1yuro))!h8}a?~Rx*u9=M5s#Rn1?`%i4+3|UdJx|Y zG=eN@hh-MXSsfklT>U1s-eR-L>IIaE2Hf8uqep zf&ITE9W-to!PY?C6wH`XrPtk*mYI3a+MH%0#p7UxzZ3QUW78-i1vanvqep*< z#yp`D0JO3zx{~W38WIv;TznbMoeh0>k;0oI8nQ68jUf5K4HE($A3o6KxS|^251eO< z=CUvxfLfoZVe8w2_J2UE97q944X+YU6ekVvzN0>ziwSUDuVq0i23vtw(6pI_ zjZH#ME|Bu3O&;iu0n2Sbw~v{AmS{Vw;e%J*7|`&DAfgFnRW5Pq18)Hp{w?;mEr753 zKles$8Blqli-Pgi&q6^Us}AQji|DrB0rtg@(f^LzSpSc0eMqa}rE=TO(FoXH7|a7n LS&2;XXK()(hjAI8 literal 0 HcmV?d00001 diff --git a/generate_docs/images/einzel_lens_radial.png b/generate_docs/images/einzel_lens_radial.png new file mode 100644 index 0000000000000000000000000000000000000000..a36eb9db9bf2a95ae5eb7fa1195f109cb4f641f9 GIT binary patch literal 25656 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≫`iNCOvXLfA)ezNJfyTW%-2w1p(;W6}8#aC|3a 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 z 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 + +## 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) +``` -## Result +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 index 25dd97e..b38aaf8 100644 --- a/generate_docs/templates/css.mako +++ b/generate_docs/templates/css.mako @@ -18,6 +18,12 @@ padding: 20px; } + #content .doc-image { + padding:0px; + display:block; + margin: 0px auto; + } + #sidebar { padding: 1.5em; padding-left:2.5em; From 75f91de1e1676fb7683336d3ee90b6a772925fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 22:20:25 +0100 Subject: [PATCH 59/85] Small improvements to docstrings --- traceon/excitation.py | 8 +++----- traceon/field.py | 9 +++++++++ traceon/mesher.py | 2 +- traceon/solver.py | 21 ++++++++++++++------- 4 files changed, 27 insertions(+), 13 deletions(-) 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) From fb15658e5573fefe037640ea98d8dcc104c34880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 22:20:34 +0100 Subject: [PATCH 60/85] Do not show documentation for traceon_pro Rust backend --- generate_docs/templates/html.mako | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/generate_docs/templates/html.mako b/generate_docs/templates/html.mako index 77a2082..77d3aa3 100644 --- a/generate_docs/templates/html.mako +++ b/generate_docs/templates/html.mako @@ -319,7 +319,9 @@
  • Traceon Pro

      % for m in traceon_pro_module.submodules(): -
    • ${link(m)}
    • + % if m.name != 'traceon_pro.traceon_pro': +
    • ${link(m)}
    • + % endif % endfor
  • From fd58dbf7faa38f8bb97077452ec4d2cb0cbd4edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 22:21:25 +0100 Subject: [PATCH 61/85] Update docs/v0.9.0rc1 --- .../images/einzel lens electron traces.png | Bin 0 -> 359157 bytes .../einzel lens potential along axis.png | Bin 0 -> 38573 bytes .../v0.9.0rc1/images/einzel_lens_radial.png | Bin 0 -> 25656 bytes docs/docs/v0.9.0rc1/traceon/einzel-lens.html | 120 +- docs/docs/v0.9.0rc1/traceon/excitation.html | 136 +- docs/docs/v0.9.0rc1/traceon/field.html | 2041 ++++++++++++++ docs/docs/v0.9.0rc1/traceon/focus.html | 11 +- docs/docs/v0.9.0rc1/traceon/geometry.html | 258 +- docs/docs/v0.9.0rc1/traceon/index.html | 21 +- .../docs/v0.9.0rc1/traceon/interpolation.html | 633 ----- docs/docs/v0.9.0rc1/traceon/logging.html | 11 +- docs/docs/v0.9.0rc1/traceon/mesher.html | 185 +- docs/docs/v0.9.0rc1/traceon/plotting.html | 15 +- docs/docs/v0.9.0rc1/traceon/solver.html | 2371 +---------------- docs/docs/v0.9.0rc1/traceon/tracing.html | 287 +- docs/docs/v0.9.0rc1/traceon_pro/field.html | 484 ++++ docs/docs/v0.9.0rc1/traceon_pro/index.html | 13 +- .../v0.9.0rc1/traceon_pro/interpolation.html | 290 -- docs/docs/v0.9.0rc1/traceon_pro/solver.html | 372 ++- .../v0.9.0rc1/traceon_pro/traceon_pro.html | 342 ++- 20 files changed, 3965 insertions(+), 3625 deletions(-) create mode 100644 docs/docs/v0.9.0rc1/images/einzel lens electron traces.png create mode 100644 docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png create mode 100644 docs/docs/v0.9.0rc1/images/einzel_lens_radial.png create mode 100644 docs/docs/v0.9.0rc1/traceon/field.html delete mode 100644 docs/docs/v0.9.0rc1/traceon/interpolation.html create mode 100644 docs/docs/v0.9.0rc1/traceon_pro/field.html delete mode 100644 docs/docs/v0.9.0rc1/traceon_pro/interpolation.html 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 0000000000000000000000000000000000000000..a01e97c433568898787620af1574544b6e921b0b GIT binary patch literal 359157 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%0Df%8&Pi(pbpqZP;XFXxQ{_wR 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͎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%0A;l@l;`tRY;>K<=N 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 LS&2;XXK()(hjAI8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a36eb9db9bf2a95ae5eb7fa1195f109cb4f641f9 GIT binary patch literal 25656 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≫`iNCOvXLfA)ezNJfyTW%-2w1p(;W6}8#aC|3a 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 z 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 - - + + @@ -49,15 +49,112 @@

    Einzel lens

    Introduction

    -

    Some introductory text

    +

    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

    -
    import traceon.geometry as G
    +

    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 = G.Path.line([0., 0., 0.], [1., 0., 0.])
    +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)
     
    -

    An einzel lens bla bla bla

    -

    Result

    -

    +

    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.

    +

    @@ -78,9 +175,9 @@

    Traceon

  • Traceon

    • traceon.excitation
    • +
    • traceon.field
    • traceon.focus
    • traceon.geometry
    • -
    • traceon.interpolation
    • traceon.logging
    • traceon.mesher
    • traceon.plotting
    • @@ -91,9 +188,8 @@

      Traceon

    • Traceon Pro

    • diff --git a/docs/docs/v0.9.0rc1/traceon/excitation.html b/docs/docs/v0.9.0rc1/traceon/excitation.html index 31e1550..cb23883 100644 --- a/docs/docs/v0.9.0rc1/traceon/excitation.html +++ b/docs/docs/v0.9.0rc1/traceon/excitation.html @@ -16,8 +16,8 @@ - - + + @@ -61,14 +61,13 @@

      Module traceon.excitation

      created with the traceon.geometry module.

      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 solve_direct() to compute the resulting field.

      @@ -115,6 +114,14 @@

      Classes

      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): """ @@ -130,7 +137,12 @@

      Classes

      """ for name, voltage in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + + 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): @@ -150,12 +162,16 @@

      Classes

      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. """ - - assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes" - - for name, current in kwargs.items(): - assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode" - self.excitation_types[name] = (ExcitationType.CURRENT, current) + 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.""" @@ -180,7 +196,11 @@

      Classes

      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(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -196,7 +216,11 @@

      Classes

      """ for name, permeability in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -211,7 +235,11 @@

      Classes

      """ for name, permittivity in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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): @@ -230,6 +258,12 @@

      Classes

      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): @@ -248,7 +282,13 @@

      Classes

      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): @@ -373,12 +413,16 @@

      Methods

      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. """ - - assert self.symmetry == Symmetry.RADIAL, "Currently magnetostatics are only supported for radially symmetric meshes" - - for name, current in kwargs.items(): - assert name in self.mesh.physical_to_triangles.keys(), "Current can only be applied to a triangle electrode" - self.excitation_types[name] = (ExcitationType.CURRENT, current)
  • + 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, @@ -417,7 +461,11 @@

    Parameters

    """ for name, permittivity in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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)
    @@ -459,6 +507,12 @@

    Parameters

    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})
    @@ -499,7 +553,11 @@

    Parameters

    """ for name, permeability in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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)
    @@ -541,7 +599,13 @@

    Parameters

    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}) @@ -580,7 +644,11 @@

    Parameters

    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(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + 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) @@ -620,7 +688,12 @@

    Parameters

    """ for name, voltage in kwargs.items(): - assert name in self.electrodes, f'Cannot add {name} to excitation, since it\'s not present in the mesh' + + 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): @@ -1076,9 +1149,9 @@

    Traceon

  • Traceon

    • traceon.excitation
    • +
    • traceon.field
    • traceon.focus
    • traceon.geometry
    • -
    • traceon.interpolation
    • traceon.logging
    • traceon.mesher
    • traceon.plotting
    • @@ -1089,9 +1162,8 @@

      Traceon

    • Traceon Pro

    • 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 index bd2915b..47180fd 100644 --- a/docs/docs/v0.9.0rc1/traceon/focus.html +++ b/docs/docs/v0.9.0rc1/traceon/focus.html @@ -15,8 +15,8 @@ - - + + @@ -155,9 +155,9 @@

      Traceon

    • Traceon

      • traceon.excitation
      • +
      • traceon.field
      • traceon.focus
      • traceon.geometry
      • -
      • traceon.interpolation
      • traceon.logging
      • traceon.mesher
      • traceon.plotting
      • @@ -168,9 +168,8 @@

        Traceon

      • Traceon Pro

      • diff --git a/docs/docs/v0.9.0rc1/traceon/geometry.html b/docs/docs/v0.9.0rc1/traceon/geometry.html index 17ef014..cf4db93 100644 --- a/docs/docs/v0.9.0rc1/traceon/geometry.html +++ b/docs/docs/v0.9.0rc1/traceon/geometry.html @@ -17,8 +17,8 @@ - - + + @@ -2756,6 +2756,7 @@

        Inherited members

      • mirror_yz
      • move
      • rotate
      • +
      • rotate_around_axis
    • @@ -3093,6 +3094,7 @@

      Inherited members

    • mirror_yz
    • move
    • rotate
    • +
    • rotate_around_axis
  • @@ -3416,6 +3418,75 @@

    Inherited members

    ----------------------- 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.): @@ -3622,6 +3693,177 @@

    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) @@ -4689,6 +4931,7 @@

    Inherited members

  • mirror_yz
  • move
  • rotate
  • +
  • rotate_around_axis
  • @@ -4905,6 +5148,7 @@

    Inherited members

  • mirror_yz
  • move
  • rotate
  • +
  • rotate_around_axis
  • @@ -4934,9 +5178,9 @@

    Traceon

  • Traceon

    • traceon.excitation
    • +
    • traceon.field
    • traceon.focus
    • traceon.geometry
    • -
    • traceon.interpolation
    • traceon.logging
    • traceon.mesher
    • traceon.plotting
    • @@ -4947,9 +5191,8 @@

      Traceon

    • Traceon Pro

    • @@ -5029,6 +5272,9 @@

      S
      • __add__
      • __call__
      • +
      • annulus_xy
      • +
      • annulus_xz
      • +
      • annulus_yz
      • aperture
      • box
      • disk_xy
      • diff --git a/docs/docs/v0.9.0rc1/traceon/index.html b/docs/docs/v0.9.0rc1/traceon/index.html index 6eed6e7..b6b6154 100644 --- a/docs/docs/v0.9.0rc1/traceon/index.html +++ b/docs/docs/v0.9.0rc1/traceon/index.html @@ -15,8 +15,8 @@ - - + + @@ -90,6 +90,11 @@

        Sub-modules

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

        + +
        traceon.field
        +
        + +
        traceon.focus
        @@ -102,11 +107,6 @@

        Sub-modules

        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.interpolation
        -
        - -
        traceon.logging
        @@ -168,9 +168,9 @@

        Traceon

      • Traceon

        • traceon.excitation
        • +
        • traceon.field
        • traceon.focus
        • traceon.geometry
        • -
        • traceon.interpolation
        • traceon.logging
        • traceon.mesher
        • traceon.plotting
        • @@ -181,9 +181,8 @@

          Traceon

        • Traceon Pro

        • diff --git a/docs/docs/v0.9.0rc1/traceon/interpolation.html b/docs/docs/v0.9.0rc1/traceon/interpolation.html deleted file mode 100644 index 7152c36..0000000 --- a/docs/docs/v0.9.0rc1/traceon/interpolation.html +++ /dev/null @@ -1,633 +0,0 @@ - - - - - - - - - - traceon.interpolation API documentation - - - - - - - - - - - - - - - - - - - - - - -
          -
          - - - - - -
          -

          Module traceon.interpolation

          -
          - -
          - -
          - -
          -
          - -
          -
          - -
          -
          - -
          -

          Classes

          -
          - -
          - class FieldAxial - (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) -
          - -
          - - - -
          - - Expand source code - -
          class FieldAxial(S.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 FieldRadialAxial - (field, zmin, zmax, N=None) -
          - -
          - - - -
          - - Expand source code - -
          class FieldRadialAxial(FieldAxial):
          -    """ """
          -    def __init__(self, field, zmin, zmax, N=None):
          -        assert isinstance(field, S.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: S.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.TracerRadialAxial(self, bounds)
          -
          - -
          - - -

          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.TracerRadialAxial(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

          - - -
          -
          -
          - -
          - - - - -
          - - - - - \ 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 index 12acb87..750a101 100644 --- a/docs/docs/v0.9.0rc1/traceon/logging.html +++ b/docs/docs/v0.9.0rc1/traceon/logging.html @@ -15,8 +15,8 @@ - - + + @@ -214,9 +214,9 @@

          Traceon

        • Traceon

          • traceon.excitation
          • +
          • traceon.field
          • traceon.focus
          • traceon.geometry
          • -
          • traceon.interpolation
          • traceon.logging
          • traceon.mesher
          • traceon.plotting
          • @@ -227,9 +227,8 @@

            Traceon

          • Traceon Pro

          • diff --git a/docs/docs/v0.9.0rc1/traceon/mesher.html b/docs/docs/v0.9.0rc1/traceon/mesher.html index 8839566..8d77f51 100644 --- a/docs/docs/v0.9.0rc1/traceon/mesher.html +++ b/docs/docs/v0.9.0rc1/traceon/mesher.html @@ -15,8 +15,8 @@ - - + + @@ -86,7 +86,7 @@

            Classes

            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:
            @@ -127,7 +127,7 @@ 

            Classes

            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 counter clockwise around the x, y or z axis. Only one axis supported at the same time + """Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time (rotations do not commute). Parameters @@ -148,23 +148,56 @@

            Classes

            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" - origin = np.array(origin) + + 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" - - if Rx != 0.: - matrix = np.array([[1, 0, 0], - [0, np.cos(Rx), -np.sin(Rx)], - [0, np.sin(Rx), np.cos(Rx)]]) - elif Ry != 0.: - matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)], - [0, 1, 0], - [-np.sin(Ry), 0, np.cos(Ry)]]) - elif Rz != 0.: - matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0], - [np.sin(Rz), np.cos(Rz), 0], - [0, 0, 1]]) - - return self.map_points(lambda p: origin + matrix @ (p - 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. @@ -197,7 +230,7 @@

            Classes

            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 GeometricObject. This means that they all can be moved, rotated, mirrored.

  • Ancestors

    @@ -426,7 +459,7 @@

    Returns

    Expand source code
    def rotate(self, Rx=0., Ry=0., Rz=0., origin=[0., 0., 0.]):
    -    """Rotate counter clockwise around the x, y or z axis. Only one axis supported at the same time
    +    """Rotate counterclockwise around the x, y or z axis. Only one axis supported at the same time
         (rotations do not commute).
     
         Parameters
    @@ -447,26 +480,16 @@ 

    Returns

    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" - origin = np.array(origin) - assert origin.shape == (3,), "Please supply a 3D point for origin" - - if Rx != 0.: - matrix = np.array([[1, 0, 0], - [0, np.cos(Rx), -np.sin(Rx)], - [0, np.sin(Rx), np.cos(Rx)]]) - elif Ry != 0.: - matrix = np.array([[np.cos(Ry), 0, np.sin(Ry)], - [0, 1, 0], - [-np.sin(Ry), 0, np.cos(Ry)]]) - elif Rz != 0.: - matrix = np.array([[np.cos(Rz), -np.sin(Rz), 0], - [np.sin(Rz), np.cos(Rz), 0], - [0, 0, 1]]) - - return self.map_points(lambda p: origin + matrix @ (p - origin))
    + + 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 counter clockwise around the x, y or z axis. Only one axis supported at the same time +

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

    Parameters

    @@ -487,6 +510,79 @@

    Returns

    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.

    +
    + @@ -1656,6 +1752,7 @@

    Inherited members

  • mirror_yz
  • move
  • rotate
  • +
  • rotate_around_axis
  • @@ -1685,9 +1782,9 @@

    Traceon

  • Traceon

  • diff --git a/docs/docs/v0.9.0rc1/traceon/plotting.html b/docs/docs/v0.9.0rc1/traceon/plotting.html index 09d8fa2..5bafe07 100644 --- a/docs/docs/v0.9.0rc1/traceon/plotting.html +++ b/docs/docs/v0.9.0rc1/traceon/plotting.html @@ -16,8 +16,8 @@ - - + + @@ -495,7 +495,7 @@

    Parameters

    excitation : Excitation
    Excitation applied
    -
    field : FieldBEM
    +
    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
    @@ -546,7 +546,7 @@

    Parameters

    Make potential color plot including equipotential lines.

    Parameters

    -
    field : Field
    +
    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'
    @@ -771,9 +771,9 @@

    Traceon

  • Traceon

    • traceon.excitation
    • +
    • traceon.field
    • traceon.focus
    • traceon.geometry
    • -
    • traceon.interpolation
    • traceon.logging
    • traceon.mesher
    • traceon.plotting
    • @@ -784,9 +784,8 @@

      Traceon

    • Traceon Pro

    • diff --git a/docs/docs/v0.9.0rc1/traceon/solver.html b/docs/docs/v0.9.0rc1/traceon/solver.html index ba26415..dd7800d 100644 --- a/docs/docs/v0.9.0rc1/traceon/solver.html +++ b/docs/docs/v0.9.0rc1/traceon/solver.html @@ -16,8 +16,8 @@ - - + + @@ -123,8 +123,7 @@

      Functions

      Returns ------- - A `FieldRadialBEM` if the geometry (contained in the given `excitation`) is radially symmetric. If the geometry is a three - dimensional geometry `Field3D_BEM` is returned. + `FieldRadialBEM` """ if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order(): excitation = _excitation_to_higher_order(excitation) @@ -132,2026 +131,32 @@

      Functions

      mag, elec = excitation.is_magnetostatic(), excitation.is_electrostatic() assert mag or elec, "Solving for an empty excitation" - - if mag and elec: - elec_field = ElectrostaticSolver(excitation).solve_matrix()[0] - mag_field = MagnetostaticSolver(excitation).solve_matrix()[0] - return elec_field + mag_field # type: ignore - elif elec and not mag: - return ElectrostaticSolver(excitation).solve_matrix()[0] - elif mag and not elec: - return MagnetostaticSolver(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

      -

      A FieldRadialBEM if the geometry (contained in the given excitation) is radially symmetric. If the geometry is a three -dimensional geometry Field3D_BEM is returned.

      - - - -
      - - def solve_direct_superposition(excitation) -
      -
      - - - -
      - - Expand source code - -
      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).
      -    """
      -    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([ElectrostaticSolver(excitations[n]).get_right_hand_side() for n in elec_names])
      -    solutions = ElectrostaticSolver(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([MagnetostaticSolver(excitations[n]).get_right_hand_side() for n in mag_names])
      -    solutions = MagnetostaticSolver(excitation).solve_matrix(right_hand_sides)
      -    mag_dict = {n:s for n, s in zip(mag_names, solutions)}
      -        
      -    return {**elec_dict, **mag_dict}
      -
      - -

      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).

      -
      - -
  • - - -
    -

    Classes

    -
    - -
    - class Field -
    - -
    - - - -
    - - Expand source code - -
    class Field(ABC):
    -    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)
    -         
    -        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 magnetostatic_potential_at_point(self, point):
    -        ...
    -    
    -    @abstractmethod
    -    def electrostatic_potential_at_point(self, point):
    -        ...
    -    
    -    @abstractmethod
    -    def magnetostatic_field_at_point(self, point):
    -        ...
    -    
    -    @abstractmethod
    -    def electrostatic_field_at_point(self, point):
    -        ...
    -
    - -

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

    - - -

    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 magnetostatic_potential_at_point(self, point) -
    -
    - - - -
    - - Expand source code - -
    @abstractmethod
    -def magnetostatic_potential_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)
    -     
    -    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 Field3D_BEM - (electrostatic_point_charges=None, magnetostatic_point_charges=None) -
    - -
    - - - -
    - - Expand source code - -
    class Field3D_BEM(FieldBEM):
    -    """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`."""
    -     
    -    def __init__(self, electrostatic_point_charges=None, magnetostatic_point_charges=None):
    -        
    -        if electrostatic_point_charges is None:
    -            electrostatic_point_charges = EffectivePointCharges.empty_3d()
    -        if magnetostatic_point_charges is None:
    -            magnetostatic_point_charges = EffectivePointCharges.empty_3d()
    -         
    -        super().__init__(electrostatic_point_charges, magnetostatic_point_charges, EffectivePointCharges.empty_3d())
    -        
    -        self.symmetry = E.Symmetry.THREE_D
    -
    -        for eff in [electrostatic_point_charges, magnetostatic_point_charges]:
    -            N = len(eff.charges)
    -            assert eff.charges.shape == (N,)
    -            assert eff.jacobians.shape == (N, backend.N_TRIANGLE_QUAD)
    -            assert eff.positions.shape == (N, backend.N_TRIANGLE_QUAD, 3)
    -     
    -    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 
    -        """
    -        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_3d(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_3d(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"
    -        charges = self.magnetostatic_point_charges.charges
    -        jacobians = self.magnetostatic_point_charges.jacobians
    -        positions = self.magnetostatic_point_charges.positions
    -        return backend.field_3d(point, charges, jacobians, positions)
    -     
    -    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_3d(point, charges, jacobians, positions)
    -     
    -    def area_of_element(self, i):
    -        jacobians = self.electrostatic_point_charges.jacobians
    -        return np.sum(jacobians[i])
    -    
    -    def get_tracer(self, bounds):
    -        return T.Tracer3D_BEM(self, bounds)
    -
    - -

    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) -
    -
    - - - -
    - - Expand source code - -
    def area_of_element(self, i):
    -    jacobians = self.electrostatic_point_charges.jacobians
    -    return np.sum(jacobians[i])
    -
    - -
    -
    - - -
    - - 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 representing the electric field 
    -    """
    -    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_3d(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 representing the electric field

    -
    - - -
    - - 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_3d(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_tracer(self, bounds) -
    -
    - - - -
    - - Expand source code - -
    def get_tracer(self, bounds):
    -    return T.Tracer3D_BEM(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"
    -    charges = self.magnetostatic_point_charges.charges
    -    jacobians = self.magnetostatic_point_charges.jacobians
    -    positions = self.magnetostatic_point_charges.positions
    -    return backend.field_3d(point, charges, jacobians, positions)
    -
    - -

    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_3d(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

    - - -
    - -
    - class FieldAxial - (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) -
    - -
    - - - -
    - - Expand source code - -
    class FieldAxial(Field):
    -    """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)}>'
    -
    - -

    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 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 - (z, electrostatic_coeffs=None, magnetostatic_coeffs=None) -
    - -
    - - - -
    - - Expand source code - -
    class FieldRadialAxial(FieldAxial):
    -    """ """
    -    def __init__(self, z, electrostatic_coeffs=None, magnetostatic_coeffs=None):
    -        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)
    -        self.symmetry = E.Symmetry.RADIAL
    -    
    -    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"
    -        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: (3,) array of float64
    -            Position at which to compute the field.
    -             
    -        Returns
    -        -------
    -        (3,) array of float64, containing the field strengths (units of A/m)
    -        """
    -        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: (3,) 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: (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"
    -        return backend.potential_radial_derivs(point, self.z, self.magnetostatic_coeffs)
    -    
    -    def get_tracer(self, bounds):
    -        return T.TracerRadialAxial(self, bounds)
    -
    - -
    - - -

    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: (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"
    -    return backend.field_radial_derivs(point, self.z, self.electrostatic_coeffs)
    -
    - -

    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 (close to the axis).
    -
    -    Parameters
    -    ----------
    -    point: (3,) 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 : (3,) 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.TracerRadialAxial(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,) array of float64, containing the field strengths (units of A/m)
    -    """
    -    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 : (3,) array of float64
    -
    Position at which to compute the field.
    -
    -

    Returns

    -

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

    -
    - - -
    - - 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: (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"
    -    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 : (3,) 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_)
    -        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(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.TracerRadialBEM(self, bounds)
    -
    - -

    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_)
    -    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(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)
    + 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]
    -

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

    +

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

    Parameters

    -
    z : (N,) np.ndarray of float64
    -
    Positions on the optical axis at which to compute the derivatives.
    +
    excitation : Excitation
    +
    The excitation that produces the resulting field.

    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.

    +

    FieldRadialBEM

    - -
    + +
    - def get_electrostatic_axial_potential_derivatives(self, z) + def solve_direct_superposition(excitation)
    @@ -2161,220 +166,58 @@

    Returns

    Expand source code -
    def get_electrostatic_axial_potential_derivatives(self, z):
    +        
    def solve_direct_superposition(excitation):
         """
    -    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) -
    -
    - - + 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). -
    - - 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.TracerRadialBEM(self, bounds)
    -
    - -
    -
    - - -
    - - def magnetostatic_field_at_point(self, point_) -
    -
    - - - -
    - - Expand source code - -
    def magnetostatic_field_at_point(self, point_):
    +    ---------------------------
    +    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.
         """
    -    Compute the magnetic field \\( \\vec{H} \\)
    +    if excitation.mesh.is_2d() and not excitation.mesh.is_higher_order():
    +        excitation = _excitation_to_higher_order(excitation)
         
    -    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)
    +    # Speedup: invert matrix only once, when using superposition
    +    excitations = excitation._split_for_superposition()
         
    -    charges = self.magnetostatic_point_charges.charges
    -    jacobians = self.magnetostatic_point_charges.jacobians
    -    positions = self.magnetostatic_point_charges.positions
    +    # 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)}
         
    -    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.

    -
    - - -
    + # 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)} - 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)
    + return {**elec_dict, **mag_dict}
    -

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

    -

    Parameters

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

    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

    -

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

    +

    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.

    -
    - - -

    Inherited members

    - - -
    +
    +
    + @@ -2395,9 +238,9 @@

    Traceon

  • Traceon

    diff --git a/docs/docs/v0.9.0rc1/traceon/tracing.html b/docs/docs/v0.9.0rc1/traceon/tracing.html index 7918ab4..4018950 100644 --- a/docs/docs/v0.9.0rc1/traceon/tracing.html +++ b/docs/docs/v0.9.0rc1/traceon/tracing.html @@ -16,8 +16,8 @@ - - + + @@ -485,7 +485,21 @@

    Classes

    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]) @@ -517,13 +531,23 @@

    Classes

    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. """ - raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance')
    + 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 : Field (or any class inheriting Field)
    +
    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) ).
    @@ -531,13 +555,6 @@

    Parameters

    -

    Subclasses

    -

    Methods

    @@ -578,7 +595,17 @@

    Methods

    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. """ - raise RuntimeError('Please use the field.get_tracer(...) method to get the appropriate Tracer instance') + 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.

    @@ -608,217 +635,6 @@

    Returns

    - - -
    - class Tracer3DAxial - (field, bounds) -
    - -
    - - - -
    - - Expand source code - -
    class Tracer3DAxial(Tracer):
    -    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    -        charge_over_mass = charge / mass
    -        velocity = _convert_velocity_to_SI(velocity, mass)
    -        return backend.trace_particle_3d_derivs(position, velocity, charge_over_mass, self.bounds, atol,
    -            self.field.z, self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs)
    -
    - -

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

    -

    Parameters

    -
    -
    field : 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) ).
    -
    - - -

    Ancestors

    - - - - -

    Inherited members

    - - -
    - -
    - class Tracer3D_BEM - (field, bounds) -
    - -
    - - - -
    - - Expand source code - -
    class Tracer3D_BEM(Tracer):
    -    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    -        charge_over_mass = charge / mass
    -        velocity = _convert_velocity_to_SI(velocity, mass)
    -        elec, mag = self.field.electrostatic_point_charges, self.field.magnetostatic_point_charges
    -        return backend.trace_particle_3d(position, velocity, charge_over_mass, self.bounds, atol, elec, mag, field_bounds=self.field.field_bounds)
    -
    - -

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

    -

    Parameters

    -
    -
    field : 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) ).
    -
    - - -

    Ancestors

    - - - - -

    Inherited members

    - - -
    - -
    - class TracerRadialAxial - (field, bounds) -
    - -
    - - - -
    - - Expand source code - -
    class TracerRadialAxial(Tracer):
    -    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    -        charge_over_mass = charge / mass
    -        velocity = _convert_velocity_to_SI(velocity, mass)
    -        
    -        elec, mag = self.field.electrostatic_coeffs, self.field.magnetostatic_coeffs
    -        
    -        return backend.trace_particle_radial_derivs(position, velocity, charge_over_mass, self.bounds, atol, self.field.z, elec, mag)
    -
    - -

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

    -

    Parameters

    -
    -
    field : 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) ).
    -
    - - -

    Ancestors

    - - - - -

    Inherited members

    - - -
    - -
    - class TracerRadialBEM - (field, bounds) -
    - -
    - - - -
    - - Expand source code - -
    class TracerRadialBEM(Tracer):
    -    def __call__(self, position, velocity, mass=m_e, charge=-e, atol=1e-10):
    -        charge_over_mass = charge / mass
    -        velocity = _convert_velocity_to_SI(velocity, mass)
    -        
    -        return backend.trace_particle_radial(
    -                position,
    -                velocity,
    -                charge_over_mass, 
    -                self.bounds,
    -                atol, 
    -                self.field.electrostatic_point_charges,
    -                self.field.magnetostatic_point_charges,
    -                self.field.current_point_charges,
    -                field_bounds=self.field.field_bounds)
    -
    - -

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

    -

    Parameters

    -
    -
    field : 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) ).
    -
    - - -

    Ancestors

    - - - - -

    Inherited members

    - -
    @@ -843,9 +659,9 @@

    Traceon

  • Traceon

    -
  • -
  • -

    Tracer3DAxial

    - -
  • -
  • -

    Tracer3D_BEM

    - -
  • -
  • -

    TracerRadialAxial

    - -
  • -
  • -

    TracerRadialBEM

    -
  • 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 index b1b4c85..fb507fe 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/index.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/index.html @@ -15,8 +15,8 @@ - - + + @@ -62,7 +62,7 @@

    Package traceon_pro

    Sub-modules

    -
    traceon_pro.interpolation
    +
    traceon_pro.field
    @@ -109,9 +109,9 @@

    Traceon

  • Traceon

    • traceon.excitation
    • +
    • traceon.field
    • traceon.focus
    • traceon.geometry
    • -
    • traceon.interpolation
    • traceon.logging
    • traceon.mesher
    • traceon.plotting
    • @@ -122,9 +122,8 @@

      Traceon

    • Traceon Pro

    • diff --git a/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html b/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html deleted file mode 100644 index da3ecce..0000000 --- a/docs/docs/v0.9.0rc1/traceon_pro/interpolation.html +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - traceon_pro.interpolation API documentation - - - - - - - - - - - - - - - - - - - - - - -
      -
      - - - - - -
      -

      Module traceon_pro.interpolation

      -
      - -
      - -
      - -
      -
      - -
      -
      - -
      -
      - -
      -

      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_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

      - - -
      -
      -
      - -
      - - - - -
      - - - - - \ 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 index c2b3ae3..f688548 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/solver.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/solver.html @@ -15,8 +15,8 @@ - - + + @@ -69,6 +69,44 @@

      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) @@ -99,6 +137,76 @@

      Functions

      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) @@ -114,7 +222,8 @@

      Classes

      Ancestors

      @@ -130,6 +239,206 @@

      Methods

      +
      +
  • + +
    + + +

    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) +
    +
    + + +
    @@ -161,9 +470,9 @@

    Traceon

  • Traceon

  • diff --git a/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html b/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html index 85348fc..5946601 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html @@ -15,8 +15,8 @@ - - + + @@ -66,12 +66,257 @@

    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) @@ -153,6 +398,39 @@

    Methods

    +
    + + +
    + + + + + +
    + class FieldEvaluationArgs + (elec, mag, currents, bounds=None) +
    + +
    + + + +
    + + + +

    Methods

    +
    + +
    + + def address(self, /) +
    +
    + + +
    @@ -184,9 +462,9 @@

    Traceon

  • Traceon

  • From 4e99bd8e13365afd857a59c0dac3e0843e3be1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 30 Dec 2024 22:31:26 +0100 Subject: [PATCH 62/85] Change python3 into python in make_docs.sh --- generate_docs/make_docs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generate_docs/make_docs.sh b/generate_docs/make_docs.sh index b5b073f..d8669d3 100644 --- a/generate_docs/make_docs.sh +++ b/generate_docs/make_docs.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION=$(python3 -c "from importlib.metadata import version; print(version('traceon'))") +VERSION=$(python -c "from importlib.metadata import version; print(version('traceon'))") DIR=../docs/docs/v$VERSION/ if [ -d $DIR ]; then @@ -8,7 +8,7 @@ if [ -d $DIR ]; then exit fi -python3 ./custom_pdoc.py traceon -o $DIR --force --html --config latex_math=True +python ./custom_pdoc.py traceon -o $DIR --force --html --config latex_math=True cp -r ./images $DIR/images/ From 5ff6822df4af91c9ed4f4472cdfa186a9c19dcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 1 Jan 2025 20:35:33 +0100 Subject: [PATCH 63/85] Add feature list to README.md --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db6f871..a1843ed 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@ # Traceon +Traceon is a Python library designed for the modeling of electron microscopes. + +Traceon features a Boundary Element Method (BEM) solver that computes electrostatic and magnetostatic fields for accurate particle tracing. By relying on the BEM rather than FEM, Traceon offers notable improvements in speed and accuracy compared to most commercial alternatives. It supports both radially symmetric and fully 3D geometries, while leveraging advanced axial field interpolations to greatly speed up the particle tracing. + +### Wait, is this commercial? + +The core of Traceon is completely free to use and open source, and distributed under the `MPL 2.0` license. The software downloaded when using `pip install traceon` does not include any closed source software. There is a commercial package called `traceon_pro` which builds on the `traceon` package to provide a more capable library. + +| Feature | Traceon | Traceon Pro | +| --- | :---: | :---: | +| Parametric mesher | ✅ | ✅| +| Plot module | ✅ | ✅ | +| Radial symmetric solver (electrostatic, magnetostatic) | ✅ | ✅ | +| Radial symmetric particle tracer | ✅ | ✅ | +| Radial symmetric axial interpolation (fast tracing) | ✅ | ✅| +| 3D direct solver (electrostatic, magnetostatic) | | ✅| +| 3D solver using fast multipole method | | ✅| +| 3D particle tracing | | ✅| +| 3D axial interpolation (fast tracing) | | ✅| -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. -The core of Traceon is completely free to use and open source. There is a commerical upgrade with more capabilities. The source code in this repository is distributed under the `MPL 2.0` license. ## Documentation From 4527e2f0a7ebcc663f83a4f95ae2377234f17ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 1 Jan 2025 21:48:51 +0100 Subject: [PATCH 64/85] Improve docstrings in field.py, solver.py and tracing.py --- traceon/field.py | 34 +++++++++++++++++++++++++++++++++- traceon/solver.py | 44 ++++++++------------------------------------ traceon/tracing.py | 6 +++--- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/traceon/field.py b/traceon/field.py index 2d8a1bf..02f185a 100644 --- a/traceon/field.py +++ b/traceon/field.py @@ -1,3 +1,19 @@ +""" +## 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. + +[1] P. Hawkes, E. Kasper. Principles of Electron Optics. Volume one: Basic Geometrical Optics. 2018. +""" + import time from abc import ABC, abstractmethod @@ -559,8 +575,24 @@ def _quintic_spline_coefficients(z, derivs): class FieldRadialAxial(FieldAxial): - """ """ def __init__(self, field, zmin, zmax, N=None): + """ + Produces a field which uses an axial interpolation to very quickly compute the field around the z-axis. + Note that the approximation degrades as the point at which the field is computed is further from the z-axis. + + Parameters + ----------------------- + field: `traceon.field.FieldRadialBEM` + Field for which to compute the axial interpolation + 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. + """ assert isinstance(field, FieldRadialBEM) z, electrostatic_coeffs, magnetostatic_coeffs = FieldRadialAxial._get_interpolation_coefficients(field, zmin, zmax, N=N) diff --git a/traceon/solver.py b/traceon/solver.py index d6070c4..db63d0b 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -2,40 +2,8 @@ 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. - +is crucial that the field evaluation can be done faster. To achieve this, interpolation techniques can be used, see `traceon.field.FieldRadialAxial`, +and `traceon_pro.field.Field3DAxial`. """ __pdoc__ = {} @@ -361,11 +329,15 @@ def solve_direct_superposition(excitation): 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). + + Parameters + --------------------- + excitation: `traceon.excitation.Excitation` + The excitation that produces the resulting field. 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. + Dictionary from str to `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) diff --git a/traceon/tracing.py b/traceon/tracing.py index da2ac3b..91f58de 100644 --- a/traceon/tracing.py +++ b/traceon/tracing.py @@ -1,5 +1,5 @@ """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 +used is RK45 with adaptive step size control [1]. The tracing code is implemented in C 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. @@ -105,8 +105,8 @@ class Tracer: Parameters ---------- - field: traceon.solver.Field (or any class inheriting Field) - The field used to compute the force felt by the charged particle. + field: `traceon.field.Field` + The field used to compute the force felt by the charged particle. Note that any child class of `traceon.field.Field` can be used. 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) ). """ From 130f566a5b69988e084c15227f9fe3b2bd5f80ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Wed, 1 Jan 2025 21:52:25 +0100 Subject: [PATCH 65/85] Update documentation of v0.9.0rc1 --- docs/docs/v0.9.0rc1/traceon/field.html | 55 ++- docs/docs/v0.9.0rc1/traceon/index.html | 2 +- docs/docs/v0.9.0rc1/traceon/solver.html | 44 +- docs/docs/v0.9.0rc1/traceon/tracing.html | 10 +- docs/docs/v0.9.0rc1/traceon_pro/field.html | 79 ++-- docs/docs/v0.9.0rc1/traceon_pro/index.html | 2 +- docs/docs/v0.9.0rc1/traceon_pro/solver.html | 438 +++----------------- 7 files changed, 165 insertions(+), 465 deletions(-) diff --git a/docs/docs/v0.9.0rc1/traceon/field.html b/docs/docs/v0.9.0rc1/traceon/field.html index 86f3232..a9dd936 100644 --- a/docs/docs/v0.9.0rc1/traceon/field.html +++ b/docs/docs/v0.9.0rc1/traceon/field.html @@ -8,7 +8,7 @@ traceon.field API documentation - + @@ -56,7 +56,17 @@

    Module traceon.field

    - +

    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.

    +

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

    @@ -575,7 +585,7 @@

    Inherited members

    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) + self.field_bounds = np.array(bounds, dtype=np.float64) assert self.field_bounds.shape == (3,2) def is_electrostatic(self): @@ -890,7 +900,7 @@

    Returns

    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) + self.field_bounds = np.array(bounds, dtype=np.float64) assert self.field_bounds.shape == (3,2)
    @@ -934,8 +944,24 @@

    Inherited members

    Expand source code
    class FieldRadialAxial(FieldAxial):
    -    """ """
         def __init__(self, field, zmin, zmax, N=None):
    +        """
    +        Produces a field which uses an axial interpolation to very quickly compute the field around the z-axis.
    +        Note that the approximation degrades as the point at which the field is computed is further from the z-axis.
    +
    +        Parameters
    +        -----------------------
    +        field: `traceon.field.FieldRadialBEM`
    +            Field for which to compute the axial interpolation
    +        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.
    +        """
             assert isinstance(field, FieldRadialBEM)
     
             z, electrostatic_coeffs, magnetostatic_coeffs = FieldRadialAxial._get_interpolation_coefficients(field, zmin, zmax, N=N)
    @@ -1040,7 +1066,24 @@ 

    Inherited members

    return backend.field_fun(("field_radial_derivs_traceable", backend.backend_lib)), args
    -
    +

    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.

    +

    Produces a field which uses an axial interpolation to very quickly compute the field around the z-axis. +Note that the approximation degrades as the point at which the field is computed is further from the z-axis.

    +

    Parameters

    +
    +
    field : FieldRadialBEM
    +
    Field for which to compute the axial interpolation
    +
    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.
    +

    Ancestors

    diff --git a/docs/docs/v0.9.0rc1/traceon/index.html b/docs/docs/v0.9.0rc1/traceon/index.html index b6b6154..5985a99 100644 --- a/docs/docs/v0.9.0rc1/traceon/index.html +++ b/docs/docs/v0.9.0rc1/traceon/index.html @@ -94,7 +94,7 @@

    Sub-modules

    traceon.field
    -
    +

    Radial series expansion in cylindrical symmetry …

    traceon.focus
    diff --git a/docs/docs/v0.9.0rc1/traceon/solver.html b/docs/docs/v0.9.0rc1/traceon/solver.html index dd7800d..fa36960 100644 --- a/docs/docs/v0.9.0rc1/traceon/solver.html +++ b/docs/docs/v0.9.0rc1/traceon/solver.html @@ -61,32 +61,8 @@

    Module traceon.solver

    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.

    +is crucial that the field evaluation can be done faster. To achieve this, interpolation techniques can be used, see FieldRadialAxial, +and Field3DAxial.

    @@ -174,11 +150,15 @@

    Returns

    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). + + Parameters + --------------------- + excitation: `traceon.excitation.Excitation` + The excitation that produces the resulting field. 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. + Dictionary from str to `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) @@ -207,9 +187,13 @@

    Returns

    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).

    +

    Parameters

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

    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.

    +

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

    diff --git a/docs/docs/v0.9.0rc1/traceon/tracing.html b/docs/docs/v0.9.0rc1/traceon/tracing.html index 4018950..62f91de 100644 --- a/docs/docs/v0.9.0rc1/traceon/tracing.html +++ b/docs/docs/v0.9.0rc1/traceon/tracing.html @@ -58,7 +58,7 @@

    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 +used is RK45 with adaptive step size control [1]. The tracing code is implemented in C 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

    @@ -473,8 +473,8 @@

    Classes

    Parameters ---------- - field: traceon.solver.Field (or any class inheriting Field) - The field used to compute the force felt by the charged particle. + field: `traceon.field.Field` + The field used to compute the force felt by the charged particle. Note that any child class of `traceon.field.Field` can be used. 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) ). """ @@ -547,8 +547,8 @@

    Classes

    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.
    +
    field : Field
    +
    The field used to compute the force felt by the charged particle. Note that any child class of Field can be used.
    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) ).
    diff --git a/docs/docs/v0.9.0rc1/traceon_pro/field.html b/docs/docs/v0.9.0rc1/traceon_pro/field.html index 0d6b52e..fe0676b 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/field.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/field.html @@ -8,7 +8,7 @@ traceon_pro.field API documentation - + @@ -56,7 +56,17 @@

    Module traceon_pro.field

    - +

    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 [1]) 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.

    +

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

    @@ -81,12 +91,13 @@

    Classes

    -

    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 +

    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

    +
    field : Field3D_BEM
    +
    The field which to approximate along the z-axis
    zmin : float
    Location on the optical axis where to start sampling the radial expansion coefficients.
    zmax : float
    @@ -148,18 +159,6 @@

    Returns

    -
    - - def get_low_level_trace_function(self) -
    -
    - - - -
    -
    - -
    def get_tracer(self, bounds) @@ -168,7 +167,9 @@

    Returns

    -
    +

    Get a Tracer class capable of tracing particle through this field.

    +

    Returns

    +

    Tracer

    @@ -235,7 +236,7 @@

    Inherited members

    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.

    +solve_direct() function. See also the comments in FieldBEM.

    Ancestors

    @@ -256,7 +257,17 @@

    Methods

    -
    +

    Compute the area of a specific triangle which makes up the field.

    +

    Parameters

    +
    +
    i : int
    +
    Index of the triangle for which to compute the area
    +
    +

    Returns

    +
    +
    float
    +
    Area of the triangle (in meter^2).
    +
    @@ -268,7 +279,17 @@

    Methods

    -
    +

    Compute the magnetic field \vec{H} resulting from the current +excitation by using the Biot-Savart law. Note that the magnetic field +does not include the field induced by the magnetizable material (for this, +see the traceon.field.Field3D_BEM.magnetostatic_field_at_point method).

    +

    Parameters

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

    Returns

    +

    (3,) array of float64 representing the magnetic field

    @@ -310,18 +331,6 @@

    Returns

    -
    - - def get_low_level_trace_function(self) -
    -
    - - - -
    -
    - -
    def get_tracer(self, bounds) @@ -330,7 +339,9 @@

    Returns

    -
    +

    Get a Tracer class capable of tracing particle through this field.

    +

    Returns

    +

    Tracer

    @@ -443,7 +454,6 @@

  • electrostatic_field_at_point
  • electrostatic_potential_at_point
  • -
  • get_low_level_trace_function
  • get_tracer
  • magnetostatic_field_at_point
  • magnetostatic_potential_at_point
  • @@ -460,7 +470,6 @@

    current_field_at_point
  • electrostatic_field_at_point
  • electrostatic_potential_at_point
  • -
  • get_low_level_trace_function
  • get_tracer
  • magnetostatic_field_at_point
  • magnetostatic_potential_at_point
  • diff --git a/docs/docs/v0.9.0rc1/traceon_pro/index.html b/docs/docs/v0.9.0rc1/traceon_pro/index.html index fb507fe..751ef2d 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/index.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/index.html @@ -65,7 +65,7 @@

    Sub-modules

    traceon_pro.field
    -
    +

    Radial series expansion in 3D …

    traceon_pro.solver
    diff --git a/docs/docs/v0.9.0rc1/traceon_pro/solver.html b/docs/docs/v0.9.0rc1/traceon_pro/solver.html index f688548..1778510 100644 --- a/docs/docs/v0.9.0rc1/traceon_pro/solver.html +++ b/docs/docs/v0.9.0rc1/traceon_pro/solver.html @@ -78,14 +78,14 @@

    Functions

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

    +into account the specified excitation.

    Parameters

    excitation : Excitation
    The excitation that produces the resulting field.

    Returns

    -

    traceon.field.Field3D_BEM

    +

    Field3D_BEM or FieldRadialBEM

    @@ -97,13 +97,20 @@

    Returns

    -

    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 or 1A) +of an electrode that was assigned a non-zero fixed voltage (or current) value. This is useful when a geometry needs +to be analyzed for many different voltage (or current) 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, position dependent voltages are invalid in the superposition process.

    +

    Parameters

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

    Returns

    +

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

    @@ -115,7 +122,22 @@

    Returns

    -
    +

    Solve for the charges on the surface of the geometry by using a fast multipole method (FMM) and taking +into account the specified excitation. The FMM works by diving the geometry up into boxes, and using approximation +formulas to compute the potential interaction of elements that are 'far apart'.

    +

    Parameters

    +
    +
    excitation : Excitation
    +
    The excitation that produces the resulting field.
    +
    N_max : int
    +
    The maximum number of triangles to put into every box when dividing up the geometry. A larger number usually +improves accuracy (and sometimes performance) but increases memory usage.
    +
    l_max : int
    +
    The order of the multipole expansions used to compute the far-away interactions. A larger number improves +accuracy but increases the solve time.
    +
    +

    Returns

    +

    Field3D_BEM

    @@ -127,327 +149,31 @@

    Returns

    -
    +

    When using superposition multiple fields are computed. Each field corresponds with a unity excitation (1V or 1A) +of an electrode that was assigned a non-zero fixed voltage (or current) value. This is useful when a geometry needs +to be analyzed for many different voltage (or current) settings. In this case taking a linear superposition of the returned fields +allows to select a different voltage 'setting' without inducing any computational cost. Note that unlike the direct method the +fast multipole method (FMM) uses an iterative solver and therefore needs to redo the entire solve step for every unity excitation.

    +

    Parameters

    +
    +
    excitation : Excitation
    +
    The excitation that produces the resulting field.
    +
    N_max : int
    +
    The maximum number of triangles to put into every box when dividing up the geometry. A larger number usually +improves accuracy (and sometimes performance) but increases memory usage.
    +
    l_max : int
    +
    The order of the multipole expansions used to compute the far-away interactions. A larger number improves +accuracy but increases the solve time.
    +
    +

    Returns

    +

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

    -

    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) -
    -
    - - - -
    -
    - -
    - - - -
    -
    @@ -502,68 +228,6 @@

    Traceon

    -
  • Classes

    - -
  • From 322007e3112e29aa9abdebe81ddd77de1840589e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 4 Jan 2025 10:42:05 +0100 Subject: [PATCH 66/85] Fix some mypy typing errors in backend/__init__.py --- traceon/backend/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 9ca7ff7..4d1f121 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -650,7 +650,7 @@ def fill_jacobian_buffer_radial(vertices: np.ndarray) -> Tuple[np.ndarray, np.nd def self_potential_radial(vertices: np.ndarray) -> float: assert vertices.shape == (4,3) and vertices.dtype == np.double user_data = vertices.ctypes.data_as(C.c_void_p) - low_level = LowLevelCallable(backend_lib.self_potential_radial, user_data=user_data) + low_level = LowLevelCallable(backend_lib.self_potential_radial, user_data=user_data) # type: ignore return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] # type: ignore class SelfFieldDotNormalRadialArgs(C.Structure): @@ -666,7 +666,7 @@ def self_field_dot_normal_radial(vertices: np.ndarray, K: float) -> float: user_data = C.cast(C.pointer(args), vp) - low_level = LowLevelCallable(backend_lib.self_field_dot_normal_radial, user_data=user_data) + low_level = LowLevelCallable(backend_lib.self_field_dot_normal_radial, user_data=user_data) # type: ignore return quad(low_level, -1, 1, points=(0,), epsabs=1e-9, epsrel=1e-9, limit=250)[0] # type: ignore From c1004270f28e5df90092bcc6a31879616e76b1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 4 Jan 2025 11:44:40 +0100 Subject: [PATCH 67/85] Add PERMANENT_MAGNET to excitation.py --- traceon/excitation.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index 7d2770a..d6b04a9 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -48,6 +48,7 @@ class ExcitationType(IntEnum): CURRENT = 4 MAGNETOSTATIC_POT = 5 MAGNETIZABLE = 6 + PERMANENT_MAGNET = 7 def is_electrostatic(self): return self in [ExcitationType.VOLTAGE_FIXED, @@ -57,7 +58,8 @@ def is_electrostatic(self): def is_magnetostatic(self): return self in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.MAGNETIZABLE, - ExcitationType.CURRENT] + ExcitationType.CURRENT, + ExcitationType.PERMANENT_MAGNET] def __str__(self): if self == ExcitationType.VOLTAGE_FIXED: @@ -72,6 +74,8 @@ def __str__(self): return 'magnetostatic potential' elif self == ExcitationType.MAGNETIZABLE: return 'magnetizable' + elif self == ExcitationType.PERMANENT_MAGNET: + return 'permanent magnet' raise RuntimeError('ExcitationType not understood in __str__ method') @@ -201,6 +205,25 @@ def add_magnetizable(self, **kwargs): self._ensure_electrode_is_triangles('magnetizable', name) self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability) + + def add_permanent_magnet(self, **kwargs): + """ + Assign a magnetization vector to a permanent magnet. + + Parameters + ---------- + **kwargs : dict + The keys of the dictionary are the geometry names, while the values are the magnetization vectors M. + """ + for name, vector in kwargs.items(): + if self.symmetry == E.Symmetry.RADIAL: + self._ensure_electrode_is_lines('magnetizable', name) + assert vector.shape == (3,) and vector[1] == 0.0, "In radial symmetry the magnetization vector must lie in the XZ plane." + elif self.symmetry == E.Symmetry.THREE_D: + self._ensure_electrode_is_triangles('magnetizable', name) + assert vector.shape == (3,), "The magnetization vector must be a 3D vector." + + self.excitation_types[name] = (ExcitationType.PERMANENT_MAGNET, vector) def add_dielectric(self, **kwargs): """ From 58931a49906b831ed01b49e044b2c261b26a7590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sat, 4 Jan 2025 13:27:56 +0100 Subject: [PATCH 68/85] Fix type signature of function higher_order_normal_radial --- traceon/backend/__init__.py | 5 +++-- traceon/backend/utilities_2d.c | 2 +- traceon/solver.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/traceon/backend/__init__.py b/traceon/backend/__init__.py index 9ca7ff7..5cd2db0 100644 --- a/traceon/backend/__init__.py +++ b/traceon/backend/__init__.py @@ -280,7 +280,7 @@ def __init__(self, z, elec, mag, *args, **kwargs): 'ellipem1' : (dbl, dbl), 'ellipe': (dbl, dbl), 'normal_2d': (None, v2, v2, v2), - 'higher_order_normal_radial': (None, dbl, v2, v2, v2, v2, v2), + 'higher_order_normal_radial': (None, dbl, v3, v3, v3, v3, v2), 'normal_3d': (None, arr(shape=(3,3)), v3), 'position_and_jacobian_3d': (None, dbl, dbl, arr(ndim=2), v3, dbl_p), 'position_and_jacobian_radial': (None, dbl, v2, v2, v2, v2, v2, dbl_p), @@ -350,8 +350,9 @@ def kronrod_adaptive(fun: Callable[[float], float], a: float, b: float, return backend_lib.kronrod_adaptive(callback, a, b, vp(None), epsabs, epsrel) def higher_order_normal_radial(alpha: float, vertices: np.ndarray) -> np.ndarray: + assert vertices.shape == (4,3) normal = np.zeros(2) - backend_lib.higher_order_normal_radial(alpha, vertices[0, :2], vertices[2, :2], vertices[3, :2], vertices[1, :2], normal) + backend_lib.higher_order_normal_radial(alpha, vertices[0], vertices[2], vertices[3], vertices[1], normal) assert np.isclose(np.linalg.norm(normal), 1.0) return normal diff --git a/traceon/backend/utilities_2d.c b/traceon/backend/utilities_2d.c index a444969..94848f8 100644 --- a/traceon/backend/utilities_2d.c +++ b/traceon/backend/utilities_2d.c @@ -29,7 +29,7 @@ normal_2d(double *p1, double *p2, double *normal) { } EXPORT void -higher_order_normal_radial(double alpha, double *v1, double *v2, double *v3, double *v4, double *normal) { +higher_order_normal_radial(double alpha, double v1[3], double v2[3], double v3[3], double v4[3], double normal[2]) { double v1x = v1[0], v1y = v1[2]; double v2x = v2[0], v2y = v2[2]; diff --git a/traceon/solver.py b/traceon/solver.py index d6070c4..644e88c 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -225,7 +225,7 @@ def __init__(self, *args, **kwargs): if not self.is_higher_order(): normals[i] = backend.normal_2d(v[0], v[1]) else: - normals[i] = backend.higher_order_normal_radial(0.0, v[:, :2]) + normals[i] = backend.higher_order_normal_radial(0.0, v) self.normals = normals From 0000ab766e238d2104b67071d181bdd2c811032b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 17:57:37 +0100 Subject: [PATCH 69/85] Add has_permanent_magnet method to Excitation class --- traceon/excitation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/traceon/excitation.py b/traceon/excitation.py index d6b04a9..a58c889 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -156,6 +156,10 @@ def add_current(self, **kwargs): else: raise ValueError('Symmetry should be one of RADIAL or THREE_D') + def has_permanent_magnet(self): + """Check whether the excitation contains a permanent magnet.""" + return any([t == ExcitationType.PERMANENT_MAGNET for t, _ in self.excitation_types.values()]) + 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()]) From ea80c5168bf3c9575a15276bd5e6090ea65d1deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 17:59:19 +0100 Subject: [PATCH 70/85] Fixes to excitation.py related to permanent magnet function --- traceon/excitation.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index a58c889..486e5f7 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -15,6 +15,7 @@ from enum import IntEnum import numpy as np +from scipy.constants import mu_0 from .backend import N_QUAD_2D from .logging import log_error @@ -212,7 +213,7 @@ def add_magnetizable(self, **kwargs): def add_permanent_magnet(self, **kwargs): """ - Assign a magnetization vector to a permanent magnet. + Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density, which has unit Tesla. Parameters ---------- @@ -220,11 +221,17 @@ def add_permanent_magnet(self, **kwargs): The keys of the dictionary are the geometry names, while the values are the magnetization vectors M. """ for name, vector in kwargs.items(): + vector = np.array(vector, dtype=np.float64) / mu_0 # Note that we convert from Tesla to A/m, since the rest of the code works with H fields (which has unit A/m) + if self.symmetry == E.Symmetry.RADIAL: - self._ensure_electrode_is_lines('magnetizable', name) - assert vector.shape == (3,) and vector[1] == 0.0, "In radial symmetry the magnetization vector must lie in the XZ plane." + self._ensure_electrode_is_lines('permanent magnet', name) + assert vector.shape == (3,) and vector[1] == 0.0 and vector[0] == 0.0, \ + "Please supply the magnetization vector in radial symmetry as the vector [0, 0, B], with B" +\ + " the residual flux density (unit Tesla). Note that a magnetization vector along r (for example [B, 0, 0]) " +\ + " would lead to a non-uniform magnetization in radial symmetry, and is currently not supported. " + elif self.symmetry == E.Symmetry.THREE_D: - self._ensure_electrode_is_triangles('magnetizable', name) + self._ensure_electrode_is_triangles('permanent magnet', name) assert vector.shape == (3,), "The magnetization vector must be a 3D vector." self.excitation_types[name] = (ExcitationType.PERMANENT_MAGNET, vector) From 9ed83d0de3c787caae96689966a51ba0c118f86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 17:59:43 +0100 Subject: [PATCH 71/85] Add method get_permanent_magnet_field to radial magnetostatic solver --- traceon/solver.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/traceon/solver.py b/traceon/solver.py index 644e88c..e513d82 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -308,6 +308,44 @@ def get_active_elements(self): def get_preexisting_field(self, point): return self.current_field.current_field_at_point(point) + def get_permanent_magnet_field(self) -> FieldBEM: + charges: list[np.ndarray] = [] + jacobians = [] + positions = [] + + mesh = self.excitation.mesh + + if not len(mesh.lines) or not self.excitation.has_permanent_magnet(): + return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_3d()) + + all_vertices = mesh.points[mesh.lines] + jac, pos = backend.fill_jacobian_buffer_radial(all_vertices) + normals = np.array([backend.higher_order_normal_radial(0.0, v) for v in all_vertices]) + + for name, v in self.excitation.excitation_types.items(): + if not v[0] == E.ExcitationType.PERMANENT_MAGNET or not name in mesh.physical_to_lines: + continue + + indices = mesh.physical_to_lines[name] + + if not len(indices): + continue + + # Magnetic charge is dot product of normal vector and magnetization vector + n = normals[indices] + assert n.shape == (len(n), 2) + vector = v[1] + dot_product = n[:, 0]*vector[0] + n[:, 1]*vector[2] # Normal currently has only (r,z) element + + charges.extend(dot_product) + jacobians.extend(jac[indices]) + positions.extend(pos[indices]) + + if not len(charges): + return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_3d()) + + return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges(np.array(charges), np.array(jacobians), np.array(positions))) + def get_current_field(self) -> FieldBEM: currents: list[np.ndarray] = [] jacobians = [] From 6ec4726a9fa494e0520cfc203114ad4897755a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 18:00:14 +0100 Subject: [PATCH 72/85] Add test_triangular_permanent_magnet to TestRadial --- tests/test_radial.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_radial.py b/tests/test_radial.py index 5074d7e..77833fa 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -239,6 +239,42 @@ def test_rectangular_coil(self): assert np.allclose(computed, correct, atol=0.0, rtol=1e-9) + def test_triangular_permanent_magnet(self): + triangle = G.Path.line([0.5, 0., -0.25], [0.5, 0., 0.25])\ + .extend_with_line([1., 0., 0.25]).close() + triangle.name = 'triangle' + + mesh = triangle.mesh(mesh_size_factor=15, higher_order=True) + + exc = E.Excitation(mesh, E.Symmetry.RADIAL) + exc.add_permanent_magnet(triangle=np.array([0., 0., 2.])) + + solver = S.MagnetostaticSolverRadial(exc) + field = solver.get_permanent_magnet_field() + + evaluation_points = np.array([ + [0.8, 0.], + [1.0, 0.], + [0.75, 0.2], + [0.75, 0.5]]) + + # Taken from a FEM package + comparison = np.array([ + [0.28152, 0.098792, -0.47191, -0.19345], + [-1.3549, 0.18957, -0.020779, -0.12875]]).T + + comparison = np.array([ + [-0.47191, -0.020779], + [-0.19345, -0.12875], + [0.28152, -1.3549], + [0.098792, 0.18957] + ]) + + for ev, comp in zip(evaluation_points, comparison): + Hr, _, Hz = field.magnetostatic_field_at_point([ev[0], 0., ev[1]]) + assert np.allclose([mu_0*Hr, mu_0*Hz], comp, rtol=1e-5, atol=0.004) + + class TestSimpleMagneticLens(unittest.TestCase): @classmethod From 698a706f375e410aec1fda594d89de782cf28c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 18:13:49 +0100 Subject: [PATCH 73/85] Fix empty_3d -> empty_2d in solver.py --- traceon/solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index e513d82..6e53da6 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -316,7 +316,7 @@ def get_permanent_magnet_field(self) -> FieldBEM: mesh = self.excitation.mesh if not len(mesh.lines) or not self.excitation.has_permanent_magnet(): - return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_3d()) + return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_2d()) all_vertices = mesh.points[mesh.lines] jac, pos = backend.fill_jacobian_buffer_radial(all_vertices) @@ -342,7 +342,7 @@ def get_permanent_magnet_field(self) -> FieldBEM: positions.extend(pos[indices]) if not len(charges): - return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_3d()) + return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges.empty_2d()) return FieldRadialBEM(magnetostatic_point_charges=EffectivePointCharges(np.array(charges), np.array(jacobians), np.array(positions))) From 120e7dca93b1f31359c076e9fb81a10b76730aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 18:13:58 +0100 Subject: [PATCH 74/85] Add permanent magnets to preexisting field in radial symmetry --- traceon/solver.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index 6e53da6..0cf8cce 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -299,15 +299,14 @@ def charges_to_field(self, charges): class MagnetostaticSolverRadial(SolverRadial): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - self.current_field = self.get_current_field() + self.preexisting_field = self.get_current_field() + self.get_permanent_magnet_field() def get_active_elements(self): return self.excitation.get_magnetostatic_active_elements() def get_preexisting_field(self, point): - return self.current_field.current_field_at_point(point) - + return self.preexisting_field.magnetostatic_field_at_point(point) + def get_permanent_magnet_field(self) -> FieldBEM: charges: list[np.ndarray] = [] jacobians = [] @@ -380,7 +379,9 @@ def get_current_field(self) -> FieldBEM: return FieldRadialBEM(current_point_charges=EffectivePointCharges(np.array(currents), np.array(jacobians), np.array(positions))) def charges_to_field(self, charges): - return FieldRadialBEM(magnetostatic_point_charges=charges, current_point_charges=self.current_field.current_point_charges) + return FieldRadialBEM( + magnetostatic_point_charges=self.preexisting_field.magnetostatic_point_charges + charges, + current_point_charges=self.preexisting_field.current_point_charges) def _excitation_to_higher_order(excitation): From 583c297b447361b669d12e4f8c4148c39d9870dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 19:10:12 +0100 Subject: [PATCH 75/85] Return pre-existing field from solve_direct function --- traceon/solver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index 0cf8cce..f938d84 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -445,8 +445,6 @@ def solve_direct(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] @@ -455,6 +453,11 @@ def solve_direct(excitation): return ElectrostaticSolverRadial(excitation).solve_matrix()[0] elif mag and not elec: return MagnetostaticSolverRadial(excitation).solve_matrix()[0] + else: + # Empty excitation? We still return the preexisting field.. + return ElectrostaticSolverRadial(excitation).charges_to_field(EffectivePointCharges.empty_2d()) + \ + MagnetostaticSolverRadial(excitation).charges_to_field(EffectivePointCharges.empty_2d()) + From 83e0f85689648ea2313acb2f8c2c31d19a2db109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 19:10:32 +0100 Subject: [PATCH 76/85] Add test_field_along_axis_of_cylindrical_permanent_magnet --- tests/test_radial.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_radial.py b/tests/test_radial.py index 77833fa..54202f8 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -239,6 +239,8 @@ def test_rectangular_coil(self): assert np.allclose(computed, correct, atol=0.0, rtol=1e-9) + +class TestRadialPermanentMagnet(unittest.TestCase): def test_triangular_permanent_magnet(self): triangle = G.Path.line([0.5, 0., -0.25], [0.5, 0., 0.25])\ .extend_with_line([1., 0., 0.25]).close() @@ -273,6 +275,33 @@ def test_triangular_permanent_magnet(self): for ev, comp in zip(evaluation_points, comparison): Hr, _, Hz = field.magnetostatic_field_at_point([ev[0], 0., ev[1]]) assert np.allclose([mu_0*Hr, mu_0*Hz], comp, rtol=1e-5, atol=0.004) + + def test_field_along_axis_of_cylindrical_permanent_magnet(self): + R = 1.5 # Radius of magnet + Br = 2.0 # Remanent flux of magnet + L = 3 # Length of magnet + + rect = G.Path.line([0., 0., 0.], [R, 0., 0.])\ + .extend_with_line([R, 0., L]).extend_with_line([0., 0., L]) + rect.name = 'magnet' + + mesh = rect.mesh(mesh_size_factor=5) + + e = E.Excitation(mesh, E.Symmetry.RADIAL) + e.add_permanent_magnet(magnet=[0., 0., Br]) + + field = S.solve_direct(e) + + for dz in [0.1, 1, 5, 10]: + # See https://e-magnetica.pl/doku.php/calculator/magnet_cylindrical + correct = Br / (2*mu_0) * ( (dz+L)/sqrt(R**2 + (dz+L)**2) - dz/sqrt(R**2 + dz**2)) + + Hz = field.magnetostatic_field_at_point([0., 0., L+dz])[2] + assert np.isclose(correct, Hz) + + + + class TestSimpleMagneticLens(unittest.TestCase): From b0688c00da4eb7992e2da6afcd04574fe6776ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 22:16:40 +0100 Subject: [PATCH 77/85] Add test_magnet_with_magnetizable_material --- tests/test_radial.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_radial.py b/tests/test_radial.py index 54202f8..4e17556 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -299,10 +299,44 @@ def test_field_along_axis_of_cylindrical_permanent_magnet(self): Hz = field.magnetostatic_field_at_point([0., 0., L+dz])[2] assert np.isclose(correct, Hz) + def test_magnet_with_magnetizable_material(self): + magnet = G.Path.circle_xz(3, -2, 1) + magnet.name = 'magnet' + magnetizable = G.Path.circle_xz(3, 2, 1) + magnetizable.name = 'circle' + mesh = (magnet + magnetizable).mesh(mesh_size_factor=40, higher_order=True) + + exc = E.Excitation(mesh, E.Symmetry.RADIAL) + exc.add_permanent_magnet(magnet=[0, 0, 2.]) + exc.add_magnetizable(circle=5) + + field = S.solve_direct(exc) + + points = np.array([ + [3, -4], + [3, -2], + [3, -0.5], + [3, 0.5], + [3, 2], + [3, 4]]) + + # From a FEM package + comparison = np.array([ + [-0.080149, 0.25363], + [-0.003367, -1.0145], + [0.085632, 0.45160], + [0.046463, 0.19612], + [0.012013, 0.023293], + [0.015441, 0.042159]]) + + for (c, (r, z)) in zip(comparison, points): + Hr, _, Hz = field.magnetostatic_field_at_point([r, 0., z]) + assert np.isclose(c[0], mu_0*Hr, rtol=1e-4, atol=0.0002) + assert np.isclose(c[1], mu_0*Hz, rtol=1e-4, atol=0.0002) + - class TestSimpleMagneticLens(unittest.TestCase): From c6b31f6a8bad884d38bb5f205327dcce29981357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 22:17:09 +0100 Subject: [PATCH 78/85] Add PERMANENT_MAGNET to Excitation.is_magnetostatic --- traceon/excitation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index 486e5f7..eef60e2 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -171,7 +171,7 @@ def is_electrostatic(self): 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()]) + return any([t in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.PERMANENT_MAGNET, ExcitationType.CURRENT] for t, _ in self.excitation_types.values()]) def add_magnetostatic_potential(self, **kwargs): """ From b74cab5e5c5e4e90601c2f5a61ec351d3bb4893c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Sun, 5 Jan 2025 22:17:54 +0100 Subject: [PATCH 79/85] Revert "Return pre-existing field from solve_direct function" This reverts commit 583c297b447361b669d12e4f8c4148c39d9870dd. --- traceon/solver.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/traceon/solver.py b/traceon/solver.py index f938d84..0cf8cce 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -445,6 +445,8 @@ def solve_direct(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] @@ -453,11 +455,6 @@ def solve_direct(excitation): return ElectrostaticSolverRadial(excitation).solve_matrix()[0] elif mag and not elec: return MagnetostaticSolverRadial(excitation).solve_matrix()[0] - else: - # Empty excitation? We still return the preexisting field.. - return ElectrostaticSolverRadial(excitation).charges_to_field(EffectivePointCharges.empty_2d()) + \ - MagnetostaticSolverRadial(excitation).charges_to_field(EffectivePointCharges.empty_2d()) - From 284908641237a9f0fdc4b4748933c0efd5735486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:05:24 +0100 Subject: [PATCH 80/85] Remove stray print statement --- traceon/excitation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index eef60e2..494f58d 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -293,7 +293,6 @@ def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True): """ if ensure_inward_normals: for electrode in args: - print('flipping normals', electrode) self.mesh.ensure_inward_normals(electrode) for name in args: From 550b0e799ff67f4bbe52bb16ec8b17a09fba9d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:06:36 +0100 Subject: [PATCH 81/85] Fix function solve_direct_superposition and add relevant test --- tests/test_radial.py | 57 +++++++++++++++++++++++++++++++++++++++++++ traceon/excitation.py | 45 +++++++++++++++++++--------------- traceon/field.py | 36 +++++++++++++++++---------- traceon/solver.py | 10 ++++---- 4 files changed, 110 insertions(+), 38 deletions(-) diff --git a/tests/test_radial.py b/tests/test_radial.py index 4e17556..413fc6f 100644 --- a/tests/test_radial.py +++ b/tests/test_radial.py @@ -239,6 +239,63 @@ def test_rectangular_coil(self): assert np.allclose(computed, correct, atol=0.0, rtol=1e-9) + def test_field_superposition(self): + boundary = G.Path.line([0., 0., 0.], [5., 0., 0.]).extend_with_line([5., 0., 5.]).extend_with_line([0., 0., 5.]) + boundary.name = 'boundary' + + rect1 = G.Path.rectangle_xz(1.0, 2.0, 1.0, 2.0) + rect1.name = 'rect1' + + rect2 = G.Path.rectangle_xz(1.0, 2.0, 3.0, 4.0) + rect2.name = 'rect2' + + rect3 = G.Surface.rectangle_xz(3.0, 4.0, 3.0, 4.0) + rect3.name = 'rect3' + + rect4 = G.Path.rectangle_xz(3.0, 4.0, 1.0, 2.0) + rect4.name = 'rect4' + + mesh = (boundary + rect1 + rect2 + rect4).mesh(mesh_size_factor=5) + rect3.mesh(mesh_size_factor=2) + + exc = E.Excitation(mesh, E.Symmetry.RADIAL) + exc.add_magnetostatic_boundary('boundary') + exc.add_voltage(rect1=10) + exc.add_voltage(rect2=0.0) + exc.add_current(rect3=2.5) + exc.add_dielectric(rect4=8) + + field = S.solve_direct(exc) + + # Excitation with half the values + exc_half = E.Excitation(mesh, E.Symmetry.RADIAL) + exc_half.add_magnetostatic_boundary('boundary') + exc_half.add_voltage(rect1=10 / 2.) + exc_half.add_voltage(rect2=1.0) + exc_half.add_current(rect3=2.5 / 2.) + exc_half.add_dielectric(rect4=8) + + superposition = S.solve_direct_superposition(exc_half) + + # The following expression is written in a weird way to test many corner cases + numpy_minus_two = np.array([-2.0])[0] # Numpy scalars sometimes give issues + superposed = -2.0* (-superposition['rect1']) - superposition['rect3']*numpy_minus_two + 0*superposition['rect2'] + + # Field and superposed should be EXACTLY the same! + for (eff1, eff2) in zip([field.electrostatic_point_charges, field.magnetostatic_point_charges, field.current_point_charges], + [superposed.electrostatic_point_charges, superposed.magnetostatic_point_charges, superposed.current_point_charges]): + assert np.allclose(eff1.jacobians, eff2.jacobians) + assert np.allclose(eff1.charges, eff2.charges) + assert np.allclose(eff1.positions, eff2.positions) + assert eff1.directions == eff2.directions or np.allclose(eff1.directions, eff2.directions) + + # Since the fields are the same they should return the same values at some arbitrary points + points = [ [0.5, 0.5, 0.5], [1.0, 0.0, 2.0], [2.0, 0.0, -2.0] ] + + for p in points: + assert np.allclose(field.electrostatic_field_at_point(p), superposed.electrostatic_field_at_point(p)) + assert np.allclose(field.magnetostatic_field_at_point(p), superposed.magnetostatic_field_at_point(p)) + assert np.allclose(field.current_field_at_point(p), superposed.current_field_at_point(p)) + class TestRadialPermanentMagnet(unittest.TestCase): def test_triangular_permanent_magnet(self): diff --git a/traceon/excitation.py b/traceon/excitation.py index 494f58d..404af0e 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -303,38 +303,43 @@ def add_magnetostatic_boundary(self, *args, ensure_inward_normals=True): self.add_magnetizable(**{a:0 for a in args}) + def _is_excitation_type_part_of_superposition(self, type_: ExcitationType) -> bool: + # When computing a superposition, should we return a field for the given excitation type with + # the given value? We should only return a field if the field is not trivially zero. + # For example, an excitation with only a boundary element will never produce a field. + # There are only a few cases that would produce a field: + return type_ in [ExcitationType.VOLTAGE_FIXED, ExcitationType.VOLTAGE_FUN, ExcitationType.CURRENT, ExcitationType.PERMANENT_MAGNET] + def _split_for_superposition(self): + types = self.excitation_types.items() + part_of_superposition = [(n, t.is_electrostatic()) for n, (t, v) in types if self._is_excitation_type_part_of_superposition(t)] - # 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 = [] + electrostatic_excitations = {} + magnetostatic_excitations = {} - for name in non_zero_fixed: + for (name, is_electrostatic) in part_of_superposition: 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: + for n, (t, v) in types: + if n == name or not self._is_excitation_type_part_of_superposition(t): new_types_dict[n] = (t, v) + elif t == ExcitationType.VOLTAGE_FUN: + new_types_dict[n] = (t, lambda _: 0.0) # Already gets its own field, don't include in this one + else: + new_types_dict[n] = (t, np.zeros_like(v) if isinstance(v, np.ndarray) else 0.0) # Already gets its own field, don't include in this one 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)} + if is_electrostatic: + electrostatic_excitations[name] = exc + else: + magnetostatic_excitations[name] = exc + assert len(electrostatic_excitations) + len(magnetostatic_excitations) == len(part_of_superposition) + return electrostatic_excitations, magnetostatic_excitations + def _get_active_elements(self, type_): assert type_ in ['electrostatic', 'magnetostatic'] diff --git a/traceon/field.py b/traceon/field.py index 02f185a..106fa97 100644 --- a/traceon/field.py +++ b/traceon/field.py @@ -32,6 +32,10 @@ __pdoc__['FieldRadialBEM.get_low_level_trace_function'] = False __pdoc__['FieldRadialAxial.get_low_level_trace_function'] = False +def _is_numeric(x): + if isinstance(x, int) or isinstance(x, float) or isinstance(x, np.generic): + return True + class EffectivePointCharges: def __init__(self, charges, jacobians, positions, directions=None): self.charges = np.array(charges, dtype=np.float64) @@ -78,8 +82,11 @@ def __add__(self, other): np.concatenate([self.jacobians, other.jacobians]), np.concatenate([self.positions, other.positions])) + def __radd__(self, other): + return self.__add__(other) + def __mul__(self, other): - if isinstance(other, int) or isinstance(other, float): + if _is_numeric(other): return EffectivePointCharges(other*self.charges, self.jacobians, self.positions) return NotImplemented @@ -87,6 +94,9 @@ def __mul__(self, other): def __neg__(self): return -1*self + def __sub__(self, other): + return self.__add__(-1*other) + def __rmul__(self, other): return self.__mul__(other) @@ -229,23 +239,23 @@ def __radd__(self, other): self.current_point_charges.__radd__(other.current_point_charges)) def __mul__(self, other): + if not _is_numeric(other): + return NotImplemented + 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)) + self.electrostatic_point_charges.__mul__(other), + self.magnetostatic_point_charges.__mul__(other), + self.current_point_charges.__mul__(other)) - def __neg__(self, other): + def __neg__(self): 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)) + self.electrostatic_point_charges.__neg__(), + self.magnetostatic_point_charges.__neg__(), + self.current_point_charges.__neg__()) 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)) - + return self.__mul__(other) + def area_of_elements(self, indices): """Compute the total area of the elements at the given indices. diff --git a/traceon/solver.py b/traceon/solver.py index be8f1d9..5d2f171 100644 --- a/traceon/solver.py +++ b/traceon/solver.py @@ -382,17 +382,17 @@ def solve_direct_superposition(excitation): excitation = _excitation_to_higher_order(excitation) # Speedup: invert matrix only once, when using superposition - excitations = excitation._split_for_superposition() + electrostatic_excitations, magnetostatic_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]) + elec_names = electrostatic_excitations.keys() + right_hand_sides = np.array([ElectrostaticSolverRadial(electrostatic_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]) + mag_names = magnetostatic_excitations.keys() + right_hand_sides = np.array([MagnetostaticSolverRadial(magnetostatic_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)} From 359b8a09998d4b19696fee834a039ef67a899306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:27:49 +0100 Subject: [PATCH 82/85] Improve documentation of add_permanent_magnet --- traceon/excitation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traceon/excitation.py b/traceon/excitation.py index 404af0e..6dc5640 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -213,12 +213,12 @@ def add_magnetizable(self, **kwargs): def add_permanent_magnet(self, **kwargs): """ - Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density, which has unit Tesla. + Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density vectors, with unit Tesla. Parameters ---------- **kwargs : dict - The keys of the dictionary are the geometry names, while the values are the magnetization vectors M. + The keys of the dictionary are the geometry names, while the values are the residual flux density vectors (Numpy shape (3,)). """ for name, vector in kwargs.items(): vector = np.array(vector, dtype=np.float64) / mu_0 # Note that we convert from Tesla to A/m, since the rest of the code works with H fields (which has unit A/m) From ad4ffc5234bdb938b5aeac63ffaf657dac382a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:28:12 +0100 Subject: [PATCH 83/85] Bump version number to v0.9.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8b43ec4..f83d07f 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name='traceon', - version='0.9.0rc1', + version='0.9.0', description='Solver and tracer for electrostatic problems', url='https://github.com/leon-vv/Traceon', author='Léon van Velzen', From 82fa83c2a4bc924f951223e6357c6ff0f3292088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:29:53 +0100 Subject: [PATCH 84/85] Add permanent magnet to module documentation of excitation.py --- traceon/excitation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/traceon/excitation.py b/traceon/excitation.py index 6dc5640..4024474 100644 --- a/traceon/excitation.py +++ b/traceon/excitation.py @@ -9,6 +9,7 @@ - Current lines (3D geometry) - Magnetostatic scalar potential - Magnetizable material, with arbitrary magnetic permeability +- Permanent magnet, with uniform magnetization Once the excitation is specified, it can be passed to `traceon.solver.solve_direct` to compute the resulting field. """ From c21ddd4016c8119a3fb84d8004f1122271fcc4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20Velzen?= Date: Mon, 6 Jan 2025 21:39:05 +0100 Subject: [PATCH 85/85] Update v0.9.0 documentation --- .../images/einzel lens electron traces.png | Bin .../einzel lens potential along axis.png | Bin .../images/einzel lens.png | Bin .../images/einzel_lens_radial.png | Bin .../traceon/einzel-lens.html | 0 .../traceon/excitation.html | 169 +++++++++++++++--- .../{v0.9.0rc1 => v0.9.0}/traceon/field.html | 24 +-- .../{v0.9.0rc1 => v0.9.0}/traceon/focus.html | 0 .../traceon/geometry.html | 0 .../{v0.9.0rc1 => v0.9.0}/traceon/index.html | 0 .../traceon/logging.html | 0 .../{v0.9.0rc1 => v0.9.0}/traceon/mesher.html | 0 .../traceon/plotting.html | 0 .../{v0.9.0rc1 => v0.9.0}/traceon/solver.html | 10 +- .../traceon/tracing.html | 0 .../traceon_pro/field.html | 0 .../traceon_pro/index.html | 0 .../traceon_pro/solver.html | 0 .../traceon_pro/traceon_pro.html | 0 19 files changed, 160 insertions(+), 43 deletions(-) rename docs/docs/{v0.9.0rc1 => v0.9.0}/images/einzel lens electron traces.png (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/images/einzel lens potential along axis.png (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/images/einzel lens.png (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/images/einzel_lens_radial.png (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/einzel-lens.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/excitation.html (87%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/field.html (99%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/focus.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/geometry.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/index.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/logging.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/mesher.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/plotting.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/solver.html (97%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon/tracing.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon_pro/field.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon_pro/index.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon_pro/solver.html (100%) rename docs/docs/{v0.9.0rc1 => v0.9.0}/traceon_pro/traceon_pro.html (100%) diff --git a/docs/docs/v0.9.0rc1/images/einzel lens electron traces.png b/docs/docs/v0.9.0/images/einzel lens electron traces.png similarity index 100% rename from docs/docs/v0.9.0rc1/images/einzel lens electron traces.png rename to docs/docs/v0.9.0/images/einzel lens electron traces.png diff --git a/docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png b/docs/docs/v0.9.0/images/einzel lens potential along axis.png similarity index 100% rename from docs/docs/v0.9.0rc1/images/einzel lens potential along axis.png rename to docs/docs/v0.9.0/images/einzel lens potential along axis.png diff --git a/docs/docs/v0.9.0rc1/images/einzel lens.png b/docs/docs/v0.9.0/images/einzel lens.png similarity index 100% rename from docs/docs/v0.9.0rc1/images/einzel lens.png rename to docs/docs/v0.9.0/images/einzel lens.png diff --git a/docs/docs/v0.9.0rc1/images/einzel_lens_radial.png b/docs/docs/v0.9.0/images/einzel_lens_radial.png similarity index 100% rename from docs/docs/v0.9.0rc1/images/einzel_lens_radial.png rename to docs/docs/v0.9.0/images/einzel_lens_radial.png diff --git a/docs/docs/v0.9.0rc1/traceon/einzel-lens.html b/docs/docs/v0.9.0/traceon/einzel-lens.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/einzel-lens.html rename to docs/docs/v0.9.0/traceon/einzel-lens.html diff --git a/docs/docs/v0.9.0rc1/traceon/excitation.html b/docs/docs/v0.9.0/traceon/excitation.html similarity index 87% rename from docs/docs/v0.9.0rc1/traceon/excitation.html rename to docs/docs/v0.9.0/traceon/excitation.html index cb23883..9b86192 100644 --- a/docs/docs/v0.9.0rc1/traceon/excitation.html +++ b/docs/docs/v0.9.0/traceon/excitation.html @@ -67,6 +67,7 @@

    Module traceon.excitation

  • Current lines (3D geometry)
  • Magnetostatic scalar potential
  • Magnetizable material, with arbitrary magnetic permeability
  • +
  • Permanent magnet, with uniform magnetization
  • Once the excitation is specified, it can be passed to solve_direct() to compute the resulting field.

    @@ -173,6 +174,10 @@

    Classes

    else: raise ValueError('Symmetry should be one of RADIAL or THREE_D') + def has_permanent_magnet(self): + """Check whether the excitation contains a permanent magnet.""" + return any([t == ExcitationType.PERMANENT_MAGNET for t, _ in self.excitation_types.values()]) + 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()]) @@ -183,7 +188,7 @@

    Classes

    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()]) + return any([t in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.PERMANENT_MAGNET, ExcitationType.CURRENT] for t, _ in self.excitation_types.values()]) def add_magnetostatic_potential(self, **kwargs): """ @@ -222,6 +227,31 @@

    Classes

    self._ensure_electrode_is_triangles('magnetizable', name) self.excitation_types[name] = (ExcitationType.MAGNETIZABLE, permeability) + + def add_permanent_magnet(self, **kwargs): + """ + Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density vectors, with unit Tesla. + + Parameters + ---------- + **kwargs : dict + The keys of the dictionary are the geometry names, while the values are the residual flux density vectors (Numpy shape (3,)). + """ + for name, vector in kwargs.items(): + vector = np.array(vector, dtype=np.float64) / mu_0 # Note that we convert from Tesla to A/m, since the rest of the code works with H fields (which has unit A/m) + + if self.symmetry == E.Symmetry.RADIAL: + self._ensure_electrode_is_lines('permanent magnet', name) + assert vector.shape == (3,) and vector[1] == 0.0 and vector[0] == 0.0, \ + "Please supply the magnetization vector in radial symmetry as the vector [0, 0, B], with B" +\ + " the residual flux density (unit Tesla). Note that a magnetization vector along r (for example [B, 0, 0]) " +\ + " would lead to a non-uniform magnetization in radial symmetry, and is currently not supported. " + + elif self.symmetry == E.Symmetry.THREE_D: + self._ensure_electrode_is_triangles('permanent magnet', name) + assert vector.shape == (3,), "The magnetization vector must be a 3D vector." + + self.excitation_types[name] = (ExcitationType.PERMANENT_MAGNET, vector) def add_dielectric(self, **kwargs): """ @@ -280,7 +310,6 @@

    Classes

    """ if ensure_inward_normals: for electrode in args: - print('flipping normals', electrode) self.mesh.ensure_inward_normals(electrode) for name in args: @@ -291,38 +320,43 @@

    Classes

    self.add_magnetizable(**{a:0 for a in args}) + def _is_excitation_type_part_of_superposition(self, type_: ExcitationType) -> bool: + # When computing a superposition, should we return a field for the given excitation type with + # the given value? We should only return a field if the field is not trivially zero. + # For example, an excitation with only a boundary element will never produce a field. + # There are only a few cases that would produce a field: + return type_ in [ExcitationType.VOLTAGE_FIXED, ExcitationType.VOLTAGE_FUN, ExcitationType.CURRENT, ExcitationType.PERMANENT_MAGNET] + def _split_for_superposition(self): + types = self.excitation_types.items() + part_of_superposition = [(n, t.is_electrostatic()) for n, (t, v) in types if self._is_excitation_type_part_of_superposition(t)] - # 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 = [] + electrostatic_excitations = {} + magnetostatic_excitations = {} - for name in non_zero_fixed: + for (name, is_electrostatic) in part_of_superposition: 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: + for n, (t, v) in types: + if n == name or not self._is_excitation_type_part_of_superposition(t): new_types_dict[n] = (t, v) + elif t == ExcitationType.VOLTAGE_FUN: + new_types_dict[n] = (t, lambda _: 0.0) # Already gets its own field, don't include in this one + else: + new_types_dict[n] = (t, np.zeros_like(v) if isinstance(v, np.ndarray) else 0.0) # Already gets its own field, don't include in this one 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)} + if is_electrostatic: + electrostatic_excitations[name] = exc + else: + magnetostatic_excitations[name] = exc + assert len(electrostatic_excitations) + len(magnetostatic_excitations) == len(part_of_superposition) + return electrostatic_excitations, magnetostatic_excitations + def _get_active_elements(self, type_): assert type_ in ['electrostatic', 'magnetostatic'] @@ -597,7 +631,6 @@

    Parameters

    """ if ensure_inward_normals: for electrode in args: - print('flipping normals', electrode) self.mesh.ensure_inward_normals(electrode) for name in args: @@ -662,6 +695,53 @@

    Parameters

    +
    + + def add_permanent_magnet(self, **kwargs) +
    +
    + + + +
    + + Expand source code + +
    def add_permanent_magnet(self, **kwargs):
    +    """
    +    Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density vectors, with unit Tesla.
    +    
    +    Parameters
    +    ----------
    +    **kwargs : dict
    +        The keys of the dictionary are the geometry names, while the values are the residual flux density vectors (Numpy shape (3,)).
    +    """
    +    for name, vector in kwargs.items():
    +        vector = np.array(vector, dtype=np.float64) / mu_0 # Note that we convert from Tesla to A/m, since the rest of the code works with H fields (which has unit A/m)
    +        
    +        if self.symmetry == E.Symmetry.RADIAL:
    +            self._ensure_electrode_is_lines('permanent magnet', name)
    +            assert vector.shape == (3,) and vector[1] == 0.0 and vector[0] == 0.0, \
    +                "Please supply the magnetization vector in radial symmetry as the vector [0, 0, B], with B" +\
    +                " the residual flux density (unit Tesla). Note that a magnetization vector along r (for example [B, 0, 0]) " +\
    +                " would lead to a non-uniform magnetization in radial symmetry, and is currently not supported. "
    +
    +        elif self.symmetry == E.Symmetry.THREE_D:
    +            self._ensure_electrode_is_triangles('permanent magnet', name)
    +            assert vector.shape == (3,), "The magnetization vector must be a 3D vector."
    +
    +        self.excitation_types[name] = (ExcitationType.PERMANENT_MAGNET, vector)
    +
    + +

    Assign a magnetization vector to a permanent magnet. The magnetization is supplied as the residual flux density vectors, with unit Tesla.

    +

    Parameters

    +
    +
    **kwargs : dict
    +
    The keys of the dictionary are the geometry names, while the values are the residual flux density vectors (Numpy shape (3,)).
    +
    +
    + +
    def add_voltage(self, **kwargs) @@ -806,6 +886,27 @@

    Returns

    +
    + + def has_permanent_magnet(self) +
    +
    + + + +
    + + Expand source code + +
    def has_permanent_magnet(self):
    +    """Check whether the excitation contains a permanent magnet."""
    +    return any([t == ExcitationType.PERMANENT_MAGNET for t, _ in self.excitation_types.values()])
    +
    + +

    Check whether the excitation contains a permanent magnet.

    +
    + +
    def is_electrostatic(self) @@ -841,7 +942,7 @@

    Returns

    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()])
    + return any([t in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.PERMANENT_MAGNET, ExcitationType.CURRENT] for t, _ in self.excitation_types.values()])

    Check whether the excitation contains magnetostatic fields.

    @@ -875,6 +976,7 @@

    Returns

    CURRENT = 4 MAGNETOSTATIC_POT = 5 MAGNETIZABLE = 6 + PERMANENT_MAGNET = 7 def is_electrostatic(self): return self in [ExcitationType.VOLTAGE_FIXED, @@ -884,7 +986,8 @@

    Returns

    def is_magnetostatic(self): return self in [ExcitationType.MAGNETOSTATIC_POT, ExcitationType.MAGNETIZABLE, - ExcitationType.CURRENT] + ExcitationType.CURRENT, + ExcitationType.PERMANENT_MAGNET] def __str__(self): if self == ExcitationType.VOLTAGE_FIXED: @@ -899,6 +1002,8 @@

    Returns

    return 'magnetostatic potential' elif self == ExcitationType.MAGNETIZABLE: return 'magnetizable' + elif self == ExcitationType.PERMANENT_MAGNET: + return 'permanent magnet' raise RuntimeError('ExcitationType not understood in __str__ method')
    @@ -945,6 +1050,14 @@

    Class variables

    +
    + + +
    var PERMANENT_MAGNET
    +
    + + +
    @@ -1004,7 +1117,8 @@

    Methods

    def is_magnetostatic(self):
         return self in [ExcitationType.MAGNETOSTATIC_POT,
                         ExcitationType.MAGNETIZABLE,
    -                    ExcitationType.CURRENT]
    + ExcitationType.CURRENT, + ExcitationType.PERMANENT_MAGNET]
    @@ -1184,10 +1298,12 @@

    add_magnetizable
  • add_magnetostatic_boundary
  • add_magnetostatic_potential
  • +
  • add_permanent_magnet
  • add_voltage
  • get_electrostatic_active_elements
  • get_magnetostatic_active_elements
  • has_current
  • +
  • has_permanent_magnet
  • is_electrostatic
  • is_magnetostatic
  • @@ -1203,6 +1319,7 @@

    DIELECTRIC
  • MAGNETIZABLE
  • MAGNETOSTATIC_POT
  • +
  • PERMANENT_MAGNET
  • VOLTAGE_FIXED
  • VOLTAGE_FUN
  • is_electrostatic
  • diff --git a/docs/docs/v0.9.0rc1/traceon/field.html b/docs/docs/v0.9.0/traceon/field.html similarity index 99% rename from docs/docs/v0.9.0rc1/traceon/field.html rename to docs/docs/v0.9.0/traceon/field.html index a9dd936..8256e51 100644 --- a/docs/docs/v0.9.0rc1/traceon/field.html +++ b/docs/docs/v0.9.0/traceon/field.html @@ -613,23 +613,23 @@

    Inherited members

    self.current_point_charges.__radd__(other.current_point_charges)) def __mul__(self, other): + if not _is_numeric(other): + return NotImplemented + 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)) + self.electrostatic_point_charges.__mul__(other), + self.magnetostatic_point_charges.__mul__(other), + self.current_point_charges.__mul__(other)) - def __neg__(self, other): + def __neg__(self): 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)) + self.electrostatic_point_charges.__neg__(), + self.magnetostatic_point_charges.__neg__(), + self.current_point_charges.__neg__()) 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)) - + return self.__mul__(other) + def area_of_elements(self, indices): """Compute the total area of the elements at the given indices. diff --git a/docs/docs/v0.9.0rc1/traceon/focus.html b/docs/docs/v0.9.0/traceon/focus.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/focus.html rename to docs/docs/v0.9.0/traceon/focus.html diff --git a/docs/docs/v0.9.0rc1/traceon/geometry.html b/docs/docs/v0.9.0/traceon/geometry.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/geometry.html rename to docs/docs/v0.9.0/traceon/geometry.html diff --git a/docs/docs/v0.9.0rc1/traceon/index.html b/docs/docs/v0.9.0/traceon/index.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/index.html rename to docs/docs/v0.9.0/traceon/index.html diff --git a/docs/docs/v0.9.0rc1/traceon/logging.html b/docs/docs/v0.9.0/traceon/logging.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/logging.html rename to docs/docs/v0.9.0/traceon/logging.html diff --git a/docs/docs/v0.9.0rc1/traceon/mesher.html b/docs/docs/v0.9.0/traceon/mesher.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/mesher.html rename to docs/docs/v0.9.0/traceon/mesher.html diff --git a/docs/docs/v0.9.0rc1/traceon/plotting.html b/docs/docs/v0.9.0/traceon/plotting.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/plotting.html rename to docs/docs/v0.9.0/traceon/plotting.html diff --git a/docs/docs/v0.9.0rc1/traceon/solver.html b/docs/docs/v0.9.0/traceon/solver.html similarity index 97% rename from docs/docs/v0.9.0rc1/traceon/solver.html rename to docs/docs/v0.9.0/traceon/solver.html index fa36960..b6b2251 100644 --- a/docs/docs/v0.9.0rc1/traceon/solver.html +++ b/docs/docs/v0.9.0/traceon/solver.html @@ -164,17 +164,17 @@

    Returns

    excitation = _excitation_to_higher_order(excitation) # Speedup: invert matrix only once, when using superposition - excitations = excitation._split_for_superposition() + electrostatic_excitations, magnetostatic_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]) + elec_names = electrostatic_excitations.keys() + right_hand_sides = np.array([ElectrostaticSolverRadial(electrostatic_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]) + mag_names = magnetostatic_excitations.keys() + right_hand_sides = np.array([MagnetostaticSolverRadial(magnetostatic_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)} diff --git a/docs/docs/v0.9.0rc1/traceon/tracing.html b/docs/docs/v0.9.0/traceon/tracing.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon/tracing.html rename to docs/docs/v0.9.0/traceon/tracing.html diff --git a/docs/docs/v0.9.0rc1/traceon_pro/field.html b/docs/docs/v0.9.0/traceon_pro/field.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon_pro/field.html rename to docs/docs/v0.9.0/traceon_pro/field.html diff --git a/docs/docs/v0.9.0rc1/traceon_pro/index.html b/docs/docs/v0.9.0/traceon_pro/index.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon_pro/index.html rename to docs/docs/v0.9.0/traceon_pro/index.html diff --git a/docs/docs/v0.9.0rc1/traceon_pro/solver.html b/docs/docs/v0.9.0/traceon_pro/solver.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon_pro/solver.html rename to docs/docs/v0.9.0/traceon_pro/solver.html diff --git a/docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html b/docs/docs/v0.9.0/traceon_pro/traceon_pro.html similarity index 100% rename from docs/docs/v0.9.0rc1/traceon_pro/traceon_pro.html rename to docs/docs/v0.9.0/traceon_pro/traceon_pro.html