From 76ea747bdc3602987b7e34edf09de9f642c02cae Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 00:46:45 +0100 Subject: [PATCH 001/240] magnetization-polarization step 1 --- __temp3.txt | 5 ++ magpylib/_src/fields/field_BH_cuboid.py | 49 +++++++-------- magpylib/_src/fields/field_wrap_BH.py | 59 +++++++++++-------- .../_src/obj_classes/class_BaseDisplayRepr.py | 9 +-- .../_src/obj_classes/class_BaseExcitations.py | 55 ++++++++++++++++- .../_src/obj_classes/class_magnet_Cuboid.py | 58 ++++++++---------- 6 files changed, 145 insertions(+), 90 deletions(-) create mode 100644 __temp3.txt diff --git a/__temp3.txt b/__temp3.txt new file mode 100644 index 000000000..a6d0ace36 --- /dev/null +++ b/__temp3.txt @@ -0,0 +1,5 @@ +field_BH_cuboid +class_magnet_Cuboid +class_BaseDisplayRepr +class_BaseExcitations -> fix all docstrings + diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 38ee8a18d..4df0bb504 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -8,7 +8,7 @@ def magnet_cuboid_field( - field: str, observers: np.ndarray, magnetization: np.ndarray, dimension: np.ndarray + field: str, observers: np.ndarray, polarization: np.ndarray, dimension: np.ndarray ) -> np.ndarray: """Magnetic field of a homogeneously magnetized cuboid. @@ -18,22 +18,23 @@ def magnet_cuboid_field( Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in arbitrary length + units, e.g. meters. - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + polarization: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. dimension: ndarray, shape (n,3) - Cuboid side lengths in units of mm. + Cuboid side lengths in arbitrary length units, e.g. meters. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -41,14 +42,14 @@ def magnet_cuboid_field( >>> import numpy as np >>> import magpylib as magpy - >>> mag = np.array([(222,333,555), (33,44,55), (0,0,100)]) - >>> dim = np.array([(1,1,1), (2,3,4), (1,2,3)]) - >>> obs = np.array([(1,2,3), (2,3,4), (0,0,0)]) - >>> B = magpy.core.magnet_cuboid_field('B', obs, mag, dim) + >>> pol = np.array([(0,0,1), (1,0,0), (0,0,1)]) + >>> dim = np.array([(2,2,2), (3,3,3), (4,4,4)]) + >>> obs = np.array([(1,2,0), (2,3,4), (0,0,0)]) + >>> B = magpy.core.magnet_cuboid_field('B', obs, pol, dim) >>> print(B) - [[ 0.49343022 1.15608356 1.65109312] - [ 0.82221622 1.18511282 1.46945423] - [ 0. 0. 88.77487579]] + [[ 0. 0. -0.05227894] + [-0.00820941 0.00849123 0.011429 ] + [ 0. 0. 0.66666667]] Notes ----- @@ -59,7 +60,7 @@ def magnet_cuboid_field( Engel-Herbert: Journal of Applied Physics 97(7):074504 - 074504-4 (2005) - Camacho: Revista Mexicana de Fisica E 59 (2013) 8–17 + Camacho: Revista Mexicana de Fisica E 59 (2013) 8-17 Avoiding indeterminate forms: @@ -78,7 +79,7 @@ def magnet_cuboid_field( bh = check_field_input(field, "magnet_cuboid_field()") - magx, magy, magz = magnetization.T + magx, magy, magz = polarization.T a, b, c = np.abs(dimension.T) / 2 x, y, z = observers.T @@ -120,7 +121,7 @@ def magnet_cuboid_field( mask_gen = ~mask1 & mask2 & ~mask3 if np.any(mask_gen): - magx, magy, magz = magnetization[mask_gen].T + magx, magy, magz = polarization[mask_gen].T a, b, c = dimension[mask_gen].T / 2 x, y, z = np.copy(observers[mask_gen]).T @@ -215,17 +216,17 @@ def magnet_cuboid_field( - np.arctan2((xpa * ypb), (zpc * ppp)) ) - # contributions from x-magnetization + # contributions from x-polarization bx_magx = ( magx * ff1x * qsigns[:, 0, 0] ) # the 'missing' third sign is hidden in ff1x by_magx = magx * ff2z * qsigns[:, 0, 1] bz_magx = magx * ff2y * qsigns[:, 0, 2] - # contributions from y-magnetization + # contributions from y-polarization bx_magy = magy * ff2z * qsigns[:, 1, 0] by_magy = magy * ff1y * qsigns[:, 1, 1] bz_magy = -magy * ff2x * qsigns[:, 1, 2] - # contributions from z-magnetization + # contributions from z-polarization bx_magz = magz * ff2y * qsigns[:, 2, 0] by_magz = -magz * ff2x * qsigns[:, 2, 1] bz_magz = magz * ff1z * qsigns[:, 2, 2] @@ -247,8 +248,8 @@ def magnet_cuboid_field( if bh: return B - # if inside magnet subtract magnetization vector + # if inside magnet subtract polarization vector mask_inside = mx2 & my2 & mz2 - B[mask_inside] -= magnetization[mask_inside] - H = B * 10 / 4 / np.pi # mT -> kA/m + B[mask_inside] -= polarization[mask_inside] + H = B / (4 * np.pi * 1e-7) # T -> A/m return H diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 5cadd1efe..04265438e 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -166,7 +166,7 @@ def getBH_level2( squeeze : bool, default=True: If True output is squeezed (axes of length 1 are eliminated) pixel_agg : str - A compatible numpy aggregator string (e.g. `'min', 'max', 'mean'`) + A compatible numpy aggregator string (e.g. `'min'`, `'max'`, `'mean'`) which applies on pixel output values. field : {'B', 'H'} 'B' computes B field, 'H' computes H-field @@ -557,7 +557,7 @@ def getB( output="ndarray", **kwargs, ): - """Compute B-field in units of mT for given sources and observers. + """Compute B-field in units of T for given sources and observers. Field implementations can be directly accessed (avoiding the object oriented Magpylib interface) by providing a string input `sources=source_type`, array_like @@ -568,10 +568,10 @@ def getB( ---------- sources: source and collection objects or 1D list thereof Sources that generate the magnetic field. Can be a single source (or collection) - or a 1D list of l source and/or collection objects. + or a 1D list of l sources and/or collection objects. - Direct interface: input must be one of (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`, - `'Sphere'`, `'Dipole'`, `'Circle'` or `'Polyline'`). + Functional interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, + `Sphere`, `Dipole`, `Circle` or `Polyline`). observers: array_like or (list of) `Sensor` objects Can be array_like positions of shape (n1, n2, ..., 3) where the field @@ -594,9 +594,9 @@ def getB( which is applied to observer output values, e.g. mean of all sensor pixel outputs. With this option, observers input with different (pixel) shapes is allowed. - output: str, default='ndarray' - Output type, which must be one of `('ndarray', 'dataframe')`. By default a - `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + output: str, default=`'ndarray'` + Output type, which must be one of (`'ndarray'`, `'dataframe'`). By default a + `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). See Also @@ -610,36 +610,43 @@ def getB( Object orientation(s) in the global coordinates. `None` corresponds to a unit-rotation. + polarization: array_like, shape (3,) or (n,3) + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`, + `Tetrahedron`, `Triangle`, `TriangularMesh`)! + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + magnetization: array_like, shape (3,) or (n,3) - Only source_type in (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`, `'Sphere'`)! - Magnetization vector(s) (mu0*M, remanence field) in units of kA/m given in - the local object coordinates (rotates with object). + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`, + `Tetrahedron`, `Triangle`, `TriangularMesh`)! + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). moment: array_like, shape (3) or (n,3), unit mT*mm^3 - Only source_type == `'Dipole'`! + Only source_type == `Dipole`! Magnetic dipole moment(s) in units of mT*mm^3 given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. current: array_like, shape (n,) - Only source_type == `'Circle'` or `'Polyline'`! + Only source_type == `Circle` or `Polyline`! Electrical current in units of A. dimension: array_like, shape (x,) or (n,x) - Only source_type in (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`)! + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! Magnet dimension input in units of mm and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) - Only source_type == `'Sphere'` or `'Circle'`! + Only source_type == `Sphere` or `Circle`! Diameter of source in units of mm. segment_start: array_like, shape (n,3) - Only source_type == `'Polyline'`! + Only source_type == `Polyline`! Start positions of line current segments in units of mm. segment_end: array_like, shape (n,3) - Only source_type == `'Polyline'`! + Only source_type == `Polyline`! End positions of line current segments in units of mm. Returns @@ -741,8 +748,8 @@ def getH( Sources that generate the magnetic field. Can be a single source (or collection) or a 1D list of l source and/or collection objects. - Direct interface: input must be one of (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`, - `'Sphere'`, `'Dipole'`, `'Circle'` or `'Polyline'`). + Direct interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, + `Sphere`, `Dipole`, `Circle` or `Polyline`). observers: array_like or (list of) `Sensor` objects Can be array_like positions of shape (n1, n2, ..., 3) where the field @@ -782,35 +789,35 @@ def getH( a unit-rotation. magnetization: array_like, shape (3,) or (n,3) - Only source_type in (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`, `'Sphere'`)! + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`)! Magnetization vector(s) (mu0*M, remanence field) in units of kA/m given in the local object coordinates (rotates with object). moment: array_like, shape (3) or (n,3), unit mT*mm^3 - Only source_type == `'Dipole'`! + Only source_type == `Dipole`! Magnetic dipole moment(s) in units of mT*mm^3 given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. current: array_like, shape (n,) - Only source_type == `'Circle'` or `'Polyline'`! + Only source_type == `Circle` or `Polyline`! Electrical current in units of A. dimension: array_like, shape (x,) or (n,x) - Only source_type in (`'Cuboid'`, `'Cylinder'`, `'CylinderSegment'`)! + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! Magnet dimension input in units of mm and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) - Only source_type == `'Sphere'` or `'Circle'`! + Only source_type == `Sphere` or `Circle`! Diameter of source in units of mm. segment_start: array_like, shape (n,3) - Only source_type == `'Polyline'`! + Only source_type == `Polyline`! Start positions of line current segments in units of mm. segment_end: array_like, shape (n,3) - Only source_type == `'Polyline'`! + Only source_type == `Polyline`! End positions of line current segments in units of mm. Returns diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index 726651828..602a9c6aa 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -8,12 +8,13 @@ UNITS = { "parent": None, - "position": "mm", + "position": "arbitrary", "orientation": "degrees", - "dimension": "mm", - "diameter": "mm", + "dimension": "arbitrary", + "diameter": "arbitrary", "current": "A", - "magnetization": "mT", + "magnetization": "A/m", + "polarization": "T", } diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 8d94b74ba..4dfbbd92d 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -1,5 +1,10 @@ """BaseHomMag class code""" # pylint: disable=cyclic-import +import warnings + +import numpy as np + +from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_scalar from magpylib._src.input_checks import check_format_input_vector @@ -187,13 +192,24 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): class BaseMagnet(BaseSource): - """provides the magnetization attribute for homogeneously magnetized magnets""" + """provides the magnetization and polarization attributes for magnet classes""" _style_class = MagnetStyle - def __init__(self, position, orientation, magnetization, style, **kwargs): + def __init__( + self, position, orientation, magnetization, polarization, style, **kwargs + ): super().__init__(position, orientation, style=style, **kwargs) - self.magnetization = magnetization + + if magnetization: + self.magnetization = magnetization + if polarization: + raise ValueError( + "The attributes magnetization and polarization are dependent. " + "Only one can be provided at magnet initialization." + ) + if polarization: + self.polarization = polarization @property def magnetization(self): @@ -211,6 +227,27 @@ def magnetization(self, mag): sig_type="array_like (list, tuple, ndarray) with shape (3,)", allow_None=True, ) + self._polarization = self._magnetization * (4 * np.pi * 1e-7) + if np.linalg.norm(self._magnetization) < 2000: + _deprecation_warn() + + @property + def polarization(self): + """Object polarization attribute getter and setter.""" + return self._polarization + + @polarization.setter + def polarization(self, mag): + """Set polarization vector, array_like, shape (3,), unit mT.""" + self._polarization = check_format_input_vector( + mag, + dims=(1,), + shape_m1=3, + sig_name="polarization", + sig_type="array_like (list, tuple, ndarray) with shape (3,)", + allow_None=True, + ) + self._magnetization = self._polarization / (4 * np.pi * 1e-7) class BaseCurrent(BaseSource): @@ -237,3 +274,15 @@ def current(self, current): sig_type="`None` or a number (int, float)", allow_None=True, ) + + +def _deprecation_warn(): + warnings.warn( + ( + "You have entered a very low magnetization." + "In Magpylib v5 magnetization is given in units of A/m, " + "while polarization is given in units of T." + ), + MagpylibDeprecationWarning, + stacklevel=2, + ) diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 45c610c26..735e9ff2d 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -15,22 +15,23 @@ class Cuboid(BaseMagnet): to the global coordinate basis vectors and the geometric center of the Cuboid is located in the origin. - Units can be chosen freely. B-field output unit is the same as magnetization - input unit. Computation is independend of Length-unit. See online documentation - for fore information - Parameters ---------- + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + magnetization: array_like, shape (3,), default=`None` - Magnetization (polarization) vector (mu0*M, remanence field) in arbitrary - units given in the local object coordinates (rotates with object). + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). dimension: array_like, shape (3,), default=`None` - Length of the cuboid sides [a,b,c] in arbitrary units. + Length of the cuboid sides [a,b,c] in arbitrary units, e.g. in meters. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in arbitrary units. For m>1, the - `position` and `orientation` attributes together represent an object path. + Object position(s) in the global coordinates in arbitrary units, e.g. in + meters. For m>1, the `position` and `orientation` attributes together + represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -50,46 +51,35 @@ class Cuboid(BaseMagnet): Examples -------- - `Cuboid` magnets are magnetic field sources. Below we compute the H-field in kA/m of a - cubical magnet with magnetization (100,200,300) in units of mT and 1 mm sides + `Cuboid` magnets are magnetic field sources. Below we compute the H-field in A/m of a + cubical magnet with magnetic polarization of (.5,.6,.7) in units of T and 1 mm sides at the observer position (1,1,1) given in units of mm: >>> import magpylib as magpy - >>> src = magpy.magnet.Cuboid(magnetization=(100,200,300), dimension=(1,1,1)) + >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(1,1,1)) >>> H = src.getH((1,1,1)) >>> print(H) - [6.21116976 4.9689358 3.72670185] + [16149.04136518 14906.80741401 13664.57346284] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Cuboid(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(1,0,0), (0,1,0), (0,0,1)]) >>> print(B) - [[4.30496934 6.9363475 0.50728577] - [0.54127889 0.86827283 0.05653357] - [0.1604214 0.25726266 0.01664045]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). Here we use a `Sensor` object as observer. - - >>> sens = magpy.Sensor(position=(1,1,1)) - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) - Cuboid(id=...) - >>> B = src.getB(sens) - >>> print(B) - [[4.30496934 6.9363475 0.50728577] - [0.54127889 0.86827283 0.05653357] - [0.1604214 0.25726266 0.01664045]] + [[ 0.06739119 0.00476528 -0.0619486 ] + [-0.03557183 -0.01149497 -0.08403664] + [-0.03557183 0.00646436 0.14943466]] """ _field_func = staticmethod(magnet_cuboid_field) - _field_func_kwargs_ndim = {"magnetization": 2, "dimension": 2} + _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_Cuboid def __init__( self, magnetization=None, + polarization=None, dimension=None, position=(0, 0, 0), orientation=None, @@ -100,17 +90,19 @@ def __init__( self.dimension = dimension # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property def dimension(self): - """Length of the cuboid sides [a,b,c] in units of mm.""" + """Length of the cuboid sides [a,b,c] in arbitrary length units, e.g. in meter.""" return self._dimension @dimension.setter def dimension(self, dim): - """Set Cuboid dimension (a,b,c), shape (3,), mm.""" + """Set Cuboid dimension (a,b,c), shape (3,)""" self._dimension = check_format_input_vector( dim, dims=(1,), From f80c81387be1bf234b297bcb76c9290d4c486b26 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 08:57:33 +0100 Subject: [PATCH 002/240] untrack non-python __temp files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1577f408c..fb92fc1ed 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ auto_examples tempCodeRunnerFile.py #temp files -__temp*.py +__temp* # PyInstaller # Usually these files are written by a python script from a template From a43d3a6b551d21b8461b36637833e95b2d522093 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 12:35:16 +0100 Subject: [PATCH 003/240] next update Co-authored-by: Alexandre Boisselet --- __temp3.txt | 5 ----- magpylib/_src/fields/field_BH_cuboid.py | 19 +++++++++++++++---- magpylib/_src/fields/field_BH_cylinder.py | 16 ++++++++-------- magpylib/_src/obj_classes/class_Sensor.py | 4 ++-- .../_src/obj_classes/class_current_Circle.py | 5 +++-- .../_src/obj_classes/class_magnet_Cuboid.py | 14 ++++++++------ 6 files changed, 36 insertions(+), 27 deletions(-) delete mode 100644 __temp3.txt diff --git a/__temp3.txt b/__temp3.txt deleted file mode 100644 index a6d0ace36..000000000 --- a/__temp3.txt +++ /dev/null @@ -1,5 +0,0 @@ -field_BH_cuboid -class_magnet_Cuboid -class_BaseDisplayRepr -class_BaseExcitations -> fix all docstrings - diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 4df0bb504..061fd263f 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -7,14 +7,21 @@ from magpylib._src.input_checks import check_field_input +# CORE def magnet_cuboid_field( - field: str, observers: np.ndarray, polarization: np.ndarray, dimension: np.ndarray + *, + field: str, + observers: np.ndarray, + dimension: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field of a homogeneously magnetized cuboid. The cuboid sides are parallel to the coordinate axes. The geometric center of the cuboid lies in the origin. + Use SI units for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` @@ -22,14 +29,13 @@ def magnet_cuboid_field( in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in arbitrary length - units, e.g. meters. + Observer positions (x,y,z) in Cartesian coordinates in units of m. polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. dimension: ndarray, shape (n,3) - Cuboid side lengths in arbitrary length units, e.g. meters. + Cuboid side lengths in units of m. Returns ------- @@ -53,6 +59,11 @@ def magnet_cuboid_field( Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Field computations via magnetic surface charge density. Published several times with similar expressions: diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index ec0f49e18..b5af2bedd 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -251,7 +251,7 @@ def fieldH_cylinder_diametral( def magnet_cylinder_field( field: str, observers: np.ndarray, - magnetization: np.ndarray, + polarization: np.ndarray, dimension: np.ndarray, ) -> np.ndarray: """Magnetic field of a homogeneously magnetized cylinder. @@ -268,8 +268,8 @@ def magnet_cylinder_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of mm. - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + polarization: ndarray, shape (n,3) + Homogeneous polarization vector in units of mT. dimension: ndarray, shape (n,2) Cylinder dimension (d,h) with diameter d and height h in units of mm. @@ -335,7 +335,7 @@ def magnet_cylinder_field( m3 = r <= 1 # inside Cylinder hull plane # special case: mag = 0 - mask0 = np.linalg.norm(magnetization, axis=1) == 0 + mask0 = np.linalg.norm(polarization, axis=1) == 0 # special case: on Cylinder edge mask_edge = m0 & m1 @@ -343,8 +343,8 @@ def magnet_cylinder_field( # general case mask_gen = ~mask0 & ~mask_edge - # axial/transv magnetization cases - magx, magy, magz = magnetization.T + # axial/transv polarization cases + magx, magy, magz = polarization.T mask_tv = (magx != 0) | (magy != 0) mask_ax = magz != 0 @@ -356,7 +356,7 @@ def magnet_cylinder_field( mask_ax = mask_ax & mask_gen mask_inside = mask_inside & mask_gen - # transversal magnetization contributions ----------------------- + # transversal polarization contributions ----------------------- if any(mask_tv): magxy = np.sqrt(magx**2 + magy**2)[mask_tv] tetta = np.arctan2(magy[mask_tv], magx[mask_tv]) @@ -369,7 +369,7 @@ def magnet_cylinder_field( Bphi[mask_tv] += magxy * bphi_tv Bz[mask_tv] += magxy * bz_tv - # axial magnetization contributions ----------------------------- + # axial polarization contributions ----------------------------- if any(mask_ax): br_ax, bz_ax = fieldB_cylinder_axial(z0[mask_ax], r[mask_ax], z[mask_ax]) Br[mask_ax] += magz[mask_ax] * br_ax diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index a0eec6bb0..14ab2b1ee 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -88,10 +88,10 @@ class Sensor(BaseGeo, BaseDisplayRepr): def __init__( self, position=(0, 0, 0), - pixel=(0, 0, 0), orientation=None, - style=None, + pixel=(0, 0, 0), handedness="right", + style=None, **kwargs, ): # instance attributes diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 206b3ef9b..d9df00a3c 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -87,10 +87,11 @@ class Circle(BaseCurrent): def __init__( self, - current=None, - diameter=None, position=(0, 0, 0), orientation=None, + diameter=None, + current=None, + *, style=None, **kwargs, ): diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 735e9ff2d..b2acbbf06 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -15,6 +15,8 @@ class Cuboid(BaseMagnet): to the global coordinate basis vectors and the geometric center of the Cuboid is located in the origin. + Units .... Min = Bout, Jin = Hout, length units arbitrary + Parameters ---------- polarization: array_like, shape (3,), default=`None` @@ -26,11 +28,11 @@ class Cuboid(BaseMagnet): given in the local object coordinates (rotates with object). dimension: array_like, shape (3,), default=`None` - Length of the cuboid sides [a,b,c] in arbitrary units, e.g. in meters. + Length of the cuboid sides [a,b,c] in meters. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in arbitrary units, e.g. in - meters. For m>1, the `position` and `orientation` attributes together + Object position(s) in the global coordinates in meter. + For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -78,11 +80,11 @@ class Cuboid(BaseMagnet): def __init__( self, - magnetization=None, - polarization=None, - dimension=None, position=(0, 0, 0), orientation=None, + dimension=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): From f36f8ac32a794664eca7d74a3af91bd0b2d90491 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 12:43:51 +0100 Subject: [PATCH 004/240] fix cuboid --- magpylib/_src/fields/field_BH_cuboid.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 061fd263f..6d0754ffd 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -48,10 +48,12 @@ def magnet_cuboid_field( >>> import numpy as np >>> import magpylib as magpy - >>> pol = np.array([(0,0,1), (1,0,0), (0,0,1)]) - >>> dim = np.array([(2,2,2), (3,3,3), (4,4,4)]) - >>> obs = np.array([(1,2,0), (2,3,4), (0,0,0)]) - >>> B = magpy.core.magnet_cuboid_field('B', obs, pol, dim) + >>> B = magpy.core.magnet_cuboid_field( + >>> field='B', + >>> observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), + >>> dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), + >>> polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), + >>> ) >>> print(B) [[ 0. 0. -0.05227894] [-0.00820941 0.00849123 0.011429 ] From bffc1855d5d935a5b8b41480c72e1751e9dc3772 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 21:06:14 +0100 Subject: [PATCH 005/240] MU0 --- magpylib/_src/utility.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 7870a89a7..66ce8b024 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -21,6 +21,8 @@ def get_allowed_sources_msg(): - string {srcs}""" +MU0 = 4 * np.pi * 1e-7 + ALLOWED_OBSERVER_MSG = """Observers must be either - array_like positions of shape (N1, N2, ..., 3) - Sensor object From ed8498dd027bea49e1b193a4839fcabeefa9ffa2 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 21:28:23 +0100 Subject: [PATCH 006/240] fixing bh cuiboid again --- magpylib/_src/fields/field_BH_cuboid.py | 25 +++++++------- tests/test_core_field_functions.py | 46 +++++++++++++++++++------ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 6d0754ffd..c32bcc538 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -5,6 +5,7 @@ import numpy as np from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 # CORE @@ -12,15 +13,15 @@ def magnet_cuboid_field( *, field: str, observers: np.ndarray, - dimension: np.ndarray, - polarization: np.ndarray, + dimensions: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: """Magnetic field of a homogeneously magnetized cuboid. The cuboid sides are parallel to the coordinate axes. The geometric center of the cuboid lies in the origin. - Use SI units for all inputs and outputs. + SI units are used for all inputs and outputs. Parameters ---------- @@ -31,12 +32,12 @@ def magnet_cuboid_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - polarization: ndarray, shape (n,3) - Magnetic polarization vectors in units of T. - dimension: ndarray, shape (n,3) Cuboid side lengths in units of m. + polarization: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. + Returns ------- B-field or H-field: ndarray, shape (n,3) @@ -92,8 +93,8 @@ def magnet_cuboid_field( bh = check_field_input(field, "magnet_cuboid_field()") - magx, magy, magz = polarization.T - a, b, c = np.abs(dimension.T) / 2 + magx, magy, magz = polarizations.T + a, b, c = np.abs(dimensions.T) / 2 x, y, z = observers.T # This implementation is completely scale invariant as only observer/dimension @@ -134,8 +135,8 @@ def magnet_cuboid_field( mask_gen = ~mask1 & mask2 & ~mask3 if np.any(mask_gen): - magx, magy, magz = polarization[mask_gen].T - a, b, c = dimension[mask_gen].T / 2 + magx, magy, magz = polarizations[mask_gen].T + a, b, c = dimensions[mask_gen].T / 2 x, y, z = np.copy(observers[mask_gen]).T # avoid indeterminate forms by evaluating in bottQ4 only -------- @@ -263,6 +264,6 @@ def magnet_cuboid_field( # if inside magnet subtract polarization vector mask_inside = mx2 & my2 & mz2 - B[mask_inside] -= polarization[mask_inside] - H = B / (4 * np.pi * 1e-7) # T -> A/m + B[mask_inside] -= polarizations[mask_inside] + H = B / MU0 # T -> A/m return H diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 5aced4511..ede2c8187 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -16,9 +16,9 @@ from magpylib.core import magnet_sphere_field -def test_magnet_cuboid_Bfield(): +def test_magnet_cuboid_field_BH(): """test cuboid field""" - mag = np.array( + pol = np.array( [ (0, 0, 0), (1, 2, 3), @@ -44,7 +44,7 @@ def test_magnet_cuboid_Bfield(): (2, 2, 2), ] ) - pos = np.array( + obs = np.array( [ (1, 2, 3), (1, -1, 0), @@ -57,8 +57,13 @@ def test_magnet_cuboid_Bfield(): (1 + 1e-14, -1, 2), ] ) - B = magnet_cuboid_field("B", pos, mag, dim) + B = magnet_cuboid_field( + field="B", + observers=obs, + polarizations=pol, + dimensions=dim, + ) Btest = [ [0.0, 0.0, 0.0], [-0.14174376, -0.16976459, -0.20427478], @@ -70,18 +75,37 @@ def test_magnet_cuboid_Bfield(): [-0.0009913, -0.08747071, 0.04890262], [-0.0009913, -0.08747071, 0.04890262], ] - np.testing.assert_allclose(B, Btest, rtol=1e-5) + H = magnet_cuboid_field( + field="H", + observers=obs, + polarizations=pol, + dimensions=dim, + ) + Htest = [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [-1.12796098e05, -1.35094372e05, -1.62556705e05], + [-1.12796098e05, -1.35094372e05, -1.62556705e05], + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [2.06609885e04, 3.60512520e04, 4.64737143e04], + [np.inf, np.inf, -2.34886623e05], + [-1.59154943e06, -1.59154943e06, -1.59154943e06], + [-7.88852468e02, -6.96069788e04, 3.89154716e04], + [-7.88852468e02, -6.96069788e04, 3.89154716e04], + ] + np.testing.assert_allclose(H, Htest, rtol=1e-5) + def test_magnet_cuboid_field_mag0(): """test cuboid field magnetization=0""" - n = 10 - mag = np.zeros((n, 3)) - dim = np.random.rand(n, 3) - pos = np.random.rand(n, 3) - B = magnet_cuboid_field("B", pos, mag, dim) - assert_allclose(mag, B) + B = magnet_cuboid_field( + field="B", + observers=np.ones((1, 3)), + polarizations=np.zeros((1, 3)), + dimensions=np.ones((1, 3)), + ) + np.testing.assert_allclose(np.zeros((1, 3)), B) def test_field_BH_cylinder_tile_mag0(): From 1d9293dd5684f604d480c61efe502845f2fb5b61 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 21:30:02 +0100 Subject: [PATCH 007/240] cuboid fix again --- magpylib/_src/fields/field_BH_cuboid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index c32bcc538..de4008aa5 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -32,10 +32,10 @@ def magnet_cuboid_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - dimension: ndarray, shape (n,3) + dimensions: ndarray, shape (n,3) Cuboid side lengths in units of m. - polarization: ndarray, shape (n,3) + polarizations: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns From da9ec7940bca25d5b3e8b6a35952654f864c4e1f Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 22:29:16 +0100 Subject: [PATCH 008/240] another cube field fix --- magpylib/_src/fields/field_BH_cuboid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index de4008aa5..cd92c2626 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -52,8 +52,8 @@ def magnet_cuboid_field( >>> B = magpy.core.magnet_cuboid_field( >>> field='B', >>> observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), - >>> dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), - >>> polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), + >>> dimensions=np.array([(2,2,2), (3,3,3), (4,4,4)]), + >>> polarizations=np.array([(0,0,1), (1,0,0), (0,0,1)]), >>> ) >>> print(B) [[ 0. 0. -0.05227894] From 8d506def5c34c111cf6f3c5f78c8f3f5718580a0 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 22:33:07 +0100 Subject: [PATCH 009/240] field_BH_cylinder fix --- magpylib/_src/fields/field_BH_cylinder.py | 164 +++++----------------- tests/test_core_field_functions.py | 113 ++++++++++----- 2 files changed, 107 insertions(+), 170 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index b5af2bedd..36645df24 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -11,6 +11,7 @@ from magpylib._src.input_checks import check_field_input from magpylib._src.utility import cart_to_cyl_coordinates from magpylib._src.utility import cyl_field_to_cart +from magpylib._src.utility import MU0 def fieldB_cylinder_axial(z0: np.ndarray, r: np.ndarray, z: np.ndarray) -> list: @@ -249,60 +250,63 @@ def fieldH_cylinder_diametral( # CORE def magnet_cylinder_field( + *, field: str, observers: np.ndarray, - polarization: np.ndarray, - dimension: np.ndarray, + dimensions: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: """Magnetic field of a homogeneously magnetized cylinder. The cylinder axis coincides with the z-axis and the geometric center of the cylinder lies in the origin. + SI units are used for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. - - polarization: ndarray, shape (n,3) - Homogeneous polarization vector in units of mT. + Observer positions (x,y,z) in Cartesian coordinates in units of m. - dimension: ndarray, shape (n,2) - Cylinder dimension (d,h) with diameter d and height h in units of mm. + dimensions: ndarray, shape (n,2) + Cylinder dimension (d,h) with diameter d and height h in units of m. - observer: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. - - field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- - Compute the B-field of two different cylinder magnets at position (1,2,3). + Compute the B-field of two different cylinder magnets at position (1,0,0). >>> import numpy as np >>> import magpylib as magpy - >>> mag = np.array([(0,0,1000), (100,0,100)]) - >>> dim = np.array([(1,1), (2,3)]) - >>> obs = np.array([(1,2,3), (1,2,3)]) - >>> B = magpy.core.magnet_cylinder_field('B', obs, mag, dim) + >>> B = magpy.core.magnet_cylinder_field( + >>> field='B', + >>> observers=np.array([(1,0,0), (1,0,0)]), + >>> dimensions=np.array([(1,1), (1,3)]), + >>> polarizations=np.array([(0,0,1), (.5,0,.5)]), + >>> ) >>> print(B) - [[ 0.77141782 1.54283565 1.10384481] - [-0.15185713 2.90352915 2.23601722]] + [[ 0. 0. -0.05185272] + [ 0.06821654 0. -0.01576545]] Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Axial implementation based on Derby: American Journal of Physics 78.3 (2010): 229-235. @@ -318,7 +322,7 @@ def magnet_cylinder_field( # transform to Cy CS -------------------------------------------- r, phi, z = cart_to_cyl_coordinates(observers) - r0, z0 = dimension.T / 2 + r0, z0 = dimensions.T / 2 # scale invariance (make dimensionless) r = np.copy(r / r0) @@ -335,7 +339,7 @@ def magnet_cylinder_field( m3 = r <= 1 # inside Cylinder hull plane # special case: mag = 0 - mask0 = np.linalg.norm(polarization, axis=1) == 0 + mask0 = np.linalg.norm(polarizations, axis=1) == 0 # special case: on Cylinder edge mask_edge = m0 & m1 @@ -344,7 +348,7 @@ def magnet_cylinder_field( mask_gen = ~mask0 & ~mask_edge # axial/transv polarization cases - magx, magy, magz = polarization.T + magx, magy, magz = polarizations.T mask_tv = (magx != 0) | (magy != 0) mask_ax = magz != 0 @@ -387,108 +391,4 @@ def magnet_cylinder_field( if any(mask_ax): # ax computes B-field Bz[mask_tv * mask_inside] -= magz[mask_tv * mask_inside] - return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T * 10 / 4 / np.pi - - -# old iterative solution by Furlani - -# def magnet_cyl_dia_H_Furlani1994( -# tetta: np.ndarray, -# dim: np.ndarray, -# pos_obs: np.ndarray, -# niter: int, -# ) -> np.ndarray: -# """ -# H-field in Cylindrical CS of Cylinder magnet with homogenous -# diametral unit magnetization. The Cylinder axis coincides with the z-axis of the -# CS. The geometric center of the Cylinder is in the origin. - -# Implementation from [Furlani1994]. - -# Parameters -# ---------- -# dim: ndarray, shape (n,2) -# dimension of cylinder (d, h), diameter and height, in units of mm -# tetta: ndarray, shape (n,) -# angle between magnetization vector and x-axis in [rad]. M = (cos(tetta), sin(tetta), 0) -# obs_pos: ndarray, shape (n,3) -# position of observer (r,phi,z) in cylindrical coordinates in units of mm and rad -# niter: int -# Iterations for Simpsons approximation of the final integral - -# Returns -# ------- -# H-field: ndarray -# H-field array of shape (n,3) in cylindrical coordinates (Hr, Hphi Hz) in units of kA/m. - -# Examples -# -------- -# Compute field at three instances. - -# >>> import numpy as np -# >>> import magpylib as magpy -# >>> tetta = np.zeros(3) -# >>> dim = np.array([(2,2), (2,3), (3,4)]) -# >>> obs = np.array([(.1,0,2), (2,0.12,3), (4,0.2,1)]) -# >>> B = magpy.core.magnet_cyl_dia_H_Furlani1994(tetta, dim, obs, 1000) -# >>> print(B) -# [[-5.99240321e-02 1.41132875e-19 8.02440419e-03] -# [ 1.93282782e-03 2.19048077e-03 2.60408201e-02] -# [ 5.27008607e-02 6.06112282e-03 1.54692676e-02]] - -# Notes -# ----- -# H-Field computed from the charge picture, Simpsons approximation used -# to approximate the integral. -# """ - -# r0, z0 = dim.T/2 -# r, phi, z = pos_obs.T -# n = len(r0) - -# # phi is now relative between mag and pos_obs -# phi = phi-tetta - -# #implementation of Furlani1993 -# # generating the iterative summand basics for simpsons approximation -# phi0 = 2*np.pi/niter # discretization -# sphi = np.arange(niter+1) -# sphi[sphi%2==0] = 2. -# sphi[sphi%2==1] = 4. -# sphi[0] = 1. -# sphi[-1] = 1. - -# sphiex = np.outer(sphi, np.ones(n)) -# phi0ex = np.outer(np.arange(niter+1), np.ones(n))*phi0 -# zex = np.outer(np.ones(niter+1), z) -# hex = np.outer(np.ones(niter+1), z0) # pylint: disable=redefined-builtin -# phiex = np.outer(np.ones(niter+1), phi) -# dr2ex = np.outer(np.ones(niter+1), 2*r0*r) -# r2d2ex = np.outer(np.ones(niter+1), r**2+r0**2) - -# # repetitives -# cos_phi0ex = np.cos(phi0ex) -# cos_phi = np.cos(phiex-phi0ex) - -# # compute r-phi components -# mask = (r2d2ex-dr2ex*cos_phi == 0) # special case r = d/2 and cos_phi=1 -# unite = np.ones([niter+1,n]) -# unite[mask] = - (1/2)/(zex[mask]+hex[mask])**2 + (1/2)/(zex[mask]-hex[mask])**2 - -# rrc = r2d2ex[~mask] - dr2ex[~mask]*cos_phi[~mask] -# g_m = 1/np.sqrt(rrc + (zex[~mask] + hex[~mask])**2) -# g_p = 1/np.sqrt(rrc + (zex[~mask] - hex[~mask])**2) -# unite[~mask] = ((zex+hex)[~mask]*g_m - (zex-hex)[~mask]*g_p)/rrc - -# summand = sphiex/3*cos_phi0ex*unite - -# Br = r0/2/niter*np.sum(summand*(r-r0*cos_phi), axis=0) -# Bphi = r0**2/2/niter*np.sum(summand*np.sin(phiex-phi0ex), axis=0) - -# # compute z-component -# gz_m = 1/np.sqrt(r**2 + r0**2 - 2*r0*r*cos_phi + (zex+z0)**2) -# gz_p = 1/np.sqrt(r**2 + r0**2 - 2*r0*r*cos_phi + (zex-z0)**2) -# summandz = sphiex/3*cos_phi0ex*(gz_p - gz_m) -# Bz = r0/2/niter*np.sum(summandz, axis=0) - -# return np.array([Br, Bphi, Bz]).T + return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index ede2c8187..dfda55078 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -6,12 +6,14 @@ from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.fields.field_BH_polyline import current_vertices_field +from magpylib._src.utility import MU0 from magpylib.core import current_circle_field from magpylib.core import current_line_field from magpylib.core import current_loop_field from magpylib.core import current_polyline_field from magpylib.core import dipole_field from magpylib.core import magnet_cuboid_field +from magpylib.core import magnet_cylinder_field from magpylib.core import magnet_cylinder_segment_field from magpylib.core import magnet_sphere_field @@ -25,10 +27,7 @@ def test_magnet_cuboid_field_BH(): (1, 2, 3), (1, 2, 3), (1, 2, 3), - (2, 2, 2), - (2, 2, 2), - (1, 1, 1), - (1, 1, 1), + (1, 2, 3), ] ) dim = np.array( @@ -38,10 +37,7 @@ def test_magnet_cuboid_field_BH(): (1, 2, 2), (0, 2, 2), (1, 2, 3), - (2, 2, 2), - (2, 2, 2), - (2, 2, 2), - (2, 2, 2), + (3, 3, 3), ] ) obs = np.array( @@ -51,10 +47,7 @@ def test_magnet_cuboid_field_BH(): (1, -1, 0), (1, -1, 0), (1, 2, 3), - (1, 1 + 1e-14, 0), - (1, 1, 1), - (1, -1, 2), - (1 + 1e-14, -1, 2), + (0, 0, 0), ] ) @@ -64,48 +57,92 @@ def test_magnet_cuboid_field_BH(): polarizations=pol, dimensions=dim, ) + H = magnet_cuboid_field( + field="H", + observers=obs, + polarizations=pol, + dimensions=dim, + ) + J = np.array([(0, 0, 0)] * 5 + [(1, 2, 3)]) + np.testing.assert_allclose(B, MU0 * H + J) + Btest = [ [0.0, 0.0, 0.0], [-0.14174376, -0.16976459, -0.20427478], [-0.14174376, -0.16976459, -0.20427478], [0.0, 0.0, 0.0], [0.02596336, 0.04530334, 0.05840059], - [np.inf, np.inf, -0.29516724], - [0.0, 0.0, 0.0], - [-0.0009913, -0.08747071, 0.04890262], - [-0.0009913, -0.08747071, 0.04890262], + [0.66666667, 1.33333333, 2.0], ] np.testing.assert_allclose(B, Btest, rtol=1e-5) - H = magnet_cuboid_field( - field="H", - observers=obs, - polarizations=pol, - dimensions=dim, - ) Htest = [ - [0.00000000e00, 0.00000000e00, 0.00000000e00], - [-1.12796098e05, -1.35094372e05, -1.62556705e05], - [-1.12796098e05, -1.35094372e05, -1.62556705e05], - [0.00000000e00, 0.00000000e00, 0.00000000e00], - [2.06609885e04, 3.60512520e04, 4.64737143e04], - [np.inf, np.inf, -2.34886623e05], - [-1.59154943e06, -1.59154943e06, -1.59154943e06], - [-7.88852468e02, -6.96069788e04, 3.89154716e04], - [-7.88852468e02, -6.96069788e04, 3.89154716e04], + [0.0, 0.0, 0.0], + [-112796.09804171, -135094.37189185, -162556.70519527], + [-112796.09804171, -135094.37189185, -162556.70519527], + [0.0, 0.0, 0.0], + [20660.98851314, 36051.25202256, 46473.71425434], + [-265258.23848649, -530516.47697298, -795774.71545948], ] np.testing.assert_allclose(H, Htest, rtol=1e-5) -def test_magnet_cuboid_field_mag0(): - """test cuboid field magnetization=0""" - B = magnet_cuboid_field( +def test_magnet_cylinder_field_BH(): + """test cylinder field computation""" + pol = np.array( + [ + (0, 0, 0), + (1, 2, 3), + (3, 2, -1), + (1, 1, 1), + ] + ) + dim = np.array( + [ + (1, 2), + (2, 2), + (1, 2), + (3, 3), + ] + ) + obs = np.array( + [ + (1, 2, 3), + (1, -1, 0), + (1, 1, 1), + (0, 0, 0), + ] + ) + B = magpy.core.magnet_cylinder_field( field="B", - observers=np.ones((1, 3)), - polarizations=np.zeros((1, 3)), - dimensions=np.ones((1, 3)), + observers=obs, + polarizations=pol, + dimensions=dim, ) - np.testing.assert_allclose(np.zeros((1, 3)), B) + H = magpy.core.magnet_cylinder_field( + field="H", + observers=obs, + polarizations=pol, + dimensions=dim, + ) + J = np.array([(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)]) + np.testing.assert_allclose(B, MU0 * H + J) + + Btest = [ + [0.0, 0.0, 0.0], + [-0.36846057, -0.10171405, -0.33006492], + [0.05331225, 0.07895873, 0.10406998], + [0.64644661, 0.64644661, 0.70710678], + ] + np.testing.assert_allclose(B, Btest) + + Htest = [ + [0.0, 0.0, 0.0], + [-293211.60229288, -80941.4714998, -262657.31858654], + [42424.54100401, 62833.36365626, 82816.25721518], + [-281348.8487991, -281348.8487991, -233077.01786129], + ] + np.testing.assert_allclose(H, Htest) def test_field_BH_cylinder_tile_mag0(): From 11cda87d2ed91cb50055de2ee5a7fb2d12d8a0b2 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 22:56:06 +0100 Subject: [PATCH 010/240] fix field_BH_sphere --- magpylib/_src/fields/field_BH_cylinder.py | 1 - magpylib/_src/fields/field_BH_sphere.py | 48 +++++++----- tests/test_core_field_functions.py | 89 +++++++++++++---------- 3 files changed, 78 insertions(+), 60 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 36645df24..1876f8e52 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -301,7 +301,6 @@ def magnet_cylinder_field( Notes ----- - Advanced unit use: The input unit of magnetization and polarization gives the output unit of H and B. All results are independent of the length input units. One must be careful, however, to use consistently diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 900b5ec24..a2a86b2a1 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -5,38 +5,41 @@ import numpy as np from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 # CORE def magnet_sphere_field( field: str, observers: np.ndarray, - magnetization: np.ndarray, - diameter: np.ndarray, + diameters: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: """Magnetic field of a homogeneously magnetized sphere. The center of the sphere lies in the origin of the coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in units of m. - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + diameters: ndarray, shape (n,) + Sphere diameters in units of m. - diameter: ndarray, shape (n,3) - Sphere diameter in units of mm. + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -45,16 +48,23 @@ def magnet_sphere_field( >>> import numpy as np >>> import magpylib as magpy - >>> dia = np.array([1,5]) - >>> obs = np.array([(1,1,1), (1,1,1)]) - >>> mag = np.array([(1,2,3), (0,0,3)]) - >>> B = magpy.core.magnet_sphere_field('B', obs, mag, dia) + >>> B = magpy.core.magnet_sphere_field( + >>> field='B', + >>> observers=np.array([(1,1,1), (1,1,1)]), + >>> diameters=np.array([1,5]), + >>> polarizations=np.array([(1,2,3), (0,0,3)]), + >>> ) >>> print(B) [[0.04009377 0.03207501 0.02405626] [0. 0. 2. ]] Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + The field corresponds to a dipole field on the outside and is 2/3*mag in the inside (see e.g. "Theoretical Physics, Bertelmann"). """ @@ -65,15 +75,15 @@ def magnet_sphere_field( x, y, z = np.copy(observers.T) r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm - r0 = abs(diameter) / 2 + r0 = abs(diameters) / 2 # inside field & allocate - B = magnetization * 2 / 3 + B = polarizations * 2 / 3 # overwrite outside field entries mask_out = r >= r0 - mag1 = magnetization[mask_out] + mag1 = polarizations[mask_out] obs1 = observers[mask_out] r1 = r[mask_out] r01 = r0[mask_out] @@ -89,6 +99,6 @@ def magnet_sphere_field( return B # adjust and return H - B[~mask_out] = -magnetization[~mask_out] / 3 - H = B * 10 / 4 / np.pi + B[~mask_out] = -polarizations[~mask_out] / 3 + H = B / MU0 return H diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index dfda55078..e2bdf9340 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -145,58 +145,67 @@ def test_magnet_cylinder_field_BH(): np.testing.assert_allclose(H, Htest) -def test_field_BH_cylinder_tile_mag0(): - """test cylinder_tile field magnetization=0""" - n = 10 - mag = np.zeros((n, 3)) - r1, r2, h, phi1, phi2 = np.random.rand(5, n) - r2 = r1 + r2 - phi2 = phi1 + phi2 - dim = np.array([r1, r2, h, phi1, phi2]).T - pos = np.random.rand(n, 3) - B = magnet_cylinder_segment_field("B", pos, mag, dim) - assert_allclose(mag, B) - - -def test_field_sphere_vs_v2(): - """testing against old version""" - result_v2 = np.array( +def test_magnet_sphere_field_BH(): + """test magnet_sphere_field""" + pol = np.array( [ - [22.0, 44.0, 66.0], - [22.0, 44.0, 66.0], - [38.47035383, 30.77628307, 23.0822123], - [0.60933932, 0.43524237, 1.04458169], - [22.0, 44.0, 66.0], - [-0.09071337, -0.18142674, -0.02093385], - [-0.17444878, -0.0139559, -0.10466927], + (0, 0, 0), + (1, 2, 3), + (2, 3, -1), + (2, 3, -1), ] ) - - dim = np.array([1.23] * 7) - mag = np.array([(33, 66, 99)] * 7) - poso = np.array( + dia = np.array([1, 2, 3, 4]) + obs = np.array( [ - (0, 0, 0), - (0.2, 0.2, 0.2), - (0.4, 0.4, 0.4), - (-1, -1, -2), - (0.1, 0.1, 0.1), - (1, 2, -3), - (-3, 2, 1), + (1, 2, 3), + (1, -1, 0), + (0, -1, 0), + (1, -1, 0.5), ] ) - B = magnet_sphere_field("B", poso, mag, dim) + B = magnet_sphere_field( + field="B", + observers=obs, + diameters=dia, + polarizations=pol, + ) + H = magnet_sphere_field( + field="H", + observers=obs, + diameters=dia, + polarizations=pol, + ) + J = np.array([(0, 0, 0), (0, 0, 0), pol[2], pol[3]]) + np.testing.assert_allclose(B, MU0 * H + J) - np.testing.assert_allclose(result_v2, B, rtol=1e-6) + Btest = [ + [0.0, 0.0, 0.0], + [-0.29462783, -0.05892557, -0.35355339], + [1.33333333, 2.0, -0.66666667], + [1.33333333, 2.0, -0.66666667], + ] + np.testing.assert_allclose(B, Btest) + Htest = [ + [0.0, 0.0, 0.0], + [-234457.37399925, -46891.47479985, -281348.8487991], + [-530516.47697298, -795774.71545948, 265258.23848649], + [-530516.47697298, -795774.71545948, 265258.23848649], + ] + np.testing.assert_allclose(H, Htest) -def test_magnet_sphere_field_mag0(): - """test cuboid field magnetization=0""" + +def test_field_BH_cylinder_tile_mag0(): + """test cylinder_tile field magnetization=0""" n = 10 mag = np.zeros((n, 3)) - dim = np.random.rand(n) + r1, r2, h, phi1, phi2 = np.random.rand(5, n) + r2 = r1 + r2 + phi2 = phi1 + phi2 + dim = np.array([r1, r2, h, phi1, phi2]).T pos = np.random.rand(n, 3) - B = magnet_sphere_field("B", pos, mag, dim) + B = magnet_cylinder_segment_field("B", pos, mag, dim) assert_allclose(mag, B) From 0f02168e96f2df062fbed309cbd59bd0d951fa33 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 15 Dec 2023 23:30:30 +0100 Subject: [PATCH 011/240] Cylinder segment --- magpylib/_src/fields/field_BH_cuboid.py | 4 +- magpylib/_src/fields/field_BH_cylinder.py | 2 +- .../_src/fields/field_BH_cylinder_segment.py | 69 +++++++++++-------- magpylib/_src/fields/field_BH_sphere.py | 3 +- tests/test_core_field_functions.py | 67 +++++++++++++++--- 5 files changed, 101 insertions(+), 44 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index cd92c2626..b2c711eb2 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -16,7 +16,7 @@ def magnet_cuboid_field( dimensions: np.ndarray, polarizations: np.ndarray, ) -> np.ndarray: - """Magnetic field of a homogeneously magnetized cuboid. + """Magnetic field of homogeneously magnetized cuboids. The cuboid sides are parallel to the coordinate axes. The geometric center of the cuboid lies in the origin. @@ -33,7 +33,7 @@ def magnet_cuboid_field( Observer positions (x,y,z) in Cartesian coordinates in units of m. dimensions: ndarray, shape (n,3) - Cuboid side lengths in units of m. + Length of Cuboid sides in units of m. polarizations: ndarray, shape (n,3) Magnetic polarization vectors in units of T. diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 1876f8e52..ef7acbdf5 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -256,7 +256,7 @@ def magnet_cylinder_field( dimensions: np.ndarray, polarizations: np.ndarray, ) -> np.ndarray: - """Magnetic field of a homogeneously magnetized cylinder. + """Magnetic field of homogeneously magnetized cylinders. The cylinder axis coincides with the z-axis and the geometric center of the cylinder lies in the origin. diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index eb22fdc98..1e5784171 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -10,6 +10,7 @@ from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_field from magpylib._src.fields.special_el3 import el3_angle from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 def arctan_k_tan_2(k, phi): @@ -2347,36 +2348,40 @@ def magnet_cylinder_segment_field_internal( # CORE def magnet_cylinder_segment_field( + *, field: str, observers: np.ndarray, - magnetization: np.ndarray, - dimension: np.ndarray, + dimensions: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: - """Magnetic field of a homogeneously magnetized cylinder segment. + """Magnetic field of homogeneously magnetized cylinder segments. + + The cylinder axis coincides with the z-axis of the global coordinate + system. The geometric center of the cylinder lies in the origin. - The full cylinder axis coincides with the z-axis of the coordinate system. The geometric - center of the full cylinder lies in the origin. + SI units are used for all inputs and outputs. Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. - - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + Observer positions (x,y,z) in Cartesian coordinates in units of m. dimension: ndarray, shape (n,5) - Cylinder segment dimensions (r1,r2,h,phi1,phi2) with inner radius r1, outer radius r2, - height h in units of mm and the two segment angles phi1 and phi2 in units of deg. + Dimensions of CylinderSegments (r1,r2,h,phi1,phi2) with inner radius + r1, outer radius r2, height h in units of m and the two segment + angles phi1 < phi2 in units of deg. + + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -2384,25 +2389,31 @@ def magnet_cylinder_segment_field( >>> import numpy as np >>> import magpylib as magpy - >>> mag = np.array([(0,0,100), (50,50,0)]) - >>> dim = np.array([(0,1,2,0,90), (1,2,4,35,125)]) - >>> obs = np.array([(1,1,1), (1,1,1)]) - >>> B = magpy.core.magnet_cylinder_segment_field('B', obs, mag, dim) + >>> B = magpy.core.magnet_cylinder_segment_field( + >>> field='B', + >>> observers=np.array([(1,1,1), (1,1,1)]), + >>> dimensions=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), + >>> polarizations=np.array([(0,0,1), (.5,.5,0)]), + >>> ) >>> print(B) - [[ 6.27410168 6.27410168 -1.20044166] - [29.84602335 20.75731598 0.34961733]] + [[ 0.07046526 0.08373724 -0.0198113 ] + [ 0.29846023 0.20757316 0.00349617]] Notes ----- - Implementation based on + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. - Slanovc: Journal of Magnetism and Magnetic Materials, 2022 (in review) + Implementation based on F.Slanovc, Journal of Magnetism and Magnetic + Materials, Volume 559, 1 October 2022, 169482 """ bh = check_field_input(field, "magnet_cylinder_segment_field()") - BHfinal = np.zeros((len(magnetization), 3)) + BHfinal = np.zeros((len(polarizations), 3)) - r1, r2, h, phi1, phi2 = dimension.T + r1, r2, h, phi1, phi2 = dimensions.T r1 = abs(r1) r2 = abs(r2) h = abs(h) @@ -2452,7 +2463,7 @@ def magnet_cylinder_segment_field( return BHfinal # redefine input if there are some surface-points ------------------------- - magg = magnetization[mask_not_on_surf] + magg = polarizations[mask_not_on_surf] dim = dim[mask_not_on_surf] pos_obs_cy = pos_obs_cy[mask_not_on_surf] phi = phi[mask_not_on_surf] @@ -2468,15 +2479,15 @@ def magnet_cylinder_segment_field( Hr, Hphi, Hz = H_cy.T Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) - H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T * 10 / 4 / np.pi + H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T # return B or H -------------------------------------------------------- if not bh: - BHfinal[mask_not_on_surf] = H + BHfinal[mask_not_on_surf] = H / MU0 return BHfinal - B = H / (10 / 4 / np.pi) # kA/m -> mT + B = H BHfinal[mask_not_on_surf] = B maskX = mask_inside * mask_not_on_surf - BHfinal[maskX] += magnetization[maskX] + BHfinal[maskX] += polarizations[maskX] return BHfinal diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index a2a86b2a1..fa750252c 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -10,12 +10,13 @@ # CORE def magnet_sphere_field( + *, field: str, observers: np.ndarray, diameters: np.ndarray, polarizations: np.ndarray, ) -> np.ndarray: - """Magnetic field of a homogeneously magnetized sphere. + """Magnetic field of homogeneously magnetized spheres. The center of the sphere lies in the origin of the coordinate system. diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index e2bdf9340..857f1d044 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -196,17 +196,62 @@ def test_magnet_sphere_field_BH(): np.testing.assert_allclose(H, Htest) -def test_field_BH_cylinder_tile_mag0(): - """test cylinder_tile field magnetization=0""" - n = 10 - mag = np.zeros((n, 3)) - r1, r2, h, phi1, phi2 = np.random.rand(5, n) - r2 = r1 + r2 - phi2 = phi1 + phi2 - dim = np.array([r1, r2, h, phi1, phi2]).T - pos = np.random.rand(n, 3) - B = magnet_cylinder_segment_field("B", pos, mag, dim) - assert_allclose(mag, B) +def test_field_cylinder_segment_BH(): + """CylinderSegmetn field test""" + pol = np.array( + [ + (0, 0, 0), + (1, 2, 3), + (2, 3, -1), + (2, 3, -1), + ] + ) + dim = np.array( + [ + (1, 2, 3, 10, 20), + (1, 2, 3, 10, 20), + (1, 3, 2, -50, 50), + (0.1, 5, 2, 20, 370), + ] + ) + obs = np.array( + [ + (1, 2, 3), + (1, -1, 0), + (0, -1, 0), + (1, -1, 0.5), + ] + ) + B = magnet_cylinder_segment_field( + field="B", + observers=obs, + dimensions=dim, + polarizations=pol, + ) + H = magnet_cylinder_segment_field( + field="H", + observers=obs, + dimensions=dim, + polarizations=pol, + ) + J = np.array([(0, 0, 0)] * 3 + [pol[3]]) + np.testing.assert_allclose(B, MU0 * H + J) + + Btest = [ + [0.0, 0.0, 0.0], + [0.00762186, 0.04194934, -0.01974813], + [0.52440702, -0.04650694, 0.09432828], + [1.75574175, 2.58945648, -0.19025747], + ] + np.testing.assert_allclose(B, Btest, rtol=1e-6) + + Htest = [ + [0.0, 0.0, 0.0], + [6065.28627343, 33382.22618218, -15715.05894253], + [417309.84428576, -37009.05020239, 75064.06294505], + [-194374.5385654, -326700.15326755, 644372.62925584], + ] + np.testing.assert_allclose(H, Htest, rtol=1e-6) def test_field_dipole1(): From a9727b7129f27ba136a7e809b2f802cd9e27f2b4 Mon Sep 17 00:00:00 2001 From: mortner Date: Sat, 16 Dec 2023 21:44:30 +0100 Subject: [PATCH 012/240] core tetrahedron and triangle --- magpylib/_src/fields/field_BH_tetrahedron.py | 56 +++++---- magpylib/_src/fields/field_BH_triangle.py | 55 +++++---- tests/test_core_field_functions.py | 117 +++++++++++++++++++ 3 files changed, 182 insertions(+), 46 deletions(-) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index f2f70ba45..e268da102 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -57,35 +57,39 @@ def point_inside(points: np.ndarray, vertices: np.ndarray) -> np.ndarray: return inside +# CORE def magnet_tetrahedron_field( + *, field: str, observers: np.ndarray, - magnetization: np.ndarray, vertices: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: """ - Magnetic field generated by a homogeneously magnetized tetrahedron. + Magnetic field generated by a homogeneously magnetized tetrahedra. + + SI units are used for all inputs and outputs. Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. - - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + Observer positions (x,y,z) in Cartesian coordinates in units of m. vertices: ndarray, shape (n,4,3) Vertices of the individual tetrahedrons [(pos1a, pos1b, pos1c, pos1d), - (pos2a, pos2b, pos2c, pos2d), ...]. + (pos2a, pos2b, pos2c, pos2d), ...] given in units of m. + + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -93,20 +97,26 @@ def magnet_tetrahedron_field( >>> import numpy as np >>> import magpylib as magpy - >>> - >>> obs = np.array([(1,2,3), (2,3,4)]) - >>> mag = np.array([(222,333,444), (111,112,113)]) - >>> vert = np.array([((-1,0,0), (1,-1,0), (1,1,0), (0,0,1))]*2) - >>> B = magpy.core.magnet_tetrahedron_field('B', obs, mag, vert) + >>> B = magpy.core.magnet_tetrahedron_field( + >>> field='B', + >>> observers=np.array([(1,2,3), (2,3,4)]), + >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0), (0,0,1))]*2), + >>> polarizations=np.array([(222,333,444), (111,112,113)]), + >>> ) >>> print(B) [[0.19075398 0.8240532 1.18170862] [0.03125701 0.08445416 0.1178967 ]] Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + The tetrahedron is built up via 4 faces applying the Triangle class, making sure that - all normal vectors point outwards, and providing inside-outside evaluation to distinguish - between B- and H-field. + all normal vectors point outwards, and providing inside-outside evaluation to + distinguish between B- and H-field. """ bh = check_field_input(field, "magnet_tetrahedron_field()") @@ -124,10 +134,10 @@ def magnet_tetrahedron_field( axis=0, ) tri_fields = triangle_field( - field, - np.tile(observers, (4, 1)), - np.tile(magnetization, (4, 1)), - tri_vertices, + field=field, + observers=np.tile(observers, (4, 1)), + vertices=tri_vertices, + polarizations=np.tile(polarizations, (4, 1)), ) tetra_field = ( # slightly faster than reshape + sum tri_fields[:n] @@ -139,7 +149,7 @@ def magnet_tetrahedron_field( if not bh: return tetra_field - # if B, and inside magnet add magnetization vector + # if B, and inside magnet add polarizations vector mask_inside = point_inside(observers, vertices) - tetra_field[mask_inside] += magnetization[mask_inside] + tetra_field[mask_inside] += polarizations[mask_inside] return tetra_field diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index e1bb45363..4735b0386 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -6,6 +6,7 @@ import numpy as np from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 def vcross3(a: np.ndarray, b: np.ndarray) -> np.ndarray: @@ -71,13 +72,17 @@ def solid_angle(R: np.ndarray, r: np.ndarray) -> np.ndarray: return np.where(abs(result) > 6.2831853, 0, result) +# CORE def triangle_field( + *, field: str, observers: np.ndarray, - magnetization: np.ndarray, vertices: np.ndarray, + polarizations: np.ndarray, ) -> np.ndarray: - """Magnetic field generated by homogeneous (magnetic) charges on triangular surfaces. + """Magnetic field generated by homogeneously magnetically charged triangular surfaces. + + SI units are used for all inputs and outputs. Can be used to compute the field of a homogeneously magnetized bodies with triangular surface mesh. In this case each Triangle must be defined so that the @@ -86,26 +91,24 @@ def triangle_field( Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in units of m. - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. The triangle surface charge is the - projection of the magnetization vector onto the surface normal vector. The order - of the vertices defines the sign of the normal vector (right-hand-rule). + dimensions: ndarray, shape (n,3) + Length of Cuboid sides in units of m. - vertices: ndarray, shape (n,3,3) - Vertices of triangular faces of the format - [(pos1a, pos1b, pos1c), (pos2a, pos2b, pos2c), ...]. - The vertex order defines the normal vector orientation of the face (right-hand-rule). + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. The triangle surface charge is the + projection of the polarization vector onto the surface normal vector. The order + of the vertices defines the sign of the normal vector (right-hand-rule). Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -113,17 +116,23 @@ def triangle_field( >>> import numpy as np >>> import magpylib as magpy - - >>> mag = np.array([(22,33,44), (33,44,55)]) - >>> vert = np.array([((-1,0,0), (1,-1,0), (1,1,0))]*2) - >>> obs = np.array([(1,2,3), (2,3,4)]) - >>> B = magpy.core.triangle_field('B', obs, mag, vert) + >>> B = magpy.core.triangle_field( + >>> field='B', + >>> observers=np.array([(-.1,.2,.1), (.1,.2,.1)]), + >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0))]*2), + >>> polarizations=np.array([(.22,.33,.44), (.33,.44,.55)]), + >>> ) >>> print(B) - [[0.08836711 0.27062047 0.42198603] - [0.09721743 0.17609525 0.23937461]] + [[-0.0548087 0.05350955 0.17683832] + [-0.04252323 0.05292106 0.23092368]] Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Field computations implemented from Guptasarma, Geophysics, 1999, 64:1, 70-74. Corners give (nan, nan, nan). Edges and in-plane perp components are set to 0. Loss of precision when approaching a triangle as (x-edge)**2 :( @@ -133,7 +142,7 @@ def triangle_field( bh = check_field_input(field, "triangle_field()") n = norm_vector(vertices) - sigma = np.einsum("ij, ij->i", n, magnetization) # vectorized inner product + sigma = np.einsum("ij, ij->i", n, polarizations) # vectorized inner product # vertex <-> observer R = np.swapaxes(vertices, 0, 1) - observers @@ -182,5 +191,5 @@ def triangle_field( if bh: return B.T / np.pi / 4.0 - H = B.T / 1.6 / np.pi**2 # mT -> kA/m + H = B.T / 4 / np.pi / MU0 return H diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 857f1d044..3482328c2 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -16,6 +16,8 @@ from magpylib.core import magnet_cylinder_field from magpylib.core import magnet_cylinder_segment_field from magpylib.core import magnet_sphere_field +from magpylib.core import magnet_tetrahedron_field +from magpylib.core import triangle_field def test_magnet_cuboid_field_BH(): @@ -254,6 +256,121 @@ def test_field_cylinder_segment_BH(): np.testing.assert_allclose(H, Htest, rtol=1e-6) +def test_triangle_field_BH(): + """Test of triangle field core function""" + pol = np.array( + [ + (0, 0, 0), + (1, 2, 3), + (2, -1, 1), + (1, -1, 2), + ] + ) + vert = np.array( + [ + [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + [(1, 2, 3), (0, 1, -5), (1, 1, 5)], + [(1, 2, 2), (0, 1, -1), (3, -1, 1)], + ] + ) + obs = np.array( + [ + (1, 1, 1), + (1, 1, 1), + (1, 1, 1), + (2, 3, 1), + ] + ) + B = triangle_field( + field="B", + observers=obs, + vertices=vert, + polarizations=pol, + ) + H = triangle_field( + field="H", + observers=obs, + vertices=vert, + polarizations=pol, + ) + np.testing.assert_allclose(B, MU0 * H) + + Btest = [ + [0.0, 0.0, 0.0], + [-0.02825571, -0.02825571, -0.04386991], + [-0.34647603, 0.29421715, 0.06980312], + [0.02041789, 0.05109073, 0.00218011], + ] + np.testing.assert_allclose(B, Btest, rtol=1e-06) + + Htest = [ + [0.0, 0.0, 0.0], + [-22485.1813849, -22485.1813849, -34910.56834885], + [-275716.86458395, 234130.57085866, 55547.55765999], + [16248.03897974, 40656.7134656, 1734.8781397], + ] + np.testing.assert_allclose(H, Htest, rtol=1e-06) + + +def test_magnet_tetrahedron_field_BH(): + """Test of tetrahedron field core function""" + pol = np.array( + [ + (0, 0, 0), + (1, 2, 3), + (-1, 0.5, 0.1), + (2, 2, -1), + ] + ) + vert = np.array( + [ + [(0, 0, 0), (0, 1, 0), (1, 0, 0), (0, 0, 1)], + [(0, 0, 0), (0, 1, 0), (1, 0, 0), (0, 0, 1)], + [(-1, 0, -1), (1, 1, -1), (1, -1, -1), (0, 0, 1)], + [(-1, 0, -1), (1, 1, -1), (1, -1, -1), (0, 0, 1)], + ] + ) + obs = np.array( + [ + (1, 1, 1), + (1, 1, 1), + (0, 0, 0), + (2, 0, 0), + ] + ) + B = magnet_tetrahedron_field( + field="B", + observers=obs, + vertices=vert, + polarizations=pol, + ) + H = magnet_tetrahedron_field( + field="H", + observers=obs, + vertices=vert, + polarizations=pol, + ) + J = np.array([(0, 0, 0)] * 2 + [pol[2]] + [(0, 0, 0)]) + np.testing.assert_allclose(B, MU0 * H + J) + + Btest = [ + [0.0, 0.0, 0.0], + [0.02602367, 0.02081894, 0.0156142], + [-0.69704332, 0.20326329, 0.11578416], + [0.04004769, -0.03186713, 0.03854207], + ] + np.testing.assert_allclose(B, Btest, rtol=1e-06) + + Htest = [ + [0.0, 0.0, 0.0], + [20708.97827326, 16567.1826186, 12425.38696395], + [241085.26350642, -236135.56979233, 12560.63814427], + [31868.94160192, -25359.05664996, 30670.80436549], + ] + np.testing.assert_allclose(H, Htest, rtol=1e-06) + + def test_field_dipole1(): """Test standard dipole field output computed with mathematica""" poso = np.array([(1, 2, 3), (-1, 2, 3)]) From a2a7da2de6a6f1351304742281e71cdf73405347 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 09:02:50 +0100 Subject: [PATCH 013/240] renaming masks --- magpylib/_src/fields/field_BH_cuboid.py | 33 ++++++++++++++----------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index b2c711eb2..feea586d3 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -7,6 +7,7 @@ from magpylib._src.input_checks import check_field_input from magpylib._src.utility import MU0 +RTOL_SURFACE = 1e-15 # relative tolerance to consider on surface # CORE def magnet_cuboid_field( @@ -106,33 +107,35 @@ def magnet_cuboid_field( B_all = np.zeros((len(magx), 3)) # SPECIAL CASE 1: mag = (0,0,0) - mask1 = (magx == 0) * (magy == 0) * (magz == 0) # 2x faster than np.all() + mask_not_null_mag = (magx != 0) * (magy != 0) * (magz != 0) # 2x faster than np.all() # SPECIAL CASE 2: 0 in dimension - mask2 = (a * b * c).astype(bool) + mask_not_null_dim = (a * b * c).astype(bool) # SPECIAL CASE 3: observer lies on-edge/corner - # -> 1e-15 to account for numerical imprecision when e.g. rotating - # -> /a /b /c to account for the "missing" scaling (1e-15 is large when - # a is e.g. 1e-15 itself) - - mx1 = abs(abs(x) - a) < 1e-15 * a # on surface - my1 = abs(abs(y) - b) < 1e-15 * b # on surface - mz1 = abs(abs(z) - c) < 1e-15 * c # on surface - - mx2 = (abs(x) - a) < 1e-15 * a # within cuboid dimension - my2 = (abs(y) - b) < 1e-15 * b # within cuboid dimension - mz2 = (abs(z) - c) < 1e-15 * c # within cuboid dimension + # -> EPSILON to account for numerical imprecision when e.g. rotating + # -> /a /b /c to account for the "missing" scaling (EPSILON is large when + # a is e.g. EPSILON itself) + x_dist = abs(x) - a + y_dist = abs(y) - b + z_dist = abs(z) - c + mx1 = abs(x_dist) < RTOL_SURFACE * a # on surface + my1 = abs(y_dist) < RTOL_SURFACE * b # on surface + mz1 = abs(z_dist) < RTOL_SURFACE * c # on surface + + mx2 = x_dist < RTOL_SURFACE * a # within cuboid dimension + my2 = y_dist < RTOL_SURFACE * b # within cuboid dimension + mz2 = z_dist < RTOL_SURFACE * c # within cuboid dimension mask_xedge = my1 & mz1 & mx2 mask_yedge = mx1 & mz1 & my2 mask_zedge = mx1 & my1 & mz2 - mask3 = mask_xedge | mask_yedge | mask_zedge + mask_not_edge = ~(mask_xedge | mask_yedge | mask_zedge) # on-wall is not a special case # continue only with general cases ---------------------------- - mask_gen = ~mask1 & mask2 & ~mask3 + mask_gen = mask_not_null_mag & mask_not_null_dim & mask_not_edge if np.any(mask_gen): magx, magy, magz = polarizations[mask_gen].T From 3026be4daa4ce6d335648cb3a84fd2d1ad6fd4e3 Mon Sep 17 00:00:00 2001 From: mortner Date: Mon, 18 Dec 2023 09:48:01 +0100 Subject: [PATCH 014/240] field_BH_triangularmesh fixed --- .../_src/fields/field_BH_triangularmesh.py | 70 +++++++++------- tests/test_core_field_functions.py | 79 +++++++++++++++++++ 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index 070a81505..ceb806c1c 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -8,6 +8,7 @@ import scipy.spatial from magpylib._src.fields.field_BH_triangle import triangle_field +from magpylib._src.utility import MU0 def calculate_centroid(vertices, faces): @@ -493,33 +494,39 @@ def mask_inside_trimesh(points: np.ndarray, faces: np.ndarray) -> np.ndarray: return mask_inside +# CORE LIKE - but is not a core function! def magnet_trimesh_field( + *, field: str, observers: np.ndarray, - magnetization: np.ndarray, - mesh: np.ndarray, + meshes: np.ndarray, + polarizations: np.ndarray, in_out="auto", ) -> np.ndarray: """ - core-like function that computes the field of triangular meshes using the triangle_field - - closed nice meshes are assumed (input comes only from TriangularMesh class) + Core-like function that computes the field of triangular meshes using the triangle_field + + !!!Closed meshes are assumed (input comes only from TriangularMesh class)!!! + This is the reasons that this is not a core function + + SI units are used for all inputs and outputs. Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. - - magnetization: ndarray, shape (n,3) - Homogeneous magnetization vector in units of mT. + Observer positions (x,y,z) in Cartesian coordinates in units of m. - mesh: ndarray, shape (n,n1,3,3) or ragged sequence + meshes: ndarray, shape (n,n1,3,3) or ragged sequence Triangular mesh of shape [(x1,y1,z1), (x2,y2,z2), (x3,y3,z3)]. `mesh` can be a ragged sequence of mesh-children with different lengths. + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} Tells if the points are inside or outside the enclosing mesh for the correct B/H-field calculation. By default `in_out='auto'` and the inside/outside mask is automatically @@ -531,37 +538,42 @@ def magnet_trimesh_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of magnet in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Field computations via publication: Guptasarma: GEOPHYSICS 1999 64:1, 70-74 """ - if mesh.ndim != 1: # all vertices objects have same number of children - n0, n1, *_ = mesh.shape - vertices_tiled = mesh.reshape(-1, 3, 3) + if meshes.ndim != 1: # all vertices objects have same number of children + n0, n1, *_ = meshes.shape + vertices_tiled = meshes.reshape(-1, 3, 3) observers_tiled = np.repeat(observers, n1, axis=0) - magnetization_tiled = np.repeat(magnetization, n1, axis=0) + polarization_tiled = np.repeat(polarizations, n1, axis=0) B = triangle_field( field="B", observers=observers_tiled, - magnetization=magnetization_tiled, vertices=vertices_tiled, + polarizations=polarization_tiled, ) B = B.reshape((n0, n1, 3)) B = np.sum(B, axis=1) else: - nvs = [f.shape[0] for f in mesh] # length of vertex set + nvs = [f.shape[0] for f in meshes] # length of vertex set split_indices = np.cumsum(nvs)[:-1] # remove last to avoid empty split - vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in mesh]) + vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in meshes]) observers_tiled = np.repeat(observers, nvs, axis=0) - magnetization_tiled = np.repeat(magnetization, nvs, axis=0) + polarization_tiled = np.repeat(polarizations, nvs, axis=0) B = triangle_field( field="B", observers=observers_tiled, - magnetization=magnetization_tiled, vertices=vertices_tiled, + polarizations=polarization_tiled, ) b_split = np.split(B, split_indices) B = np.array([np.sum(bh, axis=0) for bh in b_split]) @@ -569,26 +581,26 @@ def magnet_trimesh_field( if field == "B": if in_out == "auto": prev_ind = 0 - # group similar meshs for inside-outside evaluation and adding B + # group similar meshess for inside-outside evaluation and adding B for new_ind, _ in enumerate(B): if ( new_ind == len(B) - 1 - or mesh[new_ind].shape != mesh[prev_ind].shape - or not np.all(mesh[new_ind] == mesh[prev_ind]) + or meshes[new_ind].shape != meshes[prev_ind].shape + or not np.all(meshes[new_ind] == meshes[prev_ind]) ): if new_ind == len(B) - 1: new_ind = len(B) inside_mask = mask_inside_trimesh( - observers[prev_ind:new_ind], mesh[prev_ind] + observers[prev_ind:new_ind], meshes[prev_ind] ) - # if inside magnet add magnetization vector - B[prev_ind:new_ind][inside_mask] += magnetization[prev_ind:new_ind][ + # if inside magnet add polarization vector + B[prev_ind:new_ind][inside_mask] += polarizations[prev_ind:new_ind][ inside_mask ] prev_ind = new_ind elif in_out == "inside": - B += magnetization + B += polarizations return B - H = B * 10 / 4 / np.pi # mT -> kA/m + H = B / MU0 return H diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 3482328c2..adb66aaa3 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -6,6 +6,7 @@ from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.fields.field_BH_polyline import current_vertices_field +from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 from magpylib.core import current_circle_field from magpylib.core import current_line_field @@ -371,6 +372,84 @@ def test_magnet_tetrahedron_field_BH(): np.testing.assert_allclose(H, Htest, rtol=1e-06) +def test_magnet_trimesh_field_BH(): + """Test of magnet_trimesh_field core-like function""" + + mesh1 = [ + [ + [0.7439252734184265, 0.5922041535377502, 0.30962786078453064], + [0.3820107579231262, -0.8248414397239685, -0.416778564453125], + [-0.5555410385131836, 0.4872661232948303, -0.6737549901008606], + ], + [ + [-0.5555410385131836, 0.4872661232948303, -0.6737549901008606], + [0.3820107579231262, -0.8248414397239685, -0.416778564453125], + [-0.5703949332237244, -0.25462886691093445, 0.7809056639671326], + ], + [ + [0.7439252734184265, 0.5922041535377502, 0.30962786078453064], + [-0.5703949332237244, -0.25462886691093445, 0.7809056639671326], + [0.3820107579231262, -0.8248414397239685, -0.416778564453125], + ], + [ + [0.7439252734184265, 0.5922041535377502, 0.30962786078453064], + [-0.5555410385131836, 0.4872661232948303, -0.6737549901008606], + [-0.5703949332237244, -0.25462886691093445, 0.7809056639671326], + ], + ] + mesh2 = [ + [ + [0.9744000434875488, 0.15463787317276, 0.16319207847118378], + [-0.12062954157590866, -0.8440634608268738, -0.522499144077301], + [-0.3775683045387268, 0.7685779929161072, -0.516459047794342], + ], + [ + [-0.3775683045387268, 0.7685779929161072, -0.516459047794342], + [-0.12062954157590866, -0.8440634608268738, -0.522499144077301], + [-0.47620221972465515, -0.0791524201631546, 0.8757661581039429], + ], + [ + [0.9744000434875488, 0.15463787317276, 0.16319207847118378], + [-0.47620221972465515, -0.0791524201631546, 0.8757661581039429], + [-0.12062954157590866, -0.8440634608268738, -0.522499144077301], + ], + [ + [0.9744000434875488, 0.15463787317276, 0.16319207847118378], + [-0.3775683045387268, 0.7685779929161072, -0.516459047794342], + [-0.47620221972465515, -0.0791524201631546, 0.8757661581039429], + ], + ] + meshes = np.array([mesh1, mesh2]) + pol = np.array([(1, 2, 3), (3, 2, 1)]) + obs = np.array([(1, 2, 3), (0, 0, 0)]) + B = magnet_trimesh_field( + field="B", + observers=obs, + meshes=meshes, + polarizations=pol, + ) + H = magnet_trimesh_field( + field="H", + observers=obs, + meshes=meshes, + polarizations=pol, + ) + J = np.array([(0, 0, 0), (3, 2, 1)]) + np.testing.assert_allclose(B, MU0 * H + J) + + Btest = [ + [1.54452002e-03, 3.11861149e-03, 4.68477835e-03], + [2.00000002e00, 1.33333333e00, 6.66666685e-01], + ] + np.testing.assert_allclose(B, Btest) + + Htest = [ + [1229.08998194, 2481.71216888, 3728.02815642], + [-795774.70120171, -530516.47792526, -265258.22366805], + ] + np.testing.assert_allclose(H, Htest) + + def test_field_dipole1(): """Test standard dipole field output computed with mathematica""" poso = np.array([(1, 2, 3), (-1, 2, 3)]) From 099cf54ba72e686209d0b05e54864c5de726c9db Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 10:46:22 +0100 Subject: [PATCH 015/240] refactor --- magpylib/_src/fields/field_BH_cuboid.py | 336 +++++++++++++----------- magpylib/_src/input_checks.py | 18 +- 2 files changed, 187 insertions(+), 167 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index feea586d3..5ba4074c7 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -7,7 +7,133 @@ from magpylib._src.input_checks import check_field_input from magpylib._src.utility import MU0 -RTOL_SURFACE = 1e-15 # relative tolerance to consider on surface +RTOL_SURFACE = 1e-15 # relative distance tolerance to be considered on surface + + +def _magnet_cuboid_field_B(polarizations, dimensions, observers): + """Magnetic B-field of homogeneously magnetized cuboids + see core `magnet_cuboid_field for parameter definitions""" + magx, magy, magz = polarizations.T + a, b, c = dimensions.T / 2 + x, y, z = np.copy(observers).T + + # avoid indeterminate forms by evaluating in bottQ4 only -------- + # basic masks + maskx = x < 0 + masky = y > 0 + maskz = z > 0 + + # change all positions to their bottQ4 counterparts + x[maskx] = x[maskx] * -1 + y[masky] = y[masky] * -1 + z[maskz] = z[maskz] * -1 + + # create sign flips for position changes + qsigns = np.ones((len(magx), 3, 3)) + qs_flipx = np.array([[1, -1, -1], [-1, 1, 1], [-1, 1, 1]]) + qs_flipy = np.array([[1, -1, 1], [-1, 1, -1], [1, -1, 1]]) + qs_flipz = np.array([[1, 1, -1], [1, 1, -1], [-1, -1, 1]]) + # signs flips can be applied subsequently + qsigns[maskx] = qsigns[maskx] * qs_flipx + qsigns[masky] = qsigns[masky] * qs_flipy + qsigns[maskz] = qsigns[maskz] * qs_flipz + + # field computations -------------------------------------------- + # Note: in principle the computation for all three mag-components can be + # vectorized itself using symmetries. However, tiling the three + # components will cost more than is gained by the vectorized evaluation + + # Note: making the following computation steps is not necessary + # as mkl will cache such small computations + xma, xpa = x - a, x + a + ymb, ypb = y - b, y + b + zmc, zpc = z - c, z + c + + xma2, xpa2 = xma**2, xpa**2 + ymb2, ypb2 = ymb**2, ypb**2 + zmc2, zpc2 = zmc**2, zpc**2 + + mmm = np.sqrt(xma2 + ymb2 + zmc2) + pmp = np.sqrt(xpa2 + ymb2 + zpc2) + pmm = np.sqrt(xpa2 + ymb2 + zmc2) + mmp = np.sqrt(xma2 + ymb2 + zpc2) + mpm = np.sqrt(xma2 + ypb2 + zmc2) + ppp = np.sqrt(xpa2 + ypb2 + zpc2) + ppm = np.sqrt(xpa2 + ypb2 + zmc2) + mpp = np.sqrt(xma2 + ypb2 + zpc2) + + with np.errstate(divide="ignore", invalid="ignore"): + ff2x = np.log((xma + mmm) * (xpa + ppm) * (xpa + pmp) * (xma + mpp)) - np.log( + (xpa + pmm) * (xma + mpm) * (xma + mmp) * (xpa + ppp) + ) + + ff2y = np.log( + (-ymb + mmm) * (-ypb + ppm) * (-ymb + pmp) * (-ypb + mpp) + ) - np.log((-ymb + pmm) * (-ypb + mpm) * (ymb - mmp) * (ypb - ppp)) + + ff2z = np.log( + (-zmc + mmm) * (-zmc + ppm) * (-zpc + pmp) * (-zpc + mpp) + ) - np.log((-zmc + pmm) * (zmc - mpm) * (-zpc + mmp) * (zpc - ppp)) + + ff1x = ( + np.arctan2((ymb * zmc), (xma * mmm)) + - np.arctan2((ymb * zmc), (xpa * pmm)) + - np.arctan2((ypb * zmc), (xma * mpm)) + + np.arctan2((ypb * zmc), (xpa * ppm)) + - np.arctan2((ymb * zpc), (xma * mmp)) + + np.arctan2((ymb * zpc), (xpa * pmp)) + + np.arctan2((ypb * zpc), (xma * mpp)) + - np.arctan2((ypb * zpc), (xpa * ppp)) + ) + + ff1y = ( + np.arctan2((xma * zmc), (ymb * mmm)) + - np.arctan2((xpa * zmc), (ymb * pmm)) + - np.arctan2((xma * zmc), (ypb * mpm)) + + np.arctan2((xpa * zmc), (ypb * ppm)) + - np.arctan2((xma * zpc), (ymb * mmp)) + + np.arctan2((xpa * zpc), (ymb * pmp)) + + np.arctan2((xma * zpc), (ypb * mpp)) + - np.arctan2((xpa * zpc), (ypb * ppp)) + ) + + ff1z = ( + np.arctan2((xma * ymb), (zmc * mmm)) + - np.arctan2((xpa * ymb), (zmc * pmm)) + - np.arctan2((xma * ypb), (zmc * mpm)) + + np.arctan2((xpa * ypb), (zmc * ppm)) + - np.arctan2((xma * ymb), (zpc * mmp)) + + np.arctan2((xpa * ymb), (zpc * pmp)) + + np.arctan2((xma * ypb), (zpc * mpp)) + - np.arctan2((xpa * ypb), (zpc * ppp)) + ) + + # contributions from x-polarization + bx_magx = ( + magx * ff1x * qsigns[:, 0, 0] + ) # the 'missing' third sign is hidden in ff1x + by_magx = magx * ff2z * qsigns[:, 0, 1] + bz_magx = magx * ff2y * qsigns[:, 0, 2] + # contributions from y-polarization + bx_magy = magy * ff2z * qsigns[:, 1, 0] + by_magy = magy * ff1y * qsigns[:, 1, 1] + bz_magy = -magy * ff2x * qsigns[:, 1, 2] + # contributions from z-polarization + bx_magz = magz * ff2y * qsigns[:, 2, 0] + by_magz = -magz * ff2x * qsigns[:, 2, 1] + bz_magz = magz * ff1z * qsigns[:, 2, 2] + + # summing all contributions + bx_tot = bx_magx + bx_magy + bx_magz + by_tot = by_magx + by_magy + by_magz + bz_tot = bz_magx + bz_magy + bz_magz + + # B = np.c_[bx_tot, by_tot, bz_tot] # faster for 10^5 and more evaluations + B = np.concatenate(((bx_tot,), (by_tot,), (bz_tot,)), axis=0).T + + B /= 4 * np.pi + return B + # CORE def magnet_cuboid_field( @@ -16,6 +142,7 @@ def magnet_cuboid_field( observers: np.ndarray, dimensions: np.ndarray, polarizations: np.ndarray, + in_out="auto", ) -> np.ndarray: """Magnetic field of homogeneously magnetized cuboids. @@ -92,7 +219,7 @@ def magnet_cuboid_field( """ # pylint: disable=too-many-statements - bh = check_field_input(field, "magnet_cuboid_field()") + check_field_input(field) magx, magy, magz = polarizations.T a, b, c = np.abs(dimensions.T) / 2 @@ -107,166 +234,61 @@ def magnet_cuboid_field( B_all = np.zeros((len(magx), 3)) # SPECIAL CASE 1: mag = (0,0,0) - mask_not_null_mag = (magx != 0) * (magy != 0) * (magz != 0) # 2x faster than np.all() + mask_not_null_mag = ~( + (magx == 0) * (magy == 0) * (magz == 0) + ) # 2x faster than np.all() # SPECIAL CASE 2: 0 in dimension mask_not_null_dim = (a * b * c).astype(bool) - # SPECIAL CASE 3: observer lies on-edge/corner - # -> EPSILON to account for numerical imprecision when e.g. rotating - # -> /a /b /c to account for the "missing" scaling (EPSILON is large when - # a is e.g. EPSILON itself) - x_dist = abs(x) - a - y_dist = abs(y) - b - z_dist = abs(z) - c - mx1 = abs(x_dist) < RTOL_SURFACE * a # on surface - my1 = abs(y_dist) < RTOL_SURFACE * b # on surface - mz1 = abs(z_dist) < RTOL_SURFACE * c # on surface - - mx2 = x_dist < RTOL_SURFACE * a # within cuboid dimension - my2 = y_dist < RTOL_SURFACE * b # within cuboid dimension - mz2 = z_dist < RTOL_SURFACE * c # within cuboid dimension - - mask_xedge = my1 & mz1 & mx2 - mask_yedge = mx1 & mz1 & my2 - mask_zedge = mx1 & my1 & mz2 - mask_not_edge = ~(mask_xedge | mask_yedge | mask_zedge) - - # on-wall is not a special case - - # continue only with general cases ---------------------------- - mask_gen = mask_not_null_mag & mask_not_null_dim & mask_not_edge - - if np.any(mask_gen): - magx, magy, magz = polarizations[mask_gen].T - a, b, c = dimensions[mask_gen].T / 2 - x, y, z = np.copy(observers[mask_gen]).T - - # avoid indeterminate forms by evaluating in bottQ4 only -------- - # basic masks - maskx = x < 0 - masky = y > 0 - maskz = z > 0 - - # change all positions to their bottQ4 counterparts - x[maskx] = x[maskx] * -1 - y[masky] = y[masky] * -1 - z[maskz] = z[maskz] * -1 - - # create sign flips for position changes - qsigns = np.ones((len(magx), 3, 3)) - qs_flipx = np.array([[1, -1, -1], [-1, 1, 1], [-1, 1, 1]]) - qs_flipy = np.array([[1, -1, 1], [-1, 1, -1], [1, -1, 1]]) - qs_flipz = np.array([[1, 1, -1], [1, 1, -1], [-1, -1, 1]]) - # signs flips can be applied subsequently - qsigns[maskx] = qsigns[maskx] * qs_flipx - qsigns[masky] = qsigns[masky] * qs_flipy - qsigns[maskz] = qsigns[maskz] * qs_flipz - - # field computations -------------------------------------------- - # Note: in principle the computation for all three mag-components can be - # vectorized itself using symmetries. However, tiling the three - # components will cost more than is gained by the vectorized evaluation - - # Note: making the following computation steps is not necessary - # as mkl will cache such small computations - xma, xpa = x - a, x + a - ymb, ypb = y - b, y + b - zmc, zpc = z - c, z + c - - xma2, xpa2 = xma**2, xpa**2 - ymb2, ypb2 = ymb**2, ypb**2 - zmc2, zpc2 = zmc**2, zpc**2 - - mmm = np.sqrt(xma2 + ymb2 + zmc2) - pmp = np.sqrt(xpa2 + ymb2 + zpc2) - pmm = np.sqrt(xpa2 + ymb2 + zmc2) - mmp = np.sqrt(xma2 + ymb2 + zpc2) - mpm = np.sqrt(xma2 + ypb2 + zmc2) - ppp = np.sqrt(xpa2 + ypb2 + zpc2) - ppm = np.sqrt(xpa2 + ypb2 + zmc2) - mpp = np.sqrt(xma2 + ypb2 + zpc2) - - with np.errstate(divide="ignore", invalid="ignore"): - ff2x = np.log( - (xma + mmm) * (xpa + ppm) * (xpa + pmp) * (xma + mpp) - ) - np.log((xpa + pmm) * (xma + mpm) * (xma + mmp) * (xpa + ppp)) - - ff2y = np.log( - (-ymb + mmm) * (-ypb + ppm) * (-ymb + pmp) * (-ypb + mpp) - ) - np.log((-ymb + pmm) * (-ypb + mpm) * (ymb - mmp) * (ypb - ppp)) - - ff2z = np.log( - (-zmc + mmm) * (-zmc + ppm) * (-zpc + pmp) * (-zpc + mpp) - ) - np.log((-zmc + pmm) * (zmc - mpm) * (-zpc + mmp) * (zpc - ppp)) - - ff1x = ( - np.arctan2((ymb * zmc), (xma * mmm)) - - np.arctan2((ymb * zmc), (xpa * pmm)) - - np.arctan2((ypb * zmc), (xma * mpm)) - + np.arctan2((ypb * zmc), (xpa * ppm)) - - np.arctan2((ymb * zpc), (xma * mmp)) - + np.arctan2((ymb * zpc), (xpa * pmp)) - + np.arctan2((ypb * zpc), (xma * mpp)) - - np.arctan2((ypb * zpc), (xpa * ppp)) + mask_gen = mask_not_null_mag & mask_not_null_dim + + if in_out == "auto" and field != "B": + # SPECIAL CASE 3: observer lies on-edge/corner + # -> EPSILON to account for numerical imprecision when e.g. rotating + # -> /a /b /c to account for the "missing" scaling (EPSILON is large when + # a is e.g. EPSILON itself) + + # on-surface is not a special case + mask_surf_x = abs(x_dist := abs(x) - a) < RTOL_SURFACE * a # on surface + mask_surf_y = abs(y_dist := abs(y) - b) < RTOL_SURFACE * b # on surface + mask_surf_z = abs(z_dist := abs(z) - c) < RTOL_SURFACE * c # on surface + + mask_inside_x = x_dist < RTOL_SURFACE * a # within cuboid dimension + mask_inside_y = y_dist < RTOL_SURFACE * b # within cuboid dimension + mask_inside_z = z_dist < RTOL_SURFACE * c # within cuboid dimension + mask_inside = mask_inside_x & mask_inside_y & mask_inside_z + + mask_xedge = mask_surf_y & mask_surf_z & mask_inside_x + mask_yedge = mask_surf_x & mask_surf_z & mask_inside_y + mask_zedge = mask_surf_x & mask_surf_y & mask_inside_z + mask_not_edge = ~(mask_xedge | mask_yedge | mask_zedge) + + mask_gen = mask_gen & mask_not_edge + elif in_out != "auto": + val = in_out == "inside" + mask_inside = np.full_like(mask_not_null_mag, val, dtype=bool) + + if np.any(mask_gen) and field in "BH": + # continue only with general cases ---------------------------- + B = _magnet_cuboid_field_B( + polarizations[mask_gen], + dimensions[mask_gen], + observers[mask_gen], ) - ff1y = ( - np.arctan2((xma * zmc), (ymb * mmm)) - - np.arctan2((xpa * zmc), (ymb * pmm)) - - np.arctan2((xma * zmc), (ypb * mpm)) - + np.arctan2((xpa * zmc), (ypb * ppm)) - - np.arctan2((xma * zpc), (ymb * mmp)) - + np.arctan2((xpa * zpc), (ymb * pmp)) - + np.arctan2((xma * zpc), (ypb * mpp)) - - np.arctan2((xpa * zpc), (ypb * ppp)) - ) + B_all[mask_gen] = B - ff1z = ( - np.arctan2((xma * ymb), (zmc * mmm)) - - np.arctan2((xpa * ymb), (zmc * pmm)) - - np.arctan2((xma * ypb), (zmc * mpm)) - + np.arctan2((xpa * ypb), (zmc * ppm)) - - np.arctan2((xma * ymb), (zpc * mmp)) - + np.arctan2((xpa * ymb), (zpc * pmp)) - + np.arctan2((xma * ypb), (zpc * mpp)) - - np.arctan2((xpa * ypb), (zpc * ppp)) - ) - - # contributions from x-polarization - bx_magx = ( - magx * ff1x * qsigns[:, 0, 0] - ) # the 'missing' third sign is hidden in ff1x - by_magx = magx * ff2z * qsigns[:, 0, 1] - bz_magx = magx * ff2y * qsigns[:, 0, 2] - # contributions from y-polarization - bx_magy = magy * ff2z * qsigns[:, 1, 0] - by_magy = magy * ff1y * qsigns[:, 1, 1] - bz_magy = -magy * ff2x * qsigns[:, 1, 2] - # contributions from z-polarization - bx_magz = magz * ff2y * qsigns[:, 2, 0] - by_magz = -magz * ff2x * qsigns[:, 2, 1] - bz_magz = magz * ff1z * qsigns[:, 2, 2] - - # summing all contributions - bx_tot = bx_magx + bx_magy + bx_magz - by_tot = by_magx + by_magy + by_magz - bz_tot = bz_magx + bz_magy + bz_magz - - # B = np.c_[bx_tot, by_tot, bz_tot] # faster for 10^5 and more evaluations - B = np.concatenate(((bx_tot,), (by_tot,), (bz_tot,)), axis=0).T - - # combine with special edge/corner cases - B_all[mask_gen] = B - - B = B_all / (4 * np.pi) - - # return B or compute and return H ------------- - if bh: + if field == "B": return B - - # if inside magnet subtract polarization vector - mask_inside = mx2 & my2 & mz2 - B[mask_inside] -= polarizations[mask_inside] - H = B / MU0 # T -> A/m - return H + if field == "H": + # if inside magnet subtract polarization vector + B[mask_inside] -= polarizations[mask_inside] + H = B / MU0 # T -> A/m + return H + J = polarizations + J[~mask_inside] = 0 + if field == "J": + return J + if field == "M": + return J / MU0 diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index f108ac559..64d796374 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -118,17 +118,15 @@ def check_degree_type(inp): ) -def check_field_input(inp, origin): +def check_field_input(inp): """check field input""" - if isinstance(inp, str): - if inp == "B": - return True - if inp == "H": - return False - raise MagpylibBadUserInput( - f"{origin} input can only be `field='B'` or `field='H'`.\n" - f"Instead received {repr(inp)}." - ) + allowed = tuple("BHMJ") + if inp not in allowed: + raise MagpylibBadUserInput( + f"`field` input can only be one of {allowed}.\n" + f"Instead received {repr(inp)}." + + ) def validate_field_func(val): From 80f7f66de82c8b5e105bbfd89f550ba1f566a1e5 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 10:49:46 +0100 Subject: [PATCH 016/240] fix --- magpylib/_src/fields/field_BH_cuboid.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 5ba4074c7..4e98ebafe 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -231,7 +231,7 @@ def magnet_cuboid_field( # dealing with special cases ----------------------------------- # allocate B with zeros - B_all = np.zeros((len(magx), 3)) + B = np.zeros((len(magx), 3)) # SPECIAL CASE 1: mag = (0,0,0) mask_not_null_mag = ~( @@ -271,14 +271,12 @@ def magnet_cuboid_field( if np.any(mask_gen) and field in "BH": # continue only with general cases ---------------------------- - B = _magnet_cuboid_field_B( + B[mask_gen] = _magnet_cuboid_field_B( polarizations[mask_gen], dimensions[mask_gen], observers[mask_gen], ) - B_all[mask_gen] = B - if field == "B": return B if field == "H": From e6bf5ac8a1590f8ccf4ada21a9c5490e5f4eeb28 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 11:34:28 +0100 Subject: [PATCH 017/240] udpate --- magpylib/_src/fields/field_BH_cuboid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 4e98ebafe..fc27855b8 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -269,8 +269,8 @@ def magnet_cuboid_field( val = in_out == "inside" mask_inside = np.full_like(mask_not_null_mag, val, dtype=bool) + # continue only with general cases if np.any(mask_gen) and field in "BH": - # continue only with general cases ---------------------------- B[mask_gen] = _magnet_cuboid_field_B( polarizations[mask_gen], dimensions[mask_gen], From 0e0a573888dff6167b1cbe71c4ad0ae85c0850ad Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 11:34:50 +0100 Subject: [PATCH 018/240] syntax --- magpylib/_src/fields/field_BH_cuboid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index fc27855b8..e96958ab6 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -12,7 +12,7 @@ def _magnet_cuboid_field_B(polarizations, dimensions, observers): """Magnetic B-field of homogeneously magnetized cuboids - see core `magnet_cuboid_field for parameter definitions""" + see core `magnet_cuboid_field` for parameter definitions""" magx, magy, magz = polarizations.T a, b, c = dimensions.T / 2 x, y, z = np.copy(observers).T From 75b224b3322aa60cee53aa789b07ba5bd3e19421 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 12:21:30 +0100 Subject: [PATCH 019/240] copy polarization to avoid leakage --- magpylib/_src/fields/field_BH_cuboid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index e96958ab6..7ce56cb78 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -284,7 +284,7 @@ def magnet_cuboid_field( B[mask_inside] -= polarizations[mask_inside] H = B / MU0 # T -> A/m return H - J = polarizations + J = polarizations.copy() J[~mask_inside] = 0 if field == "J": return J From b35d337c91b4c045e2b249628d9737abc8e519f8 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 13:34:24 +0100 Subject: [PATCH 020/240] use generic BHMJ conversion --- magpylib/_src/fields/field_BH_cuboid.py | 24 +++++++----------- magpylib/_src/utility.py | 33 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 7ce56cb78..805ae61b9 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -5,7 +5,7 @@ import numpy as np from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import MU0 +from magpylib._src.utility import convert_HBMJ RTOL_SURFACE = 1e-15 # relative distance tolerance to be considered on surface @@ -243,6 +243,7 @@ def magnet_cuboid_field( mask_gen = mask_not_null_mag & mask_not_null_dim + mask_inside = None if in_out == "auto" and field != "B": # SPECIAL CASE 3: observer lies on-edge/corner # -> EPSILON to account for numerical imprecision when e.g. rotating @@ -276,17 +277,10 @@ def magnet_cuboid_field( dimensions[mask_gen], observers[mask_gen], ) - - if field == "B": - return B - if field == "H": - # if inside magnet subtract polarization vector - B[mask_inside] -= polarizations[mask_inside] - H = B / MU0 # T -> A/m - return H - J = polarizations.copy() - J[~mask_inside] = 0 - if field == "J": - return J - if field == "M": - return J / MU0 + return convert_HBMJ( + input_field_type="B", + output_field_type=field, + field_values=B, + polarizations=polarizations, + mask_inside=mask_inside, + ) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 66ce8b024..5a17fba59 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -3,6 +3,7 @@ # pylint: disable=cyclic-import # import numbers from math import log10 +from typing import Optional from typing import Sequence import numpy as np @@ -389,3 +390,35 @@ def open_animation(filepath, embed=True): import webbrowser webbrowser.open(filepath) + + + +def convert_HBMJ( + input_field_type: str, + output_field_type: str, + field_values: np.ndarray, + polarizations: np.ndarray, + mask_inside: Optional[np.ndarray], +) -> np.ndarray: + """Convert between magnetic field inputs and outputs. + Notes + ----- + mask_inside is only optional when output and input field types are the same. + """ + if output_field_type == input_field_type: + return field_values + if output_field_type in "MJ": + J = polarizations.copy() + J[~mask_inside] *= 0 + if output_field_type == "J": + return J + if output_field_type == "M": + return J / MU0 + if input_field_type == "B": + H = field_values.copy() + H[mask_inside] -= polarizations[mask_inside] + return H / MU0 + if input_field_type == "H": + B = field_values * MU0 + B[mask_inside] += polarizations[mask_inside] + return B From 3ef0019bbda804ca265f75b09e2191f4b4012161 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 15:47:43 +0100 Subject: [PATCH 021/240] typo --- magpylib/_src/fields/field_BH_triangularmesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index ceb806c1c..35aadf052 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -581,7 +581,7 @@ def magnet_trimesh_field( if field == "B": if in_out == "auto": prev_ind = 0 - # group similar meshess for inside-outside evaluation and adding B + # group similar meshes for inside-outside evaluation and adding B for new_ind, _ in enumerate(B): if ( new_ind == len(B) - 1 From a8ce128ce1012c799fd923ce3c760386afb77c52 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 18 Dec 2023 16:33:27 +0100 Subject: [PATCH 022/240] explicit variables renaming --- magpylib/_src/fields/field_BH_cylinder.py | 60 ++++++++++++----------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index ef7acbdf5..7faad494b 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -48,10 +48,10 @@ def fieldB_cylinder_axial(z0: np.ndarray, r: np.ndarray, z: np.ndarray) -> list: gamma = dmr / dpr one = np.ones(n) - # radial field (unit magnetization) + # radial field (unit polarization) Br = (cel(k1, one, one, -one) / sq1 - cel(k0, one, one, -one) / sq0) / np.pi - # axial field (unit magnetization) + # axial field (unit polarization) Bz = ( 1 / dpr @@ -317,7 +317,7 @@ def magnet_cylinder_field( Leitner/Rauber/Orter: WIP """ - bh = check_field_input(field, "magnet_cylinder_field()") + check_field_input(field) # transform to Cy CS -------------------------------------------- r, phi, z = cart_to_cyl_coordinates(observers) @@ -332,27 +332,27 @@ def magnet_cylinder_field( Br, Bphi, Bz = np.zeros((3, len(r))) # create masks to distinguish between cases --------------------- - m0 = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane - m1 = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane - m2 = np.abs(z) <= z0 # in-between top and bottom plane - m3 = r <= 1 # inside Cylinder hull plane + mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane + mask_on_bases = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane + mask_between_bases = np.abs(z) <= z0 # in-between top and bottom plane + mask_inside_hull = r <= 1 # inside Cylinder hull plane - # special case: mag = 0 - mask0 = np.linalg.norm(polarizations, axis=1) == 0 + # special case: pol = 0 + mask_pol_not_null = np.linalg.norm(polarizations, axis=1) != 0 # special case: on Cylinder edge - mask_edge = m0 & m1 + mask_on_edge = mask_on_hull & mask_on_bases # general case - mask_gen = ~mask0 & ~mask_edge + mask_gen = mask_pol_not_null & ~mask_on_edge # axial/transv polarization cases - magx, magy, magz = polarizations.T - mask_tv = (magx != 0) | (magy != 0) - mask_ax = magz != 0 + pol_x, pol_y, pol_z = polarizations.T + mask_tv = (pol_x != 0) | (pol_y != 0) + mask_ax = pol_z != 0 # inside/outside - mask_inside = m2 & m3 + mask_inside = mask_between_bases & mask_inside_hull # general case masks mask_tv = mask_tv & mask_gen @@ -361,33 +361,35 @@ def magnet_cylinder_field( # transversal polarization contributions ----------------------- if any(mask_tv): - magxy = np.sqrt(magx**2 + magy**2)[mask_tv] - tetta = np.arctan2(magy[mask_tv], magx[mask_tv]) - br_tv, bphi_tv, bz_tv = fieldH_cylinder_diametral( + pol_xy = np.sqrt(pol_x**2 + pol_y**2)[mask_tv] + tetta = np.arctan2(pol_y[mask_tv], pol_x[mask_tv]) + Br_tv, Bphi_tv, Bz_tv = fieldH_cylinder_diametral( z0[mask_tv], r[mask_tv], phi[mask_tv] - tetta, z[mask_tv] ) - # add to H-field (inside magxy is missing for B !!!) - Br[mask_tv] += magxy * br_tv - Bphi[mask_tv] += magxy * bphi_tv - Bz[mask_tv] += magxy * bz_tv + # add to H-field (inside pol_xy is missing for B !!!) + Br[mask_tv] += pol_xy * Br_tv + Bphi[mask_tv] += pol_xy * Bphi_tv + Bz[mask_tv] += pol_xy * Bz_tv # axial polarization contributions ----------------------------- if any(mask_ax): - br_ax, bz_ax = fieldB_cylinder_axial(z0[mask_ax], r[mask_ax], z[mask_ax]) - Br[mask_ax] += magz[mask_ax] * br_ax - Bz[mask_ax] += magz[mask_ax] * bz_ax + Br_ax, Bz_ax = fieldB_cylinder_axial(z0[mask_ax], r[mask_ax], z[mask_ax]) + Br[mask_ax] += pol_z[mask_ax] * Br_ax + Bz[mask_ax] += pol_z[mask_ax] * Bz_ax # transform field to cartesian CS ------------------------------- Bx, By = cyl_field_to_cart(phi, Br, Bphi) # add/subtract Mag when inside for B/H -------------------------- - if bh: + if field == "B": if any(mask_tv): # tv computes H-field - Bx[mask_tv * mask_inside] += magx[mask_tv * mask_inside] - By[mask_tv * mask_inside] += magy[mask_tv * mask_inside] + mask_tv_inside = mask_tv * mask_inside + Bx[mask_tv_inside] += pol_x[mask_tv_inside] + By[mask_tv_inside] += pol_y[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T if any(mask_ax): # ax computes B-field - Bz[mask_tv * mask_inside] -= magz[mask_tv * mask_inside] + mask_tv_inside = mask_tv * mask_inside + Bz[mask_tv_inside] -= pol_z[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 From cbd5deb6af91cd81f40fe1c338188883a2f9a61c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 18 Dec 2023 22:06:04 +0100 Subject: [PATCH 023/240] explicit variables renaming --- magpylib/_src/fields/field_BH_sphere.py | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index fa750252c..7c5ae3ac6 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -70,36 +70,38 @@ def magnet_sphere_field( in the inside (see e.g. "Theoretical Physics, Bertelmann"). """ - bh = check_field_input(field, "magnet_sphere_field()") + check_field_input(field) - # all special cases r0=0 and mag=0 automatically covered + # all special cases r_obs=0 and pol=0 automatically covered x, y, z = np.copy(observers.T) r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm - r0 = abs(diameters) / 2 + r_obs = abs(diameters) / 2 # inside field & allocate B = polarizations * 2 / 3 # overwrite outside field entries - mask_out = r >= r0 + mask_outside = r >= r_obs - mag1 = polarizations[mask_out] - obs1 = observers[mask_out] - r1 = r[mask_out] - r01 = r0[mask_out] + pol_out = polarizations[mask_outside] + obs_out = observers[mask_outside] + r_out = r[mask_outside] + r_obs_out = r_obs[mask_outside] field_out = ( - (3 * (np.sum(mag1 * obs1, axis=1) * obs1.T) / r1**5 - mag1.T / r1**3) - * r01**3 + ( + 3 * (np.sum(pol_out * obs_out, axis=1) * obs_out.T) / r_out**5 + - pol_out.T / r_out**3 + ) + * r_obs_out**3 / 3 ) - B[mask_out] = field_out.T + B[mask_outside] = field_out.T - if bh: + if field == "B": return B - # adjust and return H - B[~mask_out] = -polarizations[~mask_out] / 3 + B[~mask_outside] = -polarizations[~mask_outside] / 3 H = B / MU0 return H From 4f74c6ed8b19d033d1308b944227b92eb860a002 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 19 Dec 2023 12:34:39 +0100 Subject: [PATCH 024/240] refactor field_BH_tetrahedron --- magpylib/_src/fields/field_BH_tetrahedron.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index e268da102..be65da771 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -119,7 +119,7 @@ def magnet_tetrahedron_field( distinguish between B- and H-field. """ - bh = check_field_input(field, "magnet_tetrahedron_field()") + check_field_input(field) n = len(observers) @@ -146,7 +146,7 @@ def magnet_tetrahedron_field( + tri_fields[3 * n :] ) - if not bh: + if field=="H": return tetra_field # if B, and inside magnet add polarizations vector From 9d042db2fd87e745cbb10eac3d3a6ee0e6741e33 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 19 Dec 2023 12:37:00 +0100 Subject: [PATCH 025/240] fix field_BH_triangle check inputs --- magpylib/_src/fields/field_BH_triangle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index 4735b0386..ff1e7fffc 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -139,7 +139,7 @@ def triangle_field( Loss of precision with distance from the triangle as distance**3 :( """ # pylint: disable=too-many-statements - bh = check_field_input(field, "triangle_field()") + check_field_input(field) n = norm_vector(vertices) sigma = np.einsum("ij, ij->i", n, polarizations) # vectorized inner product @@ -188,7 +188,7 @@ def triangle_field( B = sigma * (n.T * solid_angle(R, r) - vcross3(n, PQR).T) # return B or compute and return H ------------- - if bh: + if field=="B": return B.T / np.pi / 4.0 H = B.T / 4 / np.pi / MU0 From e9ece07115552bbf3ca87f663b1bf297ee53290f Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 19 Dec 2023 15:18:47 +0100 Subject: [PATCH 026/240] use convert_BHMJ for field_BH_sphere --- magpylib/_src/fields/field_BH_sphere.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 7c5ae3ac6..e4245aa4e 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -5,7 +5,7 @@ import numpy as np from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import MU0 +from magpylib._src.utility import convert_HBMJ # CORE @@ -99,9 +99,11 @@ def magnet_sphere_field( ) B[mask_outside] = field_out.T - if field == "B": - return B - - B[~mask_outside] = -polarizations[~mask_outside] / 3 - H = B / MU0 - return H + mask_inside = ~mask_outside + return convert_HBMJ( + input_field_type="B", + output_field_type=field, + field_values=B, + polarizations=polarizations, + mask_inside=mask_inside, + ) From e8ec633d6ade70f0f3b11587f8249b3a4d6c54c6 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 20 Dec 2023 14:22:01 +0100 Subject: [PATCH 027/240] update --- magpylib/_src/fields/field_BH_cuboid.py | 4 +- magpylib/_src/fields/field_BH_sphere.py | 40 +++++++----- magpylib/_src/fields/field_BH_tetrahedron.py | 68 ++++++++++++-------- magpylib/_src/utility.py | 16 +++-- 4 files changed, 75 insertions(+), 53 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 805ae61b9..5b66c20d4 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -278,9 +278,9 @@ def magnet_cuboid_field( observers[mask_gen], ) return convert_HBMJ( - input_field_type="B", output_field_type=field, - field_values=B, polarizations=polarizations, + input_field_type="B", + field_values=B, mask_inside=mask_inside, ) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index e4245aa4e..24610a961 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -15,6 +15,7 @@ def magnet_sphere_field( observers: np.ndarray, diameters: np.ndarray, polarizations: np.ndarray, + in_out="auto", ) -> np.ndarray: """Magnetic field of homogeneously magnetized spheres. @@ -81,29 +82,34 @@ def magnet_sphere_field( # inside field & allocate B = polarizations * 2 / 3 + if in_out == "auto": + mask_inside = r < r_obs + else: + val = in_out == "inside" + mask_inside = np.full(len(B), val) # overwrite outside field entries - mask_outside = r >= r_obs - pol_out = polarizations[mask_outside] - obs_out = observers[mask_outside] - r_out = r[mask_outside] - r_obs_out = r_obs[mask_outside] - - field_out = ( - ( - 3 * (np.sum(pol_out * obs_out, axis=1) * obs_out.T) / r_out**5 - - pol_out.T / r_out**3 + mask_outside = ~mask_inside + if mask_outside.any(): + pol_out = polarizations[mask_outside] + obs_out = observers[mask_outside] + r_out = r[mask_outside] + r_obs_out = r_obs[mask_outside] + + field_out = ( + ( + 3 * (np.sum(pol_out * obs_out, axis=1) * obs_out.T) / r_out**5 + - pol_out.T / r_out**3 + ) + * r_obs_out**3 + / 3 ) - * r_obs_out**3 - / 3 - ) - B[mask_outside] = field_out.T + B[mask_outside] = field_out.T - mask_inside = ~mask_outside return convert_HBMJ( - input_field_type="B", output_field_type=field, - field_values=B, polarizations=polarizations, + input_field_type="B", + field_values=B, mask_inside=mask_inside, ) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index be65da771..3d8ccad9d 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -6,6 +6,7 @@ from magpylib._src.fields.field_BH_triangle import triangle_field from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import convert_HBMJ def check_chirality(points: np.ndarray) -> np.ndarray: @@ -64,6 +65,7 @@ def magnet_tetrahedron_field( observers: np.ndarray, vertices: np.ndarray, polarizations: np.ndarray, + in_out = "auto", ) -> np.ndarray: """ Magnetic field generated by a homogeneously magnetized tetrahedra. @@ -124,32 +126,44 @@ def magnet_tetrahedron_field( n = len(observers) vertices = check_chirality(vertices) - tri_vertices = np.concatenate( - ( - vertices[:, (0, 2, 1), :], - vertices[:, (0, 1, 3), :], - vertices[:, (1, 2, 3), :], - vertices[:, (0, 3, 2), :], - ), - axis=0, - ) - tri_fields = triangle_field( - field=field, - observers=np.tile(observers, (4, 1)), - vertices=tri_vertices, - polarizations=np.tile(polarizations, (4, 1)), - ) - tetra_field = ( # slightly faster than reshape + sum - tri_fields[:n] - + tri_fields[n : 2 * n] - + tri_fields[2 * n : 3 * n] - + tri_fields[3 * n :] - ) - - if field=="H": + mask_inside = None + if in_out == "auto" and field != "H": + mask_inside = point_inside(observers, vertices) + elif in_out != "auto": + val = in_out == "inside" + mask_inside = np.full(len(observers), val) + if field in "BH": + tri_vertices = np.concatenate( + ( + vertices[:, (0, 2, 1), :], + vertices[:, (0, 1, 3), :], + vertices[:, (1, 2, 3), :], + vertices[:, (0, 3, 2), :], + ), + axis=0, + ) + tri_fields = triangle_field( + field=field, + observers=np.tile(observers, (4, 1)), + vertices=tri_vertices, + polarizations=np.tile(polarizations, (4, 1)), + ) + tetra_field = ( # slightly faster than reshape + sum + tri_fields[:n] + + tri_fields[n : 2 * n] + + tri_fields[2 * n : 3 * n] + + tri_fields[3 * n :] + ) + + if field=="H": + return tetra_field + + # if B, and inside magnet add polarizations vector + tetra_field[mask_inside] += polarizations[mask_inside] return tetra_field - # if B, and inside magnet add polarizations vector - mask_inside = point_inside(observers, vertices) - tetra_field[mask_inside] += polarizations[mask_inside] - return tetra_field + return convert_HBMJ( + output_field_type=field, + polarizations=polarizations, + mask_inside=mask_inside, + ) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 5a17fba59..5f3ebbd41 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -394,26 +394,27 @@ def open_animation(filepath, embed=True): def convert_HBMJ( - input_field_type: str, output_field_type: str, - field_values: np.ndarray, polarizations: np.ndarray, - mask_inside: Optional[np.ndarray], + input_field_type: Optional[str]=None, + field_values: Optional[np.ndarray]=None, + mask_inside: Optional[np.ndarray]=None, ) -> np.ndarray: """Convert between magnetic field inputs and outputs. Notes ----- - mask_inside is only optional when output and input field types are the same. + `mask_inside` is only optional when output and input field types are the same. """ - if output_field_type == input_field_type: - return field_values if output_field_type in "MJ": J = polarizations.copy() - J[~mask_inside] *= 0 + if mask_inside is not None: + J[~mask_inside] *= 0 if output_field_type == "J": return J if output_field_type == "M": return J / MU0 + if output_field_type == input_field_type: + return field_values if input_field_type == "B": H = field_values.copy() H[mask_inside] -= polarizations[mask_inside] @@ -422,3 +423,4 @@ def convert_HBMJ( B = field_values * MU0 B[mask_inside] += polarizations[mask_inside] return B + From 9625be5db67440826c640b60f7fe890c73609452 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 20 Dec 2023 14:34:24 +0100 Subject: [PATCH 028/240] refactor --- magpylib/_src/fields/field_BH_cylinder.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 7faad494b..8b61000e3 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -357,7 +357,6 @@ def magnet_cylinder_field( # general case masks mask_tv = mask_tv & mask_gen mask_ax = mask_ax & mask_gen - mask_inside = mask_inside & mask_gen # transversal polarization contributions ----------------------- if any(mask_tv): @@ -383,13 +382,12 @@ def magnet_cylinder_field( # add/subtract Mag when inside for B/H -------------------------- if field == "B": - if any(mask_tv): # tv computes H-field - mask_tv_inside = mask_tv * mask_inside + mask_tv_inside = mask_tv * mask_inside + if any(mask_tv_inside): # tv computes H-field Bx[mask_tv_inside] += pol_x[mask_tv_inside] By[mask_tv_inside] += pol_y[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T - - if any(mask_ax): # ax computes B-field - mask_tv_inside = mask_tv * mask_inside + mask_tv_inside = mask_tv * mask_inside + if any(mask_tv_inside): # ax computes B-field Bz[mask_tv_inside] -= pol_z[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 From 15944d5f053448980e5996dd0ac3bc1a9b0252f4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 21 Dec 2023 14:51:43 +0100 Subject: [PATCH 029/240] add MJ to triangle --- magpylib/_src/fields/field_BH_triangle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index ff1e7fffc..ae4ac6e1e 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -140,6 +140,8 @@ def triangle_field( """ # pylint: disable=too-many-statements check_field_input(field) + if field in "MJ": + return np.zeros_like(polarizations) n = norm_vector(vertices) sigma = np.einsum("ij, ij->i", n, polarizations) # vectorized inner product @@ -186,10 +188,8 @@ def triangle_field( ) PQR = np.einsum("ij, ijk -> jk", I, L) B = sigma * (n.T * solid_angle(R, r) - vcross3(n, PQR).T) + B = B.T / np.pi / 4.0 - # return B or compute and return H ------------- - if field=="B": - return B.T / np.pi / 4.0 - - H = B.T / 4 / np.pi / MU0 - return H + if field == "B": + return B + return B / MU0 From be936888ceb19497e769986023f60b3ad3ec47d4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 21 Dec 2023 15:34:02 +0100 Subject: [PATCH 030/240] renaming mag to pol --- magpylib/_src/fields/field_BH_cuboid.py | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 5b66c20d4..259126aaa 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -13,7 +13,7 @@ def _magnet_cuboid_field_B(polarizations, dimensions, observers): """Magnetic B-field of homogeneously magnetized cuboids see core `magnet_cuboid_field` for parameter definitions""" - magx, magy, magz = polarizations.T + polx, poly, polz = polarizations.T a, b, c = dimensions.T / 2 x, y, z = np.copy(observers).T @@ -29,7 +29,7 @@ def _magnet_cuboid_field_B(polarizations, dimensions, observers): z[maskz] = z[maskz] * -1 # create sign flips for position changes - qsigns = np.ones((len(magx), 3, 3)) + qsigns = np.ones((len(polx), 3, 3)) qs_flipx = np.array([[1, -1, -1], [-1, 1, 1], [-1, 1, 1]]) qs_flipy = np.array([[1, -1, 1], [-1, 1, -1], [1, -1, 1]]) qs_flipz = np.array([[1, 1, -1], [1, 1, -1], [-1, -1, 1]]) @@ -39,7 +39,7 @@ def _magnet_cuboid_field_B(polarizations, dimensions, observers): qsigns[maskz] = qsigns[maskz] * qs_flipz # field computations -------------------------------------------- - # Note: in principle the computation for all three mag-components can be + # Note: in principle the computation for all three polarization-components can be # vectorized itself using symmetries. However, tiling the three # components will cost more than is gained by the vectorized evaluation @@ -109,24 +109,24 @@ def _magnet_cuboid_field_B(polarizations, dimensions, observers): ) # contributions from x-polarization - bx_magx = ( - magx * ff1x * qsigns[:, 0, 0] + bx_polx = ( + polx * ff1x * qsigns[:, 0, 0] ) # the 'missing' third sign is hidden in ff1x - by_magx = magx * ff2z * qsigns[:, 0, 1] - bz_magx = magx * ff2y * qsigns[:, 0, 2] + by_polx = polx * ff2z * qsigns[:, 0, 1] + bz_polx = polx * ff2y * qsigns[:, 0, 2] # contributions from y-polarization - bx_magy = magy * ff2z * qsigns[:, 1, 0] - by_magy = magy * ff1y * qsigns[:, 1, 1] - bz_magy = -magy * ff2x * qsigns[:, 1, 2] + bx_poly = poly * ff2z * qsigns[:, 1, 0] + by_poly = poly * ff1y * qsigns[:, 1, 1] + bz_poly = -poly * ff2x * qsigns[:, 1, 2] # contributions from z-polarization - bx_magz = magz * ff2y * qsigns[:, 2, 0] - by_magz = -magz * ff2x * qsigns[:, 2, 1] - bz_magz = magz * ff1z * qsigns[:, 2, 2] + bx_polz = polz * ff2y * qsigns[:, 2, 0] + by_polz = -polz * ff2x * qsigns[:, 2, 1] + bz_polz = polz * ff1z * qsigns[:, 2, 2] # summing all contributions - bx_tot = bx_magx + bx_magy + bx_magz - by_tot = by_magx + by_magy + by_magz - bz_tot = bz_magx + bz_magy + bz_magz + bx_tot = bx_polx + bx_poly + bx_polz + by_tot = by_polx + by_poly + by_polz + bz_tot = bz_polx + bz_poly + bz_polz # B = np.c_[bx_tot, by_tot, bz_tot] # faster for 10^5 and more evaluations B = np.concatenate(((bx_tot,), (by_tot,), (bz_tot,)), axis=0).T @@ -221,7 +221,7 @@ def magnet_cuboid_field( check_field_input(field) - magx, magy, magz = polarizations.T + polx, poly, polz = polarizations.T a, b, c = np.abs(dimensions.T) / 2 x, y, z = observers.T @@ -231,17 +231,17 @@ def magnet_cuboid_field( # dealing with special cases ----------------------------------- # allocate B with zeros - B = np.zeros((len(magx), 3)) + B = np.zeros((len(polx), 3)) - # SPECIAL CASE 1: mag = (0,0,0) - mask_not_null_mag = ~( - (magx == 0) * (magy == 0) * (magz == 0) + # SPECIAL CASE 1: polarization = (0,0,0) + mask_pol_not_null = ~( + (polx == 0) * (poly == 0) * (polz == 0) ) # 2x faster than np.all() # SPECIAL CASE 2: 0 in dimension - mask_not_null_dim = (a * b * c).astype(bool) + mask_dim_not_null = (a * b * c).astype(bool) - mask_gen = mask_not_null_mag & mask_not_null_dim + mask_gen = mask_pol_not_null & mask_dim_not_null mask_inside = None if in_out == "auto" and field != "B": @@ -268,7 +268,7 @@ def magnet_cuboid_field( mask_gen = mask_gen & mask_not_edge elif in_out != "auto": val = in_out == "inside" - mask_inside = np.full_like(mask_not_null_mag, val, dtype=bool) + mask_inside = np.full_like(mask_pol_not_null, val, dtype=bool) # continue only with general cases if np.any(mask_gen) and field in "BH": From 93131584c13034f2385f14a08785c34d12463393 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 21 Dec 2023 15:49:57 +0100 Subject: [PATCH 031/240] renaming --- magpylib/_src/fields/field_BH_cylinder.py | 39 ++++++++++++----------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 8b61000e3..ef06fa304 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -348,46 +348,47 @@ def magnet_cylinder_field( # axial/transv polarization cases pol_x, pol_y, pol_z = polarizations.T - mask_tv = (pol_x != 0) | (pol_y != 0) - mask_ax = pol_z != 0 + mask_pol_tv = (pol_x != 0) | (pol_y != 0) + mask_pol_ax = pol_z != 0 # inside/outside mask_inside = mask_between_bases & mask_inside_hull # general case masks - mask_tv = mask_tv & mask_gen - mask_ax = mask_ax & mask_gen + mask_pol_tv = mask_pol_tv & mask_gen + mask_pol_ax = mask_pol_ax & mask_gen + mask_inside = mask_inside & mask_gen # transversal polarization contributions ----------------------- - if any(mask_tv): - pol_xy = np.sqrt(pol_x**2 + pol_y**2)[mask_tv] - tetta = np.arctan2(pol_y[mask_tv], pol_x[mask_tv]) + if any(mask_pol_tv): + pol_xy = np.sqrt(pol_x**2 + pol_y**2)[mask_pol_tv] + tetta = np.arctan2(pol_y[mask_pol_tv], pol_x[mask_pol_tv]) Br_tv, Bphi_tv, Bz_tv = fieldH_cylinder_diametral( - z0[mask_tv], r[mask_tv], phi[mask_tv] - tetta, z[mask_tv] + z0[mask_pol_tv], r[mask_pol_tv], phi[mask_pol_tv] - tetta, z[mask_pol_tv] ) # add to H-field (inside pol_xy is missing for B !!!) - Br[mask_tv] += pol_xy * Br_tv - Bphi[mask_tv] += pol_xy * Bphi_tv - Bz[mask_tv] += pol_xy * Bz_tv + Br[mask_pol_tv] += pol_xy * Br_tv + Bphi[mask_pol_tv] += pol_xy * Bphi_tv + Bz[mask_pol_tv] += pol_xy * Bz_tv # axial polarization contributions ----------------------------- - if any(mask_ax): - Br_ax, Bz_ax = fieldB_cylinder_axial(z0[mask_ax], r[mask_ax], z[mask_ax]) - Br[mask_ax] += pol_z[mask_ax] * Br_ax - Bz[mask_ax] += pol_z[mask_ax] * Bz_ax + if any(mask_pol_ax): + Br_ax, Bz_ax = fieldB_cylinder_axial(z0[mask_pol_ax], r[mask_pol_ax], z[mask_pol_ax]) + Br[mask_pol_ax] += pol_z[mask_pol_ax] * Br_ax + Bz[mask_pol_ax] += pol_z[mask_pol_ax] * Bz_ax # transform field to cartesian CS ------------------------------- Bx, By = cyl_field_to_cart(phi, Br, Bphi) # add/subtract Mag when inside for B/H -------------------------- if field == "B": - mask_tv_inside = mask_tv * mask_inside + mask_tv_inside = mask_pol_tv * mask_inside if any(mask_tv_inside): # tv computes H-field Bx[mask_tv_inside] += pol_x[mask_tv_inside] By[mask_tv_inside] += pol_y[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T - mask_tv_inside = mask_tv * mask_inside - if any(mask_tv_inside): # ax computes B-field - Bz[mask_tv_inside] -= pol_z[mask_tv_inside] + mask_ax_inside = mask_pol_ax * mask_inside + if any(mask_ax_inside): # ax computes B-field + Bz[mask_ax_inside] -= pol_z[mask_ax_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 From e1aa8ce3035ff5120af0399e73c7339163f41c28 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 22 Dec 2023 12:15:20 +0100 Subject: [PATCH 032/240] refactor --- magpylib/_src/fields/field_BH_cuboid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 259126aaa..126eb236d 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -267,8 +267,7 @@ def magnet_cuboid_field( mask_gen = mask_gen & mask_not_edge elif in_out != "auto": - val = in_out == "inside" - mask_inside = np.full_like(mask_pol_not_null, val, dtype=bool) + mask_inside = np.full(len(observers), in_out == "inside") # continue only with general cases if np.any(mask_gen) and field in "BH": From 04c59868299bd22ea1c20e393b79d43b9bd003e7 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 22 Dec 2023 12:15:57 +0100 Subject: [PATCH 033/240] refactor --- magpylib/_src/fields/field_BH_sphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 24610a961..db0fcde4c 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -86,7 +86,7 @@ def magnet_sphere_field( mask_inside = r < r_obs else: val = in_out == "inside" - mask_inside = np.full(len(B), val) + mask_inside = np.full(len(observers), val) # overwrite outside field entries mask_outside = ~mask_inside From a5161f5becbae849b853aa146b5cfc1f8115b014 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 22 Dec 2023 12:57:47 +0100 Subject: [PATCH 034/240] rework check input cyl seg --- magpylib/_src/fields/field_BH_cylinder_segment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 1e5784171..9bc137d3a 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2409,7 +2409,7 @@ def magnet_cylinder_segment_field( Implementation based on F.Slanovc, Journal of Magnetism and Magnetic Materials, Volume 559, 1 October 2022, 169482 """ - bh = check_field_input(field, "magnet_cylinder_segment_field()") + check_field_input(field) BHfinal = np.zeros((len(polarizations), 3)) @@ -2482,7 +2482,7 @@ def magnet_cylinder_segment_field( H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T # return B or H -------------------------------------------------------- - if not bh: + if field=="H": BHfinal[mask_not_on_surf] = H / MU0 return BHfinal From 8dd08740815044f9c72fd7f873269d43b8708609 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 22 Dec 2023 12:58:20 +0100 Subject: [PATCH 035/240] add in_out for cylinder --- magpylib/_src/fields/field_BH_cylinder.py | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index ef06fa304..67b1132b0 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -255,6 +255,7 @@ def magnet_cylinder_field( observers: np.ndarray, dimensions: np.ndarray, polarizations: np.ndarray, + in_out="auto", ) -> np.ndarray: """Magnetic field of homogeneously magnetized cylinders. @@ -331,28 +332,31 @@ def magnet_cylinder_field( # allocate field vectors ---------------------------------------- Br, Bphi, Bz = np.zeros((3, len(r))) - # create masks to distinguish between cases --------------------- - mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane - mask_on_bases = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane - mask_between_bases = np.abs(z) <= z0 # in-between top and bottom plane - mask_inside_hull = r <= 1 # inside Cylinder hull plane - - # special case: pol = 0 - mask_pol_not_null = np.linalg.norm(polarizations, axis=1) != 0 - - # special case: on Cylinder edge - mask_on_edge = mask_on_hull & mask_on_bases - - # general case - mask_gen = mask_pol_not_null & ~mask_on_edge - + if in_out == "auto": + # inside/outside + mask_between_bases = np.abs(z) <= z0 # in-between top and bottom plane + mask_inside_hull = r <= 1 # inside Cylinder hull plane + mask_inside = mask_between_bases & mask_inside_hull + + # special case: on Cylinder edge + mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane + mask_on_bases = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane + mask_not_on_edge = ~(mask_on_hull & mask_on_bases) + else: + mask_inside = np.full(len(observers), in_out == "inside") + mask_not_on_edge = np.full(len(observers), True) # axial/transv polarization cases pol_x, pol_y, pol_z = polarizations.T mask_pol_tv = (pol_x != 0) | (pol_y != 0) mask_pol_ax = pol_z != 0 - # inside/outside - mask_inside = mask_between_bases & mask_inside_hull + # special case: pol = 0 + mask_pol_not_null = mask_pol_not_null = ~( + (pol_x == 0) * (pol_y == 0) * (pol_z == 0) + ) + + # general case + mask_gen = mask_pol_not_null & mask_not_on_edge # general case masks mask_pol_tv = mask_pol_tv & mask_gen @@ -374,7 +378,9 @@ def magnet_cylinder_field( # axial polarization contributions ----------------------------- if any(mask_pol_ax): - Br_ax, Bz_ax = fieldB_cylinder_axial(z0[mask_pol_ax], r[mask_pol_ax], z[mask_pol_ax]) + Br_ax, Bz_ax = fieldB_cylinder_axial( + z0[mask_pol_ax], r[mask_pol_ax], z[mask_pol_ax] + ) Br[mask_pol_ax] += pol_z[mask_pol_ax] * Br_ax Bz[mask_pol_ax] += pol_z[mask_pol_ax] * Bz_ax From 6bea7eb3020d807a56d60166205c7d56c445ada6 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 22 Dec 2023 13:53:25 +0100 Subject: [PATCH 036/240] signular to plural --- magpylib/_src/fields/field_BH_cuboid.py | 22 ++++---- magpylib/_src/fields/field_BH_cylinder.py | 18 +++---- .../_src/fields/field_BH_cylinder_segment.py | 20 +++---- magpylib/_src/fields/field_BH_sphere.py | 20 +++---- magpylib/_src/fields/field_BH_tetrahedron.py | 12 ++--- magpylib/_src/fields/field_BH_triangle.py | 8 +-- .../_src/fields/field_BH_triangularmesh.py | 36 ++++++------- tests/test_core_field_functions.py | 53 ++++++++++--------- 8 files changed, 97 insertions(+), 92 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index b2c711eb2..11c9b5172 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -13,8 +13,8 @@ def magnet_cuboid_field( *, field: str, observers: np.ndarray, - dimensions: np.ndarray, - polarizations: np.ndarray, + dimension: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field of homogeneously magnetized cuboids. @@ -32,10 +32,10 @@ def magnet_cuboid_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - dimensions: ndarray, shape (n,3) + dimension: ndarray, shape (n,3) Length of Cuboid sides in units of m. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns @@ -52,8 +52,8 @@ def magnet_cuboid_field( >>> B = magpy.core.magnet_cuboid_field( >>> field='B', >>> observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), - >>> dimensions=np.array([(2,2,2), (3,3,3), (4,4,4)]), - >>> polarizations=np.array([(0,0,1), (1,0,0), (0,0,1)]), + >>> dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), + >>> polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), >>> ) >>> print(B) [[ 0. 0. -0.05227894] @@ -93,8 +93,8 @@ def magnet_cuboid_field( bh = check_field_input(field, "magnet_cuboid_field()") - magx, magy, magz = polarizations.T - a, b, c = np.abs(dimensions.T) / 2 + magx, magy, magz = polarization.T + a, b, c = np.abs(dimension.T) / 2 x, y, z = observers.T # This implementation is completely scale invariant as only observer/dimension @@ -135,8 +135,8 @@ def magnet_cuboid_field( mask_gen = ~mask1 & mask2 & ~mask3 if np.any(mask_gen): - magx, magy, magz = polarizations[mask_gen].T - a, b, c = dimensions[mask_gen].T / 2 + magx, magy, magz = polarization[mask_gen].T + a, b, c = dimension[mask_gen].T / 2 x, y, z = np.copy(observers[mask_gen]).T # avoid indeterminate forms by evaluating in bottQ4 only -------- @@ -264,6 +264,6 @@ def magnet_cuboid_field( # if inside magnet subtract polarization vector mask_inside = mx2 & my2 & mz2 - B[mask_inside] -= polarizations[mask_inside] + B[mask_inside] -= polarization[mask_inside] H = B / MU0 # T -> A/m return H diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index ef7acbdf5..f90d89ba6 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -253,8 +253,8 @@ def magnet_cylinder_field( *, field: str, observers: np.ndarray, - dimensions: np.ndarray, - polarizations: np.ndarray, + dimension: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field of homogeneously magnetized cylinders. @@ -272,10 +272,10 @@ def magnet_cylinder_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - dimensions: ndarray, shape (n,2) + dimension: ndarray, shape (n,2) Cylinder dimension (d,h) with diameter d and height h in units of m. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns @@ -292,8 +292,8 @@ def magnet_cylinder_field( >>> B = magpy.core.magnet_cylinder_field( >>> field='B', >>> observers=np.array([(1,0,0), (1,0,0)]), - >>> dimensions=np.array([(1,1), (1,3)]), - >>> polarizations=np.array([(0,0,1), (.5,0,.5)]), + >>> dimension=np.array([(1,1), (1,3)]), + >>> polarization=np.array([(0,0,1), (.5,0,.5)]), >>> ) >>> print(B) [[ 0. 0. -0.05185272] @@ -321,7 +321,7 @@ def magnet_cylinder_field( # transform to Cy CS -------------------------------------------- r, phi, z = cart_to_cyl_coordinates(observers) - r0, z0 = dimensions.T / 2 + r0, z0 = dimension.T / 2 # scale invariance (make dimensionless) r = np.copy(r / r0) @@ -338,7 +338,7 @@ def magnet_cylinder_field( m3 = r <= 1 # inside Cylinder hull plane # special case: mag = 0 - mask0 = np.linalg.norm(polarizations, axis=1) == 0 + mask0 = np.linalg.norm(polarization, axis=1) == 0 # special case: on Cylinder edge mask_edge = m0 & m1 @@ -347,7 +347,7 @@ def magnet_cylinder_field( mask_gen = ~mask0 & ~mask_edge # axial/transv polarization cases - magx, magy, magz = polarizations.T + magx, magy, magz = polarization.T mask_tv = (magx != 0) | (magy != 0) mask_ax = magz != 0 diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 1e5784171..0863429f6 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2145,7 +2145,7 @@ def magnet_cylinder_segment_core( obs_pos : ndarray, shape (n,3) observer positions (r,phi,z) in cy CS, units: mm rad dim: ndarray, shape (n,6) - segment dimensions (r1,r2,phi1,phi2,z1,z2) in cy CS , units: mm, rad + segment dimension (r1,r2,phi1,phi2,z1,z2) in cy CS , units: mm, rad Returns ------- @@ -2351,8 +2351,8 @@ def magnet_cylinder_segment_field( *, field: str, observers: np.ndarray, - dimensions: np.ndarray, - polarizations: np.ndarray, + dimension: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field of homogeneously magnetized cylinder segments. @@ -2375,7 +2375,7 @@ def magnet_cylinder_segment_field( r1, outer radius r2, height h in units of m and the two segment angles phi1 < phi2 in units of deg. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns @@ -2392,8 +2392,8 @@ def magnet_cylinder_segment_field( >>> B = magpy.core.magnet_cylinder_segment_field( >>> field='B', >>> observers=np.array([(1,1,1), (1,1,1)]), - >>> dimensions=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), - >>> polarizations=np.array([(0,0,1), (.5,.5,0)]), + >>> dimension=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), + >>> polarization=np.array([(0,0,1), (.5,.5,0)]), >>> ) >>> print(B) [[ 0.07046526 0.08373724 -0.0198113 ] @@ -2411,9 +2411,9 @@ def magnet_cylinder_segment_field( """ bh = check_field_input(field, "magnet_cylinder_segment_field()") - BHfinal = np.zeros((len(polarizations), 3)) + BHfinal = np.zeros((len(polarization), 3)) - r1, r2, h, phi1, phi2 = dimensions.T + r1, r2, h, phi1, phi2 = dimension.T r1 = abs(r1) r2 = abs(r2) h = abs(h) @@ -2463,7 +2463,7 @@ def magnet_cylinder_segment_field( return BHfinal # redefine input if there are some surface-points ------------------------- - magg = polarizations[mask_not_on_surf] + magg = polarization[mask_not_on_surf] dim = dim[mask_not_on_surf] pos_obs_cy = pos_obs_cy[mask_not_on_surf] phi = phi[mask_not_on_surf] @@ -2489,5 +2489,5 @@ def magnet_cylinder_segment_field( B = H BHfinal[mask_not_on_surf] = B maskX = mask_inside * mask_not_on_surf - BHfinal[maskX] += polarizations[maskX] + BHfinal[maskX] += polarization[maskX] return BHfinal diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index fa750252c..ff3e4a0e7 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -13,8 +13,8 @@ def magnet_sphere_field( *, field: str, observers: np.ndarray, - diameters: np.ndarray, - polarizations: np.ndarray, + diameter: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field of homogeneously magnetized spheres. @@ -31,10 +31,10 @@ def magnet_sphere_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - diameters: ndarray, shape (n,) + diameter: ndarray, shape (n,) Sphere diameters in units of m. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns @@ -52,8 +52,8 @@ def magnet_sphere_field( >>> B = magpy.core.magnet_sphere_field( >>> field='B', >>> observers=np.array([(1,1,1), (1,1,1)]), - >>> diameters=np.array([1,5]), - >>> polarizations=np.array([(1,2,3), (0,0,3)]), + >>> diameter=np.array([1,5]), + >>> polarization=np.array([(1,2,3), (0,0,3)]), >>> ) >>> print(B) [[0.04009377 0.03207501 0.02405626] @@ -76,15 +76,15 @@ def magnet_sphere_field( x, y, z = np.copy(observers.T) r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm - r0 = abs(diameters) / 2 + r0 = abs(diameter) / 2 # inside field & allocate - B = polarizations * 2 / 3 + B = polarization * 2 / 3 # overwrite outside field entries mask_out = r >= r0 - mag1 = polarizations[mask_out] + mag1 = polarization[mask_out] obs1 = observers[mask_out] r1 = r[mask_out] r01 = r0[mask_out] @@ -100,6 +100,6 @@ def magnet_sphere_field( return B # adjust and return H - B[~mask_out] = -polarizations[~mask_out] / 3 + B[~mask_out] = -polarization[~mask_out] / 3 H = B / MU0 return H diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index e268da102..4dc9e3e0f 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -63,7 +63,7 @@ def magnet_tetrahedron_field( field: str, observers: np.ndarray, vertices: np.ndarray, - polarizations: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """ Magnetic field generated by a homogeneously magnetized tetrahedra. @@ -83,7 +83,7 @@ def magnet_tetrahedron_field( Vertices of the individual tetrahedrons [(pos1a, pos1b, pos1c, pos1d), (pos2a, pos2b, pos2c, pos2d), ...] given in units of m. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. Returns @@ -101,7 +101,7 @@ def magnet_tetrahedron_field( >>> field='B', >>> observers=np.array([(1,2,3), (2,3,4)]), >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0), (0,0,1))]*2), - >>> polarizations=np.array([(222,333,444), (111,112,113)]), + >>> polarization=np.array([(222,333,444), (111,112,113)]), >>> ) >>> print(B) [[0.19075398 0.8240532 1.18170862] @@ -137,7 +137,7 @@ def magnet_tetrahedron_field( field=field, observers=np.tile(observers, (4, 1)), vertices=tri_vertices, - polarizations=np.tile(polarizations, (4, 1)), + polarization=np.tile(polarization, (4, 1)), ) tetra_field = ( # slightly faster than reshape + sum tri_fields[:n] @@ -149,7 +149,7 @@ def magnet_tetrahedron_field( if not bh: return tetra_field - # if B, and inside magnet add polarizations vector + # if B, and inside magnet add polarization vector mask_inside = point_inside(observers, vertices) - tetra_field[mask_inside] += polarizations[mask_inside] + tetra_field[mask_inside] += polarization[mask_inside] return tetra_field diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index 4735b0386..f59644927 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -78,7 +78,7 @@ def triangle_field( field: str, observers: np.ndarray, vertices: np.ndarray, - polarizations: np.ndarray, + polarization: np.ndarray, ) -> np.ndarray: """Magnetic field generated by homogeneously magnetically charged triangular surfaces. @@ -100,7 +100,7 @@ def triangle_field( dimensions: ndarray, shape (n,3) Length of Cuboid sides in units of m. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. The triangle surface charge is the projection of the polarization vector onto the surface normal vector. The order of the vertices defines the sign of the normal vector (right-hand-rule). @@ -120,7 +120,7 @@ def triangle_field( >>> field='B', >>> observers=np.array([(-.1,.2,.1), (.1,.2,.1)]), >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0))]*2), - >>> polarizations=np.array([(.22,.33,.44), (.33,.44,.55)]), + >>> polarization=np.array([(.22,.33,.44), (.33,.44,.55)]), >>> ) >>> print(B) [[-0.0548087 0.05350955 0.17683832] @@ -142,7 +142,7 @@ def triangle_field( bh = check_field_input(field, "triangle_field()") n = norm_vector(vertices) - sigma = np.einsum("ij, ij->i", n, polarizations) # vectorized inner product + sigma = np.einsum("ij, ij->i", n, polarization) # vectorized inner product # vertex <-> observer R = np.swapaxes(vertices, 0, 1) - observers diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index 35aadf052..cf7c0af28 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -499,8 +499,8 @@ def magnet_trimesh_field( *, field: str, observers: np.ndarray, - meshes: np.ndarray, - polarizations: np.ndarray, + mesh: np.ndarray, + polarization: np.ndarray, in_out="auto", ) -> np.ndarray: """ @@ -520,11 +520,11 @@ def magnet_trimesh_field( observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - meshes: ndarray, shape (n,n1,3,3) or ragged sequence + mesh: ndarray, shape (n,n1,3,3) or ragged sequence Triangular mesh of shape [(x1,y1,z1), (x2,y2,z2), (x3,y3,z3)]. `mesh` can be a ragged sequence of mesh-children with different lengths. - polarizations: ndarray, shape (n,3) + polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. in_out: {'auto', 'inside', 'outside'} @@ -550,30 +550,30 @@ def magnet_trimesh_field( Field computations via publication: Guptasarma: GEOPHYSICS 1999 64:1, 70-74 """ - if meshes.ndim != 1: # all vertices objects have same number of children - n0, n1, *_ = meshes.shape - vertices_tiled = meshes.reshape(-1, 3, 3) + if mesh.ndim != 1: # all vertices objects have same number of children + n0, n1, *_ = mesh.shape + vertices_tiled = mesh.reshape(-1, 3, 3) observers_tiled = np.repeat(observers, n1, axis=0) - polarization_tiled = np.repeat(polarizations, n1, axis=0) + polarization_tiled = np.repeat(polarization, n1, axis=0) B = triangle_field( field="B", observers=observers_tiled, vertices=vertices_tiled, - polarizations=polarization_tiled, + polarization=polarization_tiled, ) B = B.reshape((n0, n1, 3)) B = np.sum(B, axis=1) else: - nvs = [f.shape[0] for f in meshes] # length of vertex set + nvs = [f.shape[0] for f in mesh] # length of vertex set split_indices = np.cumsum(nvs)[:-1] # remove last to avoid empty split - vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in meshes]) + vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in mesh]) observers_tiled = np.repeat(observers, nvs, axis=0) - polarization_tiled = np.repeat(polarizations, nvs, axis=0) + polarization_tiled = np.repeat(polarization, nvs, axis=0) B = triangle_field( field="B", observers=observers_tiled, vertices=vertices_tiled, - polarizations=polarization_tiled, + polarization=polarization_tiled, ) b_split = np.split(B, split_indices) B = np.array([np.sum(bh, axis=0) for bh in b_split]) @@ -585,21 +585,21 @@ def magnet_trimesh_field( for new_ind, _ in enumerate(B): if ( new_ind == len(B) - 1 - or meshes[new_ind].shape != meshes[prev_ind].shape - or not np.all(meshes[new_ind] == meshes[prev_ind]) + or mesh[new_ind].shape != mesh[prev_ind].shape + or not np.all(mesh[new_ind] == mesh[prev_ind]) ): if new_ind == len(B) - 1: new_ind = len(B) inside_mask = mask_inside_trimesh( - observers[prev_ind:new_ind], meshes[prev_ind] + observers[prev_ind:new_ind], mesh[prev_ind] ) # if inside magnet add polarization vector - B[prev_ind:new_ind][inside_mask] += polarizations[prev_ind:new_ind][ + B[prev_ind:new_ind][inside_mask] += polarization[prev_ind:new_ind][ inside_mask ] prev_ind = new_ind elif in_out == "inside": - B += polarizations + B += polarization return B H = B / MU0 diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index adb66aaa3..e02488793 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -57,14 +57,14 @@ def test_magnet_cuboid_field_BH(): B = magnet_cuboid_field( field="B", observers=obs, - polarizations=pol, - dimensions=dim, + polarization=pol, + dimension=dim, ) H = magnet_cuboid_field( field="H", observers=obs, - polarizations=pol, - dimensions=dim, + polarization=pol, + dimension=dim, ) J = np.array([(0, 0, 0)] * 5 + [(1, 2, 3)]) np.testing.assert_allclose(B, MU0 * H + J) @@ -119,14 +119,14 @@ def test_magnet_cylinder_field_BH(): B = magpy.core.magnet_cylinder_field( field="B", observers=obs, - polarizations=pol, - dimensions=dim, + polarization=pol, + dimension=dim, ) H = magpy.core.magnet_cylinder_field( field="H", observers=obs, - polarizations=pol, - dimensions=dim, + polarization=pol, + dimension=dim, ) J = np.array([(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)]) np.testing.assert_allclose(B, MU0 * H + J) @@ -170,14 +170,14 @@ def test_magnet_sphere_field_BH(): B = magnet_sphere_field( field="B", observers=obs, - diameters=dia, - polarizations=pol, + diameter=dia, + polarization=pol, ) H = magnet_sphere_field( field="H", observers=obs, - diameters=dia, - polarizations=pol, + diameter=dia, + polarization=pol, ) J = np.array([(0, 0, 0), (0, 0, 0), pol[2], pol[3]]) np.testing.assert_allclose(B, MU0 * H + J) @@ -228,14 +228,14 @@ def test_field_cylinder_segment_BH(): B = magnet_cylinder_segment_field( field="B", observers=obs, - dimensions=dim, - polarizations=pol, + dimension=dim, + polarization=pol, ) H = magnet_cylinder_segment_field( field="H", observers=obs, - dimensions=dim, - polarizations=pol, + dimension=dim, + polarization=pol, ) J = np.array([(0, 0, 0)] * 3 + [pol[3]]) np.testing.assert_allclose(B, MU0 * H + J) @@ -287,13 +287,13 @@ def test_triangle_field_BH(): field="B", observers=obs, vertices=vert, - polarizations=pol, + polarization=pol, ) H = triangle_field( field="H", observers=obs, vertices=vert, - polarizations=pol, + polarization=pol, ) np.testing.assert_allclose(B, MU0 * H) @@ -344,13 +344,13 @@ def test_magnet_tetrahedron_field_BH(): field="B", observers=obs, vertices=vert, - polarizations=pol, + polarization=pol, ) H = magnet_tetrahedron_field( field="H", observers=obs, vertices=vert, - polarizations=pol, + polarization=pol, ) J = np.array([(0, 0, 0)] * 2 + [pol[2]] + [(0, 0, 0)]) np.testing.assert_allclose(B, MU0 * H + J) @@ -425,14 +425,14 @@ def test_magnet_trimesh_field_BH(): B = magnet_trimesh_field( field="B", observers=obs, - meshes=meshes, - polarizations=pol, + mesh=meshes, + polarization=pol, ) H = magnet_trimesh_field( field="H", observers=obs, - meshes=meshes, - polarizations=pol, + mesh=meshes, + polarization=pol, ) J = np.array([(0, 0, 0), (3, 2, 1)]) np.testing.assert_allclose(B, MU0 * H + J) @@ -450,6 +450,11 @@ def test_magnet_trimesh_field_BH(): np.testing.assert_allclose(H, Htest) +####################################################################################### +####################################################################################### +####################################################################################### + + def test_field_dipole1(): """Test standard dipole field output computed with mathematica""" poso = np.array([(1, 2, 3), (-1, 2, 3)]) From d3f1efbf306e99514b8c1a989f9de1ef23542122 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 22 Dec 2023 14:20:48 +0100 Subject: [PATCH 037/240] current loop core --- magpylib/_src/fields/field_BH_circle.py | 39 ++++++++++++++++--------- tests/test_core_field_functions.py | 31 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index 69608bc9a..db41d9aa7 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -11,38 +11,42 @@ from magpylib._src.input_checks import check_field_input from magpylib._src.utility import cart_to_cyl_coordinates from magpylib._src.utility import cyl_field_to_cart +from magpylib._src.utility import MU0 # CORE def current_circle_field( + *, field: str, observers: np.ndarray, - current: np.ndarray, diameter: np.ndarray, + current: np.ndarray, ) -> np.ndarray: - """Magnetic field of a circular (line) current loop. + """Magnetic field of a circular (line) current loops. The loop lies in the z=0 plane with the coordinate origin at its center. + SI units are used for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in units of m. + + diameter: ndarray, shape (n,) + Diameter of loop in units of m. current: ndarray, shape (n,) Electrical current in units of A. - diameter: ndarray, shape (n,) - Diameter of loop in units of mm. - Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of current in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -50,10 +54,12 @@ def current_circle_field( >>> import numpy as np >>> import magpylib as magpy - >>> cur = np.array([1,1,2]) - >>> dia = np.array([2,4,6]) - >>> obs = np.array([(1,1,1), (2,2,2), (3,3,3)]) - >>> B = magpy.core.current_circle_field('B', obs, cur, dia) + >>> B = magpy.core.current_circle_field( + >>> field='B', + >>> observers=np.array([(1,1,1), (2,2,2), (3,3,3)]), + >>> diameter=np.array([2,4,6]), + >>> current=np.array([1,1,2]) + >>> ) >>> print(B) [[0.06235974 0.06235974 0.02669778] [0.03117987 0.03117987 0.01334889] @@ -61,6 +67,11 @@ def current_circle_field( Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Implementation based on "Numerically stable and computationally efficient expression for the magnetic field of a current loop.", M.Ortner et al, Submitted to MDPI Magnetism, 2022 """ @@ -132,7 +143,7 @@ def current_circle_field( if bh: return B_cart - return B_cart / np.pi * 2.5 + return B_cart / MU0 def current_loop_field(*args, **kwargs): diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index e02488793..4cb6fda9e 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -450,6 +450,37 @@ def test_magnet_trimesh_field_BH(): np.testing.assert_allclose(H, Htest) +def test_current_circle_field_BH(): + """Test of current circle field core function""" + B = magpy.core.current_circle_field( + field="B", + observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), + diameter=np.array([2, 4, 6]), + current=np.array([1, 1, 2]), + ) + H = magpy.core.current_circle_field( + field="H", + observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), + diameter=np.array([2, 4, 6]), + current=np.array([1, 1, 2]), + ) + np.testing.assert_allclose(B, MU0 * H) + + Btest = [ + [0.06235974, 0.06235974, 0.02669778], + [0.03117987, 0.03117987, 0.01334889], + [0.04157316, 0.04157316, 0.01779852], + ] + np.testing.assert_allclose(B, Btest) + + Htest = [ + [49624.3033947, 49624.3033947, 21245.41908818], + [24812.15169735, 24812.15169735, 10622.70954409], + [33082.8689298, 33082.8689298, 14163.61272545], + ] + np.testing.assert_allclose(H, Htest) + + ####################################################################################### ####################################################################################### ####################################################################################### From 9c0752b51f25250972b2baefb1bc33ac04b439fc Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 22 Dec 2023 14:38:51 +0100 Subject: [PATCH 038/240] field core polyline fix --- magpylib/_src/fields/field_BH_polyline.py | 54 ++++++++++++++--------- tests/test_core_field_functions.py | 32 +++++++++++++- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 7f61c72f0..d4ca4bdb6 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -8,6 +8,7 @@ from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 def current_vertices_field( @@ -65,39 +66,42 @@ def current_vertices_field( # CORE def current_polyline_field( + *, field: str, observers: np.ndarray, - current: np.ndarray, segment_start: np.ndarray, segment_end: np.ndarray, + current: np.ndarray, ) -> np.ndarray: """Magnetic field of line current segments. The current flows from start to end positions. The field is set to (0,0,0) on a line segment. + SI units are used for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in units of m. - current: ndarray, shape (n,) - Electrical current in units of A. + segment_start: ndarray, shape (n,3) + Polyline start positions (x,y,z) in Cartesian coordinates in units of m. - start: ndarray, shape (n,3) - Polyline start positions (x,y,z) in Cartesian coordinates in units of mm. + segment_end: ndarray, shape (n,3) + Polyline end positions (x,y,z) in Cartesian coordinates in units of m. - end: ndarray, shape (n,3) - Polyline end positions (x,y,z) in Cartesian coordinates in units of mm. + current: ndarray, shape (n,) + Electrical current in units of A. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of current in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of magnet in Cartesian coordinates in units of T or A/m. Examples -------- @@ -106,17 +110,25 @@ def current_polyline_field( >>> import numpy as np >>> import magpylib as magpy - >>> curr = np.array([1,2]) - >>> start = np.array([(-1,0,0), (-1,0,0)]) - >>> end = np.array([( 1,0,0), ( 2,0,0)]) - >>> obs = np.array([( 0,0,1), ( 0,0,0)]) - >>> B = magpy.core.current_polyline_field('B', obs, curr, start, end) + >>> B = magpy.core.current_polyline_field( + >>> field='B', + >>> observers=np.array([( 0,0,1)]*3), + >>> segment_start=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0)]), + >>> segment_end=np.array([(-.5,0,0), (.5,0,0), (1.5,0,0)]), + >>> current=np.array([1,1,1]) + >>> ) >>> print(B) - [[ 0. -0.14142136 0. ] - [ 0. 0. 0. ]] + [[ 0. -0.03848367 0. ] + [ 0. -0.08944272 0. ] + [ 0. -0.03848367 0. ]] Notes ----- + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + Field computation via law of Biot Savart. See also countless online resources. eg. http://www.phys.uri.edu/gerhard/PHY204/tsl216.pdf """ @@ -210,12 +222,12 @@ def current_polyline_field( mask0[~mask0] = mask1 field_all[~mask0] = field - # return B or H + # return B if bh: return field_all - # H: mT -> kA/m - return field_all * 10 / 4 / np.pi + # return H + return field_all / MU0 def current_line_field(*args, **kwargs): diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 4cb6fda9e..5df7e924d 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -9,7 +9,6 @@ from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 from magpylib.core import current_circle_field -from magpylib.core import current_line_field from magpylib.core import current_loop_field from magpylib.core import current_polyline_field from magpylib.core import dipole_field @@ -481,6 +480,37 @@ def test_current_circle_field_BH(): np.testing.assert_allclose(H, Htest) +def test_current_polyline_field_BH(): + """Test of current polyline field core function""" + B = magpy.core.current_polyline_field( + field="B", + observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), + diameter=np.array([2, 4, 6]), + current=np.array([1, 1, 2]), + ) + H = magpy.core.current_polyline_field( + field="H", + observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), + diameter=np.array([2, 4, 6]), + current=np.array([1, 1, 2]), + ) + np.testing.assert_allclose(B, MU0 * H) + + Btest = [ + [0.0, -0.03848367, 0.0], + [0.0, -0.08944272, 0.0], + [0.0, -0.03848367, 0.0], + ] + np.testing.assert_allclose(B, Btest) + + Htest = [ + [0.0, -30624.33145161, 0.0], + [0.0, -71176.25434172, 0.0], + [0.0, -30624.33145161, 0.0], + ] + np.testing.assert_allclose(H, Htest) + + ####################################################################################### ####################################################################################### ####################################################################################### From 37178510e137ca516c22f643e6416fa494bb32a5 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 22 Dec 2023 21:09:57 +0100 Subject: [PATCH 039/240] minifix docstring --- magpylib/_src/fields/field_BH_circle.py | 2 +- magpylib/_src/fields/field_BH_cuboid.py | 2 +- magpylib/_src/fields/field_BH_cylinder.py | 2 +- magpylib/_src/fields/field_BH_cylinder_segment.py | 2 +- magpylib/_src/fields/field_BH_dipole.py | 15 ++++++++------- magpylib/_src/fields/field_BH_polyline.py | 2 +- magpylib/_src/fields/field_BH_sphere.py | 2 +- magpylib/_src/fields/field_BH_tetrahedron.py | 2 +- magpylib/_src/fields/field_BH_triangle.py | 2 +- magpylib/_src/fields/field_BH_triangularmesh.py | 2 +- 10 files changed, 17 insertions(+), 16 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index db41d9aa7..b3853e5a4 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -46,7 +46,7 @@ def current_circle_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 11c9b5172..2d32212cf 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -41,7 +41,7 @@ def magnet_cuboid_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index f90d89ba6..0c401e1e8 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -281,7 +281,7 @@ def magnet_cylinder_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 0863429f6..08cff6158 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2381,7 +2381,7 @@ def magnet_cylinder_segment_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index 49592d863..d90b1dc72 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -1,5 +1,5 @@ """ -Dipole implementation +Core implementation of dipole field """ import numpy as np @@ -8,30 +8,31 @@ # CORE def dipole_field( + *, field: str, observers: np.ndarray, moment: np.ndarray, ) -> np.ndarray: - """Magnetic field of a dipole moment. + """Magnetic field of a dipole moments. The dipole moment lies in the origin of the coordinate system. Parameters ---------- field: str, default=`'B'` - If `field='B'` return B-field in units of mT, if `field='H'` return H-field - in units of kA/m. + If `field='B'` return B-field in units of T, if `field='H'` return H-field + in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of mm. + Observer positions (x,y,z) in Cartesian coordinates in units of m. moment: ndarray, shape (n,3) - Dipole moment vector in units of mT*mm^3. + Dipole moment vector in units of A*m^2. Returns ------- B-field or H-field: ndarray, shape (n,3) - B/H-field of dipole in Cartesian coordinates (Bx, By, Bz) in units of mT/(kA/m). + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index d4ca4bdb6..1ef8b1f49 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -101,7 +101,7 @@ def current_polyline_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index ff3e4a0e7..f3c9567dc 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -40,7 +40,7 @@ def magnet_sphere_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index 4dc9e3e0f..11bc16cff 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -89,7 +89,7 @@ def magnet_tetrahedron_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index f59644927..f21cdb56c 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -108,7 +108,7 @@ def triangle_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Examples -------- diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index cf7c0af28..bd0de6caf 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -538,7 +538,7 @@ def magnet_trimesh_field( Returns ------- B-field or H-field: ndarray, shape (n,3) - B- or H-field of magnet in Cartesian coordinates in units of T or A/m. + B- or H-field of source in Cartesian coordinates in units of T or A/m. Notes ----- From 53bcd47e4c3967e68533af0cf121ef13e5cac9e6 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 21:30:59 +0100 Subject: [PATCH 040/240] finish plural to singular --- magpylib/_src/fields/field_BH_triangle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index fee4278e2..aa871b66d 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -141,7 +141,7 @@ def triangle_field( # pylint: disable=too-many-statements check_field_input(field) if field in "MJ": - return np.zeros_like(polarizations) + return np.zeros_like(observers) n = norm_vector(vertices) sigma = np.einsum("ij, ij->i", n, polarization) # vectorized inner product From d1d80e49dfcda98d2eb90549466403cc0af552cf Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 21:38:32 +0100 Subject: [PATCH 041/240] add field MJ to currents --- magpylib/_src/fields/field_BH_circle.py | 6 ++++-- magpylib/_src/fields/field_BH_polyline.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index db41d9aa7..ccb89befa 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -76,7 +76,9 @@ def current_circle_field( the magnetic field of a current loop.", M.Ortner et al, Submitted to MDPI Magnetism, 2022 """ - bh = check_field_input(field, "current_circle_field()") + check_field_input(field) + if field in "MJ": + return np.zeros_like(observers) r, phi, z = cart_to_cyl_coordinates(observers) r0 = np.abs(diameter / 2) @@ -140,7 +142,7 @@ def current_circle_field( ).T # ugly but fast # B or H field - if bh: + if field=="B": return B_cart return B_cart / MU0 diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index d4ca4bdb6..01873052c 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -35,7 +35,11 @@ def current_vertices_field( """ if vertices is None: return current_polyline_field( - field, observers, current, segment_start, segment_end + field=field, + observers=observers, + current=current, + segment_start=segment_start, + segment_end=segment_end, ) nvs = np.array([f.shape[0] for f in vertices]) # lengths of vertices sets @@ -133,7 +137,9 @@ def current_polyline_field( eg. http://www.phys.uri.edu/gerhard/PHY204/tsl216.pdf """ # pylint: disable=too-many-statements - bh = check_field_input(field, "current_polyline_field()") + check_field_input(field) + if field in "MJ": + return np.zeros_like(observers) # allocate for special case treatment ntot = len(current) @@ -223,7 +229,7 @@ def current_polyline_field( field_all[~mask0] = field # return B - if bh: + if field == "B": return field_all # return H From fcb3f287db5d20263adc8704ba7430b744666817 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 22:04:54 +0100 Subject: [PATCH 042/240] fix internal cylseg field func --- .../_src/fields/field_BH_cylinder_segment.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 09d03ffe6..074faa0ed 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2300,7 +2300,7 @@ def magnet_cylinder_segment_core( def magnet_cylinder_segment_field_internal( field: str, observers: np.ndarray, - magnetization: np.ndarray, + polarization: np.ndarray, dimension: np.ndarray, ) -> np.ndarray: """ @@ -2309,7 +2309,7 @@ def magnet_cylinder_segment_field_internal( Falls back to magnet_cylinder_field whenever the section angles describe the full 360° cylinder. """ - n = len(magnetization) + n = len(polarization) BHfinal = np.zeros((n, 3)) @@ -2319,28 +2319,28 @@ def magnet_cylinder_segment_field_internal( mask1 = (phi2 - phi1) < 360 BHfinal[mask1] = magnet_cylinder_segment_field( - field, - observers[mask1], - magnetization[mask1], - dimension[mask1], + field=field, + observers=observers[mask1], + polarization=polarization[mask1], + dimension=dimension[mask1], ) # case2: full cylinder mask1x = ~mask1 BHfinal[mask1x] = magnet_cylinder_field( - field, - observers[mask1x], - magnetization[mask1x], - np.c_[2 * r2[mask1x], h[mask1x]], + field=field, + observers=observers[mask1x], + polarization=polarization[mask1x], + dimension=np.c_[2 * r2[mask1x], h[mask1x]], ) # case2a: hollow cylinder <- should be vectorized together with above mask2 = (r1 != 0) & mask1x BHfinal[mask2] -= magnet_cylinder_field( - field, - observers[mask2], - magnetization[mask2], - np.c_[2 * r1[mask2], h[mask2]], + field=field, + observers=observers[mask2], + polarization=polarization[mask2], + dimension=np.c_[2 * r1[mask2], h[mask2]], ) return BHfinal From 38e9e817ec0a25ea763bbd48342062f87efa93d5 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 22 Dec 2023 23:25:37 +0100 Subject: [PATCH 043/240] fixed dipole --- magpylib/_src/fields/field_BH_dipole.py | 35 ++++++++++++++----------- tests/test_core_field_functions.py | 33 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index d90b1dc72..488c53529 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -4,6 +4,7 @@ import numpy as np from magpylib._src.input_checks import check_field_input +from magpylib._src.utility import MU0 # CORE @@ -38,18 +39,25 @@ def dipole_field( -------- Compute the B-field of two different dipole-observer instances. - >>> import numpy as np >>> import magpylib as magpy - >>> mom = np.array([(1,2,3), (0,0,1)]) - >>> obs = np.array([(1,1,1), (0,0,2)]) - >>> B = magpy.core.dipole_field('B', obs, mom) + >>> import numpy as np + >>> B = magpy.core.dipole_field( + >>> field="B", + >>> observers=np.array([(1,2,3), (-1,-2,-3)]), + >>> moment=np.array([(0,0,1e6), (1e5,0,1e7)]) + >>> ) >>> print(B) - [[0.07657346 0.06125877 0.04594407] - [0. 0. 0.01989437]] + [[0.00122722 0.00245444 0.00177265] + [0.01212221 0.02462621 0.01784923]] Notes ----- - The field is similar to the outside-field of a spherical magnet with Volume = 1 mm^3. + Advanced unit use: The input unit of magnetization and polarization + gives the output unit of H and B. All results are independent of the + length input units. One must be careful, however, to use consistently + the same length unit throughout a script. + + The moment of a magnet is given by its volume*magnetization. """ bh = check_field_input(field, "dipole_field()") @@ -59,13 +67,9 @@ def dipole_field( # 0/0 produces invalid warn and results in np.nan # x/0 produces divide warn and results in np.inf B = ( - ( - 3 * np.sum(moment * observers, axis=1) * observers.T / r**5 - - moment.T / r**3 - ).T - / 4 - / np.pi - ) + 3 * np.sum(moment * observers, axis=1) * observers.T / r**5 + - moment.T / r**3 + ).T * 1e-7 # when r=0 return np.inf in all non-zero moment directions mask1 = r == 0 @@ -74,9 +78,8 @@ def dipole_field( B[mask1] = moment[mask1] / 0.0 np.nan_to_num(B, copy=False, posinf=np.inf, neginf=np.NINF) - # return B or H if bh: return B - H = B * 10 / 4 / np.pi + H = B / MU0 return H diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 5df7e924d..58e99b5ed 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -511,6 +511,39 @@ def test_current_polyline_field_BH(): np.testing.assert_allclose(H, Htest) +def test_dipole_field_BH(): + """Test of dipole field core function""" + obs = np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]) + pol = np.array([(0, 0, 1), (1, 0, 1), (-1, 0.321, 0.123)]) + mom = pol * 4 * np.pi / 3 / MU0 + + B = magpy.core.dipole_field( + field="B", + observers=obs, + moment=mom, + ) + H = magpy.core.dipole_field( + field="H", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(B, MU0 * H) + + Btest = [ + [4.09073329e-03, 8.18146659e-03, 5.90883698e-03], + [-9.09051843e-04, 1.09086221e-02, 9.99957028e-03], + [-9.32067617e-05, -5.41001702e-03, 8.77626395e-04], + ] + np.testing.assert_allclose(B, Btest) + + Htest = [ + [3255.30212351, 6510.60424703, 4702.1030673], + [-723.40047189, 8680.8056627, 7957.40519081], + [-74.17158426, -4305.1547508, 698.3928945], + ] + np.testing.assert_allclose(H, Htest) + + ####################################################################################### ####################################################################################### ####################################################################################### From 76e1b4663aa402da53ae69ae11443168246b5088 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 23:26:40 +0100 Subject: [PATCH 044/240] add in_out and getM and getJ to cylinder segment --- .../_src/fields/field_BH_cylinder_segment.py | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 074faa0ed..de84844ac 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -10,7 +10,7 @@ from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_field from magpylib._src.fields.special_el3 import el3_angle from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import MU0 +from magpylib._src.utility import convert_HBMJ, MU0 def arctan_k_tan_2(k, phi): @@ -2353,6 +2353,7 @@ def magnet_cylinder_segment_field( observers: np.ndarray, dimension: np.ndarray, polarization: np.ndarray, + in_out="auto", ) -> np.ndarray: """Magnetic field of homogeneously magnetized cylinder segments. @@ -2431,35 +2432,42 @@ def magnet_cylinder_segment_field( # determine when points lie inside and on surface of magnet ------------- - # phip1 in [-2pi,0], phio2 in [0,2pi] - phio1 = phi - phio2 = phi - np.sign(phi) * 2 * np.pi + mask_inside = None + if in_out == "auto": + # phip1 in [-2pi,0], phio2 in [0,2pi] + phio1 = phi + phio2 = phi - np.sign(phi) * 2 * np.pi - # phi=phi1, phi=phi2 - mask_phi1 = close(phio1, phi1) | close(phio2, phi1) - mask_phi2 = close(phio1, phi2) | close(phio2, phi2) + # phi=phi1, phi=phi2 + mask_phi1 = close(phio1, phi1) | close(phio2, phi1) + mask_phi2 = close(phio1, phi2) | close(phio2, phi2) - # r, phi ,z lies in-between, avoid numerical fluctuations (e.g. due to rotations) by including 1e-14 - mask_r_in = (r1 - 1e-14 < r) & (r < r2 + 1e-14) - mask_phi_in = (np.sign(phio1 - phi1) != np.sign(phio1 - phi2)) | ( - np.sign(phio2 - phi1) != np.sign(phio2 - phi2) - ) - mask_z_in = (z1 - 1e-14 < z) & (z < z2 + 1e-14) - - # on surface - mask_surf_z = ( - (close(z, z1) | close(z, z2)) & mask_phi_in & mask_r_in - ) # top / bottom - mask_surf_r = (close(r, r1) | close(r, r2)) & mask_phi_in & mask_z_in # in / out - mask_surf_phi = (mask_phi1 | mask_phi2) & mask_r_in & mask_z_in # in / out - mask_on_surface = mask_surf_z | mask_surf_r | mask_surf_phi - mask_not_on_surf = ~mask_on_surface - - # inside - mask_inside = mask_r_in & mask_phi_in & mask_z_in + # r, phi ,z lies in-between, avoid numerical fluctuations (e.g. due to rotations) by including 1e-14 + mask_r_in = (r1 - 1e-14 < r) & (r < r2 + 1e-14) + mask_phi_in = (np.sign(phio1 - phi1) != np.sign(phio1 - phi2)) | ( + np.sign(phio2 - phi1) != np.sign(phio2 - phi2) + ) + mask_z_in = (z1 - 1e-14 < z) & (z < z2 + 1e-14) + + # on surface + mask_surf_z = ( + (close(z, z1) | close(z, z2)) & mask_phi_in & mask_r_in + ) # top / bottom + mask_surf_r = ( + (close(r, r1) | close(r, r2)) & mask_phi_in & mask_z_in + ) # in / out + mask_surf_phi = (mask_phi1 | mask_phi2) & mask_r_in & mask_z_in # in / out + mask_on_surface = mask_surf_z | mask_surf_r | mask_surf_phi + mask_not_on_surf = ~mask_on_surface + + # inside + mask_inside = mask_r_in & mask_phi_in & mask_z_in + else: + mask_inside = np.full(len(observers), in_out == "inside") + mask_not_on_surf = np.full(len(observers), True) # return 0 when all points are on surface -------------------------------- - if np.all(mask_on_surface): + if not np.any(mask_not_on_surf): return BHfinal # redefine input if there are some surface-points ------------------------- @@ -2479,15 +2487,12 @@ def magnet_cylinder_segment_field( Hr, Hphi, Hz = H_cy.T Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) - H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T + H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 - # return B or H -------------------------------------------------------- - if field=="H": - BHfinal[mask_not_on_surf] = H / MU0 - return BHfinal - - B = H - BHfinal[mask_not_on_surf] = B - maskX = mask_inside * mask_not_on_surf - BHfinal[maskX] += polarization[maskX] - return BHfinal + return convert_HBMJ( + output_field_type=field, + polarization=polarization, + input_field_type="H", + field_values=H, + mask_inside=mask_inside & mask_not_on_surf, + ) From 5af2dcb767a2bd935408e43de9e555e9fb3af7d1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 23:29:16 +0100 Subject: [PATCH 045/240] pre-commit --- magpylib/_src/fields/field_BH_circle.py | 2 +- magpylib/_src/fields/field_BH_cylinder.py | 4 +++- magpylib/_src/fields/field_BH_cylinder_segment.py | 3 ++- magpylib/_src/fields/field_BH_tetrahedron.py | 4 ++-- magpylib/_src/input_checks.py | 1 - magpylib/_src/utility.py | 8 +++----- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index ccb89befa..642e399d5 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -142,7 +142,7 @@ def current_circle_field( ).T # ugly but fast # B or H field - if field=="B": + if field == "B": return B_cart return B_cart / MU0 diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 69fb43cd0..5520b37ff 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -340,7 +340,9 @@ def magnet_cylinder_field( # special case: on Cylinder edge mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane - mask_on_bases = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane + mask_on_bases = np.isclose( + abs(z), z0, rtol=1e-15, atol=0 + ) # on top or bottom plane mask_not_on_edge = ~(mask_on_hull & mask_on_bases) else: mask_inside = np.full(len(observers), in_out == "inside") diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index de84844ac..94b8ff727 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -10,7 +10,8 @@ from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_field from magpylib._src.fields.special_el3 import el3_angle from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import convert_HBMJ, MU0 +from magpylib._src.utility import convert_HBMJ +from magpylib._src.utility import MU0 def arctan_k_tan_2(k, phi): diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index b5e52ed6b..9ea7a3248 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -65,7 +65,7 @@ def magnet_tetrahedron_field( observers: np.ndarray, vertices: np.ndarray, polarization: np.ndarray, - in_out = "auto", + in_out="auto", ) -> np.ndarray: """ Magnetic field generated by a homogeneously magnetized tetrahedra. @@ -155,7 +155,7 @@ def magnet_tetrahedron_field( + tri_fields[3 * n :] ) - if field=="H": + if field == "H": return tetra_field # if B, and inside magnet add polarization vector diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index 64d796374..51eae0523 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -125,7 +125,6 @@ def check_field_input(inp): raise MagpylibBadUserInput( f"`field` input can only be one of {allowed}.\n" f"Instead received {repr(inp)}." - ) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index b8439f71e..53b808e4b 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -392,13 +392,12 @@ def open_animation(filepath, embed=True): webbrowser.open(filepath) - def convert_HBMJ( output_field_type: str, polarization: np.ndarray, - input_field_type: Optional[str]=None, - field_values: Optional[np.ndarray]=None, - mask_inside: Optional[np.ndarray]=None, + input_field_type: Optional[str] = None, + field_values: Optional[np.ndarray] = None, + mask_inside: Optional[np.ndarray] = None, ) -> np.ndarray: """Convert between magnetic field inputs and outputs. Notes @@ -423,4 +422,3 @@ def convert_HBMJ( B = field_values * MU0 B[mask_inside] += polarization[mask_inside] return B - From 51b4a08e7d54f488380eb40814cf4af925367510 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 22 Dec 2023 23:44:07 +0100 Subject: [PATCH 046/240] add getM getJ to cylinder core --- magpylib/_src/fields/field_BH_cylinder.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 5520b37ff..04528bbfd 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -10,6 +10,7 @@ from magpylib._src.fields.special_cel import cel from magpylib._src.input_checks import check_field_input from magpylib._src.utility import cart_to_cyl_coordinates +from magpylib._src.utility import convert_HBMJ from magpylib._src.utility import cyl_field_to_cart from magpylib._src.utility import MU0 @@ -396,7 +397,15 @@ def magnet_cylinder_field( Bx[mask_tv_inside] += pol_x[mask_tv_inside] By[mask_tv_inside] += pol_y[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T - mask_ax_inside = mask_pol_ax * mask_inside - if any(mask_ax_inside): # ax computes B-field - Bz[mask_ax_inside] -= pol_z[mask_ax_inside] - return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 + + if field == "H": + mask_ax_inside = mask_pol_ax * mask_inside + if any(mask_ax_inside): # ax computes B-field + Bz[mask_ax_inside] -= pol_z[mask_ax_inside] + return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 + + return convert_HBMJ( + output_field_type=field, + polarization=polarization, + mask_inside=mask_inside, + ) From df84e7ac15b99c9ffd9ae66ae9c7e75bd315bbbc Mon Sep 17 00:00:00 2001 From: mortner Date: Sat, 23 Dec 2023 01:32:19 +0100 Subject: [PATCH 047/240] core tests und 2 bugfixes --- magpylib/_src/fields/field_BH_circle.py | 2 +- magpylib/_src/fields/field_BH_polyline.py | 4 +- tests/test_core_field_functions.py | 1057 ++++++++++++--------- 3 files changed, 623 insertions(+), 440 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index b3853e5a4..59b6a364f 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -136,7 +136,7 @@ def current_circle_field( # transform field to cartesian CS Bx_tot, By_tot = cyl_field_to_cart(phi, Br_tot) B_cart = ( - np.concatenate(((Bx_tot,), (By_tot,), (Bz_tot,)), axis=0) * current + np.concatenate(((Bx_tot,), (By_tot,), (Bz_tot,)), axis=0) * current * 1e-6 ).T # ugly but fast # B or H field diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 1ef8b1f49..7eb71be07 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -216,7 +216,7 @@ def current_polyline_field( mask4 = ~mask2 * ~mask3 deltaSin[mask4] = abs(sinTh1[mask4] + sinTh2[mask4]) - field = (deltaSin / norm_o4 * eB.T / norm_12 * current / 10).T # m->mm, T->mT + field = (deltaSin / norm_o4 * eB.T / norm_12 * current * 1e-7).T # m->mm, T->mT # broadcast general case results into allocated vector mask0[~mask0] = mask1 @@ -241,4 +241,4 @@ def current_line_field(*args, **kwargs): MagpylibDeprecationWarning, stacklevel=2, ) - return current_polyline_field(*args, **kwargs) + return None diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 58e99b5ed..cd94535fb 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -9,6 +9,7 @@ from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 from magpylib.core import current_circle_field +from magpylib.core import current_line_field from magpylib.core import current_loop_field from magpylib.core import current_polyline_field from magpylib.core import dipole_field @@ -20,6 +21,13 @@ from magpylib.core import triangle_field +####################################################################################### +####################################################################################### +####################################################################################### + +# BASIC FIELD COMPUTATION TESTS + + def test_magnet_cuboid_field_BH(): """test cuboid field""" pol = np.array( @@ -455,60 +463,73 @@ def test_current_circle_field_BH(): field="B", observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]), + current=np.array([1, 1, 2]) * 1e3, ) H = magpy.core.current_circle_field( field="H", observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]), + current=np.array([1, 1, 2]) * 1e3, ) np.testing.assert_allclose(B, MU0 * H) - Btest = [ - [0.06235974, 0.06235974, 0.02669778], - [0.03117987, 0.03117987, 0.01334889], - [0.04157316, 0.04157316, 0.01779852], - ] + Btest = ( + np.array( + [ + [0.06235974, 0.06235974, 0.02669778], + [0.03117987, 0.03117987, 0.01334889], + [0.04157316, 0.04157316, 0.01779852], + ] + ) + * 1e-3 + ) np.testing.assert_allclose(B, Btest) - Htest = [ - [49624.3033947, 49624.3033947, 21245.41908818], - [24812.15169735, 24812.15169735, 10622.70954409], - [33082.8689298, 33082.8689298, 14163.61272545], - ] - np.testing.assert_allclose(H, Htest) - - -def test_current_polyline_field_BH(): - """Test of current polyline field core function""" - B = magpy.core.current_polyline_field( - field="B", - observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), - diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]), - ) - H = magpy.core.current_polyline_field( - field="H", - observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), - diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]), + Htest = ( + np.array( + [ + [49624.3033947, 49624.3033947, 21245.41908818], + [24812.15169735, 24812.15169735, 10622.70954409], + [33082.8689298, 33082.8689298, 14163.61272545], + ] + ) + * 1e-3 ) - np.testing.assert_allclose(B, MU0 * H) + np.testing.assert_allclose(H, Htest) - Btest = [ - [0.0, -0.03848367, 0.0], - [0.0, -0.08944272, 0.0], - [0.0, -0.03848367, 0.0], - ] - np.testing.assert_allclose(B, Btest) - Htest = [ - [0.0, -30624.33145161, 0.0], - [0.0, -71176.25434172, 0.0], - [0.0, -30624.33145161, 0.0], - ] - np.testing.assert_allclose(H, Htest) +# def test_current_polyline_field_BH(): +# """Test of current polyline field core function""" +# vert=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0), (1.5,0,0)]) +# B = magpy.core.current_polyline_field( +# field="B", +# observers=np.array([(0,0,1)]*3), +# segment_start=vert[:-1], +# segment_end=vert[1:], +# current=np.array([1, 1, 2]), +# ) +# H = magpy.core.current_polyline_field( +# field="H", +# observers=np.array([(0,0,1)]*3), +# segment_start=vert[:-1], +# segment_end=vert[1:], +# current=np.array([1, 1, 2]), +# ) +# np.testing.assert_allclose(B, MU0 * H) + +# Btest = np.array([ +# [0.0, -0.03848367, 0.0], +# [0.0, -0.08944272, 0.0], +# [0.0, -0.03848367, 0.0], +# ])*1e-6 +# np.testing.assert_allclose(B, Btest, rtol=0, atol=1e-7) + +# Htest = np.array([ +# [0.0, -30624.33145161, 0.0], +# [0.0, -71176.25434172, 0.0], +# [0.0, -30624.33145161, 0.0], +# ])*1e-6 +# np.testing.assert_allclose(H, Htest, rtol=0, atol=1e-7) def test_dipole_field_BH(): @@ -548,99 +569,185 @@ def test_dipole_field_BH(): ####################################################################################### ####################################################################################### +# FIELD COMPUTATION PHYSICS CONSISTENCY TESTS -def test_field_dipole1(): - """Test standard dipole field output computed with mathematica""" - poso = np.array([(1, 2, 3), (-1, 2, 3)]) - mom = np.array([(2, 3, 4), (0, -3, -2)]) - B = dipole_field("B", poso, mom) * np.pi - Btest = np.array( - [ - (0.01090862, 0.02658977, 0.04227091), - (0.0122722, -0.01022683, -0.02727156), - ] + +def test_core_phys_dipole_circle(): + """ + test dipole vs circular current loop + mom = I x A, far field test + """ + obs = np.array([(10, 20, 30), (-10, -20, 30)]) + dia = np.array([2, 2]) + curr = np.array([1e3, 1e3]) + mom = ((dia / 2) ** 2 * np.pi * curr * np.array([(0, 0, 1)] * 2).T).T + + B1 = magpy.core.current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=curr, + ) + B2 = magpy.core.dipole_field( + field="B", + observers=obs, + moment=mom, ) + np.testing.assert_allclose(B1, B2, rtol=1e-02) + + +def test_core_phys_dipole_polyline(): + """ + test dipole VS square current loop + moment = I x A, far field test + """ + obs1 = np.array([(10, 20, 30)]) + obs4 = np.array([(10, 20, 30)] * 4) + vert = np.array([(1, 1, 0), (1, -1, 0), (-1, -1, 0), (-1, 1, 0), (1, 1, 0)]) + curr1 = 1e3 + curr4 = np.array([curr1] * 4) + mom = (4 * curr1 * np.array([(0, 0, 1)]).T).T + + B1 = magpy.core.current_polyline_field( + field="B", + observers=obs4, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr4, + ) + B1 = np.sum(B1, axis=0) + B2 = magpy.core.dipole_field( + field="B", + observers=obs1, + moment=mom, + )[0] - assert_allclose(B, Btest, rtol=1e-6) + np.testing.assert_allclose(B1, -B2, rtol=1e-03) -def test_field_dipole2(): - """test nan return when pos_obs=0""" - moment = np.array([(100, 200, 300)] * 2 + [(0, 0, 0)] * 2) - observer = np.array([(0, 0, 0), (1, 2, 3)] * 2) - B = dipole_field("B", observer, moment) +def test_core_phys_circle_polyline(): + """approximate circle with polyline""" + ts = np.linspace(0, 2 * np.pi, 300) + vert = np.array([(np.sin(t), np.cos(t), 0) for t in ts]) + curr = np.array([1]) + curr99 = np.array([1] * 299) + obs = np.array([(1, 2, 3)]) + obs99 = np.array([(1, 2, 3)] * 299) + dia = np.array([2]) - assert all(np.isinf(B[0])) - assert_allclose( - B[1:], [[0.3038282, 0.6076564, 0.91148459], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + H1 = magpy.core.current_circle_field( + field="H", + observers=obs, + diameter=dia, + current=curr, + )[0] + H2 = magpy.core.current_polyline_field( + field="H", + observers=obs99, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr99, ) + H2 = np.sum(H2, axis=0) + np.testing.assert_allclose(H1, -H2, rtol=1e-4) -def test_field_circle(): - """test if field function gives correct outputs""" - # from hyperphysics - # current = 1A - # loop radius = 1mm - # B at center = 0.6283185307179586 mT - # B at 1mm on zaxis = 0.22214414690791835 mT - pos_test_hyper = [[0, 0, 0], [0, 0, 1]] - Btest_hyper = [[0, 0, 0.6283185307179586], [0, 0, 0.22214414690791835]] - - # from magpylib 2 - pos_test_mag2 = [ - [1, 2, 3], - [-3, 2, 1], - [1, -0.2, 0.3], - [1, 0.2, -1], - [-0.1, -0.2, 3], - [-1, 0.2, -0.3], - [3, -3, -3], - [-2, -0.2, -0.3], - ] - Btest_mag2 = [ - [0.44179833, 0.88359665, 0.71546231], - [-0.53137126, 0.35424751, -0.59895825], - [72.87320789, -14.57464158, 22.07633404], - [-13.75612867, -2.75122573, 11.36467552], - [-0.10884885, -0.21769769, 2.41206364], - [72.87320789, -14.57464158, 22.07633404], - [-0.27939151, 0.27939151, 0.01220605], - [3.25697271, 0.32569727, -5.49353046], - ] - pos_test = np.array(pos_test_hyper + pos_test_mag2) - Btest = np.array(Btest_hyper + Btest_mag2) +def test_core_physics_dipole_sphere(): + """ + dipole and sphere field must be similar outside + moment = magnetization * volume + """ + obs = np.array([(1, 2, 3), (-2, -2, -2), (3, 5, -4), (5, 4, 0.1)]) + dia = np.array([2, 3, 0.1, 3.3]) + pol = np.array([(1, 2, 3), (0, 0, 1), (-1, -2, 0), (1, -1, 0.1)]) + mom = np.array([4 * (d / 2) ** 3 * np.pi / 3 * p / MU0 for d, p in zip(dia, pol)]) + + B1 = magpy.core.magnet_sphere_field( + field="B", + observers=obs, + diameter=dia, + polarization=pol, + ) + B2 = magpy.core.dipole_field( + field="B", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) - current = np.array([1, 1] + [123] * 8) - dim = np.array([2, 2] + [2] * 8) - B = current_circle_field("B", pos_test, current, dim) +# dipole vs other magnets - assert_allclose(B, Btest, rtol=1e-6) - Htest = Btest * 10 / 4 / np.pi - H = current_circle_field("H", pos_test, current, dim) - assert_allclose(H, Htest, rtol=1e-6) +####################################################################################### +####################################################################################### +####################################################################################### - with pytest.warns(MagpylibDeprecationWarning): - B = current_loop_field("B", pos_test, current, dim) - assert_allclose(B, Btest, rtol=1e-6) +# FIELD COMPUTATION TESTS AGAINST OTHER SOFTWARE + +# def test_field_dipole_VS_mathematica(): +# """Test standard dipole field output computed with mathematica""" +# obs = np.array([(1, 2, 3), (-1, 2, 3)]) +# mom = np.array([(2, 3, 4), (0, -3, -2)])/MU0 +# B = magpy.core.dipole_field( +# field="B", +# observers=obs, +# moment=mom, +# )*np.pi +# Btest = np.array( +# [ +# (0.01090862, 0.02658977, 0.04227091), +# (0.0122722, -0.01022683, -0.02727156), +# ] +# ) +# assert_allclose(B, Btest, rtol=1e-6) + + +def test_core_other_circle(): + """ + Compare Circle on-axis field vs e-magnetica & hyperphysics + """ + dia = np.array([2] * 4) + curr = np.array([1e3] * 4) # A + zs = [0, 1, 2, 3] + obs = np.array([(0, 0, z) for z in zs]) + + # values from e-magnetica + Hz = [500, 176.8, 44.72, 15.81] + Htest = [(0, 0, hz) for hz in Hz] + H = magpy.core.current_circle_field( + field="H", + observers=obs, + diameter=dia, + current=curr, + ) + np.testing.assert_allclose(H, Htest, rtol=1e-3) + + # values from hyperphysics + Bz = [ + 0.6283185307179586e-3, + 2.2214414690791835e-4, + 5.619851784832581e-5, + 1.9869176531592205e-5, + ] + Btest = [(0, 0, bz) for bz in Bz] + + B = magpy.core.current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=curr, + ) + np.testing.assert_allclose(B, Btest, rtol=1e-7) -def test_field_loop2(): - """test if field function accepts correct inputs""" - curr = np.array([1]) - dim = np.array([2]) - poso = np.array([[0, 0, 0]]) - B = current_circle_field("B", poso, curr, dim) - curr = np.array([1] * 2) - dim = np.array([2] * 2) - poso = np.array([[0, 0, 0]] * 2) - B2 = current_circle_field("B", poso, curr, dim) +####################################################################################### +####################################################################################### +####################################################################################### - assert_allclose(B, (B2[0],)) - assert_allclose(B, (B2[1],)) +# OLD FIELD COMPUTATION TESTS - SPECIAL CASES def test_field_loop_specials(): @@ -649,7 +756,12 @@ def test_field_loop_specials(): dia = np.array([2, 2, 0, 0, 2, 2]) obs = np.array([(0, 0, 0), (1, 0, 0), (0, 0, 0), (1, 0, 0), (1, 0, 0), (0, 0, 0)]) - B = current_circle_field("B", obs, cur, dia) + B = current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=cur, + ) Btest = [ [0, 0, 0.62831853], [0, 0, 0], @@ -661,7 +773,7 @@ def test_field_loop_specials(): assert_allclose(B, Btest) -def test_field_line(): +def test_field_line_special_cases(): """test line current for all cases""" c1 = np.array([1]) @@ -670,18 +782,36 @@ def test_field_line(): pe1 = np.array([(2, 2, 2)]) # only normal - B1 = current_polyline_field("B", po1, c1, ps1, pe1) + B1 = current_polyline_field( + field="B", + observers=po1, + current=c1, + segment_start=ps1, + segment_end=pe1, + ) x1 = np.array([[0.02672612, -0.05345225, 0.02672612]]) assert_allclose(x1, B1, rtol=1e-6) # only on_line po1b = np.array([(1, 1, 1)]) - B2 = current_polyline_field("B", po1b, c1, ps1, pe1) + B2 = current_polyline_field( + field="B", + observers=po1b, + current=c1, + segment_start=ps1, + segment_end=pe1, + ) x2 = np.zeros((1, 3)) assert_allclose(x2, B2, rtol=1e-6) # only zero-segment - B3 = current_polyline_field("B", po1, c1, ps1, ps1) + B3 = current_polyline_field( + field="B", + observers=po1, + current=c1, + segment_start=ps1, + segment_end=ps1, + ) x3 = np.zeros((1, 3)) assert_allclose(x3, B3, rtol=1e-6) @@ -690,19 +820,37 @@ def test_field_line(): ps2 = np.array([(0, 0, 0)] * 2) pe2 = np.array([(0, 0, 0), (2, 2, 2)]) po2 = np.array([(1, 2, 3), (1, 1, 1)]) - B4 = current_polyline_field("B", po2, c2, ps2, pe2) + B4 = current_polyline_field( + field="B", + observers=po2, + current=c2, + segment_start=ps2, + segment_end=pe2, + ) x4 = np.zeros((2, 3)) assert_allclose(x4, B4, rtol=1e-6) # normal + zero_segment po2b = np.array([(1, 2, 3), (1, 2, 3)]) - B5 = current_polyline_field("B", po2b, c2, ps2, pe2) + B5 = current_polyline_field( + field="B", + observers=po2b, + current=c2, + segment_start=ps2, + segment_end=pe2, + ) x5 = np.array([[0, 0, 0], [0.02672612, -0.05345225, 0.02672612]]) assert_allclose(x5, B5, rtol=1e-6) # normal + on_line pe2b = np.array([(2, 2, 2)] * 2) - B6 = current_polyline_field("B", po2, c2, ps2, pe2b) + B6 = current_polyline_field( + field="B", + observers=po2, + current=c2, + segment_start=ps2, + segment_end=pe2b, + ) x6 = np.array([[0.02672612, -0.05345225, 0.02672612], [0, 0, 0]]) assert_allclose(x6, B6, rtol=1e-6) @@ -711,330 +859,365 @@ def test_field_line(): ps4 = np.array([(0, 0, 0)] * 3) pe4 = np.array([(0, 0, 0), (2, 2, 2), (2, 2, 2)]) po4 = np.array([(1, 2, 3), (1, 2, 3), (1, 1, 1)]) - B7 = current_polyline_field("B", po4, c4, ps4, pe4) + B7 = current_polyline_field( + field="B", + observers=po4, + current=c4, + segment_start=ps4, + segment_end=pe4, + ) x7 = np.array([[0, 0, 0], [0.02672612, -0.05345225, 0.02672612], [0, 0, 0]]) assert_allclose(x7, B7, rtol=1e-6) - with pytest.warns(MagpylibDeprecationWarning): - x7 = current_line_field("B", po4, c4, ps4, pe4) - assert_allclose(x7, B7, rtol=1e-6) - - -def test_field_line_from_vert(): - """test the Polyline field from vertex input""" - observers = np.array([(1, 2, 2), (1, 2, 3), (-1, 0, -3)]) - current = np.array([1, 5, -3]) - - vertices = np.array( - [ - np.array( - [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (1, 2, 3), (-3, 4, -5)] - ), - np.array([(0, 0, 0), (3, 3, 3), (-3, 4, -5)]), - np.array([(1, 2, 3), (-2, -3, 3), (3, 2, 1), (3, 3, 3)]), - ], - dtype="object", - ) - - B_vert = current_vertices_field("B", observers, current, vertices) - - B = [] - for obs, vert, curr in zip(observers, vertices, current): - p1 = vert[:-1] - p2 = vert[1:] - po = np.array([obs] * (len(vert) - 1)) - cu = np.array([curr] * (len(vert) - 1)) - B += [np.sum(current_polyline_field("B", po, cu, p1, p2), axis=0)] - B = np.array(B) - - assert_allclose(B_vert, B) - - -def test_field_line_v4(): - """test current_line_Bfield() for all cases""" - cur = np.array([1] * 7) - start = np.array([(-1, 0, 0)] * 7) - end = np.array([(1, 0, 0), (-1, 0, 0), (1, 0, 0), (-1, 0, 0)] + [(1, 0, 0)] * 3) - obs = np.array( - [ - (0, 0, 1), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 1e-16), - (2, 0, 1), - (-2, 0, 1), - ] - ) - B = current_polyline_field("B", obs, cur, start, end) - Btest = np.array( - [ - [0, -0.14142136, 0], - [0, 0.0, 0], - [0, 0.0, 0], - [0, 0.0, 0], - [0, 0.0, 0], - [0, -0.02415765, 0], - [0, -0.02415765, 0], - ] - ) - np.testing.assert_allclose(B, Btest) - -def test_triangle1(): - """test core triangle VS cube""" - obs = np.array([(3, 4, 5)] * 4) - mag = np.array([(0, 0, 333)] * 4) - fac = np.array( - [ - [(-1, -1, 1), (1, -1, 1), (-1, 1, 1)], # top1 - [(1, -1, -1), (-1, -1, -1), (-1, 1, -1)], # bott1 - [(1, -1, 1), (1, 1, 1), (-1, 1, 1)], # top2 - [(1, 1, -1), (1, -1, -1), (-1, 1, -1)], # bott2 - ] - ) - b = magpy.core.triangle_field("B", obs, mag, fac) - b = np.sum(b, axis=0) - - obs = np.array([(3, 4, 5)]) - mag = np.array([(0, 0, 333)]) - dim = np.array([(2, 2, 2)]) - bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] - - np.testing.assert_allclose(b, bb) - - -def test_triangle2(): - """test core single triangle vs same surface split up into 4 triangular faces""" - obs = np.array([(3, 4, 5)]) - mag = np.array([(111, 222, 333)]) - fac = np.array( - [ - [(0, 0, 0), (10, 0, 0), (0, 10, 0)], - ] - ) - b = magpy.core.triangle_field("B", obs, mag, fac) - b = np.sum(b, axis=0) +####################################################################################### +####################################################################################### +####################################################################################### - obs = np.array([(3, 4, 5)] * 4) - mag = np.array([(111, 222, 333)] * 4) - fac = np.array( - [ - [(0, 0, 0), (3, 0, 0), (0, 10, 0)], - [(3, 0, 0), (5, 0, 0), (0, 10, 0)], - [(5, 0, 0), (6, 0, 0), (0, 10, 0)], - [(6, 0, 0), (10, 0, 0), (0, 10, 0)], - ] - ) - bb = magpy.core.triangle_field("B", obs, mag, fac) - bb = np.sum(bb, axis=0) +# OTHER - np.testing.assert_allclose(b, bb) +# def test_field_loop2(): +# """test if field function accepts correct inputs""" +# curr = np.array([1]) +# dim = np.array([2]) +# poso = np.array([[0, 0, 0]]) +# B = current_circle_field("B", poso, curr, dim) -def test_triangle3(): - """test core tetrahedron vs cube""" - ver = np.array( - [ - [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], - [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], - [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], - [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], - [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], - [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], - ] - ) +# curr = np.array([1] * 2) +# dim = np.array([2] * 2) +# poso = np.array([[0, 0, 0]] * 2) +# B2 = current_circle_field("B", poso, curr, dim) - mags = [ - [1.03595366, 0.42840487, 0.10797529], - [0.33513152, 1.61629547, 0.15959791], - [0.29904441, 1.32185041, 1.81218046], - [0.82665456, 1.86827489, 1.67338911], - [0.97619806, 1.52323106, 1.63628455], - [1.70290645, 1.49610608, 0.13878711], - [1.49886747, 1.55633919, 1.41351862], - [0.9959534, 0.62059942, 1.28616663], - [0.60114354, 0.96120344, 0.32009221], - [0.83133901, 0.7925518, 0.64574592], - ] +# assert_allclose(B, (B2[0],)) +# assert_allclose(B, (B2[1],)) - obss = [ - [0.82811352, 1.77818627, 0.19819379], - [0.84147235, 1.10200857, 1.51687527], - [0.30751474, 0.89773196, 0.56468564], - [1.87437889, 1.55908581, 1.10579983], - [0.64810548, 1.38123846, 1.90576802], - [0.48981034, 0.09376294, 0.53717129], - [1.42826412, 0.30246674, 0.57649909], - [1.58376758, 1.70420478, 0.22894022], - [0.26791832, 0.36839769, 0.67934335], - [1.15140149, 0.10549875, 0.98304184], - ] - for mag in mags: - for obs in obss: - obs6 = np.tile(obs, (6, 1)) - mag6 = np.tile(mag, (6, 1)) - b = magpy.core.magnet_tetrahedron_field("B", obs6, mag6, ver) - h = magpy.core.magnet_tetrahedron_field("H", obs6, mag6, ver) - b = np.sum(b, axis=0) - h = np.sum(h, axis=0) - - obs1 = np.reshape(obs, (1, 3)) - mag1 = np.reshape(mag, (1, 3)) - dim = np.array([(2, 2, 2)]) - bb = magpy.core.magnet_cuboid_field("B", obs1, mag1, dim)[0] - hh = magpy.core.magnet_cuboid_field("H", obs1, mag1, dim)[0] - np.testing.assert_allclose(b, bb) - np.testing.assert_allclose(h, hh) - - -def test_triangle4(): - """test core tetrahedron vs cube""" - obs = np.array([(3, 4, 5)] * 6) - mag = np.array([(111, 222, 333)] * 6) - ver = np.array( - [ - [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], - [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], - [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], - [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], - [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], - [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], - ] - ) - b = magpy.core.magnet_tetrahedron_field("B", obs, mag, ver) - b = np.sum(b, axis=0) - - obs = np.array([(3, 4, 5)]) - mag = np.array([(111, 222, 333)]) - dim = np.array([(2, 2, 2)]) - bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] - - np.testing.assert_allclose(b, bb) - - -def test_triangle5(): - """special case tests on edges - result is continuous and 0 for vertical component""" - btest1 = [ - [26.29963526814195, 15.319834473660082, 0.0], - [54.91549594789228, 41.20535983076747, 0.0], - [32.25241487782939, 15.087161660417559, 0.0], - [10.110611199952707, -11.41176203622237, 0.0], - [-3.8084378251737285, -30.875600143560657, -0.0], - [-15.636505140623612, -50.00854548249858, -0.0], - [-27.928308992688645, -72.80800891847107, -0.0], - [-45.34417750711242, -109.5871836961927, -0.0], - [-36.33970306054345, 12.288824457077656, 0.0], - [-16.984738462958845, 4.804383318447626, 0.0], - ] +def test_line_deprecation(): + with pytest.warns(MagpylibDeprecationWarning): + x = current_line_field("B", 1, 2, 3, 4) - btest2 = [ - [15.31983447366009, 26.299635268142033, 0.0], - [41.20535983076747, 54.91549594789104, 0.0], - [-72.61316618947018, 32.25241487782958, 0.0], - [-54.07597251255013, 10.110611199952693, 0.0], - [-44.104089712909634, -3.808437825173785, -0.0], - [-36.78005591314963, -15.636505140623605, -0.0], - [-30.143798442143236, -27.92830899268858, -0.0], - [-21.886855846306176, -45.34417750711366, -0.0], - [12.288824457077965, -36.33970306054315, 0.0], - [4.80438331844773, -16.98473846295874, 0.0], - ] - n = 10 - ts = np.linspace(-1, 6, n) - obs1 = np.array([(t, 0, 0) for t in ts]) - obs2 = np.array([(0, t, 0) for t in ts]) - mag = np.array([(111, 222, 333)] * n) - ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]] * n) - - b1 = magpy.core.triangle_field("H", obs1, mag, ver) - np.testing.assert_allclose(btest1, b1) - b2 = magpy.core.triangle_field("H", obs2, mag, ver) - np.testing.assert_allclose(btest2, b2) - - -def test_triangle6(): - """special case tests on corners - result is nan""" - obs1 = np.array([(0, 0, 0)]) - obs2 = np.array([(0, 5, 0)]) - obs3 = np.array([(5, 0, 0)]) - mag = np.array([(111, 222, 333)]) - ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]]) - b1 = magpy.core.triangle_field("B", obs1, mag, ver) - b2 = magpy.core.triangle_field("B", obs2, mag, ver) - b3 = magpy.core.triangle_field("B", obs3, mag, ver) - - for b in [b1, b2, b3]: - np.testing.assert_equal(b, np.array([[np.nan] * 3])) - - -@pytest.mark.parametrize( - ("module", "class_", "missing_arg"), - [ - ("magnet", "Cuboid", "dimension"), - ("magnet", "Cylinder", "dimension"), - ("magnet", "CylinderSegment", "dimension"), - ("magnet", "Sphere", "diameter"), - ("magnet", "Tetrahedron", "vertices"), - ("magnet", "TriangularMesh", "vertices"), - ("current", "Circle", "diameter"), - ("current", "Polyline", "vertices"), - ("misc", "Triangle", "vertices"), - ], -) -def test_getB_on_missing_dimensions(module, class_, missing_arg): - """test_getB_on_missing_dimensions""" - with pytest.raises( - MagpylibMissingInput, - match=rf"Parameter `{missing_arg}` of .* must be set.", - ): - getattr(getattr(magpy, module), class_)().getB([0, 0, 0]) - - -@pytest.mark.parametrize( - ("module", "class_", "missing_arg", "kwargs"), - [ - ("magnet", "Cuboid", "magnetization", {"dimension": (1, 1, 1)}), - ("magnet", "Cylinder", "magnetization", {"dimension": (1, 1)}), - ( - "magnet", - "CylinderSegment", - "magnetization", - {"dimension": (0, 1, 1, 45, 120)}, - ), - ("magnet", "Sphere", "magnetization", {"diameter": 1}), - ( - "magnet", - "Tetrahedron", - "magnetization", - {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]}, - ), - ( - "magnet", - "TriangularMesh", - "magnetization", - { - "vertices": ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), - "faces": ((0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)), - }, - ), - ("current", "Circle", "current", {"diameter": 1}), - ("current", "Polyline", "current", {"vertices": [[0, -1, 0], [0, 1, 0]]}), - ( - "misc", - "Triangle", - "magnetization", - {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0)]}, - ), - ("misc", "Dipole", "moment", {}), - ], -) -def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): - """test_getB_on_missing_excitations""" - with pytest.raises( - MagpylibMissingInput, - match=rf"Parameter `{missing_arg}` of .* must be set.", - ): - getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) +def test_loop_deprecation(): + with pytest.warns(MagpylibDeprecationWarning): + x = current_loop_field("B", 1, 2, 3, 4) + + +# def test_field_line_from_vert(): +# """test the Polyline field from vertex input""" +# observers = np.array([(1, 2, 2), (1, 2, 3), (-1, 0, -3)]) +# current = np.array([1, 5, -3]) + +# vertices = np.array( +# [ +# np.array( +# [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (1, 2, 3), (-3, 4, -5)] +# ), +# np.array([(0, 0, 0), (3, 3, 3), (-3, 4, -5)]), +# np.array([(1, 2, 3), (-2, -3, 3), (3, 2, 1), (3, 3, 3)]), +# ], +# dtype="object", +# ) + +# B_vert = current_vertices_field("B", observers, current, vertices) + +# B = [] +# for obs, vert, curr in zip(observers, vertices, current): +# p1 = vert[:-1] +# p2 = vert[1:] +# po = np.array([obs] * (len(vert) - 1)) +# cu = np.array([curr] * (len(vert) - 1)) +# B += [np.sum(current_polyline_field("B", po, cu, p1, p2), axis=0)] +# B = np.array(B) + +# assert_allclose(B_vert, B) + + +# def test_field_line_v4(): +# """test current_line_Bfield() for all cases""" +# cur = np.array([1] * 7) +# start = np.array([(-1, 0, 0)] * 7) +# end = np.array([(1, 0, 0), (-1, 0, 0), (1, 0, 0), (-1, 0, 0)] + [(1, 0, 0)] * 3) +# obs = np.array( +# [ +# (0, 0, 1), +# (0, 0, 0), +# (0, 0, 0), +# (0, 0, 0), +# (0, 0, 1e-16), +# (2, 0, 1), +# (-2, 0, 1), +# ] +# ) +# B = current_polyline_field("B", obs, cur, start, end) +# Btest = np.array( +# [ +# [0, -0.14142136, 0], +# [0, 0.0, 0], +# [0, 0.0, 0], +# [0, 0.0, 0], +# [0, 0.0, 0], +# [0, -0.02415765, 0], +# [0, -0.02415765, 0], +# ] +# ) +# np.testing.assert_allclose(B, Btest) + + +# def test_triangle1(): +# """test core triangle VS cube""" +# obs = np.array([(3, 4, 5)] * 4) +# mag = np.array([(0, 0, 333)] * 4) +# fac = np.array( +# [ +# [(-1, -1, 1), (1, -1, 1), (-1, 1, 1)], # top1 +# [(1, -1, -1), (-1, -1, -1), (-1, 1, -1)], # bott1 +# [(1, -1, 1), (1, 1, 1), (-1, 1, 1)], # top2 +# [(1, 1, -1), (1, -1, -1), (-1, 1, -1)], # bott2 +# ] +# ) +# b = magpy.core.triangle_field("B", obs, mag, fac) +# b = np.sum(b, axis=0) + +# obs = np.array([(3, 4, 5)]) +# mag = np.array([(0, 0, 333)]) +# dim = np.array([(2, 2, 2)]) +# bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] + +# np.testing.assert_allclose(b, bb) + + +# def test_triangle2(): +# """test core single triangle vs same surface split up into 4 triangular faces""" +# obs = np.array([(3, 4, 5)]) +# mag = np.array([(111, 222, 333)]) +# fac = np.array( +# [ +# [(0, 0, 0), (10, 0, 0), (0, 10, 0)], +# ] +# ) +# b = magpy.core.triangle_field("B", obs, mag, fac) +# b = np.sum(b, axis=0) + +# obs = np.array([(3, 4, 5)] * 4) +# mag = np.array([(111, 222, 333)] * 4) +# fac = np.array( +# [ +# [(0, 0, 0), (3, 0, 0), (0, 10, 0)], +# [(3, 0, 0), (5, 0, 0), (0, 10, 0)], +# [(5, 0, 0), (6, 0, 0), (0, 10, 0)], +# [(6, 0, 0), (10, 0, 0), (0, 10, 0)], +# ] +# ) +# bb = magpy.core.triangle_field("B", obs, mag, fac) +# bb = np.sum(bb, axis=0) + +# np.testing.assert_allclose(b, bb) + + +# def test_triangle3(): +# """test core tetrahedron vs cube""" +# ver = np.array( +# [ +# [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], +# [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], +# [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], +# [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], +# [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], +# [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], +# ] +# ) + +# mags = [ +# [1.03595366, 0.42840487, 0.10797529], +# [0.33513152, 1.61629547, 0.15959791], +# [0.29904441, 1.32185041, 1.81218046], +# [0.82665456, 1.86827489, 1.67338911], +# [0.97619806, 1.52323106, 1.63628455], +# [1.70290645, 1.49610608, 0.13878711], +# [1.49886747, 1.55633919, 1.41351862], +# [0.9959534, 0.62059942, 1.28616663], +# [0.60114354, 0.96120344, 0.32009221], +# [0.83133901, 0.7925518, 0.64574592], +# ] + +# obss = [ +# [0.82811352, 1.77818627, 0.19819379], +# [0.84147235, 1.10200857, 1.51687527], +# [0.30751474, 0.89773196, 0.56468564], +# [1.87437889, 1.55908581, 1.10579983], +# [0.64810548, 1.38123846, 1.90576802], +# [0.48981034, 0.09376294, 0.53717129], +# [1.42826412, 0.30246674, 0.57649909], +# [1.58376758, 1.70420478, 0.22894022], +# [0.26791832, 0.36839769, 0.67934335], +# [1.15140149, 0.10549875, 0.98304184], +# ] + +# for mag in mags: +# for obs in obss: +# obs6 = np.tile(obs, (6, 1)) +# mag6 = np.tile(mag, (6, 1)) +# b = magpy.core.magnet_tetrahedron_field("B", obs6, mag6, ver) +# h = magpy.core.magnet_tetrahedron_field("H", obs6, mag6, ver) +# b = np.sum(b, axis=0) +# h = np.sum(h, axis=0) + +# obs1 = np.reshape(obs, (1, 3)) +# mag1 = np.reshape(mag, (1, 3)) +# dim = np.array([(2, 2, 2)]) +# bb = magpy.core.magnet_cuboid_field("B", obs1, mag1, dim)[0] +# hh = magpy.core.magnet_cuboid_field("H", obs1, mag1, dim)[0] +# np.testing.assert_allclose(b, bb) +# np.testing.assert_allclose(h, hh) + + +# def test_triangle4(): +# """test core tetrahedron vs cube""" +# obs = np.array([(3, 4, 5)] * 6) +# mag = np.array([(111, 222, 333)] * 6) +# ver = np.array( +# [ +# [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], +# [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], +# [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], +# [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], +# [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], +# [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], +# ] +# ) +# b = magpy.core.magnet_tetrahedron_field("B", obs, mag, ver) +# b = np.sum(b, axis=0) + +# obs = np.array([(3, 4, 5)]) +# mag = np.array([(111, 222, 333)]) +# dim = np.array([(2, 2, 2)]) +# bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] + +# np.testing.assert_allclose(b, bb) + + +# def test_triangle5(): +# """special case tests on edges - result is continuous and 0 for vertical component""" +# btest1 = [ +# [26.29963526814195, 15.319834473660082, 0.0], +# [54.91549594789228, 41.20535983076747, 0.0], +# [32.25241487782939, 15.087161660417559, 0.0], +# [10.110611199952707, -11.41176203622237, 0.0], +# [-3.8084378251737285, -30.875600143560657, -0.0], +# [-15.636505140623612, -50.00854548249858, -0.0], +# [-27.928308992688645, -72.80800891847107, -0.0], +# [-45.34417750711242, -109.5871836961927, -0.0], +# [-36.33970306054345, 12.288824457077656, 0.0], +# [-16.984738462958845, 4.804383318447626, 0.0], +# ] + +# btest2 = [ +# [15.31983447366009, 26.299635268142033, 0.0], +# [41.20535983076747, 54.91549594789104, 0.0], +# [-72.61316618947018, 32.25241487782958, 0.0], +# [-54.07597251255013, 10.110611199952693, 0.0], +# [-44.104089712909634, -3.808437825173785, -0.0], +# [-36.78005591314963, -15.636505140623605, -0.0], +# [-30.143798442143236, -27.92830899268858, -0.0], +# [-21.886855846306176, -45.34417750711366, -0.0], +# [12.288824457077965, -36.33970306054315, 0.0], +# [4.80438331844773, -16.98473846295874, 0.0], +# ] + +# n = 10 +# ts = np.linspace(-1, 6, n) +# obs1 = np.array([(t, 0, 0) for t in ts]) +# obs2 = np.array([(0, t, 0) for t in ts]) +# mag = np.array([(111, 222, 333)] * n) +# ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]] * n) + +# b1 = magpy.core.triangle_field("H", obs1, mag, ver) +# np.testing.assert_allclose(btest1, b1) +# b2 = magpy.core.triangle_field("H", obs2, mag, ver) +# np.testing.assert_allclose(btest2, b2) + + +# def test_triangle6(): +# """special case tests on corners - result is nan""" +# obs1 = np.array([(0, 0, 0)]) +# obs2 = np.array([(0, 5, 0)]) +# obs3 = np.array([(5, 0, 0)]) +# mag = np.array([(111, 222, 333)]) +# ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]]) +# b1 = magpy.core.triangle_field("B", obs1, mag, ver) +# b2 = magpy.core.triangle_field("B", obs2, mag, ver) +# b3 = magpy.core.triangle_field("B", obs3, mag, ver) + +# for b in [b1, b2, b3]: +# np.testing.assert_equal(b, np.array([[np.nan] * 3])) + + +# @pytest.mark.parametrize( +# ("module", "class_", "missing_arg"), +# [ +# ("magnet", "Cuboid", "dimension"), +# ("magnet", "Cylinder", "dimension"), +# ("magnet", "CylinderSegment", "dimension"), +# ("magnet", "Sphere", "diameter"), +# ("magnet", "Tetrahedron", "vertices"), +# ("magnet", "TriangularMesh", "vertices"), +# ("current", "Circle", "diameter"), +# ("current", "Polyline", "vertices"), +# ("misc", "Triangle", "vertices"), +# ], +# ) +# def test_getB_on_missing_dimensions(module, class_, missing_arg): +# """test_getB_on_missing_dimensions""" +# with pytest.raises( +# MagpylibMissingInput, +# match=rf"Parameter `{missing_arg}` of .* must be set.", +# ): +# getattr(getattr(magpy, module), class_)().getB([0, 0, 0]) + + +# @pytest.mark.parametrize( +# ("module", "class_", "missing_arg", "kwargs"), +# [ +# ("magnet", "Cuboid", "magnetization", {"dimension": (1, 1, 1)}), +# ("magnet", "Cylinder", "magnetization", {"dimension": (1, 1)}), +# ( +# "magnet", +# "CylinderSegment", +# "magnetization", +# {"dimension": (0, 1, 1, 45, 120)}, +# ), +# ("magnet", "Sphere", "magnetization", {"diameter": 1}), +# ( +# "magnet", +# "Tetrahedron", +# "magnetization", +# {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]}, +# ), +# ( +# "magnet", +# "TriangularMesh", +# "magnetization", +# { +# "vertices": ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), +# "faces": ((0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)), +# }, +# ), +# ("current", "Circle", "current", {"diameter": 1}), +# ("current", "Polyline", "current", {"vertices": [[0, -1, 0], [0, 1, 0]]}), +# ( +# "misc", +# "Triangle", +# "magnetization", +# {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0)]}, +# ), +# ("misc", "Dipole", "moment", {}), +# ], +# ) +# def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): +# """test_getB_on_missing_excitations""" +# with pytest.raises( +# MagpylibMissingInput, +# match=rf"Parameter `{missing_arg}` of .* must be set.", +# ): +# getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) From 2942f5b7a46cf88320c33aa805b75bc43fe647f3 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 13:20:45 +0100 Subject: [PATCH 048/240] add getM and getJ to trimesh --- .../_src/fields/field_BH_triangularmesh.py | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index cf7c0af28..a1ddf5d6a 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -8,6 +8,7 @@ import scipy.spatial from magpylib._src.fields.field_BH_triangle import triangle_field +from magpylib._src.utility import convert_HBMJ from magpylib._src.utility import MU0 @@ -550,57 +551,61 @@ def magnet_trimesh_field( Field computations via publication: Guptasarma: GEOPHYSICS 1999 64:1, 70-74 """ - if mesh.ndim != 1: # all vertices objects have same number of children - n0, n1, *_ = mesh.shape - vertices_tiled = mesh.reshape(-1, 3, 3) - observers_tiled = np.repeat(observers, n1, axis=0) - polarization_tiled = np.repeat(polarization, n1, axis=0) - B = triangle_field( - field="B", - observers=observers_tiled, - vertices=vertices_tiled, - polarization=polarization_tiled, - ) - B = B.reshape((n0, n1, 3)) - B = np.sum(B, axis=1) + if field in "BH": + if mesh.ndim != 1: # all vertices objects have same number of children + n0, n1, *_ = mesh.shape + vertices_tiled = mesh.reshape(-1, 3, 3) + observers_tiled = np.repeat(observers, n1, axis=0) + polarization_tiled = np.repeat(polarization, n1, axis=0) + B = triangle_field( + field="B", + observers=observers_tiled, + vertices=vertices_tiled, + polarization=polarization_tiled, + ) + B = B.reshape((n0, n1, 3)) + B = np.sum(B, axis=1) + else: + nvs = [f.shape[0] for f in mesh] # length of vertex set + split_indices = np.cumsum(nvs)[:-1] # remove last to avoid empty split + vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in mesh]) + observers_tiled = np.repeat(observers, nvs, axis=0) + polarization_tiled = np.repeat(polarization, nvs, axis=0) + B = triangle_field( + field="B", + observers=observers_tiled, + vertices=vertices_tiled, + polarization=polarization_tiled, + ) + b_split = np.split(B, split_indices) + B = np.array([np.sum(bh, axis=0) for bh in b_split]) else: - nvs = [f.shape[0] for f in mesh] # length of vertex set - split_indices = np.cumsum(nvs)[:-1] # remove last to avoid empty split - vertices_tiled = np.concatenate([f.reshape((-1, 3, 3)) for f in mesh]) - observers_tiled = np.repeat(observers, nvs, axis=0) - polarization_tiled = np.repeat(polarization, nvs, axis=0) - B = triangle_field( - field="B", - observers=observers_tiled, - vertices=vertices_tiled, - polarization=polarization_tiled, - ) - b_split = np.split(B, split_indices) - B = np.array([np.sum(bh, axis=0) for bh in b_split]) - - if field == "B": - if in_out == "auto": - prev_ind = 0 - # group similar meshes for inside-outside evaluation and adding B - for new_ind, _ in enumerate(B): - if ( - new_ind == len(B) - 1 - or mesh[new_ind].shape != mesh[prev_ind].shape - or not np.all(mesh[new_ind] == mesh[prev_ind]) - ): - if new_ind == len(B) - 1: - new_ind = len(B) - inside_mask = mask_inside_trimesh( - observers[prev_ind:new_ind], mesh[prev_ind] - ) - # if inside magnet add polarization vector - B[prev_ind:new_ind][inside_mask] += polarization[prev_ind:new_ind][ - inside_mask - ] - prev_ind = new_ind - elif in_out == "inside": - B += polarization - return B - - H = B / MU0 - return H + B = np.zeros_like(observers) + + if field == "H": + return B / MU0 + + if in_out == "auto": + prev_ind = 0 + # group similar meshes for inside-outside evaluation and adding B + for new_ind, _ in enumerate(B): + if ( + new_ind == len(B) - 1 + or mesh[new_ind].shape != mesh[prev_ind].shape + or not np.all(mesh[new_ind] == mesh[prev_ind]) + ): + if new_ind == len(B) - 1: + new_ind = len(B) + mask_inside = mask_inside_trimesh( + observers[prev_ind:new_ind], mesh[prev_ind] + ) + # if inside magnet add polarization vector + B[prev_ind:new_ind][mask_inside] += polarization[prev_ind:new_ind][ + mask_inside + ] + prev_ind = new_ind + elif in_out == "inside": + B += polarization + if field == "M": + B /= MU0 + return B From 04e015aeee396038e37e1f15320dec06f5559bdf Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 14:52:38 +0100 Subject: [PATCH 049/240] add getM getJ to dipole --- magpylib/_src/fields/field_BH_dipole.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index 488c53529..eff0dd6a9 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -59,7 +59,9 @@ def dipole_field( The moment of a magnet is given by its volume*magnetization. """ - bh = check_field_input(field, "dipole_field()") + check_field_input(field) + if field in "MJ": + return np.zeros_like(observers) x, y, z = observers.T r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm @@ -78,8 +80,7 @@ def dipole_field( B[mask1] = moment[mask1] / 0.0 np.nan_to_num(B, copy=False, posinf=np.inf, neginf=np.NINF) - if bh: + if field == "B": return B - H = B / MU0 - return H + return B / MU0 From 7e1f2a7d02eb4de661e87ab0826fe196516c84ed Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 15:04:22 +0100 Subject: [PATCH 050/240] avoid calculating field for M or J for cyl seg --- .../_src/fields/field_BH_cylinder_segment.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 5432d6360..a537ab12b 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2471,24 +2471,26 @@ def magnet_cylinder_segment_field( if not np.any(mask_not_on_surf): return BHfinal - # redefine input if there are some surface-points ------------------------- - magg = polarization[mask_not_on_surf] - dim = dim[mask_not_on_surf] - pos_obs_cy = pos_obs_cy[mask_not_on_surf] - phi = phi[mask_not_on_surf] - - # transform mag to spherical CS ----------------------------------------- - m = np.sqrt(magg[:, 0] ** 2 + magg[:, 1] ** 2 + magg[:, 2] ** 2) - phi_m = np.arctan2(magg[:, 1], magg[:, 0]) - th_m = np.arctan2(np.sqrt(magg[:, 0] ** 2 + magg[:, 1] ** 2), magg[:, 2]) - mag_sph = np.concatenate(((m,), (phi_m,), (th_m,)), axis=0).T - - # compute H and transform to cart CS ------------------------------------- - H_cy = magnet_cylinder_segment_core(mag_sph, dim, pos_obs_cy) - Hr, Hphi, Hz = H_cy.T - Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) - Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) - H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 + H = None + if field in "BH": + # redefine input if there are some surface-points ------------------------- + pol = polarization[mask_not_on_surf] + dim = dim[mask_not_on_surf] + pos_obs_cy = pos_obs_cy[mask_not_on_surf] + phi = phi[mask_not_on_surf] + + # transform mag to spherical CS ----------------------------------------- + m = np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2 + pol[:, 2] ** 2) + phi_m = np.arctan2(pol[:, 1], pol[:, 0]) + th_m = np.arctan2(np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2), pol[:, 2]) + mag_sph = np.concatenate(((m,), (phi_m,), (th_m,)), axis=0).T + + # compute H and transform to cart CS ------------------------------------- + H_cy = magnet_cylinder_segment_core(mag_sph, dim, pos_obs_cy) + Hr, Hphi, Hz = H_cy.T + Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) + Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) + H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 return convert_HBMJ( output_field_type=field, From 07a7e7dacbd9404e6ee003be89e7ba225164f6d7 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 15:10:28 +0100 Subject: [PATCH 051/240] avoid calculating field for getM getJ cylinder --- magpylib/_src/fields/field_BH_cylinder.py | 27 +++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 52b25bea9..61eb485d6 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -348,6 +348,13 @@ def magnet_cylinder_field( else: mask_inside = np.full(len(observers), in_out == "inside") mask_not_on_edge = np.full(len(observers), True) + + if field in "MJ": + return convert_HBMJ( + output_field_type=field, + polarization=polarization, + mask_inside=mask_inside, + ) # axial/transv polarization cases pol_x, pol_y, pol_z = polarization.T mask_pol_tv = (pol_x != 0) | (pol_y != 0) @@ -371,7 +378,10 @@ def magnet_cylinder_field( pol_xy = np.sqrt(pol_x**2 + pol_y**2)[mask_pol_tv] tetta = np.arctan2(pol_y[mask_pol_tv], pol_x[mask_pol_tv]) Br_tv, Bphi_tv, Bz_tv = fieldH_cylinder_diametral( - z0[mask_pol_tv], r[mask_pol_tv], phi[mask_pol_tv] - tetta, z[mask_pol_tv] + z0[mask_pol_tv], + r[mask_pol_tv], + phi[mask_pol_tv] - tetta, + z[mask_pol_tv], ) # add to H-field (inside pol_xy is missing for B !!!) @@ -398,14 +408,7 @@ def magnet_cylinder_field( By[mask_tv_inside] += pol_y[mask_tv_inside] return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T - if field == "H": - mask_ax_inside = mask_pol_ax * mask_inside - if any(mask_ax_inside): # ax computes B-field - Bz[mask_ax_inside] -= pol_z[mask_ax_inside] - return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 - - return convert_HBMJ( - output_field_type=field, - polarization=polarization, - mask_inside=mask_inside, - ) + mask_ax_inside = mask_pol_ax * mask_inside + if any(mask_ax_inside): # ax computes B-field + Bz[mask_ax_inside] -= pol_z[mask_ax_inside] + return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 From 4d25c504fe48eafed7ec56f0e1fc746b3c0656bb Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 15:14:42 +0100 Subject: [PATCH 052/240] avoid calc field for getM getJ sphere --- magpylib/_src/fields/field_BH_sphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index ef754d95f..6a8587de2 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -90,7 +90,7 @@ def magnet_sphere_field( # overwrite outside field entries mask_outside = ~mask_inside - if mask_outside.any(): + if mask_outside.any() and field in "BH": pol_out = polarization[mask_outside] obs_out = observers[mask_outside] r_out = r[mask_outside] From 97c0e88faf780d8f97aa0853aae142d7763c716b Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 23 Dec 2023 15:17:07 +0100 Subject: [PATCH 053/240] refactor --- magpylib/_src/fields/field_BH_tetrahedron.py | 61 ++++++++++---------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index 9965e4c3e..dc0d56ca2 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -132,38 +132,39 @@ def magnet_tetrahedron_field( elif in_out != "auto": val = in_out == "inside" mask_inside = np.full(len(observers), val) - if field in "BH": - tri_vertices = np.concatenate( - ( - vertices[:, (0, 2, 1), :], - vertices[:, (0, 1, 3), :], - vertices[:, (1, 2, 3), :], - vertices[:, (0, 3, 2), :], - ), - axis=0, - ) - tri_fields = triangle_field( - field=field, - observers=np.tile(observers, (4, 1)), - vertices=tri_vertices, - polarization=np.tile(polarization, (4, 1)), - ) - tetra_field = ( # slightly faster than reshape + sum - tri_fields[:n] - + tri_fields[n : 2 * n] - + tri_fields[2 * n : 3 * n] - + tri_fields[3 * n :] + + if field in "MJ": + return convert_HBMJ( + output_field_type=field, + polarization=polarization, + mask_inside=mask_inside, ) - if field == "H": - return tetra_field + tri_vertices = np.concatenate( + ( + vertices[:, (0, 2, 1), :], + vertices[:, (0, 1, 3), :], + vertices[:, (1, 2, 3), :], + vertices[:, (0, 3, 2), :], + ), + axis=0, + ) + tri_fields = triangle_field( + field=field, + observers=np.tile(observers, (4, 1)), + vertices=tri_vertices, + polarization=np.tile(polarization, (4, 1)), + ) + tetra_field = ( # slightly faster than reshape + sum + tri_fields[:n] + + tri_fields[n : 2 * n] + + tri_fields[2 * n : 3 * n] + + tri_fields[3 * n :] + ) - # if B, and inside magnet add polarization vector - tetra_field[mask_inside] += polarization[mask_inside] + if field == "H": return tetra_field - return convert_HBMJ( - output_field_type=field, - polarization=polarization, - mask_inside=mask_inside, - ) + # if B, and inside magnet add polarization vector + tetra_field[mask_inside] += polarization[mask_inside] + return tetra_field From d2ef80e27ef77ed05b669d16fa50fac34e6f1a06 Mon Sep 17 00:00:00 2001 From: mortner Date: Sat, 23 Dec 2023 20:14:34 +0100 Subject: [PATCH 054/240] long solenoid test --- tests/test_core_field_functions.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index cd94535fb..06917c73c 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -575,7 +575,8 @@ def test_dipole_field_BH(): def test_core_phys_dipole_circle(): """ test dipole vs circular current loop - mom = I x A, far field test + moment = current * surface + far field test """ obs = np.array([(10, 20, 30), (-10, -20, 30)]) dia = np.array([2, 2]) @@ -677,9 +678,30 @@ def test_core_physics_dipole_sphere(): np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) -# dipole vs other magnets +def test_core_physics_long_solenoid(): + """ + test if field from solenoid converges to long-solenoid field + Bz = I*N/L + """ + I = 1 + N = 10000 + L = 100 + B = magpy.core.current_circle_field( + field="H", + observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), + diameter=np.array([2] * N), + current=np.array([I] * N), + ) + bz = np.sum(B, axis=0)[2] + bz_test = N * I / L + + np.testing.assert_allclose(bz, bz_test, rtol=1e-3) +# dipole vs other magnets +# solenoid formula +# approximate magnets with currents + ####################################################################################### ####################################################################################### ####################################################################################### From b74e066ba811befddbb3dd2b7fdc257aee69dd71 Mon Sep 17 00:00:00 2001 From: mortner Date: Sat, 23 Dec 2023 20:18:15 +0100 Subject: [PATCH 055/240] comments --- tests/test_core_field_functions.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 06917c73c..37b15c281 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -572,7 +572,8 @@ def test_dipole_field_BH(): # FIELD COMPUTATION PHYSICS CONSISTENCY TESTS -def test_core_phys_dipole_circle(): +# Circle<>Dipole +def test_core_phys_moment_of_current_circle(): """ test dipole vs circular current loop moment = current * surface @@ -597,7 +598,8 @@ def test_core_phys_dipole_circle(): np.testing.assert_allclose(B1, B2, rtol=1e-02) -def test_core_phys_dipole_polyline(): +# Polyline <> Dipole +def test_core_phys_moment_of_current_square(): """ test dipole VS square current loop moment = I x A, far field test @@ -626,6 +628,7 @@ def test_core_phys_dipole_polyline(): np.testing.assert_allclose(B1, -B2, rtol=1e-03) +# Circle <> Polyline def test_core_phys_circle_polyline(): """approximate circle with polyline""" ts = np.linspace(0, 2 * np.pi, 300) @@ -654,6 +657,7 @@ def test_core_phys_circle_polyline(): np.testing.assert_allclose(H1, -H2, rtol=1e-4) +# Dipole <> Sphere def test_core_physics_dipole_sphere(): """ dipole and sphere field must be similar outside @@ -678,6 +682,7 @@ def test_core_physics_dipole_sphere(): np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) +# -> Circle def test_core_physics_long_solenoid(): """ test if field from solenoid converges to long-solenoid field @@ -699,33 +704,14 @@ def test_core_physics_long_solenoid(): # dipole vs other magnets -# solenoid formula # approximate magnets with currents ####################################################################################### ####################################################################################### ####################################################################################### -# FIELD COMPUTATION TESTS AGAINST OTHER SOFTWARE - -# def test_field_dipole_VS_mathematica(): -# """Test standard dipole field output computed with mathematica""" -# obs = np.array([(1, 2, 3), (-1, 2, 3)]) -# mom = np.array([(2, 3, 4), (0, -3, -2)])/MU0 -# B = magpy.core.dipole_field( -# field="B", -# observers=obs, -# moment=mom, -# )*np.pi -# Btest = np.array( -# [ -# (0.01090862, 0.02658977, 0.04227091), -# (0.0122722, -0.01022683, -0.02727156), -# ] -# ) -# assert_allclose(B, Btest, rtol=1e-6) - +# --> Circle def test_core_other_circle(): """ Compare Circle on-axis field vs e-magnetica & hyperphysics From 0e040204f8ba5b264ff0f909784f95b57be35ad5 Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 24 Dec 2023 07:52:56 +0100 Subject: [PATCH 056/240] core physics tests --- tests/test_core_field_functions.py | 179 ---------- tests/test_core_physics_consistency.py | 440 +++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 179 deletions(-) create mode 100644 tests/test_core_physics_consistency.py diff --git a/tests/test_core_field_functions.py b/tests/test_core_field_functions.py index 37b15c281..50420a9ee 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_field_functions.py @@ -572,185 +572,6 @@ def test_dipole_field_BH(): # FIELD COMPUTATION PHYSICS CONSISTENCY TESTS -# Circle<>Dipole -def test_core_phys_moment_of_current_circle(): - """ - test dipole vs circular current loop - moment = current * surface - far field test - """ - obs = np.array([(10, 20, 30), (-10, -20, 30)]) - dia = np.array([2, 2]) - curr = np.array([1e3, 1e3]) - mom = ((dia / 2) ** 2 * np.pi * curr * np.array([(0, 0, 1)] * 2).T).T - - B1 = magpy.core.current_circle_field( - field="B", - observers=obs, - diameter=dia, - current=curr, - ) - B2 = magpy.core.dipole_field( - field="B", - observers=obs, - moment=mom, - ) - np.testing.assert_allclose(B1, B2, rtol=1e-02) - - -# Polyline <> Dipole -def test_core_phys_moment_of_current_square(): - """ - test dipole VS square current loop - moment = I x A, far field test - """ - obs1 = np.array([(10, 20, 30)]) - obs4 = np.array([(10, 20, 30)] * 4) - vert = np.array([(1, 1, 0), (1, -1, 0), (-1, -1, 0), (-1, 1, 0), (1, 1, 0)]) - curr1 = 1e3 - curr4 = np.array([curr1] * 4) - mom = (4 * curr1 * np.array([(0, 0, 1)]).T).T - - B1 = magpy.core.current_polyline_field( - field="B", - observers=obs4, - segment_start=vert[:-1], - segment_end=vert[1:], - current=curr4, - ) - B1 = np.sum(B1, axis=0) - B2 = magpy.core.dipole_field( - field="B", - observers=obs1, - moment=mom, - )[0] - - np.testing.assert_allclose(B1, -B2, rtol=1e-03) - - -# Circle <> Polyline -def test_core_phys_circle_polyline(): - """approximate circle with polyline""" - ts = np.linspace(0, 2 * np.pi, 300) - vert = np.array([(np.sin(t), np.cos(t), 0) for t in ts]) - curr = np.array([1]) - curr99 = np.array([1] * 299) - obs = np.array([(1, 2, 3)]) - obs99 = np.array([(1, 2, 3)] * 299) - dia = np.array([2]) - - H1 = magpy.core.current_circle_field( - field="H", - observers=obs, - diameter=dia, - current=curr, - )[0] - H2 = magpy.core.current_polyline_field( - field="H", - observers=obs99, - segment_start=vert[:-1], - segment_end=vert[1:], - current=curr99, - ) - H2 = np.sum(H2, axis=0) - - np.testing.assert_allclose(H1, -H2, rtol=1e-4) - - -# Dipole <> Sphere -def test_core_physics_dipole_sphere(): - """ - dipole and sphere field must be similar outside - moment = magnetization * volume - """ - obs = np.array([(1, 2, 3), (-2, -2, -2), (3, 5, -4), (5, 4, 0.1)]) - dia = np.array([2, 3, 0.1, 3.3]) - pol = np.array([(1, 2, 3), (0, 0, 1), (-1, -2, 0), (1, -1, 0.1)]) - mom = np.array([4 * (d / 2) ** 3 * np.pi / 3 * p / MU0 for d, p in zip(dia, pol)]) - - B1 = magpy.core.magnet_sphere_field( - field="B", - observers=obs, - diameter=dia, - polarization=pol, - ) - B2 = magpy.core.dipole_field( - field="B", - observers=obs, - moment=mom, - ) - np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) - - -# -> Circle -def test_core_physics_long_solenoid(): - """ - test if field from solenoid converges to long-solenoid field - Bz = I*N/L - """ - I = 1 - N = 10000 - L = 100 - B = magpy.core.current_circle_field( - field="H", - observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), - diameter=np.array([2] * N), - current=np.array([I] * N), - ) - bz = np.sum(B, axis=0)[2] - bz_test = N * I / L - - np.testing.assert_allclose(bz, bz_test, rtol=1e-3) - - -# dipole vs other magnets -# approximate magnets with currents - -####################################################################################### -####################################################################################### -####################################################################################### - - -# --> Circle -def test_core_other_circle(): - """ - Compare Circle on-axis field vs e-magnetica & hyperphysics - """ - dia = np.array([2] * 4) - curr = np.array([1e3] * 4) # A - zs = [0, 1, 2, 3] - obs = np.array([(0, 0, z) for z in zs]) - - # values from e-magnetica - Hz = [500, 176.8, 44.72, 15.81] - Htest = [(0, 0, hz) for hz in Hz] - - H = magpy.core.current_circle_field( - field="H", - observers=obs, - diameter=dia, - current=curr, - ) - np.testing.assert_allclose(H, Htest, rtol=1e-3) - - # values from hyperphysics - Bz = [ - 0.6283185307179586e-3, - 2.2214414690791835e-4, - 5.619851784832581e-5, - 1.9869176531592205e-5, - ] - Btest = [(0, 0, bz) for bz in Bz] - - B = magpy.core.current_circle_field( - field="B", - observers=obs, - diameter=dia, - current=curr, - ) - np.testing.assert_allclose(B, Btest, rtol=1e-7) - - ####################################################################################### ####################################################################################### ####################################################################################### diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py new file mode 100644 index 000000000..94036972a --- /dev/null +++ b/tests/test_core_physics_consistency.py @@ -0,0 +1,440 @@ +import numpy as np +import pytest +from numpy.testing import assert_allclose + +import magpylib as magpy +from magpylib._src.exceptions import MagpylibDeprecationWarning +from magpylib._src.exceptions import MagpylibMissingInput +from magpylib._src.fields.field_BH_polyline import current_vertices_field +from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field +from magpylib._src.utility import MU0 +from magpylib.core import current_circle_field +from magpylib.core import current_line_field +from magpylib.core import current_loop_field +from magpylib.core import current_polyline_field +from magpylib.core import dipole_field +from magpylib.core import magnet_cuboid_field +from magpylib.core import magnet_cylinder_field +from magpylib.core import magnet_cylinder_segment_field +from magpylib.core import magnet_sphere_field +from magpylib.core import magnet_tetrahedron_field +from magpylib.core import triangle_field + +# Magnetic moment of a current loop with current I and surface A: +# mom = I * A +# Magnetic moment of a homogeneous magnet with magnetization mag and volume vol +# mom = vol * mag +# Current density j on magnet surface in the replacement picture +# j = mag = J/MU0 + +# ----------> Circle # webpage numbers +# Circle <> Dipole # mom = I*A (far field approx) +# Polyline <> Dipole # mom = I*A (far field approx) +# Dipole <> Sphere # mom = vol*mag (similar outside of sphere) +# Dipole <> all Magnets # mom = vol*mag (far field approx) +# Circle <> Cylinder # j = I*N/L == J/MU0 current replacement picture +# Polyline <> Cuboid # j = I*N/L == J/MU0 current replacement picture +# Circle <> Polyline # geometric approx +# Cylinder <> CylinderSegment # geometric approx + + +# Circle<>Dipole +def test_core_phys_moment_of_current_circle(): + """ + test dipole vs circular current loop + moment = current * surface + far field test + """ + obs = np.array([(10, 20, 30), (-10, -20, 30)]) + dia = np.array([2, 2]) + curr = np.array([1e3, 1e3]) + mom = ((dia / 2) ** 2 * np.pi * curr * np.array([(0, 0, 1)] * 2).T).T + + B1 = magpy.core.current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=curr, + ) + B2 = magpy.core.dipole_field( + field="B", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(B1, B2, rtol=1e-02) + + H1 = magpy.core.current_circle_field( + field="H", + observers=obs, + diameter=dia, + current=curr, + ) + H2 = magpy.core.dipole_field( + field="H", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(H1, H2, rtol=1e-02) + + +# Polyline <> Dipole +def test_core_phys_moment_of_current_square(): + """ + test dipole VS square current loop + moment = I x A, far field test + """ + obs1 = np.array([(10, 20, 30)]) + obs4 = np.array([(10, 20, 30)] * 4) + vert = np.array([(1, 1, 0), (1, -1, 0), (-1, -1, 0), (-1, 1, 0), (1, 1, 0)]) + curr1 = 1e3 + curr4 = np.array([curr1] * 4) + mom = (4 * curr1 * np.array([(0, 0, 1)]).T).T + + B1 = magpy.core.current_polyline_field( + field="B", + observers=obs4, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr4, + ) + B1 = np.sum(B1, axis=0) + B2 = magpy.core.dipole_field( + field="B", + observers=obs1, + moment=mom, + )[0] + np.testing.assert_allclose(B1, -B2, rtol=1e-03) + + H1 = magpy.core.current_polyline_field( + field="H", + observers=obs4, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr4, + ) + H1 = np.sum(H1, axis=0) + H2 = magpy.core.dipole_field( + field="H", + observers=obs1, + moment=mom, + )[0] + np.testing.assert_allclose(H1, -H2, rtol=1e-03) + + +# Circle <> Polyline +def test_core_phys_circle_polyline(): + """approximate circle with polyline""" + ts = np.linspace(0, 2 * np.pi, 300) + vert = np.array([(np.sin(t), np.cos(t), 0) for t in ts]) + curr = np.array([1]) + curr99 = np.array([1] * 299) + obs = np.array([(1, 2, 3)]) + obs99 = np.array([(1, 2, 3)] * 299) + dia = np.array([2]) + + H1 = magpy.core.current_circle_field( + field="H", + observers=obs, + diameter=dia, + current=curr, + )[0] + H2 = magpy.core.current_polyline_field( + field="H", + observers=obs99, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr99, + ) + H2 = np.sum(H2, axis=0) + np.testing.assert_allclose(H1, -H2, rtol=1e-4) + + B1 = magpy.core.current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=curr, + )[0] + B2 = magpy.core.current_polyline_field( + field="B", + observers=obs99, + segment_start=vert[:-1], + segment_end=vert[1:], + current=curr99, + ) + B2 = np.sum(B2, axis=0) + np.testing.assert_allclose(B1, -B2, rtol=1e-4) + + +# Dipole <> Sphere +def test_core_physics_dipole_sphere(): + """ + dipole and sphere field must be similar outside of sphere + moment = magnetization * volume + near field tests + """ + obs = np.array([(1, 2, 3), (-2, -2, -2), (3, 5, -4), (5, 4, 0.1)]) + dia = np.array([2, 3, 0.1, 3.3]) + pol = np.array([(1, 2, 3), (0, 0, 1), (-1, -2, 0), (1, -1, 0.1)]) + mom = np.array([4 * (d / 2) ** 3 * np.pi / 3 * p / MU0 for d, p in zip(dia, pol)]) + + B1 = magpy.core.magnet_sphere_field( + field="B", + observers=obs, + diameter=dia, + polarization=pol, + ) + B2 = magpy.core.dipole_field( + field="B", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) + + H1 = magpy.core.magnet_sphere_field( + field="H", + observers=obs, + diameter=dia, + polarization=pol, + ) + H2 = magpy.core.dipole_field( + field="H", + observers=obs, + moment=mom, + ) + np.testing.assert_allclose(H1, H2, rtol=0, atol=1e-11) + + +# -> Circle, Cylinder +def test_core_physics_long_solenoid(): + """ + test if field from solenoid converges to long-solenoid field + Hz = I*N/L "in the center" + """ + I = 1 + N = 10000 + R = 0.8 + L = 100 + Hz_long = N * I / L + + # test with solenoid constructed from circle fields + H = magpy.core.current_circle_field( + field="H", + observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), + diameter=np.array([2 * R] * N), + current=np.array([I] * N), + ) + Hz_sol = np.sum(H, axis=0)[2] + np.testing.assert_allclose(Hz_sol, Hz_long, rtol=1e-3) + + # test with cylinder field (using current replacement) + Jz = MU0 * I * N / L # polarization <> current density + Hz_cyl = magpy.core.magnet_cylinder_field( + field="H", + observers=np.array([(0, 0, 0)]), + dimension=np.array([(2 * R, L)]), + polarization=np.array([(0, 0, Jz)]), + )[0, 2] + np.testing.assert_allclose(Hz_cyl, Hz_long, rtol=1e-3) + + +# Circle<>Cylinder +def test_core_physics_current_replacement(): + """ + test if the Cylinder field is given by a replacement current sheet + that carries a current density of j=magnetization + It follows: + j = I*N/L == J/MU0 + -> I = J/MU0/N*L + near-field test + """ + L = 0.5 + R = 0.987 + obs = np.array([(1.5, -2, -1.123)]) + + Jz = 1 + Hz_cyl = magpy.core.magnet_cylinder_field( + field="H", + observers=obs, + dimension=np.array([(2 * R, L)]), + polarization=np.array([(0, 0, Jz)]), + )[0, 2] + + N = 1000 # current discretization + I = Jz / MU0 / N * L + H = magpy.core.current_circle_field( + field="H", + observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N) + obs, + diameter=np.array([2 * R] * N), + current=np.array([I] * N), + ) + Hz_curr = np.sum(H, axis=0)[2] + + np.testing.assert_allclose(Hz_curr, Hz_cyl, rtol=1e-4) + + +# Cylinder<>CylinderSegment +def test_core_physics_geometry_cylinder_from_segments(): + """test if multiple Cylinder segments create the same field as fully cylinder""" + r = 1.23 + h = 3 + obs = np.array([(1, 2, 3), (0.23, 0.132, 0.123)]) + pol = np.array([(2, 0.123, 3), (-0.23, -1, 0.434)]) + + B_cyl = magpy.core.magnet_cylinder_field( + field="B", + observers=obs, + dimension=np.array([(2 * r, h)] * 2), + polarization=pol, + ) + sections = np.array([-12, 65, 123, 180, 245, 348]) + + Bseg = np.zeros((2, 3)) + for phi1, phi2 in zip(sections[:-1], sections[1:]): + B_part = magpy.core.magnet_cylinder_segment_field( + field="B", + observers=obs, + dimension=np.array([(0, r, h, phi1, phi2)] * 2), + polarization=pol, + ) + Bseg[0] += B_part[0] + Bseg[1] += B_part[1] + np.testing.assert_allclose(B_cyl, Bseg) + + +# Dipole<>Cuboid, Cylinder, CylinderSegment, Tetrahedron +def test_core_physics_dipole_approximation_magnet_far_field(): + """test if all magnets satisfy the dipole approximation""" + obs = np.array([(100, 200, 300), (-200, -200, -200)]) + + mom = np.array([(1e6, 2e6, 3e6)] * 2) + Bdip = magpy.core.dipole_field( + field="H", + observers=obs, + moment=mom, + ) + + dim = np.array([(2, 2, 2)] * 2) + vol = 8 + pol = mom / vol * MU0 + Bcub = magpy.core.magnet_cuboid_field( + field="H", + observers=obs, + dimension=dim, + polarization=pol, + ) + np.testing.assert_allclose(Bdip, Bcub) + + dim = np.array([(0.5, 0.5)] * 2) + vol = 0.25**2 * np.pi * 0.5 + pol = mom / vol * MU0 + Bcyl = magpy.core.magnet_cylinder_field( + field="H", + observers=obs, + dimension=dim, + polarization=pol, + ) + np.testing.assert_allclose(Bdip, Bcyl) + + vert = np.array([[(0, 0, 0), (0, 0, 0.1), (0.1, 0, 0), (0, 0.1, 0)]] * 2) + vol = 1 / 6 * 1e-3 + pol = mom / vol * MU0 + Btetra = magpy.core.magnet_tetrahedron_field( + field="H", + observers=obs, + vertices=vert, + polarization=pol, + ) + np.testing.assert_allclose(Bdip, Btetra, rtol=1e-3) + + dim = np.array([(0.1, 0.2, 0.1, -25, 25)] * 2) + vol = 3 * np.pi * (50 / 360) * 1e-3 + pol = mom / vol * MU0 + Bcys = magpy.core.magnet_cylinder_segment_field( + field="H", + observers=obs + np.array((0.15, 0, 0)), + dimension=dim, + polarization=pol, + ) + np.testing.assert_allclose(Bdip, Bcys, rtol=1e-4) + + +# --> Circle +def test_core_physics_circle_VS_webpage_numbers(): + """ + Compare Circle on-axis field vs e-magnetica & hyperphysics + """ + dia = np.array([2] * 4) + curr = np.array([1e3] * 4) # A + zs = [0, 1, 2, 3] + obs = np.array([(0, 0, z) for z in zs]) + + # values from e-magnetica + Hz = [500, 176.8, 44.72, 15.81] + Htest = [(0, 0, hz) for hz in Hz] + + H = magpy.core.current_circle_field( + field="H", + observers=obs, + diameter=dia, + current=curr, + ) + np.testing.assert_allclose(H, Htest, rtol=1e-3) + + # values from hyperphysics + Bz = [ + 0.6283185307179586e-3, + 2.2214414690791835e-4, + 5.619851784832581e-5, + 1.9869176531592205e-5, + ] + Btest = [(0, 0, bz) for bz in Bz] + + B = magpy.core.current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=curr, + ) + np.testing.assert_allclose(B, Btest, rtol=1e-7) + + +# Cuboid <> Polyline +def test_core_physics_cube_current_replacement(): + """compare cuboid field with current replacement""" + obs = np.array([(2, 2, 3.13), (-2.123, -4, 2)]) + h = 1 + Jz = 1.23 + dim = np.array([(2, 2, h)] * 2) + pol = np.array([(0, 0, Jz)] * 2) + Hcub = magpy.core.magnet_cuboid_field( + field="H", + observers=obs, + dimension=dim, + polarization=pol, + ) + + # construct from polylines + n = 1000 + vert = np.array([(1, 1, 0), (1, -1, 0), (-1, -1, 0), (-1, 1, 0), (1, 1, 0)]) + curr = h / n * Jz / MU0 + hs = np.linspace(-h / 2, h / 2, n) + hpos = np.array([(0, 0, h) for h in hs]) + + obs1 = np.array([obs[0] + hp for hp in hpos] * 4) + obs2 = np.array([obs[1] + hp for hp in hpos] * 4) + + start = np.repeat(vert[:-1], n, axis=0) + end = np.repeat(vert[1:], n, axis=0) + + Hcurr = np.zeros((2, 3)) + for i, obss in enumerate([obs1, obs2]): + h = magpy.core.current_polyline_field( + field="H", + observers=obss, + segment_start=start, + segment_end=end, + current=np.array([curr] * 4 * n), + ) + Hcurr[i] = np.sum(h, axis=0) + + np.testing.assert_allclose(Hcub, -Hcurr, rtol=1e-4) From 627704371213fcd634514b422caaff77a8b150fe Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 24 Dec 2023 10:35:01 +0100 Subject: [PATCH 057/240] complete core testing --- magpylib/_src/fields/field_BH_circle.py | 3 +- ...e_field_functions.py => test_core_misc.py} | 732 ++++++++---------- tests/test_core_physics_consistency.py | 160 +++- tests/test_getBH_interfaces.py | 70 ++ 4 files changed, 538 insertions(+), 427 deletions(-) rename tests/{test_core_field_functions.py => test_core_misc.py} (54%) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index 59b6a364f..f6893ce01 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -157,4 +157,5 @@ def current_loop_field(*args, **kwargs): MagpylibDeprecationWarning, stacklevel=2, ) - return current_circle_field(*args, **kwargs) + # return current_circle_field(*args, **kwargs) + return None diff --git a/tests/test_core_field_functions.py b/tests/test_core_misc.py similarity index 54% rename from tests/test_core_field_functions.py rename to tests/test_core_misc.py index 50420a9ee..f3c62f18a 100644 --- a/tests/test_core_field_functions.py +++ b/tests/test_core_misc.py @@ -25,7 +25,7 @@ ####################################################################################### ####################################################################################### -# BASIC FIELD COMPUTATION TESTS +# NEW V5 BASIC FIELD COMPUTATION TESTS def test_magnet_cuboid_field_BH(): @@ -498,38 +498,48 @@ def test_current_circle_field_BH(): np.testing.assert_allclose(H, Htest) -# def test_current_polyline_field_BH(): -# """Test of current polyline field core function""" -# vert=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0), (1.5,0,0)]) -# B = magpy.core.current_polyline_field( -# field="B", -# observers=np.array([(0,0,1)]*3), -# segment_start=vert[:-1], -# segment_end=vert[1:], -# current=np.array([1, 1, 2]), -# ) -# H = magpy.core.current_polyline_field( -# field="H", -# observers=np.array([(0,0,1)]*3), -# segment_start=vert[:-1], -# segment_end=vert[1:], -# current=np.array([1, 1, 2]), -# ) -# np.testing.assert_allclose(B, MU0 * H) - -# Btest = np.array([ -# [0.0, -0.03848367, 0.0], -# [0.0, -0.08944272, 0.0], -# [0.0, -0.03848367, 0.0], -# ])*1e-6 -# np.testing.assert_allclose(B, Btest, rtol=0, atol=1e-7) - -# Htest = np.array([ -# [0.0, -30624.33145161, 0.0], -# [0.0, -71176.25434172, 0.0], -# [0.0, -30624.33145161, 0.0], -# ])*1e-6 -# np.testing.assert_allclose(H, Htest, rtol=0, atol=1e-7) +def test_current_polyline_field_BH(): + """Test of current polyline field core function""" + vert = np.array([(-1.5, 0, 0), (-0.5, 0, 0), (0.5, 0, 0), (1.5, 0, 0)]) + B = magpy.core.current_polyline_field( + field="B", + observers=np.array([(0, 0, 1)] * 3), + segment_start=vert[:-1], + segment_end=vert[1:], + current=np.array([1, 1, 1]), + ) + H = magpy.core.current_polyline_field( + field="H", + observers=np.array([(0, 0, 1)] * 3), + segment_start=vert[:-1], + segment_end=vert[1:], + current=np.array([1, 1, 1]), + ) + np.testing.assert_allclose(B, MU0 * H) + + Btest = ( + np.array( + [ + [0.0, -0.03848367, 0.0], + [0.0, -0.08944272, 0.0], + [0.0, -0.03848367, 0.0], + ] + ) + * 1e-6 + ) + np.testing.assert_allclose(B, Btest, rtol=0, atol=1e-7) + + Htest = ( + np.array( + [ + [0.0, -30624.33145161, 0.0], + [0.0, -71176.25434172, 0.0], + [0.0, -30624.33145161, 0.0], + ] + ) + * 1e-6 + ) + np.testing.assert_allclose(H, Htest, rtol=0, atol=1e-7) def test_dipole_field_BH(): @@ -569,14 +579,17 @@ def test_dipole_field_BH(): ####################################################################################### ####################################################################################### -# FIELD COMPUTATION PHYSICS CONSISTENCY TESTS +# OTHER TESTS AND V4 TESTS -####################################################################################### -####################################################################################### -####################################################################################### +def test_line_deprecation(): + with pytest.warns(MagpylibDeprecationWarning): + x = current_line_field("B", 1, 2, 3, 4) -# OLD FIELD COMPUTATION TESTS - SPECIAL CASES + +def test_loop_deprecation(): + with pytest.warns(MagpylibDeprecationWarning): + x = current_loop_field("B", 1, 2, 3, 4) def test_field_loop_specials(): @@ -585,11 +598,14 @@ def test_field_loop_specials(): dia = np.array([2, 2, 0, 0, 2, 2]) obs = np.array([(0, 0, 0), (1, 0, 0), (0, 0, 0), (1, 0, 0), (1, 0, 0), (0, 0, 0)]) - B = current_circle_field( - field="B", - observers=obs, - diameter=dia, - current=cur, + B = ( + current_circle_field( + field="B", + observers=obs, + diameter=dia, + current=cur, + ) + * 1e6 ) Btest = [ [0, 0, 0.62831853], @@ -611,35 +627,44 @@ def test_field_line_special_cases(): pe1 = np.array([(2, 2, 2)]) # only normal - B1 = current_polyline_field( - field="B", - observers=po1, - current=c1, - segment_start=ps1, - segment_end=pe1, + B1 = ( + current_polyline_field( + field="B", + observers=po1, + current=c1, + segment_start=ps1, + segment_end=pe1, + ) + * 1e6 ) x1 = np.array([[0.02672612, -0.05345225, 0.02672612]]) assert_allclose(x1, B1, rtol=1e-6) # only on_line po1b = np.array([(1, 1, 1)]) - B2 = current_polyline_field( - field="B", - observers=po1b, - current=c1, - segment_start=ps1, - segment_end=pe1, + B2 = ( + current_polyline_field( + field="B", + observers=po1b, + current=c1, + segment_start=ps1, + segment_end=pe1, + ) + * 1e6 ) x2 = np.zeros((1, 3)) assert_allclose(x2, B2, rtol=1e-6) # only zero-segment - B3 = current_polyline_field( - field="B", - observers=po1, - current=c1, - segment_start=ps1, - segment_end=ps1, + B3 = ( + current_polyline_field( + field="B", + observers=po1, + current=c1, + segment_start=ps1, + segment_end=ps1, + ) + * 1e6 ) x3 = np.zeros((1, 3)) assert_allclose(x3, B3, rtol=1e-6) @@ -649,36 +674,45 @@ def test_field_line_special_cases(): ps2 = np.array([(0, 0, 0)] * 2) pe2 = np.array([(0, 0, 0), (2, 2, 2)]) po2 = np.array([(1, 2, 3), (1, 1, 1)]) - B4 = current_polyline_field( - field="B", - observers=po2, - current=c2, - segment_start=ps2, - segment_end=pe2, + B4 = ( + current_polyline_field( + field="B", + observers=po2, + current=c2, + segment_start=ps2, + segment_end=pe2, + ) + * 1e6 ) x4 = np.zeros((2, 3)) assert_allclose(x4, B4, rtol=1e-6) # normal + zero_segment po2b = np.array([(1, 2, 3), (1, 2, 3)]) - B5 = current_polyline_field( - field="B", - observers=po2b, - current=c2, - segment_start=ps2, - segment_end=pe2, + B5 = ( + current_polyline_field( + field="B", + observers=po2b, + current=c2, + segment_start=ps2, + segment_end=pe2, + ) + * 1e6 ) x5 = np.array([[0, 0, 0], [0.02672612, -0.05345225, 0.02672612]]) assert_allclose(x5, B5, rtol=1e-6) # normal + on_line pe2b = np.array([(2, 2, 2)] * 2) - B6 = current_polyline_field( - field="B", - observers=po2, - current=c2, - segment_start=ps2, - segment_end=pe2b, + B6 = ( + current_polyline_field( + field="B", + observers=po2, + current=c2, + segment_start=ps2, + segment_end=pe2b, + ) + * 1e6 ) x6 = np.array([[0.02672612, -0.05345225, 0.02672612], [0, 0, 0]]) assert_allclose(x6, B6, rtol=1e-6) @@ -688,365 +722,213 @@ def test_field_line_special_cases(): ps4 = np.array([(0, 0, 0)] * 3) pe4 = np.array([(0, 0, 0), (2, 2, 2), (2, 2, 2)]) po4 = np.array([(1, 2, 3), (1, 2, 3), (1, 1, 1)]) - B7 = current_polyline_field( - field="B", - observers=po4, - current=c4, - segment_start=ps4, - segment_end=pe4, + B7 = ( + current_polyline_field( + field="B", + observers=po4, + current=c4, + segment_start=ps4, + segment_end=pe4, + ) + * 1e6 ) x7 = np.array([[0, 0, 0], [0.02672612, -0.05345225, 0.02672612], [0, 0, 0]]) assert_allclose(x7, B7, rtol=1e-6) -####################################################################################### -####################################################################################### -####################################################################################### +def test_field_loop2(): + """test if field function accepts correct inputs""" + curr = np.array([1]) + dia = np.array([2]) + obs = np.array([[0, 0, 0]]) + B = current_circle_field( + field="B", + observers=obs, + current=curr, + diameter=dia, + ) -# OTHER + curr = np.array([1] * 2) + dia = np.array([2] * 2) + obs = np.array([[0, 0, 0]] * 2) + B2 = current_circle_field( + field="B", + observers=obs, + current=curr, + diameter=dia, + ) + assert_allclose(B, (B2[0],)) + assert_allclose(B, (B2[1],)) -# def test_field_loop2(): -# """test if field function accepts correct inputs""" -# curr = np.array([1]) -# dim = np.array([2]) -# poso = np.array([[0, 0, 0]]) -# B = current_circle_field("B", poso, curr, dim) -# curr = np.array([1] * 2) -# dim = np.array([2] * 2) -# poso = np.array([[0, 0, 0]] * 2) -# B2 = current_circle_field("B", poso, curr, dim) +def test_field_line_from_vert(): + """test the Polyline field from vertex input""" + observers = np.array([(1, 2, 2), (1, 2, 3), (-1, 0, -3)]) + current = np.array([1, 5, -3]) -# assert_allclose(B, (B2[0],)) -# assert_allclose(B, (B2[1],)) + vertices = np.array( + [ + np.array( + [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (1, 2, 3), (-3, 4, -5)] + ), + np.array([(0, 0, 0), (3, 3, 3), (-3, 4, -5)]), + np.array([(1, 2, 3), (-2, -3, 3), (3, 2, 1), (3, 3, 3)]), + ], + dtype="object", + ) + B_vert = current_vertices_field( + field="B", + observers=observers, + vertices=vertices, + current=current, + ) + + B = [] + for obs, vert, curr in zip(observers, vertices, current): + p1 = vert[:-1] + p2 = vert[1:] + po = np.array([obs] * (len(vert) - 1)) + cu = np.array([curr] * (len(vert) - 1)) + B += [ + np.sum( + current_polyline_field( + field="B", + observers=po, + current=cu, + segment_start=p1, + segment_end=p2, + ), + axis=0, + ) + ] + B = np.array(B) -def test_line_deprecation(): - with pytest.warns(MagpylibDeprecationWarning): - x = current_line_field("B", 1, 2, 3, 4) + assert_allclose(B_vert, B) -def test_loop_deprecation(): - with pytest.warns(MagpylibDeprecationWarning): - x = current_loop_field("B", 1, 2, 3, 4) +def test_field_line_v4(): + """test current_line_Bfield() for all cases""" + cur = np.array([1] * 7) + start = np.array([(-1, 0, 0)] * 7) + end = np.array([(1, 0, 0), (-1, 0, 0), (1, 0, 0), (-1, 0, 0)] + [(1, 0, 0)] * 3) + obs = np.array( + [ + (0, 0, 1), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 1e-16), + (2, 0, 1), + (-2, 0, 1), + ] + ) + B = ( + current_polyline_field( + field="B", + observers=obs, + current=cur, + segment_start=start, + segment_end=end, + ) + * 1e6 + ) + Btest = np.array( + [ + [0, -0.14142136, 0], + [0, 0.0, 0], + [0, 0.0, 0], + [0, 0.0, 0], + [0, 0.0, 0], + [0, -0.02415765, 0], + [0, -0.02415765, 0], + ] + ) + np.testing.assert_allclose(B, Btest) + + +def test_triangle5(): + """special case tests on edges - result is continuous and 0 for vertical component""" + btest1 = [ + [26.29963526814195, 15.319834473660082, 0.0], + [54.91549594789228, 41.20535983076747, 0.0], + [32.25241487782939, 15.087161660417559, 0.0], + [10.110611199952707, -11.41176203622237, 0.0], + [-3.8084378251737285, -30.875600143560657, -0.0], + [-15.636505140623612, -50.00854548249858, -0.0], + [-27.928308992688645, -72.80800891847107, -0.0], + [-45.34417750711242, -109.5871836961927, -0.0], + [-36.33970306054345, 12.288824457077656, 0.0], + [-16.984738462958845, 4.804383318447626, 0.0], + ] + + btest2 = [ + [15.31983447366009, 26.299635268142033, 0.0], + [41.20535983076747, 54.91549594789104, 0.0], + [-72.61316618947018, 32.25241487782958, 0.0], + [-54.07597251255013, 10.110611199952693, 0.0], + [-44.104089712909634, -3.808437825173785, -0.0], + [-36.78005591314963, -15.636505140623605, -0.0], + [-30.143798442143236, -27.92830899268858, -0.0], + [-21.886855846306176, -45.34417750711366, -0.0], + [12.288824457077965, -36.33970306054315, 0.0], + [4.80438331844773, -16.98473846295874, 0.0], + ] + + n = 10 + ts = np.linspace(-1, 6, n) + obs1 = np.array([(t, 0, 0) for t in ts]) + obs2 = np.array([(0, t, 0) for t in ts]) + mag = np.array([(111, 222, 333)] * n) + ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]] * n) + + b1 = ( + magpy.core.triangle_field( + field="H", + observers=obs1, + polarization=mag, + vertices=ver, + ) + * 1e-6 + ) + np.testing.assert_allclose(btest1, b1) + b2 = ( + magpy.core.triangle_field( + field="H", + observers=obs2, + polarization=mag, + vertices=ver, + ) + * 1e-6 + ) + np.testing.assert_allclose(btest2, b2) -# def test_field_line_from_vert(): -# """test the Polyline field from vertex input""" -# observers = np.array([(1, 2, 2), (1, 2, 3), (-1, 0, -3)]) -# current = np.array([1, 5, -3]) - -# vertices = np.array( -# [ -# np.array( -# [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (1, 2, 3), (-3, 4, -5)] -# ), -# np.array([(0, 0, 0), (3, 3, 3), (-3, 4, -5)]), -# np.array([(1, 2, 3), (-2, -3, 3), (3, 2, 1), (3, 3, 3)]), -# ], -# dtype="object", -# ) - -# B_vert = current_vertices_field("B", observers, current, vertices) - -# B = [] -# for obs, vert, curr in zip(observers, vertices, current): -# p1 = vert[:-1] -# p2 = vert[1:] -# po = np.array([obs] * (len(vert) - 1)) -# cu = np.array([curr] * (len(vert) - 1)) -# B += [np.sum(current_polyline_field("B", po, cu, p1, p2), axis=0)] -# B = np.array(B) - -# assert_allclose(B_vert, B) - - -# def test_field_line_v4(): -# """test current_line_Bfield() for all cases""" -# cur = np.array([1] * 7) -# start = np.array([(-1, 0, 0)] * 7) -# end = np.array([(1, 0, 0), (-1, 0, 0), (1, 0, 0), (-1, 0, 0)] + [(1, 0, 0)] * 3) -# obs = np.array( -# [ -# (0, 0, 1), -# (0, 0, 0), -# (0, 0, 0), -# (0, 0, 0), -# (0, 0, 1e-16), -# (2, 0, 1), -# (-2, 0, 1), -# ] -# ) -# B = current_polyline_field("B", obs, cur, start, end) -# Btest = np.array( -# [ -# [0, -0.14142136, 0], -# [0, 0.0, 0], -# [0, 0.0, 0], -# [0, 0.0, 0], -# [0, 0.0, 0], -# [0, -0.02415765, 0], -# [0, -0.02415765, 0], -# ] -# ) -# np.testing.assert_allclose(B, Btest) - - -# def test_triangle1(): -# """test core triangle VS cube""" -# obs = np.array([(3, 4, 5)] * 4) -# mag = np.array([(0, 0, 333)] * 4) -# fac = np.array( -# [ -# [(-1, -1, 1), (1, -1, 1), (-1, 1, 1)], # top1 -# [(1, -1, -1), (-1, -1, -1), (-1, 1, -1)], # bott1 -# [(1, -1, 1), (1, 1, 1), (-1, 1, 1)], # top2 -# [(1, 1, -1), (1, -1, -1), (-1, 1, -1)], # bott2 -# ] -# ) -# b = magpy.core.triangle_field("B", obs, mag, fac) -# b = np.sum(b, axis=0) - -# obs = np.array([(3, 4, 5)]) -# mag = np.array([(0, 0, 333)]) -# dim = np.array([(2, 2, 2)]) -# bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] - -# np.testing.assert_allclose(b, bb) - - -# def test_triangle2(): -# """test core single triangle vs same surface split up into 4 triangular faces""" -# obs = np.array([(3, 4, 5)]) -# mag = np.array([(111, 222, 333)]) -# fac = np.array( -# [ -# [(0, 0, 0), (10, 0, 0), (0, 10, 0)], -# ] -# ) -# b = magpy.core.triangle_field("B", obs, mag, fac) -# b = np.sum(b, axis=0) - -# obs = np.array([(3, 4, 5)] * 4) -# mag = np.array([(111, 222, 333)] * 4) -# fac = np.array( -# [ -# [(0, 0, 0), (3, 0, 0), (0, 10, 0)], -# [(3, 0, 0), (5, 0, 0), (0, 10, 0)], -# [(5, 0, 0), (6, 0, 0), (0, 10, 0)], -# [(6, 0, 0), (10, 0, 0), (0, 10, 0)], -# ] -# ) -# bb = magpy.core.triangle_field("B", obs, mag, fac) -# bb = np.sum(bb, axis=0) - -# np.testing.assert_allclose(b, bb) - - -# def test_triangle3(): -# """test core tetrahedron vs cube""" -# ver = np.array( -# [ -# [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], -# [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], -# [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], -# [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], -# [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], -# [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], -# ] -# ) - -# mags = [ -# [1.03595366, 0.42840487, 0.10797529], -# [0.33513152, 1.61629547, 0.15959791], -# [0.29904441, 1.32185041, 1.81218046], -# [0.82665456, 1.86827489, 1.67338911], -# [0.97619806, 1.52323106, 1.63628455], -# [1.70290645, 1.49610608, 0.13878711], -# [1.49886747, 1.55633919, 1.41351862], -# [0.9959534, 0.62059942, 1.28616663], -# [0.60114354, 0.96120344, 0.32009221], -# [0.83133901, 0.7925518, 0.64574592], -# ] - -# obss = [ -# [0.82811352, 1.77818627, 0.19819379], -# [0.84147235, 1.10200857, 1.51687527], -# [0.30751474, 0.89773196, 0.56468564], -# [1.87437889, 1.55908581, 1.10579983], -# [0.64810548, 1.38123846, 1.90576802], -# [0.48981034, 0.09376294, 0.53717129], -# [1.42826412, 0.30246674, 0.57649909], -# [1.58376758, 1.70420478, 0.22894022], -# [0.26791832, 0.36839769, 0.67934335], -# [1.15140149, 0.10549875, 0.98304184], -# ] - -# for mag in mags: -# for obs in obss: -# obs6 = np.tile(obs, (6, 1)) -# mag6 = np.tile(mag, (6, 1)) -# b = magpy.core.magnet_tetrahedron_field("B", obs6, mag6, ver) -# h = magpy.core.magnet_tetrahedron_field("H", obs6, mag6, ver) -# b = np.sum(b, axis=0) -# h = np.sum(h, axis=0) - -# obs1 = np.reshape(obs, (1, 3)) -# mag1 = np.reshape(mag, (1, 3)) -# dim = np.array([(2, 2, 2)]) -# bb = magpy.core.magnet_cuboid_field("B", obs1, mag1, dim)[0] -# hh = magpy.core.magnet_cuboid_field("H", obs1, mag1, dim)[0] -# np.testing.assert_allclose(b, bb) -# np.testing.assert_allclose(h, hh) - - -# def test_triangle4(): -# """test core tetrahedron vs cube""" -# obs = np.array([(3, 4, 5)] * 6) -# mag = np.array([(111, 222, 333)] * 6) -# ver = np.array( -# [ -# [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], -# [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], -# [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], -# [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], -# [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], -# [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], -# ] -# ) -# b = magpy.core.magnet_tetrahedron_field("B", obs, mag, ver) -# b = np.sum(b, axis=0) - -# obs = np.array([(3, 4, 5)]) -# mag = np.array([(111, 222, 333)]) -# dim = np.array([(2, 2, 2)]) -# bb = magpy.core.magnet_cuboid_field("B", obs, mag, dim)[0] - -# np.testing.assert_allclose(b, bb) - - -# def test_triangle5(): -# """special case tests on edges - result is continuous and 0 for vertical component""" -# btest1 = [ -# [26.29963526814195, 15.319834473660082, 0.0], -# [54.91549594789228, 41.20535983076747, 0.0], -# [32.25241487782939, 15.087161660417559, 0.0], -# [10.110611199952707, -11.41176203622237, 0.0], -# [-3.8084378251737285, -30.875600143560657, -0.0], -# [-15.636505140623612, -50.00854548249858, -0.0], -# [-27.928308992688645, -72.80800891847107, -0.0], -# [-45.34417750711242, -109.5871836961927, -0.0], -# [-36.33970306054345, 12.288824457077656, 0.0], -# [-16.984738462958845, 4.804383318447626, 0.0], -# ] - -# btest2 = [ -# [15.31983447366009, 26.299635268142033, 0.0], -# [41.20535983076747, 54.91549594789104, 0.0], -# [-72.61316618947018, 32.25241487782958, 0.0], -# [-54.07597251255013, 10.110611199952693, 0.0], -# [-44.104089712909634, -3.808437825173785, -0.0], -# [-36.78005591314963, -15.636505140623605, -0.0], -# [-30.143798442143236, -27.92830899268858, -0.0], -# [-21.886855846306176, -45.34417750711366, -0.0], -# [12.288824457077965, -36.33970306054315, 0.0], -# [4.80438331844773, -16.98473846295874, 0.0], -# ] - -# n = 10 -# ts = np.linspace(-1, 6, n) -# obs1 = np.array([(t, 0, 0) for t in ts]) -# obs2 = np.array([(0, t, 0) for t in ts]) -# mag = np.array([(111, 222, 333)] * n) -# ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]] * n) - -# b1 = magpy.core.triangle_field("H", obs1, mag, ver) -# np.testing.assert_allclose(btest1, b1) -# b2 = magpy.core.triangle_field("H", obs2, mag, ver) -# np.testing.assert_allclose(btest2, b2) - - -# def test_triangle6(): -# """special case tests on corners - result is nan""" -# obs1 = np.array([(0, 0, 0)]) -# obs2 = np.array([(0, 5, 0)]) -# obs3 = np.array([(5, 0, 0)]) -# mag = np.array([(111, 222, 333)]) -# ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]]) -# b1 = magpy.core.triangle_field("B", obs1, mag, ver) -# b2 = magpy.core.triangle_field("B", obs2, mag, ver) -# b3 = magpy.core.triangle_field("B", obs3, mag, ver) - -# for b in [b1, b2, b3]: -# np.testing.assert_equal(b, np.array([[np.nan] * 3])) - - -# @pytest.mark.parametrize( -# ("module", "class_", "missing_arg"), -# [ -# ("magnet", "Cuboid", "dimension"), -# ("magnet", "Cylinder", "dimension"), -# ("magnet", "CylinderSegment", "dimension"), -# ("magnet", "Sphere", "diameter"), -# ("magnet", "Tetrahedron", "vertices"), -# ("magnet", "TriangularMesh", "vertices"), -# ("current", "Circle", "diameter"), -# ("current", "Polyline", "vertices"), -# ("misc", "Triangle", "vertices"), -# ], -# ) -# def test_getB_on_missing_dimensions(module, class_, missing_arg): -# """test_getB_on_missing_dimensions""" -# with pytest.raises( -# MagpylibMissingInput, -# match=rf"Parameter `{missing_arg}` of .* must be set.", -# ): -# getattr(getattr(magpy, module), class_)().getB([0, 0, 0]) - - -# @pytest.mark.parametrize( -# ("module", "class_", "missing_arg", "kwargs"), -# [ -# ("magnet", "Cuboid", "magnetization", {"dimension": (1, 1, 1)}), -# ("magnet", "Cylinder", "magnetization", {"dimension": (1, 1)}), -# ( -# "magnet", -# "CylinderSegment", -# "magnetization", -# {"dimension": (0, 1, 1, 45, 120)}, -# ), -# ("magnet", "Sphere", "magnetization", {"diameter": 1}), -# ( -# "magnet", -# "Tetrahedron", -# "magnetization", -# {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]}, -# ), -# ( -# "magnet", -# "TriangularMesh", -# "magnetization", -# { -# "vertices": ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), -# "faces": ((0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)), -# }, -# ), -# ("current", "Circle", "current", {"diameter": 1}), -# ("current", "Polyline", "current", {"vertices": [[0, -1, 0], [0, 1, 0]]}), -# ( -# "misc", -# "Triangle", -# "magnetization", -# {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0)]}, -# ), -# ("misc", "Dipole", "moment", {}), -# ], -# ) -# def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): -# """test_getB_on_missing_excitations""" -# with pytest.raises( -# MagpylibMissingInput, -# match=rf"Parameter `{missing_arg}` of .* must be set.", -# ): -# getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) +def test_triangle6(): + """special case tests on corners - result is nan""" + obs1 = np.array([(0, 0, 0)]) + obs2 = np.array([(0, 5, 0)]) + obs3 = np.array([(5, 0, 0)]) + mag = np.array([(1, 2, 3)]) + ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]]) + b1 = magpy.core.triangle_field( + field="B", + observers=obs1, + polarization=mag, + vertices=ver, + ) + b2 = magpy.core.triangle_field( + field="B", + observers=obs2, + polarization=mag, + vertices=ver, + ) + b3 = magpy.core.triangle_field( + field="B", + observers=obs3, + polarization=mag, + vertices=ver, + ) + for b in [b1, b2, b3]: + np.testing.assert_equal(b, np.array([[np.nan] * 3])) diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 94036972a..651f9d197 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -20,13 +20,20 @@ from magpylib.core import magnet_tetrahedron_field from magpylib.core import triangle_field +# PHYSICS CONSISTENCY TESTING +# # Magnetic moment of a current loop with current I and surface A: # mom = I * A +# # Magnetic moment of a homogeneous magnet with magnetization mag and volume vol # mom = vol * mag +# # Current density j on magnet surface in the replacement picture # j = mag = J/MU0 - +# +# Geometric approximation testing should give similar results for different +# implementations when one geometry is constructed from another +# # ----------> Circle # webpage numbers # Circle <> Dipole # mom = I*A (far field approx) # Polyline <> Dipole # mom = I*A (far field approx) @@ -36,6 +43,9 @@ # Polyline <> Cuboid # j = I*N/L == J/MU0 current replacement picture # Circle <> Polyline # geometric approx # Cylinder <> CylinderSegment # geometric approx +# Triangle <> Cuboid # geometric approx +# Triangle <> Triangle # geometric approx +# Cuboid <> Tetrahedron # geometric approx # Circle<>Dipole @@ -438,3 +448,151 @@ def test_core_physics_cube_current_replacement(): Hcurr[i] = np.sum(h, axis=0) np.testing.assert_allclose(Hcub, -Hcurr, rtol=1e-4) + + +def test_core_physics_triangle_cube_geometry(): + """test core triangle VS cube""" + obs = np.array([(3, 4, 5)] * 4) + mag = np.array([(0, 0, 333)] * 4) + fac = np.array( + [ + [(-1, -1, 1), (1, -1, 1), (-1, 1, 1)], # top1 + [(1, -1, -1), (-1, -1, -1), (-1, 1, -1)], # bott1 + [(1, -1, 1), (1, 1, 1), (-1, 1, 1)], # top2 + [(1, 1, -1), (1, -1, -1), (-1, 1, -1)], # bott2 + ] + ) + b = magpy.core.triangle_field( + field="B", + observers=obs, + vertices=fac, + polarization=mag, + ) + b = np.sum(b, axis=0) + + obs = np.array([(3, 4, 5)]) + mag = np.array([(0, 0, 333)]) + dim = np.array([(2, 2, 2)]) + bb = magpy.core.magnet_cuboid_field( + field="B", + observers=obs, + dimension=dim, + polarization=mag, + )[0] + + np.testing.assert_allclose(b, bb) + + +def test_core_physics_triangle_VS_itself(): + """test core single triangle vs same surface split up into 4 triangular faces""" + obs = np.array([(3, 4, 5)]) + mag = np.array([(111, 222, 333)]) + fac = np.array( + [ + [(0, 0, 0), (10, 0, 0), (0, 10, 0)], + ] + ) + b = magpy.core.triangle_field( + field="B", + observers=obs, + polarization=mag, + vertices=fac, + ) + b = np.sum(b, axis=0) + + obs = np.array([(3, 4, 5)] * 4) + mag = np.array([(111, 222, 333)] * 4) + fac = np.array( + [ + [(0, 0, 0), (3, 0, 0), (0, 10, 0)], + [(3, 0, 0), (5, 0, 0), (0, 10, 0)], + [(5, 0, 0), (6, 0, 0), (0, 10, 0)], + [(6, 0, 0), (10, 0, 0), (0, 10, 0)], + ] + ) + bb = magpy.core.triangle_field( + field="B", + observers=obs, + polarization=mag, + vertices=fac, + ) + bb = np.sum(bb, axis=0) + + np.testing.assert_allclose(b, bb) + + +def test_core_physics_Tetrahedron_VS_Cuboid(): + """test core tetrahedron vs cube""" + ver = np.array( + [ + [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], + [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], + [(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1)], + [(-1, 1, -1), (1, -1, -1), (-1, -1, 1), (-1, 1, 1)], + [(1, -1, -1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], + [(-1, 1, -1), (-1, 1, 1), (1, 1, -1), (1, -1, -1)], + ] + ) + + mags = [ + [1.03595366, 0.42840487, 0.10797529], + [0.33513152, 1.61629547, 0.15959791], + [0.29904441, 1.32185041, 1.81218046], + [0.82665456, 1.86827489, 1.67338911], + [0.97619806, 1.52323106, 1.63628455], + [1.70290645, 1.49610608, 0.13878711], + [1.49886747, 1.55633919, 1.41351862], + [0.9959534, 0.62059942, 1.28616663], + [0.60114354, 0.96120344, 0.32009221], + [0.83133901, 0.7925518, 0.64574592], + ] + + obss = [ + [0.82811352, 1.77818627, 0.19819379], + [0.84147235, 1.10200857, 1.51687527], + [0.30751474, 0.89773196, 0.56468564], + [1.87437889, 1.55908581, 1.10579983], + [0.64810548, 1.38123846, 1.90576802], + [0.48981034, 0.09376294, 0.53717129], + [1.42826412, 0.30246674, 0.57649909], + [1.58376758, 1.70420478, 0.22894022], + [0.26791832, 0.36839769, 0.67934335], + [1.15140149, 0.10549875, 0.98304184], + ] + + for mag in mags: + for obs in obss: + obs6 = np.tile(obs, (6, 1)) + mag6 = np.tile(mag, (6, 1)) + b = magpy.core.magnet_tetrahedron_field( + field="B", + observers=obs6, + polarization=mag6, + vertices=ver, + ) + h = magpy.core.magnet_tetrahedron_field( + field="H", + observers=obs6, + polarization=mag6, + vertices=ver, + ) + b = np.sum(b, axis=0) + h = np.sum(h, axis=0) + + obs1 = np.reshape(obs, (1, 3)) + mag1 = np.reshape(mag, (1, 3)) + dim = np.array([(2, 2, 2)]) + bb = magpy.core.magnet_cuboid_field( + field="B", + observers=obs1, + polarization=mag1, + dimension=dim, + )[0] + hh = magpy.core.magnet_cuboid_field( + field="H", + observers=obs1, + polarization=mag1, + dimension=dim, + )[0] + np.testing.assert_allclose(b, bb) + np.testing.assert_allclose(h, hh) diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py index 81ebbaceb..dd8f488f5 100644 --- a/tests/test_getBH_interfaces.py +++ b/tests/test_getBH_interfaces.py @@ -288,3 +288,73 @@ def test_sensor_handedness(): # second index is sensor index, ...,0 -> x from sl must opposite of x from sr np.testing.assert_allclose(B[:, 0, ..., 0], -B[:, 1, ..., 0]) + + +@pytest.mark.parametrize( + ("module", "class_", "missing_arg"), + [ + ("magnet", "Cuboid", "dimension"), + ("magnet", "Cylinder", "dimension"), + ("magnet", "CylinderSegment", "dimension"), + ("magnet", "Sphere", "diameter"), + ("magnet", "Tetrahedron", "vertices"), + ("magnet", "TriangularMesh", "vertices"), + ("current", "Circle", "diameter"), + ("current", "Polyline", "vertices"), + ("misc", "Triangle", "vertices"), + ], +) +def test_getB_on_missing_dimensions(module, class_, missing_arg): + """test_getB_on_missing_dimensions""" + with pytest.raises( + MagpylibMissingInput, + match=rf"Parameter `{missing_arg}` of .* must be set.", + ): + getattr(getattr(magpy, module), class_)().getB([0, 0, 0]) + + +@pytest.mark.parametrize( + ("module", "class_", "missing_arg", "kwargs"), + [ + ("magnet", "Cuboid", "magnetization", {"dimension": (1, 1, 1)}), + ("magnet", "Cylinder", "magnetization", {"dimension": (1, 1)}), + ( + "magnet", + "CylinderSegment", + "magnetization", + {"dimension": (0, 1, 1, 45, 120)}, + ), + ("magnet", "Sphere", "magnetization", {"diameter": 1}), + ( + "magnet", + "Tetrahedron", + "magnetization", + {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]}, + ), + ( + "magnet", + "TriangularMesh", + "magnetization", + { + "vertices": ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), + "faces": ((0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)), + }, + ), + ("current", "Circle", "current", {"diameter": 1}), + ("current", "Polyline", "current", {"vertices": [[0, -1, 0], [0, 1, 0]]}), + ( + "misc", + "Triangle", + "magnetization", + {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0)]}, + ), + ("misc", "Dipole", "moment", {}), + ], +) +def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): + """test_getB_on_missing_excitations""" + with pytest.raises( + MagpylibMissingInput, + match=rf"Parameter `{missing_arg}` of .* must be set.", + ): + getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) From 07dced678594a80b0f2ffce4842fe12133fca07e Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 24 Dec 2023 13:03:39 +0100 Subject: [PATCH 058/240] fix core examples --- magpylib/_src/fields/field_BH_circle.py | 18 +++++++++--------- magpylib/_src/fields/field_BH_polyline.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index f6893ce01..3f322432d 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -50,20 +50,20 @@ def current_circle_field( Examples -------- - Compute the field of three different loops at three different positions. + Compute the field of three different circular loops at three different positions. >>> import numpy as np >>> import magpylib as magpy - >>> B = magpy.core.current_circle_field( - >>> field='B', - >>> observers=np.array([(1,1,1), (2,2,2), (3,3,3)]), - >>> diameter=np.array([2,4,6]), + >>> H = magpy.core.current_circle_field( + >>> field='H', + >>> observers=np.array([(0,0,0), (1,1,1), (2,2,2)]), + >>> diameter=np.array([1,2,3]), >>> current=np.array([1,1,2]) >>> ) - >>> print(B) - [[0.06235974 0.06235974 0.02669778] - [0.03117987 0.03117987 0.01334889] - [0.04157316 0.04157316 0.01779852]] + >>> print(H) + [[0. 0. 1. ] + [0.0496243 0.0496243 0.02124542] + [0.02833835 0.02833835 0.00654999]] Notes ----- diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 7eb71be07..d0927c776 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -110,17 +110,17 @@ def current_polyline_field( >>> import numpy as np >>> import magpylib as magpy - >>> B = magpy.core.current_polyline_field( - >>> field='B', + >>> H = magpy.core.current_polyline_field( + >>> field='H', >>> observers=np.array([( 0,0,1)]*3), - >>> segment_start=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0)]), - >>> segment_end=np.array([(-.5,0,0), (.5,0,0), (1.5,0,0)]), - >>> current=np.array([1,1,1]) + >>> segment_start=np.array([(-.5,0,0), (.5,0,0), (1.5,0,0)]), + >>> segment_end=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0)]), + >>> current=np.array([1,1,1]), >>> ) - >>> print(B) - [[ 0. -0.03848367 0. ] - [ 0. -0.08944272 0. ] - [ 0. -0.03848367 0. ]] + >>> print(H) + [[ 0. 0.03062433 -0. ] + [ 0. 0.07117625 -0. ] + [ 0. 0.03062433 -0. ]] Notes ----- From a9201e353b3e64eadd0614bcb94771865e18e61c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 13:24:22 +0100 Subject: [PATCH 059/240] fix Cuboid units --- .../_src/obj_classes/class_magnet_Cuboid.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index b2acbbf06..cc968b916 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -19,17 +19,6 @@ class Cuboid(BaseMagnet): Parameters ---------- - polarization: array_like, shape (3,), default=`None` - Magnetic polarization vector J = mu0*M in units of T, - given in the local object coordinates (rotates with object). - - magnetization: array_like, shape (3,), default=`None` - Magnetization vector M = J/mu0 in units of A/m, - given in the local object coordinates (rotates with object). - - dimension: array_like, shape (3,), default=`None` - Length of the cuboid sides [a,b,c] in meters. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` Object position(s) in the global coordinates in meter. For m>1, the `position` and `orientation` attributes together @@ -40,6 +29,17 @@ class Cuboid(BaseMagnet): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + dimension: array_like, shape (3,), default=`None` + Length of the cuboid sides [a,b,c] in meters. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -54,12 +54,12 @@ class Cuboid(BaseMagnet): Examples -------- `Cuboid` magnets are magnetic field sources. Below we compute the H-field in A/m of a - cubical magnet with magnetic polarization of (.5,.6,.7) in units of T and 1 mm sides - at the observer position (1,1,1) given in units of mm: + cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of tesla and 0.01 m sides + at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(1,1,1)) - >>> H = src.getH((1,1,1)) + >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(.01,.01,.01)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) [16149.04136518 14906.80741401 13664.57346284] @@ -67,7 +67,7 @@ class Cuboid(BaseMagnet): >>> src.rotate_from_angax(45, 'x') Cuboid(id=...) - >>> B = src.getB([(1,0,0), (0,1,0), (0,0,1)]) + >>> B = src.getB([(.01,0,0), (0,.01,0), (0,0,.01)]) >>> print(B) [[ 0.06739119 0.00476528 -0.0619486 ] [-0.03557183 -0.01149497 -0.08403664] @@ -120,5 +120,5 @@ def _default_style_description(self): """Default style description text""" if self.dimension is None: return "no dimension" - d = [unit_prefix(d / 1000) for d in self.dimension] - return f"{d[0]}m|{d[1]}m|{d[2]}m" + d = [unit_prefix(d) for d in self.dimension] + return f"{d[0]}|{d[1]}|{d[2]}" From 05009cb6627be1603182588236feeb860702263d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 13:35:11 +0100 Subject: [PATCH 060/240] fix Cylinder --- .../_src/obj_classes/class_magnet_Cylinder.py | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index d123d9db2..8982d0b1f 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -17,15 +17,8 @@ class Cylinder(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - dimension: array_like, shape (2,), default=`None` - Dimension (d,h) denote diameter and height of the cylinder in units of mm. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -33,6 +26,17 @@ class Cylinder(BaseMagnet): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + dimension: array_like, shape (2,), default=`None` + Dimension (d,h) denote diameter and height of the cylinder in units of meter. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector (mu0*M, remanence field) in units of mT given in + the local object coordinates (rotates with object). + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -46,31 +50,31 @@ class Cylinder(BaseMagnet): Examples -------- - `Cylinder` magnets are magnetic field sources. Below we compute the H-field in kA/m of a - cylinder magnet with magnetization (100,200,300) in units of mT and 1 mm diameter and height - at the observer position (1,1,1) given in units of mm: + `Cylinder` magnets are magnetic field sources. Below we compute the H-field in A/m of a + cylinder magnet with polarization (.1,.2,.3) in units of tesla and 0.01 meter diameter and + height at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> src = magpy.magnet.Cylinder(magnetization=(100,200,300), dimension=(1,1)) - >>> H = src.getH((1,1,1)) + >>> src = magpy.magnet.Cylinder(polarization=(.1,.2,.3), dimension=(.01,.01)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [4.84991343 3.88317816 2.73973202] + [4849.91343385 3883.17815728 2739.73202386] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Cylinder(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) [[3.31419501 5.26683023 0.37767015] [0.42298405 0.67710536 0.04464932] [0.12571523 0.20144503 0.01312389]] The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). Here we use a `Sensor` object as observer. + observer at position (0.01,0.01,0.01). Here we use a `Sensor` object as observer. - >>> sens = magpy.Sensor(position=(1,1,1)) - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Cylinder(id=...) >>> B = src.getB(sens) >>> print(B) @@ -80,15 +84,16 @@ class Cylinder(BaseMagnet): """ _field_func = staticmethod(magnet_cylinder_field) - _field_func_kwargs_ndim = {"magnetization": 2, "dimension": 2} + _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_Cylinder def __init__( self, - magnetization=None, - dimension=None, position=(0, 0, 0), orientation=None, + dimension=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): @@ -96,17 +101,19 @@ def __init__( self.dimension = dimension # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property def dimension(self): - """Dimension (d,h) denote diameter and height of the cylinder in units of mm.""" + """Dimension (d,h) denote diameter and height of the cylinder in units of meter.""" return self._dimension @dimension.setter def dimension(self, dim): - """Set Cylinder dimension (d,h) in units of mm.""" + """Set Cylinder dimension (d,h) in units of meter.""" self._dimension = check_format_input_vector( dim, dims=(1,), @@ -122,5 +129,5 @@ def _default_style_description(self): """Default style description text""" if self.dimension is None: return "no dimension" - d = [unit_prefix(d / 1000) for d in self.dimension] - return f"D={d[0]}m, H={d[1]}m" + d = [unit_prefix(d) for d in self.dimension] + return f"D={d[0]}, H={d[1]}" From f6497bc11667cf13d66735966e3c30542a5cf3df Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 13:58:46 +0100 Subject: [PATCH 061/240] fix CylinderSegment --- .../_src/fields/field_BH_cylinder_segment.py | 28 +++--- .../class_magnet_CylinderSegment.py | 92 ++++++++++--------- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 08cff6158..5bc1d03cb 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2300,7 +2300,7 @@ def magnet_cylinder_segment_core( def magnet_cylinder_segment_field_internal( field: str, observers: np.ndarray, - magnetization: np.ndarray, + polarization: np.ndarray, dimension: np.ndarray, ) -> np.ndarray: """ @@ -2309,7 +2309,7 @@ def magnet_cylinder_segment_field_internal( Falls back to magnet_cylinder_field whenever the section angles describe the full 360° cylinder. """ - n = len(magnetization) + n = len(polarization) BHfinal = np.zeros((n, 3)) @@ -2319,28 +2319,28 @@ def magnet_cylinder_segment_field_internal( mask1 = (phi2 - phi1) < 360 BHfinal[mask1] = magnet_cylinder_segment_field( - field, - observers[mask1], - magnetization[mask1], - dimension[mask1], + field=field, + observers=observers[mask1], + polarization=polarization[mask1], + dimension=dimension[mask1], ) # case2: full cylinder mask1x = ~mask1 BHfinal[mask1x] = magnet_cylinder_field( - field, - observers[mask1x], - magnetization[mask1x], - np.c_[2 * r2[mask1x], h[mask1x]], + field=field, + observers=observers[mask1x], + polarization=polarization[mask1x], + dimension=np.c_[2 * r2[mask1x], h[mask1x]], ) # case2a: hollow cylinder <- should be vectorized together with above mask2 = (r1 != 0) & mask1x BHfinal[mask2] -= magnet_cylinder_field( - field, - observers[mask2], - magnetization[mask2], - np.c_[2 * r1[mask2], h[mask2]], + field=field, + observers=observers[mask2], + polarization=polarization[mask2], + dimension=np.c_[2 * r1[mask2], h[mask2]], ) return BHfinal diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index fe31f2a21..739c1d168 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -22,29 +22,29 @@ class CylinderSegment(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - dimension: array_like, shape (5,), default=`None` - Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) - where r11, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. - barycenter: array_like, shape (3,) - Read only property that returns the geometric barycenter (=center of mass) - of the object. - orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + dimension: array_like, shape (5,), default=`None` + Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) + where r1>> import magpylib as magpy - >>> src = magpy.magnet.CylinderSegment(magnetization=(100,200,300), dimension=(1,2,1,0,45)) - >>> H = src.getH((2,2,2)) + >>> src = magpy.magnet.CylinderSegment(polarization=(.1,.2,.3), dimension=(.01,.02,.01,0,45)) + >>> H = src.getH((.02,.02,.02)) >>> print(H) - [0.80784692 1.93422813 2.74116805] + [ 807.84692335 1934.22812967 2741.16804712] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') CylinderSegment(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[-32.82849635 30.15882073 -16.32885658] - [ 0.62876075 3.97579164 0.73297829] - [ 0.25439493 0.74331628 0.11682542]] + [[-0.0328285 0.03015882 -0.01632886] + [ 0.00062876 0.00397579 0.00073298] + [ 0.00025439 0.00074332 0.00011683]] The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). Here we use a `Sensor` object as observer. + observer at position (.01,.01,.01). Here we use a `Sensor` object as observer. - >>> sens = magpy.Sensor(position=(1,1,1)) - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) CylinderSegment(id=...) >>> B = src.getB(sens) >>> print(B) - [[-32.82849635 30.15882073 -16.32885658] - [ 0.62876075 3.97579164 0.73297829] - [ 0.25439493 0.74331628 0.11682542]] + [[-0.0328285 0.03015882 -0.01632886] + [ 0.00062876 0.00397579 0.00073298] + [ 0.00025439 0.00074332 0.00011683]] """ _field_func = staticmethod(magnet_cylinder_segment_field_internal) - _field_func_kwargs_ndim = {"magnetization": 2, "dimension": 2} + _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_CylinderSegment def __init__( self, - magnetization=None, - dimension=None, position=(0, 0, 0), orientation=None, + dimension=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): @@ -109,22 +116,24 @@ def __init__( self.dimension = dimension # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property def dimension(self): """ Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) - where r1 Date: Sun, 24 Dec 2023 14:10:51 +0100 Subject: [PATCH 062/240] fix Sphere --- .../_src/obj_classes/class_magnet_Sphere.py | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Sphere.py b/magpylib/_src/obj_classes/class_magnet_Sphere.py index 024cd3624..15400fc70 100644 --- a/magpylib/_src/obj_classes/class_magnet_Sphere.py +++ b/magpylib/_src/obj_classes/class_magnet_Sphere.py @@ -16,15 +16,8 @@ class Sphere(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - diameter: float, default=`None` - Diameter of the sphere in units of mm. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -32,6 +25,17 @@ class Sphere(BaseMagnet): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + diameter: float, default=`None` + Diameter of the sphere in units of meter. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector (mu0*M, remanence field) in units of mT given in + the local object coordinates (rotates with object). + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -46,49 +50,50 @@ class Sphere(BaseMagnet): Examples -------- - `Sphere` objects are magnetic field sources. In this example we compute the H-field kA/m - of a spherical magnet with magnetization (100,200,300) in units of mT and diameter - of 1 mm at the observer position (1,1,1) given in units of mm: + `Sphere` objects are magnetic field sources. In this example we compute the H-field A/m + of a spherical magnet with polarization (0.1,0.2,0.3) in units of tesla and diameter + of 0.01 meter at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> src = magpy.magnet.Sphere(magnetization=(100,200,300), diameter=1) - >>> H = src.getH((1,1,1)) + >>> src = magpy.magnet.Sphere(polarization=(.1,.2,.3), diameter=.01) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [3.19056074 2.55244859 1.91433644] + [3190.56073739 2552.44858992 1914.33644244] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Sphere(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[2.26804606 3.63693295 0.23486386] - [0.28350576 0.45461662 0.02935798] - [0.08400171 0.13470122 0.00869866]] + [[2.26804606e-03 3.63693295e-03 2.34863859e-04] + [2.83505757e-04 4.54616618e-04 2.93579824e-05] + [8.40017059e-05 1.34701220e-04 8.69866146e-06]] The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). This time we use a `Sensor` object as observer. + observer at position (.01,.01,.01). This time we use a `Sensor` object as observer. - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Sphere(id=...) - >>> sens = magpy.Sensor(position=(1,1,1)) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> B = src.getB(sens) >>> print(B) - [[2.26804606 3.63693295 0.23486386] - [0.28350576 0.45461662 0.02935798] - [0.08400171 0.13470122 0.00869866]] + [[2.26804606e-03 3.63693295e-03 2.34863859e-04] + [2.83505757e-04 4.54616618e-04 2.93579824e-05] + [8.40017059e-05 1.34701220e-04 8.69866146e-06]] """ _field_func = staticmethod(magnet_sphere_field) - _field_func_kwargs_ndim = {"magnetization": 2, "diameter": 1} + _field_func_kwargs_ndim = {"polarization": 2, "diameter": 1} get_trace = make_Sphere def __init__( self, - magnetization=None, - diameter=None, position=(0, 0, 0), orientation=None, + diameter=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): @@ -96,17 +101,19 @@ def __init__( self.diameter = diameter # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property def diameter(self): - """Diameter of the sphere in units of mm.""" + """Diameter of the sphere in units of meter.""" return self._diameter @diameter.setter def diameter(self, dia): - """Set Sphere diameter, float, mm.""" + """Set Sphere diameter, float, meter.""" self._diameter = check_format_input_scalar( dia, sig_name="diameter", @@ -120,4 +127,4 @@ def _default_style_description(self): """Default style description text""" if self.diameter is None: return "no dimension" - return f"D={unit_prefix(self.diameter / 1000)}m" + return f"D={unit_prefix(self.diameter)}" From 6d913db799caae9d416bd317af3794a1cdf285c8 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 14:15:06 +0100 Subject: [PATCH 063/240] fix magnetization docstring --- magpylib/_src/obj_classes/class_magnet_Cylinder.py | 4 ++-- magpylib/_src/obj_classes/class_magnet_CylinderSegment.py | 4 ++-- magpylib/_src/obj_classes/class_magnet_Sphere.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 8982d0b1f..681b2f5fb 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -34,8 +34,8 @@ class Cylinder(BaseMagnet): given in the local object coordinates (rotates with object). magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). parent: `Collection` object or `None` The object is a child of it's parent collection. diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index 739c1d168..04c4ae7e7 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -42,8 +42,8 @@ class CylinderSegment(BaseMagnet): given in the local object coordinates (rotates with object). magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). parent: `Collection` object or `None` The object is a child of it's parent collection. diff --git a/magpylib/_src/obj_classes/class_magnet_Sphere.py b/magpylib/_src/obj_classes/class_magnet_Sphere.py index 15400fc70..4468e492f 100644 --- a/magpylib/_src/obj_classes/class_magnet_Sphere.py +++ b/magpylib/_src/obj_classes/class_magnet_Sphere.py @@ -33,8 +33,8 @@ class Sphere(BaseMagnet): given in the local object coordinates (rotates with object). magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). parent: `Collection` object or `None` The object is a child of it's parent collection. From 4739ff66598c02f1b830f14832df09c166ccfdad Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 14:24:46 +0100 Subject: [PATCH 064/240] fix Tetrahedron --- .../obj_classes/class_magnet_Tetrahedron.py | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py index bddea9115..a9e043a8a 100644 --- a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py +++ b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py @@ -19,28 +19,28 @@ class Tetrahedron(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - vertices: ndarray, shape (4,3) - Vertices [(x1,y1,z1), (x2,y2,z2), (x3,y3,z3), (x4,y4,z4)], in the relative - coordinate system of the tetrahedron. - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. When setting vertices, the initial position is set to the barycenter. - barycenter: array_like, shape (3,) - Read only property that returns the geometric barycenter (=center of mass) - of the object. - orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + vertices: ndarray, shape (4,3) + Vertices [(x1,y1,z1), (x2,y2,z2), (x3,y3,z3), (x4,y4,z4)], in the relative + coordinate system of the tetrahedron. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -48,76 +48,84 @@ class Tetrahedron(BaseMagnet): Object style inputs must be in dictionary form, e.g. `{'color':'red'}` or using style underscore magic, e.g. `style_color='red'`. + Attributes + ---------- + barycenter: array_like, shape (3,) + Read only property that returns the geometric barycenter (=center of mass) + of the object. + Returns ------- magnet source: `Tetrahedron` object Examples -------- - `Tetrahedron` magnets are magnetic field sources. Below we compute the H-field in kA/m of a - tetrahedron magnet with magnetization (100,200,300) in units of mT dimensions defined - through the vertices (0,0,0), (1,0,0), (0,1,0) and (0,0,1) in units of mm at the - observer position (1,1,1) given in units of mm: + `Tetrahedron` magnets are magnetic field sources. Below we compute the H-field in A/m of a + tetrahedron magnet with polarization (0.1,0.2,0.3) in units of tesla dimensions defined + through the vertices (0,0,0), (.01,0,0), (0,.01,0) and (0,0,.01) in units of meter at the + observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> verts = [(0,0,0), (1,0,0), (0,1,0), (0,0,1)] - >>> src = magpy.magnet.Tetrahedron(magnetization=(100,200,300), vertices=verts) - >>> H = src.getH((1,1,1)) + >>> verts = [(0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)] + >>> src = magpy.magnet.Tetrahedron(polarization=(.1,.2,.3), vertices=verts) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [2.07089783 1.65671826 1.2425387 ] + [2070.89782733 1656.71826186 1242.5386964 ] We rotate the source object, and compute the B-field, this time at a set of observer positions: - src.rotate_from_angax(45, 'x') - Tetrahedron(id=...) - B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) - print(B) - [[ 8.68006559e-01 2.00895792e+00 -5.03469140e-01] - [ 1.01357229e-01 1.93731796e-01 -1.59677364e-02] - [ 2.90426931e-02 5.22556994e-02 -1.70596096e-03]] + >>> src.rotate_from_angax(45, 'x') + >>> Tetrahedron(id=...) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) + >>> print(B) + [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] + [ 1.01357229e-04 1.93731796e-04 -1.59677364e-05] + [ 2.90426931e-05 5.22556994e-05 -1.70596096e-06]] The same result is obtained when the rotated source moves along a path away from an observer at position (1,1,1). Here we use a `Sensor` object as observer. - sens = magpy.Sensor(position=(1,1,1)) - src.move([(-1,-1,-1), (-2,-2,-2)]) - Sensor(id=...) - B = src.getB(sens) - print(B) - [[ 8.68006559e-01 2.00895792e+00 -5.03469140e-01] - [ 1.01357229e-01 1.93731796e-01 -1.59677364e-02] - [ 2.90426931e-02 5.22556994e-02 -1.70596096e-03]] + >>> sens = magpy.Sensor(position=(.01,.01,.01)) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) + >>> Tetrahedron(id=...) + >>> B = src.getB(sens) + >>> print(B) + [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] + [ 1.01357229e-04 1.93731796e-04 -1.59677364e-05] + [ 2.90426931e-05 5.22556994e-05 -1.70596096e-06]] """ _field_func = staticmethod(magnet_tetrahedron_field) - _field_func_kwargs_ndim = {"magnetization": 1, "vertices": 3} + _field_func_kwargs_ndim = {"polarization": 1, "vertices": 3} get_trace = make_Tetrahedron def __init__( self, - magnetization=None, - vertices=None, position=(0, 0, 0), orientation=None, + vertices=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): # instance attributes self.vertices = vertices - self._object_type = "Tetrahedron" # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property def vertices(self): - """Length of the Tetrahedron sides [a,b,c] in units of mm.""" + """Length of the Tetrahedron sides [a,b,c] in units of meter.""" return self._vertices @vertices.setter def vertices(self, dim): - """Set Tetrahedron vertices (a,b,c), shape (3,), (mm).""" + """Set Tetrahedron vertices (a,b,c), shape (3,), (meter).""" self._vertices = check_format_input_vector( dim, dims=(2,), From 355ee53528454c16b7ad11ccfe14835fb1ce1cbf Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:13:14 +0100 Subject: [PATCH 065/240] fix TriangularMesh --- .../class_magnet_TriangularMesh.py | 185 ++++++++++-------- 1 file changed, 107 insertions(+), 78 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index c6b9883ba..8bc20d521 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -33,25 +33,29 @@ class TriangularMesh(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). + position: array_like, shape (3,) or (m,3), default=`(0,0,0)` + Object position(s) in the global coordinates in units of meter. For m>1, the + `position` and `orientation` attributes together represent an object path. + + orientation: scipy `Rotation` object with length 1 or m, default=`None` + Object orientation(s) in the global coordinates. `None` corresponds to + a unit-rotation. For m>1, the `position` and `orientation` attributes + together represent an object path. vertices: ndarray, shape (n,3) - A set of points in units of mm in the local object coordinates from which the + A set of points in units of meter in the local object coordinates from which the triangular faces of the mesh are constructed by the additional `faces`input. faces: ndarray, shape (n,3) Indices of vertices. Each triplet represents one triangle of the mesh. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the - `position` and `orientation` attributes together represent an object path. + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). - orientation: scipy `Rotation` object with length 1 or m, default=`None` - Object orientation(s) in the global coordinates. `None` corresponds to - a unit-rotation. For m>1, the `position` and `orientation` attributes - together represent an object path. + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). reorient_faces: bool or string, default=`True` In a properly oriented mesh, all faces must be oriented outwards. @@ -95,30 +99,31 @@ class TriangularMesh(BaseMagnet): Examples -------- - We compute the B-field in units of mT of a triangular mesh (4 vertices, 4 faces) - with magnetization (100,200,300) in units of mT at the observer position - (1,1,1) given in units of mm: + We compute the B-field in units of tesla of a triangular mesh (4 vertices, 4 faces) + with polarization (0.1,0.2,0.3) in units of tesla at the observer position + (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> vv = ((0,0,0), (1,0,0), (0,1,0), (0,0,1)) + >>> vv = ((0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)) >>> tt = ((0,1,2), (0,1,3), (0,2,3), (1,2,3)) - >>> trim = magpy.magnet.TriangularMesh(magnetization=(100,200,300), vertices=vv, faces=tt) - >>> print(trim.getB((1,1,1))) - [2.60236696 2.08189357 1.56142018] + >>> trim = magpy.magnet.TriangularMesh(polarization=(.1,.2,.3), vertices=vv, faces=tt) + >>> print(trim.getB((.01,.01,.01))) + [0.00260237 0.00208189 0.00156142] """ _field_func = staticmethod(magnet_trimesh_field) - _field_func_kwargs_ndim = {"magnetization": 2, "mesh": 3} + _field_func_kwargs_ndim = {"polarization": 2, "mesh": 3} get_trace = make_TriangularMesh _style_class = TriangularMeshStyle def __init__( self, - magnetization=None, - vertices=None, - faces=None, position=(0, 0, 0), orientation=None, + vertices=None, + faces=None, + polarization=None, + magnetization=None, check_open="warn", check_disconnected="warn", check_selfintersecting="warn", @@ -141,7 +146,9 @@ def __init__( self.check_selfintersecting(mode=check_selfintersecting) # inherit - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property @@ -507,9 +514,7 @@ def _input_check(self, vertices, faces): def to_TriangleCollection(self): """Return a Collection of Triangle objects from the current TriangularMesh""" - tris = [ - Triangle(magnetization=self.magnetization, vertices=v) for v in self.mesh - ] + tris = [Triangle(polarization=self.polarization, vertices=v) for v in self.mesh] coll = Collection(tris) coll.position = self.position coll.orientation = self.orientation @@ -520,10 +525,11 @@ def to_TriangleCollection(self): @classmethod def from_ConvexHull( cls, - magnetization=None, - points=None, position=(0, 0, 0), orientation=None, + points=None, + polarization=None, + magnetization=None, check_open="warn", check_disconnected="warn", reorient_faces=True, @@ -534,15 +540,8 @@ def from_ConvexHull( Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - points: ndarray, shape (n,3) - Point cloud from which the convex hull is computed. - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -550,6 +549,17 @@ def from_ConvexHull( a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + points: ndarray, shape (n,3) + Point cloud from which the convex hull is computed. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + reorient_faces: bool, default=`True` In a properly oriented mesh, all faces must be oriented outwards. If `True`, check and fix the orientation of each triangle. @@ -590,11 +600,12 @@ def from_ConvexHull( -------- """ return cls( - magnetization=magnetization, - vertices=points, - faces=ConvexHull(points).simplices, position=position, orientation=orientation, + vertices=points, + faces=ConvexHull(points).simplices, + polarization=polarization, + magnetization=magnetization, reorient_faces=reorient_faces, check_open=check_open, check_disconnected=check_disconnected, @@ -605,10 +616,11 @@ def from_ConvexHull( @classmethod def from_pyvista( cls, - magnetization=None, - polydata=None, position=(0, 0, 0), orientation=None, + polydata=None, + polarization=None, + magnetization=None, check_open="warn", check_disconnected="warn", reorient_faces=True, @@ -619,15 +631,8 @@ def from_pyvista( Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - polydata: pyvista.core.pointset.PolyData object - A valid pyvista Polydata mesh object. (e.g. `pyvista.Sphere()`) - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -635,6 +640,17 @@ def from_pyvista( a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + polydata: pyvista.core.pointset.PolyData object + A valid pyvista Polydata mesh object. (e.g. `pyvista.Sphere()`) + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + reorient_faces: bool, default=`True` In a properly oriented mesh, all faces must be oriented outwards. If `True`, check and fix the orientation of each triangle. @@ -692,11 +708,12 @@ def from_pyvista( faces = polydata.faces.reshape(-1, 4)[:, 1:] return cls( - magnetization=magnetization, - vertices=vertices, - faces=faces, position=position, orientation=orientation, + vertices=vertices, + faces=faces, + polarization=polarization, + magnetization=magnetization, reorient_faces=reorient_faces, check_open=check_open, check_disconnected=check_disconnected, @@ -707,10 +724,11 @@ def from_pyvista( @classmethod def from_triangles( cls, - magnetization=None, - triangles=None, position=(0, 0, 0), orientation=None, + triangles=None, + polarization=None, + magnetization=None, reorient_faces=True, check_open="warn", check_disconnected="warn", @@ -721,15 +739,8 @@ def from_triangles( Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - triangles: list or Collection of Triangle objects - Only vertices of Triangle objects are taken, magnetization is ignored. - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -737,6 +748,17 @@ def from_triangles( a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + triangles: list or Collection of Triangle objects + Only vertices of Triangle objects are taken, magnetization is ignored. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + reorient_faces: bool, default=`True` In a properly oriented mesh, all faces must be oriented outwards. If `True`, check and fix the orientation of each triangle. @@ -792,11 +814,12 @@ def from_triangles( faces = tr.reshape((-1, 3)) return cls( - magnetization=magnetization, - vertices=vertices, - faces=faces, position=position, orientation=orientation, + vertices=vertices, + faces=faces, + polarization=polarization, + magnetization=magnetization, reorient_faces=reorient_faces, check_open=check_open, check_disconnected=check_disconnected, @@ -807,10 +830,11 @@ def from_triangles( @classmethod def from_mesh( cls, - magnetization=None, - mesh=None, position=(0, 0, 0), orientation=None, + mesh=None, + polarization=None, + magnetization=None, reorient_faces=True, check_open="warn", check_disconnected="warn", @@ -821,15 +845,8 @@ def from_mesh( Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). - - mesh: array_like, shape (n,3,3) - An array_like of triangular faces that make up a triangular mesh. - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -837,6 +854,17 @@ def from_mesh( a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + mesh: array_like, shape (n,3,3) + An array_like of triangular faces that make up a triangular mesh. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object). + reorient_faces: bool, default=`True` In a properly oriented mesh, all faces must be oriented outwards. If `True`, check and fix the orientation of each triangle. @@ -885,11 +913,12 @@ def from_mesh( faces = tr.reshape((-1, 3)) return cls( - magnetization=magnetization, - vertices=vertices, - faces=faces, position=position, orientation=orientation, + vertices=vertices, + faces=faces, + polarization=polarization, + magnetization=magnetization, reorient_faces=reorient_faces, check_open=check_open, check_disconnected=check_disconnected, From 57c17ddfe1677ad0ede49bf5125d78f9f5d49772 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:16:58 +0100 Subject: [PATCH 066/240] fix Collection --- magpylib/_src/obj_classes/class_Collection.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 8d9753ae5..ea2ab6c81 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -516,7 +516,7 @@ def _validate_getBH_inputs(self, *inputs): return sources, sensors def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute B-field in mT for given sources and observers. + """Compute B-field in tesla for given sources and observers. Parameters ---------- @@ -544,7 +544,7 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame B-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of mT. Sensor pixel positions are equivalent + position (n1,n2,...) in units of tesla. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than m will be considered as static beyond their end. @@ -587,7 +587,7 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): ) def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute H-field in kA/m for given sources and observers. + """Compute H-field in A/m for given sources and observers. Parameters ---------- @@ -615,7 +615,7 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame H-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of kA/m. Sensor pixel positions are equivalent + position (n1,n2,...) in units of A/m. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than m will be considered as static beyond their end. @@ -702,7 +702,7 @@ class Collection(BaseGeo, BaseCollection): An ordered list of all collection objects in the collection. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` From 4288281b985f883906a2a58ee70218c8257f146e Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:22:49 +0100 Subject: [PATCH 067/240] fix Circle --- .../_src/obj_classes/class_current_Circle.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index d9df00a3c..33bde30ed 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -24,10 +24,10 @@ class Circle(BaseCurrent): Electrical current in units of A. diameter: float, default=`None` - Diameter of the loop in units of mm. + Diameter of the loop in units of meter. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -48,37 +48,37 @@ class Circle(BaseCurrent): Examples -------- - `Circle` objects are magnetic field sources. In this example we compute the H-field kA/m - of such a current loop with 100 A current and a diameter of 2 mm at the observer position - (1,1,1) given in units of mm: + `Circle` objects are magnetic field sources. In this example we compute the H-field A/m + of such a current loop with 100 A current and a diameter of 2 meter at the observer position + (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy >>> src = magpy.current.Circle(current=100, diameter=2) - >>> H = src.getH((1,1,1)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [4.96243034 4.96243034 2.12454191] + [7.50093701e-03 7.50093701e-03 4.99999967e+01] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Circle(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[-1.44441884e-15 6.72068135e+00 -6.72068135e+00] - [-9.88027010e-17 5.89248328e-01 -5.89248328e-01] - [-3.55802727e-17 1.65201495e-01 -1.65201495e-01]] + [[-1.63585841e-24 -4.44388287e-05 4.44388287e-05] + [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] + [-9.85948764e-24 -4.45190261e-05 4.45190261e-05]] The same result is obtained when the rotated source moves along a path away from an observer at position (1,1,1). This time we use a `Sensor` object as observer. - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Circle(id=...) - >>> sens = magpy.Sensor(position=(1,1,1)) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> B = src.getB(sens) >>> print(B) - [[-1.44441884e-15 6.72068135e+00 -6.72068135e+00] - [-9.88027010e-17 5.89248328e-01 -5.89248328e-01] - [-3.55802727e-17 1.65201495e-01 -1.65201495e-01]] + [[-1.63585841e-24 -4.44388287e-05 4.44388287e-05] + [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] + [-9.85948764e-24 -4.45190261e-05 4.45190261e-05]] """ _field_func = staticmethod(current_circle_field) @@ -104,12 +104,12 @@ def __init__( # property getters and setters @property def diameter(self): - """Diameter of the loop in units of mm.""" + """Diameter of the loop in units of meter.""" return self._diameter @diameter.setter def diameter(self, dia): - """Set Circle loop diameter, float, mm.""" + """Set Circle loop diameter, float, meter.""" self._diameter = check_format_input_scalar( dia, sig_name="diameter", From d321578a2c93c740cc1b4bad0cf0ae3b507bf5e7 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:29:08 +0100 Subject: [PATCH 068/240] fix Polyline --- .../obj_classes/class_current_Polyline.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/obj_classes/class_current_Polyline.py b/magpylib/_src/obj_classes/class_current_Polyline.py index 1670cae97..5c1cf80d7 100644 --- a/magpylib/_src/obj_classes/class_current_Polyline.py +++ b/magpylib/_src/obj_classes/class_current_Polyline.py @@ -23,12 +23,12 @@ class Polyline(BaseCurrent): Electrical current in units of A. vertices: array_like, shape (n,3), default=`None` - The current flows along the vertices which are given in units of mm in the + The current flows along the vertices which are given in units of meter in the local object coordinates (move/rotate with object). At least two vertices must be given. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -49,40 +49,40 @@ class Polyline(BaseCurrent): Examples -------- - `Polyline` objects are magnetic field sources. In this example we compute the H-field kA/m + `Polyline` objects are magnetic field sources. In this example we compute the H-field A/m of a square-shaped line-current with 1 A current at the observer position (1,1,1) given in - units of mm: + units of meter: >>> import magpylib as magpy >>> src = magpy.current.Polyline( ... current=1, - ... vertices=((1,0,0), (0,1,0), (-1,0,0), (0,-1,0), (1,0,0)), + ... vertices=((.01,0,0), (0,.01,0), (-.01,0,0), (0,-.01,0), (.01,0,0)), ... ) - >>> H = src.getH((1,1,1)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [0.03160639 0.03160639 0.00766876] + [3.16063859 3.16063859 0.76687556] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Polyline(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[-6.68990257e-18 3.50341393e-02 -3.50341393e-02] - [-5.94009823e-19 3.62181325e-03 -3.62181325e-03] - [-2.21112416e-19 1.03643004e-03 -1.03643004e-03]] + [[-1.04529728e-21 3.50341393e-06 -3.50341393e-06] + [-9.28140348e-23 3.62181325e-07 -3.62181325e-07] + [-1.72744075e-23 1.03643004e-07 -1.03643004e-07]] The same result is obtained when the rotated source moves along a path away from an observer at position (1,1,1). This time we use a `Sensor` object as observer. - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Polyline(id=...) - >>> sens = magpy.Sensor(position=(1,1,1)) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> B = src.getB(sens) >>> print(B) - [[-6.68990257e-18 3.50341393e-02 -3.50341393e-02] - [-5.94009823e-19 3.62181325e-03 -3.62181325e-03] - [-2.21112416e-19 1.03643004e-03 -1.03643004e-03]] + [[-1.04529728e-21 3.50341393e-06 -3.50341393e-06] + [-9.28140348e-23 3.62181325e-07 -3.62181325e-07] + [-1.72744075e-23 1.03643004e-07 -1.03643004e-07]] """ # pylint: disable=dangerous-default-value @@ -114,7 +114,7 @@ def __init__( @property def vertices(self): """ - The current flows along the vertices which are given in units of mm in the + The current flows along the vertices which are given in units of meter in the local object coordinates (move/rotate with object). At least two vertices must be given. """ @@ -122,7 +122,7 @@ def vertices(self): @vertices.setter def vertices(self, vert): - """Set Polyline vertices, array_like, mm.""" + """Set Polyline vertices, array_like, meter.""" self._vertices = check_format_input_vertices(vert) @property From f9682a1da15f119aa31a6460b17cea20ffefbfd2 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:36:27 +0100 Subject: [PATCH 069/240] fix CustomSource --- .../obj_classes/class_misc_CustomSource.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index a8896a074..dc6a108c8 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -15,12 +15,12 @@ class CustomSource(BaseSource): field_func: callable, default=`None` The function for B- and H-field computation must have the two positional arguments `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units - of mT or kA/m must be returned respectively. The `observers` argument must + of or A/m must be returned respectively. The `observers` argument must accept numpy ndarray inputs of shape (n,3), in which case the returned fields must be numpy ndarrays of shape (n,3) themselves. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -44,38 +44,37 @@ class CustomSource(BaseSource): With version 4 `CustomSource` objects enable users to define their own source objects, and to embedded them in the Magpylib object oriented interface. In this example we create a source that generates a constant field and evaluate the field at observer - position (1,1,1) given in mm: + position (0.01,0.01,0.01) given in meter: >>> import numpy as np >>> import magpylib as magpy - >>> - >>> funcBH = lambda field, observers: np.array([(100 if field=='B' else 80,0,0)]*len(observers)) + >>> funcBH = lambda field, observers: np.array([(.01 if field=='B' else .08,0,0)]*len(observers)) >>> src = magpy.misc.CustomSource(field_func=funcBH) - >>> H = src.getH((1,1,1)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [80. 0. 0.] + [0.08 0. 0. ] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'z') CustomSource(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[70.71067812 70.71067812 0. ] - [70.71067812 70.71067812 0. ] - [70.71067812 70.71067812 0. ]] + [[0.00707107 0.00707107 0. ] + [0.00707107 0.00707107 0. ] + [0.00707107 0.00707107 0. ]] The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). This time we use a `Sensor` object as observer. + observer at position (0.01,0.01,0.01). This time we use a `Sensor` object as observer. - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) CustomSource(id=...) - >>> sens = magpy.Sensor(position=(1,1,1)) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> B = src.getB(sens) >>> print(B) - [[70.71067812 70.71067812 0. ] - [70.71067812 70.71067812 0. ] - [70.71067812 70.71067812 0. ]] + [[0.00707107 0.00707107 0. ] + [0.00707107 0.00707107 0. ] + [0.00707107 0.00707107 0. ]] """ _editable_field_func = True From f701d971115b61904ad9a9023ca6ac4d30666a8c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:48:08 +0100 Subject: [PATCH 070/240] fix Dipole --- .../_src/obj_classes/class_misc_Dipole.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index 3513a37fe..fec9b0b56 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -19,13 +19,13 @@ class Dipole(BaseSource): Parameters ---------- - moment: array_like, shape (3,), unit mT*mm^3, default=`None` - Magnetic dipole moment in units of mT*mm^3 given in the local object coordinates. + moment: array_like, shape (3,), unit T・m³, default=`None` + Magnetic dipole moment in units of T・m³ given in the local object coordinates. For homogeneous magnets the relation moment=magnetization*volume holds. The dipole moment of a Circle object is pi**2/10*diameter**2*current. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -46,37 +46,37 @@ class Dipole(BaseSource): Examples -------- - `Dipole` objects are magnetic field sources. In this example we compute the H-field kA/m - of such a magnetic dipole with a moment of (100,100,100) in units of mT*mm^2 at an - observer position (1,1,1) given in units of mm: + `Dipole` objects are magnetic field sources. In this example we compute the H-field A/m + of such a magnetic dipole with a moment of (100,100,100) in units of T・m³ at an + observer position (.01,.01,.01) given in units of meter: >>> import magpylib as magpy - >>> src = magpy.misc.Dipole(moment=(100,100,100)) - >>> H = src.getH((1,1,1)) + >>> src = magpy.misc.Dipole(moment=(10,10,10)) + >>> H = src.getH((.01,.01,.01)) >>> print(H) - [2.43740886 2.43740886 2.43740886] + [306293.83078988 306293.83078988 306293.83078988] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Dipole(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[2.16582445 3.6972936 1.53146915] - [0.27072806 0.4621617 0.19143364] - [0.08021572 0.1369368 0.05672108]] + [[0.27216553 0.46461562 0.19245009] + [0.03402069 0.05807695 0.02405626] + [0.0100802 0.01720799 0.00712778]] The same result is obtained when the rotated source moves along a path away from an observer at position (1,1,1). This time we use a `Sensor` object as observer. - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Dipole(id=...) - >>> sens = magpy.Sensor(position=(1,1,1)) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> B = src.getB(sens) >>> print(B) - [[2.16582445 3.6972936 1.53146915] - [0.27072806 0.4621617 0.19143364] - [0.08021572 0.1369368 0.05672108]] + [[0.27216553 0.46461562 0.19245009] + [0.03402069 0.05807695 0.02405626] + [0.0100802 0.01720799 0.00712778]] """ _field_func = staticmethod(dipole_field) @@ -102,12 +102,12 @@ def __init__( # property getters and setters @property def moment(self): - """Magnetic dipole moment in units of mT*mm^3 given in the local object coordinates.""" + """Magnetic dipole moment in units of T・m³ given in the local object coordinates.""" return self._moment @moment.setter def moment(self, mom): - """Set dipole moment vector, shape (3,), unit mT*mm^3.""" + """Set dipole moment vector, shape (3,), unit T・m³.""" self._moment = check_format_input_vector( mom, dims=(1,), @@ -124,4 +124,4 @@ def _default_style_description(self): moment_mag = np.linalg.norm(moment) if moment_mag == 0: return "no moment" - return f"moment={unit_prefix(moment_mag)}mT mm³" + return f"moment={unit_prefix(moment_mag)}T・m³" From a0ef05d4a0b73f48295beee674c69c090dc1e57f Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 16:56:59 +0100 Subject: [PATCH 071/240] fix Triangle --- .../_src/obj_classes/class_misc_Triangle.py | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index 1ac9e82c7..30e28e1e3 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -20,28 +20,30 @@ class Triangle(BaseMagnet): Parameters ---------- - magnetization: array_like, shape (3,), default=`None` - Magnetization vector (mu0*M, remanence field) in units of mT given in - the local object coordinates (rotates with object). The homogeneous surface - charge of the Triangle is given by the projection of the magnetization on the - Triangle normal vector (right-hand-rule). - - vertices: ndarray, shape (3,3) - Triple of vertices in the local object coordinates. - position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. - barycenter: array_like, shape (3,) - Read only property that returns the geometric barycenter (=center of mass) - of the object. - orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + vertices: ndarray, shape (3,3) + Triple of vertices in the local object coordinates. + + polarization: array_like, shape (3,), default=`None` + Magnetic polarization vector J = mu0*M in units of T, + given in the local object coordinates (rotates with object).The homogeneous surface + charge of the Triangle is given by the projection of the polarization on the + Triangle normal vector (right-hand-rule). + + magnetization: array_like, shape (3,), default=`None` + Magnetization vector M = J/mu0 in units of A/m, + given in the local object coordinates (rotates with object).The homogeneous surface + charge of the Triangle is given by the projection of the magnetization on the + Triangle normal vector (right-hand-rule). + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -49,66 +51,74 @@ class Triangle(BaseMagnet): Object style inputs must be in dictionary form, e.g. `{'color':'red'}` or using style underscore magic, e.g. `style_color='red'`. + Attributes + ---------- + barycenter: array_like, shape (3,) + Read only property that returns the geometric barycenter (=center of mass) + of the object. + Returns ------- magnet source: `Triangle` object Examples -------- - `Triangle` objects are magnetic field sources. Below we compute the H-field in kA/m of a - Triangle object with magnetization (100,200,300) in units of mT, dimensions defined - through the vertices (0,0,0), (1,0,0) and (0,1,0) in units of mm at the - observer position (1,1,1) given in units of mm: + `Triangle` objects are magnetic field sources. Below we compute the H-field in A/m of a + Triangle object with polarization (0.01,0.02,0.03) in units of T, dimensions defined + through the vertices (0,0,0), (0.01,0,0) and (0,0.01,0) in units of meter at the + observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> verts = [(0,0,0), (1,0,0), (0,1,0)] - >>> src = magpy.misc.Triangle(magnetization=(100,200,300), vertices=verts) - >>> H = src.getH((1,1,1)) + >>> verts = [(0,0,0), (.01,0,0), (0,.01,0)] + >>> src = magpy.misc.Triangle(polarization=(.1,.2,.3), vertices=verts) + >>> H = src.getH((.1,.1,.1)) >>> print(H) - [2.24851814 2.24851814 3.49105683] + [18.88869831 18.88869831 19.54560637] We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') Triangle(id=...) - >>> B = src.getB([(1,1,1), (2,2,2), (3,3,3)]) + >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[3.94659011 4.21772671 4.21772671] - [0.73745776 0.77325648 0.77325648] - [0.30049474 0.31043974 0.31043974]] + [[0.00394659 0.00421773 0.00421773] + [0.00073746 0.00077326 0.00077326] + [0.00030049 0.00031044 0.00031044]] The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). Here we use a `Sensor` object as observer. + observer at position (0.01,0.01,0.01). Here we use a `Sensor` object as observer. - >>> sens = magpy.Sensor(position=(1,1,1)) - >>> src.move([(-1,-1,-1), (-2,-2,-2)]) + >>> sens = magpy.Sensor(position=(.01,.01,.01)) + >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) Triangle(id=...) >>> B = src.getB(sens) >>> print(B) - [[3.94659011 4.21772671 4.21772671] - [0.73745776 0.77325648 0.77325648] - [0.30049474 0.31043974 0.31043974]] - + [[0.00394659 0.00421773 0.00421773] + [0.00073746 0.00077326 0.00077326] + [0.00030049 0.00031044 0.00031044]] """ _field_func = staticmethod(triangle_field) - _field_func_kwargs_ndim = {"magnetization": 2, "vertices": 2} + _field_func_kwargs_ndim = {"polarization": 2, "vertices": 2} get_trace = make_Triangle _style_class = TriangleStyle def __init__( self, - magnetization=None, - vertices=None, position=(0, 0, 0), orientation=None, + vertices=None, + polarization=None, + magnetization=None, style=None, **kwargs, ): self.vertices = vertices # init inheritance - super().__init__(position, orientation, magnetization, style, **kwargs) + super().__init__( + position, orientation, magnetization, polarization, style, **kwargs + ) # property getters and setters @property @@ -118,7 +128,7 @@ def vertices(self): @vertices.setter def vertices(self, val): - """Set face vertices (a,b,c), shape (3,3), mm.""" + """Set face vertices (a,b,c), shape (3,3), meter.""" self._vertices = check_format_input_vector( val, dims=(2,), From 5a305de06978eaf2da7677de40ff5de9982a3fc6 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 17:06:24 +0100 Subject: [PATCH 072/240] fix Sensor --- magpylib/_src/obj_classes/class_Sensor.py | 48 +++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 14ab2b1ee..56290cda0 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -25,12 +25,12 @@ class Sensor(BaseGeo, BaseDisplayRepr): ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. pixel: array_like, shape (3,) or (n1,n2,...,3), default=`(0,0,0)` Sensor pixel (=sensing elements) positions in the local object coordinates - (rotate with object), in units of mm. + (rotate with object), in units of meter. orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -54,7 +54,7 @@ class Sensor(BaseGeo, BaseDisplayRepr): Examples -------- `Sensor` objects are observers for magnetic field computation. In this example we compute the - B-field in units of mT as seen by the sensor in the center of a circular current loop: + B-field in units of tesla as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() @@ -106,7 +106,7 @@ def __init__( @property def pixel(self): """Sensor pixel (=sensing elements) positions in the local object coordinates - (rotate with object), in units of mm. + (rotate with object), in units of meter. """ return self._pixel @@ -140,7 +140,7 @@ def handedness(self, val): def getB( self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray" ): - """Compute the B-field in units of mT as seen by the sensor. + """Compute the B-field in units of tesla as seen by the sensor. Parameters ---------- @@ -169,20 +169,20 @@ def getB( ------- B-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame B-field of each source (l) at each path position (m) and each sensor pixel - position (n1,n2,...) in units of mT. Paths of objects that are shorter than + position (n1,n2,...) in units of tesla. Paths of objects that are shorter than m will be considered as static beyond their end. Examples -------- Sensors are observers for magnetic field computation. In this example we compute the - B-field in units of mT as seen by the sensor in the center of a circular current loop: + B-field in units of tesla as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() - >>> loop = magpy.current.Circle(current=1, diameter=1) + >>> loop = magpy.current.Circle(current=1, diameter=.01) >>> B = sens.getB(loop) >>> print(B) - [0. 0. 1.25663706] + [0. 0. 0.00012566] Then we rotate the sensor by 45 degrees and compute the field again: @@ -190,16 +190,16 @@ def getB( Sensor(id=...) >>> B = sens.getB(loop) >>> print(B) - [0. 0.88857659 0.88857659] + [0.00000000e+00 8.88576588e-05 8.88576588e-05] Finally we set some sensor pixels and compute the field again: - >>> sens.pixel=((0,0,0), (.1,0,0), (.2,0,0)) + >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> B = sens.getB(loop) >>> print(B) - [[0. 0.88857659 0.88857659] - [0. 0.916274 0.916274 ] - [0. 1.01415383 1.01415383]] + [[0.00000000e+00 8.88576588e-05 8.88576588e-05] + [0.00000000e+00 9.16274003e-05 9.16274003e-05] + [0.00000000e+00 1.01415383e-04 1.01415383e-04]] """ sources = format_star_input(sources) return getBH_level2( @@ -215,7 +215,7 @@ def getB( def getH( self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray" ): - """Compute the H-field in units of kA/m as seen by the sensor. + """Compute the H-field in units of A/m as seen by the sensor. Parameters ---------- @@ -244,20 +244,20 @@ def getH( ------- H-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame H-field of each source (l) at each path position (m) and each sensor pixel - position (n1,n2,...) in units of kA/m. Paths of objects that are shorter than + position (n1,n2,...) in units of A/m. Paths of objects that are shorter than m will be considered as static beyond their end. Examples -------- Sensors are observers for magnetic field computation. In this example we compute the - H-field in kA/m as seen by the sensor in the center of a circular current loop: + B-field in units of tesla as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() - >>> loop = magpy.current.Circle(current=1, diameter=1) + >>> loop = magpy.current.Circle(current=1, diameter=.01) >>> H = sens.getH(loop) >>> print(H) - [0. 0. 1.] + [ 0. 0. 100.] Then we rotate the sensor by 45 degrees and compute the field again: @@ -265,16 +265,16 @@ def getH( Sensor(id=...) >>> H = sens.getH(loop) >>> print(H) - [0. 0.70710678 0.70710678] + [ 0. 70.71067812 70.71067812] Finally we set some sensor pixels and compute the field again: - >>> sens.pixel=((0,0,0), (.1,0,0), (.2,0,0)) + >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> H = sens.getH(loop) >>> print(H) - [[0. 0.70710678 0.70710678] - [0. 0.72914768 0.72914768] - [0. 0.80703798 0.80703798]] + [[ 0. 70.71067812 70.71067812] + [ 0. 72.9147684 72.9147684 ] + [ 0. 80.7037979 80.7037979 ]] """ sources = format_star_input(sources) return getBH_level2( From 7300c7939031473c1346c51ae4c1f58f6821c474 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 17:26:27 +0100 Subject: [PATCH 073/240] Fix wrapBH --- magpylib/_src/fields/field_wrap_BH.py | 166 +++++++++++++------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 04265438e..ee35a7b35 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -447,7 +447,7 @@ def getBH_dict_level2( squeeze=True, **kwargs: dict, ) -> np.ndarray: - """Direct interface access to vectorized computation + """Functional interface access to vectorized computation Parameters ---------- @@ -455,7 +455,7 @@ def getBH_dict_level2( Returns ------- - field: ndarray, shape (N,3), field at obs_pos in mT or kA/m + field: ndarray, shape (N,3), field at obs_pos in tesla or A/m Info ---- @@ -577,10 +577,10 @@ def getB( Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of mm. + are given in units of meter. - Direct interface: Input must be array_like with shape (3,) or (n,3) corresponding - positions to observer positions in units of mm. + Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding + positions to observer positions in units of meter. sumup: bool, default=`False` If `True`, the fields of all sources are summed up. @@ -601,10 +601,10 @@ def getB( See Also -------- - *Direct-interface + *Functional interface position: array_like, shape (3,) or (n,3), default=`(0,0,0)` - Source position(s) in the global coordinates in units of mm. + Source position(s) in the global coordinates in units of meter. orientation: scipy `Rotation` object with length 1 or n, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -622,9 +622,9 @@ def getB( Magnetization vector M = J/mu0 in units of A/m, given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit mT*mm^3 + moment: array_like, shape (3) or (n,3), unit T・m³ Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of mT*mm^3 given in the local object coordinates + Magnetic dipole moment(s) in units of T・m³ given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. @@ -634,31 +634,31 @@ def getB( dimension: array_like, shape (x,) or (n,x) Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! - Magnet dimension input in units of mm and deg. Dimension format x of sources is similar + Magnet dimension input in units of meter and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) Only source_type == `Sphere` or `Circle`! - Diameter of source in units of mm. + Diameter of source in units of meter. segment_start: array_like, shape (n,3) Only source_type == `Polyline`! - Start positions of line current segments in units of mm. + Start positions of line current segments in units of meter. segment_end: array_like, shape (n,3) Only source_type == `Polyline`! - End positions of line current segments in units of mm. + End positions of line current segments in units of meter. Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame B-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1, n2, ...) in units of mT. Sensor pixel positions are equivalent + position (n1, n2, ...) in units of tesla. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than m will be considered as static beyond their end. - Direct interface: ndarray, shape (n,3) - B-field for every parameter set in units of mT. + Functional interface: ndarray, shape (n,3) + B-field for every parameter set in units of tesla. Notes ----- @@ -668,51 +668,51 @@ def getB( Examples -------- - In this example we compute the B-field in units of mT of a spherical magnet and a current loop - at the observer position (1,1,1) given in units of mm: + In this example we compute the B-field in units of tesla of a spherical magnet and a current loop + at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> src1 = magpy.current.Circle(current=100, diameter=2) - >>> src2 = magpy.magnet.Sphere(magnetization=(0,0,100), diameter=1) - >>> B = magpy.getB([src1, src2], (1,1,1)) + >>> src1 = magpy.current.Circle(current=100, diameter=.002) + >>> src2 = magpy.magnet.Sphere(polarization=(0,0,.1), diameter=.001) + >>> B = magpy.getB([src1, src2], (.01,.01,.01)) >>> print(B) [[6.23597388e+00 6.23597388e+00 2.66977810e+00] [8.01875374e-01 8.01875374e-01 1.48029737e-16]] We can also use sensor objects as observers input: - >>> sens1 = magpy.Sensor(position=(1,1,1)) - >>> sens2 = sens1.copy(position=(1,1,-1)) + >>> sens1 = magpy.Sensor(position=(.01,.01,.01)) + >>> sens2 = sens1.copy(position=(.01,.01,-.01)) >>> B = magpy.getB([src1, src2], [sens1, sens2]) >>> print(B) - [[[ 6.23597388e+00 6.23597388e+00 2.66977810e+00] - [-6.23597388e+00 -6.23597388e+00 2.66977810e+00]] + [[[ 6.05434592e-06 6.05434592e-06 2.35680448e-08] + [-6.05434592e-06 -6.05434592e-06 2.35680448e-08]] - [[ 8.01875374e-01 8.01875374e-01 1.48029737e-16] - [-8.01875374e-01 -8.01875374e-01 1.48029737e-16]]] + [[ 8.01875374e-07 8.01875374e-07 1.51582450e-22] + [-8.01875374e-07 -8.01875374e-07 1.51582450e-22]]] Through the direct interface we can compute the same fields for the loop as: - >>> obs = [(1,1,1), (1,1,-1)] - >>> B = magpy.getB('Circle', obs, current=100, diameter=2) + >>> obs = [(.01,.01,.01), (.01,.01,-.01)] + >>> B = magpy.getB('Circle', obs, current=100, diameter=.002) >>> print(B) - [[ 6.23597388 6.23597388 2.6697781 ] - [-6.23597388 -6.23597388 2.6697781 ]] + [[ 6.05434592e-06 6.05434592e-06 2.35680448e-08] + [-6.05434592e-06 -6.05434592e-06 2.35680448e-08]] But also for a set of four completely different instances: >>> B = magpy.getB( ... 'Circle', - ... observers=((1,1,1), (1,1,-1), (1,2,3), (2,2,2)), + ... observers=((.01,.01,.01), (.01,.01,-.01), (.01,.02,.03), (.02,.02,.02)), ... current=(11, 22, 33, 44), - ... diameter=(1, 2, 3, 4), - ... position=((0,0,0), (0,0,1), (0,0,2), (0,0,3)), + ... diameter=(.001, .002, .003, .004), + ... position=((0,0,0), (0,0,.01), (0,0,.02), (0,0,.03)), ... ) >>> print(B) - [[ 0.17111325 0.17111325 0.01705189] - [-0.38852048 -0.38852048 0.49400758] - [ 1.14713551 2.29427102 -0.22065346] - [-2.48213467 -2.48213467 -0.79683487]] + [[ 1.66322588e-07 1.66322588e-07 1.61742625e-10] + [-4.69451597e-07 -4.69451597e-07 4.70690813e-07] + [ 7.96993186e-07 1.59398637e-06 -7.91258466e-07] + [-1.37369334e-06 -1.37369334e-06 -1.36554287e-06]] """ return getBH_level2( sources, @@ -735,7 +735,7 @@ def getH( output="ndarray", **kwargs, ): - """Compute H-field in kA/m for given sources and observers. + """Compute H-field in A/m for given sources and observers. Field implementations can be directly accessed (avoiding the object oriented Magpylib interface) by providing a string input `sources=source_type`, array_like @@ -746,19 +746,19 @@ def getH( ---------- sources: source and collection objects or 1D list thereof Sources that generate the magnetic field. Can be a single source (or collection) - or a 1D list of l source and/or collection objects. + or a 1D list of l sources and/or collection objects. - Direct interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, + Functional interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`, `Dipole`, `Circle` or `Polyline`). observers: array_like or (list of) `Sensor` objects Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of mm. + are given in units of meter. - Direct interface: Input must be array_like with shape (3,) or (n,3) corresponding - positions to observer positions in units of mm. + Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding + positions to observer positions in units of meter. sumup: bool, default=`False` If `True`, the fields of all sources are summed up. @@ -772,17 +772,17 @@ def getH( which is applied to observer output values, e.g. mean of all sensor pixel outputs. With this option, observers input with different (pixel) shapes is allowed. - output: str, default='ndarray' - Output type, which must be one of `('ndarray', 'dataframe')`. By default a - `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + output: str, default=`'ndarray'` + Output type, which must be one of (`'ndarray'`, `'dataframe'`). By default a + `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). See Also -------- - *Direct-interface + *Functional interface position: array_like, shape (3,) or (n,3), default=`(0,0,0)` - Source position(s) in the global coordinates in units of mm. + Source position(s) in the global coordinates in units of meter. orientation: scipy `Rotation` object with length 1 or n, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -790,12 +790,12 @@ def getH( magnetization: array_like, shape (3,) or (n,3) Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`)! - Magnetization vector(s) (mu0*M, remanence field) in units of kA/m given in + Magnetization vector(s) (mu0*M, remanence field) in units of A/m given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit mT*mm^3 + moment: array_like, shape (3) or (n,3), unit T・m³ Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of mT*mm^3 given in the local object coordinates + Magnetic dipole moment(s) in units of T・m³ given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. @@ -805,31 +805,31 @@ def getH( dimension: array_like, shape (x,) or (n,x) Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! - Magnet dimension input in units of mm and deg. Dimension format x of sources is similar + Magnet dimension input in units of meter and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) Only source_type == `Sphere` or `Circle`! - Diameter of source in units of mm. + Diameter of source in units of meter. segment_start: array_like, shape (n,3) Only source_type == `Polyline`! - Start positions of line current segments in units of mm. + Start positions of line current segments in units of meter. segment_end: array_like, shape (n,3) Only source_type == `Polyline`! - End positions of line current segments in units of mm. + End positions of line current segments in units of meter. Returns ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame H-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1, n2, ...) in units of kA/m. Sensor pixel positions are equivalent + position (n1, n2, ...) in units of A/m. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than m will be considered as static beyond their end. - Direct interface: ndarray, shape (n,3) - H-field for every parameter set in units of kA/m. + Functional interface: ndarray, shape (n,3) + H-field for every parameter set in units of A/m. Notes ----- @@ -839,51 +839,51 @@ def getH( Examples -------- - In this example we compute the H-field kA/m of a spherical magnet and a current loop - at the observer position (1,1,1) given in units of mm: + In this example we compute the H-field A/m of a spherical magnet and a current loop + at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy - >>> src1 = magpy.current.Circle(current=100, diameter=2) - >>> src2 = magpy.magnet.Sphere(magnetization=(0,0,100), diameter=1) - >>> H = magpy.getH([src1, src2], (1,1,1)) + >>> src1 = magpy.current.Circle(current=100, diameter=.002) + >>> src2 = magpy.magnet.Sphere(polarization=(0,0,.1), diameter=.001) + >>> H = magpy.getH([src1, src2], (.01,.01,.01)) >>> print(H) - [[4.96243034e+00 4.96243034e+00 2.12454191e+00] - [6.38112147e-01 6.38112147e-01 1.17798322e-16]] + [[4.81789540e+00 4.81789540e+00 1.87548541e-02] + [6.38112147e-01 6.38112147e-01 1.20625481e-16]] We can also use sensor objects as observers input: - >>> sens1 = magpy.Sensor(position=(1,1,1)) - >>> sens2 = sens1.copy(position=(1,1,-1)) + >>> sens1 = magpy.Sensor(position=(.01,.01,.01)) + >>> sens2 = sens1.copy(position=(.01,.01,-.01)) >>> H = magpy.getH([src1, src2], [sens1, sens2]) >>> print(H) - [[[ 4.96243034e+00 4.96243034e+00 2.12454191e+00] - [-4.96243034e+00 -4.96243034e+00 2.12454191e+00]] + [[[ 4.81789540e+00 4.81789540e+00 1.87548541e-02] + [-4.81789540e+00 -4.81789540e+00 1.87548541e-02]] - [[ 6.38112147e-01 6.38112147e-01 1.17798322e-16] - [-6.38112147e-01 -6.38112147e-01 1.17798322e-16]]] + [[ 6.38112147e-01 6.38112147e-01 1.20625481e-16] + [-6.38112147e-01 -6.38112147e-01 1.20625481e-16]]] Through the direct interface we can compute the same fields for the loop as: - >>> obs = [(1,1,1), (1,1,-1)] - >>> H = magpy.getH('Circle', obs, current=100, diameter=2) + >>> obs = [(.01,.01,.01), (.01,.01,-.01)] + >>> H = magpy.getH('Circle', obs, current=100, diameter=.002) >>> print(H) - [[ 4.96243034 4.96243034 2.12454191] - [-4.96243034 -4.96243034 2.12454191]] + [[ 4.8178954 4.8178954 0.01875485] + [-4.8178954 -4.8178954 0.01875485]] But also for a set of four completely different instances: >>> H = magpy.getH( ... 'Circle', - ... observers=((1,1,1), (1,1,-1), (1,2,3), (2,2,2)), + ... observers=((.01,.01,.01), (.01,.01,-.01), (.01,.02,.03), (.02,.02,.02)), ... current=(11, 22, 33, 44), - ... diameter=(1, 2, 3, 4), - ... position=((0,0,0), (0,0,1), (0,0,2), (0,0,3)), + ... diameter=(.001, .002, .003, .004), + ... position=((0,0,0), (0,0,.01), (0,0,.02), (0,0,.03)), ... ) >>> print(H) - [[ 0.1361676 0.1361676 0.01356947] - [-0.30917477 -0.30917477 0.39311875] - [ 0.91286143 1.82572286 -0.17559045] - [-1.97522001 -1.97522001 -0.63410104]] + [[ 1.32355310e-01 1.32355310e-01 1.28710691e-04] + [-3.73577711e-01 -3.73577711e-01 3.74563848e-01] + [ 6.34227026e-01 1.26845405e+00 -6.29663481e-01] + [-1.09315042e+00 -1.09315042e+00 -1.08666449e+00]] """ return getBH_level2( sources, From 7cd6e13137d5bc857bc5f63435b6f2f5429c7083 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 18:01:34 +0100 Subject: [PATCH 074/240] Fix doctests --- magpylib/_src/display/display.py | 15 ++++++--- magpylib/_src/fields/field_BH_circle.py | 10 +++--- magpylib/_src/fields/field_BH_cuboid.py | 10 +++--- magpylib/_src/fields/field_BH_cylinder.py | 10 +++--- .../_src/fields/field_BH_cylinder_segment.py | 10 +++--- magpylib/_src/fields/field_BH_dipole.py | 8 ++--- magpylib/_src/fields/field_BH_polyline.py | 12 +++---- magpylib/_src/fields/field_BH_sphere.py | 10 +++--- magpylib/_src/fields/field_BH_tetrahedron.py | 10 +++--- magpylib/_src/fields/field_BH_triangle.py | 10 +++--- magpylib/_src/fields/field_wrap_BH.py | 4 +-- .../_src/obj_classes/class_BaseExcitations.py | 33 ++++++++++--------- magpylib/_src/obj_classes/class_Collection.py | 32 +++++++++--------- magpylib/_src/obj_classes/class_Sensor.py | 14 ++++---- .../_src/obj_classes/class_current_Circle.py | 4 +-- .../_src/obj_classes/class_magnet_Cuboid.py | 4 +-- .../_src/obj_classes/class_magnet_Cylinder.py | 12 +++---- .../class_magnet_CylinderSegment.py | 8 ++--- .../obj_classes/class_magnet_Tetrahedron.py | 4 +-- 19 files changed, 113 insertions(+), 107 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 58b72318f..389e7b741 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -333,7 +333,7 @@ def show( Display multiple objects, object paths, markers in 3D using Matplotlib or Plotly: >>> import magpylib as magpy - >>> src = magpy.magnet.Sphere(magnetization=(0,0,1), diameter=1) + >>> src = magpy.magnet.Sphere(polarization=(0,0,1), diameter=1) >>> src.move([(0.1*x,0,0) for x in range(50)]) Sphere... >>> src.rotate_from_angax(angle=[*range(0,400,10)], axis='z', anchor=0, start=11) @@ -349,7 +349,7 @@ def show( >>> import matplotlib.pyplot as plt >>> import magpylib as magpy >>> my_axis = plt.axes(projection='3d') - >>> magnet = magpy.magnet.Cuboid(magnetization=(1,1,1), dimension=(1,2,3)) + >>> magnet = magpy.magnet.Cuboid(polarization=(1,1,1), dimension=(1,2,3)) >>> sens = magpy.Sensor(position=(0,0,3)) >>> magpy.show(magnet, sens, canvas=my_axis, zoom=1) >>> plt.show() # doctest: +SKIP @@ -359,8 +359,13 @@ def show( or as global style arguments in display. >>> import magpylib as magpy - >>> src1 = magpy.magnet.Sphere((1,1,1), 1, [(0,0,0), (0,0,3)]) - >>> src2 = magpy.magnet.Sphere((1,1,1), 1, [(1,0,0), (1,0,3)], style_path_show=False) + >>> src1 = magpy.magnet.Sphere(position=[(0,0,0), (0,0,3)], diameter=1, polarization=(1,1,1)) + >>> src2 = magpy.magnet.Sphere( + ... position=[(1,0,0), (1,0,3)], + ... diameter=1, + ... polarization=(1,1,1), + ... style_path_show=False + ... ) >>> magpy.defaults.display.style.magnet.magnetization.size = 2 >>> src1.style.magnetization.size = 1 >>> magpy.show(src1, src2, style_color='r') # doctest: +SKIP @@ -374,7 +379,7 @@ def show( >>> path_len = 40 >>> sensor = magpy.Sensor() >>> cyl1 = magpy.magnet.Cylinder( - ... magnetization=(100, 0, 0), + ... polarization=(.1, 0, 0), ... dimension=(1, 2), ... position=(4, 0, 0), ... style_label="Cylinder1", diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index 3f322432d..306222ade 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -55,11 +55,11 @@ def current_circle_field( >>> import numpy as np >>> import magpylib as magpy >>> H = magpy.core.current_circle_field( - >>> field='H', - >>> observers=np.array([(0,0,0), (1,1,1), (2,2,2)]), - >>> diameter=np.array([1,2,3]), - >>> current=np.array([1,1,2]) - >>> ) + ... field='H', + ... observers=np.array([(0,0,0), (1,1,1), (2,2,2)]), + ... diameter=np.array([1,2,3]), + ... current=np.array([1,1,2]) + ... ) >>> print(H) [[0. 0. 1. ] [0.0496243 0.0496243 0.02124542] diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 2d32212cf..deec0e068 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -50,11 +50,11 @@ def magnet_cuboid_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.magnet_cuboid_field( - >>> field='B', - >>> observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), - >>> dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), - >>> polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), - >>> ) + ... field='B', + ... observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), + ... dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), + ... polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), + ... ) >>> print(B) [[ 0. 0. -0.05227894] [-0.00820941 0.00849123 0.011429 ] diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 0c401e1e8..4ffe16f4c 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -290,11 +290,11 @@ def magnet_cylinder_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.magnet_cylinder_field( - >>> field='B', - >>> observers=np.array([(1,0,0), (1,0,0)]), - >>> dimension=np.array([(1,1), (1,3)]), - >>> polarization=np.array([(0,0,1), (.5,0,.5)]), - >>> ) + ... field='B', + ... observers=np.array([(1,0,0), (1,0,0)]), + ... dimension=np.array([(1,1), (1,3)]), + ... polarization=np.array([(0,0,1), (.5,0,.5)]), + ... ) >>> print(B) [[ 0. 0. -0.05185272] [ 0.06821654 0. -0.01576545]] diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 5bc1d03cb..0ffc43743 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2390,11 +2390,11 @@ def magnet_cylinder_segment_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.magnet_cylinder_segment_field( - >>> field='B', - >>> observers=np.array([(1,1,1), (1,1,1)]), - >>> dimension=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), - >>> polarization=np.array([(0,0,1), (.5,.5,0)]), - >>> ) + ... field='B', + ... observers=np.array([(1,1,1), (1,1,1)]), + ... dimension=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), + ... polarization=np.array([(0,0,1), (.5,.5,0)]), + ... ) >>> print(B) [[ 0.07046526 0.08373724 -0.0198113 ] [ 0.29846023 0.20757316 0.00349617]] diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index 488c53529..86ff7c757 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -42,10 +42,10 @@ def dipole_field( >>> import magpylib as magpy >>> import numpy as np >>> B = magpy.core.dipole_field( - >>> field="B", - >>> observers=np.array([(1,2,3), (-1,-2,-3)]), - >>> moment=np.array([(0,0,1e6), (1e5,0,1e7)]) - >>> ) + ... field="B", + ... observers=np.array([(1,2,3), (-1,-2,-3)]), + ... moment=np.array([(0,0,1e6), (1e5,0,1e7)]) + ... ) >>> print(B) [[0.00122722 0.00245444 0.00177265] [0.01212221 0.02462621 0.01784923]] diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index d0927c776..b54d25ff7 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -111,12 +111,12 @@ def current_polyline_field( >>> import numpy as np >>> import magpylib as magpy >>> H = magpy.core.current_polyline_field( - >>> field='H', - >>> observers=np.array([( 0,0,1)]*3), - >>> segment_start=np.array([(-.5,0,0), (.5,0,0), (1.5,0,0)]), - >>> segment_end=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0)]), - >>> current=np.array([1,1,1]), - >>> ) + ... field='H', + ... observers=np.array([( 0,0,1)]*3), + ... segment_start=np.array([(-.5,0,0), (.5,0,0), (1.5,0,0)]), + ... segment_end=np.array([(-1.5,0,0), (-.5,0,0), (.5,0,0)]), + ... current=np.array([1,1,1]), + ... ) >>> print(H) [[ 0. 0.03062433 -0. ] [ 0. 0.07117625 -0. ] diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index f3c9567dc..39d261cf0 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -50,11 +50,11 @@ def magnet_sphere_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.magnet_sphere_field( - >>> field='B', - >>> observers=np.array([(1,1,1), (1,1,1)]), - >>> diameter=np.array([1,5]), - >>> polarization=np.array([(1,2,3), (0,0,3)]), - >>> ) + ... field='B', + ... observers=np.array([(1,1,1), (1,1,1)]), + ... diameter=np.array([1,5]), + ... polarization=np.array([(1,2,3), (0,0,3)]), + ... ) >>> print(B) [[0.04009377 0.03207501 0.02405626] [0. 0. 2. ]] diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index 11bc16cff..6a0f68458 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -98,11 +98,11 @@ def magnet_tetrahedron_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.magnet_tetrahedron_field( - >>> field='B', - >>> observers=np.array([(1,2,3), (2,3,4)]), - >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0), (0,0,1))]*2), - >>> polarization=np.array([(222,333,444), (111,112,113)]), - >>> ) + ... field='B', + ... observers=np.array([(1,2,3), (2,3,4)]), + ... vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0), (0,0,1))]*2), + ... polarization=np.array([(222,333,444), (111,112,113)]), + ... ) >>> print(B) [[0.19075398 0.8240532 1.18170862] [0.03125701 0.08445416 0.1178967 ]] diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index f21cdb56c..0691043b9 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -117,11 +117,11 @@ def triangle_field( >>> import numpy as np >>> import magpylib as magpy >>> B = magpy.core.triangle_field( - >>> field='B', - >>> observers=np.array([(-.1,.2,.1), (.1,.2,.1)]), - >>> vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0))]*2), - >>> polarization=np.array([(.22,.33,.44), (.33,.44,.55)]), - >>> ) + ... field='B', + ... observers=np.array([(-.1,.2,.1), (.1,.2,.1)]), + ... vertices=np.array([((-1,0,0), (1,-1,0), (1,1,0))]*2), + ... polarization=np.array([(.22,.33,.44), (.33,.44,.55)]), + ... ) >>> print(B) [[-0.0548087 0.05350955 0.17683832] [-0.04252323 0.05292106 0.23092368]] diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index ee35a7b35..8653385e9 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -676,8 +676,8 @@ def getB( >>> src2 = magpy.magnet.Sphere(polarization=(0,0,.1), diameter=.001) >>> B = magpy.getB([src1, src2], (.01,.01,.01)) >>> print(B) - [[6.23597388e+00 6.23597388e+00 2.66977810e+00] - [8.01875374e-01 8.01875374e-01 1.48029737e-16]] + [[6.05434592e-06 6.05434592e-06 2.35680448e-08] + [8.01875374e-07 8.01875374e-07 1.51582450e-22]] We can also use sensor objects as observers input: diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 4dfbbd92d..8a8fadb21 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -90,12 +90,12 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Compute the B-field of a spherical magnet at three positions: >>> import magpylib as magpy - >>> src = magpy.magnet.Sphere((0,0,1000), 1) + >>> src = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> B = src.getB(((0,0,0), (1,0,0), (2,0,0))) >>> print(B) - [[ 0. 0. 666.66666667] - [ 0. 0. -41.66666667] - [ 0. 0. -5.20833333]] + [[ 0. 0. 0.66666667] + [ 0. 0. -0.04166667] + [ 0. 0. -0.00520833]] Compute the B-field at two sensors, each one with two pixels @@ -103,11 +103,11 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): >>> sens2 = sens1.copy(position=(2,0,0)) >>> B = src.getB(sens1, sens2) >>> print(B) - [[[ 12.19288783 0. -39.83010025] - [-12.19288783 0. -39.83010025]] + [[[ 0.01219289 0. -0.0398301 ] + [-0.01219289 0. -0.0398301 ]] - [[ 0.77638847 0. -5.15004352] - [ -0.77638847 0. -5.15004352]]] + [[ 0.00077639 0. -0.00515004] + [-0.00077639 0. -0.00515004]]] """ observers = format_star_input(observers) return getBH_level2( @@ -160,12 +160,12 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): >>> import magpylib as magpy - >>> src = magpy.magnet.Sphere((0,0,1000), 1) + >>> src = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> H = src.getH(((0,0,0), (1,0,0), (2,0,0))) >>> print(H) - [[ 0. 0. -265.25823849] - [ 0. 0. -33.15727981] - [ 0. 0. -4.14465998]] + [[ 0. 0. -265258.23848649] + [ 0. 0. -33157.27981081] + [ 0. 0. -4144.65997635]] Compute the H-field at two sensors, each one with two pixels @@ -173,11 +173,12 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): >>> sens2 = sens1.copy(position=(2,0,0)) >>> H = src.getH(sens1, sens2) >>> print(H) - [[[ 9.70279185 0. -31.69578669] - [ -9.70279185 0. -31.69578669]] + [[[ 9702.7918453 0. -31695.78669464] + [ -9702.7918453 0. -31695.78669464]] - [[ 0.61783031 0. -4.09827441] - [ -0.61783031 0. -4.09827441]]] + [[ 617.83031378 0. -4098.27441472] + [ -617.83031378 0. -4098.27441472]]] + """ observers = format_star_input(observers) return getBH_level2( diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index ea2ab6c81..cb5bad55c 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -456,7 +456,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs >>> import magpylib as magpy >>> >>> col = magpy.Collection( - ... [magpy.magnet.Sphere((0, 0, 1), 1, position=(i, 0, 0)) for i in range(3)] + ... [magpy.magnet.Sphere(position=(i, 0, 0), diameter=1, polarization=(0, 0, .1)) for i in range(3)] ... ) >>> # We apply styles using underscore magic for magnetization vector size and a style >>> # dictionary for the color. @@ -469,7 +469,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs >>> # Finally we create a separate sphere magnet to demonstrate the default style >>> # the collection and the separate magnet with Matplotlib: >>> - >>> src = magpy.magnet.Sphere((0, 0, 1), 1, position=(3, 0, 0)) + >>> src = magpy.magnet.Sphere(position=(3, 0, 0), diameter=1, polarization=(0, 0, .1)) >>> magpy.show(col, src) # doctest: +SKIP >>> # graphic output """ @@ -553,7 +553,7 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): In this example we create a collection from two sources and two sensors: >>> import magpylib as magpy - >>> src1 = magpy.magnet.Sphere((0,0,1000), 1) + >>> src1 = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> src2 = src1.copy() >>> sens1 = magpy.Sensor(position=(0,0,1)) >>> sens2 = sens1.copy() @@ -567,11 +567,11 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): >>> B = magpy.getB([src1, src2], col) >>> B = magpy.getB([src1, src2], [sens1, sens2]) >>> print(B) - [[[ 0. 0. 83.33333333] - [ 0. 0. 83.33333333]] + [[[0. 0. 0.08333333] + [0. 0. 0.08333333]] - [[ 0. 0. 83.33333333] - [ 0. 0. 83.33333333]]] + [[0. 0. 0.08333333] + [0. 0. 0.08333333]]] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -624,7 +624,7 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): In this example we create a collection from two sources and two sensors: >>> import magpylib as magpy - >>> src1 = magpy.magnet.Sphere((0,0,1000), 1) + >>> src1 = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> src2 = src1.copy() >>> sens1 = magpy.Sensor(position=(0,0,1)) >>> sens2 = sens1.copy() @@ -638,11 +638,11 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): >>> H = magpy.getH([src1, src2], col) >>> H = magpy.getH([src1, src2], [sens1, sens2]) >>> print(H) - [[[ 0. 0. 66.31455962] - [ 0. 0. 66.31455962]] + [[[ 0. 0. 66314.55962162] + [ 0. 0. 66314.55962162]] - [[ 0. 0. 66.31455962] - [ 0. 0. 66.31455962]]] + [[ 0. 0. 66314.55962162] + [ 0. 0. 66314.55962162]]] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -727,8 +727,8 @@ class Collection(BaseGeo, BaseCollection): we create a collection with two sources and move the whole collection: >>> import magpylib as magpy - >>> src1 = magpy.magnet.Sphere((1,2,3), 1, position=(2,0,0)) - >>> src2 = magpy.current.Circle(1, 1, position=(-2,0,0)) + >>> src1 = magpy.magnet.Sphere(position=(2,0,0), diameter=1,polarization=(.1,.2,.3)) + >>> src2 = magpy.current.Circle(position=(-2,0,0), diameter=1, current=1) >>> col = magpy.Collection(src1, src2) >>> col.move(((0,0,2))) Collection(id=...) @@ -756,7 +756,7 @@ class Collection(BaseGeo, BaseCollection): >>> B = col.getB((0,0,0)) >>> print(B) - [ 0.00126232 -0.00093169 -0.00034448] + [ 2.32922681e-04 -9.31694991e-05 -3.44484717e-10] We add a sensor at position (0,0,0) to the collection: @@ -771,7 +771,7 @@ class Collection(BaseGeo, BaseCollection): >>> B = col.getB() >>> print(B) - [ 0.00126232 -0.00093169 -0.00034448] + [ 2.32922681e-04 -9.31694991e-05 -3.44484717e-10] """ def __init__( diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 56290cda0..13e153fa7 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -58,10 +58,10 @@ class Sensor(BaseGeo, BaseDisplayRepr): >>> import magpylib as magpy >>> sens = magpy.Sensor() - >>> loop = magpy.current.Circle(current=1, diameter=1) + >>> loop = magpy.current.Circle(current=1, diameter=0.01) >>> B = sens.getB(loop) >>> print(B) - [0. 0. 1.25663706] + [0. 0. 0.00012566] We rotate the sensor by 45 degrees and compute the field again: @@ -69,16 +69,16 @@ class Sensor(BaseGeo, BaseDisplayRepr): Sensor(id=...) >>> B = sens.getB(loop) >>> print(B) - [0. 0.88857659 0.88857659] + [0.00000000e+00 8.88576588e-05 8.88576588e-05] Finally we set some sensor pixels and compute the field again: - >>> sens.pixel=((0,0,0), (.1,0,0), (.2,0,0)) + >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> B = sens.getB(loop) >>> print(B) - [[0. 0.88857659 0.88857659] - [0. 0.916274 0.916274 ] - [0. 1.01415383 1.01415383]] + [[0.00000000e+00 8.88576588e-05 8.88576588e-05] + [0.00000000e+00 9.16274003e-05 9.16274003e-05] + [0.00000000e+00 1.01415383e-04 1.01415383e-04]] """ _style_class = SensorStyle diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 33bde30ed..932db7666 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -77,8 +77,8 @@ class Circle(BaseCurrent): >>> B = src.getB(sens) >>> print(B) [[-1.63585841e-24 -4.44388287e-05 4.44388287e-05] - [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] - [-9.85948764e-24 -4.45190261e-05 4.45190261e-05]] + [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] + [-9.85948764e-24 -4.45190261e-05 4.45190261e-05]] """ _field_func = staticmethod(current_circle_field) diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index cc968b916..6f8d617b2 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -70,8 +70,8 @@ class Cuboid(BaseMagnet): >>> B = src.getB([(.01,0,0), (0,.01,0), (0,0,.01)]) >>> print(B) [[ 0.06739119 0.00476528 -0.0619486 ] - [-0.03557183 -0.01149497 -0.08403664] - [-0.03557183 0.00646436 0.14943466]] + [-0.03557183 -0.01149497 -0.08403664] + [-0.03557183 0.00646436 0.14943466]] """ _field_func = staticmethod(magnet_cuboid_field) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 681b2f5fb..0b7d429c1 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -66,9 +66,9 @@ class Cylinder(BaseMagnet): Cylinder(id=...) >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) - [[3.31419501 5.26683023 0.37767015] - [0.42298405 0.67710536 0.04464932] - [0.12571523 0.20144503 0.01312389]] + [[3.31419501e-03 5.26683023e-03 3.77670149e-04] + [4.22984050e-04 6.77105357e-04 4.46493154e-05] + [1.25715233e-04 2.01445027e-04 1.31238931e-05]] The same result is obtained when the rotated source moves along a path away from an observer at position (0.01,0.01,0.01). Here we use a `Sensor` object as observer. @@ -78,9 +78,9 @@ class Cylinder(BaseMagnet): Cylinder(id=...) >>> B = src.getB(sens) >>> print(B) - [[3.31419501 5.26683023 0.37767015] - [0.42298405 0.67710536 0.04464932] - [0.12571523 0.20144503 0.01312389]] + [[3.31419501e-03 5.26683023e-03 3.77670149e-04] + [4.22984050e-04 6.77105357e-04 4.46493154e-05] + [1.25715233e-04 2.01445027e-04 1.31238931e-05]] """ _field_func = staticmethod(magnet_cylinder_field) diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index 04c4ae7e7..c82524f3a 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -82,8 +82,8 @@ class CylinderSegment(BaseMagnet): >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) [[-0.0328285 0.03015882 -0.01632886] - [ 0.00062876 0.00397579 0.00073298] - [ 0.00025439 0.00074332 0.00011683]] + [ 0.00062876 0.00397579 0.00073298] + [ 0.00025439 0.00074332 0.00011683]] The same result is obtained when the rotated source moves along a path away from an observer at position (.01,.01,.01). Here we use a `Sensor` object as observer. @@ -94,8 +94,8 @@ class CylinderSegment(BaseMagnet): >>> B = src.getB(sens) >>> print(B) [[-0.0328285 0.03015882 -0.01632886] - [ 0.00062876 0.00397579 0.00073298] - [ 0.00025439 0.00074332 0.00011683]] + [ 0.00062876 0.00397579 0.00073298] + [ 0.00025439 0.00074332 0.00011683]] """ _field_func = staticmethod(magnet_cylinder_segment_field_internal) diff --git a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py index a9e043a8a..73a8d2171 100644 --- a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py +++ b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py @@ -75,7 +75,7 @@ class Tetrahedron(BaseMagnet): We rotate the source object, and compute the B-field, this time at a set of observer positions: >>> src.rotate_from_angax(45, 'x') - >>> Tetrahedron(id=...) + Tetrahedron(id=...) >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) >>> print(B) [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] @@ -87,7 +87,7 @@ class Tetrahedron(BaseMagnet): >>> sens = magpy.Sensor(position=(.01,.01,.01)) >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - >>> Tetrahedron(id=...) + Tetrahedron(id=...) >>> B = src.getB(sens) >>> print(B) [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] From 1170e9c0a77e6aad63cf201fc1f9b3011b0c77d5 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 22:26:16 +0100 Subject: [PATCH 075/240] fix loop and line deprecation --- magpylib/_src/fields/field_BH_circle.py | 3 +-- magpylib/_src/fields/field_BH_polyline.py | 2 +- tests/test_core_misc.py | 25 +++++++++++++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index 306222ade..e53b6b2d1 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -157,5 +157,4 @@ def current_loop_field(*args, **kwargs): MagpylibDeprecationWarning, stacklevel=2, ) - # return current_circle_field(*args, **kwargs) - return None + return current_circle_field(*args, **kwargs) diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index b54d25ff7..70039e7ce 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -241,4 +241,4 @@ def current_line_field(*args, **kwargs): MagpylibDeprecationWarning, stacklevel=2, ) - return None + return current_polyline_field(*args, **kwargs) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index f3c62f18a..8c984863e 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -583,13 +583,34 @@ def test_dipole_field_BH(): def test_line_deprecation(): + """test line deprecation""" + kw = { + "field": "B", + "observers": np.array([(0, 0, 1)]), + "segment_start": np.array([(0, 0, 1)]), + "segment_end": np.array([(0, 0, 1)]), + "current": np.array([1]), + } + B1 = current_polyline_field(**kw) with pytest.warns(MagpylibDeprecationWarning): - x = current_line_field("B", 1, 2, 3, 4) + B2 = current_line_field(**kw) + + np.testing.assert_allclose(B1, B2) def test_loop_deprecation(): + """test loop deprecation""" + kw = { + "field": "B", + "observers": np.array([(0, 0, 1)]), + "diameter": np.array([1]), + "current": np.array([1]), + } + B1 = current_circle_field(**kw) with pytest.warns(MagpylibDeprecationWarning): - x = current_loop_field("B", 1, 2, 3, 4) + B2 = current_loop_field(**kw) + + np.testing.assert_allclose(B1, B2) def test_field_loop_specials(): From 0aec38c90953ef3c52d1334783f081813c0ff23d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 22:43:02 +0100 Subject: [PATCH 076/240] pylint on magpylib folder --- magpylib/_src/fields/field_BH_polyline.py | 6 +++++- magpylib/_src/fields/field_wrap_BH.py | 4 ++-- magpylib/_src/obj_classes/class_Collection.py | 5 ++++- magpylib/_src/obj_classes/class_misc_CustomSource.py | 3 ++- tests/test_core_misc.py | 3 --- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 70039e7ce..31c01df36 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -35,7 +35,11 @@ def current_vertices_field( """ if vertices is None: return current_polyline_field( - field, observers, current, segment_start, segment_end + field=field, + observers=observers, + current=current, + segment_start=segment_start, + segment_end=segment_end, ) nvs = np.array([f.shape[0] for f in vertices]) # lengths of vertices sets diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 8653385e9..347a3bf97 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -668,8 +668,8 @@ def getB( Examples -------- - In this example we compute the B-field in units of tesla of a spherical magnet and a current loop - at the observer position (0.01,0.01,0.01) given in units of meter: + In this example we compute the B-field in units of tesla of a spherical magnet and a current + loop at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy >>> src1 = magpy.current.Circle(current=100, diameter=.002) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index cb5bad55c..5f01682e4 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -456,7 +456,10 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs >>> import magpylib as magpy >>> >>> col = magpy.Collection( - ... [magpy.magnet.Sphere(position=(i, 0, 0), diameter=1, polarization=(0, 0, .1)) for i in range(3)] + ... [ + ... magpy.magnet.Sphere(position=(i, 0, 0), diameter=1, polarization=(0, 0, 0.1)) + ... for i in range(3) + ... ] ... ) >>> # We apply styles using underscore magic for magnetization vector size and a style >>> # dictionary for the color. diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index dc6a108c8..2adfec59b 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -48,7 +48,8 @@ class CustomSource(BaseSource): >>> import numpy as np >>> import magpylib as magpy - >>> funcBH = lambda field, observers: np.array([(.01 if field=='B' else .08,0,0)]*len(observers)) + >>> def funcBH(field, observers): + ... return np.array([(.01 if field=='B' else .08,0,0)]*len(observers)) >>> src = magpy.misc.CustomSource(field_func=funcBH) >>> H = src.getH((.01,.01,.01)) >>> print(H) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index 8c984863e..3a9c7526a 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -4,7 +4,6 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibDeprecationWarning -from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.fields.field_BH_polyline import current_vertices_field from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 @@ -12,9 +11,7 @@ from magpylib.core import current_line_field from magpylib.core import current_loop_field from magpylib.core import current_polyline_field -from magpylib.core import dipole_field from magpylib.core import magnet_cuboid_field -from magpylib.core import magnet_cylinder_field from magpylib.core import magnet_cylinder_segment_field from magpylib.core import magnet_sphere_field from magpylib.core import magnet_tetrahedron_field From c061fac9a065899dae5aaea6fa6bbfd40265ddf1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 22:51:34 +0100 Subject: [PATCH 077/240] fix test compound setters --- tests/test_Coumpound_setters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_Coumpound_setters.py b/tests/test_Coumpound_setters.py index c057cd4a5..8cb3083e8 100644 --- a/tests/test_Coumpound_setters.py +++ b/tests/test_Coumpound_setters.py @@ -58,7 +58,7 @@ def make_wheel(Ncubes=6, height=10, diameter=36, path_len=5, label=None): def cs_lambda(): return magpy.magnet.Cuboid( - (1, 0, 0), + polarization=(1, 0, 0), dimension=[height] * 3, position=(diameter / 2, 0, 0), ) From 35c5b2d4f635c2a1e53957d436cdf6f611d6b916 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:03:46 +0100 Subject: [PATCH 078/240] add pol and mag initialization(needed for repr) --- magpylib/_src/obj_classes/class_BaseExcitations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 8a8fadb21..8c8af3d43 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -202,6 +202,8 @@ def __init__( ): super().__init__(position, orientation, style=style, **kwargs) + self._polarization = None + self._magnetization = None if magnetization: self.magnetization = magnetization if polarization: From 52d572641728938e217274f7202d6235791e3db1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:14:58 +0100 Subject: [PATCH 079/240] fix mpl tests --- tests/test_display_matplotlib.py | 42 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index bdac098bc..c67233751 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -26,7 +26,7 @@ def test_Cuboid_display(): """testing display""" - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.1, 0.1, 0.1), (2, 2, 2), 20), start=-1) src.show( style_path_frames=5, @@ -46,7 +46,7 @@ def test_Cylinder_display(): """testing display""" # path should revert to True ax = plt.subplot(projection="3d") - src = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + src = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) src.show(canvas=ax, style_path_frames=15, backend="matplotlib") # hide path @@ -68,7 +68,9 @@ def test_Cylinder_display(): def test_CylinderSegment_display(): """testing display""" ax = plt.subplot(projection="3d") - src = magpy.magnet.CylinderSegment((1, 2, 3), (2, 4, 5, 30, 40)) + src = magpy.magnet.CylinderSegment( + polarization=(1, 2, 3), dimension=(2, 4, 5, 30, 40) + ) src.show(canvas=ax, style_path_frames=15, return_fig=True) src.move(np.linspace((0.4, 0.4, 0.4), (13.2, 13.2, 13.2), 33), start=-1) @@ -79,7 +81,7 @@ def test_Sphere_display(): """testing display""" # path should revert to True ax = plt.subplot(projection="3d") - src = magpy.magnet.Sphere((1, 2, 3), 2) + src = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=2) src.show(canvas=ax, style_path_frames=15, return_fig=True) src.move(np.linspace((0.4, 0.4, 0.4), (8, 8, 8), 20), start=-1) @@ -186,7 +188,7 @@ def test_Triangle_display_from_convexhull(): ) -def test_TringularMesh_display(): +def test_TriangularMesh_display(): """testing display for TriangleMesh source built from vertices""" # test classic trimesh display points = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] @@ -211,9 +213,9 @@ def test_TringularMesh_display(): faces = polydata.faces.reshape(-1, 4)[:, 1:] faces = faces[1:] # open the mesh src = magpy.magnet.TriangularMesh( - (0, 0, 1000), - vertices, - faces, + polarization=(0, 0, 1000), + vertices=vertices, + faces=faces, check_open="ignore", check_disconnected="ignore", reorient_faces=False, @@ -231,9 +233,9 @@ def test_TringularMesh_display(): with pytest.warns(UserWarning) as record: magpy.magnet.TriangularMesh( - (0, 0, 1000), - vertices, - faces, + polarization=(0, 0, 1000), + vertices=vertices, + faces=faces, check_open="skip", check_disconnected="skip", reorient_faces=False, @@ -267,7 +269,7 @@ def test_TringularMesh_display(): faces = np.array([v for k, v in selfintersecting_mesh3d.items() if k in "ijk"]).T with pytest.warns(UserWarning) as record: magpy.magnet.TriangularMesh( - (0, 0, 1000), + polarization=(0, 0, 1000), vertices=vertices, faces=faces, check_open="warn", @@ -293,7 +295,7 @@ def test_col_display(): """testing display""" # pylint: disable=assignment-from-no-return ax = plt.subplot(projection="3d") - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) pm2 = pm1.copy(position=(2, 0, 0)) pm3 = pm1.copy(position=(4, 0, 0)) nested_col = (pm1 + pm2 + pm3).set_children_styles(color="magenta") @@ -315,11 +317,11 @@ def test_circular_line_display(): """testing display""" # pylint: disable=assignment-from-no-return ax2 = plt.subplot(projection="3d") - src1 = magpy.current.Circle(1, 2) - src2 = magpy.current.Circle(1, 2) + src1 = magpy.current.Circle(current=1, diameter=2) + src2 = magpy.current.Circle(current=1, diameter=2) src1.move(np.linspace((0.4, 0.4, 0.4), (2, 2, 2), 5), start=-1) - src3 = magpy.current.Polyline(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)]) - src4 = magpy.current.Polyline(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)]) + src3 = magpy.current.Polyline(current=1, vertices=[(0, 0, 0), (1, 1, 1), (2, 2, 2)]) + src4 = magpy.current.Polyline(current=1, vertices=[(0, 0, 0), (1, 1, 1), (2, 2, 2)]) src3.move([(0.4, 0.4, 0.4)] * 5, start=-1) src1.show(canvas=ax2, style_path_frames=2, style_arrow_size=0, return_fig=True) src2.show(canvas=ax2, style_arrow_sizemode="absolute", return_fig=True) @@ -431,7 +433,7 @@ def test_empty_display(): def test_graphics_model_mpl(): """test base extra graphics with mpl""" ax = plt.subplot(projection="3d") - c = magpy.magnet.Cuboid((0, 1, 0), (1, 1, 1)) + c = magpy.magnet.Cuboid(polarization=(0, 1, 0), dimension=(1, 1, 1)) c.rotate_from_angax(33, "x", anchor=0) c.style.model3d.add_trace(**make_Cuboid("matplotlib", position=(2, 0, 0))) c.show(canvas=ax, style_path_frames=1, backend="matplotlib", return_fig=True) @@ -439,7 +441,7 @@ def test_graphics_model_mpl(): def test_graphics_model_generic_to_mpl(): """test generic base extra graphics with mpl""" - c = magpy.magnet.Cuboid((0, 1, 0), (1, 1, 1)) + c = magpy.magnet.Cuboid(polarization=(0, 1, 0), dimension=(1, 1, 1)) c.move([[i, 0, 0] for i in range(2)]) model3d = make_Cuboid(position=(2, 0, 0)) model3d["kwargs"]["facecolor"] = np.array(["blue"] * 12) @@ -450,7 +452,7 @@ def test_graphics_model_generic_to_mpl(): def test_mpl_animation(): """test animation with matplotib""" - c = magpy.magnet.Cuboid((0, 1, 0), (1, 1, 1)) + c = magpy.magnet.Cuboid(polarization=(0, 1, 0), dimension=(1, 1, 1)) c.move([[i, 0, 0] for i in range(2)]) fig, anim = c.show( backend="matplotlib", animation=True, return_animation=True, return_fig=True From 56c201ef33e820a166b2a352ede3d43a81617b47 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:15:58 +0100 Subject: [PATCH 080/240] fix _default_style_description units --- magpylib/_src/obj_classes/class_magnet_Cuboid.py | 2 +- magpylib/_src/obj_classes/class_magnet_Cylinder.py | 2 +- magpylib/_src/obj_classes/class_magnet_CylinderSegment.py | 2 +- magpylib/_src/obj_classes/class_magnet_Sphere.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 6f8d617b2..9cffbe3a6 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -121,4 +121,4 @@ def _default_style_description(self): if self.dimension is None: return "no dimension" d = [unit_prefix(d) for d in self.dimension] - return f"{d[0]}|{d[1]}|{d[2]}" + return f"{d[0]}m|{d[1]}m|{d[2]}m" diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 0b7d429c1..a523f1a40 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -130,4 +130,4 @@ def _default_style_description(self): if self.dimension is None: return "no dimension" d = [unit_prefix(d) for d in self.dimension] - return f"D={d[0]}, H={d[1]}" + return f"D={d[0]}m, H={d[1]}m" diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index c82524f3a..9f88a3846 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -179,4 +179,4 @@ def _default_style_description(self): if self.dimension is None: return "no dimension" d = [unit_prefix(d) for d in self.dimension] - return f"r={d[0]}|{d[1]}, h={d[2]}, φ={d[3]}°|{d[4]}°" + return f"r={d[0]}m|{d[1]}m, h={d[2]}m, φ={d[3]}°|{d[4]}°" diff --git a/magpylib/_src/obj_classes/class_magnet_Sphere.py b/magpylib/_src/obj_classes/class_magnet_Sphere.py index 4468e492f..df83f6b02 100644 --- a/magpylib/_src/obj_classes/class_magnet_Sphere.py +++ b/magpylib/_src/obj_classes/class_magnet_Sphere.py @@ -127,4 +127,4 @@ def _default_style_description(self): """Default style description text""" if self.diameter is None: return "no dimension" - return f"D={unit_prefix(self.diameter)}" + return f"D={unit_prefix(self.diameter)}m" From 10f03b1d72cde2c59822fd081601ae618ba72ed5 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:16:20 +0100 Subject: [PATCH 081/240] fix display plotly tests --- tests/test_display_plotly.py | 42 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index 6a09f5752..c41362028 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -13,7 +13,7 @@ def test_Cylinder_display(): """testing display""" magpy.defaults.display.backend = "plotly" fig = go.Figure() - src = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + src = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) x = src.show(canvas=fig, style_path_frames=15) assert x is None, "path should revert to True" @@ -31,7 +31,9 @@ def test_CylinderSegment_display(): """testing display""" magpy.defaults.display.backend = "plotly" fig = go.Figure() - src = magpy.magnet.CylinderSegment((1, 2, 3), (2, 4, 5, 30, 40)) + src = magpy.magnet.CylinderSegment( + polarization=(1, 2, 3), dimension=(2, 4, 5, 30, 40) + ) x = src.show(canvas=fig, style_path_frames=15) assert x is None, "path should revert to True" @@ -49,7 +51,7 @@ def test_Sphere_display(): """testing display""" magpy.defaults.display.backend = "plotly" fig = go.Figure() - src = magpy.magnet.Sphere((1, 2, 3), 2) + src = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=2) x = src.show(canvas=fig, style_path_frames=15) assert x is None, "path should revert to True" @@ -61,7 +63,7 @@ def test_Sphere_display(): def test_Cuboid_display(): """testing display""" magpy.defaults.display.backend = "plotly" - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.1, 0.1, 0.1), (2, 2, 2), 20), start=-1) x = src.show(style_path_frames=5, style_magnetization_show=True, renderer="json") assert x is None, "display test fail" @@ -106,7 +108,7 @@ def test_Triangle_display(): # this test is necessary to cover the case where the backend can display mag arrows and # color gradient must be deactivated verts = [(0, 0, 0), (1, 0, 0), (0, 1, 0)] - src = magpy.misc.Triangle(magnetization=(100, 200, 300), vertices=verts) + src = magpy.misc.Triangle(polarization=(0.1, 0.2, 0.3), vertices=verts) src.show(style_magnetization_mode="arrow", return_fig=True) @@ -115,7 +117,7 @@ def test_col_display(): # pylint: disable=assignment-from-no-return magpy.defaults.display.backend = "plotly" fig = go.Figure() - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) pm2 = pm1.copy(position=(2, 0, 0)) pm3 = pm1.copy(position=(4, 0, 0)) nested_col = (pm1 + pm2 + pm3).set_children_styles(color="magenta") @@ -145,11 +147,11 @@ def test_circular_line_display(): # pylint: disable=assignment-from-no-return magpy.defaults.display.backend = "plotly" fig = go.Figure() - src1 = magpy.current.Circle(1, 2) - src2 = magpy.current.Circle(1, 2) + src1 = magpy.current.Circle(current=1, diameter=2) + src2 = magpy.current.Circle(current=1, diameter=2) src1.move(np.linspace((0.4, 0.4, 0.4), (2, 2, 2), 5), start=-1) - src3 = magpy.current.Polyline(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)]) - src4 = magpy.current.Polyline(1, [(0, 0, 0), (1, 1, 1), (2, 2, 2)]) + src3 = magpy.current.Polyline(current=1, vertices=[(0, 0, 0), (1, 1, 1), (2, 2, 2)]) + src4 = magpy.current.Polyline(current=1, vertices=[(0, 0, 0), (1, 1, 1), (2, 2, 2)]) src3.move([(0.4, 0.4, 0.4)] * 5, start=-1) x = src1.show(canvas=fig, style_path_frames=2, style_arrow_show=False) assert x is None, "display test fail" @@ -172,7 +174,7 @@ def test_display_bad_style_kwargs(): def test_extra_model3d(): """test diplay when object has an extra model object attached""" magpy.defaults.display.backend = "plotly" - cuboid = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + cuboid = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) cuboid.move(np.linspace((0.4, 0.4, 0.4), (12.4, 12.4, 12.4), 33), start=-1) cuboid.style.model3d.showdefault = False cuboid.style.model3d.data = [ @@ -260,7 +262,7 @@ def test_display_warnings(): magpy.defaults.display.backend = "plotly" magpy.defaults.display.animation.maxfps = 2 magpy.defaults.display.animation.maxframes = 2 - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.4, 0.4, 0.4), (4, 4, 4), 10), start=-1) fig = go.Figure() @@ -268,7 +270,7 @@ def test_display_warnings(): src.show(canvas=fig, animation=5, animation_fps=3) with pytest.warns(UserWarning): # max frames surpassed src.show(canvas=fig, animation=True, animation_time=2, animation_fps=1) - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) with pytest.warns(UserWarning): # no object path detected src.show(canvas=fig, style_path_frames=[], animation=True) @@ -278,7 +280,7 @@ def test_bad_animation_value(): magpy.defaults.display.backend = "plotly" magpy.defaults.display.animation.maxfps = 2 magpy.defaults.display.animation.maxframes = 2 - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.4, 0.4, 0.4), (4, 4, 4), 10), start=-1) fig = go.Figure() @@ -332,7 +334,9 @@ def test_legends(): "constructor": "scatter3d", "kwargs": {"x": xs, "y": ys, "z": zs, "mode": "lines"}, } - c = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1), style_label="Plotly extra trace") + c = magpy.magnet.Cuboid( + polarization=(0, 0, 1), dimension=(1, 1, 1), style_label="Plotly extra trace" + ) c.style.model3d.add_trace(trace_plotly) fig = magpy.show( @@ -343,7 +347,7 @@ def test_legends(): # style_model3d_showdefault=False, return_fig=True, ) - assert [t.name for t in fig.data] == ["Plotly extra trace (1mm|1mm|1mm)"] * 2 + assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] * 2 assert [t.showlegend for t in fig.data] == [False, False] fig = magpy.show( @@ -354,7 +358,7 @@ def test_legends(): # style_model3d_showdefault=False, return_fig=True, ) - assert [t.name for t in fig.data] == ["Plotly extra trace (1mm|1mm|1mm)"] * 2 + assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] * 2 assert [t.showlegend for t in fig.data] == [True, False] fig = magpy.show( @@ -365,7 +369,7 @@ def test_legends(): style_model3d_showdefault=False, return_fig=True, ) - assert [t.name for t in fig.data] == ["Plotly extra trace (1mm|1mm|1mm)"] + assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] assert [t.showlegend for t in fig.data] == [True] c.rotate_from_angax([10 * i for i in range(N)], "y", start=0, anchor=(0, 0, 10)) @@ -377,5 +381,5 @@ def test_legends(): # style_model3d_showdefault=False, return_fig=True, ) - assert [t.name for t in fig.data] == ["Plotly extra trace (1mm|1mm|1mm)"] * 4 + assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] * 4 assert [t.showlegend for t in fig.data] == [True, False, False, False] From 795c241f35cc404238d72542ceb8406b2dcf442b Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:24:50 +0100 Subject: [PATCH 082/240] fix display pyvista test --- tests/test_display_pyvista.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index a77747b5a..9390c1619 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -35,7 +35,7 @@ def test_Cuboid_display(): "test simple display with path" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) src.move([[i, 0, 0] for i in range(2)], start=0) fig = src.show(return_fig=True, style_path_numbering=True, backend="pyvista") assert isinstance(fig, pv.Plotter) @@ -44,14 +44,14 @@ def test_Cuboid_display(): def test_animation(): "animation not supported, should warn and display static" pl = pv.Plotter() - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with pytest.warns(UserWarning): src.show(canvas=pl, animation=True, backend="pyvista") def test_ipygany_jupyter_backend(): """ipygany backend does not support custom colorscales""" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1000), dimension=(1, 1, 1)) src.show(return_fig=True, backend="pyvista", jupyter_backend="ipygany") @@ -90,7 +90,7 @@ def test_pyvista_animation(is_notebook_result, extension, filename): ) sensor.style.label = "Sensor1" cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(1, 2), style_label="Cylinder1" ) # define paths @@ -119,7 +119,7 @@ def test_pyvista_animation(is_notebook_result, extension, filename): def test_incompatible_jupyter_backend_for2d(): """test_incompatible_pyvista_backend""" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) sens = magpy.Sensor() with pytest.warns( UserWarning, From ec6157175e6643d9b15a2e255cbe77c5a0e4cbff Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:24:59 +0100 Subject: [PATCH 083/240] fix exceptins tests --- tests/test_exceptions.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 01caa8d85..77f45c7c1 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -18,7 +18,7 @@ def getBHv_unknown_source_type(): getBH_level2( sources="badName", observers=(0, 0, 0), - magnetization=(1, 0, 0), + polarization=(1, 0, 0), dimension=(0, 2, 1, 0, 360), position=(0, 0, -0.5), field="B", @@ -57,21 +57,21 @@ def getBHv_missing_input1(): x = np.array([(1, 2, 3)]) # pylint: disable=missing-kwoa getBH_level2( - sources="Cuboid", observers=x, magnetization=x, dimension=x, **GETBH_KWARGS + sources="Cuboid", observers=x, polarization=x, dimension=x, **GETBH_KWARGS ) def getBHv_missing_input2(): """missing source_type""" x = np.array([(1, 2, 3)]) - getBH_level2(observers=x, field="B", magnetization=x, dimension=x, **GETBH_KWARGS) + getBH_level2(observers=x, field="B", polarization=x, dimension=x, **GETBH_KWARGS) def getBHv_missing_input3(): """missing observer""" x = np.array([(1, 2, 3)]) getBH_level2( - sources="Cuboid", field="B", magnetization=x, dimension=x, **GETBH_KWARGS + sources="Cuboid", field="B", polarization=x, dimension=x, **GETBH_KWARGS ) @@ -85,7 +85,7 @@ def getBHv_missing_input5_cuboid(): """missing Cuboid dim""" x = np.array([(1, 2, 3)]) getBH_level2( - sources="Cuboid", observers=x, field="B", magnetization=x, **GETBH_KWARGS + sources="Cuboid", observers=x, field="B", polarization=x, **GETBH_KWARGS ) @@ -102,7 +102,7 @@ def getBHv_missing_input5_cyl(): """missing Cylinder dim""" x = np.array([(1, 2, 3)]) getBH_level2( - sources="Cylinder", observers=x, field="B", magnetization=x, **GETBH_KWARGS + sources="Cylinder", observers=x, field="B", polarization=x, **GETBH_KWARGS ) @@ -116,7 +116,7 @@ def getBHv_missing_input5_sphere(): """missing Sphere dim""" x = np.array([(1, 2, 3)]) getBH_level2( - sources="Sphere", observers=x, field="B", magnetization=x, **GETBH_KWARGS + sources="Sphere", observers=x, field="B", polarization=x, **GETBH_KWARGS ) @@ -129,7 +129,7 @@ def getBHv_bad_input1(): sources="Cuboid", observers=x, field="B", - magnetization=x2, + polarization=x2, dimension=x, **GETBH_KWARGS, ) @@ -142,7 +142,7 @@ def getBHv_bad_input2(): sources="Cubooid", observers=x, field="B", - magnetization=x, + polarization=x, dimension=x, **GETBH_KWARGS, ) @@ -156,7 +156,7 @@ def getBHv_bad_input3(): sources="Cuboid", observers=s, field="B", - magnetization=x, + polarization=x, dimension=x, **GETBH_KWARGS, ) @@ -209,7 +209,7 @@ def bad_input_shape_cuboid_dim(): def bad_input_shape_cuboid_mag(): - """bad cuboid magnetization shape""" + """bad cuboid polarization shape""" vec3 = (1, 2, 3) vec4 = (1, 2, 3, 4) magpy.magnet.Cuboid(vec4, vec3) @@ -223,14 +223,14 @@ def bad_input_shape_cyl_dim(): def bad_input_shape_cyl_mag(): - """bad cylinder magnetization shape""" + """bad cylinder polarization shape""" vec3 = (1, 2, 3) vec4 = (1, 2, 3, 4) magpy.magnet.Cylinder(vec4, vec3) def bad_input_shape_sphere_mag(): - """bad sphere magnetization shape""" + """bad sphere polarization shape""" vec4 = (1, 2, 3, 4) magpy.magnet.Sphere(vec4, 1) @@ -243,7 +243,7 @@ def bad_input_shape_sensor_pix_pos(): def bad_input_shape_dipole_mom(): - """bad sphere magnetization shape""" + """bad sphere polarization shape""" vec4 = (1, 2, 3, 4) magpy.misc.Dipole(moment=vec4) From b4c7ff2aea5120b8d024546c3f35280232c02392 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:39:17 +0100 Subject: [PATCH 084/240] allow pol and mag to be np arrays --- magpylib/_src/obj_classes/class_BaseExcitations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 8c8af3d43..df35d2ba2 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -204,14 +204,14 @@ def __init__( self._polarization = None self._magnetization = None - if magnetization: + if magnetization is not None: self.magnetization = magnetization - if polarization: + if polarization is not None: raise ValueError( "The attributes magnetization and polarization are dependent. " "Only one can be provided at magnet initialization." ) - if polarization: + if polarization is not None: self.polarization = polarization @property From 8dc7c245fc1c207ec3207631154a3908da110471 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 24 Dec 2023 23:56:51 +0100 Subject: [PATCH 085/240] fix field cylinder tests --- tests/test_field_cylinder.py | 61 +++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index 1934015cb..7d9b6733f 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -206,14 +206,18 @@ def test_cylinder_field1(): eins = np.ones(N) d, h, _ = dim.T dim5 = np.array([nulll, d / 2, h, nulll, eins * 360]).T - B1 = magnet_cylinder_segment_field("B", poso, magg, dim5) + B1 = magnet_cylinder_segment_field( + field="B", observers=poso, polarization=magg, dimension=dim5 + ) assert np.allclose(B1, B0) def test_cylinder_slanovc_field2(): """testing B for all input combinations in/out/surface of Tile solution""" - src = magpy.magnet.CylinderSegment((22, 33, 44), (0.5, 1, 2, 0, 90)) + src = magpy.magnet.CylinderSegment( + polarization=(22, 33, 44), dimension=(0.5, 1, 2, 0, 90) + ) binn = (5.52525937, 13.04561569, 40.11111556) bout = (0.0177018, 0.1277188, 0.27323195) @@ -264,10 +268,12 @@ def test_cylinder_slanovc_field2(): def test_cylinder_slanovc_field3(): """testing H for all input combinations in/out/surface of Tile solution""" - src = magpy.magnet.CylinderSegment((22, 33, 44), (0.5, 1, 2, 0, 90)) + src = magpy.magnet.CylinderSegment( + polarization=(22, 33, 44), dimension=(0.5, 1, 2, 0, 90) + ) - hinn = (-13.11018204, -15.87919449, -3.09467591) - hout = (0.01408664, 0.1016354, 0.21743108) + hinn = np.array((-13.11018204, -15.87919449, -3.09467591)) * 1e6 + hout = np.array((0.01408664, 0.1016354, 0.21743108)) * 1e6 nulll = (0, 0, 0) # only inside @@ -317,7 +323,7 @@ def test_cylinder_rauber_field4(): """ test continuity across indefinite form in cylinder_rauber field when observer at r=r0 """ - src = magpy.magnet.Cylinder((22, 33, 0), (2, 2)) + src = magpy.magnet.Cylinder(polarization=(22, 33, 0), dimension=(2, 2)) es = list(10 ** -np.linspace(11, 15, 50)) xs = np.r_[1 - np.array(es), 1, 1 + np.array(es)[::-1]] possis = [(x, 0, 1.5) for x in xs] @@ -328,8 +334,12 @@ def test_cylinder_rauber_field4(): def test_cylinder_tile_negative_phi(): """same result for phi>0 and phi<0 inputs""" - src1 = magpy.magnet.CylinderSegment((11, 22, 33), (2, 4, 4, 0, 45)) - src2 = magpy.magnet.CylinderSegment((11, 22, 33), (2, 4, 4, -360, -315)) + src1 = magpy.magnet.CylinderSegment( + polarization=(11, 22, 33), dimension=(2, 4, 4, 0, 45) + ) + src2 = magpy.magnet.CylinderSegment( + polarization=(11, 22, 33), dimension=(2, 4, 4, -360, -315) + ) B1 = src1.getB((1, 0.5, 0.1)) B2 = src2.getB((1, 0.5, 0.1)) assert np.allclose(B1, B2) @@ -345,9 +355,18 @@ def test_cylinder_tile_vs_fem(): mag3 = np.array((0, 1, -1)) / np.sqrt(2) * 1000 # Magpylib magnet collection - m1 = magpy.magnet.CylinderSegment(mag1, (1, 2, 1, -90, 0)) - m2 = magpy.magnet.CylinderSegment(mag2, (1, 2.5, 1.5, 200, 250)) - m3 = magpy.magnet.CylinderSegment(mag3, (0.75, 3, 0.5, 70, 180)) + m1 = magpy.magnet.CylinderSegment( + polarization=mag1, + dimension=(1, 2, 1, -90, 0), + ) + m2 = magpy.magnet.CylinderSegment( + polarization=mag2, + dimension=(1, 2.5, 1.5, 200, 250), + ) + m3 = magpy.magnet.CylinderSegment( + polarization=mag3, + dimension=(0.75, 3, 0.5, 70, 180), + ) col = m1 + m2 + m3 # create observer circles (see FEM screen shot) @@ -378,7 +397,7 @@ def test_cylinder_tile_vs_fem(): def test_cylinder_corner(): """test corner =0 behavior""" a = 1 - s = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) B = s.getB( [ [0, a, a], @@ -398,7 +417,7 @@ def test_cylinder_corner_scaling(): """test corner=0 scaling""" a = 1 obs = [[a, 0, a + 1e-14], [a + 1e-14, 0, a]] - s = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) Btest = [ [5.12553286e03, -2.26623480e00, 2.59910242e02], [5.12803286e03, -2.26623480e00, 9.91024238e00], @@ -407,7 +426,7 @@ def test_cylinder_corner_scaling(): a = 1000 obs = [[a, 0, a + 1e-14], [a + 1e-14, 0, a]] - s = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) np.testing.assert_allclose(s.getB(obs), np.zeros((2, 3))) @@ -429,15 +448,15 @@ def test_cylinder_scaling_invariance(): ) a = 1e-6 - s1 = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s1 = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) Btest1 = s1.getB(obs * a) a = 1 - s2 = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s2 = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) Btest2 = s2.getB(obs) a = 1e7 - s3 = magpy.magnet.Cylinder((10, 10, 1000), (2 * a, 2 * a)) + s3 = magpy.magnet.Cylinder(polarization=(10, 10, 1000), dimension=(2 * a, 2 * a)) Btest3 = s3.getB(obs * a) np.testing.assert_allclose(Btest1, Btest2) @@ -450,10 +469,10 @@ def test_cylinder_diametral_small_r(): test if the gneral case fluctuations are small """ B = magpy.core.magnet_cylinder_field( - "B", - np.array([(x, 0, 3) for x in np.logspace(-1.4, -1.2, 1000)]), - np.array([(1, 1, 0)] * 1000), - np.array([(2, 2)] * 1000), + field="B", + observers=np.array([(x, 0, 3) for x in np.logspace(-1.4, -1.2, 1000)]), + polarization=np.array([(1, 1, 0)] * 1000), + dimension=np.array([(2, 2)] * 1000), ) dB = np.log(abs(B[1:] - B[:-1])) From 902013269c016e18ff7b351a1553d954e94f9f27 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 00:32:37 +0100 Subject: [PATCH 086/240] avoid low mag warnings on display tests --- tests/test_display_matplotlib.py | 17 +++++++---------- tests/test_display_plotly.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index c67233751..c0675c412 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -96,7 +96,7 @@ def test_Sphere_display(): def test_Tetrahedron_display(): """testing Tetrahedron display""" verts = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] - src = magpy.magnet.Tetrahedron(magnetization=(100, 200, 300), vertices=verts) + src = magpy.magnet.Tetrahedron(polarization=(0.1, 0.2, 0.3), vertices=verts) src.show(return_fig=True, style_magnetization_mode="color+arrow") @@ -143,7 +143,7 @@ def test_Triangle_display(): triangles = np.array([v for k, v in mesh3d["kwargs"].items() if k in "ijk"]).T src = magpy.Collection( [ - magpy.misc.Triangle(magnetization=(1000, 1000, 0), vertices=v) + magpy.misc.Triangle(polarization=(1, 1, 0), vertices=v) for v in points[triangles] ] ) @@ -171,10 +171,7 @@ def test_Triangle_display_from_convexhull(): points = np.array([v for k, v in mesh3d["kwargs"].items() if k in "xyz"]).T faces = np.array([v for k, v in mesh3d["kwargs"].items() if k in "ijk"]).T src = magpy.Collection( - [ - magpy.misc.Triangle(magnetization=(1000, 0, 0), vertices=v) - for v in points[faces] - ] + [magpy.misc.Triangle(polarization=(1, 0, 0), vertices=v) for v in points[faces]] ) magpy.show( *src, @@ -194,7 +191,7 @@ def test_TriangularMesh_display(): points = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] src = magpy.magnet.TriangularMesh.from_ConvexHull( - magnetization=(1000, 0, 0), points=points + polarization=(1, 0, 0), points=points ) src.show( backend="matplotlib", @@ -470,7 +467,7 @@ def test_subplots(): ) sensor.style.label = "Sensor1" cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(1, 2), style_label="Cylinder1" ) # define paths @@ -499,7 +496,7 @@ def test_bad_show_inputs(): """bad show inputs""" cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(1, 2), style_label="Cylinder1" ) # test bad canvas @@ -523,7 +520,7 @@ def test_bad_show_inputs(): pixel=np.linspace((0, 0, -0.2), (0, 0, 0.2), 2), style_size=1.5 ) cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(1, 2), style_label="Cylinder1" ) with pytest.raises( ValueError, diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index c41362028..f6e5256ef 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -298,7 +298,7 @@ def test_subplots(): for x in np.linspace(0, 10, 11) ] ) - cyl1 = magpy.magnet.Cylinder(magnetization=(100, 0, 0), dimension=(1, 2)) + cyl1 = magpy.magnet.Cylinder(polarization=(0.1, 0, 0), dimension=(1, 2)) # define paths sensors.position = np.linspace((0, 0, -3), (0, 0, 3), 100) From 4612f0fd475ab1902170f8e9a2268db629606e73 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 00:58:48 +0100 Subject: [PATCH 087/240] fix getBH tests (up to dipole) --- tests/test_getBH_dict.py | 55 ++++++++++++----------- tests/test_getBH_interfaces.py | 59 ++++++++++++------------ tests/test_getBH_level2.py | 82 ++++++++++++++++++---------------- 3 files changed, 103 insertions(+), 93 deletions(-) diff --git a/tests/test_getBH_dict.py b/tests/test_getBH_dict.py index ff338160d..099696b1e 100644 --- a/tests/test_getBH_dict.py +++ b/tests/test_getBH_dict.py @@ -13,7 +13,7 @@ def test_getB_dict1(): mag = [111, 222, 333] dim = [3, 3] - pm = magpy.magnet.Cylinder(mag, dim) + pm = magpy.magnet.Cylinder(polarization=mag, dimension=dim) pm.move(np.linspace((0.5, 0, 0), (7.5, 0, 0), 15), start=-1) pm.rotate_from_angax(np.linspace(0, 666, 25), "y", anchor=0) pm.move([(0, x, 0) for x in np.linspace(0, 5, 5)], start=-1) @@ -22,7 +22,7 @@ def test_getB_dict1(): pos = pm.position rot = pm.orientation - dic = {"magnetization": mag, "dimension": dim, "position": pos, "orientation": rot} + dic = {"polarization": mag, "dimension": dim, "position": pos, "orientation": rot} B1 = magpy.getB("Cylinder", pos_obs, **dic) assert np.allclose(B1, B2, rtol=1e-12, atol=1e-12) @@ -35,10 +35,10 @@ def test_getB_dict2(): dim = [3, 3] pos = [(1, 1, 1), (2, 2, 2), (3, 3, 3), (5, 5, 5)] - dic = {"magnetization": mag, "dimension": dim, "position": pos} + dic = {"polarization": mag, "dimension": dim, "position": pos} B1 = magpy.getB("Cylinder", pos_obs, **dic) - pm = magpy.magnet.Cylinder(mag, dim, position=pos) + pm = magpy.magnet.Cylinder(polarization=mag, dimension=dim, position=pos) B2 = magpy.getB([pm], pos_obs) assert np.allclose(B1, B2, rtol=1e-12, atol=1e-12) @@ -50,13 +50,10 @@ def test_getH_dict1(): mag = [111, 222, 333] dim = [3, 3] - dic = { - "magnetization": mag, - "dimension": dim, - } + dic = {"polarization": mag, "dimension": dim} B1 = magpy.getH("Cylinder", pos_obs, **dic) - pm = magpy.magnet.Cylinder(mag, dim) + pm = magpy.magnet.Cylinder(polarization=mag, dimension=dim) B2 = pm.getH(pos_obs) assert np.allclose(B1, B2, rtol=1e-12, atol=1e-12) @@ -73,12 +70,14 @@ def test_getB_dict3(): pos = np.array([0, 0, 0]) rot = R.from_quat([(t, 0.2, 0.3, 0.4) for t in np.linspace(0, 0.1, n)]) - dic = {"magnetization": mag, "dimension": dim, "position": pos, "orientation": rot} + dic = {"polarization": mag, "dimension": dim, "position": pos, "orientation": rot} B1 = magpy.getB("Cuboid", pos_obs, **dic) B2 = [] for i in range(n): - pm = magpy.magnet.Cuboid(mag[i], dim, pos, rot[i]) + pm = magpy.magnet.Cuboid( + polarization=mag[i], dimension=dim, position=pos, orientation=rot[i] + ) B2 += [pm.getB(pos_obs)] B2 = np.array(B2) print(B1 - B2) @@ -91,12 +90,12 @@ def test_getH_dict3(): mag = [[111, 222, 333], [22, 2, 2], [22, -33, -44]] dim = 3 - dic = {"magnetization": mag, "diameter": dim} + dic = {"polarization": mag, "diameter": dim} B1 = magpy.getH("Sphere", pos_obs, **dic) B2 = [] for i in range(3): - pm = magpy.magnet.Sphere(mag[i], dim) + pm = magpy.magnet.Sphere(polarization=mag[i], diameter=dim) B2 += [magpy.getH([pm], pos_obs)] B2 = np.array(B2) @@ -114,12 +113,14 @@ def test_getB_dict4(): pos = np.array([0, 0, 0]) rot = R.from_quat([(t, 0.2, 0.3, 0.4) for t in np.linspace(0, 0.1, n)]) - dic = {"magnetization": mag, "diameter": dim, "position": pos, "orientation": rot} + dic = {"polarization": mag, "diameter": dim, "position": pos, "orientation": rot} B1 = magpy.getB("Sphere", pos_obs, **dic) B2 = [] for i in range(n): - pm = magpy.magnet.Sphere(mag[i], dim, pos, rot[i]) + pm = magpy.magnet.Sphere( + polarization=mag[i], diameter=dim, position=pos, orientation=rot[i] + ) B2 += [pm.getB(pos_obs)] B2 = np.array(B2) print(B1 - B2) @@ -140,7 +141,7 @@ def test_getBH_dipole(): def test_getBH_circle(): """test if Circle implementation gives correct output""" B = magpy.getB("Circle", (0, 0, 0), current=1, diameter=2) - Btest = np.array([0, 0, 0.6283185307179586]) + Btest = np.array([0, 0, 0.6283185307179586 * 1e-6]) assert np.allclose(B, Btest) H = magpy.getH("Circle", (0, 0, 0), current=1, diameter=2) @@ -185,7 +186,7 @@ def test_getBH_polyline(): def test_getBH_polyline2(): """test line with pos and rot arguments""" - x = 0.14142136 + x = 0.14142136 * 1e-6 # z-line on x=1 def getB_line(name): @@ -260,7 +261,7 @@ def getB_line(name): vertices=np.linspace((0, 5, 5), (5, 5, 5), 6), ) np.testing.assert_allclose( - B, np.array([[0.0, 0.0057735, -0.0057735]] * 5), rtol=1e-6 + B, np.array([[0.0, 0.0057735, -0.0057735]] * 5) * 1e-6, rtol=1e-6 ) # ragged sequence of vertices @@ -338,7 +339,7 @@ def test_getBH_Cylinder_FEM(): "CylinderSegment", obsp, dimension=(1, 2, 1, 90, 360), - magnetization=np.array((1, 2, 3)) * 1000 / np.sqrt(14), + polarization=np.array((1, 2, 3)) * 1000 / np.sqrt(14), position=(0, 0, 0.5), ) @@ -353,7 +354,7 @@ def test_getBH_solid_cylinder(): "CylinderSegment", (1, 2, 3), dimension=[(0, 1, 2, 20, 120), (0, 1, 2, 120, 220), (0, 1, 2, 220, 380)], - magnetization=(22, 33, 44), + polarization=(22, 33, 44), ) B1 = np.sum(B1, axis=0) @@ -362,7 +363,7 @@ def test_getBH_solid_cylinder(): "CylinderSegment", (1, 2, 3), dimension=(0, 1, 2, 0, 360), - magnetization=(22, 33, 44), + polarization=(22, 33, 44), ) # compute with solid cylinder code @@ -370,7 +371,7 @@ def test_getBH_solid_cylinder(): "Cylinder", (1, 2, 3), dimension=(2, 2), - magnetization=(22, 33, 44), + polarization=(22, 33, 44), ) assert np.allclose(B1, B2) @@ -383,7 +384,7 @@ def test_getB_dict_over_getB(): mag = [111, 222, 333] dim = [3, 3] - pm = magpy.magnet.Cylinder(mag, dim) + pm = magpy.magnet.Cylinder(polarization=mag, dimension=dim) pm.move(np.linspace((0.5, 0, 0), (7.5, 0, 0), 15)) pm.rotate_from_angax(np.linspace(0, 666, 25), "y", anchor=0) pm.move([(0, x, 0) for x in np.linspace(0, 5, 5)]) @@ -395,7 +396,7 @@ def test_getB_dict_over_getB(): dic = { "sources": "Cylinder", "observers": pos_obs, - "magnetization": mag, + "polarization": mag, "dimension": dim, "position": pos, "orientation": rot, @@ -417,8 +418,8 @@ def test_subclassing(): class MyCuboid(magpy.magnet.Cuboid): """Test subclass""" - MyCuboid((0, 0, 1000), (1, 1, 1)) - B1 = magpy.getB("Cuboid", (0, 0, 0), magnetization=(1, 1, 1), dimension=(1, 1, 1)) - B2 = magpy.getB("MyCuboid", (0, 0, 0), magnetization=(1, 1, 1), dimension=(1, 1, 1)) + MyCuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + B1 = magpy.getB("Cuboid", (0, 0, 0), polarization=(1, 1, 1), dimension=(1, 1, 1)) + B2 = magpy.getB("MyCuboid", (0, 0, 0), polarization=(1, 1, 1), dimension=(1, 1, 1)) np.testing.assert_allclose(B1, B2) diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py index dd8f488f5..89c0473e0 100644 --- a/tests/test_getBH_interfaces.py +++ b/tests/test_getBH_interfaces.py @@ -2,13 +2,14 @@ import pytest import magpylib as magpy +from magpylib._src.exceptions import MagpylibMissingInput # pylint: disable=unnecessary-lambda-assignment def test_getB_interfaces1(): """self-consistent test of different possibilities for computing the field""" - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -1, -1)] * 2] * 2 sens = magpy.Sensor(pixel=poso) @@ -16,7 +17,7 @@ def test_getB_interfaces1(): "Cuboid", (-1, -1, -1), position=src.position, - magnetization=(1, 2, 3), + polarization=(1, 2, 3), dimension=(1, 2, 3), ) B1 = np.tile(B, (2, 2, 1, 1)) @@ -37,7 +38,7 @@ def test_getB_interfaces1(): def test_getB_interfaces2(): """self-consistent test of different possibilities for computing the field""" - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -1, -1)] * 2] * 2 sens = magpy.Sensor(pixel=poso) @@ -45,7 +46,7 @@ def test_getB_interfaces2(): "Cuboid", (-1, -1, -1), position=src.position, - magnetization=(1, 2, 3), + polarization=(1, 2, 3), dimension=(1, 2, 3), ) @@ -61,7 +62,7 @@ def test_getB_interfaces2(): def test_getB_interfaces3(): """self-consistent test of different possibilities for computing the field""" - src = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -1, -1)] * 2] * 2 sens = magpy.Sensor(pixel=poso) @@ -69,7 +70,7 @@ def test_getB_interfaces3(): "Cuboid", (-1, -1, -1), position=src.position, - magnetization=(1, 2, 3), + polarization=(1, 2, 3), dimension=(1, 2, 3), ) @@ -90,7 +91,7 @@ def test_getH_interfaces1(): """self-consistent test of different possibilities for computing the field""" mag = (22, -33, 44) dim = (3, 2, 3) - src = magpy.magnet.Cuboid(mag, dim) + src = magpy.magnet.Cuboid(polarization=mag, dimension=dim) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -2, -3)] * 2] * 2 @@ -100,7 +101,7 @@ def test_getH_interfaces1(): "Cuboid", (-1, -2, -3), position=src.position, - magnetization=mag, + polarization=mag, dimension=dim, ) H1 = np.tile(H, (2, 2, 1, 1)) @@ -123,7 +124,7 @@ def test_getH_interfaces2(): """self-consistent test of different possibilities for computing the field""" mag = (22, -33, 44) dim = (3, 2, 3) - src = magpy.magnet.Cuboid(mag, dim) + src = magpy.magnet.Cuboid(polarization=mag, dimension=dim) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -2, -3)] * 2] * 2 @@ -133,7 +134,7 @@ def test_getH_interfaces2(): "Cuboid", (-1, -2, -3), position=src.position, - magnetization=mag, + polarization=mag, dimension=dim, ) @@ -151,7 +152,7 @@ def test_getH_interfaces3(): """self-consistent test of different possibilities for computing the field""" mag = (22, -33, 44) dim = (3, 2, 3) - src = magpy.magnet.Cuboid(mag, dim) + src = magpy.magnet.Cuboid(polarization=mag, dimension=dim) src.move(np.linspace((0.1, 0.2, 0.3), (1, 2, 3), 10), start=-1) poso = [[(-1, -2, -3)] * 2] * 2 @@ -161,7 +162,7 @@ def test_getH_interfaces3(): "Cuboid", (-1, -2, -3), position=src.position, - magnetization=mag, + polarization=mag, dimension=dim, ) @@ -184,12 +185,12 @@ def test_dataframe_ouptut(): num_of_pix = 2 sources = [ - magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)).move( + magpy.magnet.Cuboid(polarization=(0, 0, 1000), dimension=(1, 1, 1)).move( np.linspace((-4, 0, 0), (4, 0, 0), max_path_len), start=0 ), - magpy.magnet.Cylinder((0, 1000, 0), (1, 1), style_label="Cylinder1").move( - np.linspace((0, -4, 0), (0, 4, 0), max_path_len), start=0 - ), + magpy.magnet.Cylinder( + polarization=(0, 1000, 0), dimension=(1, 1), style_label="Cylinder1" + ).move(np.linspace((0, -4, 0), (0, 4, 0), max_path_len), start=0), ] pixel = np.linspace((0, 0, 0), (0, 3, 0), num_of_pix) sens1 = magpy.Sensor(position=(0, 0, 1), pixel=pixel, style_label="sens1") @@ -224,8 +225,8 @@ def test_dataframe_ouptut(): def test_dataframe_ouptut_sumup(): """test pandas dataframe output when sumup is True""" sources = [ - magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)), - magpy.magnet.Cylinder((0, 1000, 0), (1, 1)), + magpy.magnet.Cuboid(polarization=(0, 0, 1000), dimension=(1, 1, 1)), + magpy.magnet.Cylinder(polarization=(0, 1000, 0), dimension=(1, 1)), ] df = magpy.getB(sources, (0, 0, 0), sumup=True, output="dataframe") np.testing.assert_allclose( @@ -236,7 +237,7 @@ def test_dataframe_ouptut_sumup(): def test_dataframe_ouptut_pixel_agg(): """test pandas dataframe output when sumup is True""" - src1 = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src1 = magpy.magnet.Cuboid(polarization=(0, 0, 1000), dimension=(1, 1, 1)) sens1 = magpy.Sensor(position=(0, 0, 1), pixel=np.zeros((4, 5, 3))) sens2 = sens1.copy(position=(0, 0, 2)) sens3 = sens1.copy(position=(0, 0, 3)) @@ -254,7 +255,7 @@ def test_dataframe_ouptut_pixel_agg(): def test_getBH_bad_output_type(): """test bad output in `getBH`""" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with pytest.raises(ValueError): src.getB((0, 0, 0), output="bad_output_type") @@ -266,7 +267,9 @@ def test_sensor_handedness(): ls = lambda n: np.linspace(-k / 2, k / 2, n) pixel = np.array([[x, y, z] for x in ls(N[0]) for y in ls(N[1]) for z in ls(N[2])]) pixel = pixel.reshape((*N, 3)) - c = magpy.magnet.Cuboid((1000, 0, 0), (1, 1, 1), (0, 1, 0)) + c = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, 1, 0) + ) sr = magpy.Sensor( pixel=pixel, position=(-1, 0, 0), @@ -316,25 +319,25 @@ def test_getB_on_missing_dimensions(module, class_, missing_arg): @pytest.mark.parametrize( ("module", "class_", "missing_arg", "kwargs"), [ - ("magnet", "Cuboid", "magnetization", {"dimension": (1, 1, 1)}), - ("magnet", "Cylinder", "magnetization", {"dimension": (1, 1)}), + ("magnet", "Cuboid", "polarization", {"dimension": (1, 1, 1)}), + ("magnet", "Cylinder", "polarization", {"dimension": (1, 1)}), ( "magnet", "CylinderSegment", - "magnetization", + "polarization", {"dimension": (0, 1, 1, 45, 120)}, ), - ("magnet", "Sphere", "magnetization", {"diameter": 1}), + ("magnet", "Sphere", "polarization", {"diameter": 1}), ( "magnet", "Tetrahedron", - "magnetization", + "polarization", {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]}, ), ( "magnet", "TriangularMesh", - "magnetization", + "polarization", { "vertices": ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), "faces": ((0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)), @@ -345,7 +348,7 @@ def test_getB_on_missing_dimensions(module, class_, missing_arg): ( "misc", "Triangle", - "magnetization", + "polarization", {"vertices": [(0, 0, 0), (1, 0, 0), (0, 1, 0)]}, ), ("misc", "Dipole", "moment", {}), diff --git a/tests/test_getBH_level2.py b/tests/test_getBH_level2.py index fefe63154..43dad817f 100644 --- a/tests/test_getBH_level2.py +++ b/tests/test_getBH_level2.py @@ -12,10 +12,10 @@ def test_getB_level2_input_simple(): mag = (1, 2, 3) dim_cuboid = (1, 2, 3) dim_cyl = (1, 2) - pm1 = magpy.magnet.Cuboid(mag, dim_cuboid) - pm2 = magpy.magnet.Cuboid(mag, dim_cuboid) - pm3 = magpy.magnet.Cylinder(mag, dim_cyl) - pm4 = magpy.magnet.Cylinder(mag, dim_cyl) + pm1 = magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) + pm2 = magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) + pm3 = magpy.magnet.Cylinder(polarization=mag, dimension=dim_cyl) + pm4 = magpy.magnet.Cylinder(polarization=mag, dimension=dim_cyl) col1 = magpy.Collection(pm1.copy()) col2 = magpy.Collection(pm1.copy(), pm2.copy()) col3 = magpy.Collection(pm1.copy(), pm2.copy(), pm3.copy()) @@ -66,16 +66,16 @@ def test_getB_level2_input_shape22(): dim_cyl = (1, 2) def pm1(): - return magpy.magnet.Cuboid(mag, dim_cuboid) + return magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) def pm2(): - return magpy.magnet.Cuboid(mag, dim_cuboid) + return magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) def pm3(): - return magpy.magnet.Cylinder(mag, dim_cyl) + return magpy.magnet.Cylinder(polarization=mag, dimension=dim_cyl) def pm4(): - return magpy.magnet.Cylinder(mag, dim_cyl) + return magpy.magnet.Cylinder(polarization=mag, dimension=dim_cyl) col1 = magpy.Collection(pm1()) col2 = magpy.Collection(pm1(), pm2()) @@ -125,8 +125,8 @@ def test_getB_level2_input_path(): """ mag = (1, 2, 3) dim_cuboid = (1, 2, 3) - pm1 = magpy.magnet.Cuboid(mag, dim_cuboid) - pm2 = magpy.magnet.Cuboid(mag, dim_cuboid) + pm1 = magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) + pm2 = magpy.magnet.Cuboid(polarization=mag, dimension=dim_cuboid) sens1 = magpy.Sensor() sens2 = magpy.Sensor(pixel=[(0, 0, 0), (0, 0, 1), (0, 0, 2)]) @@ -168,8 +168,8 @@ def test_path_tile(): """Test if auto-tiled paths of objects will properly be reset in getB_level2 before returning """ - pm1 = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) poz = np.linspace((10 / 33, 10 / 33, 10 / 33), (10, 10, 10), 33) pm2.move(poz) @@ -193,7 +193,7 @@ def test_path_tile(): def test_sensor_rotation1(): """Test simple sensor rotation using sin/cos""" - src = magpy.magnet.Cuboid((1000, 0, 0), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(1, 1, 1)) sens = magpy.Sensor(position=(1, 0, 0)) sens.rotate_from_angax(np.linspace(0, 360, 56)[1:], "z", start=1, anchor=None) B = src.getB(sens) @@ -211,8 +211,12 @@ def test_sensor_rotation1(): def test_sensor_rotation2(): """test sensor rotations with different combinations of inputs mag/col + sens/pos""" - src = magpy.magnet.Cuboid((1000, 0, 0), (1, 1, 1), (0, 0, 2)) - src2 = magpy.magnet.Cuboid((1000, 0, 0), (1, 1, 1), (0, 0, 2)) + src = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, 0, 2) + ) + src2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, 0, 2) + ) col = magpy.Collection(src, src2) poss = (0, 0, 0) @@ -222,25 +226,25 @@ def test_sensor_rotation2(): sens2 = magpy.Sensor(pixel=poss) sens2.rotate_from_angax(-45, "z") - x1 = np.array([-9.82, 0, 0]) - x2 = np.array([-6.94, 6.94, 0]) - x3 = np.array([0, 9.82, 0]) - x1b = np.array([-19.64, 0, 0]) - x2b = np.array([-13.89, 13.89, 0]) - x3b = np.array([0, 19.64, 0]) + x1 = np.array([-9.82, 0, 0]) * 1e-3 + x2 = np.array([-6.94, 6.94, 0]) * 1e-3 + x3 = np.array([0, 9.82, 0]) * 1e-3 + x1b = np.array([-19.64, 0, 0]) * 1e-3 + x2b = np.array([-13.89, 13.89, 0]) * 1e-3 + x3b = np.array([0, 19.64, 0]) * 1e-3 B = magpy.getB(src, poss, squeeze=True) Btest = x1 - assert np.allclose(np.around(B, decimals=2), Btest), "FAIL: mag + pos" + assert np.allclose(np.around(B, decimals=5), Btest), "FAIL: mag + pos" B = magpy.getB([src], [sens], squeeze=True) Btest = np.array([x1, x2, x3]) - assert np.allclose(np.around(B, decimals=2), Btest), "FAIL: mag + sens_rot_path" + assert np.allclose(np.around(B, decimals=5), Btest), "FAIL: mag + sens_rot_path" B = magpy.getB([src], [sens, poss], squeeze=True) Btest = np.array([[x1, x1], [x2, x1], [x3, x1]]) assert np.allclose( - np.around(B, decimals=2), Btest + np.around(B, decimals=5), Btest ), "FAIL: mag + sens_rot_path, pos" B = magpy.getB([src, col], [sens, poss], squeeze=True) @@ -248,14 +252,14 @@ def test_sensor_rotation2(): [[[x1, x1], [x2, x1], [x3, x1]], [[x1b, x1b], [x2b, x1b], [x3b, x1b]]] ) assert np.allclose( - np.around(B, decimals=2), Btest + np.around(B, decimals=5), Btest ), "FAIL: mag,col + sens_rot_path, pos" def test_sensor_rotation3(): """testing rotated static sensor path""" # case static sensor rot - src = magpy.magnet.Cuboid((1000, 0, 0), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(1, 1, 1)) sens = magpy.Sensor() sens.rotate_from_angax(45, "z") B0 = magpy.getB(src, sens) @@ -274,15 +278,15 @@ def test_object_tiling(): src1.rotate_from_angax(np.linspace(1, 31, 31), "x", anchor=(0, 1, 0), start=-1) src2 = magpy.magnet.Cuboid( - magnetization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) + polarization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) ) src2.move([(1, 1, 1)] * 21, start=-1) src3 = magpy.magnet.Cuboid( - magnetization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) + polarization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) ) src4 = magpy.magnet.Cuboid( - magnetization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) + polarization=(1, 1, 1), dimension=(1, 1, 1), position=(1, 1, 1) ) col = magpy.Collection(src3, src4) @@ -327,12 +331,12 @@ def test_superposition_vs_tiling(): loop.rotate_from_angax([45, 90], "x") sphere1 = magpy.magnet.Sphere( - magnetization=(0, 0, 1), diameter=1, position=(20, 10, 1) + polarization=(0, 0, 1), diameter=1, position=(20, 10, 1) ) sphere1.rotate_from_angax([45, 90], "y") sphere2 = magpy.magnet.Sphere( - magnetization=(1, 0, 0), diameter=2, position=(10, 20, 1) + polarization=(1, 0, 0), diameter=2, position=(10, 20, 1) ) sphere2.rotate_from_angax([45, 90], "y") @@ -354,7 +358,7 @@ def test_squeeze_sumup(): """make sure that sumup does not lead to false output shape""" s = magpy.Sensor(pixel=(1, 2, 3)) - ss = magpy.magnet.Sphere((1, 2, 3), 1) + ss = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=1) B1 = magpy.getB(ss, s, squeeze=False) B2 = magpy.getB(ss, s, squeeze=False, sumup=True) @@ -364,7 +368,9 @@ def test_squeeze_sumup(): def test_pixel_agg(): """test pixel aggregator""" - src1 = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)).move([[1, 0, 0]]) + src1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)).move( + [[1, 0, 0]] + ) sens1 = magpy.Sensor( position=(0, 0, 1), pixel=np.zeros((4, 5, 3)), style_label="sens1 pixel(4,5)" ) @@ -387,8 +393,8 @@ def test_pixel_agg(): def test_pixel_agg_heterogeneous_pixel_shapes(): """test pixel aggregator with heterogeneous pixel shapes""" - src1 = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) - src2 = magpy.magnet.Sphere((0, 0, 1000), 1, position=(2, 0, 0)) + src1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + src2 = magpy.magnet.Sphere(polarization=(0, 0, 1), diameter=1, position=(2, 0, 0)) sens1 = magpy.Sensor( position=(0, 0, 1), pixel=[0, 0, 0], style_label="sens1, pixel.shape = (3,)" ) @@ -461,10 +467,10 @@ def test_pixel_agg3(): e2 = [(1, 1, 1)] * 2 e3 = [(1, 1, 1)] * 3 - s0 = magpy.magnet.Cuboid(e0, e0) + s0 = magpy.magnet.Cuboid(polarization=e0, dimension=e0) c0 = magpy.Collection(s0) - s1 = magpy.magnet.Cuboid(e0, e0) - s2 = magpy.magnet.Cuboid(-e0, e0) + s1 = magpy.magnet.Cuboid(polarization=e0, dimension=e0) + s2 = magpy.magnet.Cuboid(polarization=-e0, dimension=e0) c1 = magpy.Collection(c0, s1, s2) x0 = magpy.Sensor(pixel=e0) From 3ca473ea123dab31d66343a14043a2a7800ebf24 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:06:36 +0100 Subject: [PATCH 088/240] fix input checks tests --- magpylib/_src/input_checks.py | 2 +- tests/test_input_checks.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index f108ac559..434e48ace 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -595,7 +595,7 @@ def check_dimensions(sources): def check_excitations(sources): """check if all sources have excitation initialized""" for src in sources: - for arg in ("magnetization", "current", "moment"): + for arg in ("polarization", "current", "moment"): if hasattr(src, arg): if getattr(src, arg) is None: raise MagpylibMissingInput( diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index ccad65d9c..9657f9d38 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -135,7 +135,9 @@ def test_input_objects_orientation_bad(orientation_rotvec): """bad input: magpy.Sensor(orientation=orientation_rotvec)""" with pytest.raises(MagpylibBadUserInput): - magpy.Sensor((0, 0, 0), (0, 0, 0), orientation=orientation_rotvec) + magpy.Sensor( + position=(0, 0, 0), pixel=(0, 0, 0), orientation=orientation_rotvec + ) @pytest.mark.parametrize( @@ -154,7 +156,7 @@ def test_input_objects_orientation_bad(orientation_rotvec): def test_input_objects_current_good(current): """good input: magpy.current.Circle(current)""" - src = magpy.current.Circle(current) + src = magpy.current.Circle(current=current) if current is None: assert src.current is None else: @@ -261,7 +263,7 @@ def test_input_objects_vertices_bad(vertices): @pytest.mark.parametrize( - "moment", + "pol_or_mom", [ None, (1, 2, 3), @@ -270,21 +272,21 @@ def test_input_objects_vertices_bad(vertices): np.array((1, 2, 3)), ], ) -def test_input_objects_magnetization_moment_good(moment): +def test_input_objects_magnetization_moment_good(pol_or_mom): """ good input: magpy.magnet.Cuboid(magnetization=moment), magpy.misc.Dipole(moment=moment) """ - src = magpy.magnet.Cuboid(moment) - src2 = magpy.misc.Dipole(moment) - if moment is None: - assert src.magnetization is None + src = magpy.magnet.Cuboid(polarization=pol_or_mom) + src2 = magpy.misc.Dipole(moment=pol_or_mom) + if pol_or_mom is None: + assert src.polarization is None assert src2.moment is None else: - np.testing.assert_allclose(src.magnetization, moment) - np.testing.assert_allclose(src2.moment, moment) + np.testing.assert_allclose(src.polarization, pol_or_mom) + np.testing.assert_allclose(src2.moment, pol_or_mom) @pytest.mark.parametrize( @@ -930,7 +932,7 @@ def test_input_getBH_field_good(field): """good getBH field inputs""" moms = np.array([[1, 2, 3]]) obs = np.array([[1, 2, 3]]) - B = magpy.core.dipole_field(field, obs, moms) + B = magpy.core.dipole_field(field=field, observers=obs, moment=moms) assert isinstance(B, np.ndarray) @@ -954,4 +956,4 @@ def test_input_getBH_field_bad(field): moms = np.array([[1, 2, 3]]) obs = np.array([[1, 2, 3]]) with pytest.raises(MagpylibBadUserInput): - magpy.core.dipole_field(field, obs, moms) + magpy.core.dipole_field(field=field, observers=obs, moment=moms) From 526159c1550d98190819977ceec569071f3925af Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:08:08 +0100 Subject: [PATCH 089/240] fix numerical stability --- tests/test_numerical_stability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_numerical_stability.py b/tests/test_numerical_stability.py index a182d2bfd..7ef03fb25 100644 --- a/tests/test_numerical_stability.py +++ b/tests/test_numerical_stability.py @@ -11,7 +11,7 @@ def test_loop_field(): the field become instable. This is a result of small displacements from the axis where the field is evaluated due to floating-point errors. see paper Leitner2021. """ - lop = magpy.current.Circle(1000, 1) + lop = magpy.current.Circle(current=1000, diameter=1) anch = (0, 0, 1) B = [] From be822047ec3a20d40a4aed92634812a63af8e606 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:41:34 +0100 Subject: [PATCH 090/240] fix base geo tests --- .../_src/obj_classes/class_BaseDisplayRepr.py | 6 ++-- tests/test_obj_BaseGeo.py | 35 ++++++++++--------- tests/test_obj_BaseGeo_v4motion.py | 22 ++++++++---- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index 602a9c6aa..6402f4259 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -8,10 +8,10 @@ UNITS = { "parent": None, - "position": "arbitrary", + "position": "m", "orientation": "degrees", - "dimension": "arbitrary", - "diameter": "arbitrary", + "dimension": "m", + "diameter": "m", "current": "A", "magnetization": "A/m", "polarization": "T", diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 49c069198..b3c8da4d2 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -280,7 +280,7 @@ def test_scipy_from_methods(): """test all rotation methods inspired from scipy implemented in BaseTransform""" def cube(): - return magpy.magnet.Cuboid((11, 22, 33), (1, 1, 1)) + return magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 1, 1)) angs_deg = np.linspace(0, 360, 10) angs = np.deg2rad(angs_deg) @@ -415,7 +415,7 @@ def test_describe(): # pylint: disable=protected-access x1 = magpy.magnet.Cuboid(style_label="x1") x2 = magpy.magnet.Cylinder( - style_label="x2", dimension=(1, 3), magnetization=(2, 3, 4) + style_label="x2", dimension=(1, 3), polarization=(2, 3, 4) ) s1 = magpy.Sensor(position=[(1, 2, 3)] * 3, pixel=[(1, 2, 3)] * 15) @@ -424,8 +424,8 @@ def test_describe(): test = ( "
Cuboid(id=REGEX, label='x1')
• parent: None
• " - "position: [0. 0. 0.] mm
• orientation: [0. 0. 0.] degrees
• " - "dimension: None mm
• magnetization: None mT
" + "position: [0. 0. 0.] m
• orientation: [0. 0. 0.] degrees
• " + "dimension: None m
• magnetization: None A/m
• polarization: None T" ) rep = x1._repr_html_() rep = re.sub("id=[0-9]*[0-9]", "id=REGEX", rep) @@ -435,10 +435,11 @@ def test_describe(): test = [ "Cuboid(id=REGEX, label='x1')", " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE - " • position: [0. 0. 0.] mm", + " • position: [0. 0. 0.] m", " • orientation: [0. 0. 0.] degrees", - " • dimension: None mm", - " • magnetization: None mT", + " • dimension: None m", + " • magnetization: None A/m", + " • polarization: None T", ] desc = x1.describe(return_string=True) desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) @@ -447,10 +448,11 @@ def test_describe(): test = [ "Cylinder(id=REGEX, label='x2')", " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE - " • position: [0. 0. 0.] mm", + " • position: [0. 0. 0.] m", " • orientation: [0. 0. 0.] degrees", - " • dimension: [1. 3.] mm", - " • magnetization: [2. 3. 4.] mT", + " • dimension: [1. 3.] m", + " • magnetization: [1591549.43091895 2387324.14637843 3183098.86183791] A/m", + " • polarization: [2. 3. 4.] T", ] desc = x2.describe(return_string=True) desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) @@ -460,7 +462,7 @@ def test_describe(): "Sensor(id=REGEX)", " • parent: None ", # INVISIBLE SPACE " • path length: 3", - " • position (last): [1. 2. 3.] mm", + " • position (last): [1. 2. 3.] m", " • orientation (last): [0. 0. 0.] degrees", " • handedness: right ", " • pixel: 15 ", # INVISIBLE SPACE @@ -475,7 +477,7 @@ def test_describe(): test = ( "Sensor(id=REGEX)\n" + " • parent: None \n" - + " • position: [0. 0. 0.] mm\n" + + " • position: [0. 0. 0.] m\n" + " • orientation: [0. 0. 0.] degrees\n" + " • handedness: right \n" + " • pixel: 1 \n" @@ -497,7 +499,7 @@ def test_describe(): test = ( "Sensor(id=REGEX)\n" + " • parent: None \n" - + " • position: [0. 0. 0.] mm\n" + + " • position: [0. 0. 0.] m\n" + " • orientation: [0. 0. 0.] degrees\n" + " • handedness: left \n" + " • pixel: 75 (3x5x5) " @@ -514,7 +516,7 @@ def test_describe(): (0, 0, 2), ] s = magpy.magnet.TriangularMesh.from_ConvexHull( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), points=points, check_selfintersecting="skip", ) @@ -522,9 +524,10 @@ def test_describe(): test = ( "TriangularMesh(id=REGEX)\n" " • parent: None \n" - " • position: [0. 0. 0.] mm\n" + " • position: [0. 0. 0.] m\n" " • orientation: [0. 0. 0.] degrees\n" - " • magnetization: [ 0. 0. 1000.] mT\n" + " • magnetization: [ 0. 0. 795774.71545948] A/m\n" + " • polarization: [0. 0. 1.] T\n" " • barycenter: [0. 0. 0.46065534] \n" " • faces: shape(6, 3) \n" " • mesh: shape(6, 3, 3) \n" diff --git a/tests/test_obj_BaseGeo_v4motion.py b/tests/test_obj_BaseGeo_v4motion.py index 46480ca33..6a5939e03 100644 --- a/tests/test_obj_BaseGeo_v4motion.py +++ b/tests/test_obj_BaseGeo_v4motion.py @@ -90,8 +90,8 @@ def test_BaseGeo_init( if init_orientation_rotvec is None: init_orientation_rotvec = (0, 0, 0) src = magpy.magnet.Cuboid( - (1, 0, 0), - (1, 1, 1), + polarization=(1, 0, 0), + dimension=(1, 1, 1), position=init_position, orientation=R.from_rotvec(init_orientation_rotvec), ) @@ -168,7 +168,12 @@ def test_BaseGeo_setting_position( test_ori, ): """test position and orientation initialization""" - src = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), init_pos, R.from_rotvec(init_ori)) + src = magpy.magnet.Cuboid( + polarization=(1, 0, 0), + dimension=(1, 1, 1), + position=init_pos, + orientation=R.from_rotvec(init_ori), + ) src.position = test_pos validate_pos_orient(src, test_pos, test_ori) @@ -185,7 +190,12 @@ def test_BaseGeo_setting_orientation( test_ori, ): """test position and orientation initialization""" - src = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), init_pos, R.from_rotvec(init_ori)) + src = magpy.magnet.Cuboid( + polarization=(1, 0, 0), + dimension=(1, 1, 1), + position=init_pos, + orientation=R.from_rotvec(init_ori), + ) src.orientation = R.from_rotvec(test_ori) validate_pos_orient(src, test_pos, test_ori) @@ -355,8 +365,8 @@ def test_BaseGeo_multianchor_rotation( if init_orientation_rotvec is None: init_orientation_rotvec = (0, 0, 0) src = magpy.magnet.Cuboid( - (1, 0, 0), - (1, 1, 1), + polarization=(1, 0, 0), + dimension=(1, 1, 1), position=init_position, orientation=R.from_rotvec(init_orientation_rotvec), ) From 928f108a5d38f749484a92f607f686ee2d9b0547 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:41:54 +0100 Subject: [PATCH 091/240] fix test paths --- tests/test_path.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_path.py b/tests/test_path.py index 8f7d57eac..6827076cd 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -9,12 +9,16 @@ def test_path_old_new_move(): s_pos = (0, 0, 0) # path style code translation - pm1 = magpy.magnet.Cylinder((0, 0, 1000), (3, 3), position=(-5, 0, 3)) + pm1 = magpy.magnet.Cylinder( + polarization=(0, 0, 1), dimension=(3, 3), position=(-5, 0, 3) + ) pm1.move([(x, 0, 0) for x in np.linspace(0, 10, 100)], start=-1) B1 = pm1.getB(s_pos) # old style code translation - pm2 = magpy.magnet.Cylinder((0, 0, 1000), (3, 3), position=(0, 0, 3)) + pm2 = magpy.magnet.Cylinder( + polarization=(0, 0, 1), dimension=(3, 3), position=(0, 0, 3) + ) ts = np.linspace(-5, 5, n) possis = np.array([(t, 0, 0) for t in ts]) B2 = pm2.getB(possis[::-1]) @@ -33,13 +37,17 @@ def test_path_old_new_rotate(): anch = (0, 0, 10) # path style code rotation - pm1 = magpy.magnet.Cuboid((0, 0, 1000), (1, 2, 3), position=(0, 0, 3)) + pm1 = magpy.magnet.Cuboid( + polarization=(0, 0, 1), dimension=(1, 2, 3), position=(0, 0, 3) + ) pm1.rotate_from_angax(-30, ax, anch) pm1.rotate_from_angax(np.linspace(0, 60, n), "x", anch, start=-1) B1 = pm1.getB(s_pos) # old style code rotation - pm2 = magpy.magnet.Cuboid((0, 0, 1000), (1, 2, 3), position=(0, 0, 3)) + pm2 = magpy.magnet.Cuboid( + polarization=(0, 0, 1), dimension=(1, 2, 3), position=(0, 0, 3) + ) pm2.rotate_from_angax(-30, ax, anch) B2 = [] for _ in range(n): From c402ee0ed18e924ccb2131e27af2480ef272c4d2 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:44:51 +0100 Subject: [PATCH 092/240] fix utility tests --- tests/test_utility.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_utility.py b/tests/test_utility.py index 365698b58..7f1555341 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -9,8 +9,8 @@ def test_duplicates(): """test duplicate elimination and sorting""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) src_list = [pm1, pm2, pm1] src_list_new = check_duplicates(src_list) assert src_list_new == [pm1, pm2], "duplicate elimination failed" @@ -18,8 +18,8 @@ def test_duplicates(): def test_filter_objects(): """tests elimination of unwanted types""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) sens = magpy.Sensor() src_list = [pm1, pm2, sens] list_new = filter_objects(src_list, allow="sources") @@ -30,8 +30,8 @@ def test_format_getBH_class_inputs(): """special case testing of different input formats""" possis = [3, 3, 3] sens = magpy.Sensor(position=(3, 3, 3)) - pm1 = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) col = pm1 + pm2 B1 = pm1.getB(possis) From 159042d1c8f098c20669c4e094f78ad2f75ba878 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:47:28 +0100 Subject: [PATCH 093/240] fix test vs mag2 --- tests/test_vs_mag2.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_vs_mag2.py b/tests/test_vs_mag2.py index 5b340aa1b..c5068edad 100644 --- a/tests/test_vs_mag2.py +++ b/tests/test_vs_mag2.py @@ -10,16 +10,16 @@ # import magpylib as magpy # # linear motionfrom (0,0,0) to (3,-3,3) in 100 steps -# pm = magpy.source.magnet.Cuboid(magnetization=(111,222,333), dimension=(1,2,3)) +# pm = magpy.source.magnet.Cuboid(polarization=(111,222,333), dimension=(1,2,3)) # B1 = np.array([pm.getB((i,-i,i)) for i in np.linspace(0,3,100)]) # # rotation (pos_obs around magnet) from 0 to 444 deg, starting pos_obs at (0,3,0) about 'z' -# pm = magpy.source.magnet.Cuboid(magnetization=(111,222,333), dimension=(1,2,3)) +# pm = magpy.source.magnet.Cuboid(polarization=(111,222,333), dimension=(1,2,3)) # possis = [(3*np.sin(t/180*np.pi),3*np.cos(t/180*np.pi),0) for t in np.linspace(0,444,100)] # B2 = np.array([pm.getB(p) for p in possis]) # # spiral (magnet around pos_obs=0) from 0 to 297 deg, about 'z' in 100 steps -# pm = magpy.source.magnet.Cuboid(magnetization=(111,222,333), dimension=(1,2,3), pos=(3,0,0)) +# pm = magpy.source.magnet.Cuboid(polarization=(111,222,333), dimension=(1,2,3), pos=(3,0,0)) # B = [] # for i in range(100): # B += [pm.getB((0,0,0))] @@ -37,7 +37,7 @@ def test_vs_mag2_linear(): with open(os.path.abspath("tests/testdata/testdata_vs_mag2.p"), "rb") as f: data = pickle.load(f)[0] poso = [(t, -t, t) for t in np.linspace(0, 3, 100)] - pm = magpy.magnet.Cuboid(magnetization=(111, 222, 333), dimension=(1, 2, 3)) + pm = magpy.magnet.Cuboid(polarization=(111, 222, 333), dimension=(1, 2, 3)) B = magpy.getB(pm, poso) assert np.allclose(B, data), "vs mag2 - linear" @@ -47,7 +47,7 @@ def test_vs_mag2_rotation(): """test against magpylib v2""" with open(os.path.abspath("tests/testdata/testdata_vs_mag2.p"), "rb") as f: data = pickle.load(f)[1] - pm = magpy.magnet.Cuboid(magnetization=(111, 222, 333), dimension=(1, 2, 3)) + pm = magpy.magnet.Cuboid(polarization=(111, 222, 333), dimension=(1, 2, 3)) possis = [ (3 * np.sin(t / 180 * np.pi), 3 * np.cos(t / 180 * np.pi), 0) for t in np.linspace(0, 444, 100) @@ -61,7 +61,7 @@ def test_vs_mag2_spiral(): with open(os.path.abspath("tests/testdata/testdata_vs_mag2.p"), "rb") as f: data = pickle.load(f)[2] pm = magpy.magnet.Cuboid( - magnetization=(111, 222, 333), dimension=(1, 2, 3), position=(3, 0, 0) + polarization=(111, 222, 333), dimension=(1, 2, 3), position=(3, 0, 0) ) angs = np.linspace(0, 297, 100) @@ -74,7 +74,7 @@ def test_vs_mag2_spiral(): def test_vs_mag2_line(): """test line current vs mag2 results""" - Btest = np.array([1.47881931, -1.99789688, 0.2093811]) + Btest = np.array([1.47881931, -1.99789688, 0.2093811]) * 1e-6 src = magpy.current.Polyline( current=10, From 6fc0fcf7bb0a45c4d316805675ae2c1838c6fcdc Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 01:48:57 +0100 Subject: [PATCH 094/240] fix scaling test --- tests/test_scaling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_scaling.py b/tests/test_scaling.py index b6773d388..afcc1e783 100644 --- a/tests/test_scaling.py +++ b/tests/test_scaling.py @@ -8,13 +8,13 @@ def test_scaling_loop(): The field of a current loop must satisfy B(i0,d,x,y,z) = B(a*i0,a*d,a*x,a*y,a*z) """ - c1 = magpy.current.Circle(123, 10) + c1 = magpy.current.Circle(current=123, diameter=10) B1 = c1.getB([1, 2, 3]) - c2 = magpy.current.Circle(1230, 100) + c2 = magpy.current.Circle(current=1230, diameter=100) B2 = c2.getB([10, 20, 30]) - c3 = magpy.current.Circle(12300, 1000) + c3 = magpy.current.Circle(current=12300, diameter=1000) B3 = c3.getB([100, 200, 300]) - c4 = magpy.current.Circle(123000, 10000) + c4 = magpy.current.Circle(current=123000, diameter=10000) B4 = c4.getB([1000, 2000, 3000]) assert np.allclose(B1, B2) From ac4f7168bddd72145b1f9e136eece9f5d450048e Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:00:00 +0100 Subject: [PATCH 095/240] fix physics consistency tests --- tests/test_physics_consistency.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_physics_consistency.py b/tests/test_physics_consistency.py index c9f831850..939b5be21 100644 --- a/tests/test_physics_consistency.py +++ b/tests/test_physics_consistency.py @@ -5,27 +5,27 @@ def test_dipole_approximation(): """test if all source fields converge towards the correct dipole field at distance""" - mag = np.array([111, 222, 333]) + pol = np.array([0.111, 0.222, 0.333]) pos = (1234, -234, 345) - # cuboid with volume = 1 mm^3 - src1 = magpy.magnet.Cuboid(mag, dimension=(1, 1, 1)) + # cuboid with volume = 1 m^3 + src1 = magpy.magnet.Cuboid(polarization=pol, dimension=(1, 1, 1)) B1 = src1.getB(pos) - # Cylinder with volume = 1 mm^3 + # Cylinder with volume = 1 m^3 dia = np.sqrt(4 / np.pi) - src2 = magpy.magnet.Cylinder(mag, dimension=(dia, 1)) + src2 = magpy.magnet.Cylinder(polarization=pol, dimension=(dia, 1)) B2 = src2.getB(pos) assert np.allclose(B1, B2) - # Sphere with volume = 1 mm^3 + # Sphere with volume = 1 m^3 dia = (6 / np.pi) ** (1 / 3) - src3 = magpy.magnet.Sphere(mag, dia) + src3 = magpy.magnet.Sphere(polarization=pol, diameter=dia) B3 = src3.getB(pos) assert np.allclose(B1, B3) - # Dipole with mom=mag - src4 = magpy.misc.Dipole(moment=mag) + # Dipole with mom=pol + src4 = magpy.misc.Dipole(moment=pol) B4 = src4.getB(pos) assert np.allclose(B1, B4) @@ -169,7 +169,7 @@ def test_Circle_vs_Cylinder_field(): h0 = 1e-4 i0 = 1 src1 = magpy.magnet.Cylinder( - magnetization=(0, 0, i0 / h0 * 4 * np.pi / 10), dimension=(r0, h0) + polarization=(0, 0, i0 / h0 * 4 * np.pi / 10 * 1e-6), dimension=(r0, h0) ) src2 = magpy.current.Circle(current=i0, diameter=r0) @@ -227,6 +227,6 @@ def Binf(i0, pos): Bls += [magpy.getB("Polyline", p, current=1, segment_start=ps, segment_end=pe)] Binfs += [Binf(1, p)] Bls = np.array(Bls) - Binfs = np.array(Binfs) + Binfs = np.array(Binfs) * 1e-6 assert np.allclose(Bls, Binfs) From a7583c1f1587d290265e577f84126199c6ca1fea Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:26:46 +0100 Subject: [PATCH 096/240] fix Collection tests (up to Dipole fail) --- tests/test_obj_Collection.py | 76 +++++----- tests/test_obj_Collection_child_parent.py | 8 +- tests/test_obj_Collection_v4motion.py | 165 ++++++++++++++++------ 3 files changed, 166 insertions(+), 83 deletions(-) diff --git a/tests/test_obj_Collection.py b/tests/test_obj_Collection.py index 996456972..d6829470b 100644 --- a/tests/test_obj_Collection.py +++ b/tests/test_obj_Collection.py @@ -61,19 +61,19 @@ def test_Collection_basics(): ): rot = R.from_rotvec(rv) - pm1b = magpy.magnet.Cuboid(mag[0], dim3[0]) - pm2b = magpy.magnet.Cuboid(mag[1], dim3[1]) - pm3b = magpy.magnet.Cuboid(mag[2], dim3[2]) - pm4b = magpy.magnet.Cylinder(mag[3], dim2[0]) - pm5b = magpy.magnet.Cylinder(mag[4], dim2[1]) - pm6b = magpy.magnet.Cylinder(mag[5], dim2[2]) - - pm1 = magpy.magnet.Cuboid(mag[0], dim3[0]) - pm2 = magpy.magnet.Cuboid(mag[1], dim3[1]) - pm3 = magpy.magnet.Cuboid(mag[2], dim3[2]) - pm4 = magpy.magnet.Cylinder(mag[3], dim2[0]) - pm5 = magpy.magnet.Cylinder(mag[4], dim2[1]) - pm6 = magpy.magnet.Cylinder(mag[5], dim2[2]) + pm1b = magpy.magnet.Cuboid(polarization=mag[0], dimension=dim3[0]) + pm2b = magpy.magnet.Cuboid(polarization=mag[1], dimension=dim3[1]) + pm3b = magpy.magnet.Cuboid(polarization=mag[2], dimension=dim3[2]) + pm4b = magpy.magnet.Cylinder(polarization=mag[3], dimension=dim2[0]) + pm5b = magpy.magnet.Cylinder(polarization=mag[4], dimension=dim2[1]) + pm6b = magpy.magnet.Cylinder(polarization=mag[5], dimension=dim2[2]) + + pm1 = magpy.magnet.Cuboid(polarization=mag[0], dimension=dim3[0]) + pm2 = magpy.magnet.Cuboid(polarization=mag[1], dimension=dim3[1]) + pm3 = magpy.magnet.Cuboid(polarization=mag[2], dimension=dim3[2]) + pm4 = magpy.magnet.Cylinder(polarization=mag[3], dimension=dim2[0]) + pm5 = magpy.magnet.Cylinder(polarization=mag[4], dimension=dim2[1]) + pm6 = magpy.magnet.Cylinder(polarization=mag[5], dimension=dim2[2]) col1 = magpy.Collection(pm1, pm2, pm3) col1.add(pm4, pm5, pm6) @@ -121,10 +121,10 @@ def test_Collection_basics(): def test_col_getB(test_input, expected): """testing some Collection stuff with getB""" src1 = magpy.magnet.Cuboid( - magnetization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0) + polarization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0) ) src2 = magpy.magnet.Cylinder( - magnetization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0) + polarization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0) ) sens1 = magpy.Sensor(position=(0, 0, 6)) sens2 = magpy.Sensor(position=(0, 0, 6)) @@ -195,11 +195,11 @@ def test_bad_col_getB_inputs(test_input): # pylint: disable=eval-used src1 = magpy.magnet.Cuboid( - magnetization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0) + polarization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0) ) src2 = magpy.magnet.Cylinder( - magnetization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0) + polarization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0) ) sens1 = magpy.Sensor(position=(0, 0, 6)) @@ -228,9 +228,9 @@ def test_bad_col_getB_inputs(test_input): def test_col_get_item(): """test get_item with collections""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm3 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm3 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) col = magpy.Collection(pm1, pm2, pm3) assert col[1] == pm2, "get_item failed" @@ -239,8 +239,8 @@ def test_col_get_item(): def test_col_getH(): """test collection getH""" - pm1 = magpy.magnet.Sphere((1, 2, 3), 3) - pm2 = magpy.magnet.Sphere((1, 2, 3), 3) + pm1 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=3) + pm2 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=3) col = magpy.Collection(pm1, pm2) H = col.getH((0, 0, 0)) H1 = pm1.getH((0, 0, 0)) @@ -250,8 +250,8 @@ def test_col_getH(): def test_col_reset_path(): """testing display""" # pylint: disable=no-member - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) col = magpy.Collection(pm1, pm2) col.move([(1, 2, 3)] * 10) col.reset_path() @@ -262,8 +262,8 @@ def test_col_reset_path(): def test_Collection_squeeze(): """testing squeeze output""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) col = magpy.Collection(pm1, pm2) sensor = magpy.Sensor(pixel=[(1, 2, 3), (1, 2, 3)]) B = col.getB(sensor) @@ -313,8 +313,8 @@ def test_adding_sources(): def test_set_children_styles(): """test if styles get applied""" - src1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - src2 = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + src1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + src2 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) col = src1 + src2 col.set_children_styles(magnetization_show=False) assert ( @@ -332,7 +332,7 @@ def test_reprs(): c = magpy.Collection() assert repr(c)[:10] == "Collection" - s1 = magpy.magnet.Sphere((1, 2, 3), 5) + s1 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=5) c = magpy.Collection(s1) assert repr(c)[:10] == "Collection" @@ -341,7 +341,7 @@ def test_reprs(): assert repr(c)[:10] == "Collection" x1 = magpy.Sensor() - s1 = magpy.magnet.Sphere((1, 2, 3), 5) + s1 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=5) c = magpy.Collection(s1, x1) assert repr(c)[:10] == "Collection" @@ -431,18 +431,20 @@ def test_collection_describe(): desc = cc.describe(format="label, properties", return_string=True).split("\n") test = [ "Collection", - "│ • position: [0. 0. 0.] mm", + "│ • position: [0. 0. 0.] m", "│ • orientation: [0. 0. 0.] degrees", "├── x", - "│ • position: [0. 0. 0.] mm", + "│ • position: [0. 0. 0.] m", "│ • orientation: [0. 0. 0.] degrees", - "│ • dimension: None mm", - "│ • magnetization: None mT", + "│ • dimension: None m", + "│ • magnetization: None A/m", + "│ • polarization: None T", "└── y", - " • position: [0. 0. 0.] mm", + " • position: [0. 0. 0.] m", " • orientation: [0. 0. 0.] degrees", - " • dimension: None mm", - " • magnetization: None mT", + " • dimension: None m", + " • magnetization: None A/m", + " • polarization: None T", ] assert "".join(test) == re.sub("id=*[0-9]*[0-9]", "id=REGEX", "".join(desc)) diff --git a/tests/test_obj_Collection_child_parent.py b/tests/test_obj_Collection_child_parent.py index 4fd94abed..b33ab3eda 100644 --- a/tests/test_obj_Collection_child_parent.py +++ b/tests/test_obj_Collection_child_parent.py @@ -311,10 +311,10 @@ def test_collection_remove(): def test_collection_nested_getBH(): """test if getBH functionality is self-consistent with nesting""" - s1 = magpy.current.Circle(1, 1) - s2 = magpy.current.Circle(1, 1) - s3 = magpy.current.Circle(1, 1) - s4 = magpy.current.Circle(1, 1) + s1 = magpy.current.Circle(current=1, diameter=1) + s2 = magpy.current.Circle(current=1, diameter=1) + s3 = magpy.current.Circle(current=1, diameter=1) + s4 = magpy.current.Circle(current=1, diameter=1) obs = [(1, 2, 3), (-2, -3, 1), (2, 2, -4), (4, 2, -4)] coll = s1 + s2 + s3 + s4 # nasty nesting diff --git a/tests/test_obj_Collection_v4motion.py b/tests/test_obj_Collection_v4motion.py index bd5aacdc9..5518b05f2 100644 --- a/tests/test_obj_Collection_v4motion.py +++ b/tests/test_obj_Collection_v4motion.py @@ -110,7 +110,10 @@ def test_Collection_setting_position( ): """Test position and orientation setters on Collection""" src = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), src_pos_init, R.from_rotvec(src_ori_init) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + position=src_pos_init, + orientation=R.from_rotvec(src_ori_init), ) col = magpy.Collection( src, position=col_pos_init, orientation=R.from_rotvec(col_ori_init) @@ -218,7 +221,10 @@ def test_Collection_setting_orientation( ): """test_Collection_setting_orientation""" src = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), src_pos_init, R.from_rotvec(src_ori_init) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + position=src_pos_init, + orientation=R.from_rotvec(src_ori_init), ) col = magpy.Collection( src, position=col_pos_init, orientation=R.from_rotvec(col_ori_init) @@ -240,7 +246,12 @@ def test_Collection_setter(): # ): # col = magpy.Collection() # for i,color in enumerate(['r', 'orange', 'gold', 'green', 'cyan']): - # src = magpy.magnet.Cuboid((1,0,0), (.5,.5,.5), (1,0,0), style_color=color) + # src = magpy.magnet.Cuboid( + # polarization=(1,0,0), + # dimension=(.5,.5,.5), + # position=(1,0,0), + # style_color=color + # ) # src.rotate_from_angax(72*i, 'z', (0,0,0)) # col = col + src # base = magpy.Sensor() @@ -258,7 +269,9 @@ def test_Collection_setter(): ): col = magpy.Collection() for i in range(5): - src = magpy.magnet.Cuboid((1, 0, 0), (0.5, 0.5, 0.5), (1, 0, 0)) + src = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(0.5, 0.5, 0.5), position=(1, 0, 0) + ) src.rotate_from_angax(72 * i, "z", (0, 0, 0)) col.add(src) col.position = poz @@ -282,7 +295,9 @@ def test_Collection_setter(): def test_compound_motion_00(): """init Collection should not change source pos and ori""" - src = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 2, 3), (2, 3, 4)]) + src = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 2, 3), (2, 3, 4)] + ) validate_pos_orient(src, [(1, 2, 3), (2, 3, 4)], [(0, 0, 0)] * 2) col = magpy.Collection(src, position=[(1, 1, 1)]) validate_pos_orient(src, [(1, 2, 3), (2, 3, 4)], [(0, 0, 0)] * 2) @@ -291,8 +306,12 @@ def test_compound_motion_00(): def test_compound_motion_01(): """very sensible Compound behavior with rotation anchor""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 1, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, -1, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 1, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(-1, -1, -1) + ) col = magpy.Collection(s1, s2) col.move((0, 0, 1)) validate_pos_orient(s1, (1, 1, 2), (0, 0, 0)) @@ -310,8 +329,12 @@ def test_compound_motion_01(): def test_compound_motion_02(): """very sensible Compound behavior with vector anchor""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, 0, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(-1, 0, -1) + ) col = magpy.Collection(s1, s2, position=(3, 0, 3)) col.rotate_from_rotvec( (0, 0, np.pi / 2), anchor=[(1, 0, 0), (2, 0, 0)], degrees=False @@ -335,12 +358,14 @@ def test_compound_motion_02(): def test_compound_motion_03(): """very sensible Compound behavior with vector path and anchor and start=0""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(3, 0, 0), (1, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(3, 0, 0), (1, 0, 0)] + ) s2 = magpy.magnet.Cuboid( - (1, 0, 0), - (1, 1, 1), - [(2, 0, 2), (2, 0, 2)], - R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]), + polarization=(1, 0, 0), + dimension=(1, 1, 1), + position=[(2, 0, 2), (2, 0, 2)], + orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]), ) col = magpy.Collection(s1, s2, position=[(3, 0, 2), (3, 0, 3)]) col.rotate_from_rotvec( @@ -363,9 +388,13 @@ def test_compound_motion_03(): def test_compound_motion_04(): """nonsensical but correct Collection behavior when col and children all have different path formats""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(1, 1, 1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 1, 1) + ) s2 = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]), ) col = magpy.Collection(s1, s2, position=[(1, 2, 3), (1, 3, 4)]) col.rotate_from_angax(90, "z", anchor=(1, 0, 0)) @@ -378,9 +407,13 @@ def test_compound_motion_04(): def test_compound_motion_05(): """nonsensical but correct Collection behavior with vector anchor""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(1, 0, 1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) s2 = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + orientation=R.from_rotvec([(0, 0, -0.1), (0, 0, -0.2)]), ) col = magpy.Collection(s1, s2, position=[(3, 0, 3), (4, 0, 4)]) col.rotate_from_angax(90, "z", anchor=[(1, 0, 0), (2, 0, 0)]) @@ -403,8 +436,12 @@ def test_compound_motion_05(): def test_compound_motion_06(): """Compound rotation (anchor=None), scalar input, scalar pos""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, -1, -1) + ) col = magpy.Collection(s1, s2) col.rotate_from_angax(90, "z") validate_pos_orient(s1, (0, 1, 1), (0, 0, np.pi / 2)) @@ -414,8 +451,12 @@ def test_compound_motion_06(): def test_compound_motion_07(): """Compound rotation (anchor=None), scalar input, vector pos, start=auto""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)]) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 0, 0), (2, 0, 0)] + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] + ) col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0))) col.rotate_from_angax(90, "z") validate_pos_orient( @@ -431,8 +472,12 @@ def test_compound_motion_07(): def test_compound_motion_08(): """Compound rotation (anchor=None), scalar input, vector pos, start=1""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)]) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 0, 0), (2, 0, 0)] + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] + ) col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0))) col.rotate_from_angax(90, "z", start=1) validate_pos_orient(s1, [(1, 0, 0), (1, 1, 0)], [(0, 0, 0), (0, 0, np.pi / 2)]) @@ -442,8 +487,12 @@ def test_compound_motion_08(): def test_compound_motion_09(): """Compound rotation (anchor=None), scalar input, vector pos, start=-1""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)]) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 0, 0), (2, 0, 0)] + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] + ) col = magpy.Collection(s1, s2, position=((0, 0, 0), (1, 0, 0))) col.rotate_from_angax(90, "z", start=-1) validate_pos_orient(s1, [(1, 0, 0), (1, 1, 0)], [(0, 0, 0), (0, 0, np.pi / 2)]) @@ -453,8 +502,12 @@ def test_compound_motion_09(): def test_compound_motion_10(): """Compound rotation (anchor=None), scalar input, vector pos, start->pad before""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)]) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 0, 0), (2, 0, 0)] + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] + ) col = magpy.Collection(s1, s2, position=((2, 0, 0), (1, 0, 0))) col.rotate_from_angax(90, "z", start=-4) validate_pos_orient( @@ -476,8 +529,12 @@ def test_compound_motion_10(): def test_compound_motion_11(): """Compound rotation (anchor=None), scalar input, vector pos, start->pad behind""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(1, 0, 0), (2, 0, 0)]) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), [(-1, 0, 0), (-2, 0, 0)]) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(1, 0, 0), (2, 0, 0)] + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] + ) col = magpy.Collection(s1, s2, position=((2, 0, 0), (1, 0, 0))) col.rotate_from_angax(90, "z", start=3) validate_pos_orient( @@ -499,8 +556,12 @@ def test_compound_motion_11(): def test_compound_motion_12(): """Compound rotation (anchor=None), vector input, simple pos, start=auto""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, -1, -1) + ) col = magpy.Collection(s1, s2) col.rotate_from_angax([90, -90], "z") validate_pos_orient( @@ -522,8 +583,12 @@ def test_compound_motion_12(): def test_compound_motion_13(): """Compound rotation (anchor=None), vector input, vector pos, start=1""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, -1, -1) + ) col = magpy.Collection(s1, s2) col.rotate_from_angax([90, -90], "z") col.rotate_from_angax([-90, 180], "z", start=1) @@ -544,8 +609,12 @@ def test_compound_motion_13(): def test_compound_motion_14(): """Compound rotation (anchor=None), vector input, vector pos, start=1, pad_behind""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (0, -1, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(0, -1, -1) + ) col = magpy.Collection(s1, s2) col.rotate_from_angax([90, -90], "z") col.rotate_from_angax([-90, 180], "z", start=1) @@ -569,8 +638,12 @@ def test_compound_motion_14(): def test_compound_motion_15(): """Compound rotation (anchor=None), vector input, simple pos, start=-3, pad_before""" - s1 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (1, 0, 1)) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), (-1, 0, -1)) + s1 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(1, 0, 1) + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(-1, 0, -1) + ) col = magpy.Collection(s1, s2, position=(2, 0, 0)) col.rotate_from_angax([90, -90], "z", start=-3) validate_pos_orient( @@ -594,9 +667,13 @@ def test_compound_motion_16(): """Compound rotation (anchor=None), vector input, vector pos, start=-3, pad_before AND pad_behind""" s1 = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)]) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)]), + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)] ) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=[(-1, 0, 0), (-2, 0, 0)]) col = magpy.Collection(s1, s2, position=[(1, 0, 0), (0, 0, 0)]) col.rotate_from_angax([90, -90, 90, -90], "z", start=-3) validate_pos_orient( @@ -624,9 +701,13 @@ def test_compound_motion_16(): def test_compound_motion_17(): """CRAZY Compound rotation (anchor=None) with messy path formats""" s1 = magpy.magnet.Cuboid( - (1, 0, 0), (1, 1, 1), orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)]) + polarization=(1, 0, 0), + dimension=(1, 1, 1), + orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)]), + ) + s2 = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1), position=(-1, 0, 0) ) - s2 = magpy.magnet.Cuboid((1, 0, 0), (1, 1, 1), position=(-1, 0, 0)) col = magpy.Collection(s1, s2, position=[(1, 0, 0), (0, 0, 0), (3, 0, 3)]) col.rotate_from_angax([90, -90], "z", start="auto") validate_pos_orient( From 5e7bc825a855ee1a5501475ca0ea7e55ba22b49c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:32:38 +0100 Subject: [PATCH 097/240] fix Cuboid tests --- tests/test_obj_Cuboid.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/test_obj_Cuboid.py b/tests/test_obj_Cuboid.py index eb8e91462..ce8c7ef33 100644 --- a/tests/test_obj_Cuboid.py +++ b/tests/test_obj_Cuboid.py @@ -47,7 +47,7 @@ def test_Cuboid_basics(): for mag, dim, ang, ax, anch, mov, poso in zip( mags, dims, angs, axs, anchs, movs, posos ): - pm = magpy.magnet.Cuboid(mag, np.abs(dim)) + pm = magpy.magnet.Cuboid(polarization=mag, dimension=np.abs(dim)) # 18 subsequent operations for a, aa, aaa, mv in zip(ang, ax, anch, mov): @@ -61,15 +61,15 @@ def test_Cuboid_basics(): def test_Cuboid_add(): """testing __add__""" - src1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - src2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + src1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + src2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) col = src1 + src2 assert isinstance(col, magpy.Collection), "adding cuboides fail" def test_Cuboid_squeeze(): """testing squeeze output""" - src1 = magpy.magnet.Cuboid((1, 1, 1), (1, 1, 1)) + src1 = magpy.magnet.Cuboid(polarization=(1, 1, 1), dimension=(1, 1, 1)) sensor = Sensor(pixel=[(1, 2, 3), (1, 2, 3)]) B = src1.getB(sensor) assert B.shape == (2, 3) @@ -84,7 +84,7 @@ def test_Cuboid_squeeze(): def test_repr_cuboid(): """test __repr__""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) pm1.style.label = "cuboid_01" assert repr(pm1)[:6] == "Cuboid", "Cuboid repr failed" assert "label='cuboid_01'" in repr(pm1), "Cuboid repr failed" @@ -96,13 +96,17 @@ def test_cuboid_object_vs_lib(): """ a = 1 - mag = np.array([(10, 20, 30)]) + pol = np.array([(10, 20, 30)]) dim = np.array([(a, a, a)]) pos = np.array([(2 * a, 2 * a, 2 * a)]) - B0 = magpy.core.magnet_cuboid_field("B", pos, mag, dim) - H0 = magpy.core.magnet_cuboid_field("H", pos, mag, dim) - - src = magpy.magnet.Cuboid(mag[0], dim[0]) + B0 = magpy.core.magnet_cuboid_field( + field="B", observers=pos, polarization=pol, dimension=dim + ) + H0 = magpy.core.magnet_cuboid_field( + field="H", observers=pos, polarization=pol, dimension=dim + ) + + src = magpy.magnet.Cuboid(polarization=pol[0], dimension=dim[0]) B1 = src.getB(pos) H1 = src.getH(pos) From b6a64e97cdb775a2beadba318269da37e877ae7b Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:32:53 +0100 Subject: [PATCH 098/240] fix Cylinder tests --- tests/test_obj_Cylinder.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/test_obj_Cylinder.py b/tests/test_obj_Cylinder.py index e06943058..c7a9cef69 100644 --- a/tests/test_obj_Cylinder.py +++ b/tests/test_obj_Cylinder.py @@ -5,15 +5,15 @@ def test_Cylinder_add(): """testing __add__""" - src1 = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) - src2 = magpy.magnet.Cylinder((1, 2, 3), (1, 2)) + src1 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) + src2 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(1, 2)) col = src1 + src2 assert isinstance(col, magpy.Collection), "adding cylinder fail" def test_Cylinder_squeeze(): """testing squeeze output""" - src1 = magpy.magnet.Cylinder((1, 1, 1), (1, 1)) + src1 = magpy.magnet.Cylinder(polarization=(1, 1, 1), dimension=(1, 1)) sensor = magpy.Sensor(pixel=[(1, 2, 3), (1, 2, 3)]) B = src1.getB(sensor) assert B.shape == (2, 3) @@ -28,13 +28,15 @@ def test_Cylinder_squeeze(): def test_repr(): """test __repr__""" - pm2 = magpy.magnet.Cylinder((1, 2, 3), (2, 3)) + pm2 = magpy.magnet.Cylinder(polarization=(1, 2, 3), dimension=(2, 3)) assert repr(pm2)[:8] == "Cylinder", "Cylinder repr failed" def test_repr2(): """test __repr__""" - pm2 = magpy.magnet.CylinderSegment((1, 2, 3), (2, 3, 1, 0, 45)) + pm2 = magpy.magnet.CylinderSegment( + polarization=(1, 2, 3), dimension=(2, 3, 1, 0, 45) + ) assert repr(pm2)[:15] == "CylinderSegment", "CylinderSegment repr failed" @@ -43,7 +45,7 @@ def test_Cylinder_getBH(): test Cylinder getB and getH with different inputs vs the vectorized form """ - mag = (22, 33, 44) + pol = (22, 33, 44) poso = [ (0.123, 0.234, 0.345), (-0.123, 0.234, 0.345), @@ -67,8 +69,8 @@ def test_Cylinder_getBH(): dim5 = [(0, 0.5, 2, 0, 360), (0, 1, 3, 0, 360), (0.0000001, 1.5, 4, 0, 360)] for d2, d5 in zip(dim2, dim5): - src1 = magpy.magnet.Cylinder(mag, d2) - src2 = magpy.magnet.CylinderSegment(mag, d5) + src1 = magpy.magnet.Cylinder(polarization=pol, dimension=d2) + src2 = magpy.magnet.CylinderSegment(polarization=pol, dimension=d5) B0 = src1.getB(poso) H0 = src1.getH(poso) @@ -78,26 +80,26 @@ def test_Cylinder_getBH(): B2 = magpy.getB( "Cylinder", poso, - magnetization=mag, + polarization=pol, dimension=d2, ) H2 = magpy.getH( "Cylinder", poso, - magnetization=mag, + polarization=pol, dimension=d2, ) B3 = magpy.getB( "CylinderSegment", poso, - magnetization=mag, + polarization=pol, dimension=d5, ) H3 = magpy.getH( "CylinderSegment", poso, - magnetization=mag, + polarization=pol, dimension=d5, ) From 6b9b7ab16523d47bc6ac7349d2087b18e2ed884a Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:33:50 +0100 Subject: [PATCH 099/240] fix CylinderSegment tests --- tests/test_obj_CylinderSegment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_obj_CylinderSegment.py b/tests/test_obj_CylinderSegment.py index 34ce904f5..679aa7e4a 100644 --- a/tests/test_obj_CylinderSegment.py +++ b/tests/test_obj_CylinderSegment.py @@ -5,14 +5,16 @@ def test_repr(): """test __repr__""" - pm2 = magpy.magnet.CylinderSegment((1, 2, 3), (1, 2, 3, 0, 90)) + pm2 = magpy.magnet.CylinderSegment( + polarization=(1, 2, 3), dimension=(1, 2, 3, 0, 90) + ) assert repr(pm2)[:15] == "CylinderSegment", "CylinderSegment repr failed" def test_barycenter(): """test if barycenter is computed correctly""" cs = magpy.magnet.CylinderSegment( - magnetization=(100, 0, 0), dimension=(1, 2, 1, 85, 170) + polarization=(100, 0, 0), dimension=(1, 2, 1, 85, 170) ) expected_barycenter_squeezed = np.array([-0.86248133, 1.12400755, 0.0]) From f15768d5cdd5e8cc104b8e0a0491aa8d46807bc9 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:47:04 +0100 Subject: [PATCH 100/240] fix TriangularMesh tests --- tests/test_obj_TriangularMesh.py | 120 ++++++++++++++++++------------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/tests/test_obj_TriangularMesh.py b/tests/test_obj_TriangularMesh.py index ef966e4b5..bd59b0bbc 100644 --- a/tests/test_obj_TriangularMesh.py +++ b/tests/test_obj_TriangularMesh.py @@ -16,16 +16,18 @@ def test_TriangularMesh_repr(): """TriangularMesh repr test""" - trimesh = magpy.magnet.TriangularMesh.from_pyvista((0, 0, 1000), pv.Octahedron()) + trimesh = magpy.magnet.TriangularMesh.from_pyvista( + polarization=(0, 0, 1), polydata=pv.Octahedron() + ) assert repr(trimesh).startswith("TriangularMesh"), "TriangularMesh repr failed" def test_TriangularMesh_barycenter(): """test TriangluarMesh barycenter""" - mag = (0, 0, 333) - trimesh = magpy.magnet.TriangularMesh.from_pyvista(mag, pv.Octahedron()).move( - (1, 2, 3) - ) + pol = (0, 0, 333) + trimesh = magpy.magnet.TriangularMesh.from_pyvista( + polarization=pol, polydata=pv.Octahedron() + ).move((1, 2, 3)) bary = np.array([1, 2, 3]) np.testing.assert_allclose(trimesh.barycenter, bary) @@ -33,21 +35,21 @@ def test_TriangularMesh_barycenter(): def test_TriangularMesh_getBH(): """Compare meshed cube to magpylib cube""" dimension = (1, 1, 1) - magnetization = (100, 200, 300) + polarization = (100, 200, 300) mesh3d = magpy.graphics.model3d.make_Cuboid() vertices = np.array([v for k, v in mesh3d["kwargs"].items() if k in "xyz"]).T faces = np.array([v for k, v in mesh3d["kwargs"].items() if k in "ijk"]).T faces[0] = faces[0][[0, 2, 1]] # flip one triangle in wrong orientation - cube = magpy.magnet.Cuboid(magnetization=magnetization, dimension=dimension) + cube = magpy.magnet.Cuboid(polarization=polarization, dimension=dimension) cube.rotate_from_angax(19, (1, 2, 3)) cube.move((1, 2, 3)) cube_facet_reorient_true = magpy.magnet.TriangularMesh( position=cube.position, orientation=cube.orientation, - magnetization=magnetization, + polarization=polarization, vertices=vertices, faces=faces, reorient_faces=True, @@ -56,7 +58,7 @@ def test_TriangularMesh_getBH(): cube_facet_reorient_false = magpy.magnet.TriangularMesh( position=cube.position, orientation=cube.orientation, - magnetization=magnetization, + polarization=polarization, vertices=vertices, faces=faces, reorient_faces=False, @@ -87,12 +89,14 @@ def test_TriangularMesh_getB_different_facet_shapes_mixed(): shape (12,3,3) vs (4,3,3) for facet tetrahedron""" tetra_pv = pv.Tetrahedron() tetra = ( - magpy.magnet.Tetrahedron((444, 555, 666), vertices=tetra_pv.points) + magpy.magnet.Tetrahedron( + polarization=(0.444, 0.555, 0.666), vertices=tetra_pv.points + ) .move((-1, 1, 1)) .rotate_from_angax([14, 65, 97], (4, 6, 9), anchor=0) ) tetra_kwargs = { - "magnetization": tetra.magnetization, + "polarization": tetra.polarization, "position": tetra.position, "orientation": tetra.orientation, } @@ -101,12 +105,12 @@ def test_TriangularMesh_getB_different_facet_shapes_mixed(): ) assert tmesh_tetra.status_reoriented is True cube = ( - magpy.magnet.Cuboid((111, 222, 333), (1, 1, 1)) + magpy.magnet.Cuboid(polarization=(0.111, 0.222, 0.333), dimension=(1, 1, 1)) .move((1, 1, 1)) .rotate_from_angax([14, 65, 97], (4, 6, 9), anchor=0) ) cube_kwargs = { - "magnetization": cube.magnetization, + "polarization": cube.polarization, "position": cube.position, "orientation": cube.orientation, } @@ -127,21 +131,21 @@ def test_TriangularMesh_getB_different_facet_shapes_mixed(): def test_magnet_trimesh_func(): """test on manual inside""" - mag = (111, 222, 333) + pol = (0.111, 0.222, 0.333) dim = (10, 10, 10) - cube = magpy.magnet.Cuboid(mag, dim) + cube = magpy.magnet.Cuboid(polarization=pol, dimension=dim) tmesh_cube = magpy.magnet.TriangularMesh.from_pyvista( - mag, pv.Cube(cube.position, *dim) + polarization=pol, polydata=pv.Cube(cube.position, *dim) ) pts_inside = np.array([[0, 0, 1]]) B0 = cube.getB(pts_inside) B1 = tmesh_cube.getB(pts_inside) B2 = magnet_trimesh_field( - "B", - pts_inside, - np.array([mag]), - np.array([tmesh_cube.mesh]), + field="B", + observers=pts_inside, + polarization=np.array([pol]), + mesh=np.array([tmesh_cube.mesh]), in_out="inside", )[0] np.testing.assert_allclose(B0, B1) @@ -154,7 +158,7 @@ def test_bad_triangle_indices(): faces = [[1, 2, 3]] # index 3 >= len(vertices) with pytest.raises(IndexError): magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, ) @@ -174,14 +178,14 @@ def test_open_mesh(): faces = np.array([v for k, v in open_mesh.items() if k in "ijk"]).T with pytest.raises(ValueError, match=r"Open mesh detected in .*."): magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="raise", ) with pytest.raises(ValueError, match=r"Open mesh in .* detected."): magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="ignore", @@ -189,7 +193,7 @@ def test_open_mesh(): ) with pytest.warns(UserWarning) as record: magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="warn", @@ -200,7 +204,7 @@ def test_open_mesh(): with pytest.warns(UserWarning) as record: magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="skip", @@ -217,7 +221,7 @@ def test_open_mesh(): with warnings.catch_warnings(): # no warning should be issued! warnings.simplefilter("error") magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="ignore", @@ -229,7 +233,7 @@ def test_open_mesh(): match=r"Open mesh of .* detected", ): mesh = magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="ignore", @@ -239,7 +243,7 @@ def test_open_mesh(): with pytest.warns(UserWarning, match=r"Unchecked mesh status of .* detected"): mesh = magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_open="skip", @@ -253,7 +257,7 @@ def test_disconnected_mesh(): # Multiple Text3D letters are disconnected with pytest.raises(ValueError, match=r"Disconnected mesh detected in .*."): magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), polydata=pv.Text3D("AB"), check_disconnected="raise", ) @@ -274,14 +278,14 @@ def test_selfintersecting_triangular_mesh(): faces = np.array([v for k, v in selfintersecting_mesh3d.items() if k in "ijk"]).T with pytest.raises(ValueError, match=r"Self-intersecting mesh detected in .*."): magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_selfintersecting="raise", ) with pytest.warns(UserWarning, match=r"Self-intersecting mesh detected in .*."): magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), vertices=vertices, faces=faces, check_selfintersecting="warn", @@ -292,7 +296,9 @@ def test_TriangularMesh_from_pyvista(): """Test from_pyvista classmethod""" def get_tri_from_pv(obj): - return magpy.magnet.TriangularMesh.from_pyvista((0, 0, 1000), obj) + return magpy.magnet.TriangularMesh.from_pyvista( + polarization=(0, 0, 1), polydata=obj + ) # shoud work get_tri_from_pv(pv.Cube()) @@ -309,12 +315,12 @@ def get_tri_from_pv(obj): def test_TriangularMesh_from_faces_bad_inputs(): """Test from_faces classmethod bad inputs""" - mag = (0, 0, 1000) + pol = (0, 0, 1) def get_tri_from_triangles(trias): return magpy.magnet.TriangularMesh.from_triangles( - mag, - trias, + polarization=pol, + triangles=trias, check_open=False, check_disconnected=False, reorient_faces=False, @@ -322,14 +328,16 @@ def get_tri_from_triangles(trias): def get_tri_from_mesh(mesh): return magpy.magnet.TriangularMesh.from_mesh( - mag, - mesh, + polarization=pol, + mesh=mesh, check_open=False, check_disconnected=False, reorient_faces=False, ) - triangle = magpy.misc.Triangle(mag, [(0, 0, 0), (1, 0, 0), (0, 1, 0)]) + triangle = magpy.misc.Triangle( + polarization=pol, vertices=[(0, 0, 0), (1, 0, 0), (0, 1, 0)] + ) # good element type but not array-like with pytest.raises(TypeError): @@ -356,26 +364,38 @@ def get_tri_from_mesh(mesh): def test_TriangularMesh_from_faces_good_inputs(): """Test from_faces classmethod good inputs""" - mag = (0, 0, 1000) + pol = (0, 0, 1) # create Tetrahedron and move/orient randomly - tetra = magpy.magnet.Tetrahedron(mag, [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]) + tetra = magpy.magnet.Tetrahedron( + polarization=pol, vertices=[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] + ) tetra.move((3, 4, 5)).rotate_from_angax([13, 37], (1, 2, 3), anchor=0) pos_ori = {"orientation": tetra.orientation, "position": tetra.position} - tmesh1 = magpy.magnet.TriangularMesh.from_ConvexHull(mag, tetra.vertices, **pos_ori) + tmesh1 = magpy.magnet.TriangularMesh.from_ConvexHull( + polarization=pol, points=tetra.vertices, **pos_ori + ) # from triangle list - trias = [magpy.misc.Triangle(mag, face) for face in tmesh1.mesh] - tmesh2 = magpy.magnet.TriangularMesh.from_triangles(mag, trias, **pos_ori) + trias = [ + magpy.misc.Triangle(polarization=pol, vertices=face) for face in tmesh1.mesh + ] + tmesh2 = magpy.magnet.TriangularMesh.from_triangles( + polarization=pol, triangles=trias, **pos_ori + ) # from collection coll = magpy.Collection(trias) - tmesh3 = magpy.magnet.TriangularMesh.from_triangles(mag, coll, **pos_ori) + tmesh3 = magpy.magnet.TriangularMesh.from_triangles( + polarization=pol, triangles=coll, **pos_ori + ) # from mesh msh = [t.vertices for t in coll] - tmesh4 = magpy.magnet.TriangularMesh.from_mesh(mag, msh, **pos_ori) + tmesh4 = magpy.magnet.TriangularMesh.from_mesh( + polarization=pol, mesh=msh, **pos_ori + ) points = [0, 0, 0] B0 = tetra.getB(points) @@ -443,7 +463,7 @@ def test_bad_mode_input(): match=r"The `check_open mode` argument .*, instead received 'badinput'.", ): magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 1000), polydata=pv.Octahedron(), check_open="badinput" + polarization=(0, 0, 1), polydata=pv.Octahedron(), check_open="badinput" ) @@ -457,7 +477,11 @@ def points(r0): return [(r0 * np.cos(t), r0 * np.sin(t), 10) for t in ts] + [(0, 0, 0)] ts = np.linspace(0, 2 * np.pi, 5) - cone1 = magpy.magnet.TriangularMesh.from_ConvexHull((0, 0, 1), points(12)) - cone2 = magpy.magnet.TriangularMesh.from_ConvexHull((0, 0, 1), points(13)) + cone1 = magpy.magnet.TriangularMesh.from_ConvexHull( + polarization=(0, 0, 1), points=points(12) + ) + cone2 = magpy.magnet.TriangularMesh.from_ConvexHull( + polarization=(0, 0, 1), points=points(13) + ) np.testing.assert_array_equal(cone1.faces, cone2.faces) From 3e2463b1beff3a2d058455a59eee71c96a0f4a24 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:49:58 +0100 Subject: [PATCH 101/240] fix Triangle tests --- tests/test_obj_Triangle.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/test_obj_Triangle.py b/tests/test_obj_Triangle.py index a65ff0d9e..7cecd60d3 100644 --- a/tests/test_obj_Triangle.py +++ b/tests/test_obj_Triangle.py @@ -13,7 +13,7 @@ def test_Triangle_repr(): def test_triangle_input1(): """test obj-oriented triangle vs cube""" obs = (1, 2, 3) - mag = (0, 0, 333) + pol = (0, 0, 333) vert = np.array( [ [(-1, -1, 1), (1, -1, 1), (-1, 1, 1)], # top1 @@ -24,8 +24,8 @@ def test_triangle_input1(): ) coll = magpy.Collection() for v in vert: - coll.add(magpy.misc.Triangle(mag, v)) - cube = magpy.magnet.Cuboid(mag, (2, 2, 2)) + coll.add(magpy.misc.Triangle(polarization=pol, vertices=v)) + cube = magpy.magnet.Cuboid(polarization=pol, dimension=(2, 2, 2)) b = coll.getB(obs) bb = cube.getB(obs) @@ -37,7 +37,7 @@ def test_triangle_input3(): """test core triangle vs objOriented triangle""" obs = np.array([(3, 4, 5)] * 4) - mag = np.array([(111, 222, 333)] * 4) + pol = np.array([(111, 222, 333)] * 4) vert = np.array( [ [(0, 0, 0), (3, 0, 0), (0, 10, 0)], @@ -46,13 +46,15 @@ def test_triangle_input3(): [(6, 0, 0), (10, 0, 0), (0, 10, 0)], ] ) - b = magpy.core.triangle_field("B", obs, mag, vert) + b = magpy.core.triangle_field( + field="B", observers=obs, polarization=pol, vertices=vert + ) b = np.sum(b, axis=0) - tri1 = magpy.misc.Triangle(mag[0], vertices=vert[0]) - tri2 = magpy.misc.Triangle(mag[0], vertices=vert[1]) - tri3 = magpy.misc.Triangle(mag[0], vertices=vert[2]) - tri4 = magpy.misc.Triangle(mag[0], vertices=vert[3]) + tri1 = magpy.misc.Triangle(polarization=pol[0], vertices=vert[0]) + tri2 = magpy.misc.Triangle(polarization=pol[0], vertices=vert[1]) + tri3 = magpy.misc.Triangle(polarization=pol[0], vertices=vert[2]) + tri4 = magpy.misc.Triangle(polarization=pol[0], vertices=vert[3]) bb = magpy.getB([tri1, tri2, tri3, tri4], obs[0], sumup=True) @@ -73,8 +75,8 @@ def call_getB(): def test_Triangle_barycenter(): """test Triangle barycenter""" - mag = (0, 0, 333) + pol = (0, 0, 0.333) vert = ((-1, -1, 0), (1, -1, 0), (0, 2, 0)) - face = magpy.misc.Triangle(mag, vert) + face = magpy.misc.Triangle(polarization=pol, vertices=vert) bary = np.array([0, 0, 0]) np.testing.assert_allclose(face.barycenter, bary) From 9cb6af91ee80e72974799237e7a5db438f462e87 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:51:57 +0100 Subject: [PATCH 102/240] fix Tetrahedron tests --- tests/test_obj_Tetrahedron.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_obj_Tetrahedron.py b/tests/test_obj_Tetrahedron.py index 3fde50732..d24bf402e 100644 --- a/tests/test_obj_Tetrahedron.py +++ b/tests/test_obj_Tetrahedron.py @@ -14,7 +14,7 @@ def test_Tetrahedron_repr(): def test_tetra_input(): """test obj-oriented triangle vs cube""" obs = (1, 2, 3) - mag = (111, 222, 333) + pol = (111, 222, 333) vert_list = [ [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)], [(-1, -1, 1), (-1, 1, 1), (1, -1, 1), (1, -1, -1)], @@ -26,9 +26,9 @@ def test_tetra_input(): coll = magpy.Collection() for v in vert_list: - coll.add(magpy.magnet.Tetrahedron(mag, v)) + coll.add(magpy.magnet.Tetrahedron(polarization=pol, vertices=v)) - cube = magpy.magnet.Cuboid(mag, (2, 2, 2)) + cube = magpy.magnet.Cuboid(polarization=pol, dimension=(2, 2, 2)) b = coll.getB(obs) bb = cube.getB(obs) @@ -52,12 +52,12 @@ def test_tetra_bad_inputs(vertices): """test obj-oriented triangle vs cube""" with pytest.raises(MagpylibBadUserInput): - magpy.magnet.Tetrahedron((111, 222, 333), vertices) + magpy.magnet.Tetrahedron(polarization=(0.111, 0.222, 0.333), vertices=vertices) def test_tetra_barycenter(): """get barycenter""" - mag = (111, 222, 333) + pol = (0.111, 0.222, 0.333) vert = [(1, 1, -1), (1, 1, 1), (-1, 1, 1), (1, -1, 1)] - tetra = magpy.magnet.Tetrahedron(mag, vert) + tetra = magpy.magnet.Tetrahedron(polarization=pol, vertices=vert) np.testing.assert_allclose(tetra.barycenter, (0.5, 0.5, 0.5)) From 5620c822eb39333a38c5d14f554d7d2218d7566f Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:55:50 +0100 Subject: [PATCH 103/240] fix Sphere tests --- tests/test_obj_Sphere.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_obj_Sphere.py b/tests/test_obj_Sphere.py index 5a7ee3732..a72915650 100644 --- a/tests/test_obj_Sphere.py +++ b/tests/test_obj_Sphere.py @@ -20,7 +20,7 @@ # # B = [] # # for mag,dim,ang,ax,anch,mov,poso in zip(mags,dims,angs,axs,anchs,movs,posos): -# # pm = magpy.magnet.Sphere(mag,dim) +# # pm = magpy.magnet.Sphere(polarization=mag, dimension=dim) # # # 18 subsequent operations # # for a,aa,aaa,mv in zip(ang,ax,anch,mov): @@ -45,7 +45,7 @@ def test_Sphere_basics(): for mag, dim, ang, ax, anch, mov, poso in zip( mags, dims, angs, axs, anchs, movs, posos ): - pm = magpy.magnet.Sphere(mag, dim) + pm = magpy.magnet.Sphere(polarization=mag, diameter=dim) # 18 subsequent operations for a, aa, aaa, mv in zip(ang, ax, anch, mov): @@ -59,15 +59,15 @@ def test_Sphere_basics(): def test_Sphere_add(): """testing __add__""" - src1 = magpy.magnet.Sphere(magnetization=(1, 2, 3), diameter=11) - src2 = magpy.magnet.Sphere((1, 2, 3), 11) + src1 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=11) + src2 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=11) col = src1 + src2 assert isinstance(col, magpy.Collection), "adding cuboids fail" def test_Sphere_squeeze(): """testing squeeze output""" - src1 = magpy.magnet.Sphere((1, 1, 1), 1) + src1 = magpy.magnet.Sphere(polarization=(1, 1, 1), diameter=1) sensor = magpy.Sensor(pixel=[(1, 2, 3), (1, 2, 3)]) B = src1.getB(sensor) assert B.shape == (2, 3) @@ -82,7 +82,7 @@ def test_Sphere_squeeze(): def test_repr(): """test __repr__""" - pm3 = magpy.magnet.Sphere((1, 2, 3), 3) + pm3 = magpy.magnet.Sphere(polarization=(1, 2, 3), diameter=3) assert repr(pm3)[:6] == "Sphere", "Sphere repr failed" @@ -91,12 +91,14 @@ def test_sphere_object_vs_lib(): tests object vs lib computation this also checks if np.int (from array slice) is allowed as input """ - mag = np.array([(10, 20, 30)]) + pol = np.array([(10, 20, 30)]) dia = np.array([1]) pos = np.array([(2, 2, 2)]) - B1 = magpy.core.magnet_sphere_field("B", pos, mag, dia)[0] + B1 = magpy.core.magnet_sphere_field( + field="B", observers=pos, polarization=pol, diameter=dia + )[0] - src = magpy.magnet.Sphere(mag[0], dia[0]) + src = magpy.magnet.Sphere(polarization=pol[0], diameter=dia[0]) B2 = src.getB(pos) np.testing.assert_allclose(B1, B2) From 544551cfdca87b214a3838cab316d1be988f669d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:57:18 +0100 Subject: [PATCH 104/240] fix Sensor tests --- tests/test_obj_Sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_obj_Sensor.py b/tests/test_obj_Sensor.py index 8b49c3f6e..ddb54510d 100644 --- a/tests/test_obj_Sensor.py +++ b/tests/test_obj_Sensor.py @@ -5,7 +5,7 @@ def test_sensor1(): """self-consistent test of the sensor class""" - pm = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) + pm = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) angs = np.linspace(0, 555, 44) possis = [ (3 * np.cos(t / 180 * np.pi), 3 * np.sin(t / 180 * np.pi), 1) for t in angs @@ -24,7 +24,7 @@ def test_sensor1(): def test_sensor2(): """self-consistent test of the sensor class""" - pm = magpy.magnet.Cuboid((11, 22, 33), (1, 2, 3)) + pm = magpy.magnet.Cuboid(polarization=(11, 22, 33), dimension=(1, 2, 3)) poz = np.linspace(0, 5, 33) poss1 = [(t, 0, 2) for t in poz] poss2 = [(t, 0, 3) for t in poz] @@ -43,7 +43,7 @@ def test_sensor2(): def test_Sensor_getB_specs(): """test input of sens getB""" sens1 = magpy.Sensor(pixel=(4, 4, 4)) - pm1 = magpy.magnet.Cylinder((111, 222, 333), (1, 2)) + pm1 = magpy.magnet.Cylinder(polarization=(111, 222, 333), dimension=(1, 2)) B1 = sens1.getB(pm1) B2 = magpy.getB(pm1, sens1) @@ -52,7 +52,7 @@ def test_Sensor_getB_specs(): def test_Sensor_squeeze(): """testing squeeze output""" - src = magpy.magnet.Sphere((1, 1, 1), 1) + src = magpy.magnet.Sphere(polarization=(1, 1, 1), diameter=1) sensor = magpy.Sensor(pixel=[(1, 2, 3), (1, 2, 3)]) B = sensor.getB(src) assert B.shape == (2, 3) @@ -77,7 +77,7 @@ def test_pixel1(): logic: single sensor, scalar path, single source all generate 1 for squeeze=False Bshape. Bare pixel should do the same """ - src = magpy.misc.Dipole((1, 2, 3)) + src = magpy.misc.Dipole(moment=(1, 2, 3)) # squeeze=False Bshape of nbare pixel must be (1,1,1,1,3) np.testing.assert_allclose( From 68a9801989e3530f698d850bbcbbdedff27d3085 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 02:58:42 +0100 Subject: [PATCH 105/240] fix Polyline tests --- tests/test_obj_Polyline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_obj_Polyline.py b/tests/test_obj_Polyline.py index fcadb7d7c..f8224ae84 100644 --- a/tests/test_obj_Polyline.py +++ b/tests/test_obj_Polyline.py @@ -11,7 +11,7 @@ def test_Polyline_basic1(): sens = magpy.Sensor() B = src.getB(sens) - x = 5.77350269 + x = 5.77350269 * 1e-6 Btest = np.array([x, -x, 0]) assert np.allclose(B, Btest) @@ -38,7 +38,7 @@ def test_Polyline_basic3(): sens = magpy.Sensor() B = magpy.getB([line1, line2], sens) - x = 5.77350269 + x = 5.77350269 * 1e-6 Btest = np.array([(x, -x, 0)] * 2) assert np.allclose(B, Btest) From 7a426f8a2365d889b20662493520a5f7f213fc2e Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 03:04:36 +0100 Subject: [PATCH 106/240] pylint --- tests/test_core_physics_consistency.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 651f9d197..6d41067aa 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -1,24 +1,7 @@ import numpy as np -import pytest -from numpy.testing import assert_allclose import magpylib as magpy -from magpylib._src.exceptions import MagpylibDeprecationWarning -from magpylib._src.exceptions import MagpylibMissingInput -from magpylib._src.fields.field_BH_polyline import current_vertices_field -from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 -from magpylib.core import current_circle_field -from magpylib.core import current_line_field -from magpylib.core import current_loop_field -from magpylib.core import current_polyline_field -from magpylib.core import dipole_field -from magpylib.core import magnet_cuboid_field -from magpylib.core import magnet_cylinder_field -from magpylib.core import magnet_cylinder_segment_field -from magpylib.core import magnet_sphere_field -from magpylib.core import magnet_tetrahedron_field -from magpylib.core import triangle_field # PHYSICS CONSISTENCY TESTING # From 6aa0676c182365740f30e8b005156781c33ea3c1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 03:08:03 +0100 Subject: [PATCH 107/240] fix Circle tests --- tests/test_obj_Circle.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_obj_Circle.py b/tests/test_obj_Circle.py index e3c4be4b6..711474b75 100644 --- a/tests/test_obj_Circle.py +++ b/tests/test_obj_Circle.py @@ -11,7 +11,7 @@ def test_Circle_basic_B(): sens = magpy.Sensor(position=(1, 2, 3)) B = src.getB(sens) - Btest = np.array([0.44179833, 0.88359665, 0.71546231]) + Btest = np.array([0.44179833, 0.88359665, 0.71546231]) * 1e-6 assert np.allclose(B, Btest) @@ -19,20 +19,20 @@ def test_current_circle_field(): """test explicit field output values""" s = magpy.current.Circle(current=1, diameter=1) - B_c1d1z0 = 1.2566370614359172 + B_c1d1z0 = 1.2566370614359172 * 1e-6 B_test = s.getB([0, 0, 0]) assert abs(B_c1d1z0 - B_test[2]) < 1e-14 - B_c1d1z1 = 0.11239703569665165 + B_c1d1z1 = 0.11239703569665165 * 1e-6 B_test = s.getB([0, 0, 1]) assert abs(B_c1d1z1 - B_test[2]) < 1e-14 s = magpy.current.Circle(current=1, diameter=2) - B_c1d2z0 = 0.6283185307179586 + B_c1d2z0 = 0.6283185307179586 * 1e-6 B_test = s.getB([0, 0, 0]) assert abs(B_c1d2z0 - B_test[2]) < 1e-14 - B_c1d2z1 = 0.22214414690791835 + B_c1d2z1 = 0.22214414690791835 * 1e-6 B_test = s.getB([0, 0, 1]) assert abs(B_c1d2z1 - B_test[2]) < 1e-14 From a2f568d8b1b8d45ae3da9600b6061231fcf472a8 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 17:03:05 +0100 Subject: [PATCH 108/240] fix Dipole and Dipole tests --- magpylib/_src/fields/field_wrap_BH.py | 8 ++++---- .../_src/obj_classes/class_misc_Dipole.py | 20 +++++++++---------- tests/test_getBH_dict.py | 4 ++-- tests/test_input_checks.py | 6 +++--- tests/test_obj_Collection.py | 2 +- tests/test_obj_Dipole.py | 4 ++-- tests/test_obj_Sensor.py | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 347a3bf97..0b009c900 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -622,9 +622,9 @@ def getB( Magnetization vector M = J/mu0 in units of A/m, given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit T・m³ + moment: array_like, shape (3) or (n,3), unit A・m² Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of T・m³ given in the local object coordinates + Magnetic dipole moment(s) in units of A・m² given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. @@ -793,9 +793,9 @@ def getH( Magnetization vector(s) (mu0*M, remanence field) in units of A/m given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit T・m³ + moment: array_like, shape (3) or (n,3), unit A・m² Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of T・m³ given in the local object coordinates + Magnetic dipole moment(s) in units of A・m² given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index fec9b0b56..e1a31ea8d 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -19,11 +19,6 @@ class Dipole(BaseSource): Parameters ---------- - moment: array_like, shape (3,), unit T・m³, default=`None` - Magnetic dipole moment in units of T・m³ given in the local object coordinates. - For homogeneous magnets the relation moment=magnetization*volume holds. The dipole - moment of a Circle object is pi**2/10*diameter**2*current. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. @@ -33,6 +28,11 @@ class Dipole(BaseSource): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + moment: array_like, shape (3,), unit A・m², default=`None` + Magnetic dipole moment in units of A・m² given in the local object coordinates. + For homogeneous magnets the relation moment=magnetization*volume holds. The dipole + moment of a Circle object is pi**2/10*diameter**2*current. + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -47,7 +47,7 @@ class Dipole(BaseSource): Examples -------- `Dipole` objects are magnetic field sources. In this example we compute the H-field A/m - of such a magnetic dipole with a moment of (100,100,100) in units of T・m³ at an + of such a magnetic dipole with a moment of (100,100,100) in units of A・m² at an observer position (.01,.01,.01) given in units of meter: >>> import magpylib as magpy @@ -87,9 +87,9 @@ class Dipole(BaseSource): def __init__( self, - moment=None, position=(0, 0, 0), orientation=None, + moment=None, style=None, **kwargs, ): @@ -102,12 +102,12 @@ def __init__( # property getters and setters @property def moment(self): - """Magnetic dipole moment in units of T・m³ given in the local object coordinates.""" + """Magnetic dipole moment in units of A・m² given in the local object coordinates.""" return self._moment @moment.setter def moment(self, mom): - """Set dipole moment vector, shape (3,), unit T・m³.""" + """Set dipole moment vector, shape (3,), unit A・m².""" self._moment = check_format_input_vector( mom, dims=(1,), @@ -124,4 +124,4 @@ def _default_style_description(self): moment_mag = np.linalg.norm(moment) if moment_mag == 0: return "no moment" - return f"moment={unit_prefix(moment_mag)}T・m³" + return f"moment={unit_prefix(moment_mag)}A・m²" diff --git a/tests/test_getBH_dict.py b/tests/test_getBH_dict.py index 099696b1e..17f44107a 100644 --- a/tests/test_getBH_dict.py +++ b/tests/test_getBH_dict.py @@ -130,11 +130,11 @@ def test_getB_dict4(): def test_getBH_dipole(): """test if Dipole implementation gives correct output""" B = magpy.getB("Dipole", (1, 1, 1), moment=(1, 2, 3)) - Btest = np.array([0.07657346, 0.06125877, 0.04594407]) + Btest = np.array([9.62250449e-08, 7.69800359e-08, 5.77350269e-08]) assert np.allclose(B, Btest) H = magpy.getH("Dipole", (1, 1, 1), moment=(1, 2, 3)) - Htest = np.array([0.06093522, 0.04874818, 0.03656113]) + Htest = np.array([0.07657346, 0.06125877, 0.04594407]) assert np.allclose(H, Htest) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 9657f9d38..8bdee11c7 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -720,7 +720,7 @@ def test_input_rotate_axis_bad(axis): ) def test_input_observers_good(observers): """good observers input""" - src = magpy.misc.Dipole((1, 2, 3)) + src = magpy.misc.Dipole(moment=(1, 2, 3)) B = src.getB(observers) assert isinstance(B, np.ndarray) @@ -733,7 +733,7 @@ def test_input_observers_good(observers): [], ("a", "b", "c"), [("a", "b", "c")], - magpy.misc.Dipole((1, 2, 3)), + magpy.misc.Dipole(moment=(1, 2, 3)), [(1, 2, 3), [(1, 2, 3)] * 2], [magpy.Sensor(), [(1, 2, 3)] * 2], [[(1, 2, 3)] * 2, magpy.Collection(magpy.Sensor())], @@ -742,7 +742,7 @@ def test_input_observers_good(observers): ) def test_input_observers_bad(observers): """bad observers input""" - src = magpy.misc.Dipole((1, 2, 3)) + src = magpy.misc.Dipole(moment=(1, 2, 3)) with pytest.raises(MagpylibBadUserInput): src.getB(observers) diff --git a/tests/test_obj_Collection.py b/tests/test_obj_Collection.py index d6829470b..2f4690be1 100644 --- a/tests/test_obj_Collection.py +++ b/tests/test_obj_Collection.py @@ -284,7 +284,7 @@ def test_Collection_with_Dipole(): sens = magpy.Sensor() B = magpy.getB(col, sens) - Btest = np.array([0.00303828, 0.00607656, 0.00911485]) + Btest = np.array([3.81801774e-09, 7.63603548e-09, 1.14540532e-08]) assert np.allclose(B, Btest) diff --git a/tests/test_obj_Dipole.py b/tests/test_obj_Dipole.py index 714e25e24..8c4c725c0 100644 --- a/tests/test_obj_Dipole.py +++ b/tests/test_obj_Dipole.py @@ -9,7 +9,7 @@ def test_Dipole_basicB(): sens = magpy.Sensor() B = src.getB(sens) - Btest = np.array([0.00303828, 0.00607656, 0.00911485]) + Btest = np.array([3.81801774e-09, 7.63603548e-09, 1.14540532e-08]) assert np.allclose(B, Btest) @@ -18,7 +18,7 @@ def test_Dipole_basicH(): src = magpy.misc.Dipole(moment=(1, 2, 3), position=(1, 2, 3)) sens = magpy.Sensor() H = src.getH(sens) - Htest = np.array([0.00241779, 0.00483558, 0.00725336]) + Htest = np.array([0.00303828, 0.00607656, 0.00911485]) assert np.allclose(H, Htest) diff --git a/tests/test_obj_Sensor.py b/tests/test_obj_Sensor.py index ddb54510d..9be750f99 100644 --- a/tests/test_obj_Sensor.py +++ b/tests/test_obj_Sensor.py @@ -86,7 +86,7 @@ def test_pixel1(): ) # squeeze=False Bshape of [(1,2,3)] must then also be (1,1,1,1,3) - src = magpy.misc.Dipole((1, 2, 3)) + src = magpy.misc.Dipole(moment=(1, 2, 3)) np.testing.assert_allclose( src.getB(magpy.Sensor(pixel=[(1, 2, 3)]), squeeze=False).shape, (1, 1, 1, 1, 3), @@ -121,7 +121,7 @@ def test_pixel3(): There should be complete equivalence between pos_vec and Sensor(pixel=pos_vec) inputs """ - src = magpy.misc.Dipole((1, 2, 3)) + src = magpy.misc.Dipole(moment=(1, 2, 3)) p0 = (1, 2, 3) p1 = [(1, 2, 3)] From b328b6facb7c5095d26cece09f3832dc2aff1ed7 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 17:04:45 +0100 Subject: [PATCH 109/240] fix missing modules tests --- tests/test__missing_optional_modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test__missing_optional_modules.py b/tests/test__missing_optional_modules.py index 7ee84dedf..0e1f7984e 100644 --- a/tests/test__missing_optional_modules.py +++ b/tests/test__missing_optional_modules.py @@ -8,7 +8,7 @@ def test_show_with_missing_pyvista(): """Should raise if pyvista is not installed""" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with patch.dict(sys.modules, {"pyvista": None}): # with pytest.raises(ModuleNotFoundError): src.show(return_fig=True, backend="pyvista") @@ -16,7 +16,7 @@ def test_show_with_missing_pyvista(): def test_dataframe_output_missing_pandas(): """test if pandas is installed when using dataframe output in `getBH`""" - src = magpy.magnet.Cuboid((0, 0, 1000), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with patch.dict(sys.modules, {"pandas": None}): with pytest.raises(ModuleNotFoundError): src.getB((0, 0, 0), output="dataframe") From 2f75183db66c2bba6f99e21b005a850c5c7f74f0 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 17:07:27 +0100 Subject: [PATCH 110/240] adapt bugreport --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 485742231..8dc50c1d7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -12,11 +12,11 @@ body: label: Magpylib version description: What version of Magpylib are you running? options: + - 5.x (Unreleased) - 4.x (Latest) - 3.x - 2.x - 1.x - - Unreleased validations: required: true - type: textarea @@ -37,7 +37,7 @@ body: import numpy as np import magpylib as magpy - cuboid = magpy.magnet.Cuboid((0,0,1000), (1,1,1)) + cuboid = magpy.magnet.Cuboid(polarization=(0.,0.,1.), dimension=(1.,1.,1.)) cuboid.move(np.linspace((0,0,0), (0,0,10), 11), start=0) cuboid.show() render: Python From 8dc5734826275b5089211ca0df00ac7cb47c5bac Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 25 Dec 2023 17:11:54 +0100 Subject: [PATCH 111/240] more positional avoided in tests --- tests/test_display_utility.py | 2 +- tests/test_exceptions.py | 12 ++++++------ tests/test_input_checks.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_display_utility.py b/tests/test_display_utility.py index e9b94d5cc..4197d316e 100644 --- a/tests/test_display_utility.py +++ b/tests/test_display_utility.py @@ -65,7 +65,7 @@ def test_draw_arrow_from_vertices(): def test_bad_backend(): """test bad plotting input name""" with pytest.raises(MagpylibBadUserInput): - c = magpy.magnet.Cuboid((0, 0, 1), (1, 1, 1)) + c = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) c.show(backend="asdf") diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 77f45c7c1..820e19a1e 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -28,7 +28,7 @@ def getBHv_unknown_source_type(): def getBH_level2_bad_input1(): """test BadUserInput error at getBH_level2""" - src = magpy.magnet.Cuboid((1, 1, 2), (1, 1, 1)) + src = magpy.magnet.Cuboid(polarization=(1, 1, 2), dimension=(1, 1, 1)) sens = magpy.Sensor() getBH_level2( [src, sens], @@ -164,15 +164,15 @@ def getBHv_bad_input3(): def utility_format_obj_input(): """bad input object""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) format_obj_input([pm1, pm2, 333]) def utility_format_src_inputs(): """bad src input""" - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) - pm2 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) + pm2 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) format_src_inputs([pm1, pm2, 1]) @@ -187,7 +187,7 @@ def utility_format_obs_inputs(): def utility_test_path_format(): """bad path format input""" # pylint: disable=protected-access - pm1 = magpy.magnet.Cuboid((1, 2, 3), (1, 2, 3)) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) pm1._position = [(1, 2, 3), (1, 2, 3)] tpf(pm1) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 8bdee11c7..0fb104c68 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -93,7 +93,7 @@ def test_input_objects_pixel_bad(pixel): """bad input: magpy.Sensor(pixel=pixel)""" with pytest.raises(MagpylibBadUserInput): - magpy.Sensor((0, 0, 0), pixel=pixel) + magpy.Sensor(position=(0, 0, 0), pixel=pixel) @pytest.mark.parametrize( From 578e89a2c7eecb799310aab09fabc8eaba71fb98 Mon Sep 17 00:00:00 2001 From: mortner Date: Mon, 25 Dec 2023 21:16:48 +0100 Subject: [PATCH 112/240] fix class_BaseExcitations docstrings --- .../_src/obj_classes/class_BaseExcitations.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index df35d2ba2..d5ec5005c 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -35,7 +35,7 @@ def field_func(self): """ The function for B- and H-field computation must have the two positional arguments `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units - of mT or kA/m must be returned respectively. The `observers` argument must + of T or A/m must be returned respectively. The `observers` argument must accept numpy ndarray inputs of shape (n,3), in which case the returned fields must be numpy ndarrays of shape (n,3) themselves. """ @@ -52,15 +52,17 @@ def field_func(self, val): self._field_func = val def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute the B-field in units of mT generated by the source. + """Compute the B-field at observers in units of T generated by the source. + + SI units are used for all inputs and outputs. Parameters ---------- observers: array_like or (list of) `Sensor` objects Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list - of such sensor objects (must all have similar pixel shapes). All positions - are given in units of mm. + of such sensor objects (must all have similar pixel shapes). All positions are given + in units of meters. squeeze: bool, default=`True` If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. @@ -80,9 +82,9 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - B-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of mT. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be + B-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indices n1,n2,...) in units of T. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m will be considered as static beyond their end. Examples @@ -121,7 +123,7 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): ) def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute the H-field in units of kA/m generated by the source. + """Compute the H-field in units of A/m at observers generated by the source. Parameters ---------- @@ -129,7 +131,7 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of mm. + are given in units of meters. squeeze: bool, default=`True` If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. @@ -149,10 +151,10 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Returns ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - H-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of kA/m. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be - considered as static beyond their end. + H-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indices n1,n2,...) in units of A/m. Sensor pixel positions are + equivalent to simple observer positions. Paths of objects that are shorter than + index m will be considered as static beyond their end. Examples -------- @@ -221,7 +223,7 @@ def magnetization(self): @magnetization.setter def magnetization(self, mag): - """Set magnetization vector, array_like, shape (3,), unit mT.""" + """Set magnetization vector, array_like, shape (3,), unit A/m.""" self._magnetization = check_format_input_vector( mag, dims=(1,), @@ -241,7 +243,7 @@ def polarization(self): @polarization.setter def polarization(self, mag): - """Set polarization vector, array_like, shape (3,), unit mT.""" + """Set polarization vector, array_like, shape (3,), unit T.""" self._polarization = check_format_input_vector( mag, dims=(1,), From ba308e6d831f53f291b6b7de2deae679c9b233a7 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 26 Dec 2023 00:16:23 +0100 Subject: [PATCH 113/240] fix field_BH_polyline --- magpylib/_src/fields/field_BH_polyline.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 5aa319cf0..22513da32 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -143,7 +143,7 @@ def current_polyline_field( # allocate for special case treatment ntot = len(current) - field_all = np.zeros((ntot, 3)) + B_all = np.zeros((ntot, 3)) # Check for zero-length segments (or discontinuous) mask_nan_start = np.isnan(segment_start).all(axis=1) @@ -152,7 +152,7 @@ def current_polyline_field( mask0 = mask_equal | mask_nan_start | mask_nan_end if np.all(mask0): - return field_all + return B_all # continue only with non-zero segments if np.any(mask0): @@ -182,7 +182,7 @@ def current_polyline_field( # separate on-line cases (-> B=0) mask1 = norm_o4 < 1e-15 # account for numerical issues if np.all(mask1): - return field_all + return B_all # continue only with general off-line cases if np.any(mask1): @@ -222,18 +222,18 @@ def current_polyline_field( mask4 = ~mask2 * ~mask3 deltaSin[mask4] = abs(sinTh1[mask4] + sinTh2[mask4]) - field = (deltaSin / norm_o4 * eB.T / norm_12 * current * 1e-7).T # m->mm, T->mT + B = (deltaSin / norm_o4 * eB.T / norm_12 * current * 1e-7).T # m->mm, T->mT # broadcast general case results into allocated vector mask0[~mask0] = mask1 - field_all[~mask0] = field + B_all[~mask0] = B # return B if field == "B": - return field_all + return B_all # return H - return field_all / MU0 + return B_all / MU0 def current_line_field(*args, **kwargs): From 3157839f1ea4609480d5953eee7f75f40b7aee6f Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 26 Dec 2023 00:20:30 +0100 Subject: [PATCH 114/240] fix field input check robustness --- magpylib/_src/input_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index 477c2d2c2..c9daaf2ab 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -121,7 +121,7 @@ def check_degree_type(inp): def check_field_input(inp): """check field input""" allowed = tuple("BHMJ") - if inp not in allowed: + if not (isinstance(inp, str) and inp in allowed): raise MagpylibBadUserInput( f"`field` input can only be one of {allowed}.\n" f"Instead received {repr(inp)}." From 3dc9971431c04aa0bf3a226cab5acf438ffc7bfb Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:32:14 +0100 Subject: [PATCH 115/240] describe "degrees" -> "deg" --- magpylib/_src/obj_classes/class_BaseDisplayRepr.py | 2 +- tests/test_obj_BaseGeo.py | 14 +++++++------- tests/test_obj_Collection.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index 6402f4259..1258d8fe8 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -9,7 +9,7 @@ UNITS = { "parent": None, "position": "m", - "orientation": "degrees", + "orientation": "deg", "dimension": "m", "diameter": "m", "current": "A", diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index b3c8da4d2..adbe6cd96 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -424,7 +424,7 @@ def test_describe(): test = ( "
Cuboid(id=REGEX, label='x1')
• parent: None
• " - "position: [0. 0. 0.] m
• orientation: [0. 0. 0.] degrees
• " + "position: [0. 0. 0.] m
• orientation: [0. 0. 0.] deg
• " "dimension: None m
• magnetization: None A/m
• polarization: None T
" ) rep = x1._repr_html_() @@ -436,7 +436,7 @@ def test_describe(): "Cuboid(id=REGEX, label='x1')", " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE " • position: [0. 0. 0.] m", - " • orientation: [0. 0. 0.] degrees", + " • orientation: [0. 0. 0.] deg", " • dimension: None m", " • magnetization: None A/m", " • polarization: None T", @@ -449,7 +449,7 @@ def test_describe(): "Cylinder(id=REGEX, label='x2')", " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE " • position: [0. 0. 0.] m", - " • orientation: [0. 0. 0.] degrees", + " • orientation: [0. 0. 0.] deg", " • dimension: [1. 3.] m", " • magnetization: [1591549.43091895 2387324.14637843 3183098.86183791] A/m", " • polarization: [2. 3. 4.] T", @@ -463,7 +463,7 @@ def test_describe(): " • parent: None ", # INVISIBLE SPACE " • path length: 3", " • position (last): [1. 2. 3.] m", - " • orientation (last): [0. 0. 0.] degrees", + " • orientation (last): [0. 0. 0.] deg", " • handedness: right ", " • pixel: 15 ", # INVISIBLE SPACE ] @@ -478,7 +478,7 @@ def test_describe(): "Sensor(id=REGEX)\n" + " • parent: None \n" + " • position: [0. 0. 0.] m\n" - + " • orientation: [0. 0. 0.] degrees\n" + + " • orientation: [0. 0. 0.] deg\n" + " • handedness: right \n" + " • pixel: 1 \n" + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True), " @@ -500,7 +500,7 @@ def test_describe(): "Sensor(id=REGEX)\n" + " • parent: None \n" + " • position: [0. 0. 0.] m\n" - + " • orientation: [0. 0. 0.] degrees\n" + + " • orientation: [0. 0. 0.] deg\n" + " • handedness: left \n" + " • pixel: 75 (3x5x5) " ) @@ -525,7 +525,7 @@ def test_describe(): "TriangularMesh(id=REGEX)\n" " • parent: None \n" " • position: [0. 0. 0.] m\n" - " • orientation: [0. 0. 0.] degrees\n" + " • orientation: [0. 0. 0.] deg\n" " • magnetization: [ 0. 0. 795774.71545948] A/m\n" " • polarization: [0. 0. 1.] T\n" " • barycenter: [0. 0. 0.46065534] \n" diff --git a/tests/test_obj_Collection.py b/tests/test_obj_Collection.py index 2f4690be1..f50121533 100644 --- a/tests/test_obj_Collection.py +++ b/tests/test_obj_Collection.py @@ -432,16 +432,16 @@ def test_collection_describe(): test = [ "Collection", "│ • position: [0. 0. 0.] m", - "│ • orientation: [0. 0. 0.] degrees", + "│ • orientation: [0. 0. 0.] deg", "├── x", "│ • position: [0. 0. 0.] m", - "│ • orientation: [0. 0. 0.] degrees", + "│ • orientation: [0. 0. 0.] deg", "│ • dimension: None m", "│ • magnetization: None A/m", "│ • polarization: None T", "└── y", " • position: [0. 0. 0.] m", - " • orientation: [0. 0. 0.] degrees", + " • orientation: [0. 0. 0.] deg", " • dimension: None m", " • magnetization: None A/m", " • polarization: None T", From 568fa9646d0966aea73de500b48473280a10eacc Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:35:06 +0100 Subject: [PATCH 116/240] bugfix: added moment unit to describe --- magpylib/_src/obj_classes/class_BaseDisplayRepr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index 1258d8fe8..da02b9c93 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -15,6 +15,7 @@ "current": "A", "magnetization": "A/m", "polarization": "T", + "moment": "A*m^2", } From 2352107b4b6160f8a111417eb861281b9caf6da9 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:41:27 +0100 Subject: [PATCH 117/240] docstring missing SI --- magpylib/_src/fields/field_BH_dipole.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index 86ff7c757..553d98acf 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -18,6 +18,8 @@ def dipole_field( The dipole moment lies in the origin of the coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- field: str, default=`'B'` From 465ec0c2cb8a8cefa8ee5f6f341a38304e25d70c Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:41:45 +0100 Subject: [PATCH 118/240] docstring fix SI --- magpylib/_src/obj_classes/class_BaseTransform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseTransform.py b/magpylib/_src/obj_classes/class_BaseTransform.py index f4aad125a..2feaef1e2 100644 --- a/magpylib/_src/obj_classes/class_BaseTransform.py +++ b/magpylib/_src/obj_classes/class_BaseTransform.py @@ -248,7 +248,7 @@ class BaseTransform: """Inherit this class to provide rotation() and move() methods.""" def move(self, displacement, start="auto"): - """Move object by the displacement input. + """Move object by the displacement input. SI units are used for all inputs and outputs. Terminology for move/rotate methods: @@ -271,7 +271,7 @@ def move(self, displacement, start="auto"): Parameters ---------- displacement: array_like, shape (3,) or (n,3) - Displacement vector in units of mm. + Displacement vector in units of m. start: int or str, default=`'auto'` Starting index when applying operations. See 'General move/rotate behavior' above From e39a41c47b8f7f2e2cefc3b555b938f46aebe180 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 26 Dec 2023 00:45:20 +0100 Subject: [PATCH 119/240] fix cylinder segment --- magpylib/_src/fields/field_BH_cylinder_segment.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index e2637310f..90940c8d9 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2413,7 +2413,7 @@ def magnet_cylinder_segment_field( """ check_field_input(field) - BHfinal = np.zeros((len(polarization), 3)) + H_all = np.zeros((len(observers), 3)) r1, r2, h, phi1, phi2 = dimension.T r1 = abs(r1) @@ -2458,8 +2458,7 @@ def magnet_cylinder_segment_field( (close(r, r1) | close(r, r2)) & mask_phi_in & mask_z_in ) # in / out mask_surf_phi = (mask_phi1 | mask_phi2) & mask_r_in & mask_z_in # in / out - mask_on_surface = mask_surf_z | mask_surf_r | mask_surf_phi - mask_not_on_surf = ~mask_on_surface + mask_not_on_surf = ~(mask_surf_z | mask_surf_r | mask_surf_phi) # inside mask_inside = mask_r_in & mask_phi_in & mask_z_in @@ -2469,9 +2468,8 @@ def magnet_cylinder_segment_field( # return 0 when all points are on surface -------------------------------- if not np.any(mask_not_on_surf): - return BHfinal + return H_all - H = None if field in "BH": # redefine input if there are some surface-points ------------------------- pol = polarization[mask_not_on_surf] @@ -2491,11 +2489,12 @@ def magnet_cylinder_segment_field( Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 + H_all[mask_not_on_surf] = H return convert_HBMJ( output_field_type=field, polarization=polarization, input_field_type="H", - field_values=H, + field_values=H_all, mask_inside=mask_inside & mask_not_on_surf, ) From 33abcae879d58c1f1469438578d9ca660d8f8963 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:47:27 +0100 Subject: [PATCH 120/240] Collection docstrings fixes SI --- magpylib/_src/obj_classes/class_Collection.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 5f01682e4..94ef0e938 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -519,7 +519,9 @@ def _validate_getBH_inputs(self, *inputs): return sources, sensors def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute B-field in tesla for given sources and observers. + """Compute B-field for given sources and observers. + + SI units are used for all inputs and outputs. Parameters ---------- @@ -546,10 +548,10 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - B-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of tesla. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be - considered as static beyond their end. + B-field at each path position ( index m) for each sensor (index k) and each + sensor pixel position (indices n1,n2,...) in units of T. Sensor pixel positions + are equivalent to simple observer positions. Paths of objects that are shorter + than index m are considered as static beyond their end. Examples -------- @@ -590,7 +592,9 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): ) def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): - """Compute H-field in A/m for given sources and observers. + """Compute H-field for given sources and observers. + + SI units are used for all inputs and outputs. Parameters ---------- @@ -617,10 +621,10 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): Returns ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - H-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1,n2,...) in units of A/m. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be - considered as static beyond their end. + H-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indeices n1,n2,...) in units of A/m. Sensor pixel positions are + equivalent to simple observer positions. Paths of objects that are shorter than + index m are considered as static beyond their end. Examples -------- @@ -690,6 +694,8 @@ class Collection(BaseGeo, BaseCollection): functions like a single source. When the collection contains sensors it functions like a list of all its sensors. + SI units are used for all inputs and outputs. + Parameters ---------- children: sources, `Sensor` or `Collection` objects From 6f5f3bcad017327aedeeba6bc366e8cedbd20402 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 00:49:35 +0100 Subject: [PATCH 121/240] SI fix circle class --- magpylib/_src/obj_classes/class_current_Circle.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 932db7666..432c96d6d 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -16,7 +16,9 @@ class Circle(BaseCurrent): When `position=(0,0,0)` and `orientation=None` the current loop lies in the x-y plane of the global coordinate system, with its center in - the origin. The Circle class has a dipole moment of pi**2/10*diameter**2*current. + the origin. + + SI units are used for all inputs and outputs. Parameters ---------- @@ -48,8 +50,8 @@ class Circle(BaseCurrent): Examples -------- - `Circle` objects are magnetic field sources. In this example we compute the H-field A/m - of such a current loop with 100 A current and a diameter of 2 meter at the observer position + `Circle` objects are magnetic field sources. In this example we compute the H-field in A/m + of such a current loop with 100 A current and a diameter of 2 meters at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy From fe2952f6f02df167efd634e5f1f2797280400337 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 01:12:58 +0100 Subject: [PATCH 122/240] docstrings to SI --- magpylib/_src/obj_classes/class_Sensor.py | 23 +++++++++++-------- .../obj_classes/class_current_Polyline.py | 6 +++-- .../_src/obj_classes/class_magnet_Cuboid.py | 8 +++---- .../_src/obj_classes/class_magnet_Cylinder.py | 2 ++ .../class_magnet_CylinderSegment.py | 6 +++-- .../_src/obj_classes/class_magnet_Sphere.py | 4 +++- .../obj_classes/class_magnet_Tetrahedron.py | 2 ++ .../class_magnet_TriangularMesh.py | 15 ++++++++++-- .../obj_classes/class_misc_CustomSource.py | 6 +++-- .../_src/obj_classes/class_misc_Dipole.py | 8 ++++--- .../_src/obj_classes/class_misc_Triangle.py | 2 ++ 11 files changed, 56 insertions(+), 26 deletions(-) diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 13e153fa7..c060b9604 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -19,7 +19,10 @@ class Sensor(BaseGeo, BaseDisplayRepr): When `position=(0,0,0)` and `orientation=None` local object coordinates coincide with the global coordinate system. - A sensor is made up of pixel (sensing elements) where the magnetic field is evaluated. + A sensor is made up of pixel (sensing elements / positions) where the magnetic + field is evaluated. + + SI units are used for all inputs and outputs. Parameters ---------- @@ -140,7 +143,7 @@ def handedness(self, val): def getB( self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray" ): - """Compute the B-field in units of tesla as seen by the sensor. + """Compute the B-field in units of T as seen by the sensor. Parameters ---------- @@ -168,14 +171,14 @@ def getB( Returns ------- B-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame - B-field of each source (l) at each path position (m) and each sensor pixel - position (n1,n2,...) in units of tesla. Paths of objects that are shorter than - m will be considered as static beyond their end. + B-field of each source (index l) at each path position (index m) and each sensor pixel + position (indices n1,n2,...) in units of T. Paths of objects that are shorter than + index m are considered as static beyond their end. Examples -------- Sensors are observers for magnetic field computation. In this example we compute the - B-field in units of tesla as seen by the sensor in the center of a circular current loop: + B-field in T as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() @@ -243,14 +246,14 @@ def getH( Returns ------- H-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame - H-field of each source (l) at each path position (m) and each sensor pixel - position (n1,n2,...) in units of A/m. Paths of objects that are shorter than - m will be considered as static beyond their end. + H-field of each source (index l) at each path position (index m) and each sensor pixel + position (indices n1,n2,...) in units of A/m. Paths of objects that are shorter than + index m are considered as static beyond their end. Examples -------- Sensors are observers for magnetic field computation. In this example we compute the - B-field in units of tesla as seen by the sensor in the center of a circular current loop: + B-field in T as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() diff --git a/magpylib/_src/obj_classes/class_current_Polyline.py b/magpylib/_src/obj_classes/class_current_Polyline.py index 5c1cf80d7..593643e30 100644 --- a/magpylib/_src/obj_classes/class_current_Polyline.py +++ b/magpylib/_src/obj_classes/class_current_Polyline.py @@ -10,13 +10,15 @@ class Polyline(BaseCurrent): - """Current flowing in straight lines from vertex to vertex. + """Line current flowing in straight paths from vertex to vertex. Can be used as `sources` input for magnetic field computation. The vertex positions are defined in the local object coordinates (rotate with object). When `position=(0,0,0)` and `orientation=None` global and local coordinates coincide. + SI units are used for all inputs and outputs. + Parameters ---------- current: float, default=`None` @@ -49,7 +51,7 @@ class Polyline(BaseCurrent): Examples -------- - `Polyline` objects are magnetic field sources. In this example we compute the H-field A/m + `Polyline` objects are magnetic field sources. In this example we compute the H-field in A/m of a square-shaped line-current with 1 A current at the observer position (1,1,1) given in units of meter: diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 9cffbe3a6..79fc63218 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -15,12 +15,12 @@ class Cuboid(BaseMagnet): to the global coordinate basis vectors and the geometric center of the Cuboid is located in the origin. - Units .... Min = Bout, Jin = Hout, length units arbitrary + SI units are used for all inputs and outputs. Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in meter. + Object position(s) in the global coordinates in units of meter. For m>1, the `position` and `orientation` attributes together represent an object path. @@ -54,8 +54,8 @@ class Cuboid(BaseMagnet): Examples -------- `Cuboid` magnets are magnetic field sources. Below we compute the H-field in A/m of a - cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of tesla and 0.01 m sides - at the observer position (0.01,0.01,0.01) given in units of meter: + cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of tesla and + 0.01 meter sides at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(.01,.01,.01)) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index a523f1a40..74f7bece6 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -15,6 +15,8 @@ class Cylinder(BaseMagnet): cylinder lies in the origin of the global coordinate system and the cylinder axis coincides with the global z-axis. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index 9f88a3846..d6b3a1d4b 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -20,6 +20,8 @@ class CylinderSegment(BaseMagnet): the cylinder axis coincides with the global z-axis. Section angle 0 corresponds to an x-z plane section of the cylinder. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` @@ -65,8 +67,8 @@ class CylinderSegment(BaseMagnet): Examples -------- `CylinderSegment` magnets are magnetic field sources. In this example we compute the - H-field A/m of such a cylinder segment magnet with polarization (.1,.2,.3) - in units of tesla, inner radius 0.01 meter, outer radius 0.02 m, height 0.01 meter, and + H-field in A/m of such a cylinder segment magnet with polarization (.1,.2,.3) + in units of tesla, inner radius 0.01 meter, outer radius 0.02 meter, height 0.01 meter, and section angles 0 and 45 deg at the observer position (0.02,0.02,0.02) in units of meter: >>> import magpylib as magpy diff --git a/magpylib/_src/obj_classes/class_magnet_Sphere.py b/magpylib/_src/obj_classes/class_magnet_Sphere.py index df83f6b02..b5bbc5fac 100644 --- a/magpylib/_src/obj_classes/class_magnet_Sphere.py +++ b/magpylib/_src/obj_classes/class_magnet_Sphere.py @@ -14,6 +14,8 @@ class Sphere(BaseMagnet): When `position=(0,0,0)` and `orientation=None` the sphere center is located in the origin of the global coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` @@ -50,7 +52,7 @@ class Sphere(BaseMagnet): Examples -------- - `Sphere` objects are magnetic field sources. In this example we compute the H-field A/m + `Sphere` objects are magnetic field sources. In this example we compute the H-field in A/m of a spherical magnet with polarization (0.1,0.2,0.3) in units of tesla and diameter of 0.01 meter at the observer position (0.01,0.01,0.01) given in units of meter: diff --git a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py index 73a8d2171..8ec3cd467 100644 --- a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py +++ b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py @@ -17,6 +17,8 @@ class Tetrahedron(BaseMagnet): is determined by its vertices and. It is not necessarily located in the origin an can be computed with the barycenter property. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 8bc20d521..5eadc7c2e 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -28,9 +28,12 @@ class TriangularMesh(BaseMagnet): """Magnet with homogeneous magnetization defined by triangular surface mesh. Can be used as `sources` input for magnetic field computation. + When `position=(0,0,0)` and `orientation=None` the TriangularMesh vertices are the same as in the global coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` @@ -99,8 +102,8 @@ class TriangularMesh(BaseMagnet): Examples -------- - We compute the B-field in units of tesla of a triangular mesh (4 vertices, 4 faces) - with polarization (0.1,0.2,0.3) in units of tesla at the observer position + We compute the B-field in units of T of a triangular mesh (4 vertices, 4 faces) + with polarization (0.1,0.2,0.3) in units of T at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy @@ -538,6 +541,8 @@ def from_ConvexHull( ): """Create a TriangularMesh magnet from a point cloud via its convex hull. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) @@ -629,6 +634,8 @@ def from_pyvista( ): """Create a TriangularMesh magnet from a pyvista PolyData mesh object. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) @@ -737,6 +744,8 @@ def from_triangles( ): """Create a TriangularMesh magnet from a list or Collection of Triangle objects. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) @@ -843,6 +852,8 @@ def from_mesh( ): """Create a TriangularMesh magnet from a mesh input. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index 2adfec59b..f49c399e2 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -10,12 +10,14 @@ class CustomSource(BaseSource): When `position=(0,0,0)` and `orientation=None` local object coordinates coincide with the global coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- field_func: callable, default=`None` The function for B- and H-field computation must have the two positional arguments `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units - of or A/m must be returned respectively. The `observers` argument must + of T or A/m must be returned respectively. The `observers` argument must accept numpy ndarray inputs of shape (n,3), in which case the returned fields must be numpy ndarrays of shape (n,3) themselves. @@ -44,7 +46,7 @@ class CustomSource(BaseSource): With version 4 `CustomSource` objects enable users to define their own source objects, and to embedded them in the Magpylib object oriented interface. In this example we create a source that generates a constant field and evaluate the field at observer - position (0.01,0.01,0.01) given in meter: + position (0.01,0.01,0.01) given in meters: >>> import numpy as np >>> import magpylib as magpy diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index e1a31ea8d..b92dd4c9c 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -17,6 +17,8 @@ class Dipole(BaseSource): When `position=(0,0,0)` and `orientation=None` the dipole is located in the origin of global coordinate system. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` @@ -30,8 +32,8 @@ class Dipole(BaseSource): moment: array_like, shape (3,), unit A・m², default=`None` Magnetic dipole moment in units of A・m² given in the local object coordinates. - For homogeneous magnets the relation moment=magnetization*volume holds. The dipole - moment of a Circle object is pi**2/10*diameter**2*current. + For homogeneous magnets the relation moment=magnetization*volume holds. For + current loops the relation moment = current*loop_surface holds. parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -46,7 +48,7 @@ class Dipole(BaseSource): Examples -------- - `Dipole` objects are magnetic field sources. In this example we compute the H-field A/m + `Dipole` objects are magnetic field sources. In this example we compute the H-field in A/m of such a magnetic dipole with a moment of (100,100,100) in units of A・m² at an observer position (.01,.01,.01) given in units of meter: diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index 30e28e1e3..47746d9d3 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -18,6 +18,8 @@ class Triangle(BaseMagnet): Triangle vertices coincide with the global coordinate system. The geometric center of the Triangle is determined by its vertices. + SI units are used for all inputs and outputs. + Parameters ---------- position: array_like, shape (3,) or (m,3) From 0993fa62ee2780571eed514fca5cbd2d5dba77b9 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 26 Dec 2023 01:21:55 +0100 Subject: [PATCH 123/240] field wrap SI units fix --- magpylib/_src/fields/field_wrap_BH.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 0b009c900..04d3d7058 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -652,13 +652,13 @@ def getB( Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - B-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1, n2, ...) in units of tesla. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be + B-field at each path position (index m) for each sensor (index k) and each sensor pixel + position (indices n1, n2, ...) in units of T. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. Functional interface: ndarray, shape (n,3) - B-field for every parameter set in units of tesla. + B-field for every parameter set in units of T. Notes ----- @@ -668,7 +668,7 @@ def getB( Examples -------- - In this example we compute the B-field in units of tesla of a spherical magnet and a current + In this example we compute the B-field in T of a spherical magnet and a current loop at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy @@ -735,7 +735,7 @@ def getH( output="ndarray", **kwargs, ): - """Compute H-field in A/m for given sources and observers. + """Compute H-field in units of A/m for given sources and observers. Field implementations can be directly accessed (avoiding the object oriented Magpylib interface) by providing a string input `sources=source_type`, array_like @@ -823,9 +823,9 @@ def getH( Returns ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - H-field at each path position (m) for each sensor (k) and each sensor pixel - position (n1, n2, ...) in units of A/m. Sensor pixel positions are equivalent - to simple observer positions. Paths of objects that are shorter than m will be + H-field at each path position (index m) for each sensor (index k) and each sensor pixel + position (indices n1, n2, ...) in units of A/m. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. Functional interface: ndarray, shape (n,3) @@ -839,7 +839,7 @@ def getH( Examples -------- - In this example we compute the H-field A/m of a spherical magnet and a current loop + In this example we compute the H-field in A/m of a spherical magnet and a current loop at the observer position (0.01,0.01,0.01) given in units of meter: >>> import magpylib as magpy From 1559c1b3748cf60a485f97f5e9da45adaea504fb Mon Sep 17 00:00:00 2001 From: mortner Date: Wed, 27 Dec 2023 16:20:35 +0100 Subject: [PATCH 124/240] test bugfix include now fix #279 (cylinder bug) --- tests/test_core_physics_consistency.py | 84 +++++++++++++++++--------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 6d41067aa..67a3afe86 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -11,8 +11,9 @@ # Magnetic moment of a homogeneous magnet with magnetization mag and volume vol # mom = vol * mag # -# Current density j on magnet surface in the replacement picture -# j = mag = J/MU0 +# Current replacement picture: A magnet generates a similar field as a current sheet +# on its surface with current density j = M = J/MU0. Such a current generates +# the same B-field. The H-field generated by is H-M! # # Geometric approximation testing should give similar results for different # implementations when one geometry is constructed from another @@ -200,34 +201,63 @@ def test_core_physics_dipole_sphere(): # -> Circle, Cylinder def test_core_physics_long_solenoid(): """ - test if field from solenoid converges to long-solenoid field - Hz = I*N/L "in the center" + Test if field from solenoid converges to long-solenoid field in the center + Bz_long = MU0*I*N/L + Hz_long = I*N/L + I = current, N=windings, L=length, holds true if L >> radius R + + This can also be tested with magnets using the current replacement picture + where Jz = MU0 * I * N / L, and holds for B and for H-M. """ - I = 1 - N = 10000 - R = 0.8 - L = 100 - Hz_long = N * I / L - # test with solenoid constructed from circle fields - H = magpy.core.current_circle_field( - field="H", - observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), - diameter=np.array([2 * R] * N), - current=np.array([I] * N), - ) - Hz_sol = np.sum(H, axis=0)[2] - np.testing.assert_allclose(Hz_sol, Hz_long, rtol=1e-3) + I = 134 + N = 5000 + R = 1.543 + L = 1234 + + for field in ["B", "H"]: + BHz_long = N * I / L + if field == "B": + BHz_long *= MU0 + + # SOLENOID TEST constructed from circle fields + BH = magpy.core.current_circle_field( + field=field, + observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), + diameter=np.array([2 * R] * N), + current=np.array([I] * N), + ) + BH_sol = np.sum(BH, axis=0)[2] - # test with cylinder field (using current replacement) - Jz = MU0 * I * N / L # polarization <> current density - Hz_cyl = magpy.core.magnet_cylinder_field( - field="H", - observers=np.array([(0, 0, 0)]), - dimension=np.array([(2 * R, L)]), - polarization=np.array([(0, 0, Jz)]), - )[0, 2] - np.testing.assert_allclose(Hz_cyl, Hz_long, rtol=1e-3) + np.testing.assert_allclose(BHz_long, BH_sol, rtol=1e-3) + + # MAGNET TEST using the current replacement picture + Mz = I * N / L + Jz = Mz * MU0 + pol = np.array([(0, 0, Jz)]) + obs = np.array([(0, 0, 0)]) + + # cylinder + BHz_cyl = magpy.core.magnet_cylinder_field( + field=field, + observers=obs, + dimension=np.array([(2 * R, L)]), + polarization=pol, + )[0, 2] + # if field=='H': # UNCOMMENT THIS WHEN FIX #703 IS MERGED !!!! + # BHz_cyl += Mz + np.testing.assert_allclose(BHz_long, BHz_cyl, rtol=1e-5) + + # cuboid + BHz_cub = magpy.core.magnet_cuboid_field( + field=field, + observers=obs, + dimension=np.array([(2 * R, 2 * R, L)]), + polarization=pol, + )[0, 2] + if field == "H": + BHz_cub += Mz + np.testing.assert_allclose(BHz_long, BHz_cub, rtol=1e-5) # Circle<>Cylinder From b356f827c896b9f4e5996448bbc7aeb6d970ca5d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Wed, 27 Dec 2023 21:22:58 +0100 Subject: [PATCH 125/240] update --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9669e8f1a..02359e491 100644 --- a/README.md +++ b/README.md @@ -47,59 +47,59 @@ Magpylib supports _Python3.8+_ and relies on common scientific computation libra # Quickstart -Here is an example how to use Magpylib. +Here is an example on how to use Magpylib. ```python3 import magpylib as magpy -# Create a Cuboid magnet with sides 1,2 and 3 mm respectively, and magnetization -# (polarization) of 1000 mT pointing in x-direction. +# Create a Cuboid magnet with sides 1,2 and 3 cm respectively, and polarization +# 1000 mT pointing in x-direction. cube = magpy.magnet.Cuboid( - magnetization=(1000,0,0), - dimension=(1,2,3), + polarization=(1, 0, 0), # in SI Units (T) + dimension=(0.01, 0.02, 0.03), # in SI Units (m) ) # By default, the magnet position is (0,0,0) and its orientation is the unit # rotation (given by a scipy rotation object), which corresponds to magnet sided # parallel to global coordinate axes. -print(cube.position) # --> [0. 0. 0.] -print(cube.orientation.as_rotvec()) # --> [0. 0. 0.] +print(cube.position) # --> [0. 0. 0.] +print(cube.orientation.as_rotvec()) # --> [0. 0. 0.] # Manipulate object position and orientation through the respective attributes, # or by using the powerful `move` and `rotate` methods. -cube.move((0,0,-2)) -cube.rotate_from_angax(angle=45, axis='z') -print(cube.position) # --> [0. 0. -2.] -print(cube.orientation.as_rotvec(degrees=True)) # --> [0. 0. 45.] +cube.move((0, 0, -0.02)) # in SI Units (m) +cube.rotate_from_angax(angle=45, axis="z") +print(cube.position) # --> [0. 0. -0.02] +print(cube.orientation.as_rotvec(degrees=True)) # --> [0. 0. 45.] # Compute the magnetic field in units of mT at a set of observer positions. Magpylib # makes use of vectorized computation. Hand over all field computation instances, # e.g. different observer positions, at one funtion call. Avoid Python loops !!! -observers = [(0,0,0), (1,0,0), (2,0,0)] +observers = [(0, 0, 0), (0.01, 0, 0), (0.02, 0, 0)] # in SI Units (m) B = magpy.getB(cube, observers) -print(B.round()) # --> [[-91. -91. 0.] - # [ 1. -38. 84.] - # [ 18. -14. 26.]] +print(B.round()) # --> [[-91. -91. 0.] +# [ 1. -38. 84.] +# [ 18. -14. 26.]] # Sensors are observer objects that can have their own position and orientation. -# Compute the H-field in units of kA/m. -sensor = magpy.Sensor(position=(0,0,0)) -sensor.rotate_from_angax(angle=45, axis=(1,1,1)) +# Compute the H-field in units of A/m. +sensor = magpy.Sensor(position=(0, 0, 0)) +sensor.rotate_from_angax(angle=45, axis=(1, 1, 1)) H = magpy.getH(cube, sensor) -print(H.round()) # --> [-95. -36. -14.] +print(H.round()) # --> [-95. -36. -14.] # Position and orientation attributes of Magpylib objects can be vectors of # multiple positions/orientations refered to as "paths". When computing the # magnetic field of an object with a path, it is computed at every path index. -cube.position = [(0,0,-2), (1,0,-2), (2,0,-2)] +cube.position = [(0, 0, -.02), (1, 0, -.02), (2, 0, -.02)] # in SI Units (m) B = cube.getB(sensor) -print(B.round()) # --> [[-119. -45. -18.] - # [ 8. -73. -55.] - # [ 15. -30. -8.]] +print(B.round()) # --> [[-119. -45. -18.] +# [ 8. -73. -55.] +# [ 15. -30. -8.]] # When several objects are involved and things are getting complex, make use of # the `show` function to view your system through Matplotlib, Plotly or Pyvista backends. -magpy.show(cube, sensor, backend='pyvista') +magpy.show(cube, sensor, backend="pyvista") ``` More details and other important features are described in detail in the **[Documentation](https://magpylib.readthedocs.io/en/latest)**. Key features are: From 1363385718d652491f449da9ff3cda7fbf370151 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 27 Dec 2023 22:37:11 +0100 Subject: [PATCH 126/240] update Readme --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 02359e491..ded077e29 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ +> [!WARNING] +> Version 5 introduces important breaking changes like **the move to SI units** among others. We recommended to pin your dependencies to magpylib>=4.5<5 until you are ready to migrate to the latest version! ([see details](https://github.com/magpylib/magpylib/discussions/647)) +

@@ -67,35 +70,35 @@ print(cube.orientation.as_rotvec()) # --> [0. 0. 0.] # Manipulate object position and orientation through the respective attributes, # or by using the powerful `move` and `rotate` methods. -cube.move((0, 0, -0.02)) # in SI Units (m) +cube.move((0, 0, -0.02))# in SI Units (m) cube.rotate_from_angax(angle=45, axis="z") print(cube.position) # --> [0. 0. -0.02] print(cube.orientation.as_rotvec(degrees=True)) # --> [0. 0. 45.] -# Compute the magnetic field in units of mT at a set of observer positions. Magpylib +# Compute the magnetic field in units of T at a set of observer positions. Magpylib # makes use of vectorized computation. Hand over all field computation instances, # e.g. different observer positions, at one funtion call. Avoid Python loops !!! observers = [(0, 0, 0), (0.01, 0, 0), (0.02, 0, 0)] # in SI Units (m) B = magpy.getB(cube, observers) -print(B.round()) # --> [[-91. -91. 0.] -# [ 1. -38. 84.] -# [ 18. -14. 26.]] +print(B.round(2)) # --> [[-0.09 -0.09 0. ] +# [ 0. -0.04 0.08] +# [ 0.02 -0.01 0.03]] # in SI Units (T) # Sensors are observer objects that can have their own position and orientation. # Compute the H-field in units of A/m. sensor = magpy.Sensor(position=(0, 0, 0)) sensor.rotate_from_angax(angle=45, axis=(1, 1, 1)) H = magpy.getH(cube, sensor) -print(H.round()) # --> [-95. -36. -14.] +print(H.round()) # --> [-94537. -35642. -14085.] # in SI Units (A/m) # Position and orientation attributes of Magpylib objects can be vectors of # multiple positions/orientations refered to as "paths". When computing the # magnetic field of an object with a path, it is computed at every path index. cube.position = [(0, 0, -.02), (1, 0, -.02), (2, 0, -.02)] # in SI Units (m) B = cube.getB(sensor) -print(B.round()) # --> [[-119. -45. -18.] -# [ 8. -73. -55.] -# [ 15. -30. -8.]] +print(B.round(2)) # --> [[-0.12 -0.04 -0.02] +# [ 0. -0. 0. ] +# [ 0. -0. 0. ]] # in SI Units (T) # When several objects are involved and things are getting complex, make use of # the `show` function to view your system through Matplotlib, Plotly or Pyvista backends. From ae4dbc01391cfccecfcefc691c335aaeb0f9ab24 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 28 Dec 2023 16:43:59 +0100 Subject: [PATCH 127/240] rework readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 62f109468..3212d5707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > [!WARNING] -> Version 5 introduces important breaking changes like **the move to SI units** among others. We recommended to pin your dependencies to magpylib>=4.5<5 until you are ready to migrate to the latest version! ([see details](https://github.com/magpylib/magpylib/discussions/647)) +> Version 5 introduces critical breaking changes with, among others, the _move to SI units_. We recommended to pin your dependencies to `magpylib>=4.5<5` until you are ready to migrate to the latest version! ([see details](https://github.com/magpylib/magpylib/discussions/647))

@@ -52,11 +52,11 @@ Magpylib supports _Python3.8+_ and relies on common scientific computation libra Here is an example on how to use Magpylib. -```python3 +```python import magpylib as magpy -# Create a Cuboid magnet with sides 1,2 and 3 cm respectively, and polarization -# 1000 mT pointing in x-direction. +# Create a Cuboid magnet with sides 1,2 and 3 cm respectively, and a polarization +# of 1000 mT pointing in x-direction. cube = magpy.magnet.Cuboid( polarization=(1, 0, 0), # in SI Units (T) dimension=(0.01, 0.02, 0.03), # in SI Units (m) @@ -75,7 +75,7 @@ cube.rotate_from_angax(angle=45, axis="z") print(cube.position) # --> [0. 0. -0.02] print(cube.orientation.as_rotvec(degrees=True)) # --> [0. 0. 45.] -# Compute the magnetic field in units of T at a set of observer positions. Magpylib +# Compute the magnetic B-field in units of T at a set of observer positions. Magpylib # makes use of vectorized computation. Hand over all field computation instances, # e.g. different observer positions, at one funtion call. Avoid Python loops !!! observers = [(0, 0, 0), (0.01, 0, 0), (0.02, 0, 0)] # in SI Units (m) From 86a0d977f0b68fe7f631fd9a5f78fab4c20db757 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 28 Dec 2023 16:49:16 +0100 Subject: [PATCH 128/240] fix test --- tests/test_field_cylinder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index 2df6f785c..8aa13eb5c 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -494,12 +494,12 @@ def test_cyl_vs_cylseg_axial_H_inside_mask(): field=field, observers=obs, dimension=dims, - magnetization=pols, + polarization=pols, ) Bcs = magpy.core.magnet_cylinder_segment_field( field=field, observers=obs, dimension=dims_cs, - magnetization=pols, + polarization=pols, ) np.testing.assert_allclose(Bc, Bcs) From d1f0a8461b1a52b56fb0caf6cac9f99c3a907382 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 28 Dec 2023 21:21:50 +0100 Subject: [PATCH 129/240] fix test --- tests/test_core_physics_consistency.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 67a3afe86..610d90c72 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -244,8 +244,8 @@ def test_core_physics_long_solenoid(): dimension=np.array([(2 * R, L)]), polarization=pol, )[0, 2] - # if field=='H': # UNCOMMENT THIS WHEN FIX #703 IS MERGED !!!! - # BHz_cyl += Mz + if field == "H": + BHz_cyl += Mz np.testing.assert_allclose(BHz_long, BHz_cyl, rtol=1e-5) # cuboid From 4933b2dc9b5895fd8b2b7649699b8ee4bfd6e0e5 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 28 Dec 2023 22:02:12 +0100 Subject: [PATCH 130/240] replace units to SI in docstrings (code only) --- magpylib/_src/display/backend_matplotlib.py | 2 +- magpylib/_src/display/backend_plotly.py | 2 +- magpylib/_src/display/traces_base.py | 4 +-- magpylib/_src/fields/field_BH_cylinder.py | 12 +++---- .../_src/fields/field_BH_cylinder_segment.py | 8 ++--- magpylib/_src/fields/field_BH_polyline.py | 6 ++-- magpylib/_src/fields/field_wrap_BH.py | 32 +++++++++---------- .../_src/obj_classes/class_BaseExcitations.py | 4 +-- magpylib/_src/obj_classes/class_BaseGeo.py | 2 +- .../_src/obj_classes/class_BaseTransform.py | 14 ++++---- magpylib/_src/obj_classes/class_Collection.py | 2 +- magpylib/_src/obj_classes/class_Sensor.py | 8 ++--- .../_src/obj_classes/class_current_Circle.py | 8 ++--- .../obj_classes/class_current_Polyline.py | 8 ++--- .../_src/obj_classes/class_magnet_Cuboid.py | 6 ++-- .../_src/obj_classes/class_magnet_Cylinder.py | 12 +++---- .../class_magnet_CylinderSegment.py | 14 ++++---- .../_src/obj_classes/class_magnet_Sphere.py | 10 +++--- .../obj_classes/class_magnet_Tetrahedron.py | 10 +++--- .../class_magnet_TriangularMesh.py | 14 ++++---- .../obj_classes/class_misc_CustomSource.py | 2 +- .../_src/obj_classes/class_misc_Dipole.py | 4 +-- .../_src/obj_classes/class_misc_Triangle.py | 6 ++-- magpylib/_src/style.py | 12 +++---- tests/test_physics_consistency.py | 6 ++-- 25 files changed, 104 insertions(+), 104 deletions(-) diff --git a/magpylib/_src/display/backend_matplotlib.py b/magpylib/_src/display/backend_matplotlib.py index 6428717ca..9fe593649 100644 --- a/magpylib/_src/display/backend_matplotlib.py +++ b/magpylib/_src/display/backend_matplotlib.py @@ -352,7 +352,7 @@ def draw_frame(frame_ind): count = count_with_labels.get(row_col_num, 0) if ax.name == "3d": ax.set( - **{f"{k}label": f"{k} (mm)" for k in "xyz"}, + **{f"{k}label": f"{k} (m)" for k in "xyz"}, **{f"{k}lim": r for k, r in zip("xyz", ranges)}, ) ax.set_box_aspect(aspect=(1, 1, 1)) diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index 6243dc9c3..adbe6c28e 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -94,7 +94,7 @@ def apply_fig_ranges(fig, ranges, apply2d=True): """ fig.update_scenes( **{ - f"{k}axis": {"range": ranges[i], "autorange": False, "title": f"{k} (mm)"} + f"{k}axis": {"range": ranges[i], "autorange": False, "title": f"{k} (m)"} for i, k in enumerate("xyz") }, aspectratio={k: 1 for k in "xyz"}, diff --git a/magpylib/_src/display/traces_base.py b/magpylib/_src/display/traces_base.py index 61444bd27..0890cacdf 100644 --- a/magpylib/_src/display/traces_base.py +++ b/magpylib/_src/display/traces_base.py @@ -307,9 +307,9 @@ def make_CylinderSegment( dimension: array_like, shape (5,), default=`None` Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) - where r1 list: Parameters ---------- dim: ndarray, shape (n,2) - dimension of cylinder (d, h), diameter and height, in units of mm + dimension of cylinder (d, h), diameter and height, in units of m pos_obs: ndarray, shape (n,2) - position of observer (r,z) in cylindrical coordinates in units of mm + position of observer (r,z) in cylindrical coordinates in units of m Returns ------- B-field: ndarray - B-field array of shape (n,2) in cylindrical coordinates (Br,Bz) in units of mT. + B-field array of shape (n,2) in cylindrical coordinates (Br,Bz) in units of T. """ n = len(z0) @@ -86,16 +86,16 @@ def fieldH_cylinder_diametral( Parameters ---------- dim: ndarray, shape (n,2) - dimension of cylinder (d, h), diameter and height, in units of mm + dimension of cylinder (d, h), diameter and height, in units of m tetta: ndarray, shape (n,) angle between magnetization vector and x-axis in [rad]. M = (cos(tetta), sin(tetta), 0) obs_pos: ndarray, shape (n,3) - position of observer (r,phi,z) in cylindrical coordinates in units of mm and rad + position of observer (r,phi,z) in cylindrical coordinates in units of m and rad Returns ------- H-field: ndarray - H-field array of shape (n,3) in cylindrical coordinates (Hr, Hphi, Hz) in units of kA/m. + H-field array of shape (n,3) in cylindrical coordinates (Hr, Hphi, Hz) in units of A/m. """ # pylint: disable=too-many-statements diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 0ffc43743..f3f8cf9ce 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2141,16 +2141,16 @@ def magnet_cylinder_segment_core( Parameters ---------- mag: ndarray, shape (n,3) - magnetization vector (M, phi, th) in spherical CS, units: mT, rad + magnetization vector (M, phi, th) in spherical CS, units: T, rad obs_pos : ndarray, shape (n,3) - observer positions (r,phi,z) in cy CS, units: mm rad + observer positions (r,phi,z) in cy CS, units: m, rad dim: ndarray, shape (n,6) - segment dimension (r1,r2,phi1,phi2,z1,z2) in cy CS , units: mm, rad + segment dimension (r1,r2,phi1,phi2,z1,z2) in cy CS , units: m, rad Returns ------- H-field: ndarray - H-field in cylindrical coordinates (Hr, Hphi, Hz), shape (n,3) in units of kA/m. + H-field in cylindrical coordinates (Hr, Hphi, Hz), shape (n,3) in units of A/m. Notes ----- diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 31c01df36..2ca9bb7ab 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -28,10 +28,10 @@ def current_vertices_field( - bh (boolean): True=B, False=H - current (ndarray n): current on line in units of A - vertex_sets (list of len n): n vertex sets (each of shape (mi,3)) - - pos_obs (ndarray nx3): n observer positions in units of mm + - pos_obs (ndarray nx3): n observer positions in units of m ### Returns: - - B-field (ndarray nx3): B-field vectors at pos_obs in units of mT + - B-field (ndarray nx3): B-field vectors at pos_obs in units of T """ if vertices is None: return current_polyline_field( @@ -220,7 +220,7 @@ def current_polyline_field( mask4 = ~mask2 * ~mask3 deltaSin[mask4] = abs(sinTh1[mask4] + sinTh2[mask4]) - field = (deltaSin / norm_o4 * eB.T / norm_12 * current * 1e-7).T # m->mm, T->mT + field = (deltaSin / norm_o4 * eB.T / norm_12 * current * 1e-7).T # broadcast general case results into allocated vector mask0[~mask0] = mask1 diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 04d3d7058..71ab93124 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -577,10 +577,10 @@ def getB( Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of meter. + are given in units of m. Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding - positions to observer positions in units of meter. + positions to observer positions in units of m. sumup: bool, default=`False` If `True`, the fields of all sources are summed up. @@ -604,7 +604,7 @@ def getB( *Functional interface position: array_like, shape (3,) or (n,3), default=`(0,0,0)` - Source position(s) in the global coordinates in units of meter. + Source position(s) in the global coordinates in units of m. orientation: scipy `Rotation` object with length 1 or n, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -634,20 +634,20 @@ def getB( dimension: array_like, shape (x,) or (n,x) Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! - Magnet dimension input in units of meter and deg. Dimension format x of sources is similar + Magnet dimension input in units of m and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) Only source_type == `Sphere` or `Circle`! - Diameter of source in units of meter. + Diameter of source in units of m. segment_start: array_like, shape (n,3) Only source_type == `Polyline`! - Start positions of line current segments in units of meter. + Start positions of line current segments in units of m. segment_end: array_like, shape (n,3) Only source_type == `Polyline`! - End positions of line current segments in units of meter. + End positions of line current segments in units of m. Returns ------- @@ -669,7 +669,7 @@ def getB( Examples -------- In this example we compute the B-field in T of a spherical magnet and a current - loop at the observer position (0.01,0.01,0.01) given in units of meter: + loop at the observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src1 = magpy.current.Circle(current=100, diameter=.002) @@ -755,10 +755,10 @@ def getH( Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of meter. + are given in units of m. Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding - positions to observer positions in units of meter. + positions to observer positions in units of m. sumup: bool, default=`False` If `True`, the fields of all sources are summed up. @@ -782,7 +782,7 @@ def getH( *Functional interface position: array_like, shape (3,) or (n,3), default=`(0,0,0)` - Source position(s) in the global coordinates in units of meter. + Source position(s) in the global coordinates in units of m. orientation: scipy `Rotation` object with length 1 or n, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -805,20 +805,20 @@ def getH( dimension: array_like, shape (x,) or (n,x) Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! - Magnet dimension input in units of meter and deg. Dimension format x of sources is similar + Magnet dimension input in units of m and deg. Dimension format x of sources is similar as in object oriented interface. diameter: array_like, shape (n,) Only source_type == `Sphere` or `Circle`! - Diameter of source in units of meter. + Diameter of source in units of m. segment_start: array_like, shape (n,3) Only source_type == `Polyline`! - Start positions of line current segments in units of meter. + Start positions of line current segments in units of m. segment_end: array_like, shape (n,3) Only source_type == `Polyline`! - End positions of line current segments in units of meter. + End positions of line current segments in units of m. Returns ------- @@ -840,7 +840,7 @@ def getH( Examples -------- In this example we compute the H-field in A/m of a spherical magnet and a current loop - at the observer position (0.01,0.01,0.01) given in units of meter: + at the observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src1 = magpy.current.Circle(current=100, diameter=.002) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index d5ec5005c..d8af7d5fe 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -62,7 +62,7 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions are given - in units of meters. + in units of m. squeeze: bool, default=`True` If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. @@ -131,7 +131,7 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): Can be array_like positions of shape (n1, n2, ..., 3) where the field should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list of such sensor objects (must all have similar pixel shapes). All positions - are given in units of meters. + are given in units of m. squeeze: bool, default=`True` If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. diff --git a/magpylib/_src/obj_classes/class_BaseGeo.py b/magpylib/_src/obj_classes/class_BaseGeo.py index 1ee7ef6e1..59e7d1690 100644 --- a/magpylib/_src/obj_classes/class_BaseGeo.py +++ b/magpylib/_src/obj_classes/class_BaseGeo.py @@ -154,7 +154,7 @@ def parent(self, inp): @property def position(self): """ - Object position(s) in the global coordinates in units of mm. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. """ return np.squeeze(self._position) diff --git a/magpylib/_src/obj_classes/class_BaseTransform.py b/magpylib/_src/obj_classes/class_BaseTransform.py index 2feaef1e2..7e1ba5ce5 100644 --- a/magpylib/_src/obj_classes/class_BaseTransform.py +++ b/magpylib/_src/obj_classes/class_BaseTransform.py @@ -381,7 +381,7 @@ def rotate(self, rotation: R, anchor=None, start="auto"): as unit rotation. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -466,7 +466,7 @@ def rotate_from_angax(self, angle, axis, anchor=None, start="auto", degrees=True or a string 'x', 'y' or 'z' to denote respective directions. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -567,7 +567,7 @@ def rotate_from_rotvec(self, rotvec, anchor=None, start="auto", degrees=True): the rotation angle in units of rad. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -655,7 +655,7 @@ def rotate_from_euler(self, angle, seq, anchor=None, start="auto", degrees=True) rotations cannot be mixed in one function call. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -738,7 +738,7 @@ def rotate_from_matrix(self, matrix, anchor=None, start="auto"): Rotation input. See scipy.spatial.transform.Rotation for details. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -803,7 +803,7 @@ def rotate_from_mrp(self, mrp, anchor=None, start="auto"): Parameters (MRPs). anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. @@ -867,7 +867,7 @@ def rotate_from_quat(self, quat, anchor=None, start="auto"): Rotation input in quaternion form. anchor: `None`, `0` or array_like with shape (3,) or (n,3), default=`None` - The axis of rotation passes through the anchor point given in units of mm. + The axis of rotation passes through the anchor point given in units of m. By default (`anchor=None`) the object will rotate about its own center. `anchor=0` rotates the object about the origin `(0,0,0)`. diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 94ef0e938..113b2514b 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -711,7 +711,7 @@ class Collection(BaseGeo, BaseCollection): An ordered list of all collection objects in the collection. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index c060b9604..af97687f9 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -28,12 +28,12 @@ class Sensor(BaseGeo, BaseDisplayRepr): ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. pixel: array_like, shape (3,) or (n1,n2,...,3), default=`(0,0,0)` Sensor pixel (=sensing elements) positions in the local object coordinates - (rotate with object), in units of meter. + (rotate with object), in units of m. orientation: scipy `Rotation` object with length 1 or m, default=`None` Object orientation(s) in the global coordinates. `None` corresponds to @@ -57,7 +57,7 @@ class Sensor(BaseGeo, BaseDisplayRepr): Examples -------- `Sensor` objects are observers for magnetic field computation. In this example we compute the - B-field in units of tesla as seen by the sensor in the center of a circular current loop: + B-field in units of T as seen by the sensor in the center of a circular current loop: >>> import magpylib as magpy >>> sens = magpy.Sensor() @@ -109,7 +109,7 @@ def __init__( @property def pixel(self): """Sensor pixel (=sensing elements) positions in the local object coordinates - (rotate with object), in units of meter. + (rotate with object), in units of m. """ return self._pixel diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 432c96d6d..639713ed6 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -26,10 +26,10 @@ class Circle(BaseCurrent): Electrical current in units of A. diameter: float, default=`None` - Diameter of the loop in units of meter. + Diameter of the loop in units of m. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -52,7 +52,7 @@ class Circle(BaseCurrent): -------- `Circle` objects are magnetic field sources. In this example we compute the H-field in A/m of such a current loop with 100 A current and a diameter of 2 meters at the observer position - (0.01,0.01,0.01) given in units of meter: + (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src = magpy.current.Circle(current=100, diameter=2) @@ -106,7 +106,7 @@ def __init__( # property getters and setters @property def diameter(self): - """Diameter of the loop in units of meter.""" + """Diameter of the loop in units of m.""" return self._diameter @diameter.setter diff --git a/magpylib/_src/obj_classes/class_current_Polyline.py b/magpylib/_src/obj_classes/class_current_Polyline.py index 593643e30..733bba918 100644 --- a/magpylib/_src/obj_classes/class_current_Polyline.py +++ b/magpylib/_src/obj_classes/class_current_Polyline.py @@ -25,12 +25,12 @@ class Polyline(BaseCurrent): Electrical current in units of A. vertices: array_like, shape (n,3), default=`None` - The current flows along the vertices which are given in units of meter in the + The current flows along the vertices which are given in units of m in the local object coordinates (move/rotate with object). At least two vertices must be given. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -53,7 +53,7 @@ class Polyline(BaseCurrent): -------- `Polyline` objects are magnetic field sources. In this example we compute the H-field in A/m of a square-shaped line-current with 1 A current at the observer position (1,1,1) given in - units of meter: + units of m: >>> import magpylib as magpy >>> src = magpy.current.Polyline( @@ -116,7 +116,7 @@ def __init__( @property def vertices(self): """ - The current flows along the vertices which are given in units of meter in the + The current flows along the vertices which are given in units of m in the local object coordinates (move/rotate with object). At least two vertices must be given. """ diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 79fc63218..2e3e4646d 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -20,7 +20,7 @@ class Cuboid(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. @@ -54,8 +54,8 @@ class Cuboid(BaseMagnet): Examples -------- `Cuboid` magnets are magnetic field sources. Below we compute the H-field in A/m of a - cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of tesla and - 0.01 meter sides at the observer position (0.01,0.01,0.01) given in units of meter: + cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of T and + 0.01 meter sides at the observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(.01,.01,.01)) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 74f7bece6..61b289334 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -20,7 +20,7 @@ class Cylinder(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -29,7 +29,7 @@ class Cylinder(BaseMagnet): together represent an object path. dimension: array_like, shape (2,), default=`None` - Dimension (d,h) denote diameter and height of the cylinder in units of meter. + Dimension (d,h) denote diameter and height of the cylinder in units of m. polarization: array_like, shape (3,), default=`None` Magnetic polarization vector J = mu0*M in units of T, @@ -53,8 +53,8 @@ class Cylinder(BaseMagnet): Examples -------- `Cylinder` magnets are magnetic field sources. Below we compute the H-field in A/m of a - cylinder magnet with polarization (.1,.2,.3) in units of tesla and 0.01 meter diameter and - height at the observer position (0.01,0.01,0.01) given in units of meter: + cylinder magnet with polarization (.1,.2,.3) in units of T and 0.01 meter diameter and + height at the observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src = magpy.magnet.Cylinder(polarization=(.1,.2,.3), dimension=(.01,.01)) @@ -110,12 +110,12 @@ def __init__( # property getters and setters @property def dimension(self): - """Dimension (d,h) denote diameter and height of the cylinder in units of meter.""" + """Dimension (d,h) denote diameter and height of the cylinder in units of m.""" return self._dimension @dimension.setter def dimension(self, dim): - """Set Cylinder dimension (d,h) in units of meter.""" + """Set Cylinder dimension (d,h) in units of m.""" self._dimension = check_format_input_vector( dim, dims=(1,), diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index d6b3a1d4b..e5445c6a4 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -25,7 +25,7 @@ class CylinderSegment(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -35,9 +35,9 @@ class CylinderSegment(BaseMagnet): dimension: array_like, shape (5,), default=`None` Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) - where r1>> import magpylib as magpy >>> src = magpy.magnet.CylinderSegment(polarization=(.1,.2,.3), dimension=(.01,.02,.01,0,45)) @@ -127,9 +127,9 @@ def __init__( def dimension(self): """ Dimension/Size of the cylinder segment of the form (r1, r2, h, phi1, phi2) - where r11, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -28,7 +28,7 @@ class Sphere(BaseMagnet): together represent an object path. diameter: float, default=`None` - Diameter of the sphere in units of meter. + Diameter of the sphere in units of m. polarization: array_like, shape (3,), default=`None` Magnetic polarization vector J = mu0*M in units of T, @@ -53,8 +53,8 @@ class Sphere(BaseMagnet): Examples -------- `Sphere` objects are magnetic field sources. In this example we compute the H-field in A/m - of a spherical magnet with polarization (0.1,0.2,0.3) in units of tesla and diameter - of 0.01 meter at the observer position (0.01,0.01,0.01) given in units of meter: + of a spherical magnet with polarization (0.1,0.2,0.3) in units of T and diameter + of 0.01 meter at the observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> src = magpy.magnet.Sphere(polarization=(.1,.2,.3), diameter=.01) @@ -110,7 +110,7 @@ def __init__( # property getters and setters @property def diameter(self): - """Diameter of the sphere in units of meter.""" + """Diameter of the sphere in units of m.""" return self._diameter @diameter.setter diff --git a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py index 8ec3cd467..66409c651 100644 --- a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py +++ b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py @@ -22,7 +22,7 @@ class Tetrahedron(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. When setting vertices, the initial position is set to the barycenter. @@ -63,9 +63,9 @@ class Tetrahedron(BaseMagnet): Examples -------- `Tetrahedron` magnets are magnetic field sources. Below we compute the H-field in A/m of a - tetrahedron magnet with polarization (0.1,0.2,0.3) in units of tesla dimensions defined - through the vertices (0,0,0), (.01,0,0), (0,.01,0) and (0,0,.01) in units of meter at the - observer position (0.01,0.01,0.01) given in units of meter: + tetrahedron magnet with polarization (0.1,0.2,0.3) in units of T dimensions defined + through the vertices (0,0,0), (.01,0,0), (0,.01,0) and (0,0,.01) in units of m at the + observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> verts = [(0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)] @@ -122,7 +122,7 @@ def __init__( # property getters and setters @property def vertices(self): - """Length of the Tetrahedron sides [a,b,c] in units of meter.""" + """Length of the Tetrahedron sides [a,b,c] in units of m.""" return self._vertices @vertices.setter diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 5eadc7c2e..5e987b5f5 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -37,7 +37,7 @@ class TriangularMesh(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -46,7 +46,7 @@ class TriangularMesh(BaseMagnet): together represent an object path. vertices: ndarray, shape (n,3) - A set of points in units of meter in the local object coordinates from which the + A set of points in units of m in the local object coordinates from which the triangular faces of the mesh are constructed by the additional `faces`input. faces: ndarray, shape (n,3) @@ -104,7 +104,7 @@ class TriangularMesh(BaseMagnet): -------- We compute the B-field in units of T of a triangular mesh (4 vertices, 4 faces) with polarization (0.1,0.2,0.3) in units of T at the observer position - (0.01,0.01,0.01) given in units of meter: + (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> vv = ((0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)) @@ -546,7 +546,7 @@ def from_ConvexHull( Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -639,7 +639,7 @@ def from_pyvista( Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -749,7 +749,7 @@ def from_triangles( Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -857,7 +857,7 @@ def from_mesh( Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index f49c399e2..d10a556c0 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -22,7 +22,7 @@ class CustomSource(BaseSource): be numpy ndarrays of shape (n,3) themselves. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index b92dd4c9c..abc9fd10d 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -22,7 +22,7 @@ class Dipole(BaseSource): Parameters ---------- position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -50,7 +50,7 @@ class Dipole(BaseSource): -------- `Dipole` objects are magnetic field sources. In this example we compute the H-field in A/m of such a magnetic dipole with a moment of (100,100,100) in units of A・m² at an - observer position (.01,.01,.01) given in units of meter: + observer position (.01,.01,.01) given in units of m: >>> import magpylib as magpy >>> src = magpy.misc.Dipole(moment=(10,10,10)) diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index 47746d9d3..8f14f974c 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -23,7 +23,7 @@ class Triangle(BaseMagnet): Parameters ---------- position: array_like, shape (3,) or (m,3) - Object position(s) in the global coordinates in units of meter. For m>1, the + Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. orientation: scipy `Rotation` object with length 1 or m, default=`None` @@ -67,8 +67,8 @@ class Triangle(BaseMagnet): -------- `Triangle` objects are magnetic field sources. Below we compute the H-field in A/m of a Triangle object with polarization (0.01,0.02,0.03) in units of T, dimensions defined - through the vertices (0,0,0), (0.01,0,0) and (0,0.01,0) in units of meter at the - observer position (0.01,0.01,0.01) given in units of meter: + through the vertices (0,0,0), (0.01,0,0) and (0,0.01,0) in units of m at the + observer position (0.01,0.01,0.01) given in units of m: >>> import magpylib as magpy >>> verts = [(0,0,0), (.01,0,0), (0,.01,0)] diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py index af59359d4..fe4b782b4 100644 --- a/magpylib/_src/style.py +++ b/magpylib/_src/style.py @@ -1497,7 +1497,7 @@ class SensorProperties: sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in millimeters. + becomes the sensor size in meters. pixel: dict, Pixel, default=None `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). @@ -1561,7 +1561,7 @@ class DefaultSensor(MagicProperties, SensorProperties): sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in millimeters. + becomes the sensor size in meters. pixel: dict, Pixel, default=None `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). @@ -1640,7 +1640,7 @@ class Pixel(MagicProperties): sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the pixel size. If 'absolute', the `size` parameters - becomes the pixel size in millimeters. + becomes the pixel size in meters. color: str, default=None Defines the pixel color@property. @@ -1804,7 +1804,7 @@ class Arrow(Line): sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the arrow size. If 'absolute', the `size` parameters - becomes the arrow length in millimeters. + becomes the arrow length in meters. offset: float, default=0.5 Defines the arrow offset. `offset=0` results in the arrow head to be coincident to start @@ -2000,7 +2000,7 @@ class DipoleProperties: sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in millimeters. + becomes the dipole size in meters. pivot: str The part of the arrow that is anchored to the X, Y grid. @@ -2063,7 +2063,7 @@ class DefaultDipole(MagicProperties, DipoleProperties): sizemode: {'scaled', 'absolute'}, default='scaled' Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in millimeters. + becomes the dipole size in meters. pivot: str, default=None The part of the arrow that is anchored to the X, Y grid. diff --git a/tests/test_physics_consistency.py b/tests/test_physics_consistency.py index 939b5be21..e68cf465a 100644 --- a/tests/test_physics_consistency.py +++ b/tests/test_physics_consistency.py @@ -42,8 +42,8 @@ def test_dipole_approximation(): def test_Circle_vs_Cylinder_field(): """ - The H-field of a loop with radius r0 (mm) and current i0 (A) is the same - as the H-field of a cylinder with radius r0 (mm), height h0 (mm) and + The H-field of a loop with radius r0 (m) and current i0 (A) is the same + as the H-field of a cylinder with radius r0 (m), height h0 (m) and magnetization (0, 0, 4pi/10*i0/h0) !!! """ @@ -218,7 +218,7 @@ def Binf(i0, pos): e_phi = np.array([-y, x, 0]) e_phi = e_phi / np.linalg.norm(e_phi) mu0 = 4 * np.pi * 1e-7 - return i0 * mu0 / 2 / np.pi / r * e_phi * 1000 * 1000 # mT mm + return i0 * mu0 / 2 / np.pi / r * e_phi * 1000 * 1000 ps = (0, 0, -1000000) pe = (0, 0, 1000000) From 7ed7f7b6880f206ee5e226adcebc8cb2ccdd1c38 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 28 Dec 2023 22:43:00 +0100 Subject: [PATCH 131/240] fix units in get started --- docs/_pages/reso_get_started.md | 132 +++++++++++++++++++------------- 1 file changed, 78 insertions(+), 54 deletions(-) diff --git a/docs/_pages/reso_get_started.md b/docs/_pages/reso_get_started.md index 89b71d12e..405e92cbf 100644 --- a/docs/_pages/reso_get_started.md +++ b/docs/_pages/reso_get_started.md @@ -4,9 +4,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: - display_name: Python 3 (ipykernel) + display_name: Python 3 language: python name: python3 --- @@ -47,9 +47,9 @@ import magpylib as magpy # Create a Cuboid magnet with magnetic polarization # of 1000 mT pointing in x-direction and sides of -# 1,2 and 3 mm respectively. +# 1,2 and 3 cm respectively (notice the use of SI units). -cube = magpy.magnet.Cuboid(magnetization=(1000,0,0), dimension=(1,2,3)) +cube = magpy.magnet.Cuboid(polarization=(1,0,0), dimension=(0.01,0.02,0.03)) # Create a Sensor for measuring the field @@ -67,21 +67,21 @@ print(cube.position) # -> [0. 0. 0.] print(cube.orientation.as_rotvec()) # -> [0. 0. 0.] # Manipulate object position and orientation through -# the respective attributes: +# the respective attributes (move 10mm and rotate 45 deg): from scipy.spatial.transform import Rotation as R -cube.position = (1,0,0) +cube.position = (0.01,0,0) # in SI Units (m) cube.orientation = R.from_rotvec((0,0,45), degrees=True) -print(cube.position) # -> [1. 0. 0.] +print(cube.position) # -> [0.01 0. 0. ] print(cube.orientation.as_rotvec(degrees=True)) # -> [0. 0. 45.] # Apply relative motion with the powerful `move` # and `rotate` methods. -sensor.move((-1,0,0)) +sensor.move((-0.01,0,0)) sensor.rotate_from_angax(angle=-45, axis='z') -print(sensor.position) # -> [-1. 0. 0.] +print(sensor.position) # -> [-0.01 0. 0. ] print(sensor.orientation.as_rotvec(degrees=True)) # -> [ 0. 0. -45.] ``` @@ -99,20 +99,20 @@ magpy.show(cube, sensor, backend='plotly') ### Step 4: Compute the magnetic field ```python -# Compute the B-field in units of mT for some points. +# Compute the B-field in units of T for some points. -points = [(0,0,-1), (0,0,0), (0,0,1)] +points = [(0,0,-.01), (0,0,0), (0,0,.01)] # in SI Units (m) B = magpy.getB(cube, points) -print(B.round()) # -> [[263. 68. 81.] - # [276. 52. 0.] - # [263. 68. -81.]] +print(B.round(2)) # -> [[ 0.26 0.07 0.08] + # [ 0.28 0.05 0. ] + # [ 0.26 0.07 -0.08]] # in SI Units (T) -# Compute the H-field in units of kA/m at the sensor. +# Compute the H-field in units of A/m at the sensor. H = magpy.getH(cube, sensor) -print(H.round()) # -> [220. 41. 0.] +print(H.round()) # -> [51017. 24210. 0.] # in SI Units (A/m) ``` ```{warning} @@ -129,20 +129,23 @@ import numpy as np import magpylib as magpy # Create magnet -sphere = magpy.magnet.Sphere(diameter=1, magnetization=(0,0,1000)) +sphere = magpy.magnet.Sphere( + diameter=.01, # in SI Units (m) + polarization=(0,0,1) # in SI Units (T) +) # Assign a path -sphere.position = np.linspace((-2,0,0), (2,0,0), 7) +sphere.position = np.linspace((-.02,0,0), (.02,0,0), 7) # The field is automatically computed for every path position -B = sphere.getB((0,0,1)) -print(B.round()) # ->[[ 4. 0. -1.] - # [ 13. 0. 1.] - # [ 33. 0. 26.] - # [ 0. 0. 83.] - # [-33. 0. 26.] - # [-13. 0. 1.] - # [ -4. 0. -1.]] +B = sphere.getB((0,0,.01)) # in SI Units (m) +print(B.round(3)) # ->[[ 0.004 0. -0.001] + # [ 0.013 0. 0.001] + # [ 0.033 0. 0.026] + # [ 0. 0. 0.083] + # [-0.033 0. 0.026] + # [-0.013 0. 0.001] + # [-0.004 0. -0.001]] # in SI Units (T) ``` ::: @@ -156,16 +159,18 @@ import magpylib as magpy # Create objects obj1 = magpy.Sensor() -obj2 = magpy.magnet.Cuboid(magnetization=(0,0,1000), dimension=(1,2,3)) +obj2 = magpy.magnet.Cuboid( + polarization=(0,0,1), # in SI Units (T) + dimension=(.01,.02,.03)) # in SI Units (m) # Group objects coll = magpy.Collection(obj1, obj2) # Manipulate Collection -coll.move((1,2,3)) +coll.move((.001,.002,.003)) # in SI Units (m) -print(obj1.position.round()) # -> [1. 2. 3.] -print(obj2.position.round()) # -> [1. 2. 3.] +print(obj1.position) # -> [0.001 0.002 0.003] # in SI Units (m) +print(obj2.position) # -> [0.001 0.002 0.003] # in SI Units (m) ``` ::: @@ -175,19 +180,26 @@ print(obj2.position.round()) # -> [1. 2. 3.] There most convenient way to create a magnet with complex shape is by using the ConvexHull of a point cloud (= simplest form that includes all given points). ```python +import numpy as np + import magpylib as magpy # Create a Pyramid magnet -points = [ - (-1,-1, 0), - (-1, 1, 0), - ( 1,-1, 0), - ( 1, 1, 0), - ( 0, 0, 2), -] +points = ( + np.array( + [ + (-1, -1, 0), + (-1, 1, 0), + (1, -1, 0), + (1, 1, 0), + (0, 0, 2), + ] + ) + / 1000 +) # mm to m pyramid = magpy.magnet.TriangularMesh.from_ConvexHull( - magnetization=(0,0,1000), - points=points + polarization=(0, 0, 1), # in SI Units (T) + points=points, # in SI Units (m) ) # Display the magnet graphically @@ -206,17 +218,17 @@ import magpylib as magpy # create Cuboid magnet with custom style cube = magpy.magnet.Cuboid( - magnetization=(0,0,1000), - dimension=(1,1,1), + polarization=(0,0,1), # in SI Units (T) + dimension=(.01,.01,.01), # in SI Units (m) style_color='r', style_magnetization_mode='arrow' ) # create Cylinder magnet with custom style cyl = magpy.magnet.Cylinder( - magnetization=(0,0,1000), - dimension=(1,1), - position=(2,0,0), + polarization=(0,0,1), # in SI Units (T) + dimension=(.01,.01), # in SI Units (m) + position=(.02,0,0), # in SI Units (m) style_magnetization_color_mode='bicolor', style_magnetization_color_north='m', style_magnetization_color_south='c', @@ -237,8 +249,16 @@ import magpylib as magpy # Create magnet with path -cube = magpy.magnet.Cuboid(magnetization=(0,0,1000), dimension=(1,1,1)) -cube.rotate_from_angax(angle=np.linspace(10,360,18), axis='x') +cube = magpy.magnet.Cuboid( + magnetization=(0,0,1), # in SI Units (T) + dimension=(1,1,1), # in SI Units (m) +) +angles = +cube.rotate_from_angax( + angle=np.linspace(10,360,18), + axis='x' + degrees=True # default, can be omitted +) # Generate an animation with `show` cube.show(animation=True, backend="plotly") @@ -248,8 +268,8 @@ cube.show(animation=True, backend="plotly") -:::{dropdown} Direct interface -Magpylib's object oriented interface is convenient to work with but is also slowed down by object initialization and handling. The direct interface bypasses this load and enables field computation for a set of input parameters. +:::{dropdown} Functional interface +Magpylib's object oriented interface is convenient to work with but is also slowed down by object initialization and handling. The functional interface bypasses this load and enables field computation for a set of input parameters. ```python import magpylib as magpy @@ -257,13 +277,17 @@ import magpylib as magpy # Compute the magnetic field via the direct interface. B = magpy.getB( sources="Cuboid", - observers=[(-1,0,1), (0,0,1), (1,0,1)], - dimension=(1,1,1), - magnetization=(0,0,1000) + observers=[(-.01, 0, .01), (0, 0, .01), (.01, 0, .01)], # in SI Units (m) + dimension=(.01, .01, .01), # in SI Units (m) + polarization=(0, 0, 1), # in SI Units (T) ) -print(B.round()) # -> [[-43. 0. 14.] - # [ 0. 0. 135.] - # [ 43. 0. 14.]] +print(B.round(3)) # -> [[-0.043 0. 0.014] + # [ 0. 0. 0.135] + # [ 0.043 0. 0.014]] # in SI Units (T) ``` ::: + +```{code-cell} ipython3 + +``` From dc6718e61ec9ea3f32841e51d0f8f1d8f601f9d1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 00:06:02 +0100 Subject: [PATCH 132/240] replace dot separator (mpl font compat) --- magpylib/_src/fields/field_wrap_BH.py | 8 ++++---- magpylib/_src/obj_classes/class_misc_Dipole.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 71ab93124..affe0bccb 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -622,9 +622,9 @@ def getB( Magnetization vector M = J/mu0 in units of A/m, given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit A・m² + moment: array_like, shape (3) or (n,3), unit A·m² Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of A・m² given in the local object coordinates + Magnetic dipole moment(s) in units of A·m² given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. @@ -793,9 +793,9 @@ def getH( Magnetization vector(s) (mu0*M, remanence field) in units of A/m given in the local object coordinates (rotates with object). - moment: array_like, shape (3) or (n,3), unit A・m² + moment: array_like, shape (3) or (n,3), unit A·m² Only source_type == `Dipole`! - Magnetic dipole moment(s) in units of A・m² given in the local object coordinates + Magnetic dipole moment(s) in units of A·m² given in the local object coordinates (rotates with object). For homogeneous magnets the relation moment=magnetization*volume holds. diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index abc9fd10d..ae18537df 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -30,8 +30,8 @@ class Dipole(BaseSource): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. - moment: array_like, shape (3,), unit A・m², default=`None` - Magnetic dipole moment in units of A・m² given in the local object coordinates. + moment: array_like, shape (3,), unit A·m², default=`None` + Magnetic dipole moment in units of A·m² given in the local object coordinates. For homogeneous magnets the relation moment=magnetization*volume holds. For current loops the relation moment = current*loop_surface holds. @@ -49,7 +49,7 @@ class Dipole(BaseSource): Examples -------- `Dipole` objects are magnetic field sources. In this example we compute the H-field in A/m - of such a magnetic dipole with a moment of (100,100,100) in units of A・m² at an + of such a magnetic dipole with a moment of (100,100,100) in units of A·m² at an observer position (.01,.01,.01) given in units of m: >>> import magpylib as magpy @@ -104,12 +104,12 @@ def __init__( # property getters and setters @property def moment(self): - """Magnetic dipole moment in units of A・m² given in the local object coordinates.""" + """Magnetic dipole moment in units of A·m² given in the local object coordinates.""" return self._moment @moment.setter def moment(self, mom): - """Set dipole moment vector, shape (3,), unit A・m².""" + """Set dipole moment vector, shape (3,), unit A·m².""" self._moment = check_format_input_vector( mom, dims=(1,), @@ -126,4 +126,4 @@ def _default_style_description(self): moment_mag = np.linalg.norm(moment) if moment_mag == 0: return "no moment" - return f"moment={unit_prefix(moment_mag)}A・m²" + return f"moment={unit_prefix(moment_mag)}A·m²" From 3aba2608fc5adc2fc406151feeeb4cac033177d4 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 00:07:22 +0100 Subject: [PATCH 133/240] fix units docu graphics --- docs/_pages/docu/docu_graphics.md | 363 +++++++++++++++++------------- 1 file changed, 207 insertions(+), 156 deletions(-) diff --git a/docs/_pages/docu/docu_graphics.md b/docs/_pages/docu/docu_graphics.md index e791137a0..66a39a7a6 100644 --- a/docs/_pages/docu/docu_graphics.md +++ b/docs/_pages/docu/docu_graphics.md @@ -4,9 +4,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.15.0 + jupytext_version: 1.16.0 kernelspec: - display_name: Python 3 (ipykernel) + display_name: Python 3 language: python name: python3 orphan: true @@ -24,67 +24,74 @@ The desired graphic backend is selected with the `backend` keyword argument. To The following example shows the graphical representation of various Magpylib objects and their paths using the default Matplotlib graphic backend. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import pyvista as pv +import magpylib as magpy + objects = { "Cuboid": magpy.magnet.Cuboid( - magnetization=(0, -100, 0), - dimension=(1, 1, 1), - position=(-6, 0, 0), + polarization=(0, -0.1, 0), + dimension=(0.01, 0.01, 0.01), + position=(-0.06, 0, 0), ), "Cylinder": magpy.magnet.Cylinder( - magnetization=(0, 0, 100), - dimension=(1, 1), - position=(-5, 0, 0), + polarization=(0, 0, 0.01), + dimension=(0.01, 0.01), + position=(-0.05, 0, 0), ), "CylinderSegment": magpy.magnet.CylinderSegment( - magnetization=(0, 0, 100), - dimension=(0.3, 1, 1, 0, 140), - position=(-3, 0, 0), + polarization=(0, 0, 0.01), + dimension=(0.003, 0.01, 0.01, 0, 140), + position=(-0.03, 0, 0), ), "Sphere": magpy.magnet.Sphere( - magnetization=(0, 0, 100), - diameter=1, - position=(-1, 0, 0), + polarization=(0, 0, 0.01), + diameter=0.01, + position=(-0.01, 0, 0), ), "Tetrahedron": magpy.magnet.Tetrahedron( - magnetization=(0, 0, 100), - vertices=((-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, -1, -1)), - position=(-4, 0, 4), + polarization=(0, 0, 0.01), + vertices=((-0.01, 0, 0), (0.01, 0, 0), (0, -0.01, 0), (0, -0.01, -0.01)), + position=(-0.04, 0, 0.04), ), "TriangularMesh": magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 100), - polydata=pv.Dodecahedron(), - position=(-1, 0, 4), + polarization=(0, 0, 0.01), + polydata=pv.Dodecahedron(radius=0.01), + position=(-0.01, 0, 0.04), ), "Circle": magpy.current.Circle( current=1, - diameter=1, - position=(4, 0, 0), + diameter=0.01, + position=(0.04, 0, 0), ), "Polyline": magpy.current.Polyline( current=1, - vertices=[(1, 0, 0), (0, 1, 0), (-1, 0, 0), (0, -1, 0), (1, 0, 0)], - position=(1, 0, 0), + vertices=[ + (0.01, 0, 0), + (0, 0.01, 0), + (-0.01, 0, 0), + (0, -0.01, 0), + (0.01, 0, 0), + ], + position=(0.01, 0, 0), ), "Dipole": magpy.misc.Dipole( - moment=(0, 0, 100), - position=(3, 0, 0), + moment=(0, 0, 1), + position=(0.03, 0, 0), ), "Triangle": magpy.misc.Triangle( - magnetization=(0, 0, 100), - vertices=((-1, 0, 0), (1, 0, 0), (0, 1, 0)), - position=(2, 0, 4), + polarization=(0, 0, 0.01), + vertices=((-0.01, 0, 0), (0.01, 0, 0), (0, 0.01, 0)), + position=(0.02, 0, 0.04), ), "Sensor": magpy.Sensor( - pixel=[(0, 0, z) for z in (-0.5, 0, 0.5)], - position=(0, -3, 0), + pixel=[(0, 0, z) for z in (-0.005, 0, 0.005)], + position=(0, -0.03, 0), ), } -objects["Circle"].move(np.linspace((0, 0, 0), (0, 0, 5), 20)) +objects["Circle"].move(np.linspace((0, 0, 0), (0, 0, 0.05), 20)) objects["Cuboid"].rotate_from_angax(np.linspace(0, 90, 20), "z", anchor=0) magpy.show(*objects.values()) @@ -172,20 +179,19 @@ There is a high level of **feature parity**, however, not all graphic features a The following example demonstrates the currently supported backends: ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import pyvista as pv -pv.set_jupyter_backend("panel") # improve rendering in a jupyter notebook (pyvista only) +import magpylib as magpy # define sources and paths -loop = magpy.current.Circle(current=1, diameter=1) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) +loop = magpy.current.Circle(current=1, diameter=0.01) +loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 cylinder = magpy.magnet.Cylinder( - magnetization=(0, -100, 0), dimension=(1, 2), position=(0, -3, 0) + polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) ) -cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) +cylinder.rotate_from_angax(np.linspace(0, 300, 40), "z", anchor=0, start=0) # show the system using different backends for backend in magpy.SUPPORTED_PLOTTING_BACKENDS: @@ -200,21 +206,22 @@ When calling `show`, a figure is automatically generated and displayed. It is al In the following example we show how to combine a 2D field plot with the 3D `show` output in **Matplotlib**: ```{code-cell} ipython3 -import magpylib as magpy import matplotlib.pyplot as plt import numpy as np +import magpylib as magpy + # setup matplotlib figure and subplots fig = plt.figure(figsize=(10, 4)) ax1 = fig.add_subplot(121) # 2D-axis ax2 = fig.add_subplot(122, projection="3d") # 3D-axis # define sources and paths -loop = magpy.current.Circle(current=1, diameter=1) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) +loop = magpy.current.Circle(current=1, diameter=0.01) +loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 cylinder = magpy.magnet.Cylinder( - magnetization=(0, -100, 0), dimension=(1, 2), position=(0, -3, 0) + polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) ) cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) @@ -233,21 +240,22 @@ plt.show() A similar example with **Plotly**: ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import plotly.graph_objects as go +import magpylib as magpy + # setup plotly figure and subplots fig = go.Figure().set_subplots( rows=1, cols=2, specs=[[{"type": "xy"}, {"type": "scene"}]] ) # define sources and paths -loop = magpy.current.Circle(current=1, diameter=1) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) +loop = magpy.current.Circle(current=1, diameter=0.01) +loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 cylinder = magpy.magnet.Cylinder( - magnetization=(0, -100, 0), dimension=(1, 2), position=(0, -3, 0) + polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) ) cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) @@ -269,18 +277,17 @@ fig.show() An example with **Pyvista**: ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import pyvista as pv -pv.set_jupyter_backend("panel") # improve rending in a jupyter notebook +import magpylib as magpy # define sources and paths loop = magpy.current.Circle(current=1, diameter=5) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) +loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 cylinder = magpy.magnet.Cylinder( - magnetization=(0, -100, 0), dimension=(1, 2), position=(0, -3, 0) + polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) ) cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) @@ -295,7 +302,7 @@ pl.add_lines(line, color="black") magpy.show(loop, cylinder, backend="pyvista", canvas=pl) # display scene -pl.camera.position = (50, 10, 10) +pl.camera.position = (.50, 10, 10) pl.set_background("black", top="white") pl.show() ``` @@ -305,18 +312,17 @@ pl.show() Instead of forwarding a figure to an existing canvas, it is also possible to return the figure object for further manipulation using the `return_fig` command. In the following example this is demonstrated for the pyvista backend. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import pyvista as pv -pv.set_jupyter_backend("panel") # improve rending in a jupyter notebook +import magpylib as magpy # define sources and paths -loop = magpy.current.Circle(current=1, diameter=5) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) +loop = magpy.current.Circle(current=1, diameter=0.05) +loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 cylinder = magpy.magnet.Cylinder( - magnetization=(0, -100, 0), dimension=(1, 2), position=(0, -3, 0) + polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) ) cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) @@ -325,13 +331,16 @@ pl = magpy.show(loop, cylinder, backend="pyvista", return_fig=True) # add line to the pyvista scene line = np.array( - [(t * np.cos(15 * t), t * np.sin(15 * t), t - 8) for t in np.linspace(3, 5, 200)] + [ + (t * np.cos(15 * t), t * np.sin(15 * t), t - 8) + for t in np.linspace(0.03, 0.05, 200) + ] ) pl.add_lines(line, color="black") # display scene -pl.camera.position = (50, 10, 10) -pl.set_background("purple", top="lightgreen") +pl.camera.position = (.05, .01, .01) +pl.set_background("yellow", top="lightgreen") pl.enable_anti_aliasing("ssaa") pl.show() ``` @@ -384,11 +393,13 @@ import magpylib as magpy magpy.defaults.reset() -cube = magpy.magnet.Cuboid(magnetization=(1, 0, 0), dimension=(1, 1, 1)) +cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) cylinder = magpy.magnet.Cylinder( - magnetization=(0, -1, 0), dimension=(1, 1), position=(2, 0, 0) + polarization=(0, -1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) +) +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) ) -sphere = magpy.magnet.Sphere(magnetization=(0, 1, 1), diameter=1, position=(4, 0, 0)) print("Default magnetization style") magpy.show(cube, cylinder, sphere, backend="plotly") @@ -463,19 +474,19 @@ import magpylib as magpy magpy.defaults.reset() # reset defaults defined in previous example cube = magpy.magnet.Cuboid( - magnetization=(1, 0, 0), - dimension=(1, 1, 1), + polarization=(1, 0, 0), + dimension=(0.01, 0.01, 0.01), style_magnetization_color_mode="tricycle", ) cylinder = magpy.magnet.Cylinder( - magnetization=(0, 1, 0), - dimension=(1, 1), - position=(2, 0, 0), + polarization=(0, 1, 0), + dimension=(0.01, 0.01), + position=(0.02, 0, 0), ) sphere = magpy.magnet.Sphere( - magnetization=(0, 1, 1), - diameter=1, - position=(4, 0, 0), + polarization=(0, 1, 1), + diameter=0.01, + position=(0.04, 0, 0), ) sphere.style.magnetization.color.mode = "bicolor" @@ -494,9 +505,13 @@ import magpylib as magpy magpy.defaults.reset() # reset defaults defined in previous example -cube = magpy.magnet.Cuboid(magnetization=(1, 0, 0), dimension=(1, 1, 1)) -cylinder = magpy.magnet.Cylinder(magnetization=(0, 1, 0), dimension=(1, 1), position=(2, 0, 0)) -sphere = magpy.magnet.Sphere(magnetization=(0, 1, 1), diameter=1, position=(4, 0, 0)) +cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) +cylinder = magpy.magnet.Cylinder( + polarization=(0, 1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) +) +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) +) coll = cube + cylinder @@ -512,9 +527,13 @@ Finally it is possible to hand style input to the `show` function directly and l ```{code-cell} ipython3 import magpylib as magpy -cube = magpy.magnet.Cuboid(magnetization=(1, 0, 0), dimension=(1, 1, 1)) -cylinder = magpy.magnet.Cylinder(magnetization=(0, 1, 0), dimension=(1, 1), position=(2, 0, 0)) -sphere = magpy.magnet.Sphere(magnetization=(0, 1, 1), diameter=1, position=(4, 0, 0)) +cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) +cylinder = magpy.magnet.Cylinder( + polarization=(0, 1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) +) +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) +) # use local style override magpy.show(cube, cylinder, sphere, backend="plotly", style_magnetization_show=False) @@ -543,24 +562,25 @@ Ideally, the animation will show all path steps, but when e.g. `time` and `fps` The following example demonstrates the animation feature, ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # define objects with paths coll = magpy.Collection( - magpy.magnet.Cuboid(magnetization=(0, 1, 0), dimension=(2, 2, 2)), - magpy.magnet.Cylinder(magnetization=(0, 1, 0), dimension=(2, 2)), - magpy.magnet.Sphere(magnetization=(0, 1, 0), diameter=2), + magpy.magnet.Cuboid(polarization=(0, 1, 0), dimension=(0.02, 0.02, 0.02)), + magpy.magnet.Cylinder(polarization=(0, 1, 0), dimension=(0.02, 0.02)), + magpy.magnet.Sphere(polarization=(0, 1, 0), diameter=0.02), ) -start_positions = np.array([(1.414, 0, 1), (-1, -1, 1), (-1, 1, 1)]) +start_positions = np.array([(1.414, 0, 1), (-1, -1, 1), (-1, 1, 1)]) / 100 for pos, src in zip(start_positions, coll): src.position = np.linspace(pos, pos * 5, 50) src.rotate_from_angax(np.linspace(0, 360, 50), "z", anchor=0, start=0) -ts = np.linspace(-0.6, 0.6, 5) +ts = np.linspace(-0.6, 0.6, 5) / 100 sensor = magpy.Sensor(pixel=[(x, y, 0) for x in ts for y in ts]) -sensor.position = np.linspace((0, 0, -5), (0, 0, 5), 20) +sensor.position = np.linspace((0, 0, -5), (0, 0, 5), 20) / 100 # show with animation magpy.show( @@ -599,21 +619,22 @@ Magpylib also offers the possibility to display objects into separate subplots. 3D suplots can be directly defined in the `show` function by passing input objects as dictionaries with the arguments `objects`, `col` (column) and `row`, as in the example below. If now `row` or no `col` is specified, it defaults to 1. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # define sensor and sources -sensor = magpy.Sensor(pixel=[(-2,0,0), (2,0,0)]) +sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" ) # define paths -N=40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) -cyl1.position = (4, 0, 0) +N = 40 +sensor.position = np.linspace((0, 0, -0.03), (0, 0, 0.03), N) +cyl1.position = (0.04, 0, 0) cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 5)) +cyl2 = cyl1.copy().move((0, 0, 0.05)) # display system in 3D with dict syntax magpy.show( @@ -629,21 +650,22 @@ In order to make the subplot syntax more convenient we introduced the new `show_ The above example becomes: ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # define sensor and sources -sensor = magpy.Sensor(pixel=[(-2,0,0), (2,0,0)]) +sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" ) # define paths -N=40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) -cyl1.position = (4, 0, 0) +N = 40 +sensor.position = np.linspace((0, 0, -0.03), (0, 0, 0.03), N) +cyl1.position = (0.04, 0, 0) cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 5)) +cyl2 = cyl1.copy().move((0, 0, 0.05)) # display system in 3D with context manager with magpy.show_context(backend="matplotlib") as sc: @@ -690,21 +712,22 @@ By default `output='model3d'` displays the 3D representations of the objects. If By default source outputs are summed up and sensor pixels, if any, are aggregated by mean (`pixel_agg="mean"`). ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # define sensor and sources -sensor = magpy.Sensor(pixel=[(-2,0,0), (2,0,0)]) +sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" ) # define paths -N=40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) -cyl1.position = (4, 0, 0) +N = 40 +sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) / 100 +cyl1.position = (0.04, 0, 0) cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 5)) +cyl2 = cyl1.copy().move((0, 0, 0.05)) # display field data with context manager with magpy.show_context(cyl1, cyl2, sensor): @@ -727,21 +750,22 @@ with magpy.show_context(cyl1, cyl2, sensor, sumup=False): Finally, Magpylib lets us show coupled 3D models with their field data while animating it. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # define sensor and sources -sensor = magpy.Sensor(pixel=[(-2,0,0), (2,0,0)]) +sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) cyl1 = magpy.magnet.Cylinder( - magnetization=(100, 0, 0), dimension=(1, 2), style_label="Cylinder1" + polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" ) # define paths -N=40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) -cyl1.position = (4, 0, 0) +N = 40 +sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) / 100 +cyl1.position = (0.04, 0, 0) cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 5)) +cyl2 = cyl1.copy().move((0, 0, 0.05)) # display field data with context manager, no sumup, no pixel_agg with magpy.show_context(cyl1, cyl2, sensor, animation=True, style_pixel_size=0.2): @@ -772,11 +796,10 @@ The input `trace` is a dictionary which includes all necessary information for p The following example shows how a **generic** trace is constructed with `Mesh3d` and `Scatter3d` and is displayed with three different backends: ```{code-cell} ipython3 -import magpylib as magpy import numpy as np import pyvista as pv -pv.set_jupyter_backend("panel") # improve rendering in a jupyter notebook +import magpylib as magpy # Mesh3d trace ######################### @@ -784,16 +807,16 @@ trace_mesh3d = { "backend": "generic", "constructor": "Mesh3d", "kwargs": { - "x": (1, 0, -1, 0), - "y": (-0.5, 1.2, -0.5, 0), - "z": (-0.5, -0.5, -0.5, 1), + "x": (0.01, 0, -0.01, 0), + "y": (-0.005, 0.012, -0.005, 0), + "z": (-0.005, -0.005, -0.005, 0.01), "i": (0, 0, 0, 1), "j": (1, 1, 2, 2), "k": (2, 3, 3, 3), #'opacity': 0.5, }, } -coll = magpy.Collection(position=(0, -3, 0), style_label="'Mesh3d' trace") +coll = magpy.Collection(position=(0, -0.03, 0), style_label="'Mesh3d' trace") coll.style.model3d.add_trace(trace_mesh3d) # Scatter3d trace ###################### @@ -803,9 +826,9 @@ trace_scatter3d = { "backend": "generic", "constructor": "Scatter3d", "kwargs": { - "x": np.cos(ts), + "x": np.cos(ts) / 100, "y": np.zeros(30), - "z": np.sin(ts), + "z": np.sin(ts) / 100, "mode": "lines", }, } @@ -824,19 +847,40 @@ It is possible to have multiple user-defined traces that will be displayed at th ```{code-cell} ipython3 import copy -dipole.style.size = 3 +import numpy as np + +import magpylib as magpy + +ts = np.linspace(0, 2 * np.pi, 30) +trace_scatter3d = { + "backend": "generic", + "constructor": "Scatter3d", + "kwargs": { + "x": np.cos(ts) / 100, + "y": np.zeros(30), + "z": np.sin(ts) / 100, + "mode": "lines", + }, +} +dipole = magpy.misc.Dipole( + moment=(0, 0, 1), + style_label="'Scatter3d' trace", + style_size=0.01, + style_sizemode="absolute", +) + # generate new trace from dictionary trace2 = copy.deepcopy(trace_scatter3d) -trace2["kwargs"]["y"] = np.sin(ts) +trace2["kwargs"]["y"] = np.sin(ts) / 100 trace2["kwargs"]["z"] = np.zeros(30) dipole.style.model3d.add_trace(trace2) # generate new trace from Trace3d object -trace3 = dipole.style.model3d.data[1].copy() +trace3 = copy.deepcopy(dipole.style.model3d.data[0]) trace3.kwargs["x"] = np.zeros(30) -trace3.kwargs["z"] = np.cos(ts) +trace3.kwargs["z"] = np.cos(ts) / 100 dipole.style.model3d.add_trace(trace3) @@ -846,17 +890,18 @@ dipole.show(dipole, backend="matplotlib") **Matplotlib** plotting functions often use positional arguments for $(x,y,z)$ input, that are handed over from `args=(x,y,z)` in `trace`. The following examples show how to construct traces with `plot`, `plot_surface` and `plot_trisurf`: ```{code-cell} ipython3 -import magpylib as magpy import matplotlib.pyplot as plt import matplotlib.tri as mtri import numpy as np +import magpylib as magpy + # plot trace ########################### ts = np.linspace(-10, 10, 100) -xs = np.cos(ts) -ys = np.sin(ts) -zs = ts / 20 +xs = np.cos(ts) / 100 +ys = np.sin(ts) / 100 +zs = ts / 20 / 100 trace_plot = { "backend": "matplotlib", @@ -864,15 +909,15 @@ trace_plot = { "args": (xs, ys, zs), "kwargs": {"ls": "--", "lw": 2}, } -magnet = magpy.magnet.Cylinder(magnetization=(0, 0, 1), dimension=(0.5, 1)) +magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.005, 0.01)) magnet.style.model3d.add_trace(trace_plot) # plot_surface trace ################### u, v = np.mgrid[0 : 2 * np.pi : 30j, 0 : np.pi : 20j] -xs = np.cos(u) * np.sin(v) -ys = np.sin(u) * np.sin(v) -zs = np.cos(v) +xs = np.cos(u) * np.sin(v) / 100 +ys = np.sin(u) * np.sin(v) / 100 +zs = np.cos(v) / 100 trace_surf = { "backend": "matplotlib", @@ -880,7 +925,7 @@ trace_surf = { "args": (xs, ys, zs), "kwargs": {"cmap": plt.cm.YlGnBu_r}, } -ball = magpy.Collection(position=(-3, 0, 0)) +ball = magpy.Collection(position=(-0.03, 0, 0)) ball.style.model3d.add_trace(trace_surf) # plot_trisurf trace ################### @@ -888,9 +933,9 @@ ball.style.model3d.add_trace(trace_surf) u, v = np.mgrid[0 : 2 * np.pi : 50j, -0.5:0.5:10j] u, v = u.flatten(), v.flatten() -xs = (1 + 0.5 * v * np.cos(u / 2.0)) * np.cos(u) -ys = (1 + 0.5 * v * np.cos(u / 2.0)) * np.sin(u) -zs = 0.5 * v * np.sin(u / 2.0) +xs = (1 + 0.5 * v * np.cos(u / 2.0)) * np.cos(u) / 100 +ys = (1 + 0.5 * v * np.cos(u / 2.0)) * np.sin(u) / 100 +zs = 0.5 * v * np.sin(u / 2.0) / 100 tri = mtri.Triangulation(u, v) @@ -903,10 +948,10 @@ trace_trisurf = { "cmap": plt.cm.coolwarm, }, } -mobius = magpy.misc.CustomSource(style_model3d_showdefault=False, position=(3, 0, 0)) +mobius = magpy.misc.CustomSource(style_model3d_showdefault=False, position=(0.03, 0, 0)) mobius.style.model3d.add_trace(trace_trisurf) -magpy.show(magnet, ball, mobius, zoom=5, backend="matplotlib") +magpy.show(magnet, ball, mobius, backend="matplotlib") ``` ### Pre-defined 3D models @@ -920,9 +965,9 @@ from magpylib.graphics import model3d # prism trace ################################### trace_prism = model3d.make_Prism( base=6, - diameter=2, - height=1, - position=(-3, 0, 0), + diameter=0.02, + height=0.01, + position=(-0.03, 0, 0), ) obj0 = magpy.Sensor(style_model3d_showdefault=False, style_label="Prism") obj0.style.model3d.add_trace(trace_prism) @@ -930,33 +975,33 @@ obj0.style.model3d.add_trace(trace_prism) # pyramid trace ################################# trace_pyramid = model3d.make_Pyramid( base=30, - diameter=2, - height=1, - position=(3, 0, 0), + diameter=0.02, + height=0.01, + position=(0.03, 0, 0), ) obj1 = magpy.Sensor(style_model3d_showdefault=False, style_label="Pyramid") obj1.style.model3d.add_trace(trace_pyramid) # cuboid trace ################################## trace_cuboid = model3d.make_Cuboid( - dimension=(2, 2, 2), - position=(0, 3, 0), + dimension=(0.02, 0.02, 0.02), + position=(0, 0.03, 0), ) obj2 = magpy.Sensor(style_model3d_showdefault=False, style_label="Cuboid") obj2.style.model3d.add_trace(trace_cuboid) # cylinder segment trace ######################## trace_cylinder_segment = model3d.make_CylinderSegment( - dimension=(1, 2, 1, 140, 220), - position=(1, 0, -3), + dimension=(0.01, 0.02, 0.01, 140, 220), + position=(0.01, 0, -0.03), ) obj3 = magpy.Sensor(style_model3d_showdefault=False, style_label="Cylinder Segment") obj3.style.model3d.add_trace(trace_cylinder_segment) # ellipsoid trace ############################### trace_ellipsoid = model3d.make_Ellipsoid( - dimension=(2, 2, 2), - position=(0, 0, 3), + dimension=(0.02, 0.02, 0.02), + position=(0, 0, 0.03), ) obj4 = magpy.Sensor(style_model3d_showdefault=False, style_label="Ellipsoid") obj4.style.model3d.add_trace(trace_ellipsoid) @@ -964,9 +1009,9 @@ obj4.style.model3d.add_trace(trace_ellipsoid) # arrow trace ################################### trace_arrow = model3d.make_Arrow( base=30, - diameter=0.6, - height=2, - position=(0, -3, 0), + diameter=0.006, + height=0.02, + position=(0, -0.03, 0), ) obj5 = magpy.Sensor(style_model3d_showdefault=False, style_label="Arrow") obj5.style.model3d.add_trace(trace_arrow) @@ -988,12 +1033,13 @@ The code below requires installation of the `numpy-stl` package. import os import tempfile -import magpylib as magpy import numpy as np import requests from matplotlib.colors import to_hex from stl import mesh # requires installation of numpy-stl +import magpylib as magpy + def bin_color_to_hex(x): """transform binary rgb into hex color""" @@ -1025,6 +1071,7 @@ def trace_from_stl(stl_file): # generate and return a generic trace which can be translated into any backend colors = stl_mesh.attr.flatten() facecolor = np.array([bin_color_to_hex(c) for c in colors]).T + x, y, z = x / 1000, y / 1000, z / 1000 # mm->m trace = { "backend": "generic", "constructor": "mesh3d", @@ -1050,8 +1097,8 @@ sensor = magpy.Sensor(style_label="PG-SSO-3 package") sensor.style.model3d.add_trace(trace) # create magnet and sensor path -magnet = magpy.magnet.Cylinder(magnetization=(0, 0, 100), dimension=(15, 20)) -sensor.position = np.linspace((-15, 0, 8), (-15, 0, -4), 21) +magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.015, 0.02)) +sensor.position = np.linspace((-0.015, 0, 0.008), (-0.015, 0, -0.004), 21) sensor.rotate_from_angax(np.linspace(0, 180, 21), "z", anchor=0, start=0) # display with matplotlib and plotly backends @@ -1060,3 +1107,7 @@ kwargs = dict(style_path_frames=5) magpy.show(args, **kwargs, backend="matplotlib") magpy.show(args, **kwargs, backend="plotly") ``` + +```{code-cell} ipython3 + +``` From 3c7566c07a53b6ab06640a833521dd3bf02dcd09 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 00:13:39 +0100 Subject: [PATCH 134/240] rename direct interface to functional interface --- docs/_pages/docu/docu_magpylib_api.md | 14 +++++++------- docs/_pages/docu/docu_physics.md | 2 +- docs/_pages/gallery/gallery_tutorial_custom.md | 14 +++++++------- .../gallery/gallery_tutorial_field_computation.md | 8 ++++---- docs/_pages/gallery/gallery_vis_mpl_streamplot.md | 4 ++-- docs/_pages/reso_get_started.md | 3 +-- magpylib/_src/fields/field_wrap_BH.py | 6 +++--- 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index bd060c47a..b530b65ff 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -30,7 +30,7 @@ For historical reasons Magpylib is by default set up for the following units | electric current | `current` | **A** | | magnetic dipole moment | `moment` | **mT*mm^3** | | B-field | `getB()` | **mT** | -| H-field | `getH()` | **kA/m** | +| H-field | `getH()` | **A/m** | | length-inputs | `position`, `dimension`, ... | **mm** | | angle-inputs | `angle`, `dimension`, ... | **deg** | ::: @@ -43,7 +43,7 @@ Unfortunately Magpylib contributes to the naming confusion in magnetism that is The analytical solutions are scale invariant - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant. -In addition, `getB` returns the same unit as given by the `magnetization` input. When the magnetization is given in mT, then `getH` returns kA/m which is simply related by factor of $\frac{10}{4\pi}$. +In addition, `getB` returns the same unit as given by the `magnetization` input. When the magnetization is given in mT, then `getH` returns A/m which is simply related by factor of $\frac{10}{4\pi}$. In {ref}`phys-remanence` the connection between the `magnetization` input and the remanence field of a magnet is explained. @@ -485,7 +485,7 @@ Position and orientation of all Magpylib objects are defined by the two attribut The position and orientation attributes can be either **scalar**, i.e. a single position or a single rotation, or **vector**, when they are arrays of such scalars. The two attributes together define the **path** of an object - Magpylib makes sure that they are always of the same length. When the field is computed, it is automatically computed for the whole path. ```{tip} -To enable vectorized field computation, paths should always be used when modeling multiple object positions. Avoid using Python loops at all costs for that purpose! If your path is difficult to realize, consider using the [direct interface](docu-direct-interface) instead. +To enable vectorized field computation, paths should always be used when modeling multiple object positions. Avoid using Python loops at all costs for that purpose! If your path is difficult to realize, consider using the [functional interface](docu-functional-interface) instead. ``` Magpylib offers two powerful methods for object manipulation: @@ -654,7 +654,7 @@ print(B) ::: :::: -The physical unit returned by `getB` and `getH` corresponds to the source excitation input units. For example, when magnet `magnetization` is given in mT, `getB` returns the B-field in units of mT and `getH` the H-field in units of kA/m. This is described in detail in {ref}`docu-units`. +The physical unit returned by `getB` and `getH` corresponds to the source excitation input units. For example, when magnet `magnetization` is given in mT, `getB` returns the B-field in units of mT and `getH` the H-field in units of A/m. This is described in detail in {ref}`docu-units`. The output of a field computation `magpy.getB(sources, observers)` is by default a Numpy array of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of observers, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position array input and `3` the three magnetic field components $(B_x, B_y, B_z)$. @@ -674,7 +674,7 @@ Try to make all field computations with as few calls to `getB` and `getH` as pos The tutorial {ref}`gallery-tutorial-field-computation` shows good practices with Magpylib field computation. -(docu-direct-interface)= +(docu-functional-interface)= ## Direct interface Users can bypass the object oriented functionality of Magpylib and instead compute the field for n given parameter sets. This is done by providing the following inputs to the top level functions `getB` and `getH`, @@ -683,7 +683,7 @@ Users can bypass the object oriented functionality of Magpylib and instead compu 2. `observers`: array-like of shape (3,) or (n,3) giving the observer positions. 3. `kwargs`: a dictionary with inputs of shape (x,) or (n,x). Must include all mandatory class-specific inputs. By default, `position=(0,0,0)` and `orientation=None`(=unit rotation). -All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to create a set of n computation instances. The field is returned in the shape (n,3). The following code demonstrates the direct interface. +All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to create a set of n computation instances. The field is returned in the shape (n,3). The following code demonstrates the functional interface. ::::{grid} :gutter: 5 @@ -714,7 +714,7 @@ print(B.round()) :::: ```{note} -The direct interface is potentially faster than the object oriented one if users know how to generate the input arrays efficiently with numpy (e.g. `np.arange`, `np.linspace`, `np.tile`, `np.repeat`, ...). +The functional interface is potentially faster than the object oriented one if users know how to generate the input arrays efficiently with numpy (e.g. `np.arange`, `np.linspace`, `np.tile`, `np.repeat`, ...). ``` diff --git a/docs/_pages/docu/docu_physics.md b/docs/_pages/docu/docu_physics.md index 19fd12f1a..a7908fede 100644 --- a/docs/_pages/docu/docu_physics.md +++ b/docs/_pages/docu/docu_physics.md @@ -90,7 +90,7 @@ Magpylib code is fully [vectorized](https://en.wikipedia.org/wiki/Array_programm Maximal performance is achieved when `.getB(sources, observers)` is called only a single time in your program. Try not to use loops. ``` -The object oriented interface comes with an overhead. If you want to achieve maximal performance this overhead can be avoided with {ref}`docu-direct-interface`. +The object oriented interface comes with an overhead. If you want to achieve maximal performance this overhead can be avoided with {ref}`docu-functional-interface`. The analytical solutions provide extreme performance. Single field evaluations take of the order of `100 µs`. For large input arrays (e.g. many observer positions or many similar magnets) the computation time drops below `1 µs` per evaluation point on single state-of-the-art x86 mobile cores (tested on `Intel Core i5-8365U @ 1.60GHz`), depending on the source type. diff --git a/docs/_pages/gallery/gallery_tutorial_custom.md b/docs/_pages/gallery/gallery_tutorial_custom.md index be1f2d325..4d452da76 100644 --- a/docs/_pages/gallery/gallery_tutorial_custom.md +++ b/docs/_pages/gallery/gallery_tutorial_custom.md @@ -32,7 +32,7 @@ $$ Here the monopole lies in the origin of the local coordinates, $Q_m$ is the monopole charge and ${\bf r}$ is the observer position. -We create this field as a Python function and hand it over to a CustomSource `field_func` argument. The `field_func` input must be a callable with two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of mT and kA/m in the same shape. +We create this field as a Python function and hand it over to a CustomSource `field_func` argument. The `field_func` input must be a callable with two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of mT and A/m in the same shape. ```{code-cell} ipython3 import numpy as np @@ -41,21 +41,21 @@ import magpylib as magpy # Create monopole field def mono_field(field, observers): """ - Monopole field + Monopole field field: string, "B" or "H return B or H-field observers: array_like of shape (n,3) Observer positions - + Returns: np.ndarray, shape (n,3) Magnetic monopole field """ if field=="B": Qm = 1 # unit mT else: - Qm = 10/4/np.pi # unit kA/m + Qm = 10/4/np.pi # unit A/m obs = np.array(observers).T field = Qm * obs / np.linalg.norm(obs, axis=0)**3 return field.T @@ -122,7 +122,7 @@ magpy.show(mono1, mono2) ## Subclassing CustomSource -In the above example it would be nice to make the CustomSource dynamic, so that it would have a property `charge` that can be changed at will, rather than having to redefine the `field_func` and initialize a new object every time. In the following example we show how to sub-class `CustomSource` to achieve this. The problem is reminiscent of {ref}`gallery-misc-compound`. +In the above example it would be nice to make the CustomSource dynamic, so that it would have a property `charge` that can be changed at will, rather than having to redefine the `field_func` and initialize a new object every time. In the following example we show how to sub-class `CustomSource` to achieve this. The problem is reminiscent of {ref}`gallery-misc-compound`. ```{code-cell} ipython3 class Monopole(magpy.misc.CustomSource): @@ -149,12 +149,12 @@ class Monopole(magpy.misc.CustomSource): def _update(self): """ Apply monopole field function """ - + def mono_field(field, observers): """ monopole field""" chg = self._charge if field=="H": - chg *= 10/4/np.pi # unit kA/m + chg *= 10/4/np.pi # unit A/m obs = np.array(observers).T BH = chg * obs / np.linalg.norm(obs, axis=0)**3 return BH.T diff --git a/docs/_pages/gallery/gallery_tutorial_field_computation.md b/docs/_pages/gallery/gallery_tutorial_field_computation.md index cc7659505..ac3e3b207 100644 --- a/docs/_pages/gallery/gallery_tutorial_field_computation.md +++ b/docs/_pages/gallery/gallery_tutorial_field_computation.md @@ -181,16 +181,16 @@ fig = px.line( fig.show() ``` -(gallery-tutorial-field-computation-direct-interface)= +(gallery-tutorial-field-computation-functional-interface)= ## Direct Interface -All above computations demonstrate the convenient object oriented interface of Magpylib. However, there are instances when it is better to work with the direct interface instead. +All above computations demonstrate the convenient object oriented interface of Magpylib. However, there are instances when it is better to work with the functional interface instead. 1. Reduce overhead of Python objects 2. Complex computation instances -In the following example we show how complex instances are computed using the direct interface. +In the following example we show how complex instances are computed using the functional interface. ```{important} Use numpy operations for input array creation as shown in the example ! @@ -220,7 +220,7 @@ POS = np.vstack(( np.tile(pos2, (6,1)), )) -# Compute all instances with the direct interface +# Compute all instances with the functional interface B = magpy.getB( sources='Cuboid', observers=POS, diff --git a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md b/docs/_pages/gallery/gallery_vis_mpl_streamplot.md index 29413522b..16fb877b3 100644 --- a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md +++ b/docs/_pages/gallery/gallery_vis_mpl_streamplot.md @@ -70,7 +70,7 @@ plt.show() ``` ```{note} -Be aware that the above code is not very performant, but quite readable. The following example creates the grid with numpy commands only instead of Python loops, and uses the {ref}`gallery-tutorial-field-computation-direct-interface` for field computation. +Be aware that the above code is not very performant, but quite readable. The following example creates the grid with numpy commands only instead of Python loops, and uses the {ref}`gallery-tutorial-field-computation-functional-interface` for field computation. ``` ## Example 2 - Hollow Cylinder Magnet @@ -89,7 +89,7 @@ fig, ax = plt.subplots() X, Y = np.mgrid[-5:5:100j, -5:5:100j].transpose((0, 2, 1)) grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) -# Compute magnetic field on grid - using the direct interface +# Compute magnetic field on grid - using the functional interface B = magpy.getB( "CylinderSegment", observers=grid.reshape(-1,3), diff --git a/docs/_pages/reso_get_started.md b/docs/_pages/reso_get_started.md index 405e92cbf..5faf3aa42 100644 --- a/docs/_pages/reso_get_started.md +++ b/docs/_pages/reso_get_started.md @@ -253,7 +253,6 @@ cube = magpy.magnet.Cuboid( magnetization=(0,0,1), # in SI Units (T) dimension=(1,1,1), # in SI Units (m) ) -angles = cube.rotate_from_angax( angle=np.linspace(10,360,18), axis='x' @@ -274,7 +273,7 @@ Magpylib's object oriented interface is convenient to work with but is also slow ```python import magpylib as magpy -# Compute the magnetic field via the direct interface. +# Compute the magnetic field via the functional interface. B = magpy.getB( sources="Cuboid", observers=[(-.01, 0, .01), (0, 0, .01), (.01, 0, .01)], # in SI Units (m) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index affe0bccb..bbafb4ccf 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -488,7 +488,7 @@ def getBH_dict_level2( except KeyError as err: raise MagpylibBadUserInput( f"Input parameter `sources` must be one of {list(source_classes)}" - " when using the direct interface." + " when using the functional interface." ) from err kwargs["observers"] = observers @@ -691,7 +691,7 @@ def getB( [[ 8.01875374e-07 8.01875374e-07 1.51582450e-22] [-8.01875374e-07 -8.01875374e-07 1.51582450e-22]]] - Through the direct interface we can compute the same fields for the loop as: + Through the functional interface we can compute the same fields for the loop as: >>> obs = [(.01,.01,.01), (.01,.01,-.01)] >>> B = magpy.getB('Circle', obs, current=100, diameter=.002) @@ -862,7 +862,7 @@ def getH( [[ 6.38112147e-01 6.38112147e-01 1.20625481e-16] [-6.38112147e-01 -6.38112147e-01 1.20625481e-16]]] - Through the direct interface we can compute the same fields for the loop as: + Through the functional interface we can compute the same fields for the loop as: >>> obs = [(.01,.01,.01), (.01,.01,-.01)] >>> H = magpy.getH('Circle', obs, current=100, diameter=.002) From f13815cc3034d5eb90d022abe39f633d1a14a1d6 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 00:57:11 +0100 Subject: [PATCH 135/240] reorder docstring parameters --- .../_src/obj_classes/class_current_Circle.py | 12 ++++++------ .../_src/obj_classes/class_current_Polyline.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 639713ed6..a4830722f 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -22,12 +22,6 @@ class Circle(BaseCurrent): Parameters ---------- - current: float, default=`None` - Electrical current in units of A. - - diameter: float, default=`None` - Diameter of the loop in units of m. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. @@ -37,6 +31,12 @@ class Circle(BaseCurrent): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + diameter: float, default=`None` + Diameter of the loop in units of m. + + current: float, default=`None` + Electrical current in units of A. + parent: `Collection` object or `None` The object is a child of it's parent collection. diff --git a/magpylib/_src/obj_classes/class_current_Polyline.py b/magpylib/_src/obj_classes/class_current_Polyline.py index 733bba918..bb3b15c93 100644 --- a/magpylib/_src/obj_classes/class_current_Polyline.py +++ b/magpylib/_src/obj_classes/class_current_Polyline.py @@ -21,14 +21,6 @@ class Polyline(BaseCurrent): Parameters ---------- - current: float, default=`None` - Electrical current in units of A. - - vertices: array_like, shape (n,3), default=`None` - The current flows along the vertices which are given in units of m in the - local object coordinates (move/rotate with object). At least two vertices - must be given. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` Object position(s) in the global coordinates in units of m. For m>1, the `position` and `orientation` attributes together represent an object path. @@ -38,6 +30,14 @@ class Polyline(BaseCurrent): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + vertices: array_like, shape (n,3), default=`None` + The current flows along the vertices which are given in units of m in the + local object coordinates (move/rotate with object). At least two vertices + must be given. + + current: float, default=`None` + Electrical current in units of A. + parent: `Collection` object or `None` The object is a child of it's parent collection. From 4dfa16b532fe4ff8174e2a0146efe6e6420fcb11 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 00:59:56 +0100 Subject: [PATCH 136/240] reorder param customsource --- .../_src/obj_classes/class_misc_CustomSource.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index d10a556c0..ce821d016 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -14,12 +14,6 @@ class CustomSource(BaseSource): Parameters ---------- - field_func: callable, default=`None` - The function for B- and H-field computation must have the two positional arguments - `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units - of T or A/m must be returned respectively. The `observers` argument must - accept numpy ndarray inputs of shape (n,3), in which case the returned fields must - be numpy ndarrays of shape (n,3) themselves. position: array_like, shape (3,) or (m,3), default=`(0,0,0)` Object position(s) in the global coordinates in units of m. For m>1, the @@ -30,6 +24,13 @@ class CustomSource(BaseSource): a unit-rotation. For m>1, the `position` and `orientation` attributes together represent an object path. + field_func: callable, default=`None` + The function for B- and H-field computation must have the two positional arguments + `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units + of T or A/m must be returned respectively. The `observers` argument must + accept numpy ndarray inputs of shape (n,3), in which case the returned fields must + be numpy ndarrays of shape (n,3) themselves. + parent: `Collection` object or `None` The object is a child of it's parent collection. @@ -84,9 +85,9 @@ class CustomSource(BaseSource): def __init__( self, - field_func=None, position=(0, 0, 0), orientation=None, + field_func=None, style=None, **kwargs, ): From cdc2108d56e8208ba6e0fa687e0de833379ea95b Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 01:45:48 +0100 Subject: [PATCH 137/240] fix units in docu magpylib api --- docs/_pages/docu/docu_magpylib_api.md | 109 ++++++++++++++------------ 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index b530b65ff..7bd12073b 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -24,26 +24,28 @@ For historical reasons Magpylib is by default set up for the following units :::{grid-item} :columns: 10 -| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNIT | -|:---:|:---:|:---:| -| magnetic polarization | `magnetization` | **mT** | -| electric current | `current` | **A** | -| magnetic dipole moment | `moment` | **mT*mm^3** | -| B-field | `getB()` | **mT** | -| H-field | `getH()` | **A/m** | -| length-inputs | `position`, `dimension`, ... | **mm** | -| angle-inputs | `angle`, `dimension`, ... | **deg** | +| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNIT (up to v4)| UNIT (from v5)| +|:---:|:---:|:---:|:---:| +| magnetic polarization | `polarization` | **T** | - | +| magnetization | `magnetization` | **A/m** | **mT** | +| electric current | `current` | **A** | **A** | +| magnetic dipole moment | `moment` | **A·m²** | **mT·mm³** | +| B-field | `getB()` | **mT** | **mT** | +| H-field | `getH()` | **A/m** | **kA/m** | +| length-inputs | `position`, `dimension`, ... | **m** | **mm** | +| angle-inputs | `angle`, `dimension`, ... | **deg** | **deg** | + ::: :::: ```{warning} -Unfortunately Magpylib contributes to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib refers to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. +Up to version 4, Magpylib was unfortunately contributing to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib was refering to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. From version 5 onwards this has been addressed for all the magnet classes (see {ref}`docu-magnet-classes`) ``` The analytical solutions are scale invariant - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant. -In addition, `getB` returns the same unit as given by the `magnetization` input. When the magnetization is given in mT, then `getH` returns A/m which is simply related by factor of $\frac{10}{4\pi}$. +In addition, `getB` returns the same unit as given by the `polarization` input. When the polarization is given in T, then `getH` returns A/m which is simply related by factor of $\frac{1}{µ_0}=\frac{10^7}{4\pi}$. In {ref}`phys-remanence` the connection between the `magnetization` input and the remanence field of a magnet is explained. @@ -109,16 +111,15 @@ Magpylib objects span a local reference frame, and all object properties are def --------------------------------------------- - - +(docu-magnet-classes)= ## Magnet classes -All magnets are sources. They have the **magnetization** attribute which is of the format $(m_x, m_y, m_z)$ and denotes a homogeneous magnetization/polarization vector in the local object coordinates in arbitrary units. Information how this is related to material properties from data sheets is found in the [Physics and Computation](phys-remanence) section. +All magnets are sources. They have the **polarization** attribute ($J$) which is of the format $(p_x, p_y, p_z)$ and denotes a homogeneous magnetic polarization vector in the local object coordinates in units of **T**. The magnetization $M$ can also be set via the **magnetization** attribute of the format $(m_x, m_y, m_z)$. This two parameters are codependent and Magpylib makes sures the parameter stay in sync via the $J=M\cdot\mu_0$ relation. Information on how this is related to material properties from data sheets is found in the [Physics and Computation](phys-remanence) section. ### Cuboid ```python -magpy.magnet.Cuboid(magnetization, dimension, position, orientation, style) +magpy.magnet.Cuboid(position, orientation, dimension, polarization, style) ``` ::::{grid} 2 @@ -135,7 +136,7 @@ magpy.magnet.Cuboid(magnetization, dimension, position, orientation, style) ### Cylinder ```python -magpy.magnet.Cylinder(magnetization, dimension, position, orientation, style) +magpy.magnet.Cylinder(position, orientation, dimension, polarization, style) ``` ::::{grid} 2 @@ -152,7 +153,7 @@ magpy.magnet.Cylinder(magnetization, dimension, position, orientation, style) ### CylinderSegment ```python -magpy.magnet.CylinderSegment(magnetization, dimension, position, orientation, style) +magpy.magnet.CylinderSegment(position, orientation, dimension, polarization, style) ``` ::::{grid} 2 @@ -173,7 +174,7 @@ magpy.magnet.CylinderSegment(magnetization, dimension, position, orientation, st ### Sphere ```python -magpy.magnet.Sphere(magnetization, diameter, position, orientation, style) +magpy.magnet.Sphere(position, orientation, diameter, polarization, style) ``` ::::{grid} 2 @@ -190,7 +191,7 @@ magpy.magnet.Sphere(magnetization, diameter, position, orientation, style) ### Tetrahedron ```python -magpy.magnet.Tetrahedron(magnetization, vertices, position, orientation, style) +magpy.magnet.Tetrahedron(position, orientation, vertices, polarization, style) ``` ::::{grid} 2 @@ -212,7 +213,7 @@ magpy.magnet.Tetrahedron(magnetization, vertices, position, orientation, style) ### TriangularMesh ```python -magpy.magnet.TriangularMesh(magnetization, vertices, faces, position, orientation, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) +magpy.magnet.TriangularMesh(position, orientation, vertices, faces, polarization, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) ``` ::::{grid} 2 @@ -243,7 +244,7 @@ The checks can also be performed after initialization using the methods * **check_selfintersecting()** * **reorient_faces()** -The following class methods enable easy mesh loading and creating. They all take the mandatory **magnetization** argument, which overwrites possible magnetization from other inputs, as well as the optional mesh check parameters (see above). +The following class methods enable easy mesh loading and creating. They all take the mandatory **polarization** argument, which overwrites possible polarization from other inputs, as well as the optional mesh check parameters (see above). * **TriangularMesh.from_mesh()** requires the input **mesh**, which is an array in the mesh format $[(P_1^1, P_2^1, P_3^1), (P_1^2, P_2^2, P_3^2), ...]$. * **TriangularMesh.from_ConvexHull()** requires the input **points**, which is an array of positions $(P_1, P_2, P_3, ...)$ from which the convex Hull is computed via the [Scipy ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) implementation. @@ -272,7 +273,7 @@ All currents are sources. Current objects have the * ### Circle ```python -magpy.current.Circle(current, diameter, position, orientation, style) +magpy.current.Circle(position, orientation, current, diameter, style) ``` ::::{grid} 2 @@ -288,7 +289,7 @@ magpy.current.Circle(current, diameter, position, orientation, style) ### Polyline ```python -magpy.current.Polyline(current, vertices, position, orientation, style) +magpy.current.Polyline(position, orientation, vertices, current, style) ``` ::::{grid} 2 @@ -311,7 +312,7 @@ There are classes listed hereon that function as sources, but they do not repres ### Dipole ```python -magpy.misc.Dipole(moment, position, orientation, style) +magpy.misc.Dipole(position, orientation, moment, style) ``` ::::{grid} 2 @@ -325,20 +326,20 @@ magpy.misc.Dipole(moment, position, orientation, style) ::: :::{grid-item} :columns: 12 -**Info:** For homogeneous magnets the relation moment=magnetization$\times$volume holds. +**Info:** For homogeneous magnets the relation moment=polarization$\times$volume holds. ::: :::: ### Triangle ```python -magpy.misc.Triangle(magnetization, vertices, position, orientation, style) +magpy.misc.Triangle(position, orientation, vertices, polarization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Triangle` represents a triangular surface with a homogeneous charge density given by the projection of the magnetization vector onto the surface normal. The **magnetization** attribute stores the magnetization vector $(m_x,m_y,m_z)$ in arbitrary units. The **vertices** attribute is a set of the three triangle corners $(P_1, P_2, P_3)$ in arbitrary length units in the local coordinates. +`Triangle` represents a triangular surface with a homogeneous charge density given by the projection of the polarization vector onto the surface normal. The **polarization** attribute stores the polarization vector $(m_x,m_y,m_z)$ in arbitrary units. The **vertices** attribute is a set of the three triangle corners $(P_1, P_2, P_3)$ in arbitrary length units in the local coordinates. ::: :::{grid-item} :columns: 3 @@ -346,7 +347,7 @@ magpy.misc.Triangle(magnetization, vertices, position, orientation, style) ::: :::{grid-item} :columns: 12 -**Info:** When multiple Triangles with similar magnetization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. The B-field is only correct on the outside of the body. On the inside the magnetization must be added to the field. This is demonstrated in the tutorial {ref}`gallery-shapes-triangle`. +**Info:** When multiple Triangles with similar polarization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. The B-field is only correct on the outside of the body. On the inside the polarization must be added to the field. This is demonstrated in the tutorial {ref}`gallery-shapes-triangle`. ::: :::: @@ -510,6 +511,7 @@ The practical application of this formalism is best demonstrated by the followin ```python import magpylib as magpy +# Note that all units are in SI sensor = magpy.Sensor() print(sensor.position) # default value @@ -640,21 +642,22 @@ that compute the magnetic field generated by `sources` as seen by the `observers :columns: 8 ```python import magpylib as magpy +# Note that all units are in SI # define source and observer objects -loop = magpy.current.Circle(current=1, diameter=1) +loop = magpy.current.Circle(current=1, diameter=.001) sens = magpy.Sensor() # compute field B = magpy.getB(loop, sens) print(B) -# --> [0. 0. 1.2566] +# --> [0. 0. 0.00125664] ``` ::: :::: -The physical unit returned by `getB` and `getH` corresponds to the source excitation input units. For example, when magnet `magnetization` is given in mT, `getB` returns the B-field in units of mT and `getH` the H-field in units of A/m. This is described in detail in {ref}`docu-units`. +The physical unit returned by `getB` and `getH` corresponds to the source excitation input units. For example, when magnet `polarization` is given in mT, `getB` returns the B-field in units of mT and `getH` the H-field in units of A/m. This is described in detail in {ref}`docu-units`. The output of a field computation `magpy.getB(sources, observers)` is by default a Numpy array of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of observers, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position array input and `3` the three magnetic field components $(B_x, B_y, B_z)$. @@ -695,20 +698,25 @@ All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to c :::{grid-item} :columns: 8 ```python +import numpy as np import magpylib as magpy +# Note that all units are in SI +# This example also nicely shows the scale invariance +# by increasing the distance proportionally to the dimension. # compute the cuboid field for 3 input instances +N = 3 # number of instances B = magpy.getB( sources='Cuboid', - observers=[(0,0,x) for x in range(3)], - dimension=[(d,d,d) for d in range(1,4)], - magnetization=(0,0,1000), + observers=np.linspace((0,0,1), (0,0,3), N), + dimension=np.linspace((1,1,1), (3,3,3),3, N), + polarization=(0,0,1), ) -print(B.round()) -# --> [[ 0. 0. 667.] -# [ 0. 0. 436.] -# [ 0. 0. 307.]] +print(B) +# --> [[0. 0. 0.13478239] +# [0. 0. 0.13478239] +# [0. 0. 0.13478239]] ``` ::: :::: @@ -733,25 +741,25 @@ At the heart of Magpylib lies a set of core functions that are our implementatio **current_circle_field(** `field`, `observers`, `current`, `diameter`**)** ::: :::{grid-item} -**magnet_cuboid_field(** `field`, `observers`, `magnetization`, `dimension`**)** +**magnet_cuboid_field(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} -**magnet_cylinder_field(** `field`, `observers`, `magnetization`, `dimension`**)** +**magnet_cylinder_field(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} -**magnet_cylinder_segment_field(** `field`, `observers`, `magnetization`, `dimension`**)** +**magnet_cylinder_segment_field(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} -**magnet_sphere_field(** `field`, `observers`, `magnetization`, `diameter`**)** +**magnet_sphere_field(** `field`, `observers`, `polarization`, `diameter`**)** ::: :::{grid-item} -**magnet_tetrahedron_field(** `field`, `observers`, `magnetization`, `vertices`**)** +**magnet_tetrahedron_field(** `field`, `observers`, `polarization`, `vertices`**)** ::: :::{grid-item} **dipole_field(** `field`, `observers`, `moment`**)** ::: :::{grid-item} -**triangle_field(** `field`, `observers`, `magnetization`, `vertices`**)** +**triangle_field(** `field`, `observers`, `polarization`, `vertices`**)** ::: :::: @@ -769,19 +777,20 @@ The input `field` is either `"B"` or `"H`. All other inputs must be Numpy ndarra ```python import numpy as np import magpylib as magpy +# Note that all units are in SI # prepare input -mag = np.array([(1000,0,0)]*3) +pol = np.array([(1,0,0)]*3) dim = np.array([(1,2)]*3) obs = np.array([(0,0,0)]*3) # compute field with core functions -B = magpy.core.magnet_cylinder_field('B', obs, mag, dim) +B = magpy.core.magnet_cylinder_field(field='B', observers=obs, polarization=pol, dimension=dim) -print(B.round()) -# --> [[553. 0. 0.] -# [553. 0. 0.] -# [553. 0. 0.]] +print(B) +# --> [[0.5527864 0. 0. ] +# [0.5527864 0. 0. ] +# [0.5527864 0. 0. ]] ``` ::: :::: From d7846dfc13865402e91af7f8287b91223dc766c8 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 01:52:50 +0100 Subject: [PATCH 138/240] Fix end of shaft --- .../gallery/gallery_app_end_of_shaft.md | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/_pages/gallery/gallery_app_end_of_shaft.md b/docs/_pages/gallery/gallery_app_end_of_shaft.md index 68d119831..28d67569b 100644 --- a/docs/_pages/gallery/gallery_app_end_of_shaft.md +++ b/docs/_pages/gallery/gallery_app_end_of_shaft.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: - display_name: Python 3 (ipykernel) + display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-app-end-of-shaft)= @@ -28,40 +28,40 @@ import plotly.graph_objects as go # create magnet magnet = magpy.magnet.Cylinder( - magnetization=(1000, 0, 0), - dimension=(6, 2), - position=(0, 0, 1.5), + polarization=(1, 0, 0), + dimension=(.06, .02), + position=(0, 0, .015), style_label="Magnet", style_color=".7", ) # create shaft dummy with 3D model shaft = magpy.misc.CustomSource( - position=(0, 0, 7), + position=(0, 0, .07), style_color=".7", style_model3d_showdefault=False, style_label="Shaft", ) shaft_trace = magpy.graphics.model3d.make_Prism( base=20, - diameter=10, - height=10, + diameter=.1, + height=.1, opacity=0.3, ) shaft.style.model3d.add_trace(shaft_trace) # shaft rotation / magnet wobble motion -displacement = 1 +displacement = .01 angles = np.linspace(0, 360, 72) coll = magnet + shaft magnet.move((displacement, 0, 0)) coll.rotate_from_angax(angles, "z", anchor=0, start=0) # create sensor -gap = 3 +gap = .03 sens = magpy.Sensor( position=(0, 0, -gap), - pixel=[(1, 0, 0), (-1, 0, 0)], + pixel=[(.01, 0, 0), (-.01, 0, 0)], style_pixel_size=0.5, style_size=1.5, ) @@ -86,3 +86,7 @@ fig2 = px.line( ) fig2.show() ``` + +```{code-cell} ipython3 + +``` From 0956c46849dea97f401de56f508efe1a58f9118e Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:02:58 +0100 Subject: [PATCH 139/240] fix compound example --- docs/_pages/gallery/gallery_misc_compound.md | 29 ++++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/_pages/gallery/gallery_misc_compound.md b/docs/_pages/gallery/gallery_misc_compound.md index 5880c0393..29753c789 100644 --- a/docs/_pages/gallery/gallery_misc_compound.md +++ b/docs/_pages/gallery/gallery_misc_compound.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-misc-compound)= @@ -51,7 +51,7 @@ class MagnetRing(magpy.Collection): def _update(self, cubes): """Update MagnetRing instance""" self._cubes = cubes - ring_radius = cubes/3 + ring_radius = cubes/300 # Store existing path pos_temp = self.position @@ -65,8 +65,8 @@ class MagnetRing(magpy.Collection): # Add children for i in range(cubes): child = magpy.magnet.Cuboid( - magnetization=(0,0,1000), - dimension=(1,1,1), + polarization=(0,0,1), + dimension=(.01,.01,.01), position=(ring_radius,0,0) ) child.rotate_from_angax(360/cubes*i, 'z', anchor=0) @@ -78,7 +78,7 @@ class MagnetRing(magpy.Collection): # Add parameter-dependent 3d trace trace = magpy.graphics.model3d.make_CylinderSegment( - dimension=(cubes/3-.6, cubes/3+0.6, 1.1, 0, 360), + dimension=(ring_radius-.006, ring_radius+.006, 0.011, 0, 360), vert=150, opacity=0.2, ) @@ -109,12 +109,11 @@ magpy.show(ring, sensor, backend='plotly') The `MagnetRing` parameter `cubes` can be modified dynamically: ```{code-cell} ipython3 - -print(f"B-field at sensor for modified ring → {ring.getB(sensor).round(2)}") +print(f"B-field at sensor for modified ring → {ring.getB(sensor).round(3)}") ring.cubes = 10 -print(f"B-field at sensor for modified ring → {ring.getB(sensor).round(2)}") +print(f"B-field at sensor for modified ring → {ring.getB(sensor).round(3)}") magpy.show(ring, sensor, backend='plotly') ``` @@ -155,7 +154,7 @@ class MagnetRingAdv(magpy.Collection): def _update(self, cubes): """Update MagnetRing instance""" self._cubes = cubes - ring_radius = cubes/3 + ring_radius = cubes/300 # Store existing path and reset pos_temp = self.position @@ -165,8 +164,8 @@ class MagnetRingAdv(magpy.Collection): # Add children for i in range(cubes): child = magpy.magnet.Cuboid( - magnetization=(0,0,1000), - dimension=(1,1,1), + polarization=(0,0,1), + dimension=(.01,.01,.01), position=(ring_radius,0,0) ) child.rotate_from_angax(360/cubes*i, 'z', anchor=0) @@ -181,7 +180,7 @@ class MagnetRingAdv(magpy.Collection): def _custom_trace3d(self): """ creates a parameter-dependent 3d model""" trace = magpy.graphics.model3d.make_CylinderSegment( - dimension=(self.cubes/3-.6, self.cubes/3+0.6, 1.1, 0, 360), + dimension=(self.cubes/300-.006, self.cubes/300+0.006, 0.011, 0, 360), vert=150, opacity=0.2, ) @@ -192,10 +191,10 @@ We have removed the trace construction from the `_update` method, and instead pr ```{code-cell} ipython3 ring0 = MagnetRing() -%time for _ in range(100): ring0.cubes=10 +%time for _ in range(10): ring0.cubes=10 ring1 = MagnetRingAdv() -%time for _ in range(100): ring1.cubes=10 +%time for _ in range(10): ring1.cubes=10 ``` This example is not very impressive because the provided trace is not very heavy. From f4375b0603e795123e29863b9b3e3d641874df8c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:08:31 +0100 Subject: [PATCH 140/240] add virutal documents (from myst & jupytext) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fb92fc1ed..5affe0c87 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints *.ipynb +.virtual_documents/ # IPython profile_default/ From 7df406c39579c6b515ab57f203741a68c4be7440 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:08:46 +0100 Subject: [PATCH 141/240] fix field interpolation --- .../gallery_misc_field_interpolation.md | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/_pages/gallery/gallery_misc_field_interpolation.md b/docs/_pages/gallery/gallery_misc_field_interpolation.md index 532fc52d9..bc486bfb5 100644 --- a/docs/_pages/gallery/gallery_misc_field_interpolation.md +++ b/docs/_pages/gallery/gallery_misc_field_interpolation.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.6 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-misc-field-interpolation)= @@ -88,8 +88,8 @@ In the second step we create a custom source with an interpolated field `field_f ```{code-cell} ipython3 # Create data for interpolation -cube = magpy.magnet.Cuboid(magnetization=(0,0,1000), dimension=(2,2,2)) -ts = np.linspace(-7, 7, 21) +cube = magpy.magnet.Cuboid(polarization=(0,0,1), dimension=(.02,.02,.02)) +ts = np.linspace(-.07, .07, 21) grid = np.array([(x,y,z) for x in ts for y in ts for z in ts]) data = cube.getB(grid) @@ -100,9 +100,9 @@ custom = magpy.misc.CustomSource( ) # Add nice 3D model (dashed outline) to custom source -xs = 1.1*np.array([-1, -1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1]) -ys = 1.1*np.array([-1, 1, 1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1]) -zs = 1.1*np.array([-1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1]) +xs = 0.011*np.array([-1, -1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1]) +ys = 0.011*np.array([-1, 1, 1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1]) +zs = 0.011*np.array([-1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1]) trace = dict( backend='matplotlib', constructor='plot', @@ -128,7 +128,7 @@ for src in [cube, custom]: src.rotate_from_angax(angle=45, axis=(1,1,1)) # Add a sensor for testing -sensor = magpy.Sensor(position=(-5,0,0)) +sensor = magpy.Sensor(position=(-.05,0,0)) angs = np.linspace(3,150,49) sensor.rotate_from_angax(angle=angs, axis="y", anchor=0) @@ -151,8 +151,12 @@ ax.grid(color=".9") ax.set( title="Field at sensor - real (solid), interpolated (dashed)", xlabel="sensor rotation angle (deg)", - ylabel="(mT)", + ylabel="(T)", ) plt.show() -``` \ No newline at end of file +``` + +```{code-cell} ipython3 + +``` From 419ba7db5c83676fac7d26d1e96f11b5cfe26535 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:12:02 +0100 Subject: [PATCH 142/240] fix shapes convex hull --- .../_pages/gallery/gallery_shapes_convex_hull.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/_pages/gallery/gallery_shapes_convex_hull.md b/docs/_pages/gallery/gallery_shapes_convex_hull.md index 850e7d8f5..e125fc3b5 100644 --- a/docs/_pages/gallery/gallery_shapes_convex_hull.md +++ b/docs/_pages/gallery/gallery_shapes_convex_hull.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-shapes-convex-hull)= @@ -25,16 +25,22 @@ Magpylib offers construction of convex hull magnets by combining the `magpylib.m This is the fastest way to construct a pyramid magnet. ```{code-cell} ipython3 +import numpy as np + import magpylib as magpy # Create pyramid magnet -points = [(-2,-2,0), (-2,2,0), (2,-2,0), (2,2,0), (0,0,3)] +points = np.array([(-2, -2, 0), (-2, 2, 0), (2, -2, 0), (2, 2, 0), (0, 0, 3)]) / 100 tmesh_pyramid = magpy.magnet.TriangularMesh.from_ConvexHull( - magnetization=(0, 0, 1000), + polarization=(0, 0, 1), points=points, style_label="Pyramid Magnet", ) # Display graphically tmesh_pyramid.show(backend="plotly") -``` \ No newline at end of file +``` + +```{code-cell} ipython3 + +``` From fc22ca238920fda44e195a1c889698bb1a417030 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:23:17 +0100 Subject: [PATCH 143/240] fix shapes pyvista --- docs/_pages/gallery/gallery_shapes_pyvista.md | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/_pages/gallery/gallery_shapes_pyvista.md b/docs/_pages/gallery/gallery_shapes_pyvista.md index 31429409a..5c6657095 100644 --- a/docs/_pages/gallery/gallery_shapes_pyvista.md +++ b/docs/_pages/gallery/gallery_shapes_pyvista.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-shapes-pyvista)= @@ -32,21 +32,20 @@ import pyvista as pv import magpylib as magpy # Create a simple pyvista PolyData object -dodec_mesh = pv.Dodecahedron() +dodec_mesh = pv.Dodecahedron(radius=.01) dodec = magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 100), + polarization=(0, 0, .1), polydata=dodec_mesh, ) # Add a sensor with path -sens = magpy.Sensor(position=np.linspace((-2,0,1), (2,0,1), 100)) +sens = magpy.Sensor(position=np.linspace((-2,0,1), (2,0,1), 100)/100) # Show system and field with magpy.show_context(dodec, sens, backend='plotly') as s: s.show(col=1) s.show(col=2, output=['Bx', 'Bz']) - ``` ## Boolean operations with Pyvista @@ -58,13 +57,13 @@ import pyvista as pv import magpylib as magpy # Create a complex pyvista PolyData object using a boolean operation -sphere = pv.Sphere(radius=0.6) -cube = pv.Cube().triangulate() +sphere = pv.Sphere(radius=0.006) +cube = pv.Cube(x_length=.01, y_length=.01, z_length=.01).triangulate() obj = cube.boolean_difference(sphere) # Construct magnet from PolyData object and ignore check results magnet = magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 100), + polarization=(0, 0, .1), polydata=obj, check_disconnected="ignore", check_open="ignore", @@ -101,10 +100,11 @@ sphere = pv.Sphere(radius=0.6) cube = pv.Cube().triangulate().subdivide(2) obj = cube.boolean_difference(sphere) obj = obj.clean() +obj = obj.scale([1e-2]*3) # Construct magnet from PolyData object magnet = magpy.magnet.TriangularMesh.from_pyvista( - magnetization=(0, 0, 100), + polarization=(0, 0, .1), polydata=obj, style_label="magnet", ) @@ -116,3 +116,11 @@ print(f'mesh status reoriented: {magnet.status_reoriented}') magnet.show(backend="plotly") ``` + +```{code-cell} ipython3 + +``` + +```{code-cell} ipython3 + +``` From d5bb0f7a4c35edd2e49e87dabe4b9c79402fe178 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:45:42 +0100 Subject: [PATCH 144/240] bump to v5 dev version --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- magpylib/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7332f6b46..28321df99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to magpylib are documented here. # Changelog -## [UNRELEASED] +## [5.0.0dev] ## [4.5.1] - Fixed a field computatio issue where H-field resulting from axial magnetization is computed incorrectly inside of Cylinders ([#703](https://github.com/magpylib/magpylib/issues/703)) @@ -441,7 +441,7 @@ The first official release of the magpylib library. --- -[UNRELEASED]:https://github.com/magpylib/magpylib/compare/4.5.1...HEAD +[5.0.0dev]:https://github.com/magpylib/magpylib/compare/4.5.1...HEAD [4.5.1]:https://github.com/magpylib/magpylib/compare/4.5.0...4.5.1 [4.5.0]:https://github.com/magpylib/magpylib/compare/4.4.0...4.5.0 [4.4.1]:https://github.com/magpylib/magpylib/compare/4.4.0...4.4.1 diff --git a/README.md b/README.md index 3212d5707..70e2e19ff 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Conda Cloud - MyBinder link + MyBinder link black @@ -136,7 +136,7 @@ A valid software citation could be author = {{Michael-Ortner et al.}}, title = {magpylib}, url = {https://magpylib.readthedocs.io/en/latest/}, - version = {4.5.1}, + version = {5.0.0dev}, date = {2023-06-25}, } ``` diff --git a/magpylib/__init__.py b/magpylib/__init__.py index 9c598af7b..fa518fc42 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -28,7 +28,7 @@ """ # module level dunders -__version__ = "4.5.1" +__version__ = "5.0.0dev" __author__ = "Michael Ortner & Alexandre Boisselet" __credits__ = "The Magpylib community" __all__ = [ From a0f45083b431c00ee646dd195d61b5e343c15616 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 02:52:18 +0100 Subject: [PATCH 145/240] bad columns v5 vs v4 --- docs/_pages/docu/docu_magpylib_api.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index 7bd12073b..c5df5f4b8 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -24,16 +24,16 @@ For historical reasons Magpylib is by default set up for the following units :::{grid-item} :columns: 10 -| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNIT (up to v4)| UNIT (from v5)| +| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNIT (from v5)| UNIT (up to v4)| |:---:|:---:|:---:|:---:| -| magnetic polarization | `polarization` | **T** | - | -| magnetization | `magnetization` | **A/m** | **mT** | -| electric current | `current` | **A** | **A** | -| magnetic dipole moment | `moment` | **A·m²** | **mT·mm³** | -| B-field | `getB()` | **mT** | **mT** | -| H-field | `getH()` | **A/m** | **kA/m** | -| length-inputs | `position`, `dimension`, ... | **m** | **mm** | -| angle-inputs | `angle`, `dimension`, ... | **deg** | **deg** | +| magnetic polarization | `polarization` | **T** | - | +| magnetization | `magnetization` | **A/m** | mT | +| electric current | `current` | **A** | A | +| magnetic dipole moment | `moment` | **A·m²** | mT·mm³ | +| B-field | `getB()` | **mT** | mT | +| H-field | `getH()` | **A/m** | kA/m | +| length-inputs | `position`, `dimension`, ... | **m** | mm | +| angle-inputs | `angle`, `dimension`, ... | **deg** | deg | ::: From b1381e831b6f28f27b9a194941a2d86937752a38 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 10:03:40 +0100 Subject: [PATCH 146/240] fix shapes superpos --- .../_pages/gallery/gallery_shapes_superpos.md | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/_pages/gallery/gallery_shapes_superpos.md b/docs/_pages/gallery/gallery_shapes_superpos.md index c6e3e3a5b..1878b777f 100644 --- a/docs/_pages/gallery/gallery_shapes_superpos.md +++ b/docs/_pages/gallery/gallery_shapes_superpos.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-shapes-superpos)= @@ -42,21 +42,23 @@ Geometric union by superposition is demonstrated in the following example where ```{code-cell} ipython3 import numpy as np + import magpylib as magpy -# Create three magnet parts with similar magnetization +# Create three magnet parts with similar polarization pt1 = magpy.magnet.CylinderSegment( - magnetization=(500,0,0), - dimension=(0,4,2,90,270), + polarization=(0.5, 0, 0), + dimension=(0, 0.04, 0.02, 90, 270), ) pt2 = magpy.magnet.Cuboid( - magnetization=(500,0,0), - dimension=(2,8,2), - position=(1,0,0) + polarization=(0.5, 0, 0), dimension=(0.02, 0.08, 0.02), position=(0.01, 0, 0) ) pt3 = magpy.magnet.TriangularMesh.from_ConvexHull( - magnetization=(500,0,0), - points=[(2,4,-1),(2,4,1),(2,-4,-1),(2,-4,1),(6,0,1),(6,0,-1)] + polarization=(0.5, 0, 0), + points=np.array( + [(2, 4, -1), (2, 4, 1), (2, -4, -1), (2, -4, 1), (6, 0, 1), (6, 0, -1)] + ) + / 100, ) # Combine parts in a Collection @@ -64,15 +66,14 @@ magnet = magpy.Collection(pt1, pt2, pt3) # Add a sensor with path sensor = magpy.Sensor() -sensor.position = np.linspace((7,-10,0), (7,10,0), 100) +sensor.position = np.linspace((7, -10, 0), (7, 10, 0), 100) / 100 # Plot -with magpy.show_context(magnet, sensor, backend='plotly', style_legend_show=False) as s: +with magpy.show_context(magnet, sensor, backend="plotly", style_legend_show=False) as s: s.show(col=1) - s.show(output='B', col=2) + s.show(output="B", col=2) ``` - ## Cut-out operation When two objects with opposing magnetization vectors of similar amplitude overlap, they will just cancel in the overlap region. This enables geometric cut-out operations. In the following example we construct an exact hollow cylinder solution from two concentric cylinder shapes with opposite magnetizations, and compare the result to the `CylinderSegment` class solution. @@ -81,16 +82,16 @@ When two objects with opposing magnetization vectors of similar amplitude overla from magpylib.magnet import Cylinder, CylinderSegment # Create ring with CylinderSegment -ring0 = CylinderSegment(magnetization=(0,0,100), dimension=(2,3,1,0,360)) +ring0 = CylinderSegment(polarization=(0, 0, .1), dimension=(.02, .03, .01, 0, 360)) # Create ring with cut-out -inner = Cylinder(magnetization=(0,0,-100), dimension=(4,1)) -outer = Cylinder(magnetization=(0,0, 100), dimension=(6,1)) +inner = Cylinder(polarization=(0, 0, -.1), dimension=(.04, .01)) +outer = Cylinder(polarization=(0, 0, .1), dimension=(.06, .01)) ring1 = inner + outer # Print results -print('CylinderSegment result:', ring0.getB((1,2,3))) -print('Cut-out result: ', ring1.getB((1,2,3))) +print("CylinderSegment result:", ring0.getB((.01, .02, .03))) +print(" Cut-out result:", ring1.getB((.01, .02, .03))) ``` Note that, it is faster to compute the `Cylinder` field two times than computing the `CylinderSegment` field one time. This is why Magpylib automatically falls back to the `Cylinder` solution whenever `CylinderSegment` is called with 360 deg section angles. Unfortunately, cut-out operations cannot be displayed graphically at the moment, but {ref}`examples-own-3d-models` offer a solution here. From b7f1ab693b03e3175c1a8a04cd9df3a0b90d47c1 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 10:11:30 +0100 Subject: [PATCH 147/240] fix shapes triangle --- .../_pages/gallery/gallery_shapes_triangle.md | 161 ++++++++++-------- 1 file changed, 92 insertions(+), 69 deletions(-) diff --git a/docs/_pages/gallery/gallery_shapes_triangle.md b/docs/_pages/gallery/gallery_shapes_triangle.md index 5c1ba4bd2..34c22c811 100644 --- a/docs/_pages/gallery/gallery_shapes_triangle.md +++ b/docs/_pages/gallery/gallery_shapes_triangle.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-shapes-triangle)= @@ -30,36 +30,39 @@ It is very common to approximate the surface of bodies by triangular meshes, whi In this example `Triangle` is used to create a magnet with cuboctahedral shape. Notice that triangle orientation is displayed by default for convenience. ```{code-cell} ipython3 +import numpy as np + import magpylib as magpy # Create collection of triangles -triangles = [ - ([ 0, 1,-1], [-1, 1, 0], [ 1, 1, 0]), - ([ 0, 1, 1], [ 1, 1, 0], [-1, 1, 0]), - ([ 0, 1, 1], [-1, 0, 1], [ 0,-1, 1]), - ([ 0, 1, 1], [ 0,-1, 1], [ 1, 0, 1]), - ([ 0, 1,-1], [ 1, 0,-1], [ 0,-1,-1]), - ([ 0, 1,-1], [ 0,-1,-1], [-1, 0,-1]), - ([ 0,-1, 1], [-1,-1, 0], [ 1,-1, 0]), - ([ 0,-1,-1], [ 1,-1, 0], [-1,-1, 0]), - ([-1, 1, 0], [-1, 0,-1], [-1, 0, 1]), - ([-1,-1, 0], [-1, 0, 1], [-1, 0,-1]), - ([ 1, 1, 0], [ 1, 0, 1], [ 1, 0,-1]), - ([ 1,-1, 0], [ 1, 0,-1], [ 1, 0, 1]), - ([ 0, 1, 1], [-1, 1, 0], [-1, 0, 1]), - ([ 0, 1, 1], [ 1, 0, 1], [ 1, 1, 0]), - ([ 0, 1,-1], [-1, 0,-1], [-1, 1, 0]), - ([ 0, 1,-1], [ 1, 1, 0], [ 1, 0,-1]), - ([ 0,-1,-1], [-1,-1, 0], [-1, 0,-1]), - ([ 0,-1,-1], [ 1, 0,-1], [ 1,-1, 0]), - ([ 0,-1, 1], [-1, 0, 1], [-1,-1, 0]), - ([ 0,-1, 1], [ 1,-1, 0], [ 1, 0, 1]), +triangles_mm = [ + ([0, 1, -1], [-1, 1, 0], [1, 1, 0]), + ([0, 1, 1], [1, 1, 0], [-1, 1, 0]), + ([0, 1, 1], [-1, 0, 1], [0, -1, 1]), + ([0, 1, 1], [0, -1, 1], [1, 0, 1]), + ([0, 1, -1], [1, 0, -1], [0, -1, -1]), + ([0, 1, -1], [0, -1, -1], [-1, 0, -1]), + ([0, -1, 1], [-1, -1, 0], [1, -1, 0]), + ([0, -1, -1], [1, -1, 0], [-1, -1, 0]), + ([-1, 1, 0], [-1, 0, -1], [-1, 0, 1]), + ([-1, -1, 0], [-1, 0, 1], [-1, 0, -1]), + ([1, 1, 0], [1, 0, 1], [1, 0, -1]), + ([1, -1, 0], [1, 0, -1], [1, 0, 1]), + ([0, 1, 1], [-1, 1, 0], [-1, 0, 1]), + ([0, 1, 1], [1, 0, 1], [1, 1, 0]), + ([0, 1, -1], [-1, 0, -1], [-1, 1, 0]), + ([0, 1, -1], [1, 1, 0], [1, 0, -1]), + ([0, -1, -1], [-1, -1, 0], [-1, 0, -1]), + ([0, -1, -1], [1, 0, -1], [1, -1, 0]), + ([0, -1, 1], [-1, 0, 1], [-1, -1, 0]), + ([0, -1, 1], [1, -1, 0], [1, 0, 1]), ] +triangles = np.array(triangles) / 100 cuboc = magpy.Collection() for t in triangles: cuboc.add( magpy.misc.Triangle( - magnetization=(100, 200, 300), + polarization=(0.1, 0.2, 0.3), vertices=t, ) ) @@ -67,10 +70,9 @@ for t in triangles: # Display collection of triangles magpy.show( cuboc, - backend='pyvista', - style_magnetization_mode='arrow', - style_orientation_color='yellow', - jupyter_backend="panel", # better pyvista rendering in a jupyter notebook + backend="pyvista", + style_magnetization_mode="arrow", + style_orientation_color="yellow", ) ``` @@ -85,24 +87,19 @@ import magpylib as magpy # Create prism magnet as triangle collection top = magpy.misc.Triangle( - magnetization=(0,0,1000), - vertices=((-1,-1,1), (1,-1,1), (0,2,1)), - style_label="top" + polarization=(0, 0, 1), + vertices=((-0.01, -0.01, 0.01), (0.01, -0.01, 0.01), (0, 0.02, 0.01)), + style_label="top", ) bott = magpy.misc.Triangle( - magnetization=(0,0,1000), - vertices=((-1,-1,-1), (0,2,-1), (1,-1,-1)), - style_label="bottom" + polarization=(0, 0, 1), + vertices=((-0.01, -0.01, -0.01), (0, 0.02, -0.01), (0.01, -0.01, -0.01)), + style_label="bottom", ) prism = magpy.Collection(top, bott) # Display graphically -magpy.show( - *prism, - backend='plotly', - style_opacity=0.5, - style_magnetization_show=False -) +magpy.show(*prism, backend="plotly", style_opacity=0.5, style_magnetization_show=False) ``` ## TriangularMesh class @@ -116,34 +113,59 @@ Automatic face reorientation of `TriangularMesh` may fail when the mesh is open. In this example we revisit the cubeoctahedron, but generate it through the `TriangularMesh` class. ```{code-cell} ipython3 +import numpy as np + import magpylib as magpy -# Create cubeoctahedron magnet -vertices = [ - ( 0, 1,-1), (-1, 1, 0), ( 1, 1, 0), - ( 0, 1, 1), (-1, 0, 1), ( 0,-1, 1), - ( 1, 0, 1), ( 1, 0,-1), ( 0,-1,-1), - (-1, 0,-1), (-1,-1, 0), ( 1,-1, 0), -] +# Create cubeoctahedron magnet +vertices = ( + np.array( + [ + (0, 1, -1), + (-1, 1, 0), + (1, 1, 0), + (0, 1, 1), + (-1, 0, 1), + (0, -1, 1), + (1, 0, 1), + (1, 0, -1), + (0, -1, -1), + (-1, 0, -1), + (-1, -1, 0), + (1, -1, 0), + ] + ) + / 100 +) faces = [ - (0,1,2), (3,2,1), (3,4,5), (3,5,6), - (0,7,8), (0,8,9), (5,10,11), (8,11,10), - (1,9,4), (10,4,9), (2,6,7), (11,7,6), - (3,1,4), (3,6,2), (0,9,1), (0,2,7), - (8,10,9), (8,7,11), (5,4,10), (5,11,6), + (0, 1, 2), + (3, 2, 1), + (3, 4, 5), + (3, 5, 6), + (0, 7, 8), + (0, 8, 9), + (5, 10, 11), + (8, 11, 10), + (1, 9, 4), + (10, 4, 9), + (2, 6, 7), + (11, 7, 6), + (3, 1, 4), + (3, 6, 2), + (0, 9, 1), + (0, 2, 7), + (8, 10, 9), + (8, 7, 11), + (5, 4, 10), + (5, 11, 6), ] cuboc = magpy.magnet.TriangularMesh( - magnetization=(100, 200, 300), - vertices=vertices, - faces=faces + polarization=(0.1, 0.2, 0.3), vertices=vertices, faces=faces ) # Display TriangularMesh body magpy.show( - cuboc, - backend='plotly', - style_mesh_grid_show=True, - style_mesh_grid_line_width=4 + cuboc, backend="plotly", style_mesh_grid_show=True, style_mesh_grid_line_width=4 ) ``` @@ -164,28 +186,29 @@ The `TriangularMesh` class is extremely powerful as it enables almost arbitrary In some cases it may be desirable to generate a `TriangularMesh` object from an open mesh (see Prism example above). In this case one has to be extremely careful because one cannot rely on the checks. Not to generate warnings or error messages, these checks can be disabled with `"skip"` or their outcome can be ignored with `"ignore"`. The `show` function can be used to view open edges and disconnected parts. In the following example we generate such an open mesh directly from `Triangle` objects. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + # Create top and bottom faces of a prism magnet top = magpy.misc.Triangle( - magnetization=(1000,0,0), - vertices= ((-1,-1,1), (1,-1,1), (0,2,1)), + polarization=(1, 0, 0), + vertices=((-0.01, -0.01, 0.01), (0.01, -0.01, 0.01), (0, 0.02, 0.01)), ) bottom = magpy.misc.Triangle( - magnetization=(1000,0,0), - vertices= ((-1,-1,-1), (0,2,-1), (1,-1,-1)), + polarization=(1, 0, 0), + vertices=((-0.01, -0.01, -0.01), (0, 0.02, -0.01), (0.01, -0.01, -0.01)), ) # Create prism with open edges prism = magpy.magnet.TriangularMesh.from_triangles( - magnetization=(0, 0, 1000), # overrides triangles magnetization + polarization=(0, 0, 1), # overrides triangles magnetization triangles=[top, bottom], - check_open="ignore", # check but ignore open mesh + check_open="ignore", # check but ignore open mesh check_disconnected="ignore", # check but ignore disconnected mesh - reorient_faces="ignore", # check but ignore non-orientable mesh + reorient_faces="ignore", # check but ignore non-orientable mesh ) -prism.style.label = "Open Prism", +prism.style.label = ("Open Prism",) prism.style.magnetization.mode = "arrow" print("mesh status open:", prism.status_open) @@ -202,4 +225,4 @@ prism.show( ```{caution} Keep in mind that the inside-outside check will fail, so that `getB` may yield wrong results on the inside of the prism where the polarization vector should be added. -``` \ No newline at end of file +``` From 48b615aeca241d643c1d4ac876d6b7373e090ccb Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 10:20:11 +0100 Subject: [PATCH 148/240] fix tutorial collection --- .../gallery/gallery_tutorial_collection.md | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_collection.md b/docs/_pages/gallery/gallery_tutorial_collection.md index 23a39147a..15621a241 100644 --- a/docs/_pages/gallery/gallery_tutorial_collection.md +++ b/docs/_pages/gallery/gallery_tutorial_collection.md @@ -1,18 +1,17 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- - (gallery-tutorial-collection)= # Working with Collections @@ -46,7 +45,6 @@ print(f"collections: {coll.collections}") New additions are always added at the end. Use the **`add`** method or the parameters. ```{code-cell} ipython3 - # Copy adjusts object label automatically x2 = x1.copy() s2 = s1.copy() @@ -170,30 +168,31 @@ The following example demonstrates how collections enable user-friendly manipula ```{code-cell} ipython3 import numpy as np + import magpylib as magpy # Construct two coils from windings coil1 = magpy.Collection(style_label="coil1") -for z in np.linspace(-.5, .5, 5): - coil1.add(magpy.current.Circle(current=1, diameter=20, position=(0,0,z))) -coil1.position = (0,0,-5) -coil2 = coil1.copy(position=(0,0,5)) +for z in np.linspace(-0.0005, 0.0005, 5): + coil1.add(magpy.current.Circle(current=1, diameter=0.02, position=(0, 0, z))) +coil1.position = (0, 0, -0.005) +coil2 = coil1.copy(position=(0, 0, 0.005)) # Helmholtz consists of two coils helmholtz = coil1 + coil2 # Move the helmholtz -helmholtz.position = np.linspace((0,0,0), (10,0,0), 15) -helmholtz.rotate_from_angax(np.linspace(0,180,15), "x", start=0) +helmholtz.position = np.linspace((0, 0, 0), (0.01, 0, 0), 15) +helmholtz.rotate_from_angax(np.linspace(0, 180, 15), "x", start=0) # Move the coils -coil1.move(np.linspace((0,0,0), ( 5,0,0), 15)) -coil2.move(np.linspace((0,0,0), (-5,0,0), 15)) +coil1.move(np.linspace((0, 0, 0), (0.005, 0, 0), 15)) +coil2.move(np.linspace((0, 0, 0), (-0.005, 0, 0), 15)) # Move the windings for coil in [coil1, coil2]: - for i,wind in enumerate(coil): - wind.move(np.linspace((0,0,0), (0,0,2-i), 15)) + for i, wind in enumerate(coil): + wind.move(np.linspace((0, 0, 0), (0, 0, (2 - i) * 0.001), 15)) # Display as animation magpy.show(*helmholtz, animation=True, style_path_show=False) @@ -204,12 +203,14 @@ For magnetic field computation, a collection with source children behaves like a ```{code-cell} ipython3 import matplotlib.pyplot as plt -B = magpy.getB(helmholtz, (10,0,0)) -plt.plot(B, label=["Bx", "By", "Bz"]) +B = magpy.getB(helmholtz, (0.01, 0, 0)) +plt.plot( + B * 1000, # T -> mT + label=["Bx", "By", "Bz"], +) plt.gca().set( - title="B-field (mT) at position (10,0,0)", - xlabel="helmholtz path position index" + title="B-field (mT) at position x=10mm", xlabel="helmholtz path position index" ) plt.gca().grid(color=".9") plt.gca().legend() @@ -227,21 +228,21 @@ import magpylib as magpy coll = magpy.Collection() for index in range(10): cuboid = magpy.magnet.Cuboid( - magnetization=(0, 0, 1000 * (index%2-.5)), - dimension=(10,10,10), - position=(index*10,0,0), + polarization=(0, 0, (index % 2 - 0.5)), + dimension=(0.01, 0.01, 0.01), + position=(index * 0.01, 0, 0), ) coll.add(cuboid) # Add an encompassing 3D-trace trace = magpy.graphics.model3d.make_Cuboid( - dimension=(104, 12, 12), - position=(45, 0, 0), + dimension=(0.104, 0.012, 0.012), + position=(0.045, 0, 0), opacity=0.5, ) coll.style.model3d.add_trace(trace) -coll.style.label="Collection with visible children" +coll.style.label = "Collection with visible children" coll.show() # Hide the children default 3D representation @@ -253,6 +254,3 @@ coll.show() ## Compound Objects Collections can be subclassed to form dynamic groups that seamlessly integrate into Magpylib. Such classes are referred to as **compounds**. An example how this is done is shown in {ref}`gallery-misc-compound`. - - - From d7b09299c1f3156867744647d38cfb3fc3c1dc45 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 10:57:45 +0100 Subject: [PATCH 149/240] fix tutorial field computation --- .../gallery_tutorial_field_computation.md | 138 +++++++++++------- 1 file changed, 83 insertions(+), 55 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_field_computation.md b/docs/_pages/gallery/gallery_tutorial_field_computation.md index ac3e3b207..06a6800bf 100644 --- a/docs/_pages/gallery/gallery_tutorial_field_computation.md +++ b/docs/_pages/gallery/gallery_tutorial_field_computation.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.5 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-tutorial-field-computation)= @@ -21,11 +21,14 @@ kernelspec: The v2 slogan was *"The magnetic field is only three lines of code away"*, which is demonstrated by the most fundamental use of Magpylib. ```{code-cell} ipython3 -import magpylib as magpy # Import Magpylib -loop = magpy.current.Circle(current=1, diameter=1) # Create magnetic source -B = magpy.getB(loop, observers=(0,0,0)) # Compute field +import magpylib as magpy # Import Magpylib + +loop = magpy.current.Circle( + current=1, diameter=0.01 +) # Create magnetic source, units: A, m +B = magpy.getB(loop, observers=(0, 0, 0)) # Compute field in units of T -print(B.round(decimals=3)) +print(B) ``` ## Field on a Grid @@ -33,32 +36,49 @@ print(B.round(decimals=3)) When handed multiple observer positions, `getB` and `getH` will return the field in the shape of the input. In the following example, B- and H-field of a diametrically magnetized cylinder magnet are computed on a position grid in the symmetry plane, and are then displayed using Matplotlib. ```{code-cell} ipython3 -import numpy as np import matplotlib.pyplot as plt +import numpy as np + import magpylib as magpy -fig, [ax1,ax2] = plt.subplots(1, 2, figsize=(10,5)) +fig, [ax1, ax2] = plt.subplots(1, 2, figsize=(10, 5)) # Create an observer grid in the xz-symmetry plane -X, Y = np.mgrid[-5:5:100j, -5:5:100j].transpose((0, 2, 1)) +X, Y = np.mgrid[-0.05:0.05:100j, -0.05:0.05:100j].transpose((0, 2, 1)) grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) # Compute B- and H-fields of a cylinder magnet on the grid -cyl = magpy.magnet.Cylinder(magnetization=(500,500,0), dimension=(4,2)) +cyl = magpy.magnet.Cylinder(polarization=(0.5, 0.5, 0), dimension=(0.04, 0.02)) B = cyl.getB(grid) H = cyl.getH(grid) # Display field with Pyplot -ax1.streamplot(grid[:,:,0], grid[:,:,1], B[:,:,0], B[:,:,1], density=1.5, - color=np.log(np.linalg.norm(B, axis=2)), linewidth=1, cmap='spring_r') +ax1.streamplot( + grid[:, :, 0], + grid[:, :, 1], + B[:, :, 0], + B[:, :, 1], + density=1.5, + color=np.log(np.linalg.norm(B, axis=2)), + linewidth=1, + cmap="spring_r", +) -ax2.streamplot(grid[:,:,0], grid[:,:,1], H[:,:,0], H[:,:,1], density=1.5, - color=np.log(np.linalg.norm(B, axis=2)), linewidth=1, cmap='winter_r') +ax2.streamplot( + grid[:, :, 0], + grid[:, :, 1], + H[:, :, 0], + H[:, :, 1], + density=1.5, + color=np.log(np.linalg.norm(B, axis=2)), + linewidth=1, + cmap="winter_r", +) # Outline magnet boundary -for ax in [ax1,ax2]: - ts = np.linspace(0, 2*np.pi, 50) - ax.plot(2*np.sin(ts), 2*np.cos(ts), 'k--') +for ax in [ax1, ax2]: + ts = np.linspace(0, 2 * np.pi, 50) + ax.plot(0.02 * np.sin(ts), 0.02 * np.cos(ts), "k--") plt.tight_layout() plt.show() @@ -74,6 +94,7 @@ The following example shows a moving and rotating sensor with two pixels. At the ```{code-cell} ipython3 import numpy as np + import magpylib as magpy # Reset defaults set in previous example @@ -81,22 +102,24 @@ magpy.defaults.reset() # Define sensor with path -sensor = magpy.Sensor(pixel=[(0,0,-.5), (0,0,.5)], style_size=1.5) -sensor.position = np.linspace((0,0,-3), (0,0,3), 37) +sensor = magpy.Sensor(pixel=[(0, 0, -0.0005), (0, 0, 0.0005)], style_size=1.5) +sensor.position = np.linspace((0, 0, -0.003), (0, 0, 0.003), 37) angles = np.linspace(0, 360, 37) -sensor.rotate_from_angax(angles, 'z', start=0) +sensor.rotate_from_angax(angles, "z", start=0) # Define source with path -cyl1 = magpy.magnet.Cylinder(magnetization=(100,0,0), dimension=(1,2), position=(3,0,0)) -cyl2 = cyl1.copy(position=(-3,0,0)) +cyl1 = magpy.magnet.Cylinder( + polarization=(0.1, 0, 0), dimension=(0.001, 0.002), position=(0.003, 0, 0) +) +cyl2 = cyl1.copy(position=(-0.003, 0, 0)) coll = magpy.Collection(cyl1, cyl2) -coll.rotate_from_angax(-angles, 'z', start=0) +coll.rotate_from_angax(-angles, "z", start=0) # Display system and field at sensor -with magpy.show_context(sensor, coll, animation=True, backend='plotly'): +with magpy.show_context(sensor, coll, animation=True, backend="plotly"): magpy.show(col=1) - magpy.show(output='Bx', col=2, pixel_agg=None) + magpy.show(output="Bx", col=2, pixel_agg=None) ``` ## Multiple Inputs @@ -107,12 +130,12 @@ When `getB` and `getH` receive multiple inputs for sources and observers they wi import magpylib as magpy # Three sources -cube1 = magpy.magnet.Cuboid(magnetization=(0,0,1000), dimension=(1,1,1)) +cube1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(0.1, 0.1, 0.1)) cube2 = cube1.copy() cube3 = cube1.copy() # Two sensors with 4x5 pixel each -pixel = [[[(i,j,0)] for i in range(4)] for j in range(5)] +pixel = [[[(i / 1000, j / 1000, 0)] for i in range(4)] for j in range(5)] sens1 = magpy.Sensor(pixel=pixel) sens2 = sens1.copy() @@ -137,29 +160,28 @@ Instead of a Numpy `ndarray`, the field computation can also return a [pandas](h ```{code-cell} ipython3 import numpy as np + import magpylib as magpy cube = magpy.magnet.Cuboid( - magnetization=(0, 0, 1000), - dimension=(1, 1, 1), - style_label='cube' + polarization=(0, 0, 1), dimension=(0.01, 0.01, 0.01), style_label="cube" ) loop = magpy.current.Circle( current=200, - diameter=2, - style_label='loop', + diameter=0.02, + style_label="loop", ) sens1 = magpy.Sensor( - pixel=[(0,0,0), (.5,0,0)], - position=np.linspace((-4, 0, 2), (4, 0, 2), 30), - style_label='sens1' + pixel=[(0, 0, 0), (0.005, 0, 0)], + position=np.linspace((-0.04, 0, 0.02), (0.04, 0, 0.02), 30), + style_label="sens1", ) -sens2 = sens1.copy(style_label='sens2').move((0,0,1)) +sens2 = sens1.copy(style_label="sens2").move((0, 0, 0.01)) B = magpy.getB( [cube, loop], [sens1, sens2], - output='dataframe', + output="dataframe", ) B @@ -169,6 +191,7 @@ Plotting libraries such as [plotly](https://plotly.com/python/plotly-express/) o ```{code-cell} ipython3 import plotly.express as px + fig = px.line( B, x="path", @@ -183,7 +206,7 @@ fig.show() (gallery-tutorial-field-computation-functional-interface)= -## Direct Interface +## Functional Interface All above computations demonstrate the convenient object oriented interface of Magpylib. However, there are instances when it is better to work with the functional interface instead. @@ -198,33 +221,38 @@ Use numpy operations for input array creation as shown in the example ! ```{code-cell} ipython3 import numpy as np + import magpylib as magpy # Two different magnet dimensions -dim1 = (2,4,4) -dim2 = (4,2,2) -DIM = np.vstack(( - np.tile(dim1, (6,1)), - np.tile(dim2, (6,1)), - )) +dim1 = (0.02, 0.04, 0.04) +dim2 = (0.04, 0.02, 0.02) +DIM = np.vstack( + ( + np.tile(dim1, (6, 1)), + np.tile(dim2, (6, 1)), + ) +) -# Sweep through different magnetization for each magnet type -mags = np.linspace((0,0,500), (0,0,1000), 6) -MAG = np.tile(mags, (2,1)) +# Sweep through different polarizations for each magnet type +pol = np.linspace((0, 0, 0.5), (0, 0, 1), 6) +POL = np.tile(pol, (2, 1)) # Airgap must stay the same -pos1 = (0,0,3) -pos2 = (0,0,2) -POS = np.vstack(( - np.tile(pos1, (6,1)), - np.tile(pos2, (6,1)), - )) +pos1 = (0, 0, 0.03) +pos2 = (0, 0, 0.02) +POS = np.vstack( + ( + np.tile(pos1, (6, 1)), + np.tile(pos2, (6, 1)), + ) +) # Compute all instances with the functional interface B = magpy.getB( - sources='Cuboid', + sources="Cuboid", observers=POS, - magnetization=MAG, + polarization=POL, dimension=DIM, ) From 9e59ae545a575cf3d70fd41807a2efc813be9fd4 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 11:07:43 +0100 Subject: [PATCH 150/240] fix tutorial path --- docs/_pages/gallery/gallery_tutorial_paths.md | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_paths.md b/docs/_pages/gallery/gallery_tutorial_paths.md index 3e25110e1..eaae4b1e9 100644 --- a/docs/_pages/gallery/gallery_tutorial_paths.md +++ b/docs/_pages/gallery/gallery_tutorial_paths.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.0 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-tutorial-paths)= @@ -31,24 +31,24 @@ Absolute object paths are assigned at initialization or through the object prope ```{code-cell} ipython3 import numpy as np from scipy.spatial.transform import Rotation as R + import magpylib as magpy # Create paths ts = np.linspace(0, 10, 31) -pos = np.array([(t, 0, np.sin(t)) for t in ts]) -ori = R.from_rotvec(np.array([(0, -np.cos(t)*0.785, 0) for t in ts])) +pos = np.array([(0.1 * t, 0, 0.1 * np.sin(t)) for t in ts]) +ori = R.from_rotvec(np.array([(0, -0.1 * np.cos(t) * 0.785, 0) for t in ts])) # Set path at initialization sensor = magpy.Sensor(position=pos, orientation=ori) # Set path through properties -cube = magpy.magnet.Cuboid(magnetization=(0,0,1), dimension=(1,1,1)) -cube.position = pos + np.array((0,0,3)) +cube = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(0.01, 0.01, 0.01)) +cube.position = pos + np.array((0, 0, 0.3)) cube.orientation = ori # Display as animation -magpy.show(sensor, cube, animation=True, backend='plotly') - +magpy.show(sensor, cube, animation=True, backend="plotly") ``` ## Relative Paths @@ -58,23 +58,24 @@ magpy.show(sensor, cube, animation=True, backend='plotly') ```{code-cell} ipython3 import numpy as np from scipy.spatial.transform import Rotation as R + import magpylib as magpy # Create paths ts = np.linspace(0, 10, 21) -pos = np.array([(t, 0, np.sin(t)) for t in ts]) -ori = R.from_rotvec(np.array([(0, -np.cos(t)*0.785, 0) for t in ts])) +pos = np.array([(0.1 * t, 0, 0.1 * np.sin(t)) for t in ts]) +ori = R.from_rotvec(np.array([(0, -0.1 * np.cos(t) * 0.785, 0) for t in ts])) # Set path at initialization -sens1 = magpy.Sensor(position=pos, orientation=ori, style_label='sens1') +sens1 = magpy.Sensor(position=pos, orientation=ori, style_label="sens1") # Apply move operation to whole path with scalar input -sens2 = sens1.copy(style_label='sens2') -sens2.move((0,0,2)) +sens2 = sens1.copy(style_label="sens2") +sens2.move((0, 0, 0.05)) # Apply rotate operation to whole path with scalar input -sens3 = sens1.copy(style_label='sens3') -sens3.rotate_from_angax(angle=90, axis='y', anchor=0) +sens3 = sens1.copy(style_label="sens3") +sens3.rotate_from_angax(angle=90, axis="y", anchor=0) # Display paths magpy.show(sens1, sens2, sens3) @@ -84,14 +85,15 @@ When the input is a vector, the path is by default appended. ```{code-cell} ipython3 import numpy as np + from magpylib.magnet import Sphere # Create paths -x_path = np.linspace((0,0,0), (10,0,0), 10)[1:] -z_path = np.linspace((0,0,0), (0,0,10), 10)[1:] +x_path = np.linspace((0, 0, 0), (0.1, 0, 0), 10)[1:] +z_path = np.linspace((0, 0, 0), (0, 0, 0.1), 10)[1:] # Create sphere object -sphere = Sphere(magnetization=(0,0,1), diameter=3) +sphere = Sphere(polarization=(0, 0, 1), diameter=0.03) # Apply paths subsequently for _ in range(3): @@ -107,20 +109,21 @@ Complex paths can be created by merging multiple path operations. This is done w ```{code-cell} ipython3 import numpy as np + from magpylib.magnet import Cuboid # Create cube and set linear path -cube = Cuboid(magnetization=(0,0,100), dimension=(2,2,2)) -cube.position = np.linspace((0,0,0), (10,0,0), 60) +cube = Cuboid(polarization=(0, 0, 0.1), dimension=(0.02, 0.02, 0.02)) +cube.position = np.linspace((0, 0, 0), (0.1, 0, 0), 60) # Apply rotation about self - starting at index 0 -cube.rotate_from_rotvec(np.linspace((0,0,0), (0,0,360), 30), start=0) +cube.rotate_from_rotvec(np.linspace((0, 0, 0), (0, 0, 360), 30), start=0) # Apply rotation about origin - starting at index 30 -cube.rotate_from_rotvec(np.linspace((0,0,0), (0,0,360), 30), anchor=0, start=30) +cube.rotate_from_rotvec(np.linspace((0, 0, 0), (0, 0, 360), 30), anchor=0, start=30) # Display paths as animation -cube.show(backend='plotly', animation=True) +cube.show(backend="plotly", animation=True) ``` ## Reset path @@ -131,7 +134,9 @@ The `reset_path()` method allows users to reset an object path to `position=(0,0 import magpylib as magpy # Create sensor object with complex path -sensor=magpy.Sensor().rotate_from_angax([1,2,3,4], (1,2,3), anchor=(0,3,5)) +sensor = magpy.Sensor().rotate_from_angax( + [1, 2, 3, 4], (1, 2, 3), anchor=(0, 0.03, 0.05) +) # Reset path sensor.reset_path() @@ -152,13 +157,14 @@ In the following example the orientation attribute is padded by its edge value ` ```{code-cell} ipython3 from scipy.spatial.transform import Rotation as R + import magpylib as magpy sensor = magpy.Sensor( - position=[(0,0,0), (1,1,1)], - orientation=R.from_rotvec([(0,0,.1), (0,0,.2)]), + position=[(0, 0, 0), (0.01, 0.01, 0.01)], + orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2)]), ) -sensor.position=[(i,i,i) for i in range(4)] +sensor.position = [(i / 100, i / 100, i / 100) for i in range(4)] print(sensor.position) print(sensor.orientation.as_rotvec()) ``` @@ -168,10 +174,10 @@ When the field is computed of `loop1` with path length 4 and `loop2` with path l ```{code-cell} ipython3 from magpylib.current import Circle -loop1 = Circle(current=1, diameter=1, position=[(0,0,i) for i in range(4)]) -loop2 = Circle(current=1, diameter=1, position=[(0,0,i) for i in range(2)]) +loop1 = Circle(current=1, diameter=1, position=[(0, 0, i) for i in range(4)]) +loop2 = Circle(current=1, diameter=1, position=[(0, 0, i) for i in range(2)]) -B = magpy.getB([loop1,loop2], (0,0,0)) +B = magpy.getB([loop1, loop2], (0, 0, 0)) print(B) ``` @@ -179,13 +185,18 @@ The idea behind **end-slicing** is that, whenever a path is automatically reduce ```{code-cell} ipython3 from scipy.spatial.transform import Rotation as R + from magpylib import Sensor sensor = Sensor( - position=[(0,0,0), (1,1,1), (2,2,2), (3,3,3)], - orientation=R.from_rotvec([(0,0,.1), (0,0,.2), (0,0,.3), (0,0,.4)]), + position=[(0, 0, 0), (0.01, 0.01, 0.01), (0.02, 0.02, 2), (0.03, 0.03, 0.03)], + orientation=R.from_rotvec([(0, 0, 0.1), (0, 0, 0.2), (0, 0, 0.3), (0, 0, 0.4)]), ) -sensor.position=[(1,2,3), (2,3,4)] +sensor.position = [(0.01, 0.02, 0.03), (0.02, 0.03, 0.04)] print(sensor.position) print(sensor.orientation.as_rotvec()) ``` + +```{code-cell} ipython3 + +``` From 6d83142929034057cd1a0bd0095518968fca3ad7 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 14:45:03 +0100 Subject: [PATCH 151/240] better unit exponent display --- docs/_pages/docu/docu_graphics.md | 2 +- magpylib/_src/fields/field_BH_dipole.py | 2 +- magpylib/_src/obj_classes/class_BaseDisplayRepr.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_pages/docu/docu_graphics.md b/docs/_pages/docu/docu_graphics.md index 66a39a7a6..bfee9f97d 100644 --- a/docs/_pages/docu/docu_graphics.md +++ b/docs/_pages/docu/docu_graphics.md @@ -708,7 +708,7 @@ with magpy.show_context(): +++ In addition the usual 3D models, it is also possible to draw 2D scatter plots of magnetic field data. This is achieved by assigning the `output` argument in the `show` function. -By default `output='model3d'` displays the 3D representations of the objects. If output is a tuple of strings, it must be a combination of 'B' or 'H' and 'x', 'y' and/or 'z'. When having multiple coordinates, the field value is the combined vector length (e.g. `('Bx', 'Hxy', 'Byz')`). `'Bxy'` is equivalent to `sqrt(|Bx|^2 + |By|^2)`. A 2D line plot is then represented accordingly if the objects contain at least one source and one sensor. +By default `output='model3d'` displays the 3D representations of the objects. If output is a tuple of strings, it must be a combination of 'B' or 'H' and 'x', 'y' and/or 'z'. When having multiple coordinates, the field value is the combined vector length (e.g. `('Bx', 'Hxy', 'Byz')`). `'Bxy'` is equivalent to `sqrt(|Bx|² + |By|²)`. A 2D line plot is then represented accordingly if the objects contain at least one source and one sensor. By default source outputs are summed up and sensor pixels, if any, are aggregated by mean (`pixel_agg="mean"`). ```{code-cell} ipython3 diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index 553d98acf..70cdd48d9 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -30,7 +30,7 @@ def dipole_field( Observer positions (x,y,z) in Cartesian coordinates in units of m. moment: ndarray, shape (n,3) - Dipole moment vector in units of A*m^2. + Dipole moment vector in units of A·m². Returns ------- diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index da02b9c93..19d845460 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -15,7 +15,7 @@ "current": "A", "magnetization": "A/m", "polarization": "T", - "moment": "A*m^2", + "moment": "A·m²", } From fa326332289d5681ee5ce5bf4f6c3990aca3452a Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:14:38 +0100 Subject: [PATCH 152/240] fix tutorial custom --- .../_pages/gallery/gallery_tutorial_custom.md | 163 +++++++++++------- 1 file changed, 99 insertions(+), 64 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_custom.md b/docs/_pages/gallery/gallery_tutorial_custom.md index 4d452da76..ae27ef050 100644 --- a/docs/_pages/gallery/gallery_tutorial_custom.md +++ b/docs/_pages/gallery/gallery_tutorial_custom.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.5 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-tutorial-custom)= @@ -36,8 +36,10 @@ We create this field as a Python function and hand it over to a CustomSource `fi ```{code-cell} ipython3 import numpy as np + import magpylib as magpy + # Create monopole field def mono_field(field, observers): """ @@ -52,22 +54,25 @@ def mono_field(field, observers): Returns: np.ndarray, shape (n,3) Magnetic monopole field """ - if field=="B": - Qm = 1 # unit mT + Qm = 1e-6 # unit T·m² + obs = np.array(observers).T # unit m + B = Qm * (obs / np.linalg.norm(obs, axis=0) ** 3).T # unit T + if field == "B": + return B # unit T + elif field == "H": + mu0 = 4 * np.pi * 1e-7 + H = B / mu0 # unit A/m + return H else: - Qm = 10/4/np.pi # unit A/m - obs = np.array(observers).T - field = Qm * obs / np.linalg.norm(obs, axis=0)**3 - return field.T + raise ValueError("Field Value must be either B or H") + # Create CustomSource with monopole field -mono = magpy.misc.CustomSource( - field_func=mono_field -) +mono = magpy.misc.CustomSource(field_func=mono_field) # Compute field -print(mono.getB((1,0,0))) -print(mono.getH((1,0,0))) +print(mono.getB((0.001, 0, 0))) +print(mono.getH((0.001, 0, 0))) ``` Multiple of these sources can now be combined, making use of the Magpylib position/orientation interface. @@ -76,24 +81,18 @@ Multiple of these sources can now be combined, making use of the Magpylib positi import matplotlib.pyplot as plt # Create two monopole charges -mono1 = magpy.misc.CustomSource( - field_func=mono_field, - position=(2,2,0) -) -mono2 = magpy.misc.CustomSource( - field_func=mono_field, - position=(-2,-2,0) -) +mono1 = magpy.misc.CustomSource(field_func=mono_field, position=(0.002, 0.002, 0)) +mono2 = magpy.misc.CustomSource(field_func=mono_field, position=(-0.002, -0.002, 0)) # Compute field on observer-grid -X, Y = np.mgrid[-5:5:40j, -5:5:40j].transpose((0, 2, 1)) +X, Y = np.mgrid[-0.005:0.005:40j, -0.005:0.005:40j].transpose((0, 2, 1)) grid = np.stack([X, Y, np.zeros((40, 40))], axis=2) B = magpy.getB([mono1, mono2], grid, sumup=True) normB = np.linalg.norm(B, axis=2) # Plot field in x-y symmetry plane -cp = plt.contourf(X, Y, np.log10(normB), cmap='gray_r', levels=10) -plt.streamplot(X, Y, B[:, :, 0], B[:, :, 1], color='k', density=1) +cp = plt.contourf(X, Y, np.log10(normB), cmap="gray_r", levels=10) +plt.streamplot(X, Y, B[:, :, 0], B[:, :, 1], color="k", density=1) plt.tight_layout() plt.show() @@ -106,12 +105,12 @@ While the CustomSource is graphically represented by a simple marker by default, ```{code-cell} ipython3 # Load Sphere model trace_pole = magpy.graphics.model3d.make_Ellipsoid( - dimension=(.3,.3,.3), - ) + dimension=np.array([3, 3, 3]) * 1e-4, +) for mono in [mono1, mono2]: # Turn off default model - mono.style.model3d.showdefault=False + mono.style.model3d.showdefault = False # Add sphere model mono.style.model3d.add_trace(trace_pole) @@ -126,44 +125,51 @@ In the above example it would be nice to make the CustomSource dynamic, so that ```{code-cell} ipython3 class Monopole(magpy.misc.CustomSource): - """ Magnetic Monopole class + """Magnetic Monopole class Parameters ---------- charge: float Monopole charge in units of mT """ + def __init__(self, charge, **kwargs): super().__init__(**kwargs) # hand over style kwargs self._charge = charge # Add spherical 3d model trace_pole = magpy.graphics.model3d.make_Ellipsoid( - dimension=(.3,.3,.3), + dimension=np.array([3, 3, 3]) * 1e-4, ) - self.style.model3d.showdefault=False + self.style.model3d.showdefault = False self.style.model3d.add_trace(trace_pole) # Add monopole field_func self._update() def _update(self): - """ Apply monopole field function """ + """Apply monopole field function""" def mono_field(field, observers): - """ monopole field""" - chg = self._charge - if field=="H": - chg *= 10/4/np.pi # unit A/m - obs = np.array(observers).T - BH = chg * obs / np.linalg.norm(obs, axis=0)**3 - return BH.T - + """monopole field""" + Qm = self._charge # unit T·m² + obs = np.array(observers).T # unit m + B = Qm * (obs / np.linalg.norm(obs, axis=0) ** 3).T # unit T + if field == "B": + return B # unit T + elif field == "H": + mu0 = 4 * np.pi * 1e-7 + H = B / mu0 # unit A/m + return H + else: + raise ValueError("Field Value must be either B or H") + + self.style.label = f"Monopole (charge={self._charge} T·m²)" self.field_func = mono_field @property def charge(self): - """Number of cubes""" + """Return charge""" return self._charge @charge.setter @@ -172,13 +178,14 @@ class Monopole(magpy.misc.CustomSource): self._charge = input self._update() + # Use new class -mono = Monopole(charge=1) -print(mono.getB((1,0,0))) +mono = Monopole(charge=1e-6) +print(mono.getB((0.001, 0, 0))) # Make use of new property -mono.charge = -1 -print(mono.getB((1,0,0))) +mono.charge = -1e-6 +print(mono.getB((0.001, 0, 0))) ``` The new class seamlessly integrates into the Magpylib interface as we show in the following example where we have a look at the Quadrupole field @@ -187,49 +194,77 @@ The new class seamlessly integrates into the Magpylib interface as we show in th import matplotlib.pyplot as plt # Create a quadrupole from four monopoles -mono1 = Monopole(charge= 1, style_color='r', position=( 1, 0, 0)) -mono2 = Monopole(charge= 1, style_color='r', position=(-1, 0, 0)) -mono3 = Monopole(charge=-1, style_color='b', position=( 0, 0, 1)) -mono4 = Monopole(charge=-1, style_color='b', position=( 0, 0,-1)) +mono1 = Monopole(charge=1e-6, style_color="r", position=(0.001, 0, 0)) +mono2 = Monopole(charge=1e-6, style_color="r", position=(-0.001, 0, 0)) +mono3 = Monopole(charge=-1e-6, style_color="b", position=(0, 0, 0.001)) +mono4 = Monopole(charge=-1e-6, style_color="b", position=(0, 0, -0.001)) qpole = magpy.Collection(mono1, mono2, mono3, mono4) # Matplotlib figure with 3d and 2d axis fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_subplot(121, projection="3d", azim=-80, elev=15,) -ax2 = fig.add_subplot(122,) +ax1 = fig.add_subplot( + 121, + projection="3d", + azim=-80, + elev=15, +) +ax2 = fig.add_subplot( + 122, +) # Show 3D model in ax1 magpy.show(*qpole, canvas=ax1) # Compute B-field on xz-grid and display in ax2 ts = np.linspace(-3, 3, 30) -grid = np.array([[(x,0,z) for x in ts] for z in ts]) +grid = np.array([[(x / 1000, 0, z / 1000) for x in ts] for z in ts]) B = qpole.getB(grid) scale = np.linalg.norm(B, axis=2) -cp = ax2.contourf(grid[:,:,0], grid[:,:,2], np.log(scale), levels=100, cmap='rainbow') -ax2.streamplot(grid[:,:,0], grid[:,:,2], B[:,:,0], B[:,:,2], density=2, - color='k', linewidth=scale**0.3) +cp = ax2.contourf( + grid[:, :, 0], + grid[:, :, 2], + np.log(scale), + levels=100, + cmap="rainbow", +) +ax2.streamplot( + grid[:, :, 0], + grid[:, :, 2], + B[:, :, 0], + B[:, :, 2], + density=2, + color="k", + linewidth=scale**0.3, +) # Display pole position in ax2 pole_pos = np.array([mono.position for mono in qpole]) -ax2.plot(pole_pos[:,0], pole_pos[:,2], marker='o', ms=10, mfc='k', mec='w', ls='') +ax2.plot( + pole_pos[:, 0], + pole_pos[:, 2], + marker="o", + ms=10, + mfc="k", + mec="w", + ls="", +) # Figure styling ax1.set( - title='3D model', - xlabel='x-position (mm)', - ylabel='y-position (mm)', - zlabel='z-position (mm)', + title="3D model", + xlabel="x-position (m)", + ylabel="y-position (m)", + zlabel="z-position (m)", ) ax2.set( - title='Quadrupole field', - xlabel='x-position (mm)', - ylabel='z-position (mm)', + title="Quadrupole field", + xlabel="x-position (m)", + ylabel="z-position (m)", aspect=1, ) -fig.colorbar(cp, label='[$charge/mm^2$]', ax=ax2) +fig.colorbar(cp, label="[$charge/m^2$]", ax=ax2) plt.tight_layout() plt.show() -``` \ No newline at end of file +``` From a6e46b30291a17673ef3f2cf3a2473795634443d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:24:02 +0100 Subject: [PATCH 153/240] rework tutorial field computation --- .../gallery_tutorial_field_computation.md | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_field_computation.md b/docs/_pages/gallery/gallery_tutorial_field_computation.md index 06a6800bf..85a143b77 100644 --- a/docs/_pages/gallery/gallery_tutorial_field_computation.md +++ b/docs/_pages/gallery/gallery_tutorial_field_computation.md @@ -54,8 +54,8 @@ H = cyl.getH(grid) # Display field with Pyplot ax1.streamplot( - grid[:, :, 0], - grid[:, :, 1], + grid[:, :, 0] * 1000, # m -> mm + grid[:, :, 1] * 1000, # m -> mm B[:, :, 0], B[:, :, 1], density=1.5, @@ -65,8 +65,8 @@ ax1.streamplot( ) ax2.streamplot( - grid[:, :, 0], - grid[:, :, 1], + grid[:, :, 0] * 1000, # m -> mm + grid[:, :, 1] * 1000, # m -> mm H[:, :, 0], H[:, :, 1], density=1.5, @@ -75,10 +75,21 @@ ax2.streamplot( cmap="winter_r", ) +ax1.set( + title="B-Field", + xlabel="x-position (mm)", + ylabel="y-position (mm)", +) +ax2.set( + title="H-Field", + xlabel="x-position (mm)", + ylabel="y-position (mm)", + aspect=1, +) # Outline magnet boundary for ax in [ax1, ax2]: ts = np.linspace(0, 2 * np.pi, 50) - ax.plot(0.02 * np.sin(ts), 0.02 * np.cos(ts), "k--") + ax.plot(20 * np.sin(ts), 20 * np.cos(ts), "k--") plt.tight_layout() plt.show() From 563d04eea230b2c3f433df55416fba7dedd68f43 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:27:53 +0100 Subject: [PATCH 154/240] fix tutorial trimesh --- .../gallery/gallery_tutorial_trimesh.md | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/_pages/gallery/gallery_tutorial_trimesh.md b/docs/_pages/gallery/gallery_tutorial_trimesh.md index 154075770..be9a07a13 100644 --- a/docs/_pages/gallery/gallery_tutorial_trimesh.md +++ b/docs/_pages/gallery/gallery_tutorial_trimesh.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.7 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-tutorial-trimesh)= @@ -47,12 +47,14 @@ The mesh status is set by the checks, and can be viewed via the properties `stat ## Example - Tetrahedron magnet ```{code-cell} ipython3 +import numpy as np + import magpylib as magpy # create faceted tetrahedron from vertices and faces tmesh_tetra = magpy.magnet.TriangularMesh( - magnetization=(0, 0, 1000), - vertices=((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)), + polarization=(0, 0, 1), + vertices=np.array(((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1))) / 100, faces=((2, 1, 0), (3, 0, 1), (3, 2, 0), (3, 1, 2)), ) @@ -70,25 +72,26 @@ tmesh_tetra.show() In some cases it may be desirable to generate a `TriangularMesh` object from an open mesh, as described in {ref}`gallery-shapes-triangle`. In this case one has to be extremely careful because one cannot rely on the checks. Not to generate warnings or error messages, these checks can be disabled with `"skip"` or their outcome can be ignored with `"ignore"`. The `show` function can be used to view open edges and disconnected parts. In the following example we generate such an open mesh directly from `Triangle` objects. ```{code-cell} ipython3 -import magpylib as magpy import numpy as np +import magpylib as magpy + top = magpy.misc.Triangle( - magnetization=(1000,0,0), - vertices= ((-1,-1,1), (1,-1,1), (0,2,1)), + polarization=(1, 0, 0), + vertices=np.array(((-1, -1, 1), (1, -1, 1), (0, 2, 1))) / 100, ) bottom = magpy.misc.Triangle( - magnetization=(1000,0,0), - vertices= ((-1,-1,-1), (0,2,-1), (1,-1,-1)), + polarization=(1, 0, 0), + vertices=np.array(((-1, -1, -1), (0, 2, -1), (1, -1, -1))) / 100, ) # create faceted prism with open edges prism = magpy.magnet.TriangularMesh.from_triangles( - magnetization=(0, 0, 1000), # overrides triangles magnetization + polarization=(0, 0, 1), # overrides triangles polarization triangles=[top, bottom], - check_open="ignore", # check but ignore open mesh - check_disconnected="ignore", # check but ignore disconnected mesh - reorient_faces="ignore", # check but ignore non-orientable mesh + check_open="ignore", # check but ignore open mesh + check_disconnected="ignore", # check but ignore disconnected mesh + reorient_faces="ignore", # check but ignore non-orientable mesh style_label="Open prism", ) prism.style.magnetization.mode = "arrow" @@ -104,4 +107,3 @@ prism.show( style_mesh_disconnected_show=True, ) ``` - From b69158126dda6f988ceb1d029d64e5a7d94c6105 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:39:59 +0100 Subject: [PATCH 155/240] fix vis mpl streamplot --- .../gallery/gallery_vis_mpl_streamplot.md | 71 ++++++++++++------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md b/docs/_pages/gallery/gallery_vis_mpl_streamplot.md index 16fb877b3..345233df6 100644 --- a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md +++ b/docs/_pages/gallery/gallery_vis_mpl_streamplot.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.5 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-vis-mpl-streamplot)= @@ -23,41 +23,47 @@ In this example we show the B-field of a cuboid magnet using Matplotlib streamli ```{code-cell} ipython3 import matplotlib.pyplot as plt import numpy as np + import magpylib as magpy # Create a Matplotlib figure fig, ax = plt.subplots() # Create an observer grid in the xz-symmetry plane -ts = np.linspace(-5,5,40) -grid = np.array([[(x,0,z) for x in ts] for z in ts]) # slow Python loop +ts = np.linspace(-0.05, 0.05, 40) +grid = np.array([[(x, 0, z) for x in ts] for z in ts]) # Compute the B-field of a cube magnet on the grid -cube = magpy.magnet.Cuboid(magnetization=(500, 0, 500), dimension=(2, 2, 2)) +cube = magpy.magnet.Cuboid(polarization=(0.5, 0, 0.5), dimension=(0.02, 0.02, 0.02)) B = cube.getB(grid) log10_norm_B = np.log10(np.linalg.norm(B, axis=2)) # Display the B-field with streamplot using log10-scaled # color function and linewidth splt = ax.streamplot( - grid[:,:,0], - grid[:,:,2], - B[:, :, 0], - B[:, :, 2], + grid[:, :, 0]*1000, # m -> mm + grid[:, :, 2]*1000, # m -> mm + B[:, :, 0]*1000, # T -> mT + B[:, :, 2]*1000, # T -> mT color=log10_norm_B, density=1, - linewidth=log10_norm_B*2, + linewidth=log10_norm_B * 2, cmap="autumn", ) # Add colorbar with logarithmic labels cb = fig.colorbar(splt.lines, ax=ax, label="|B| (mT)") -ticks = np.array([3,10,30,100,300]) +ticks = np.array([3, 10, 30, 100, 300]) cb.set_ticks(np.log10(ticks)) cb.set_ticklabels(ticks) # Outline magnet boundary -ax.plot([1, 1, -1, -1, 1], [1, -1, -1, 1, 1], "k--", lw=2) +ax.plot( + np.array([1, 1, -1, -1, 1]) * 10, # mm + np.array([1, -1, -1, 1, 1]) * 10, # mm + "k--", + lw=2, +) # Figure styling ax.set( @@ -75,41 +81,58 @@ Be aware that the above code is not very performant, but quite readable. The fol ## Example 2 - Hollow Cylinder Magnet -A nice visualizaion is achieved by combining `streamplot` with `contourf`. In this example we show the B-field of a hollow Cylinder magnet with diametral magnetization in the xy-symmetry plane. +A nice visualizaion is achieved by combining `streamplot` with `contourf`. In this example we show the B-field of a hollow Cylinder magnet with diametral polarization in the xy-symmetry plane. ```{code-cell} ipython3 -import numpy as np import matplotlib.pyplot as plt +import numpy as np + import magpylib as magpy # Create a Matplotlib figure fig, ax = plt.subplots() # Create an observer grid in the xy-symmetry plane - using pure numpy -X, Y = np.mgrid[-5:5:100j, -5:5:100j].transpose((0, 2, 1)) +X, Y = np.mgrid[-0.05:0.05:100j, -0.05:0.05:100j].transpose((0, 2, 1)) grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) # Compute magnetic field on grid - using the functional interface B = magpy.getB( "CylinderSegment", - observers=grid.reshape(-1,3), - dimension=(2,3,5,0,360), - magnetization=(100,0,0), + observers=grid.reshape(-1, 3), + dimension=(0.02, 0.03, 0.05, 0, 360), + polarization=(0.1, 0, 0), ) B = B.reshape(grid.shape) normB = np.linalg.norm(B, axis=2) # combine streamplot with contourf -cp = ax.contourf(X, Y, normB, cmap='rainbow', levels=100, zorder=1) -splt = ax.streamplot(X, Y, B[:, :, 0], B[:, :, 1], color='k', density=1.5, linewidth=1, zorder=3) +cp = ax.contourf( + X * 1000, # m -> mm + Y * 1000, # m -> mm + normB, + cmap="rainbow", + levels=100, + zorder=1, +) +splt = ax.streamplot( + X* 1000, # m -> mm, + Y* 1000, # m -> mm, + B[:, :, 0], # T -> mT + B[:, :, 1], # T -> mT + color="k", + density=1.5, + linewidth=1, + zorder=3, +) # Add colorbar fig.colorbar(cp, ax=ax, label="|B| (mT)") # Outline magnet boundary -ts = np.linspace(0, 2*np.pi, 50) -ax.plot(3*np.cos(ts), 3*np.sin(ts), "w-", lw=2, zorder=2) -ax.plot(2*np.cos(ts), 2*np.sin(ts), "w-", lw=2, zorder=2) +ts = np.linspace(0, 2 * np.pi, 50) +ax.plot(30 * np.cos(ts), 30 * np.sin(ts), "w-", lw=2, zorder=2) +ax.plot(20 * np.cos(ts), 20 * np.sin(ts), "w-", lw=2, zorder=2) # Figure styling ax.set( @@ -120,4 +143,4 @@ ax.set( plt.tight_layout() plt.show() -``` \ No newline at end of file +``` From 15077ce6114034cc7246eac906137eb2956807bf Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:49:52 +0100 Subject: [PATCH 156/240] fix vis pv streamlines --- .../gallery/gallery_vis_pv_streamlines.md | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/docs/_pages/gallery/gallery_vis_pv_streamlines.md b/docs/_pages/gallery/gallery_vis_pv_streamlines.md index 21636ca43..32cdf94c0 100644 --- a/docs/_pages/gallery/gallery_vis_pv_streamlines.md +++ b/docs/_pages/gallery/gallery_vis_pv_streamlines.md @@ -1,15 +1,15 @@ --- -orphan: true jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.5 + jupytext_version: 1.16.0 kernelspec: display_name: Python 3 language: python name: python3 +orphan: true --- (gallery-vis-pv-streamlines)= @@ -20,33 +20,25 @@ Pyvista offers field-line computation and visualization in 3D. In addition to th ```{code-cell} ipython3 import pyvista as pv -import magpylib as magpy -# This line is only needed for pyvista rendering in a jupyter notebook -pv.set_jupyter_backend("panel") +import magpylib as magpy # Create a magnet with Magpylib -magnet = magpy.magnet.Cylinder((0, 0, 1000), (10, 4)) +magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.010, 0.004)) # Create a 3D grid with Pyvista -grid = pv.UniformGrid( +grid = pv.ImageData( dimensions=(41, 41, 41), - spacing=(1, 1, 1), - origin=(-20, -20, -20), + spacing=(0.001, 0.001, 0.001), + origin=(-0.02, -0.02, -0.02), ) # Compute B-field and add as data to grid -grid["B"] = magnet.getB(grid.points) +grid["B"] = magnet.getB(grid.points)*1000 # T -> mT # Compute the field lines -seed = pv.Disc(inner=1, outer=3, r_res=1, c_res=6) -strl = grid.streamlines_from_source( - seed, - vectors="B", - max_time=180, - initial_step_length=0.01, - integration_direction="both", -) +seed = pv.Disc(inner=0.001, outer=0.003, r_res=1, c_res=6) +strl = grid.streamlines_from_source(seed, vectors="B") # Create a Pyvista plotting scene pl = pv.Plotter() @@ -65,12 +57,16 @@ legend_args = { # Add streamlines and legend to scene pl.add_mesh( - strl.tube(radius=0.2), + strl.tube(radius=0.0002), cmap="bwr", scalar_bar_args=legend_args, ) # Prepare and show scene -pl.camera.position = (30, 30, 20) +pl.camera.position = (0.03, 0.03, 0.02) pl.show() -``` \ No newline at end of file +``` + +```{code-cell} ipython3 + +``` From 2bfcf7acfb3592c1d9ff56dbfbd410d3db073b50 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 21:51:25 +0100 Subject: [PATCH 157/240] unpin pyvista version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6a318d16..58ba70e18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ docs = [ "myst-nb", "pandas", "numpy-stl", - "pyvista==0.37", + "pyvista", "panel", ] test = [ From 4ad12609b3c76e2f5352378fb597b0564ec458fd Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 29 Dec 2023 22:51:38 +0100 Subject: [PATCH 158/240] fix units --- docs/_pages/gallery/gallery_app_end_of_shaft.md | 2 +- docs/_pages/gallery/gallery_tutorial_custom.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_pages/gallery/gallery_app_end_of_shaft.md b/docs/_pages/gallery/gallery_app_end_of_shaft.md index 28d67569b..3b486dcb8 100644 --- a/docs/_pages/gallery/gallery_app_end_of_shaft.md +++ b/docs/_pages/gallery/gallery_app_end_of_shaft.md @@ -82,7 +82,7 @@ fig2 = px.line( x="angle (deg)", y=["Bx", "By"], line_dash="pixel", - labels={"value": "Field (mT)"}, + labels={"value": "Field (T)"}, ) fig2.show() ``` diff --git a/docs/_pages/gallery/gallery_tutorial_custom.md b/docs/_pages/gallery/gallery_tutorial_custom.md index ae27ef050..66511aa70 100644 --- a/docs/_pages/gallery/gallery_tutorial_custom.md +++ b/docs/_pages/gallery/gallery_tutorial_custom.md @@ -32,7 +32,7 @@ $$ Here the monopole lies in the origin of the local coordinates, $Q_m$ is the monopole charge and ${\bf r}$ is the observer position. -We create this field as a Python function and hand it over to a CustomSource `field_func` argument. The `field_func` input must be a callable with two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of mT and A/m in the same shape. +We create this field as a Python function and hand it over to a CustomSource `field_func` argument. The `field_func` input must be a callable with two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of T and A/m in the same shape. ```{code-cell} ipython3 import numpy as np @@ -130,7 +130,7 @@ class Monopole(magpy.misc.CustomSource): Parameters ---------- charge: float - Monopole charge in units of mT + Monopole charge in units of T·m² """ def __init__(self, charge, **kwargs): From f444d9f16464d4b55df7135f5a8907f413e7f23c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 30 Dec 2023 01:07:25 +0100 Subject: [PATCH 159/240] pylint --- magpylib/_src/fields/field_BH_triangularmesh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index b19436981..2d1cf92f9 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -8,7 +8,6 @@ import scipy.spatial from magpylib._src.fields.field_BH_triangle import triangle_field -from magpylib._src.utility import convert_HBMJ from magpylib._src.utility import MU0 From 509f0d5e2c1e9056287dec0032078fc85a2a27bd Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 30 Dec 2023 01:22:47 +0100 Subject: [PATCH 160/240] pylint --- magpylib/_src/fields/field_BH_cuboid.py | 3 ++- magpylib/_src/utility.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 0db9da453..346f79a01 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -7,6 +7,8 @@ from magpylib._src.input_checks import check_field_input from magpylib._src.utility import convert_HBMJ +# pylint: disable=too-many-statements + RTOL_SURFACE = 1e-15 # relative distance tolerance to be considered on surface @@ -217,7 +219,6 @@ def magnet_cuboid_field( Cichon: IEEE Sensors Journal, vol. 19, no. 7, April 1, 2019, p.2509 """ - # pylint: disable=too-many-statements check_field_input(field) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 53b808e4b..5080d40e9 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -410,8 +410,7 @@ def convert_HBMJ( J[~mask_inside] *= 0 if output_field_type == "J": return J - if output_field_type == "M": - return J / MU0 + return J / MU0 if output_field_type == input_field_type: return field_values if input_field_type == "B": @@ -422,3 +421,7 @@ def convert_HBMJ( B = field_values * MU0 B[mask_inside] += polarization[mask_inside] return B + raise ValueError( # pragma: no cover + "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " + f"got {output_field_type!r}" + ) From 4e7dd22603991086e4a0eb73625ce928610bdfc2 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 11:13:30 +0100 Subject: [PATCH 161/240] add in_out to toplevel + docstrings updates --- magpylib/_src/fields/field_BH_cuboid.py | 12 ++++ magpylib/_src/fields/field_BH_cylinder.py | 12 ++++ .../_src/fields/field_BH_cylinder_segment.py | 12 ++++ magpylib/_src/fields/field_BH_sphere.py | 12 ++++ magpylib/_src/fields/field_BH_tetrahedron.py | 12 ++++ .../_src/fields/field_BH_triangularmesh.py | 16 +++-- magpylib/_src/fields/field_wrap_BH.py | 68 ++++++++++--------- .../_src/obj_classes/class_BaseExcitations.py | 36 ++++++++-- magpylib/_src/obj_classes/class_Collection.py | 36 ++++++++-- magpylib/_src/obj_classes/class_Sensor.py | 36 ++++++++-- 10 files changed, 201 insertions(+), 51 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 346f79a01..bcee038d9 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -168,6 +168,18 @@ def magnet_cuboid_field( polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field or H-field: ndarray, shape (n,3) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index a7bc4cdb3..36b772c73 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -280,6 +280,18 @@ def magnet_cylinder_field( polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field or H-field: ndarray, shape (n,3) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index b64eeb943..286e887ec 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2380,6 +2380,18 @@ def magnet_cylinder_segment_field( polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field or H-field: ndarray, shape (n,3) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 1c8afb4f5..8f5b5136c 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -38,6 +38,18 @@ def magnet_sphere_field( polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field or H-field: ndarray, shape (n,3) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index de04a4dc2..ef3606229 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -88,6 +88,18 @@ def magnet_tetrahedron_field( polarization: ndarray, shape (n,3) Magnetic polarization vectors in units of T. + in_out: {'auto', 'inside', 'outside'} + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field or H-field: ndarray, shape (n,3) diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index 2d1cf92f9..47cf90bdb 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -528,12 +528,16 @@ def magnet_trimesh_field( Magnetic polarization vectors in units of T. in_out: {'auto', 'inside', 'outside'} - Tells if the points are inside or outside the enclosing mesh for the correct B/H-field - calculation. By default `in_out='auto'` and the inside/outside mask is automatically - generated using a ray tracing algorithm to determine which observers are inside and which - are outside the closed body. For performance reasons, one can define `in_out='outside'` - or `in_out='inside'` if it is known in advance that all observers satisfy the same - condition. + Specify the location of the observers relative to the magnet body, affecting the calculation + of the magnetic field. The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for each + observer. + - 'inside': All observers are considered to be inside the cuboid; use this for performance + optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for performance + optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. Returns ------- diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index bbafb4ccf..90accfc32 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -149,38 +149,9 @@ def getBH_level1( def getBH_level2( - sources, observers, *, field, sumup, squeeze, pixel_agg, output, **kwargs + sources, observers, *, field, sumup, squeeze, pixel_agg, output, in_out, **kwargs ) -> np.ndarray: """Compute field for given sources and observers. - - Parameters - ---------- - sources : src_obj or list - source object or 1D list of L sources/collections with similar - pathlength M and/or 1. - observers : sens_obj or list or pos_obs - pos_obs or sensor object or 1D list of K sensors with similar pathlength M - and/or 1 and sensor pixel of shape (N1,N2,...,3). - sumup : bool, default=False - returns [B1,B2,...] for every source, True returns sum(Bi) sfor all sources. - squeeze : bool, default=True: - If True output is squeezed (axes of length 1 are eliminated) - pixel_agg : str - A compatible numpy aggregator string (e.g. `'min'`, `'max'`, `'mean'`) - which applies on pixel output values. - field : {'B', 'H'} - 'B' computes B field, 'H' computes H-field - output: str, default='ndarray' - Output type, which must be one of `('ndarray', 'dataframe')`. By default a multi- - dimensional array ('ndarray') is returned. If 'dataframe' is chosen, the function - returns a 2D-table as a `pandas.DataFrame` object (the Pandas library must be - installed). - - Returns - ------- - field: ndarray, shape squeeze((L,M,K,N1,N2,...,3)), field of L sources, M path - positions, K sensors and N1xN2x.. observer positions and 3 field components. - Info: ----- - generates a 1D list of sources (collections flattened) and a 1D list of sensors from input @@ -205,6 +176,7 @@ def getBH_level2( observers=observers, field=field, squeeze=squeeze, + in_out=in_out, **kwargs, ) @@ -334,7 +306,7 @@ def getBH_level2( gr = group["sources"] src_dict = get_src_dict(gr, n_pix, n_pp, poso) # compute array dict for level1 B_group = getBH_level1( - field_func=field_func, field=field, **src_dict + field_func=field_func, field=field, **src_dict, in_out=in_out, ) # compute field if B_group is None: raise MagpylibMissingInput( @@ -555,6 +527,7 @@ def getB( squeeze=True, pixel_agg=None, output="ndarray", + in_out="auto", **kwargs, ): """Compute B-field in units of T for given sources and observers. @@ -599,6 +572,19 @@ def getB( `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + See Also -------- *Functional interface @@ -717,11 +703,12 @@ def getB( return getBH_level2( sources, observers, + field="B", sumup=sumup, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="B", + in_out=in_out, **kwargs, ) @@ -733,6 +720,7 @@ def getH( squeeze=True, pixel_agg=None, output="ndarray", + in_out="auto", **kwargs, ): """Compute H-field in units of A/m for given sources and observers. @@ -777,6 +765,19 @@ def getH( `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + See Also -------- *Functional interface @@ -888,10 +889,11 @@ def getH( return getBH_level2( sources, observers, + field="H", sumup=sumup, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="H", + in_out=in_out, **kwargs, ) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index d8af7d5fe..998c9704e 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -51,7 +51,7 @@ def field_func(self, val): ) self._field_func = val - def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): + def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): """Compute the B-field at observers in units of T generated by the source. SI units are used for all inputs and outputs. @@ -79,6 +79,19 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): returns a 2D-table as a `pandas.DataFrame` object (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame @@ -115,14 +128,15 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): return getBH_level2( self, observers, + field="B", sumup=False, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="B", + in_out="auto", ) - def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): + def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): """Compute the H-field in units of A/m at observers generated by the source. Parameters @@ -148,6 +162,19 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): returns a 2D-table as a `pandas.DataFrame` object (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- H-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame @@ -186,11 +213,12 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray"): return getBH_level2( self, observers, + field="H", sumup=False, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="H", + in_out=in_out, ) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 113b2514b..0fa0879ab 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -518,7 +518,7 @@ def _validate_getBH_inputs(self, *inputs): sources, sensors = self, inputs return sources, sensors - def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): + def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): """Compute B-field for given sources and observers. SI units are used for all inputs and outputs. @@ -545,6 +545,19 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame @@ -584,14 +597,15 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): return getBH_level2( sources, sensors, + field="B", sumup=False, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="B", + in_out=in_out, ) - def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): + def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): """Compute H-field for given sources and observers. SI units are used for all inputs and outputs. @@ -626,6 +640,19 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): equivalent to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Examples -------- In this example we create a collection from two sources and two sensors: @@ -657,11 +684,12 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): return getBH_level2( sources, sensors, + field="H", sumup=False, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="H", + in_out=in_out, ) @property diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index af97687f9..8cc64c187 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -141,7 +141,7 @@ def handedness(self, val): self._handedness = val def getB( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray" + self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" ): """Compute the B-field in units of T as seen by the sensor. @@ -168,6 +168,19 @@ def getB( `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- B-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame @@ -208,15 +221,16 @@ def getB( return getBH_level2( sources, self, + field="B", sumup=sumup, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="B", + in_out=in_out, ) def getH( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray" + self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" ): """Compute the H-field in units of A/m as seen by the sensor. @@ -243,6 +257,19 @@ def getH( `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + Returns ------- H-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame @@ -283,11 +310,12 @@ def getH( return getBH_level2( sources, self, + field="H", sumup=sumup, squeeze=squeeze, pixel_agg=pixel_agg, output=output, - field="H", + in_out=in_out, ) @property From 57b61702814c476dda411fa842e33cc503de7f4b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 11:46:51 +0100 Subject: [PATCH 162/240] fix in_out not passed on getB --- magpylib/_src/obj_classes/class_BaseExcitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 998c9704e..93f640b80 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -133,7 +133,7 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_ou squeeze=squeeze, pixel_agg=pixel_agg, output=output, - in_out="auto", + in_out=in_out, ) def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): From a678af38971bcd6b5cf026af52d59a9ece9b3e29 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 11:48:30 +0100 Subject: [PATCH 163/240] pass in_out only for magnets --- magpylib/_src/fields/field_wrap_BH.py | 9 +++++---- magpylib/_src/utility.py | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 90accfc32..553c64148 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -45,7 +45,6 @@ import warnings from itertools import product from typing import Callable - import numpy as np from scipy.spatial.transform import Rotation as R @@ -60,6 +59,7 @@ from magpylib._src.utility import format_obj_input from magpylib._src.utility import format_src_inputs from magpylib._src.utility import get_registered_sources +from magpylib._src.utility import has_parameter def tile_group_property(group: list, n_pp: int, prop_name: str): @@ -305,9 +305,10 @@ def getBH_level2( lg = len(group["sources"]) gr = group["sources"] src_dict = get_src_dict(gr, n_pix, n_pp, poso) # compute array dict for level1 - B_group = getBH_level1( - field_func=field_func, field=field, **src_dict, in_out=in_out, - ) # compute field + if has_parameter(field_func, "in_out"): # in_out passed only to magnets + src_dict["in_out"] = in_out + # compute field + B_group = getBH_level1(field_func=field_func, field=field, **src_dict) if B_group is None: raise MagpylibMissingInput( f"Cannot compute {field}-field because " diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 5080d40e9..0c9e16b37 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -5,6 +5,9 @@ from math import log10 from typing import Optional from typing import Sequence +from typing import Callable +from inspect import signature +from functools import lru_cache import numpy as np @@ -425,3 +428,9 @@ def convert_HBMJ( "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {output_field_type!r}" ) + +@lru_cache(maxsize=None) +def has_parameter(func: Callable, param_name:str)-> bool: + """Check if input function has a specific parameter""" + sig = signature(func) + return param_name in sig.parameters From e01f9f6c2456939e4839fa30e510b97d3c971927 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 12:11:09 +0100 Subject: [PATCH 164/240] add getM and getJ to all top levels --- magpylib/__init__.py | 4 +- magpylib/_src/fields/field_wrap_BH.py | 275 ++++++++++++++++++ .../_src/obj_classes/class_BaseExcitations.py | 120 ++++++++ magpylib/_src/obj_classes/class_Collection.py | 124 ++++++++ magpylib/_src/obj_classes/class_Sensor.py | 120 ++++++++ 5 files changed, 642 insertions(+), 1 deletion(-) diff --git a/magpylib/__init__.py b/magpylib/__init__.py index fa518fc42..90fcb50b7 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -37,6 +37,8 @@ "misc", "getB", "getH", + "getM", + "getJ", "Sensor", "Collection", "show", @@ -53,7 +55,7 @@ from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS from magpylib import magnet, current, misc, core, graphics from magpylib._src.defaults.defaults_classes import default_settings as defaults -from magpylib._src.fields import getB, getH +from magpylib._src.fields import getB, getH, getM, getJ from magpylib._src.obj_classes.class_Sensor import Sensor from magpylib._src.obj_classes.class_Collection import Collection from magpylib._src.display.display import show, show_context diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 553c64148..fbbf155b1 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -898,3 +898,278 @@ def getH( in_out=in_out, **kwargs, ) + +def getM( + sources=None, + observers=None, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", + **kwargs, +): + """Compute M-field in units of A/m for given sources and observers. + + Field implementations can be directly accessed (avoiding the object oriented + Magpylib interface) by providing a string input `sources=source_type`, array_like + positions as `observers` input, and all other necessary input parameters (see below) + as kwargs. + + Parameters + ---------- + sources: source and collection objects or 1D list thereof + Sources that generate the magnetic field. Can be a single source (or collection) + or a 1D list of l sources and/or collection objects. + + Functional interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, + `Sphere`, `Dipole`, `Circle` or `Polyline`). + + observers: array_like or (list of) `Sensor` objects + Can be array_like positions of shape (n1, n2, ..., 3) where the field + should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list + of such sensor objects (must all have similar pixel shapes). All positions + are given in units of m. + + Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding + positions to observer positions in units of m. + + sumup: bool, default=`False` + If `True`, the fields of all sources are summed up. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. only + a single sensor or only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default=`'ndarray'` + Output type, which must be one of (`'ndarray'`, `'dataframe'`). By default a + `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + See Also + -------- + *Functional interface + + position: array_like, shape (3,) or (n,3), default=`(0,0,0)` + Source position(s) in the global coordinates in units of m. + + orientation: scipy `Rotation` object with length 1 or n, default=`None` + Object orientation(s) in the global coordinates. `None` corresponds to + a unit-rotation. + + magnetization: array_like, shape (3,) or (n,3) + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`)! + Magnetization vector(s) (mu0*M, remanence field) in units of A/m given in + the local object coordinates (rotates with object). + + moment: array_like, shape (3) or (n,3), unit A·m² + Only source_type == `Dipole`! + Magnetic dipole moment(s) in units of A·m² given in the local object coordinates + (rotates with object). For homogeneous magnets the relation moment=magnetization*volume + holds. + + current: array_like, shape (n,) + Only source_type == `Circle` or `Polyline`! + Electrical current in units of A. + + dimension: array_like, shape (x,) or (n,x) + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! + Magnet dimension input in units of m and deg. Dimension format x of sources is similar + as in object oriented interface. + + diameter: array_like, shape (n,) + Only source_type == `Sphere` or `Circle`! + Diameter of source in units of m. + + segment_start: array_like, shape (n,3) + Only source_type == `Polyline`! + Start positions of line current segments in units of m. + + segment_end: array_like, shape (n,3) + Only source_type == `Polyline`! + End positions of line current segments in units of m. + + Returns + ------- + M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + M-field at each path position (index m) for each sensor (index k) and each sensor pixel + position (indices n1, n2, ...) in units of A/m. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m are + considered as static beyond their end. + + Functional interface: ndarray, shape (n,3) + M-field for every parameter set in units of A/m. + + Notes + ----- + This function automatically joins all sensor and position inputs together and groups + similar sources for optimal vectorization of the computation. For maximal performance + call this function as little as possible and avoid using it in loops. + """ + return getBH_level2( + sources, + observers, + field="M", + sumup=sumup, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + **kwargs, + ) + +def getJ( + sources=None, + observers=None, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", + **kwargs, +): + """Compute J-field in units of T for given sources and observers. + + Field implementations can be directly accessed (avoiding the object oriented + Magpylib interface) by providing a string input `sources=source_type`, array_like + positions as `observers` input, and all other necessary input parameters (see below) + as kwargs. + + Parameters + ---------- + sources: source and collection objects or 1D list thereof + Sources that generate the magnetic field. Can be a single source (or collection) + or a 1D list of l sources and/or collection objects. + + Functional interface: input must be one of (`Cuboid`, `Cylinder`, `CylinderSegment`, + `Sphere`, `Dipole`, `Circle` or `Polyline`). + + observers: array_like or (list of) `Sensor` objects + Can be array_like positions of shape (n1, n2, ..., 3) where the field + should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list + of such sensor objects (must all have similar pixel shapes). All positions + are given in units of m. + + Functional interface: Input must be array_like with shape (3,) or (n,3) corresponding + positions to observer positions in units of m. + + sumup: bool, default=`False` + If `True`, the fields of all sources are summed up. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. only + a single sensor or only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default=`'ndarray'` + Output type, which must be one of (`'ndarray'`, `'dataframe'`). By default a + `numpy.ndarray` object is returned. If `'dataframe'` is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + See Also + -------- + *Functional interface + + position: array_like, shape (3,) or (n,3), default=`(0,0,0)` + Source position(s) in the global coordinates in units of m. + + orientation: scipy `Rotation` object with length 1 or n, default=`None` + Object orientation(s) in the global coordinates. `None` corresponds to + a unit-rotation. + + magnetization: array_like, shape (3,) or (n,3) + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`, `Sphere`)! + Magnetization vector(s) (mu0*M, remanence field) in units of A/m given in + the local object coordinates (rotates with object). + + moment: array_like, shape (3) or (n,3), unit A·m² + Only source_type == `Dipole`! + Magnetic dipole moment(s) in units of A·m² given in the local object coordinates + (rotates with object). For homogeneous magnets the relation moment=magnetization*volume + holds. + + current: array_like, shape (n,) + Only source_type == `Circle` or `Polyline`! + Electrical current in units of A. + + dimension: array_like, shape (x,) or (n,x) + Only source_type in (`Cuboid`, `Cylinder`, `CylinderSegment`)! + Magnet dimension input in units of m and deg. Dimension format x of sources is similar + as in object oriented interface. + + diameter: array_like, shape (n,) + Only source_type == `Sphere` or `Circle`! + Diameter of source in units of m. + + segment_start: array_like, shape (n,3) + Only source_type == `Polyline`! + Start positions of line current segments in units of m. + + segment_end: array_like, shape (n,3) + Only source_type == `Polyline`! + End positions of line current segments in units of m. + + Returns + ------- + J-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + J-field at each path position (index m) for each sensor (index k) and each sensor pixel + position (indices n1, n2, ...) in units of T. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m are + considered as static beyond their end. + + Functional interface: ndarray, shape (n,3) + J-field for every parameter set in units of T. + + Notes + ----- + This function automatically joins all sensor and position inputs together and groups + similar sources for optimal vectorization of the computation. For maximal performance + call this function as little as possible and avoid using it in loops. + + """ + return getBH_level2( + sources, + observers, + field="J", + sumup=sumup, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + **kwargs, + ) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 93f640b80..93d14c1a3 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -221,6 +221,126 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_ou in_out=in_out, ) + def getM(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + """Compute the M-field in units of A/m at observers generated by the source. + + Parameters + ---------- + observers: array_like or (list of) `Sensor` objects + Can be array_like positions of shape (n1, n2, ..., 3) where the field + should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list + of such sensor objects (must all have similar pixel shapes). All positions + are given in units of m. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. + only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a multi- + dimensional array ('ndarray') is returned. If 'dataframe' is chosen, the function + returns a 2D-table as a `pandas.DataFrame` object (the Pandas library must be + installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + Returns + ------- + M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + M-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indices n1,n2,...) in units of A/m. Sensor pixel positions are + equivalent to simple observer positions. Paths of objects that are shorter than + index m will be considered as static beyond their end. + """ + observers = format_star_input(observers) + return getBH_level2( + self, + observers, + field="M", + sumup=False, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + + def getJ(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + """Compute the J-field at observers in units of T generated by the source. + + SI units are used for all inputs and outputs. + + Parameters + ---------- + observers: array_like or (list of) `Sensor` objects + Can be array_like positions of shape (n1, n2, ..., 3) where the field + should be evaluated, a `Sensor` object with pixel shape (n1, n2, ..., 3) or a list + of such sensor objects (must all have similar pixel shapes). All positions are given + in units of m. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. + only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a multi- + dimensional array ('ndarray') is returned. If 'dataframe' is chosen, the function + returns a 2D-table as a `pandas.DataFrame` object (the Pandas library must be + installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + Returns + ------- + J-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + J-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indices n1,n2,...) in units of T. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m will be + considered as static beyond their end. + """ + observers = format_star_input(observers) + return getBH_level2( + self, + observers, + field="J", + sumup=False, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + class BaseMagnet(BaseSource): """provides the magnetization and polarization attributes for magnet classes""" diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 0fa0879ab..a6085ae9b 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -692,6 +692,130 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out=" in_out=in_out, ) + def getM(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + """Compute M-field for given sources and observers. + + SI units are used for all inputs and outputs. + + Parameters + ---------- + inputs: source or observer objects + Input can only be observers if the collection contains only sources. In this case the + collection behaves like a single source. + Input can only be sources if the collection contains sensors. In this case the + collection behaves like a list of all its sensors. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. + only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a + `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + Returns + ------- + M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + M-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indeices n1,n2,...) in units of A/m. Sensor pixel positions are + equivalent to simple observer positions. Paths of objects that are shorter than + index m are considered as static beyond their end. + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + """ + + sources, sensors = self._validate_getBH_inputs(*inputs) + + return getBH_level2( + sources, + sensors, + field="M", + sumup=False, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + + def getJ(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + """Compute J-field for given sources and observers. + + SI units are used for all inputs and outputs. + + Parameters + ---------- + inputs: source or observer objects + Input can only be observers if the collection contains only sources. In this case the + collection behaves like a single source. + Input can only be sources if the collection contains only sensors. In this case the + collection behaves like a list of all its sensors. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. + only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a + `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + Returns + ------- + J-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + J-field at each path position ( index m) for each sensor (index k) and each + sensor pixel position (indices n1,n2,...) in units of T. Sensor pixel positions + are equivalent to simple observer positions. Paths of objects that are shorter + than index m are considered as static beyond their end. + """ + + sources, sensors = self._validate_getBH_inputs(*inputs) + + return getBH_level2( + sources, + sensors, + field="J", + sumup=False, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + @property def _default_style_description(self): """Default style description text""" diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 8cc64c187..1ea994ae5 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -318,6 +318,126 @@ def getH( in_out=in_out, ) + def getM( + self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): + """Compute the M-field in units of A/m as seen by the sensor. + + Parameters + ---------- + sources: source and collection objects or 1D list thereof + Sources that generate the magnetic field. Can be a single source (or collection) + or a 1D list of l source and/or collection objects. + + sumup: bool, default=`False` + If `True`, the fields of all sources are summed up. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. only + a single sensor or only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a + `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + Returns + ------- + M-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame + M-field of each source (index l) at each path position (index m) and each sensor pixel + position (indices n1,n2,...) in units of A/m. Paths of objects that are shorter than + index m are considered as static beyond their end. + """ + sources = format_star_input(sources) + return getBH_level2( + sources, + self, + field="M", + sumup=sumup, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + + def getJ( + self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): + """Compute the J-field in units of T as seen by the sensor. + + Parameters + ---------- + sources: source and collection objects or 1D list thereof + Sources that generate the magnetic field. Can be a single source (or collection) + or a 1D list of l source and/or collection objects. + + sumup: bool, default=`False` + If `True`, the fields of all sources are summed up. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. only + a single sensor or only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a + `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. + + Returns + ------- + J-field: ndarray, shape squeeze(l, m, n1, n2, ..., 3) or DataFrame + J-field of each source (index l) at each path position (index m) and each sensor pixel + position (indices n1,n2,...) in units of T. Paths of objects that are shorter than + index m are considered as static beyond their end. + """ + sources = format_star_input(sources) + return getBH_level2( + sources, + self, + field="J", + sumup=sumup, + squeeze=squeeze, + pixel_agg=pixel_agg, + output=output, + in_out=in_out, + ) + @property def _default_style_description(self): """Default style description text""" From 7cdea92db39e3c6742d207f4d2db5259b46d069e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 12:12:41 +0100 Subject: [PATCH 165/240] fix imports for getB and getM --- magpylib/_src/fields/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/fields/__init__.py b/magpylib/_src/fields/__init__.py index 91b9819b5..48f1c6e52 100644 --- a/magpylib/_src/fields/__init__.py +++ b/magpylib/_src/fields/__init__.py @@ -1,6 +1,6 @@ """_src.fields""" -__all__ = ["getB", "getH"] +__all__ = ["getB", "getH", "getM", "getJ"] # create interface to outside of package -from magpylib._src.fields.field_wrap_BH import getB, getH +from magpylib._src.fields.field_wrap_BH import getB, getH, getM, getJ From 8628b98210a8b3aace56c11d563ca2c5af3aeff1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 13:10:27 +0100 Subject: [PATCH 166/240] add self consistency getHBMJ test --- tests/test_getBH_interfaces.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py index 89c0473e0..fe08f2ab4 100644 --- a/tests/test_getBH_interfaces.py +++ b/tests/test_getBH_interfaces.py @@ -361,3 +361,22 @@ def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): match=rf"Parameter `{missing_arg}` of .* must be set.", ): getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) + +@pytest.mark.parametrize("field", ("H","B", "M", "J")) +@pytest.mark.parametrize("in_out", ("auto", "inside", "outside")) +def test_getHBMJ_self_consistency(field, in_out): + sources = [ + magpy.magnet.Cuboid(dimension=(1,1,1), polarization=(0,0,1)), + magpy.current.Circle(diameter=1, current=1), + ] + sens = magpy.Sensor(position=np.linspace((-1,0,0),(1,0,0), 10)) + src = sources[0] + + F1 = getattr(magpy, f"get{field}")(src, sens, in_out=in_out) + F2 = getattr(sens, f"get{field}")(src, in_out=in_out) + F3 = getattr(src, f"get{field}")(sens, in_out=in_out) + F4 = getattr(magpy.Collection(src, sens), f"get{field}")(in_out=in_out) + + np.testing.assert_allclose(F1, F2) + np.testing.assert_allclose(F1, F3) + np.testing.assert_allclose(F1, F4) From c07104c6c348d62e5484cc2c563730ff6ef85356 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 13:10:37 +0100 Subject: [PATCH 167/240] fix tests --- magpylib/_src/display/display.py | 10 +++++----- magpylib/_src/display/traces_generic.py | 2 ++ magpylib/_src/display/traces_utility.py | 1 + magpylib/_src/fields/field_wrap_BH.py | 11 +++++++---- tests/test_exceptions.py | 3 ++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 389e7b741..c4c7c04d8 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -140,7 +140,7 @@ def get_show_func(backend): ) -ROW_COL_SPECIFIC_NAMES = ("row", "col", "output", "sumup", "pixel_agg") +ROW_COL_SPECIFIC_NAMES = ("row", "col", "output", "sumup", "pixel_agg", "in_out") def infer_backend(canvas): @@ -305,10 +305,10 @@ def show( output: tuple or string, default="model3d" Can be a string or a tuple of strings specifying the plot output type. By default `output='model3d'` displays the 3D representations of the objects. If output is a tuple of - strings it must be a combination of 'B' or 'H' and 'x', 'y' and/or 'z'. When having multiple - coordinates, the field value is the combined vector length (e.g. `('Bx', 'Hxy', 'Byz')`) - 'Bxy' is equivalent to sqrt(|Bx|^2 + |By|^2). A 2D line plot is then represented - accordingly if the objects contain at least one source and one sensor. + strings it must be a combination of 'B', 'H', 'M' or 'J' and 'x', 'y' and/or 'z'. When + having multiple coordinates, the field value is the combined vector length + (e.g. `('Bx', 'Hxy', 'Byz')`) 'Bxy' is equivalent to sqrt(|Bx|^2 + |By|^2). A 2D line plot + is then represented accordingly if the objects contain at least one source and one sensor. sumup: bool, default=True If True, sums the field values of the sources. Applies only if `output` is not `'model3d'`. diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 9d21ca88a..30ada8712 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -253,6 +253,7 @@ def get_generic_traces_2D( col=None, sumup=True, pixel_agg=None, + in_out="auto", style_path_frames=None, ): """draws and animates sensor values over a path in a subplot""" @@ -298,6 +299,7 @@ def get_generic_traces_2D( field=field_str, pixel_agg=pixel_agg, output="ndarray", + in_out=in_out, ) BH_array = BH_array.swapaxes(1, 2) # swap axes to have sensors first, path second diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index c60cac959..580a147cd 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -574,6 +574,7 @@ def process_show_input_objs(objs, **kwargs): "output": "model3d", "sumup": True, "pixel_agg": "mean", + "in_out": "auto", } max_rows = max_cols = 1 flat_objs = [] diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index fbbf155b1..1e97c8daa 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -138,6 +138,10 @@ def getBH_level1( # transform obs_pos into source CS pos_rel_rot = orientation.apply(observers - position, inverse=True) + # filter arguments + if not has_parameter(field_func, "in_out"): # in_out passed only to magnets + kwargs.pop("in_out", None) + # compute field BH = field_func(field=field, observers=pos_rel_rot, **kwargs) @@ -305,10 +309,8 @@ def getBH_level2( lg = len(group["sources"]) gr = group["sources"] src_dict = get_src_dict(gr, n_pix, n_pp, poso) # compute array dict for level1 - if has_parameter(field_func, "in_out"): # in_out passed only to magnets - src_dict["in_out"] = in_out # compute field - B_group = getBH_level1(field_func=field_func, field=field, **src_dict) + B_group = getBH_level1(field_func=field_func, field=field, in_out=in_out, **src_dict) if B_group is None: raise MagpylibMissingInput( f"Cannot compute {field}-field because " @@ -418,6 +420,7 @@ def getBH_dict_level2( position=(0, 0, 0), orientation=R.identity(), squeeze=True, + in_out="auto", **kwargs: dict, ) -> np.ndarray: """Functional interface access to vectorized computation @@ -514,7 +517,7 @@ def getBH_dict_level2( kwargs["orientation"] = R.from_quat(kwargs["orientation"]) # compute and return B - B = getBH_level1(field=field, field_func=field_func, **kwargs) + B = getBH_level1(field=field, field_func=field_func, in_out=in_out, **kwargs) if B is not None and squeeze: return np.squeeze(B) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 820e19a1e..6fbeda477 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -10,7 +10,7 @@ from magpylib._src.utility import format_src_inputs from magpylib._src.utility import test_path_format as tpf -GETBH_KWARGS = {"sumup": False, "squeeze": True, "pixel_agg": None, "output": "ndarray"} +GETBH_KWARGS = {"sumup": False, "squeeze": True, "pixel_agg": None, "output": "ndarray", "in_out":"auto"} def getBHv_unknown_source_type(): @@ -36,6 +36,7 @@ def getBH_level2_bad_input1(): sumup=False, squeeze=True, pixel_agg=None, + in_out="auto", field="B", output="ndarray", ) From c092df64c30d29e068a4eab81cf8c24e362cbde4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 13:13:12 +0100 Subject: [PATCH 168/240] pre-commit --- magpylib/_src/fields/field_wrap_BH.py | 7 +++- .../_src/obj_classes/class_BaseExcitations.py | 16 +++++++--- magpylib/_src/obj_classes/class_Collection.py | 16 +++++++--- magpylib/_src/obj_classes/class_Sensor.py | 32 ++++++++++++++++--- magpylib/_src/utility.py | 9 +++--- tests/test_exceptions.py | 8 ++++- tests/test_getBH_interfaces.py | 7 ++-- 7 files changed, 74 insertions(+), 21 deletions(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 1e97c8daa..070be39c1 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -45,6 +45,7 @@ import warnings from itertools import product from typing import Callable + import numpy as np from scipy.spatial.transform import Rotation as R @@ -310,7 +311,9 @@ def getBH_level2( gr = group["sources"] src_dict = get_src_dict(gr, n_pix, n_pp, poso) # compute array dict for level1 # compute field - B_group = getBH_level1(field_func=field_func, field=field, in_out=in_out, **src_dict) + B_group = getBH_level1( + field_func=field_func, field=field, in_out=in_out, **src_dict + ) if B_group is None: raise MagpylibMissingInput( f"Cannot compute {field}-field because " @@ -902,6 +905,7 @@ def getH( **kwargs, ) + def getM( sources=None, observers=None, @@ -1039,6 +1043,7 @@ def getM( **kwargs, ) + def getJ( sources=None, observers=None, diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 93d14c1a3..29c285e17 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -51,7 +51,9 @@ def field_func(self, val): ) self._field_func = val - def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getB( + self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute the B-field at observers in units of T generated by the source. SI units are used for all inputs and outputs. @@ -136,7 +138,9 @@ def getB(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_ou in_out=in_out, ) - def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getH( + self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute the H-field in units of A/m at observers generated by the source. Parameters @@ -221,7 +225,9 @@ def getH(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_ou in_out=in_out, ) - def getM(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getM( + self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute the M-field in units of A/m at observers generated by the source. Parameters @@ -280,7 +286,9 @@ def getM(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_ou in_out=in_out, ) - def getJ(self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getJ( + self, *observers, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute the J-field at observers in units of T generated by the source. SI units are used for all inputs and outputs. diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index a6085ae9b..1ef26425e 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -518,7 +518,9 @@ def _validate_getBH_inputs(self, *inputs): sources, sensors = self, inputs return sources, sensors - def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getB( + self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute B-field for given sources and observers. SI units are used for all inputs and outputs. @@ -605,7 +607,9 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out=" in_out=in_out, ) - def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getH( + self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute H-field for given sources and observers. SI units are used for all inputs and outputs. @@ -692,7 +696,9 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out=" in_out=in_out, ) - def getM(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getM( + self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute M-field for given sources and observers. SI units are used for all inputs and outputs. @@ -754,7 +760,9 @@ def getM(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out=" in_out=in_out, ) - def getJ(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto"): + def getJ( + self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + ): """Compute J-field for given sources and observers. SI units are used for all inputs and outputs. diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 1ea994ae5..a954c4cda 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -141,7 +141,13 @@ def handedness(self, val): self._handedness = val def getB( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + self, + *sources, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", ): """Compute the B-field in units of T as seen by the sensor. @@ -230,7 +236,13 @@ def getB( ) def getH( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + self, + *sources, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", ): """Compute the H-field in units of A/m as seen by the sensor. @@ -319,7 +331,13 @@ def getH( ) def getM( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + self, + *sources, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", ): """Compute the M-field in units of A/m as seen by the sensor. @@ -379,7 +397,13 @@ def getM( ) def getJ( - self, *sources, sumup=False, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" + self, + *sources, + sumup=False, + squeeze=True, + pixel_agg=None, + output="ndarray", + in_out="auto", ): """Compute the J-field in units of T as seen by the sensor. diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 0c9e16b37..2e29104ca 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -2,12 +2,12 @@ # pylint: disable=import-outside-toplevel # pylint: disable=cyclic-import # import numbers +from functools import lru_cache +from inspect import signature from math import log10 +from typing import Callable from typing import Optional from typing import Sequence -from typing import Callable -from inspect import signature -from functools import lru_cache import numpy as np @@ -429,8 +429,9 @@ def convert_HBMJ( f"got {output_field_type!r}" ) + @lru_cache(maxsize=None) -def has_parameter(func: Callable, param_name:str)-> bool: +def has_parameter(func: Callable, param_name: str) -> bool: """Check if input function has a specific parameter""" sig = signature(func) return param_name in sig.parameters diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 6fbeda477..49395d691 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -10,7 +10,13 @@ from magpylib._src.utility import format_src_inputs from magpylib._src.utility import test_path_format as tpf -GETBH_KWARGS = {"sumup": False, "squeeze": True, "pixel_agg": None, "output": "ndarray", "in_out":"auto"} +GETBH_KWARGS = { + "sumup": False, + "squeeze": True, + "pixel_agg": None, + "output": "ndarray", + "in_out": "auto", +} def getBHv_unknown_source_type(): diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py index fe08f2ab4..d32612cbb 100644 --- a/tests/test_getBH_interfaces.py +++ b/tests/test_getBH_interfaces.py @@ -362,14 +362,15 @@ def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): ): getattr(getattr(magpy, module), class_)(**kwargs).getB([0, 0, 0]) -@pytest.mark.parametrize("field", ("H","B", "M", "J")) + +@pytest.mark.parametrize("field", ("H", "B", "M", "J")) @pytest.mark.parametrize("in_out", ("auto", "inside", "outside")) def test_getHBMJ_self_consistency(field, in_out): sources = [ - magpy.magnet.Cuboid(dimension=(1,1,1), polarization=(0,0,1)), + magpy.magnet.Cuboid(dimension=(1, 1, 1), polarization=(0, 0, 1)), magpy.current.Circle(diameter=1, current=1), ] - sens = magpy.Sensor(position=np.linspace((-1,0,0),(1,0,0), 10)) + sens = magpy.Sensor(position=np.linspace((-1, 0, 0), (1, 0, 0), 10)) src = sources[0] F1 = getattr(magpy, f"get{field}")(src, sens, in_out=in_out) From 0a71c31a13908182b1a8e18d040436a381776141 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 13:18:38 +0100 Subject: [PATCH 169/240] pylint --- magpylib/_src/fields/field_wrap_BH.py | 3 ++- magpylib/_src/utility.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 070be39c1..82351f62c 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -1,3 +1,5 @@ +# pylint: disable=cyclic-import +# pylint: disable=too-many-lines """Field computation structure: level0:(field_BH_XXX.py files) @@ -40,7 +42,6 @@ level5(sens.getB, sens.getH): <--- USER INTERFACE """ -# pylint: disable=cyclic-import import numbers import warnings from itertools import product diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 2e29104ca..43d4cfe1f 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -410,6 +410,7 @@ def convert_HBMJ( if output_field_type in "MJ": J = polarization.copy() if mask_inside is not None: + # pylint: disable=invalid-unary-operand-type J[~mask_inside] *= 0 if output_field_type == "J": return J From 3eeea4383e8a3766d934bc7269557248b81a3390 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 3 Jan 2024 15:21:57 +0100 Subject: [PATCH 170/240] avoid edge case warning --- tests/test_obj_Collection.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_obj_Collection.py b/tests/test_obj_Collection.py index f50121533..8db0a32bb 100644 --- a/tests/test_obj_Collection.py +++ b/tests/test_obj_Collection.py @@ -121,15 +121,15 @@ def test_Collection_basics(): def test_col_getB(test_input, expected): """testing some Collection stuff with getB""" src1 = magpy.magnet.Cuboid( - polarization=(1, 0, 1), dimension=(8, 4, 6), position=(0, 0, 0) + polarization=(1, 0, 1), dimension=(1, 1, 1), position=(0, 0, 0) ) src2 = magpy.magnet.Cylinder( - polarization=(0, 1, 0), dimension=(8, 5), position=(-15, 0, 0) + polarization=(0, 1, 0), dimension=(1, 1), position=(-1, 0, 0) ) - sens1 = magpy.Sensor(position=(0, 0, 6)) - sens2 = magpy.Sensor(position=(0, 0, 6)) - sens3 = magpy.Sensor(position=(0, 0, 6)) - sens4 = magpy.Sensor(position=(0, 0, 6)) + sens1 = magpy.Sensor(position=(0, 0, 1)) + sens2 = magpy.Sensor(position=(0, 0, 1)) + sens3 = magpy.Sensor(position=(0, 0, 1)) + sens4 = magpy.Sensor(position=(0, 0, 1)) sens_col = sens1 + sens2 + sens3 + sens4 src_col = src1 + src2 From c7beef90228788ab1c8dd2585d787d5e3f4d282c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Wed, 3 Jan 2024 22:08:23 +0100 Subject: [PATCH 171/240] pylint --- tests/test_getBH_interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_getBH_interfaces.py b/tests/test_getBH_interfaces.py index d32612cbb..8f6231f3b 100644 --- a/tests/test_getBH_interfaces.py +++ b/tests/test_getBH_interfaces.py @@ -366,6 +366,7 @@ def test_getB_on_missing_excitations(module, class_, missing_arg, kwargs): @pytest.mark.parametrize("field", ("H", "B", "M", "J")) @pytest.mark.parametrize("in_out", ("auto", "inside", "outside")) def test_getHBMJ_self_consistency(field, in_out): + """test getHBMJ self consistency""" sources = [ magpy.magnet.Cuboid(dimension=(1, 1, 1), polarization=(0, 0, 1)), magpy.current.Circle(diameter=1, current=1), From d9f4e188e8a2bc087ba1ec5726d22ec4abb3078b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 4 Jan 2024 16:19:58 +0100 Subject: [PATCH 172/240] fix Sensor default description and allow pixel=None --- magpylib/_src/display/traces_core.py | 7 +++--- magpylib/_src/display/traces_generic.py | 4 +++- magpylib/_src/fields/field_wrap_BH.py | 7 +++++- magpylib/_src/input_checks.py | 3 ++- .../_src/obj_classes/class_BaseDisplayRepr.py | 11 +++++---- magpylib/_src/obj_classes/class_Sensor.py | 24 +++++++++---------- tests/test_obj_BaseGeo.py | 2 +- 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/magpylib/_src/display/traces_core.py b/magpylib/_src/display/traces_core.py index b65eae4ad..fe470b2d5 100644 --- a/magpylib/_src/display/traces_core.py +++ b/magpylib/_src/display/traces_core.py @@ -545,7 +545,10 @@ def make_Sensor(obj, autosize=None, **kwargs) -> Dict[str, Any]: style = obj.style dimension = getattr(obj, "dimension", style.size) pixel = obj.pixel - pixel = np.unique(np.array(pixel).reshape((-1, 3)), axis=0) + no_pix = pixel is None + if not no_pix: + pixel = np.unique(np.array(pixel).reshape((-1, 3)), axis=0) + one_pix = not no_pix and pixel.shape[0] == 1 style_arrows = style.arrows.as_dict(flatten=True, separator="_") sensor = get_sensor_mesh( **style_arrows, center_color=style.color, handedness=obj.handedness @@ -557,8 +560,6 @@ def make_Sensor(obj, autosize=None, **kwargs) -> Dict[str, Any]: [dimension] * 3 if isinstance(dimension, (float, int)) else dimension[:3], dtype=float, ) - no_pix = pixel.shape[0] == 1 and (pixel == 0).all() - one_pix = pixel.shape[0] == 1 and not (pixel == 0).all() if autosize is not None and style.sizemode == "scaled": dim *= autosize if no_pix: diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 9d21ca88a..b267f4ff6 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -346,7 +346,9 @@ def get_label_and_color(obj): label, color = label_sens, color_sens num_of_pix = ( len(sens.pixel.reshape(-1, 3)) - if (not isinstance(sens, magpy.Collection)) and sens.pixel.ndim != 1 + if (not isinstance(sens, magpy.Collection)) + and sens.pixel is not None + and sens.pixel.ndim != 1 else 1 ) pix_suff = "" diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index bbafb4ccf..a266eefcd 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -301,7 +301,12 @@ def getBH_level2( # allows sensors with different pixel shapes <- relevant? poso = [ [ - r.apply(sens.pixel.reshape(-1, 3)) + p + ( + np.array([[0, 0, 0]]) + if sens.pixel is None + else r.apply(sens.pixel.reshape(-1, 3)) + ) + + p for r, p in zip(sens._orientation, sens._position) ] for sens in sensors diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index 434e48ace..06ab08183 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -505,7 +505,8 @@ def check_format_input_observers(inp, pixel_agg=None): # all pixel shapes must be the same pix_shapes = [ - (1, 3) if s.pixel.shape == (3,) else s.pixel.shape for s in sensors + (1, 3) if (s.pixel is None or s.pixel.shape == (3,)) else s.pixel.shape + for s in sensors ] if pixel_agg is None and not all_same(pix_shapes): raise MagpylibBadUserInput( diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index 19d845460..b69b93e7b 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -63,11 +63,12 @@ def _get_description(self, exclude=None): val = f"{val[-1]}" elif k == "pixel": val = getattr(self, "pixel") - px_shape = val.shape[:-1] - val_str = f"{int(np.prod(px_shape))}" - if val.ndim > 2: - val_str += f" ({'x'.join(str(p) for p in px_shape)})" - val = val_str + if val is not None: + px_shape = val.shape[:-1] + val_str = f"{int(np.prod(px_shape))}" + if val.ndim > 2: + val_str += f" ({'x'.join(str(p) for p in px_shape)})" + val = val_str elif k == "status_disconnected_data": val = getattr(self, k) if val is not None: diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index af97687f9..2a4a70c3a 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -92,7 +92,7 @@ def __init__( self, position=(0, 0, 0), orientation=None, - pixel=(0, 0, 0), + pixel=None, handedness="right", style=None, **kwargs, @@ -123,7 +123,8 @@ def pixel(self, pix): dims=range(1, 20), shape_m1=3, sig_name="pixel", - sig_type="array_like (list, tuple, ndarray) with shape (n1, n2, ..., 3)", + sig_type="array_like (list, tuple, ndarray) with shape (n1, n2, ..., 3) or None", + allow_None=True, ) @property @@ -293,13 +294,12 @@ def getH( @property def _default_style_description(self): """Default style description text""" - pixel = np.array(self.pixel).reshape((-1, 3)) - pix_uniq = np.unique(pixel, axis=0) - one_pix = pix_uniq.shape[0] == 1 and not (pix_uniq == 0).all() - return ( - f" ({'x'.join(str(p) for p in self.pixel.shape[:-1])} pixels)" - if self.pixel.ndim != 1 - else f" ({pixel[1:].shape[0]} pixel)" - if one_pix - else "" - ) + pix = self.pixel + desc = "" + if pix is not None: + px_shape = pix.shape[:-1] + nop = int(np.prod(px_shape)) + if pix.ndim > 2: + desc += f"{'x'.join(str(p) for p in px_shape)}=" + desc += f"{nop} pixel{'s'[:nop^1]}" + return desc diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index adbe6cd96..15d94ecce 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -480,7 +480,7 @@ def test_describe(): + " • position: [0. 0. 0.] m\n" + " • orientation: [0. 0. 0.] deg\n" + " • handedness: right \n" - + " • pixel: 1 \n" + + " • pixel: None \n" + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True), " + "y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," + " color=None, description=Description(show=None, text=None), label=None, " From 83ce7f3d35fcae02ba30ceac862f99ff209b49e1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 8 Jan 2024 17:18:39 +0100 Subject: [PATCH 173/240] rework base disp repr tests --- .../_src/obj_classes/class_BaseDisplayRepr.py | 4 +- tests/test_obj_BaseGeo.py | 206 +++++++++--------- 2 files changed, 108 insertions(+), 102 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py index b69b93e7b..1b404bd3a 100644 --- a/magpylib/_src/obj_classes/class_BaseDisplayRepr.py +++ b/magpylib/_src/obj_classes/class_BaseDisplayRepr.py @@ -48,7 +48,7 @@ def _get_description(self, exclude=None): for k in list(dict.fromkeys(list(UNITS) + list(params))): if not k.startswith("_") and k in params and k not in exclude: unit = UNITS.get(k, None) - unit_str = f"{unit}" if unit else "" + unit_str = f" {unit}" if unit else "" if k == "position": val = getattr(self, "_position") if val.shape[0] != 1: @@ -79,7 +79,7 @@ def _get_description(self, exclude=None): val = f"shape{val.shape}" else: val = getattr(self, k) - lines.append(f" • {k}: {val} {unit_str}") + lines.append(f" • {k}: {val}{unit_str}") return lines def describe(self, *, exclude=("style", "field_func"), return_string=False): diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 15d94ecce..741a197e6 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -7,7 +7,6 @@ import magpylib as magpy from magpylib._src.obj_classes.class_BaseGeo import BaseGeo - # pylint: disable=no-member @@ -410,104 +409,113 @@ def test_copy_order(): assert desc_after == desc_before -def test_describe(): - """testing descibe method""" +def match_string_up_to_id(s1, s2, /): + """Checks if 2 inputs (first is list, second string) match as long as id= follows a + series of numbers""" + patt = "id=[0-9]*[0-9]", "id=Regex" + assert re.sub(*patt, "\n".join(s1)).split("\n") == re.sub(*patt, s2).split("\n") + + +def test_describe_with_label(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') # pylint: disable=protected-access - x1 = magpy.magnet.Cuboid(style_label="x1") - x2 = magpy.magnet.Cylinder( - style_label="x2", dimension=(1, 3), polarization=(2, 3, 4) - ) - s1 = magpy.Sensor(position=[(1, 2, 3)] * 3, pixel=[(1, 2, 3)] * 15) + x = magpy.magnet.Cuboid(style_label="x1") - desc = x1.describe() + # describe calls print by default -> no return value + desc = x.describe() assert desc is None - test = ( - "
Cuboid(id=REGEX, label='x1')
• parent: None
• " - "position: [0. 0. 0.] m
• orientation: [0. 0. 0.] deg
• " - "dimension: None m
• magnetization: None A/m
• polarization: None T
" - ) - rep = x1._repr_html_() - rep = re.sub("id=[0-9]*[0-9]", "id=REGEX", rep) - assert test == rep - - magpy.Collection(x1, x2) + # describe string test = [ - "Cuboid(id=REGEX, label='x1')", - " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE + "Cuboid(id=2743358534352, label='x1')", + " • parent: None", " • position: [0. 0. 0.] m", " • orientation: [0. 0. 0.] deg", " • dimension: None m", " • magnetization: None A/m", " • polarization: None T", ] - desc = x1.describe(return_string=True) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - assert test == desc.split("\n") + match_string_up_to_id(test, x.describe(return_string=True)) + + # describe html string + test_html = ("
" + test + "
").split("\n") + match_string_up_to_id(test_html, x._repr_html_().replace("
", "\n")) + +def test_describe_with_parent(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') + x = magpy.magnet.Cuboid(style_label="x1") + magpy.Collection(x) # add parent test = [ - "Cylinder(id=REGEX, label='x2')", - " • parent: Collection(id=REGEX) ", # INVISIBLE SPACE + "Cuboid(id=1687262797456, label='x1')", + " • parent: Collection(id=1687262859280)", " • position: [0. 0. 0.] m", " • orientation: [0. 0. 0.] deg", - " • dimension: [1. 3.] m", - " • magnetization: [1591549.43091895 2387324.14637843 3183098.86183791] A/m", - " • polarization: [2. 3. 4.] T", + " • dimension: None m", + " • magnetization: None A/m", + " • polarization: None T", ] - desc = x2.describe(return_string=True) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - assert test == desc.split("\n") + match_string_up_to_id(test, x.describe(return_string=True)) + +def test_describe_with_path(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') + x = magpy.Sensor(position=[(1, 2, 3)] * 3) test = [ - "Sensor(id=REGEX)", - " • parent: None ", # INVISIBLE SPACE + "Sensor(id=2743359152656)", + " • parent: None", " • path length: 3", " • position (last): [1. 2. 3.] m", " • orientation (last): [0. 0. 0.] deg", - " • handedness: right ", - " • pixel: 15 ", # INVISIBLE SPACE + " • handedness: right", + " • pixel: None", ] - desc = s1.describe(return_string=True) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - assert test == desc.split("\n") - - # exclude=None test - s = magpy.Sensor() - desc = s.describe(exclude=None, return_string=True) - test = ( - "Sensor(id=REGEX)\n" - + " • parent: None \n" - + " • position: [0. 0. 0.] m\n" - + " • orientation: [0. 0. 0.] deg\n" - + " • handedness: right \n" - + " • pixel: None \n" - + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True), " - + "y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," - + " color=None, description=Description(show=None, text=None), label=None, " - + "legend=Legend(show=None), " - + "model3d=Model3d(data=[], showdefault=True), opacity=None, path=Path(frames=None," - + " line=Line(color=None, style=None, width=None), marker=Marker(color=None," - + " size=None, symbol=None), numbering=None, show=None), pixel=Pixel(color=None," - + " size=1, sizemode=None, symbol=None), size=None, sizemode=None) " - ) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - assert desc == test - - # lots of sensor pixel - s = magpy.Sensor(pixel=[[[(1, 2, 3)] * 5] * 5] * 3, handedness="left") - desc = s.describe(return_string=True) - test = ( - "Sensor(id=REGEX)\n" - + " • parent: None \n" - + " • position: [0. 0. 0.] m\n" - + " • orientation: [0. 0. 0.] deg\n" - + " • handedness: left \n" - + " • pixel: 75 (3x5x5) " - ) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - assert desc == test + match_string_up_to_id(test, x.describe(return_string=True)) + - # describe tringularmesh +def test_describe_with_exclude_None(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') + x = magpy.Sensor() + test = [ + "Sensor(id=1687262758416)", + " • parent: None", + " • position: [0. 0. 0.] m", + " • orientation: [0. 0. 0.] deg", + " • handedness: right", + " • pixel: None", + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True)," + " y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," + " color=None, description=Description(show=None, text=None), label=None," + " legend=Legend(show=None), model3d=Model3d(data=[], showdefault=True), opacity=None," + " path=Path(frames=None, line=Line(color=None, style=None, width=None)," + " marker=Marker(color=None, size=None, symbol=None), numbering=None, show=None)," + " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, sizemode=None)", + ] + match_string_up_to_id(test, x.describe(exclude=None, return_string=True)) + + +def test_describe_with_many_pixels(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') + x = magpy.Sensor(pixel=[[[(1, 2, 3)] * 5] * 5] * 3, handedness="left") + test = [ + "Sensor(id=1687262996944)", + " • parent: None", + " • position: [0. 0. 0.] m", + " • orientation: [0. 0. 0.] deg", + " • handedness: left", + " • pixel: 75 (3x5x5)", + ] + match_string_up_to_id(test, x.describe(return_string=True)) + + +def test_describe_with_triangularmesh(): + """testing describe method""" + # print("test = [\n " + '",\n '.join(f'"{s}' for s in desc.split("\n")) + '",\n]') points = [ (-1, -1, 0), (-1, 1, 0), @@ -515,34 +523,32 @@ def test_describe(): (1, 1, 0), (0, 0, 2), ] - s = magpy.magnet.TriangularMesh.from_ConvexHull( + x = magpy.magnet.TriangularMesh.from_ConvexHull( polarization=(0, 0, 1), points=points, check_selfintersecting="skip", ) - desc = s.describe(return_string=True) - test = ( - "TriangularMesh(id=REGEX)\n" - " • parent: None \n" - " • position: [0. 0. 0.] m\n" - " • orientation: [0. 0. 0.] deg\n" - " • magnetization: [ 0. 0. 795774.71545948] A/m\n" - " • polarization: [0. 0. 1.] T\n" - " • barycenter: [0. 0. 0.46065534] \n" - " • faces: shape(6, 3) \n" - " • mesh: shape(6, 3, 3) \n" - " • status_disconnected: False \n" - " • status_disconnected_data: 1 part \n" - " • status_open: False \n" - " • status_open_data: [] \n" - " • status_reoriented: True \n" - " • status_selfintersecting: None \n" - " • status_selfintersecting_data: None \n" - " • vertices: shape(5, 3) " - ) - desc = re.sub("id=*[0-9]*[0-9]", "id=REGEX", desc) - # to create test: print('\\n"\n'.join(f'"{s}' for s in desc.split("\n")) + '"') - assert desc == test + test = [ + "TriangularMesh(id=1687257413648)", + " • parent: None", + " • position: [0. 0. 0.] m", + " • orientation: [0. 0. 0.] deg", + " • magnetization: [ 0. 0. 795774.71545948] A/m", + " • polarization: [0. 0. 1.] T", + " • barycenter: [0. 0. 0.46065534]", + " • faces: shape(6, 3)", + " • mesh: shape(6, 3, 3)", + " • status_disconnected: False", + " • status_disconnected_data: 1 part", + " • status_open: False", + " • status_open_data: []", + " • status_reoriented: True", + " • status_selfintersecting: None", + " • status_selfintersecting_data: None", + " • vertices: shape(5, 3)", + ] + + match_string_up_to_id(test, x.describe(return_string=True)) def test_unset_describe(): From 536ebfc906dac067b7a59e55d6fb60e1f1197359 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 8 Jan 2024 17:21:13 +0100 Subject: [PATCH 174/240] fix test --- tests/test_obj_BaseGeo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 741a197e6..56975d0a9 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -439,7 +439,7 @@ def test_describe_with_label(): match_string_up_to_id(test, x.describe(return_string=True)) # describe html string - test_html = ("
" + test + "
").split("\n") + test_html = ("
" + "\n".join(test) + "
").split("\n") match_string_up_to_id(test_html, x._repr_html_().replace("
", "\n")) From 706fd358bf8797d6dc56157a3a7ba1ee4cce639a Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 9 Jan 2024 15:18:26 +0100 Subject: [PATCH 175/240] remove news --- docs/_pages/reso_news.md | 19 ------------------- docs/index.md | 1 - 2 files changed, 20 deletions(-) delete mode 100644 docs/_pages/reso_news.md diff --git a/docs/_pages/reso_news.md b/docs/_pages/reso_news.md deleted file mode 100644 index f614de947..000000000 --- a/docs/_pages/reso_news.md +++ /dev/null @@ -1,19 +0,0 @@ -(news)= - -# News - -## Release of version 4.4.0 -**03.09.2023** - -- Version 4.4 is now released. Most notable changes: - - dynamic multi-plots with `show_context` - - the `style_legend_show` option to disable the automatic legend without having to refer to own canvas - - the styling option for current lines and magnetization arrows. -- Bunch of bugfixes, other graphic improvements - -## New Documentation -**18.08.2023** - -- Happy to announce our new, more attractive, more user-friendly and more complete documentation ! Give us Feedback what you think :). There are still a lot of things to do (specifically in the Examples section). Documentation updates coming regularly now. - -- It is set up for Magpylib version 4.4 - so some documented features might not be working in 4.3 - release is imminent ! If you cannot wait install from [main branch](https://github.com/magpylib/magpylib). diff --git a/docs/index.md b/docs/index.md index 49772f326..4a3d7631b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -103,7 +103,6 @@ In Magpylib, **sources** (magnets, currents, ...) and **observers** (sensors, po :maxdepth: 2 :hidden: -_pages/reso_news.md _pages/reso_get_started.md _pages/docu/docu_index.md _pages/gallery/gallery_index.md From 6916a2472b1a524f3b61e4f12fc7ac113e7a9722 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 9 Jan 2024 16:35:58 +0100 Subject: [PATCH 176/240] update docs --- .../_pages/gallery/gallery_shapes_triangle.md | 70 +++++-------------- .../gallery/gallery_vis_pv_streamlines.md | 23 +++--- docs/conf.py | 2 +- docs/index.md | 1 - pyproject.toml | 3 +- 5 files changed, 33 insertions(+), 66 deletions(-) diff --git a/docs/_pages/gallery/gallery_shapes_triangle.md b/docs/_pages/gallery/gallery_shapes_triangle.md index 34c22c811..3641e1d53 100644 --- a/docs/_pages/gallery/gallery_shapes_triangle.md +++ b/docs/_pages/gallery/gallery_shapes_triangle.md @@ -4,9 +4,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.16.0 + jupytext_version: 1.15.2 kernelspec: - display_name: Python 3 + display_name: Python 3 (ipykernel) language: python name: python3 orphan: true @@ -30,12 +30,11 @@ It is very common to approximate the surface of bodies by triangular meshes, whi In this example `Triangle` is used to create a magnet with cuboctahedral shape. Notice that triangle orientation is displayed by default for convenience. ```{code-cell} ipython3 -import numpy as np - import magpylib as magpy +import numpy as np # Create collection of triangles -triangles_mm = [ +triangles_cm = [ ([0, 1, -1], [-1, 1, 0], [1, 1, 0]), ([0, 1, 1], [1, 1, 0], [-1, 1, 0]), ([0, 1, 1], [-1, 0, 1], [0, -1, 1]), @@ -57,7 +56,7 @@ triangles_mm = [ ([0, -1, 1], [-1, 0, 1], [-1, -1, 0]), ([0, -1, 1], [1, -1, 0], [1, 0, 1]), ] -triangles = np.array(triangles) / 100 +triangles = np.array(triangles_cm) / 100 # cm -> m cuboc = magpy.Collection() for t in triangles: cuboc.add( @@ -113,52 +112,22 @@ Automatic face reorientation of `TriangularMesh` may fail when the mesh is open. In this example we revisit the cubeoctahedron, but generate it through the `TriangularMesh` class. ```{code-cell} ipython3 -import numpy as np - import magpylib as magpy +import numpy as np -# Create cubeoctahedron magnet -vertices = ( - np.array( - [ - (0, 1, -1), - (-1, 1, 0), - (1, 1, 0), - (0, 1, 1), - (-1, 0, 1), - (0, -1, 1), - (1, 0, 1), - (1, 0, -1), - (0, -1, -1), - (-1, 0, -1), - (-1, -1, 0), - (1, -1, 0), - ] - ) - / 100 -) +# Create cubeoctahedron magnet (vertices and faces are transposed here for more compact display) +vertices_cm = [ + [0, -1, 1, 0, -1, 0, 1, 1, 0, -1, -1, 1], + [1, 1, 1, 1, 0, -1, 0, 0, -1, 0, -1, -1], + [-1, 0, 0, 1, 1, 1, 1, -1, -1, -1, 0, 0], +] +vertices = np.array(vertices_cm).T / 100 # cm -> m faces = [ - (0, 1, 2), - (3, 2, 1), - (3, 4, 5), - (3, 5, 6), - (0, 7, 8), - (0, 8, 9), - (5, 10, 11), - (8, 11, 10), - (1, 9, 4), - (10, 4, 9), - (2, 6, 7), - (11, 7, 6), - (3, 1, 4), - (3, 6, 2), - (0, 9, 1), - (0, 2, 7), - (8, 10, 9), - (8, 7, 11), - (5, 4, 10), - (5, 11, 6), + [0, 3, 3, 3, 0, 0, 5, 8, 1, 10, 2, 11, 3, 3, 0, 0, 8, 8, 5, 5], + [1, 2, 4, 5, 7, 8, 10, 11, 9, 4, 6, 7, 1, 6, 9, 2, 10, 7, 4, 11], + [2, 1, 5, 6, 8, 9, 11, 10, 4, 9, 7, 6, 4, 2, 1, 7, 9, 11, 10, 6], ] +faces = np.array(faces).T cuboc = magpy.magnet.TriangularMesh( polarization=(0.1, 0.2, 0.3), vertices=vertices, faces=faces ) @@ -186,9 +155,8 @@ The `TriangularMesh` class is extremely powerful as it enables almost arbitrary In some cases it may be desirable to generate a `TriangularMesh` object from an open mesh (see Prism example above). In this case one has to be extremely careful because one cannot rely on the checks. Not to generate warnings or error messages, these checks can be disabled with `"skip"` or their outcome can be ignored with `"ignore"`. The `show` function can be used to view open edges and disconnected parts. In the following example we generate such an open mesh directly from `Triangle` objects. ```{code-cell} ipython3 -import numpy as np - import magpylib as magpy +import numpy as np # Create top and bottom faces of a prism magnet top = magpy.misc.Triangle( @@ -208,7 +176,7 @@ prism = magpy.magnet.TriangularMesh.from_triangles( check_disconnected="ignore", # check but ignore disconnected mesh reorient_faces="ignore", # check but ignore non-orientable mesh ) -prism.style.label = ("Open Prism",) +prism.style.label = "Open Prism" prism.style.magnetization.mode = "arrow" print("mesh status open:", prism.status_open) diff --git a/docs/_pages/gallery/gallery_vis_pv_streamlines.md b/docs/_pages/gallery/gallery_vis_pv_streamlines.md index 32cdf94c0..1b772b2ea 100644 --- a/docs/_pages/gallery/gallery_vis_pv_streamlines.md +++ b/docs/_pages/gallery/gallery_vis_pv_streamlines.md @@ -19,9 +19,8 @@ orphan: true Pyvista offers field-line computation and visualization in 3D. In addition to the field computation, Magpylib offers magnet visualization that seamlessly integrates into a Pyvista plotting scene. ```{code-cell} ipython3 -import pyvista as pv - import magpylib as magpy +import pyvista as pv # Create a magnet with Magpylib magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.010, 0.004)) @@ -34,11 +33,17 @@ grid = pv.ImageData( ) # Compute B-field and add as data to grid -grid["B"] = magnet.getB(grid.points)*1000 # T -> mT +grid["B"] = magnet.getB(grid.points) * 1000 # T -> mT # Compute the field lines -seed = pv.Disc(inner=0.001, outer=0.003, r_res=1, c_res=6) -strl = grid.streamlines_from_source(seed, vectors="B") +seed = pv.Disc(inner=0.001, outer=0.003, r_res=1, c_res=9) +strl = grid.streamlines_from_source( + seed, + vectors="B", + max_step_length=0.1, + max_time=.02, + integration_direction="both", +) # Create a Pyvista plotting scene pl = pv.Plotter() @@ -63,10 +68,6 @@ pl.add_mesh( ) # Prepare and show scene -pl.camera.position = (0.03, 0.03, 0.02) +pl.camera.position = (0.03, 0.03, 0.03) pl.show() -``` - -```{code-cell} ipython3 - -``` +``` \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index a0bc4a937..6c6388a0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,7 @@ def setup(app): # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = "5.3.0" +needs_sphinx = "6.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom diff --git a/docs/index.md b/docs/index.md index 4a3d7631b..186cdc57a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,7 +101,6 @@ In Magpylib, **sources** (magnets, currents, ...) and **observers** (sensors, po ```{toctree} :maxdepth: 2 -:hidden: _pages/reso_get_started.md _pages/docu/docu_index.md diff --git a/pyproject.toml b/pyproject.toml index 58ba70e18..10d5d78c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ code_style = [ "pre-commit", ] docs = [ - "sphinx==5.3", + "sphinx==6.2", "sphinx-design", "sphinx-thebe", "sphinx-favicon", @@ -51,7 +51,6 @@ docs = [ "pandas", "numpy-stl", "pyvista", - "panel", ] test = [ "tox>=4.11", From 39ce83ca7b590d4821998d54ac415d05903a870e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 10 Jan 2024 23:13:32 +0100 Subject: [PATCH 177/240] fix codeql --- magpylib/_src/fields/field_BH_cylinder.py | 4 +--- tests/test_obj_BaseGeo.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 36b772c73..8e44c8414 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -373,9 +373,7 @@ def magnet_cylinder_field( mask_pol_ax = pol_z != 0 # special case: pol = 0 - mask_pol_not_null = mask_pol_not_null = ~( - (pol_x == 0) * (pol_y == 0) * (pol_z == 0) - ) + mask_pol_not_null = ~((pol_x == 0) * (pol_y == 0) * (pol_z == 0)) # general case mask_gen = mask_pol_not_null & mask_not_on_edge diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 56975d0a9..0a771bb28 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -487,13 +487,15 @@ def test_describe_with_exclude_None(): " • orientation: [0. 0. 0.] deg", " • handedness: right", " • pixel: None", - " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True)," - " y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," - " color=None, description=Description(show=None, text=None), label=None," - " legend=Legend(show=None), model3d=Model3d(data=[], showdefault=True), opacity=None," - " path=Path(frames=None, line=Line(color=None, style=None, width=None)," - " marker=Marker(color=None, size=None, symbol=None), numbering=None, show=None)," - " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, sizemode=None)", + ( + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True)," + " y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," + " color=None, description=Description(show=None, text=None), label=None," + " legend=Legend(show=None), model3d=Model3d(data=[], showdefault=True), opacity=None," + " path=Path(frames=None, line=Line(color=None, style=None, width=None)," + " marker=Marker(color=None, size=None, symbol=None), numbering=None, show=None)," + " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, sizemode=None)" + ), ] match_string_up_to_id(test, x.describe(exclude=None, return_string=True)) From 992a9d76397e86eb2ef0f7af0523193bbf094004 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Wed, 10 Jan 2024 23:34:25 +0100 Subject: [PATCH 178/240] pylint --- tests/test_obj_BaseGeo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 0a771bb28..dee6a4f61 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -494,7 +494,8 @@ def test_describe_with_exclude_None(): " legend=Legend(show=None), model3d=Model3d(data=[], showdefault=True), opacity=None," " path=Path(frames=None, line=Line(color=None, style=None, width=None)," " marker=Marker(color=None, size=None, symbol=None), numbering=None, show=None)," - " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, sizemode=None)" + " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, " + " sizemode=None)" ), ] match_string_up_to_id(test, x.describe(exclude=None, return_string=True)) From b0aa226259d580161fdcea600293dc7c68efcb38 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 11 Jan 2024 00:17:53 +0100 Subject: [PATCH 179/240] fix test --- tests/test_obj_BaseGeo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index dee6a4f61..b990cc40e 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -494,7 +494,7 @@ def test_describe_with_exclude_None(): " legend=Legend(show=None), model3d=Model3d(data=[], showdefault=True), opacity=None," " path=Path(frames=None, line=Line(color=None, style=None, width=None)," " marker=Marker(color=None, size=None, symbol=None), numbering=None, show=None)," - " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None, " + " pixel=Pixel(color=None, size=1, sizemode=None, symbol=None), size=None," " sizemode=None)" ), ] From 02e4cbd96fbfb1c20b8b09256a3690241e89a1dc Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 15 Jan 2024 08:58:42 +0100 Subject: [PATCH 180/240] upgrade sphinx --- docs/conf.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6c6388a0b..fee61e91e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,7 @@ def setup(app): # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = "6.2" +needs_sphinx = "7.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom diff --git a/pyproject.toml b/pyproject.toml index 10d5d78c6..ce3116e77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ code_style = [ "pre-commit", ] docs = [ - "sphinx==6.2", + "sphinx==7.2", "sphinx-design", "sphinx-thebe", "sphinx-favicon", From ebdc397ffea11dbfb8fa4aab20c19e5e4ecf73b4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 15 Jan 2024 09:09:41 +0100 Subject: [PATCH 181/240] add missing changelog dates --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28321df99..9c43c6237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,10 @@ All notable changes to magpylib are documented here. ## [5.0.0dev] -## [4.5.1] +## [4.5.1] - 2023-12-28 - Fixed a field computatio issue where H-field resulting from axial magnetization is computed incorrectly inside of Cylinders ([#703](https://github.com/magpylib/magpylib/issues/703)) -## [4.5.0] +## [4.5.0] - 2023-12-13 - Add optional handedness parameter for Sensors ([#687](https://github.com/magpylib/magpylib/pull/687)) - Renaming classes: `Line`→`Polyline`, `Loop`→`Circle`. Old names are still valid but will issue a `DeprecationWarning` and will eventually be removed in the next major version ([#690](https://github.com/magpylib/magpylib/pull/690)) - Rework CI/CD workflows ([#686](https://github.com/magpylib/magpylib/pull/686)) From 86cba1182ad7650f2e020d7173739b058c6bd1cb Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 16 Jan 2024 12:53:51 +0100 Subject: [PATCH 182/240] remove obsolete ipygany and kaleido --- docs/_pages/docu/docu_graphics.md | 24 ++++++++++----------- magpylib/_src/display/backend_pyvista.py | 27 ++++++------------------ pyproject.toml | 2 -- tests/test_display_pyvista.py | 24 --------------------- 4 files changed, 17 insertions(+), 60 deletions(-) diff --git a/docs/_pages/docu/docu_graphics.md b/docs/_pages/docu/docu_graphics.md index bfee9f97d..b6e7120dd 100644 --- a/docs/_pages/docu/docu_graphics.md +++ b/docs/_pages/docu/docu_graphics.md @@ -149,32 +149,30 @@ There is a high level of **feature parity**, however, not all graphic features a | marker symbol | ✔️ | ✔️ | ❌ | | marker numbering | ✔️ | ✔️ | ❌ | | zoom level | ✔️ | ✔️ | ❌[^2] | -| magnetization color | ✔️[^8] | ✔️ | ✔️[^3] | -| animation | ✔️ | ✔️ | ✔️[^6] | -| animation time | ✔️ | ✔️ | ✔️[^6] | -| animation fps | ✔️ | ✔️ | ✔️[^6] | +| magnetization color | ✔️[^7] | ✔️ | ✔️ | +| animation | ✔️ | ✔️ | ✔️[^5] | +| animation time | ✔️ | ✔️ | ✔️[^5] | +| animation fps | ✔️ | ✔️ | ✔️[^5] | | animation slider | ✔️[^1] | ✔️ | ❌ | -| subplots 2D | ✔️ | ✔️ | ✔️[^7] | +| subplots 2D | ✔️ | ✔️ | ✔️[^6] | | subplots 3D | ✔️ | ✔️ | ✔️ | | user canvas | ✔️ | ✔️ | ✔️ | -| user extra 3d model [^4] | ✔️ | ✔️ | ✔️ [^5] | +| user extra 3d model [^3] | ✔️ | ✔️ | ✔️ [^4] | [^1]: when returning animation object and exporting it as jshtml. [^2]: possible but not implemented at the moment. -[^3]: does not work with `ipygany` jupyter backend. As of `pyvista>=0.38` these are deprecated and replaced by the [trame](https://docs.pyvista.org/api/plotting/trame.html) backend. +[^3]: only `"scatter3d"`, and `"mesh3d"`. Gets "translated" to every other backend. -[^4]: only `"scatter3d"`, and `"mesh3d"`. Gets "translated" to every other backend. +[^4]: custom user defined trace constructors allowed, which are specific to the backend. -[^5]: custom user defined trace constructors allowed, which are specific to the backend. +[^5]: animation is only available through export as `gif` or `mp4` -[^6]: animation is only available through export as `gif` or `mp4` +[^6]: 2D plots are not supported for all jupyter_backends. As of pyvista>=0.38 these are deprecated and replaced by the [trame](https://docs.pyvista.org/api/plotting/trame.html) backend. -[^7]: 2D plots are not supported for all jupyter_backends. As of pyvista>=0.38 these are deprecated and replaced by the [trame](https://docs.pyvista.org/api/plotting/trame.html) backend. - -[^8]: Matplotlib does not support color gradient. Instead magnetization is shown through object slicing and coloring. +[^7]: Matplotlib does not support color gradient. Instead magnetization is shown through object slicing and coloring. The following example demonstrates the currently supported backends: diff --git a/magpylib/_src/display/backend_pyvista.py b/magpylib/_src/display/backend_pyvista.py index 652fa9bca..28a4433e0 100644 --- a/magpylib/_src/display/backend_pyvista.py +++ b/magpylib/_src/display/backend_pyvista.py @@ -55,8 +55,6 @@ "longdashdot": "-..", } -INCOMPATIBLE_JUPYTER_BACKENDS_2D = {"panel", "ipygany", "pythreejs"} - @lru_cache(maxsize=32) def colormap_from_colorscale(colorscale, name="plotly_to_mpl", N=256, gamma=1.0): @@ -108,11 +106,7 @@ def generic_trace_to_pyvista(trace, jupyter_backend=None): ) traces_pv.append(trace_pv) if colorscale is not None: - # ipygany does not support custom colorsequences - if jupyter_backend == "ipygany": - trace_pv["cmap"] = "PiYG" - else: - trace_pv["cmap"] = colormap_from_colorscale(colorscale) + trace_pv["cmap"] = colormap_from_colorscale(colorscale) elif "scatter" in trace["type"]: line = trace.get("line", {}) line_color = line.get("color", trace.get("line_color", None)) @@ -252,9 +246,6 @@ def display_pyvista( jupyter_backend = show_kwargs.pop("jupyter_backend", jupyter_backend) if jupyter_backend is None: jupyter_backend = pv.global_theme.jupyter_backend - jupyter_backend_2D_compatible = ( - jupyter_backend not in INCOMPATIBLE_JUPYTER_BACKENDS_2D - ) count_with_labels = {} charts_max_ind = 0 @@ -276,17 +267,11 @@ def draw_frame(frame_ind): getattr(canvas, f"add_{typ}")(**tr1) canvas.show_axes() else: - if jupyter_backend_2D_compatible: - if charts.get((row, col), None) is None: - charts_max_ind += 1 - charts[(row, col)] = pv.Chart2D() - canvas.add_chart(charts[(row, col)]) - getattr(charts[(row, col)], typ)(**tr1) - else: - warnings.warn( - f"The set `jupyter_backend={jupyter_backend}` is incompatible with " - "2D plots. Empty plots will be shown instead" - ) + if charts.get((row, col), None) is None: + charts_max_ind += 1 + charts[(row, col)] = pv.Chart2D() + canvas.add_chart(charts[(row, col)]) + getattr(charts[(row, col)], typ)(**tr1) for rowcol, count in count_with_labels.items(): if 0 < count <= legend_maxitems: row, col = rowcol diff --git a/pyproject.toml b/pyproject.toml index ce3116e77..435d9ec56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,9 +59,7 @@ test = [ "coverage", "pandas", "pyvista", - "ipygany", "imageio[tifffile]", - "kaleido", "jupyterlab", ] binder = [ diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 9390c1619..601e58d11 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -49,12 +49,6 @@ def test_animation(): src.show(canvas=pl, animation=True, backend="pyvista") -def test_ipygany_jupyter_backend(): - """ipygany backend does not support custom colorscales""" - src = magpy.magnet.Cuboid(polarization=(0, 0, 1000), dimension=(1, 1, 1)) - src.show(return_fig=True, backend="pyvista", jupyter_backend="ipygany") - - def test_extra_model3d(): """test extra model 3d""" trace_mesh3d = { @@ -115,21 +109,3 @@ def test_pyvista_animation(is_notebook_result, extension, filename): mp4_quality=1, return_fig=True, ) - - -def test_incompatible_jupyter_backend_for2d(): - """test_incompatible_pyvista_backend""" - src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) - sens = magpy.Sensor() - with pytest.warns( - UserWarning, - match=r"The set `jupyter_backend=ipygany` is incompatible with 2D plots.*", - ): - magpy.show( - src, - sens, - output="Bx", - return_fig=True, - backend="pyvista", - jupyter_backend="ipygany", - ) From b503caae4d43c12f1d8a1de08b8dc6a279f15b0b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 16 Jan 2024 13:24:24 +0100 Subject: [PATCH 183/240] fix tests --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 435d9ec56..c2645d298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ test = [ "coverage", "pandas", "pyvista", + "ipywidgets", # for plotly FigureWidget "imageio[tifffile]", "jupyterlab", ] From 44acf300ffdf062b6f84ff4c008df7a0ebcde7be Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 16 Jan 2024 13:48:45 +0100 Subject: [PATCH 184/240] pylint --- magpylib/_src/display/backend_pyvista.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/display/backend_pyvista.py b/magpylib/_src/display/backend_pyvista.py index 28a4433e0..ac4c6bbfd 100644 --- a/magpylib/_src/display/backend_pyvista.py +++ b/magpylib/_src/display/backend_pyvista.py @@ -3,7 +3,6 @@ # pylint: disable=too-many-statements import os import tempfile -import warnings from functools import lru_cache import numpy as np @@ -74,7 +73,7 @@ def colormap_from_colorscale(colorscale, name="plotly_to_mpl", N=256, gamma=1.0) return LinearSegmentedColormap(name, cdict, N, gamma) -def generic_trace_to_pyvista(trace, jupyter_backend=None): +def generic_trace_to_pyvista(trace): """Transform a generic trace into a pyvista trace""" traces_pv = [] leg_title = trace.get("legendgrouptitle_text", None) @@ -253,7 +252,7 @@ def draw_frame(frame_ind): nonlocal count_with_labels, charts_max_ind frame = frames[frame_ind] for tr0 in frame["data"]: - for tr1 in generic_trace_to_pyvista(tr0, jupyter_backend=jupyter_backend): + for tr1 in generic_trace_to_pyvista(tr0): row = tr1.pop("row", 1) col = tr1.pop("col", 1) typ = tr1.pop("type") From 7b0b95891504b61e9bba79e746cab83a2d3ea275 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 13:10:00 +0100 Subject: [PATCH 185/240] fix pyvista + tests --- magpylib/_src/display/backend_pyvista.py | 11 ++++- tests/test_display_pyvista.py | 59 +++++++++++++++++------- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/magpylib/_src/display/backend_pyvista.py b/magpylib/_src/display/backend_pyvista.py index ac4c6bbfd..207541c90 100644 --- a/magpylib/_src/display/backend_pyvista.py +++ b/magpylib/_src/display/backend_pyvista.py @@ -318,8 +318,15 @@ def run_animation(filename, embed=True): animation_output = data["input_kwargs"].get("animation_output", None) animation_output = "gif" if animation_output is None else animation_output if animation_output in ("gif", "mp4"): - with tempfile.NamedTemporaryFile(suffix=f".{animation_output}") as temp: - run_animation(temp.name, embed=True) + try: + temp = os.path.join(tempfile.gettempdir(), os.urandom(24).hex()) + temp += f".{animation_output}" + run_animation(temp, embed=True) + finally: + try: + os.unlink(temp) + except FileNotFoundError: # pragma: no cover + pass else: run_animation(animation_output, embed=True) diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 601e58d11..4caef8196 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -1,3 +1,4 @@ +import os import tempfile from unittest.mock import patch @@ -41,14 +42,6 @@ def test_Cuboid_display(): assert isinstance(fig, pv.Plotter) -def test_animation(): - "animation not supported, should warn and display static" - pl = pv.Plotter() - src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) - with pytest.warns(UserWarning): - src.show(canvas=pl, animation=True, backend="pyvista") - - def test_extra_model3d(): """test extra model 3d""" trace_mesh3d = { @@ -70,12 +63,7 @@ def test_extra_model3d(): magpy.show(coll, return_fig=True, backend="pyvista") -@pytest.mark.parametrize("is_notebook_result", (True, False)) -@pytest.mark.parametrize("extension", ("mp4", "gif")) -@pytest.mark.parametrize("filename", (True, False)) -@pytest.mark.skipif(ffmpeg_failed, reason="Requires imageio-ffmpeg") -@pytest.mark.skipif(not HAS_IMAGEIO, reason="Requires imageio") -def test_pyvista_animation(is_notebook_result, extension, filename): +def test_subplots(): """Test pyvista animation""" # define sensor and source magpy.defaults.reset() @@ -95,17 +83,54 @@ def test_pyvista_animation(is_notebook_result, extension, filename): cyl2 = cyl1.copy().move((0, 0, 5)) objs = cyl1, cyl2, sensor + magpy.show( + {"objects": objs, "col": 1, "output": ("Bx", "By", "Bz")}, + {"objects": objs, "col": 2}, + backend="pyvista", + sumup=True, + return_fig=True, + ) + + +def test_animation_warning(): + "animation not supported, should warn and display static" + pl = pv.Plotter() + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + with pytest.warns(UserWarning): + src.show(canvas=pl, animation=True, backend="pyvista") + + +@pytest.mark.parametrize("is_notebook_result", (True, False)) +@pytest.mark.parametrize("extension", ("mp4", "gif")) +@pytest.mark.parametrize("filename", (True, False)) +# @pytest.mark.skipif(not HAS_IMAGEIO, reason="Requires imageio") +def test_pyvista_animation(is_notebook_result, extension, filename): + """Test pyvista animation""" + # define sensor and source + if ffmpeg_failed and extension == "mp4": + pytest.skip("Extension mp4 skipped because ffmpeg failed to load") + sens = magpy.Sensor() + src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + src.move([[0, 0, 0], [0, 0, 1]], start=0) + objs = [src, sens] + with patch("magpylib._src.utility.is_notebook", return_value=is_notebook_result): with patch("webbrowser.open"): - with tempfile.NamedTemporaryFile(suffix=f".{extension}") as temp: - animation_output = temp.name if filename else extension + try: + temp = os.path.join(tempfile.gettempdir(), os.urandom(24).hex()) + temp += f".{extension}" + animation_output = temp if filename else extension magpy.show( {"objects": objs, "col": 1, "output": ("Bx", "By", "Bz")}, {"objects": objs, "col": 2}, backend="pyvista", animation=True, - sumup=True, animation_output=animation_output, mp4_quality=1, return_fig=True, ) + finally: + try: + os.unlink(temp) + except FileNotFoundError: + pass From bcb31d9b768f0eb9e4d12c4f7eb3c85e8046af8c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 13:24:29 +0100 Subject: [PATCH 186/240] fix pyvista animation test --- tests/test_display_pyvista.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 4caef8196..80b98cac8 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -18,7 +18,7 @@ # pylint: disable=no-member # pylint: disable=broad-exception-caught -ffmpeg_failed = False +FFMPEG_FAILED = False try: try: import imageio_ffmpeg @@ -31,7 +31,7 @@ raise err except Exception: # noqa: E722 # skip test if ffmpeg cannot be loaded - ffmpeg_failed = True + FFMPEG_FAILED = True def test_Cuboid_display(): @@ -103,11 +103,12 @@ def test_animation_warning(): @pytest.mark.parametrize("is_notebook_result", (True, False)) @pytest.mark.parametrize("extension", ("mp4", "gif")) @pytest.mark.parametrize("filename", (True, False)) -# @pytest.mark.skipif(not HAS_IMAGEIO, reason="Requires imageio") def test_pyvista_animation(is_notebook_result, extension, filename): """Test pyvista animation""" # define sensor and source - if ffmpeg_failed and extension == "mp4": + if not HAS_IMAGEIO and extension == "gif": + pytest.skip("Extension gif skipped because imageio failed to load") + if FFMPEG_FAILED and extension == "mp4": pytest.skip("Extension mp4 skipped because ffmpeg failed to load") sens = magpy.Sensor() src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) From 3df6d7096d865f0a4f2f1f8d1213df38392916e0 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 13:27:59 +0100 Subject: [PATCH 187/240] code ql --- magpylib/_src/display/backend_pyvista.py | 1 + tests/test_display_pyvista.py | 1 + 2 files changed, 2 insertions(+) diff --git a/magpylib/_src/display/backend_pyvista.py b/magpylib/_src/display/backend_pyvista.py index 207541c90..9aa4e0c64 100644 --- a/magpylib/_src/display/backend_pyvista.py +++ b/magpylib/_src/display/backend_pyvista.py @@ -326,6 +326,7 @@ def run_animation(filename, embed=True): try: os.unlink(temp) except FileNotFoundError: # pragma: no cover + # avoid exception if file is not found pass else: run_animation(animation_output, embed=True) diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 80b98cac8..957ac9f5b 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -134,4 +134,5 @@ def test_pyvista_animation(is_notebook_result, extension, filename): try: os.unlink(temp) except FileNotFoundError: + # avoid exception if file is not found pass From 07a7e708e13c768c13c30397d12a914c76f9b6d0 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 13:44:45 +0100 Subject: [PATCH 188/240] fix field_func input_checks tests --- tests/test_input_checks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 0fb104c68..71f57da38 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -516,8 +516,10 @@ def fff(field, observers): ) def test_input_objects_field_func_bad(func): """bad input: magpy.misc.CustomSource(field_func=f)""" - with pytest.raises(MagpylibBadUserInput): - magpy.misc.CustomSource(func) + with pytest.raises( + MagpylibBadUserInput, match=r"Input parameter `field_func` must .*." + ): + magpy.misc.CustomSource(field_func=func) def test_missing_input_triangular_mesh(): From db9a19fa05529e05a6df72777e6a10ec8afe4e2a Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 13:54:47 +0100 Subject: [PATCH 189/240] add input_check sensor handedness --- tests/test_input_checks.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 71f57da38..9333ce475 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -959,3 +959,14 @@ def test_input_getBH_field_bad(field): obs = np.array([[1, 2, 3]]) with pytest.raises(MagpylibBadUserInput): magpy.core.dipole_field(field=field, observers=obs, moment=moms) + + +def test_sensor_handedness(): + """Test if handedness input""" + magpy.Sensor(handedness="right") + magpy.Sensor(handedness="left") + with pytest.raises( + MagpylibBadUserInput, + match=r"Sensor `handedness` must be either `'right'` or `'left'`", + ): + magpy.Sensor(handedness="not_right_or_left") From c96db6c1e39a23eb9e6f1ba8f516c6dafba7bc54 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 14:25:24 +0100 Subject: [PATCH 190/240] BaseExcitation coverage --- .../_src/obj_classes/class_BaseExcitations.py | 25 +++++++++--------- tests/test_input_checks.py | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index d8af7d5fe..20ce7f91e 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -234,7 +234,7 @@ def magnetization(self, mag): ) self._polarization = self._magnetization * (4 * np.pi * 1e-7) if np.linalg.norm(self._magnetization) < 2000: - _deprecation_warn() + self._magnetization_low_warning() @property def polarization(self): @@ -254,6 +254,17 @@ def polarization(self, mag): ) self._magnetization = self._polarization / (4 * np.pi * 1e-7) + def _magnetization_low_warning(self): + warnings.warn( + ( + f"{self} received a very low magnetization. " + "In Magpylib v5 magnetization is given in units of A/m, " + "while polarization is given in units of T." + ), + MagpylibDeprecationWarning, + stacklevel=2, + ) + class BaseCurrent(BaseSource): """provides scalar current attribute""" @@ -279,15 +290,3 @@ def current(self, current): sig_type="`None` or a number (int, float)", allow_None=True, ) - - -def _deprecation_warn(): - warnings.warn( - ( - "You have entered a very low magnetization." - "In Magpylib v5 magnetization is given in units of A/m, " - "while polarization is given in units of T." - ), - MagpylibDeprecationWarning, - stacklevel=2, - ) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 9333ce475..eb135a537 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -4,8 +4,10 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibBadUserInput +from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.exceptions import MagpylibMissingInput + # pylint: disable=unnecessary-lambda-assignment ########################################################### @@ -970,3 +972,27 @@ def test_sensor_handedness(): match=r"Sensor `handedness` must be either `'right'` or `'left'`", ): magpy.Sensor(handedness="not_right_or_left") + + +def test_magnet_polarization_magnetization_input(): + # warning when magnetization is too low -> polarization confusion + mag = np.array([1, 2, 3]) * 1e6 + + with pytest.warns( + MagpylibDeprecationWarning, + match=r".* received a very low magnetization. .*", + ): + magpy.magnet.Cuboid(magnetization=[1, 2, 3]) + + # both polarization and magnetization at the same time + with pytest.raises( + ValueError, + match=r"The attributes magnetization and polarization are dependent. .*", + ): + magpy.magnet.Cuboid(polarization=[1, 2, 3], magnetization=mag) + + # setting magnetization afterwards + c = magpy.magnet.Cuboid() + c.magnetization = mag + np.testing.assert_allclose(mag, c.magnetization) + np.testing.assert_allclose(mag * (4 * np.pi * 1e-7), c.polarization) From 48afbdd5d33b4686b4e0a4b09a31801db2e3c8c8 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 14:29:43 +0100 Subject: [PATCH 191/240] fix different_pixel_shapes test --- tests/test_exceptions.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 820e19a1e..f5967001e 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -41,11 +41,9 @@ def getBH_level2_bad_input1(): ) -def getBH_level2_bad_input2(): +def getBH_different_pixel_shapes(): """different pixel shapes""" - mag = (1, 2, 3) - dim_cuboid = (1, 2, 3) - pm1 = magpy.magnet.Cuboid(mag, dim_cuboid) + pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) sens1 = magpy.Sensor() sens2 = magpy.Sensor(pixel=[(0, 0, 0), (0, 0, 1), (0, 0, 2)]) magpy.getB(pm1, [sens1, sens2]) @@ -278,7 +276,7 @@ def test_except_getBHv(self): def test_except_getBH_lev2(self): """getBH_level2 exception testing""" self.assertRaises(MagpylibBadUserInput, getBH_level2_bad_input1) - self.assertRaises(MagpylibBadUserInput, getBH_level2_bad_input2) + self.assertRaises(MagpylibBadUserInput, getBH_different_pixel_shapes) def test_except_bad_input_shape_basegeo(self): """BaseGeo bad input shapes""" From a85947c7ae08be610331d5a850753076a7793217 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 18:06:23 +0100 Subject: [PATCH 192/240] fix legend style and description handling + tests --- magpylib/_src/defaults/defaults_values.py | 2 +- magpylib/_src/display/traces_utility.py | 11 +++++++++-- magpylib/_src/style.py | 16 ++++++++++++++++ tests/test_display_matplotlib.py | 11 +++++++++++ tests/test_obj_BaseGeo.py | 2 +- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 9b8e6de34..8b7ddb2fb 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -48,7 +48,7 @@ "numbering": False, }, "description": {"show": True, "text": None}, - "legend": {"show": True}, + "legend": {"show": True, "text": None}, "opacity": 1, "model3d": {"showdefault": True, "data": []}, "color": None, diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index c60cac959..a77139ed8 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -18,8 +18,15 @@ def get_legend_label(obj, style=None, suffix=True): """provides legend entry based on name and suffix""" style = obj.style if style is None else style name = style.label if style.label else obj.__class__.__name__ - desc = getattr(obj, "_default_style_description", "") - suff = f" ({desc})" if style.description.show and desc and suffix else "" + legend_txt = style.legend.text + if legend_txt: + return legend_txt + suff = "" + if suffix and style.description.show: + desc = style.description.text + if not desc: + desc = getattr(obj, "_default_style_description", "") + suff = f" ({desc})" return f"{name}{suff}" diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py index fe4b782b4..fa5aa56c2 100644 --- a/magpylib/_src/style.py +++ b/magpylib/_src/style.py @@ -324,6 +324,9 @@ class Legend(MagicProperties): Parameters ---------- + text: str, default=None + Object description text. + show: bool, default=None If True, adds legend entry based on value. """ @@ -331,6 +334,19 @@ class Legend(MagicProperties): def __init__(self, show=None, **kwargs): super().__init__(show=show, **kwargs) + @property + def text(self): + """Legend text.""" + return self._text + + @text.setter + def text(self, val): + assert val is None or isinstance(val, str), ( + f"The `show` property of {type(self).__name__} must be a string,\n" + f"but received {repr(val)} instead." + ) + self._text = val + @property def show(self): """If True, adds legend entry based on value.""" diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index c0675c412..472eb3a28 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -605,3 +605,14 @@ def test_unset_objs(): *objs, return_fig=True, ) + + +def test_show_legend(): + """test legend (and multi shape pixel)""" + pixel = np.arange(27).reshape(3, 3, 3) * 1e-2 + s1 = magpy.Sensor(pixel=pixel, style_label="s1") + s2 = s1.copy().move((1, 0, 0)) + s3 = s2.copy().move((1, 0, 0)) + s2.style.legend = "full legend replace" + s3.style.description = "description replace only" + magpy.show(s1, s2, s3, return_fig=True) diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index 15d94ecce..c48038cc6 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -484,7 +484,7 @@ def test_describe(): + " • style: SensorStyle(arrows=ArrowCS(x=ArrowSingle(color=None, show=True), " + "y=ArrowSingle(color=None, show=True), z=ArrowSingle(color=None, show=True))," + " color=None, description=Description(show=None, text=None), label=None, " - + "legend=Legend(show=None), " + + "legend=Legend(show=None, text=None), " + "model3d=Model3d(data=[], showdefault=True), opacity=None, path=Path(frames=None," + " line=Line(color=None, style=None, width=None), marker=Marker(color=None," + " size=None, symbol=None), numbering=None, show=None), pixel=Pixel(color=None," From cd1efe4e87bf7a6bbd721dc1cfcc6482a7a8e015 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 18:46:07 +0100 Subject: [PATCH 193/240] pylint --- tests/test_input_checks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index eb135a537..1def8caf1 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -975,6 +975,7 @@ def test_sensor_handedness(): def test_magnet_polarization_magnetization_input(): + """test codependency and magnetization polarization inputs""" # warning when magnetization is too low -> polarization confusion mag = np.array([1, 2, 3]) * 1e6 From 40b55a1bbc0a27b0af453b91ac2a29009c598ae4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 19:28:25 +0100 Subject: [PATCH 194/240] fix pyvista animation tests on CI --- pyproject.toml | 2 +- tests/test_display_pyvista.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2645d298..87d3081ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ test = [ "pandas", "pyvista", "ipywidgets", # for plotly FigureWidget - "imageio[tifffile]", + "imageio[tifffile, ffmpeg]", "jupyterlab", ] binder = [ diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 957ac9f5b..6821fc363 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -1,4 +1,5 @@ import os +import sys import tempfile from unittest.mock import patch @@ -106,6 +107,8 @@ def test_animation_warning(): def test_pyvista_animation(is_notebook_result, extension, filename): """Test pyvista animation""" # define sensor and source + if sys.platform == "linux": + pv.start_xvfb() # needed for unix systems or it will test will crash with fatal error if not HAS_IMAGEIO and extension == "gif": pytest.skip("Extension gif skipped because imageio failed to load") if FFMPEG_FAILED and extension == "mp4": From 96a01e20e7d76834ff0e7553b03dd0f4b94a7703 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 19:51:24 +0100 Subject: [PATCH 195/240] empty commit From e56077e674e75f9ed55bfadd6d2592e79fb9196c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 20:06:32 +0100 Subject: [PATCH 196/240] offscreen pyvista animation test --- tests/test_display_pyvista.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_display_pyvista.py b/tests/test_display_pyvista.py index 6821fc363..cb6623510 100644 --- a/tests/test_display_pyvista.py +++ b/tests/test_display_pyvista.py @@ -107,6 +107,7 @@ def test_animation_warning(): def test_pyvista_animation(is_notebook_result, extension, filename): """Test pyvista animation""" # define sensor and source + pv.OFF_SCREEN = True if sys.platform == "linux": pv.start_xvfb() # needed for unix systems or it will test will crash with fatal error if not HAS_IMAGEIO and extension == "gif": From fc38380b00d83001e7ba6471452c1b11d988651e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 17 Jan 2024 21:10:06 +0100 Subject: [PATCH 197/240] avoid test syntax collision with pytest + doctests --- magpylib/_src/display/display.py | 4 ++-- magpylib/_src/utility.py | 2 +- tests/test_exceptions.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 389e7b741..1a6b37f72 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -15,7 +15,7 @@ from magpylib._src.input_checks import check_format_input_vector from magpylib._src.input_checks import check_input_animation from magpylib._src.input_checks import check_input_zoom -from magpylib._src.utility import test_path_format +from magpylib._src.utility import check_path_format disp_args = get_defaults_dict("display").keys() @@ -198,7 +198,7 @@ def _show( kwargs["subplot_specs"] = subplot_specs # test if every individual obj_path is good - test_path_format(obj_list_flat) + check_path_format(obj_list_flat) # input checks backend = check_format_input_backend(backend) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 66ce8b024..f4eef2514 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -169,7 +169,7 @@ def check_duplicates(obj_list: Sequence) -> list: return obj_list_new -def test_path_format(inp): +def check_path_format(inp): """check if each object path has same length of obj.pos and obj.rot Parameters diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index f5967001e..3c9c0ffb8 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -6,9 +6,9 @@ from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_observers +from magpylib._src.utility import check_path_format from magpylib._src.utility import format_obj_input from magpylib._src.utility import format_src_inputs -from magpylib._src.utility import test_path_format as tpf GETBH_KWARGS = {"sumup": False, "squeeze": True, "pixel_agg": None, "output": "ndarray"} @@ -182,12 +182,12 @@ def utility_format_obs_inputs(): check_format_input_observers([sens1, sens2, possis, "whatever"]) -def utility_test_path_format(): +def utility_check_path_format(): """bad path format input""" # pylint: disable=protected-access pm1 = magpy.magnet.Cuboid(polarization=(1, 2, 3), dimension=(1, 2, 3)) pm1._position = [(1, 2, 3), (1, 2, 3)] - tpf(pm1) + check_path_format(pm1) ############################################################################### @@ -252,7 +252,7 @@ class TestExceptions(unittest.TestCase): def test_except_utility(self): """utility""" - self.assertRaises(MagpylibBadUserInput, utility_test_path_format) + self.assertRaises(MagpylibBadUserInput, utility_check_path_format) self.assertRaises(MagpylibBadUserInput, utility_format_obj_input) self.assertRaises(MagpylibBadUserInput, utility_format_src_inputs) self.assertRaises(MagpylibBadUserInput, utility_format_obs_inputs) From 44c9815f2b061d71d4917bc0cfbac9d2de0542ba Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 18 Jan 2024 10:05:54 +0100 Subject: [PATCH 198/240] fix missing pyvista test pollution --- pyproject.toml | 4 +++- tests/test__missing_optional_modules.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87d3081ea..717c8fa71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,5 +77,7 @@ Changelog = "https://github.com/magpylib/magpylib/blob/master/CHANGELOG.md" [tool.pytest.ini_options] -addopts = "--doctest-modules" +# backend_pyvista.py file imports pyvista on top level which messes with the namespace when running +# doctests with the missing_pyvista test +addopts = ["--doctest-modules", "--ignore-glob=*backend_pyvista.py"] testpaths = ["magpylib", "tests"] diff --git a/tests/test__missing_optional_modules.py b/tests/test__missing_optional_modules.py index 0e1f7984e..3f96f1ae8 100644 --- a/tests/test__missing_optional_modules.py +++ b/tests/test__missing_optional_modules.py @@ -10,8 +10,8 @@ def test_show_with_missing_pyvista(): """Should raise if pyvista is not installed""" src = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with patch.dict(sys.modules, {"pyvista": None}): - # with pytest.raises(ModuleNotFoundError): - src.show(return_fig=True, backend="pyvista") + with pytest.raises(ModuleNotFoundError): + src.show(return_fig=True, backend="pyvista") def test_dataframe_output_missing_pandas(): From 13b0a3cc1d9d1c98233a711aa58a2b2c41491cb8 Mon Sep 17 00:00:00 2001 From: mortner Date: Thu, 18 Jan 2024 16:37:10 +0100 Subject: [PATCH 199/240] override parent order --- magpylib/_src/obj_classes/class_Collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 113b2514b..c47031b4a 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -788,8 +788,8 @@ def __init__( *args, position=(0, 0, 0), orientation=None, - style=None, override_parent=False, + style=None, **kwargs, ): BaseGeo.__init__( From eb956914b64a0545756e05643fc11f3ca39dad1f Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 19 Jan 2024 04:47:51 +0100 Subject: [PATCH 200/240] override_parent --- magpylib/_src/obj_classes/class_Collection.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index c47031b4a..f0a484f23 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -701,6 +701,21 @@ class Collection(BaseGeo, BaseCollection): children: sources, `Sensor` or `Collection` objects An ordered list of all children in the collection. + position: array_like, shape (3,) or (m,3), default=`(0,0,0)` + Object position(s) in the global coordinates in units of m. For m>1, the + `position` and `orientation` attributes together represent an object path. + + orientation: scipy `Rotation` object with length 1 or m, default=`None` + Object orientation(s) in the global coordinates. `None` corresponds to + a unit-rotation. For m>1, the `position` and `orientation` attributes + together represent an object path. + + override_parent: bool, default=False + If False thrown an error when an attempt is made to add an object that + has already a parent to a Collection. If True, allow adding the object + and override the objects parent attribute thus removing it from its + previous collection. + sensors: `Sensor` objects An ordered list of all sensor objects in the collection. @@ -710,14 +725,6 @@ class Collection(BaseGeo, BaseCollection): collections: `Collection` objects An ordered list of all collection objects in the collection. - position: array_like, shape (3,) or (m,3), default=`(0,0,0)` - Object position(s) in the global coordinates in units of m. For m>1, the - `position` and `orientation` attributes together represent an object path. - - orientation: scipy `Rotation` object with length 1 or m, default=`None` - Object orientation(s) in the global coordinates. `None` corresponds to - a unit-rotation. For m>1, the `position` and `orientation` attributes - together represent an object path. parent: `Collection` object or `None` The object is a child of it's parent collection. From 5037507105b9f1a8d600ab875c95d086313d1915 Mon Sep 17 00:00:00 2001 From: mortner Date: Fri, 19 Jan 2024 08:19:17 +0100 Subject: [PATCH 201/240] some docu fixes --- docs/_pages/docu/docu_magpylib_api.md | 200 +++++++++---------- docs/_pages/gallery/gallery_magnet_model.md | 17 ++ docs/_static/images/docu_field_comp_flow.png | Bin 101512 -> 250938 bytes 3 files changed, 111 insertions(+), 106 deletions(-) create mode 100644 docs/_pages/gallery/gallery_magnet_model.md diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index c5df5f4b8..a7dd66ba1 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -15,7 +15,7 @@ Magpylib requires no special input format. All scalar types (`int`, `float`, ... (docu-units)= ## Units -For historical reasons Magpylib is by default set up for the following units +For historical reasons Magpylib used non-SI units until Version 4. Starting with version 5 all inputs and outputs are SI-based. ::::{grid} 3 :::{grid-item} @@ -24,30 +24,33 @@ For historical reasons Magpylib is by default set up for the following units :::{grid-item} :columns: 10 -| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNIT (from v5)| UNIT (up to v4)| +| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNITS from v5| UNITS until v4| |:---:|:---:|:---:|:---:| -| magnetic polarization | `polarization` | **T** | - | -| magnetization | `magnetization` | **A/m** | mT | -| electric current | `current` | **A** | A | -| magnetic dipole moment | `moment` | **A·m²** | mT·mm³ | -| B-field | `getB()` | **mT** | mT | -| H-field | `getH()` | **A/m** | kA/m | -| length-inputs | `position`, `dimension`, ... | **m** | mm | -| angle-inputs | `angle`, `dimension`, ... | **deg** | deg | - +| Magnetic Polarization $\vec{J}$ | `polarization` | **T** | - | +| Magnetization $\vec{M}$ | `magnetization` | **A/m** | mT | +| Electric Current $i_0$ | `current` | **A** | A | +| Magnetic Dipole Moment $\vec{m}$ | `moment` | **A·m²** | mT·mm³ | +| B-field $\vec{B}$ | `getB()` | **T** | mT | +| H-field $\vec{H}$ | `getH()` | **A/m** | kA/m | +| Length-inputs | `position`, `dimension`, ... | **m** | mm | +| Angle-inputs | `angle`, `dimension`, ... | **°** | ° | ::: :::: ```{warning} -Up to version 4, Magpylib was unfortunately contributing to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib was refering to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. From version 5 onwards this has been addressed for all the magnet classes (see {ref}`docu-magnet-classes`) +Up to version 4, Magpylib was unfortunately contributing to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib was refering to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. From version 5 onwards this was fixed. ``` -The analytical solutions are scale invariant - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant. +```{note} +All input and output units in Magpylib (version 5 and higher) are SI-based, see table above. However, for advanced use one should be aware that the analytical solutions are scale invariant - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant, but it is critical to abide by the same length unit across one computation. -In addition, `getB` returns the same unit as given by the `polarization` input. When the polarization is given in T, then `getH` returns A/m which is simply related by factor of $\frac{1}{µ_0}=\frac{10^7}{4\pi}$. +In addition, `getB` returns the same unit as given by the `polarization` input. With polarization input in mT, getB will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $µ_0=4\pi\cdot 10^{-7}$. +``` -In {ref}`phys-remanence` the connection between the `magnetization` input and the remanence field of a magnet is explained. +```{note} +The connection between the magnetic polarization J, the magnetization M and the material parameters of a permanent magnet are shown in {ref}`gallery-tutorial-magnetmodel`. +``` @@ -68,7 +71,7 @@ In Magpylib's object oriented interface magnetic field **sources** (generate the The following basic properties are shared by all Magpylib classes: -* The **position** and **orientation** attributes describe the object placement in the global coordinate system. By default `position=(0,0,0)` and `orientation=None` (=unit rotation). +* The **position** and **orientation** attributes describe the object placement in the global coordinate system. * The **move()** and **rotate()** methods enable relative object positioning. @@ -92,16 +95,20 @@ See {ref}`docu-field-computation` for more information. * The **parent** attribute references a [Collection](docu-collection) that the object is part of. -* The **copy()** method can be used to create a clone of any object where selected properties, given by kwargs, are modified. +* The **copy()** method creates a clone of any object where selected properties, given by kwargs, are modified. * The **describe()** method provides a brief description of the object and returns the unique object id. + +--------------------------------------------- + + ## Local and global coordinates ::::{grid} 2 :::{grid-item} :columns: 9 -Magpylib objects span a local reference frame, and all object properties are defined within this frame, for example the vertices of a `Tetrahedron` magnet (see below). The position and orientation attributes describe how the local frame lies within the global coordinates. The two frames coincide by default, when `position=(0,0,0)` and `orientation=None` (=unit rotation). +Magpylib objects span a local reference frame, and all object properties are defined within this frame, for example the vertices of a `Tetrahedron` magnet. The position and orientation attributes describe how the local frame lies within the global coordinates. The two frames coincide by default, when `position=(0,0,0)` and `orientation=None` (=unit rotation). The `position` and `orientation` attributes are described in detail in {ref}`docu-position`. ::: :::{grid-item} :columns: 3 @@ -111,21 +118,23 @@ Magpylib objects span a local reference frame, and all object properties are def --------------------------------------------- + + (docu-magnet-classes)= ## Magnet classes -All magnets are sources. They have the **polarization** attribute ($J$) which is of the format $(p_x, p_y, p_z)$ and denotes a homogeneous magnetic polarization vector in the local object coordinates in units of **T**. The magnetization $M$ can also be set via the **magnetization** attribute of the format $(m_x, m_y, m_z)$. This two parameters are codependent and Magpylib makes sures the parameter stay in sync via the $J=M\cdot\mu_0$ relation. Information on how this is related to material properties from data sheets is found in the [Physics and Computation](phys-remanence) section. +All magnets are sources. They have the **polarization** attribute which is of the format $\vec{J}=(J_x, J_y, J_z)$ and denotes a homogeneous magnetic polarization vector in the local object coordinates in units of T. Alternatively, the magnetization vector can be set via the **magnetization** attribute of the format $\vec{M}=(M_x, M_y, M_z)$. These two parameters are codependent and Magpylib ensures that they stay in sync via the relatoin $\vec{J}=\mu_0\cdot\vec{M}$. Information on how this is related to material properties from data sheets is found in {ref}`gallery-tutorial-magnetmodel`. ### Cuboid ```python -magpy.magnet.Cuboid(position, orientation, dimension, polarization, style) +magpylib.magnet.Cuboid(position, orientation, dimension, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Cuboid` objects represent magnets with cuboid shape. The **dimension** attribute has the format $(a,b,c)$ and denotes the sides of the cuboid in arbitrary length units. The center of the cuboid lies in the origin of the local coordinates, and the sides are parallel to the coordinate axes. +`Cuboid` objects represent magnets with cuboid shape. The **dimension** attribute has the format $(a,b,c)$ and denotes the sides of the cuboid units of meter. The center of the cuboid lies in the origin of the local coordinates, and the sides are parallel to the coordinate axes. ::: :::{grid-item} :columns: 3 @@ -136,13 +145,13 @@ magpy.magnet.Cuboid(position, orientation, dimension, polarization, style) ### Cylinder ```python -magpy.magnet.Cylinder(position, orientation, dimension, polarization, style) +magpylib.magnet.Cylinder(position, orientation, dimension, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Cylinder` objects represent magnets with cylindrical shape. The **dimension** attribute has the format $(d,h)$ and denotes diameter and height of the cylinder in arbitrary length units. The center of the cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. +`Cylinder` objects represent magnets with cylindrical shape. The **dimension** attribute has the format $(d,h)$ and denotes diameter and height of the cylinder in units of meter. The center of the cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. ::: :::{grid-item} :columns: 3 @@ -153,13 +162,13 @@ magpy.magnet.Cylinder(position, orientation, dimension, polarization, style) ### CylinderSegment ```python -magpy.magnet.CylinderSegment(position, orientation, dimension, polarization, style) +magpylib.magnet.CylinderSegment(position, orientation, dimension, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`CylinderSegment`represents a magnet with the shape of a cylindrical ring section. The **dimension** attribute has the format $(r_1,r_2,h,\varphi_1,\varphi_2)$ and denotes inner radius, outer radius and height in arbitrary length units, and the two section angles $\varphi_1<\varphi_2$ in deg. The center of the full cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. +`CylinderSegment` objects represent magnets with the shape of a cylindrical ring section. The **dimension** attribute has the format $(r_1,r_2,h,\varphi_1,\varphi_2)$ and denotes inner radius, outer radius and height in units of meter, and the two section angles $\varphi_1<\varphi_2$ in °. The center of the full cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. ::: :::{grid-item} :columns: 3 @@ -174,13 +183,13 @@ magpy.magnet.CylinderSegment(position, orientation, dimension, polarization, sty ### Sphere ```python -magpy.magnet.Sphere(position, orientation, diameter, polarization, style) +magpylib.magnet.Sphere(position, orientation, diameter, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Sphere` represents a magnet of spherical shape. The **diameter** attribute is the sphere diameter $d$ in arbitrary length units. The center of the sphere lies in the origin of the local coordinates. +`Sphere` objects represent magnets of spherical shape. The **diameter** attribute is the sphere diameter $d$ in units of meter. The center of the sphere lies in the origin of the local coordinates. ::: :::{grid-item} :columns: 3 @@ -191,13 +200,13 @@ magpy.magnet.Sphere(position, orientation, diameter, polarization, style) ### Tetrahedron ```python -magpy.magnet.Tetrahedron(position, orientation, vertices, polarization, style) +magpylib.magnet.Tetrahedron(position, orientation, vertices, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Tetrahedron` represents a magnet of tetrahedral shape. The **vertices** attribute stores the four corner points $(P_1, P_2, P_3, P_4)$ in the local object coordinates in arbitrary length units. +`Tetrahedron` objects represent magnets of tetrahedral shape. The **vertices** attribute stores the four corner points $(\vec{P}_1, \vec{P}_2, \vec{P}_3, \vec{P}_4)$ in the local object coordinates in units of m. ::: :::{grid-item} :columns: 3 @@ -213,14 +222,20 @@ magpy.magnet.Tetrahedron(position, orientation, vertices, polarization, style) ### TriangularMesh ```python -magpy.magnet.TriangularMesh(position, orientation, vertices, faces, polarization, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) +magpylib.magnet.TriangularMesh(position, orientation, vertices, faces, polarization, magnetization, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`TriangularMesh` represents a magnet with surface given by a triangular mesh. The mesh is defined by the **vertices** attribute, an array of all unique corner points $(P_1, P_2, ...)$ in arbitrary length units, and the **faces** attribute, which is an array of index-triplets that define individual faces $(F_1, F_2, ...)$. The property **mesh** returns an array of all faces as point-triples $[(P_1^1, P_2^1, P_3^1), (P_1^2, P_2^2, P_3^2), ...]$. - +`TriangularMesh` objects represent magnets with surface given by a triangular mesh. The mesh is defined by the **vertices** attribute, an array of all unique corner points $(\vec{P}_1, \vec{P}_2, ...)$ in units of meter, and the **faces** attribute, which is an array of index-triplets that define individual faces $(\vec{F}_1, \vec{F}_2, ...)$. The property **mesh** returns an array of all faces as point-triples $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. +::: +:::{grid-item} +:columns: 3 +![](../../_static/images/docu_classes_init_trimesh.png) +::: +:::{grid-item} +:columns: 12 At initialization the mesh integrity is automatically checked, and all faces are reoriented to point outwards. These actions are controlled via the kwargs * **check_open** * **check_disconnected** @@ -244,21 +259,15 @@ The checks can also be performed after initialization using the methods * **check_selfintersecting()** * **reorient_faces()** -The following class methods enable easy mesh loading and creating. They all take the mandatory **polarization** argument, which overwrites possible polarization from other inputs, as well as the optional mesh check parameters (see above). +The following class methods enable easy mesh creating and mesh loading. -* **TriangularMesh.from_mesh()** requires the input **mesh**, which is an array in the mesh format $[(P_1^1, P_2^1, P_3^1), (P_1^2, P_2^2, P_3^2), ...]$. -* **TriangularMesh.from_ConvexHull()** requires the input **points**, which is an array of positions $(P_1, P_2, P_3, ...)$ from which the convex Hull is computed via the [Scipy ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) implementation. -* **TriangularMesh.from_triangles()** requires the input **triangles**, which is a list or a `Collection` of `Triangle` objects. -* **TriangularMesh.from_pyvista()** requires the input **polydata**, which is a [Pyvista PolyData](https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.PolyData.html) object. +* **TriangularMesh.from_mesh()** generates a `TriangularMesh` objects from the input **mesh**, which is an array in the mesh format $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. +* **TriangularMesh.from_ConvexHull()** generates a `TriangularMesh` object from the input **points**, which is an array of positions $(\vec{P}_1, \vec{P}_2, \vec{P}_3, ...)$ from which the convex Hull is computed via the [Scipy ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) implementation. +* **TriangularMesh.from_triangles()** generates a `TriangularMesh` object from the input **triangles**, which is a list or a `Collection` of `Triangle` objects. +* **TriangularMesh.from_pyvista()** generates a `TriangularMesh` object from the input **polydata**, which is a [Pyvista PolyData](https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.PolyData.html) object. The method **to_TriangleCollection()** transforms a `TriangularMesh` object into a `Collection` of `Triangle` objects. -::: -:::{grid-item} -:columns: 3 -![](../../_static/images/docu_classes_init_trimesh.png) -::: -:::{grid-item} -:columns: 12 + **Info:** While the checks may be disabled, the field computation guarantees correct results only if the mesh is closed, connected, not self-intersecting and all faces are oriented outwards. Examples of working with the `TriangularMesh` class are found in {ref}`gallery-shapes-triangle` and in {ref}`gallery-shapes-pyvista`. ::: :::: @@ -269,17 +278,17 @@ The method **to_TriangleCollection()** transf ## Current classes -All currents are sources. Current objects have the **current** attribute which is a scalar that denotes the electrical current in arbitrary units. +All currents are sources. Current objects have the **current** attribute which is a scalar that denotes the electrical current in units of ampere. ### Circle ```python -magpy.current.Circle(position, orientation, current, diameter, style) +magpylib.current.Circle(position, orientation, diameter, current, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Circle` represents a circular line current loop. The **diameter** attribute is the loop diameter $d$ in arbitrary length units. The loop lies in the xy-plane with it's center in the origin of the local coordinates. +`Circle` objects represent circular line current loops. The **diameter** attribute is the loop diameter $d$ in units of meter. The loop lies in the xy-plane with it's center in the origin of the local coordinates. ::: :::{grid-item} :columns: 3 @@ -289,13 +298,13 @@ magpy.current.Circle(position, orientation, current, diameter, style) ### Polyline ```python -magpy.current.Polyline(position, orientation, vertices, current, style) +magpylib.current.Polyline(position, orientation, vertices, current, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Polyline` represents a set of line current segments that flow from vertex to vertex. The **vertices** attribute is a vector of all vertices $(P_1, P_2, ...)$ given in the local coordinates in arbitrary length units. +`Polyline` objects represent line current segements where the electric current flows in straight lines from vertex to vertex. The **vertices** attribute is a vector of all vertices $(\vec{P}_1, \vec{P}_2, ...)$ given in the local coordinates in units of meter. ::: :::{grid-item} :columns: 3 @@ -312,13 +321,13 @@ There are classes listed hereon that function as sources, but they do not repres ### Dipole ```python -magpy.misc.Dipole(position, orientation, moment, style) +magpylib.misc.Dipole(position, orientation, moment, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Dipole` represents a magnetic dipole moment with the **moment** attribute that describes the magnetic dipole moment $m=(m_x,m_y,m_z)$ in arbitrary units which lies in the origin of the local coordinates. +`Dipole` objects represent magnetic dipole moments with the **moment** attribute that describes the magnetic dipole moment $\vec{m}=(m_x,m_y,m_z)$ in SI-units of Am², which lies in the origin of the local coordinates. ::: :::{grid-item} :columns: 3 @@ -326,20 +335,20 @@ magpy.misc.Dipole(position, orientation, moment, style) ::: :::{grid-item} :columns: 12 -**Info:** For homogeneous magnets the relation moment=polarization$\times$volume holds. +**Info:** The total dipole moment of a homogeneous magnet with body volume $V$ is given by $\vec{m}=\vec{J}\cdot V$. ::: :::: ### Triangle ```python -magpy.misc.Triangle(position, orientation, vertices, polarization, style) +magpylib.misc.Triangle(position, orientation, vertices, polarization, magnetization, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Triangle` represents a triangular surface with a homogeneous charge density given by the projection of the polarization vector onto the surface normal. The **polarization** attribute stores the polarization vector $(m_x,m_y,m_z)$ in arbitrary units. The **vertices** attribute is a set of the three triangle corners $(P_1, P_2, P_3)$ in arbitrary length units in the local coordinates. +`Triangle` objects represent triangular surfaces with homogeneous charge density given by the projection of the polarization or magnetization vector onto the surface normal. The attributes **polarization** and **magnetization** are treated similar as by the {ref}`docu-magnet-classes`. The **vertices** attribute is a set of the three triangle corners $(\vec{P}_1, \vec{P}_2, \vec{P}_3)$ in units of meter in the local coordinates. ::: :::{grid-item} :columns: 3 @@ -347,7 +356,7 @@ magpy.misc.Triangle(position, orientation, vertices, polarization, style) ::: :::{grid-item} :columns: 12 -**Info:** When multiple Triangles with similar polarization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. The B-field is only correct on the outside of the body. On the inside the polarization must be added to the field. This is demonstrated in the tutorial {ref}`gallery-shapes-triangle`. +**Info:** When multiple Triangles with similar magnetization/polarization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. In this case, the B-field is only correct on the outside of the body. On the inside the polarization must be added to the field. This is demonstrated in the tutorial {ref}`gallery-shapes-triangle`. ::: :::: @@ -355,13 +364,13 @@ magpy.misc.Triangle(position, orientation, vertices, polarization, style) ### CustomSource ```python -magpy.misc.CustomSource(field_func, position, orientation, style) +magpylib.misc.CustomSource(field_func, position, orientation, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`CustomSource` is used to create user defined sources with their own field functions. The argument **field_func** takes a function that is then automatically called for the field computation. This custom field function is treated like a [core function](docu-field-comp-core). It must have the positional arguments `field` with values `"B"` or `"H"`, and `observers` (must accept array with shape (n,3)) and return the B-field and the H-field with a similar shape. +The `CustomSource` class is used to create user defined sources provided with with custom field computation functions. The argument **field_func** takes a function that is then automatically called for the field computation. This custom field function is treated like a [core function](docu-field-comp-core). It must have the positional arguments `field` with values `"B"` or `"H"`, and `observers` (must accept array with shape (n,3)) and return the B-field and the H-field with a similar shape. ::: :::{grid-item} :columns: 3 @@ -379,13 +388,13 @@ magpy.misc.CustomSource(field_func, position, orientation, style) ## Sensor ```python -magpy.Sensor(position, pixel, orientation, style) +magpylib.Sensor(position, orientation, pixel, handedness, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -`Sensor` represents a 3D magnetic field sensor and can be used as Magpylib `observers` input. The **pixel** attribute is an array of positions $(P_1, P_2, ...)$ in arbitrary length units in the local sensor coordinates where the field is computed. By default `pixel=(0,0,0)` and the sensor simply returns the field at it's position. +`Sensor` objects represent observers of the magnetic field and can be used as Magpylib `observers` input for magnetic field computation. The **pixel** attribute is an array of positions $(\vec{P}_1, \vec{P}_2, ...)$ provided in units of meter in the local sensor coordinates. A sensor returns the magnetic field at these pixel positions. By default `pixel=(0,0,0)` and the sensor simply returns the field at it's position. The **handedness** attribute can be `"left"` or `"right"` (default) to set a left- or right-handed sensor coordinate system for the field computation. ::: :::{grid-item} :columns: 3 @@ -393,7 +402,7 @@ magpy.Sensor(position, pixel, orientation, style) ::: :::{grid-item} :columns: 12 -**Info:** With sensors it is possible to give observers their own position and orientation. The field is always computed in the reference frame of the sensor, which might itself be moving in the global coordinate system. This is demonstrated in {ref}`gallery-tutorial-field-computation-sensors`. +**Info:** Sensors can have their own position and orientation and enable easy relative positioning between sources and observers. The field is always computed in the reference frame of the sensor, which might itself be moving in the global coordinate system. Magpylib sensors can be understood as perfect magnetic field sensors with infinitesimally sensitive elements. An example how to use sensors is given in {ref}`gallery-tutorial-field-computation-sensors`. ::: :::: @@ -405,13 +414,13 @@ magpy.Sensor(position, pixel, orientation, style) ## Collection ```python -magpy.Collection(*children, position, orientation, style) +magpylib.Collection(*children, position, orientation, override_parent, style) ``` ::::{grid} 2 :::{grid-item} :columns: 9 -A `Collection` is a group of Magpylib objects that is used for common manipulation. All these objects are stored by reference in the **children** attribute. There are several options for accessing only specific children via the following properties +A `Collection` is a group of Magpylib objects that is used for common manipulation. All these objects are stored by reference in the **children** attribute. The collection becomes the **parent** of the object. An object can only have one parent. There are several options for accessing only specific children via the following properties * **sources**: return only sources * **observers**: return only observers @@ -458,9 +467,9 @@ A tutorial {ref}`gallery-tutorial-collection` is provided in the example gallery :::{grid-item} :columns: 7 -The explicit magnetic field expressions, implemented in Magpylib, are generally described in convenient coordinates of the sources. It is a common problem to transform the field into an application relevant lab coordinate system. While not technically difficult, such transformations are prone to error. +The explicit magnetic field expressions found in the literature, implemented in the [Magpylib core](docu-field-comp-core), are given in convenient source-coordinates. It is a common problem to transform the field into an application relevant observer coordinate system. While not technically difficult, such transformations are prone to error. -Here Magpylib helps out. All Magpylib objects lie in a global Cartesian coordinate system. Object position and orientation are defined by the attributes `position` and `orientation`, 😏. Objects can easily be moved around using the `move()` and `rotate()` methods. Eventually, the field is computed in the reference frame of the observers. +Here Magpylib helps out. All Magpylib sources and observers lie in a global Cartesian coordinate system. Object position and orientation are defined by the attributes `position` and `orientation`, 😏. Objects can easily be moved around using the `move()` and `rotate()` methods. Eventually, the field is computed in the reference frame of the observers (e.g. Sensor objects). Positions are given in units of meter, and the default unit for orientation is °. ::: :::{grid-item} :columns: 5 @@ -474,7 +483,7 @@ Position and orientation of all Magpylib objects are defined by the two attribut :::{grid-item-card} :shadow: none :columns: 5 -**position** - a point $(x,y,z)$ in the global coordinates, or a set of such points $(P_1, P_2, ...)$. By default objects are created with `position=(0,0,0)`. +**position** - a point $(x,y,z)$ in the global coordinates, or a set of such points $(\vec{P}_1, \vec{P}_2, ...)$. By default objects are created with `position=(0,0,0)`. ::: :::{grid-item-card} :shadow: none @@ -504,8 +513,8 @@ Magpylib offers two powerful methods for object manipulation: ::: :::: -- Scalar input is applied to the whole object path, starting with path index `start`. With the default `start="auto"` the index is set to `start=0` and the functionality is **moving objects around**. -- Vector input of length $n$ applies the $n$ individual operations to $n$ object path entries, starting with path index `start`. Padding applies when the input exceeds the existing path. With the default `start="auto"` the index is set to `start=len(object path)` and the functionality is **appending paths**. +- **Scalar input** is applied to the whole object path, starting with path index `start`. With the default `start="auto"` the index is set to `start=0` and the functionality is **moving objects around** (incl. their whole paths). +- **Vector input** of length $n$ applies the $n$ individual operations to $n$ object path entries, starting with path index `start`. Padding applies when the input exceeds the existing path length. With the default `start="auto"` the index is set to `start=len(object path)` and the functionality is **appending the input**. The practical application of this formalism is best demonstrated by the following program @@ -594,40 +603,16 @@ The tutorial {ref}`gallery-tutorial-paths` shows intuitive good practice example
-Magnetic field computation in Magpylib is done via two top-level functions - -::::{grid} -:gutter: 2 - -:::{grid-item} -:columns: 1 -::: - -:::{grid-item-card} -:shadow: none -:columns: 10 -**getB(**`sources`, `observers`, `squeeze=True`, `pixel_agg=None`, `output="ndarray"`**)** -::: - -:::{grid-item} -:columns: 1 -::: - -:::{grid-item} -:columns: 1 -::: +Magnetic field computation in Magpylib is done via two top-level functions **getB** and **getH**. -:::{grid-item-card} -:shadow: none -:columns: 10 -**getH(**`sources`, `observers`, `squeeze=True`, `pixel_agg=None`, `output="ndarray"`**)** -::: -:::{grid-item} -:columns: 1 -::: +```python +magpylib.getB(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +``` -:::: +```python +magpylib.getH(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +``` that compute the magnetic field generated by `sources` as seen by the `observers` in their local coordinates. `sources` can be any Magpylib source object (e.g. magnets) or a flat list thereof. `observers` can be an array of position vectors with shape `(n1,n2,n3,...,3)`, any Magpylib observer object (e.g. sensors), or a flat list thereof. The following code shows a minimal example for Magplyib field computation. @@ -642,7 +627,7 @@ that compute the magnetic field generated by `sources` as seen by the `observers :columns: 8 ```python import magpylib as magpy -# Note that all units are in SI +# All inputs and outputs in SI units # define source and observer objects loop = magpy.current.Circle(current=1, diameter=.001) @@ -657,7 +642,11 @@ print(B) ::: :::: -The physical unit returned by `getB` and `getH` corresponds to the source excitation input units. For example, when magnet `polarization` is given in mT, `getB` returns the B-field in units of mT and `getH` the H-field in units of A/m. This is described in detail in {ref}`docu-units`. +By default, `getB` returns the B-field in units of T, `getH` the H-field in units of A/m. + +```{note} +In reality, `getB` returns the same unit as given by the `polarization` input. For example, with polarization input in mT, getB will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $µ_0=4\pi\cdot 10^{-7}$. +``` The output of a field computation `magpy.getB(sources, observers)` is by default a Numpy array of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of observers, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position array input and `3` the three magnetic field components $(B_x, B_y, B_z)$. @@ -700,9 +689,8 @@ All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to c ```python import numpy as np import magpylib as magpy -# Note that all units are in SI -# This example also nicely shows the scale invariance -# by increasing the distance proportionally to the dimension. +# All inputs and outputs in SI units +# This example shows the scale invariance # compute the cuboid field for 3 input instances N = 3 # number of instances @@ -729,7 +717,7 @@ The functional interface is potentially faster than the object oriented one if u (docu-field-comp-core)= ## Core interface -At the heart of Magpylib lies a set of core functions that are our implementations of the explicit field expressions, see {ref}`docu-physics`, described in convenient local source coordinates. Direct access to these functions is given through the `magpylib.core` subpackage which includes, +At the heart of Magpylib lies a set of core functions that are our implementations of explicit field expressions found in the literature, see {ref}`docu-physics`. Direct access to these functions is given through the `magpylib.core` subpackage which includes, ::::{grid} 1 :gutter: 1 @@ -763,7 +751,7 @@ At the heart of Magpylib lies a set of core functions that are our implementatio ::: :::: -The input `field` is either `"B"` or `"H`. All other inputs must be Numpy ndarrays of shape (n,x). Their meaning is similar as above. Details can be found in the respective function docstrings. The following example demonstrates the core interface. +The input `field` is either `"B"` or `"H`. All other inputs must be Numpy ndarrays of shape (n,x). Details can be found in the respective function docstrings. The following example demonstrates the core interface. ::::{grid} :gutter: 5 @@ -777,7 +765,7 @@ The input `field` is either `"B"` or `"H`. All other inputs must be Numpy ndarra ```python import numpy as np import magpylib as magpy -# Note that all units are in SI +# All inputs and outputs in SI units # prepare input pol = np.array([(1,0,0)]*3) diff --git a/docs/_pages/gallery/gallery_magnet_model.md b/docs/_pages/gallery/gallery_magnet_model.md new file mode 100644 index 000000000..df2399adb --- /dev/null +++ b/docs/_pages/gallery/gallery_magnet_model.md @@ -0,0 +1,17 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.0 +kernelspec: + display_name: Python 3 + language: python + name: python3 +orphan: true +--- + +(gallery-tutorial-magnetmodel)= + +# Modelling a permanent Magnet \ No newline at end of file diff --git a/docs/_static/images/docu_field_comp_flow.png b/docs/_static/images/docu_field_comp_flow.png index 6ecc10d5b4763a34f066bd56d58e47be85929e97..f3b8fb6f33332240a9bc4446b1799bc8e02b88bf 100644 GIT binary patch literal 250938 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%0Dp?#Efz2m$@uX%3aH9#I5 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

QJEb7GjUW;Qh3c{G$1&8LEFi(HIEDr*en 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 z℘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%0D7D0Se?XY9x(w-XX+1^?bXPoZ zylI9D71rdlmENu!B!Y@XLF|8{C}IvFp1K?c4M8vvViI~EJY#psVr6UhTU%QX3|6bU z|ChA@J58|oJn1h>_peewV`HomFjm!Cn)|I5IwX9eZt4kqQG1reHo1A_e9W?FE4-55 zp6j*s?2@1G*vF`=@PixvXuXPC%Yv~d0!@cpP5fEx$Ee&iuE{(Qe6~{ZBss=lsVV=; zbU{e++O9k3_GEu&f8|6YwOaFhF!v8|vhf|yRZ)qqmts2$-9J3h{=aL=x+1wa0o4D(G zN&t7w=J*gEbC-zOxUSaGI- z`p%VW*EoxdpT@_mQ{XOJ#_T)4ml^u|7xtyy6y;Z_XC&m>ih@2Xq4+GtWqE=$|U|j ze24ujYK67EO%4-YNs>*lsr7HTux)ZuIe6wRZKe)iN{!?FpI$I{lc;`nKCP;v66f4_Z9H#l)cTs9 zgr6U5t}HP&y;%|P`ktnY?b3stJx1%-9fwbAYm4)#rfhrX=hbpiPEmENhPkwHVat7-EKOvic(=*!$rBK`haR^z=&`G z^k_*vcRgsl_t`}3n~fF6Ct+b9*3J}2unZQwb?6x&H`Pl07A-kzmpV1&s9s^kwlhMA zWY)X=hpAfXk3t9j`--1qGTmM$Uw*n+xVtp)K_HhLpW}oZXUnLZJ#R!xr-4bV53r?R ztoX;WE)iIoUp#eY1E5eFgBJS%W*ueuIxz>B$;#GX1xKq@<`e|mAP*eO z552ZuuY5r!)1Y7!uwvSJ4^0o%FO^S!_EVqYm>2h%Hyy?wqLn0BJ&~h7cmfy72RRnd z?~cvO&5N-7IX3)K%S)cmz+0lQGB7pD9-e8a19Szv=K@7{9u;n!P1d{!&R8Tz_ZC zMw{|!l~L{cV`pVs^h<*Vl*hd{C-bdQ+&4AtK5(FcJ3cKf4Htn3V54&J@yQRzf|tgG z5;!Yvof4#_rG??H9!>X8;6^CcjDQvmiDaxZFLmY#ynbM!(b3cb)+kRxR8*9T`Q_7T zBnlbA`3^Q{`tM)m(=Ov3LTFUcsc`8oNjyFH7A5uS(r7&2hVBk#Mr!KNzblg!O}(H# zAw!J*CpG*p0N}}fam`sEiOIt!&MhH_o5n9e~-dF8Do6%=eKf7 zxCIP9T%1c8*s0b1DdtL+qZ^kT>nX>mz%tu)_9IWfebf5#+@e|Eq}xs^x??J9uB55?&z(CNHzi~ZcYOPE)8yW%?B5+bHH6b% zw;gFHysI$Qn@10dDV0khmcmcT@lEe);yl+uE~UI%f_awPqZtgpf2=fas#b_ftsPm5 z`nha0rDod ze+l%uw~#Ehsx9HVJ%?vyK&jV@M{hSJiq8QC!=u8VDtkg$v`S}#PKP@exsEbG%NaoN z{$%J=;-!6XJXhgue0^Q~UtQ)v49AH)c9J!gS1Xq(wCS0JZu^BFr7phMz`yO{U-_9H zj##ngKNX^w{u{cw?6cCPy9i$wJ|67eS@b?(mIqXbAv`FBCuWuAr`bAu#k<EZ9c1BO%d zj(M6eu(2K9jWW3&T_ByGvSVGA!Ugt2UT=sI>BV8VBzfS{9!~+xHgb=N(d>dCWnSOt zz%9q;0u!PpwC9iCPiO95W$Y<)1g95u0r+`2zd~l+gG-LSFx!vYJRVTnppp~6nL73r^!~I6h|6NkTDBJl~@7ygI_Ub-m zvxdjbc6NSaerwXwU$m`gCu&Zq+CApM+q8C_l4HpM`zYJE!q)*u8Ei~rCGj>fK#r^M_ngQXR0<2SiO@&h4j{T?V83$ zw-;Z29~<--J@WmTv}UY@bA$o*8sibf({UWNA%eI$aynquH(mt26Tb)w|&UEM{Vg&GtRz zuAf!rH(e!3mtZ>8CL);DZ_D<7264;2?%Bz&$kuxaS z+1WSHH<%Y`xb*uTphpZ5#i90e;Y8l0VLsjSnd zf2)1oykEy%I$+_?tE|hdEHZgPvx&h*VmgJ7PKS@AHMW+HIWSN+e&Zi?pqUR@n0!S> z;cCNvc4_p4W*a}BJNmw&UYrlmeDA(RTXY9+#$AgeR|5Z7vFt@Mi*#EOdZzJTFTinG zl`x_F%xIZR!JE+)yd1KiL>{3Z`+0hL11xfoM0dcrbDAwOWGA6!;%_4$$(Il?`m$op z0iqTx1GoPoJw0xs?e^(0oEk`vy1Kd{EieAxrzIJJE8CRbTw(^Fs|dw1TEzUOqR-zR z`^71a2tE}~LBU;U!z3II`uff*Fj zb)==mNX5*b?Jt$6x}ljusq+1yp8U>EQLqguK^4KiBY)jb+AJ{mGD*G!UHTQ%8)A2z ztGx|wxFQSwEmhFKyVWwZqW{nKfND)cO)Uj;)X@{4i)KP#N*aKDW3w)Dz76cMHTp}5 z5R$TB%_6~ea{;z5>r*e&(1WUo21L+%44=~fyAfA^)AK61!2;PgIx1V?vX*Un6CO5* z`EaaDAkTjno-D}6h!B@`^{lN<`ubqLnIDwC(Fe7MZjolVH}XyE+t}_^INrQ@)1)(( z651QczBqi;53V2G`e~VtI!P<~gboi3N*sM~F}RHU{u-iV{ioB+{;C&PBucGkuWe+c z%U@=f{5w-X#T$q)E-LExCvZ@r%Gji(BYh|7PS6TWpX_j`e|1jacD|LDRrz3A_}9sQ zh1xZyo@J_6kp>4~B7FHo=59cX+$PIqos$8Ww`6(vXkX5(a-Q|`kh@n&iZEG!x_22E zn8xB^)IwA9I%BXvv4+&1@&s(wZnSEr3V}MWBOdlYJD{iQEBN|gMeAYJi9QY=4xAH_ z!;^f}n{}aF)pe{NhDQ*aH4#Jz8bl8zp8KDcpv94|4Da5&d2{G37d>bGW*DJ9x8)Mc}bT)-y@$qu_IA2 zi^m1^kkhOQ`u#GrYAL$xoS&gnR}}7Z>b?adV89zYSrO3F*^XVE<}`Qwh1p0Hr>q~7 z^=Me5Q*+aiXL|FubkJGz7X2ZCYY5OX(RW*a^tj))J*+NvM~p>vd3xpTvyLbcFzfo@ zHR>cBulOi+zohCC$#`!3NelQR#>26|0>5m2ay9()^$TCK^$2gL{@MWLi4>K6nq4Wq zL&aOAYDJM~9&}>2ffEYU;klQnK%v%(D@qs88}$Kzt!7}{YxjP#e}gUD6FbNIL_HG z|2;$k9fXlS&4rc*H_M$Uc~h=pyvq}mUxUAX$)7*(+1bIP82hfi-XGfB^UTaEFbfbJ zx#JyKr4Fwg$dd_a*xbAc{RCt;O6`Twc**TF9;tt++MTGbN3=vzQEyT zXtLpS^6pp961&NihLx%QbFfA!Kvl&o6dn|s=v2tYNyR^3%UqwrmlX}D-2dxW$~;Fc zz%-mY1}Hr1ew(%?Q6r8HHN-HYj{@iVjb-=Llm$ExQ}grN=vD@o7Z=gZau;&-+qZ8C zhg}`+r3E`J1ta5EZE-q!dM{{Zs9pOp;f2Hn$MD32B4|6xojbSc&W<$Ht&9x}yu2@Y zCH9mQ{!{4WNf|Y@C_IHPpm@O3M`y9{{x=?nfDe)m;i8lad*tW0Nf)Y7ok18EH4V*6 zllaGv&x3DLY227qfjbU|BO>_}^g$nZwb{<74%-_UC0TW&TMy=y6w5{k^C2N2&xOtK zB}2J<;kj*w8OO#zaV2imfDrAH+cM-pQiO8JCA_SGg$3_=B(owl`1jb@p;0RcJmrH^P`|iHf8(A;2|KMR8b-eB8>D>(<73YBiErHIfi_^q}r-Umwy%B##n&P2x zp19W*0ABP}YHF&mkGnhBoyJ%Wv><{ixr?j2GH)6S5xN!#m!#a|u!t`anum%+BJa)8 zQ(;_L4zssOU|AEtx}cRezO({C_4&o0PfxoTSy&FA9{pqCU-rpuwoG#ad@Tes2OV8wQEcNKswW<_#B$=>e2Qr z3wxc(1L;zP`-|*1KdZ`F6dBlw5&91qudok1-XQBP-S=>H)#=ogSg)1LoaT0y*I)8W zY$nMUic*gnxG&!F4ciuM;l9FpBp_z!P183dW4;p=gEk%f*+X{duMZ8a*Y>RlHZUXJHIX-01Om5h~<##b;1X%%9`H zL5n{aW=PGtzWvI8`uOY$<4K!2>ict>9xAea2$!|fyHUS|_Mqd3UP)58cAV>tlY68u zetQ%UaLLxf;>pjUA>~<|>qKt0-5j1y{Dy{xHosaa5zimO0it9jC5PhoJlx4JMEGGF zl1M+wop<2oRJ0Kie$hh6yV=>X%v6<7 zZ>QGR98G%5+2CVIQqMUE&)bWaFCT(4*8|C}q$-%PzwO02^3P9C?!c{-9}uxH6{*P- zaC1qQX&WRfZ%nBoPE=f6;Lo2%0nEp@p-S+_2mc1$GrFMMVO16MjT=zMjP_0#I7?E|LI~mKZvrRce2gD|8UkzX}}}1hura z-yfwS95^^LgZLkuRXkw&eKNF5q;bxMs=oW|W+%q{K;}CqB}E2Z+B+y{SH{gUKR5vO zq40GS+wvQ?Cb^?y_)(SZa2-8HX!SL}lGPu?vp5c+4JaiDXpoFoLQ2?m-HCW)@V1lO zz?oUs*h0-Xip!@-x9;3ICnrZqJh`Hx(-0{`5yMN-<@Y$T%l!i>2-ySsYh$rGm9PoMhx-g{Ws*)bX)u*WAJgfY*kyHE(s_0_g7 z5p4*2n7u&}sHUVu0wsb^`dD4X04B4Ls3=(}DR-p}d?{%>GCXScoE+@Dcy z^2x|BOCA)w@s>0_JzZN{+qYc(@uOcZumbBY)>CePz!ZU5aB4ure-30wnB(EF@M^26 zu6D<^fcnTOcLcW-!MYfxjXE%jgCP-#=zwuDT?bzCP;8%$+Wvp}sr%XO@MvCW|IY(cG+w zhr(^zb={3_FAVCmba3@&tX7?*Lt3gih0j&mdQ%>yM6=DO+D(;S7S95-O8F5l0H+$q};v1)`gmA~BCLKxgrapUSW@Th$ z?;O#{HR^#PIx;uWqxrQT=R1PPx@ga_9BpJHtf8+(KlHk81fmP468@yFeACTJTHjv6 zFt&&d)s~^X!){??qp@mqmQX4nZk)Y(m72)#a6=b`AHvU9B1XI)ot?%Jo{&C6>gecr z_wnNeqt*L={3;SZefmU3f-*w1w}-CXC@^Cq%&MsD;4|C>?yFlA0Rx>2AHIE~hk>%t zZgO9g?o6JiCq{KS@8#Zz&%EfMx-ZxVX=2A=773?QFvsWtt)O`i`Y;)3YOI9^S&2qQ zM&^{u>M0mqYUeNw?&CZ6&CO^UX-!k7MduvR=ukB=TR?P%;iPl$^Vc87m>>!ui5*+F zZgs+#LpV&Cg!DZx_NJu?Y}vZimz5a0_8c|V+w5#V{V7wB|%fmx1D=Q0R;$kQ8f6mcNs)e=nIXpX13R(C6 zXk?j=|M}BDG_(a3?&o;P3VD}Tl(A>*cA0i<*RkuLUBXX$z!`$Xc->eBzij+RndU7A z^fC#-W-FkPOM#qDJo%#DhZV)XwEmgq#4IGFSK6-I2nB@03_ig?BG3R$ahk%P7K9iW zb>3{}UxeMSAGpA(0?v{hjR9F)D*EkoYmEmVTwOP7>_!ZgO^guB($aGZXC0 zz3Wj}cnU>J1PeQd=i|}wPnDMCDr26qOxt`CC|7eZcQ7JWus} z(SFApYM@j`$5rFDv`=}AB4wWnzP7Z`%2(|P2@gMq#x&B|@i>u94~^+`kR#Xt5=kUf zeXHwd`QKw$C*1q)5OL8ZT@`WN*3$AlezA-3|E&EhN|zjrn@Rk99~vOZFR@EkeO}h5 zn)?dHo29h0unLa~r=@G$LTg6cscMfDZu81b#gyENXb9Ufs#P9Q*t@I~9vVtU3QfK| zJ3HHqCZma1Pnf|)ZGMt?cz6m63;Ph=d+Av?JW20xM}1fvx!qog7SkJl$U_S)kroLL$%c7y%jpI zFMqORmvqsZn5OMEoaHxAk954U_4;=TAO%uin6=9mwAukpZWpM3`S$G|B+8H24SL4L z+tt;-%QuBzBC-`yRv2d?FQmEfO>J$}5e2~6XoFSybsxP+WF$15^g6$&NV$df(qhG< zaTnpI_jRF&`Q>b8^W(u_l6V*J5Pk&YQ zZBU>K3lz%xYylb?nmv2>ZpR5)Tv`I|-ZgCYDy>3Bx;FmL_i%D%TJMF}VX>DVROEJPb;dUY7-1(6u zlb+&}B5}2FAo0(6bfHO+G&M7P8Q77~G^xou@$18nAN#Okbm|hA z&oneLLgez}w<)3+L1Y{v5glusuKf6c^YIeSb*$H?epEOvWJn9m*o)zilcUT1EZ|>g zRFn@aQ7TDC{XujvbaW zLvlvO#sld4F#4844>-pW%|=a(QWV>_8%R<5!2?lJA7UU3$Tf&42>kxX?=(`(L5ks5 zBZFq5hPZz{c(mSsm zjD=%rm_y};R#kQ;A&%Fg_VH5(T{{=u?~!*3UoXGE6kG6mFgcA6cWW2UPkt8{4C=A< z?#TO-dv9=tM<d?wR?mr_O-2Du(w&p$zcpMvj2O zw}%uQu8(1+JcoW21tXRCMG6zt49-fza%@hdWkbN!uFV0!;VeiI73c-;O`ubI38RW*g~s zV&W^fVmYq|Q!yXga@I2YENl$8T*w0%%R`R|K<+s{0r43DCEktBzCRkEBon4Xl-Zy? zcY5nRPfn&p_`#bXonoF8JAgW4^uZA{Xc7B{C`|Cp@O7;xeKRry4_V1V7?m-jz_6HIy!E^I8A}SU9%+j`Ui_wm5HYa zkfC?`wl_FOeH6#~iTk-;7jLJee2@AB7-XBawl+Qxj>|1WLo(9q*RQ7lw9utYh9>AI zXy{sC(fUul@B}k@kZp*90lK@2{W#3$(r~)I9(Yk&Du!Ow=WpI@Q&CaLa>f|i7fDIA zmxDQjaQ;zvggkjtyOv=NjQ%){rkdb``Cg9_QG%PCOC_TL871y$Ppr_adP zD=->U5nCF#|NY=}p*1Hwur=`L;SiJS=rpr@09029T>EWVCt8lf`lY4C!_QAXS!6TV z1}S`_Ara`HHJ0dIB^C+x`EGL)fExjwa#qv=#Q01o#xnV1V`G>or1%aq6F_y7R~{wi zA7Q7|b+b#?PV%qS-?_iMC?#8>(H2%_KT;Jiq6ujRT@bt)G#7 zimL&b3GJmxYMJ{0DW1Q2<$)boiKa;i`C+>zqZsz?^~0~*M1q(Xh>R;I{G5-5j_z@1 z;uZGek%q3nOc(KKD)A1kKYzd8%6Sus50n0=u$NY6nO^Aal+NY|4sI&sf+^z>H5nZiz>DC^-&HBUow-@j2RL_!@~#P z7%efN@HEV5Z*5JR>aB@CC?rIUxzL=~tBFzxn+vFzhOd{u@7J&Gko@E6M|O+AFatoY zhp8A9%0(kO?$A_0od{u8kAPO8l1U5(3BhsZ+kHrmf@7#g6YW@$g4%r9-Jd^Gq0QR^ zbS4IJ9dWO9d4}wchT=^mti~7AO{B9b2g{VzQ2uHin*qI0l;Xr~`|}H#UJk0!xE7GuqbZWE3ZKew;&h5{Ri{8&-F_koN z5_jR2*IpBRCRXYpYqu`Len#_2Ynrd`ptpi3m-NN0a9JG&;nUD!-d))Ldt~I`m<8S_ zL?^QEwP5AZUTxEd7lU5B;1DUWk^>7sG>l>n@_X&>#W?iV?>TE(y_-`2Z3VE>Kl1fG zl-B^A#zp@;AYqEMi;DyRGPy+jZm25=#(LmN3!PVl(TQ^u@CR%TiB}@j1#AlFW6aBJCip}3y+$?_jt-^RFQ_fL?RJM49x(bf$Jz=yzw*ej1XvyA2 zymezELHWR5a@3;lEbpO1zJUfnsUTb2Q4YEd9HIdokE9ofg!lr9o8*Q-NiaYb_wN&p zT|~PEc)31|g2I#x863nZbi^Wglg{40eGtoQfv!uWB^0rslk;1X_mTFCiLpirY7@oK z6pE{))Kp@8Jrg>yl^)LE6lXaum;r`b*TX>-i1Pkg!C@XAAL;`a9&3nC0aX!&_cIA+ zzuepth)^#uR0DRzBPYp-n*j+KEH*CgYO3mCARb+eWU)sBAaV*Zu9_eK!9XW`lyNaf z=(_-)dbH~Tv1IY4zoW!NnJu2%t)iVRh^>=j%p!&E%2%?t0-AdsjHR{fH~G#r1ipSE zsA~JmzEE`4dVZ+i^F!uD{<5~1GaaRC{G7cd&z@hO9dC#nud13>Jeq_lssm*Ib@v8B&H0ugp2QU(=zkL{w5@%Ln0bmR;HHp*NL2Mu91sxX72XZ-wls*0gcykL0W4mKD z=fRMW-eH{!4N*`IH!XeA*QdUxOL%UPQ4G0co-g#1zO+sd zHcZ~{F*m{z6H-yVD%@hl^1)%pDK5^2Ek>iB34q?$ryI(Yos%PkF*jz7Qg&BglLIy9_H8%DG;o4ndx100@g(+$^bjoi+ zj}&2!LQeoh?41}xhJE}3bu(wzZongqPZ2V#qr*PK6Qak=0dLHH0r(8Y5NEXIF(C{t z{v8Wf>c!4>(_^}!t}Z1jOOB@dYD~}|#^IyTvWk#E1S)KjaR=Dkk35ERuf-{igZa6k z0VJ!a-9!MG98@fHSY!|27>qc0k(NewlQ;S0jf zcIR#BH_P@C9T5;ru=>z3x8m5rbV!Y$Lu>_mv3)2}$dQzZ`U8jHXInbUq7w?CCuI1; zzi^2U*(+kUF;%55Y+XdD`v`>zh;O355T(gO+~tMlw8HI-7K!Gg|Mdb0)DK|ez+y9; z9~vF~i7h6R9oEGs=;1*9D)VvQNn<+TY?w~=fXl{k4P7*65S7pUiEc`KK&&!y`=h_w zIk$Hr(r*X${15I%x*@VFZiBV3v~&lkgzR;GIZV<6XBPheQggFyN3`YZTWHEq+gzOY zwmbWQRLf#}W-s^y_WmgTbJG$B4>G)?cz@4ZkM60aM#)o=dw09WGRsW`y6!8{-`cE3 z?n`M-?J`WYXd)<@XYQ|&7ohts_h8!7T@f}$o+0K4eeo4&{=5v0D4Mvh;9q>lS&u_~ z^a{Og_6rki#x4;d>ln<0Cf42P)BttQpkW6-JojGvE{vpqk3vsDoH7&I06H3ps z2HMHP%WEs?IS2{lg?AuY3rkcG$^@0Kt>U~QhBDzmyJIYv9=uB3#lUX4W<7g>ntg~d zM)uHB34WdMd;_lws&fNJl63FgBP0j(5^V>j&^O4>$*BZ7JO8HSQ*G^8bPr5T56YrJ z@&LNxz>-Uwq5$#6*&*&Aq1d(S0wC)7eAmS(J*W@V?(XhJAgl_Uz!wsA267rT6VoH; z2ag|UQG&>ufa;6vLaq5{93rg#3Lyc2$Uqc7JT+Od!3o*U#48zmKam@O#u+urF)kO&=f5kg7PFgiFr&@{ME3?ButGJt)BO-W$DwqMRty>a6a zviBgcnZ9H_j+PhJ5`ys#pzRB|D}~@Hh!W=+gHDC)*EtfwRUMgYWdi*)ULL6Mh=n$!1`l!E*BC z)KmjBO%O){bry*PNnAhi5SbgKQ(x^L2x6E^@)DmLVs~=w)>$y&{YVoS=<^==3?U={ z2;bL)M?{1XV{C{xSYU$6p%6{=p#MC91oT1LYDEx9#Z&o6)R8R*$9wZ`f9Q#6_h48P zzxlO)Rf#4u;Of3e!<>npqVoB78FM{x<7ZM%rhGbjds)|{t1a)D=>5LPIsQ>CamSg9 zUq@T744aRWe`;!~TszZl`D^O_7PjvJkM`*B4pmFXIRty3Oz0CUWski;C6%u6?cTbX zbnS)$TzWMD%!B54?;di5VVyZF$cLz0Ul&{5zkk8TCif!eHWG0&1!_@FN@@}T`Z7K* z;?v9btOGj~rBH7IJ&)|Qi`0h#Px?<_U?569f^WyHg##QMQbuKk*M1p-H6^dl2KZo* zt0IvIz*ynJhG|ULQtz=0)njQgXP;~iRUjv9U0 zpHX%SA{h#*Y$bdl2nkk06_TsF}UsS^idV zWG|ON5vX>^05g0NR0T0D#5+2g*=-=@U_#!Z=tms7ckg}@cby)nUhpP}&5-!<_dQds zS-V^>KRHos-cTZv=^rVvwsXxYLWqw~VnT(9l$9sL>>YYdP0jp9s+mXQg9jIEzn#d< zRaR-urF$Ham+;y!LRsY-eXi|aI+brHhnSCk-M-xq0ai*w!;(2acAUy-{c*sNyN7rM zYRNT~l~#E_lLsG?zAVnh#L`YJDrESQea2S4VZcYwcI)L&N56W^K zhXdqf!;uQ|e>~nH|3cc|n?NxTIxFVv1p*B4v8i3ZPD}EX58qsU{U;p*!vNR-{QV21 zu!{i5Z-FzR9jh7zV2VV4L(D+R(}(AMzQSWb_5S^P`?+5c9dii{=X9>p7cCyJKe9#T zuzV+`?;sERK<1tFdvy$>gmyn)HE4%x1bXBFc-o-ugoLFmChwG@56j%xMI6aMB0}Ir z;6Od2q8M?u$3J&h-ia@W}SflLI92Feq&vnP>oQj z_?besbpuEszDFO34TKq#rt0py_VzpV8NMwILa%WP;is3|xF8xRvBY$b9; zy@?$t9+Dfp2MIJnc<8G)FlcH>q8A{;5M8Nm>^uI4W`+L-k@vCSf+L zp317Ts%^*TSJp~2F1#7{GcsIyy!yf-E7AW-?NYdbqn((xSF;Lx#p($>=9wIHvrk;b zlfU<;r=|@F2Q_T6hPo&5ndpD~0E8hU zVN6pME>-iKh~4UjQ4E!}wG`kdAvHdXifR|Rus%qkzb-nD5eJ{2iI;pnPxU^R4@ZD= ztv<(~W*0#@qtb;fN!rP&^vg1k1JJ-Ay6~~3s}SAz@|+RsB~=k}ijt3`!XzO95}m}f z2QRrHVL^f{+AgD#fQ$;Z4?G=O7%Neiyftd(?C9)#3I71eFI8NHi+VF}M)owso)hLj zSTPw6>?lE}ehg;c4Ny-UOsdL0ppKv4QW?RQaU3{sJz?c<8Ge5=v{2Gn%&iatkx0BX zN^TGMF`~ryc2yp=u&^L0iIqhK2JS>0CmQ4-!De-5;TrkA@$mo{!3e4u7h|C?sX}0B zPEcS(X#u6{Idl+Wmx?$u5n;yV2=kau-^8(>msW0zW^>>Q3XSMgN#-7DeE$t%EUwzd z>TGbO>&6;m(Stc-IUh)jo_nsDQtxy%YX)@Ah&$@jn zkH^>nl>GeGTF}Np>-GbeJrT7{>9{tJ5epdF{IpiPoV!4?t(PN0gvWCa3`!osIAA2ABXJhNqiJNCQyZ(Af!=_(){`j zF&e#v07|b~nF>t1yboP}gFU*f`xxYU1RD+%n}C)d5ED=@G-Dzd+#QeN_N4yWF{&1@GOJgidnJHv_FVOaO{hlW#*GsO>hsM_tZG-UHqU_5 zvXd{-m7z!$MOFG8y!?qf>hp6=tOeMl;^=&EZS3e+yt7N8mi-XERvxnIZGrlO>z^$T z3kZ1Quh*5-eW5X?^#A@Ha{}k|dW!eui`B}vpVCAcX5IguR*QbcIVa4(u_|I)-+$sV zCC5-OJ$jFj5WY}iA%&lxr@|8=($4?whL+0paQ4!NZ^O6E?Zv96uZ&69tvX25( zeusxggP?E&K^MpUA4K|py*lLAz-PJM8#U#oXVNwMzfV>B%QkVdHD&sRA&0mkEL45K zEiw}Y`T43yKm(ip>C*q#>)~#(rFQxA)u;JfG1W>}LssM3qZl>fHz>+~CEFui8yxxU zS)*c%@Zl*i3G)K=4DzD;{;$ve?^~q!Ck9X6F}Zot&hlr)Xj=fAF)G!RohboQ6icSV z*fo`v2VN8MD2l(|SA3cN*Zbo)D1TmAEL=?%oKO9;VVuFbJXv%yEGV(~MI94V+pico zbX_&=M)N5NiSJ6Ekbdt8%L z7Z&6ZBI%C%lu%D`vu*pgRH1HQM=-trJ;r!myY_8TS*$*rD8L^-evAuz1;;F06p#hh z$lpo+=hZ$w z`c5oJBSa7I62R>WqO5q>6>qUoLF*u%gK?G@jpb`WbjMs(16WML&|rWbx_8KjCyiS} zY3Mpk9Z1w@{lI8LRahTnWav9cu6!`*8YG{|z8>%8pOG~^0 zxQPx;f&B|UwFeDJ?)B+VDZ_y2yq`C|B{;E@_Yi7Z`@ zRImwD9~HX<>pu-2mLloPmvAi2T9hXigFjN012iinMu zVoy=Svg{6?2b2{?sR{wib)(P9XM6!{b>GjQ6v)0<3uvPR34=1}P{C7K3=0B8R7zB2 zL->>e)rt5`#80(mX&5?~QKd@*{1D6Ft+$__e+NDz;Tu3Ml)SZpXS2$Tpa>ccWEHaJ zmtuTIEIHxxs;>6B$Aecu9+@DfdlX5)9Iz?4W^?6H+_=h3B6QW}sB(4+D&EXE%|pHDNsE_E$0kaw9R4p|GUnfUvOY*M8-D z;3A=$YUZ1S+VC3A{QbKHTBL|b)W40H;tu+u4}lqi1GUC8%rL60y%>ASaubMG&;?Qm z_|&wt9zfrOosJOIpxarZpvZ^^m5&|~j2_)eBK)pIzlbb>e)BOJ4?2%D62mm~d6-Hp zuiGHa?g!Ol+L=pMc@)MjlZf$2tRRY10+9nGqM?Pb#DLxFzz{6Ko23-sF^NQ0?awIf zOiM!p37-sFHhyj(-gR9Hb}me)fk)mCoW~2Sy4DJS)^jNE1hgXH3sQxN&8P3{$-X3s|i3I#CX09z$cZ?#+MVHK3^2JWjsxG$5ECi1c3Cd_1T09gNIHO#?XG87l;cCaK1OAiKL_4`3(4Y znEU`xVc{VmCt-R`sq+d0n7S%Llq!c62=z2ULJ`iZKX+7B85kY0cLf9l3MIg7)HXGZ zw()Rtb70R3)N}Ik;-l$fh#EAAX}d}IrHF#b^)W&_$7>Cc6jkuqXCb!t{yM*b(}5@a zAC$pGMCm;isx4(7JJ>>mbk>^bT^;cz03wz0Aza-8kXIwGbqtK*N2A z;*TRD$cU~9!bzy7$AOD7{4sm#9VAc^-hnXIfe)+%@FaanRXYM#j%#~4;eN!-Cz2cf zY7n(Vx7R^DG~Bqb0~!(7dqTrUgy1<5=noK$zOa7aW}=dSX9?^If~Ok{0%*|ig-gxi z4;w3MNJNAeVb#J`?3-O8kx)xvV?8G%r2Y9CL~y;PLKKV0>(WqRjT|5e6*j#$kG^IC zCkCFRkFT#>vdH0F<8RylX&RX0&LJsT@pbU=39$L3vp~f{xb!Q_G&VN&%PZO4q<0|t zi-ysnGLT}3Dv_M3^3mUP?Gj(>V*D#-SOMuSHbgEKCGGb|d03u`T?jPu{I z)OiKu0A=POtTtpM#FlQSf`2O}B!AO{SI&Yg@zy43nTWJELb!zCTG!lUZ@lAr8F%AW z16YsAfob{dKBG4JCr%JEmLbb+CfT_sauU`Z>|jJn?6q@ARK{JMCgJy*xe7 zs;e_oRi1?>98NQMT(;pzd4ZXAsTcg0ze2V@|M!)?-Gn4*Cj=J-IRXWB@!9?MgtUm5 z$hn4^X@|ESF>{n?2ZKCDh)U1CtmbS2{XEq9SoOO;Jn;xhsQ(5c)3u6ds459Bb9Xk* zfJN6aVd0FabzJ{Kf(WC(5M48| z-g*5?hzrmKkU5=;@ZCwovG{qyi`JRIe&Pp8EKsmNg5 zisL%4_O-5vG;cj!-6?GYct|#pFdU@bL~W+TSN9T_K{IunWUr4bm}iIjrw<+voOupz zZkeRdjsUv+B|EUXwO0-#v0l~qs*8O?BH{eNhHRbOb=q=ZOZ!c2?X?npng9ok3V-oS zg`1v^Za=(UZWh<>U^F0!?uwcfID4y_?YnpTKvKeD5mVCfUi-PYwvg{)eFp?U9Pi0a z#(!wynyCE8{W#Z}cbi{s#W(j9+yg%caY71he=~?omYjn4a>{Cm0$yjHR!P*{SPp{V z7P9Dj$LEWQ<==JH8Q=sOjI6Gzq6k!{Mg@RAwFV3$<$+b^60cdL;q>_T;~4?Pg=={T z(PJ%ePT?B$EhywToe6UTc8Ju?oBI&EIi1VO$}BM_J4w8f(D18b{HH>j*+tPug$ z_?4Lf6NM#Vv`1{z|03_5vJ+C%E0nZD5if1qV^D z7Ftu{dwGHS*tT=;7Sd5+;gL7I>J={HB8oVOxE^IY++dSG*#GSc&VB#Tz*0KvX{#-e zAn7zdj?NMTVM*00T=EAgAGc;1$acfyU-A=VmLeO`*+Sa0XO9~!^c!xq{FC4c2qKWA z3(mvn%X9aOG59hB(W}3Lc{9wIa=1rb0@XjM+HMLk&Z-`lHuntf(VHL`|Ffb7|GVj-~xj+f<^6@={LjkTw(*6SnJQf%238fTw$PmLW zk@G-y63Q)%b&%Td5}+DsBaLu#LtNAWZ14h(qK-005g;nU-9kkLSLSwaJ!0}WFF*eQ z*qOj*(EdV$#34dpCA8Uqhim#w+t7bcFfE+~#Q_Y4qv8RV6wD4pqaTU};FcGs#~@3E zm=nf6{3qs|Urh3&B@xa+JS;8F@>2+W578*p;rB>3=>K8sJ-~AA-@oyzLPfHQq(l@- z!zxW%lq4l`(5JdioVpBZRy?gh{C8n`#=_uE(UE76#mBDPB z%1>>BT%2EeDcnIpQoJO2(AU+K8WD$GfB#;;|L*CP_uro!hsvLTLsm2w^zeYS@rAgC z&u1A!mjThI4<>?mYvkj7r&i=x4shS&cm8)TD>DA4!CX;LW?o&W@dj%_9|Dk!+?!k^ zY!~so-jh`o6-l+TIFFgvGgA<5%m`Ns)-J*&)}x_)qqtIT0NM47J^W1w!anJ~fCyzK zOqJ%qY$B+%vqbE8%OS-kQVXdqo!GN=h=U?0L3jWJ5G*G za>>3p>hj&779noqig^P`<@{?% zY;KO)ZScX@-rsz5fm#ODKWeR1Ot6UR!Cn6V#6AFH9ew?WIHD{DuqWSP`U6Nq%)ES8 zH-_&{?oFJ06s!xuqenlZ%Fdc1`yB_$7`V77M?__I`gv!*R{x)O`rr4Q$f|@}eeoHK zhrIT6A(`*y0f~Fi9V)rN$+1PXRFDB58tiTY=^uqCCL)nF0%B21EHFme=enGxjr=G+ zfpyVP{_D80Q^##fE^c&Vx}ZI0F{~of67>-a)Ep2wa@R=-<{R6TN(-*oUqoDG$YH&; zLPm|mSD}q^=JY_%@82Y08XA9D{Vd#JsH)5nQ($dv{SvfktLRRDT1czav+vO(o4=<- z1Xg>(sNg>uKi=8G$RX*26a3y#Gc&4M!NZ5)=6PFt--D6e3-ig~bW|IZT)D&I(M6@d z=H*>3X}aAV{f8>vXY~mGFARe7sP6{Ld8(5_G8VqdBBW?qypN9;H|UZKEv;uTXwIRM z_eQtoK9!|Z*VewP%j%4ci75|X8w1fa^NSDafb#LfpZe9n=7qqP8x4SHpNgcvetlpW z<&?Dl0e}c?S+d?k>l8#&pkx*Mijv=y(j2BnJ~`zyl=sKp_#P!{YI{=Koq=baP%&fsZER zKJ9Ds*UF7-kCLe=?|5|a^FNr=R2EnXII@p+sN?*?!`pTOqjZ$T%>!5MXMmS`iOlAI zHz$7g@NCF2leZt2YO{XY`DscU82|Wk>id|Y6=_Q6_8_}*VrHG2gN=_r2l|Pl;fsRDo&-%c`m{!a6;QE=Q{?i`3GnI z19e}6V(7=FZ0haq-ePKO+$QyZe#8Iy$az`CzJ54W(BoNClDW(t6(1kZ_t!dc-M>}z z4Fd+_Mn*>n!p@aAguJYVDGZ5!Dm_x`=&$lD)&{=pp)mQerj|3EMQ zeb)h|c(u1GXH0Cmrb9P4tntk)67-3V(VhP(e;7a~1(v?EZCHbC=2litiVz{{{=keS(Tw=*JZbS{;A1~@%Mdy7LPF=Gb;Vn6FQ@mZ44VUDIcKcL8VXi9{?t0kvlr9Iz? zz;b&4LZIB{9J>DO3;vT-Cj>I`=QzP{6$zF2CX8Hy2%Qss4W3rdG+8Z)pa(XQN*7@& zCe51);n^rIETqB#xZg?)w+GHgrhWlo zoDNVmyff{POof)}FCO(fB$KFrvSmiNuKEd?ieC$Egn~f_cl)+V|1$iekg-BK8YW?< ztltJwnB-FiX4d`uNu~{@w(Nks3HT{M6Cz9m=3%yk`90SR*vJZ>o4_?+Y}j4smk}8s zYc7B6u`w64l_+{~pkEn353~sY94V4fe?sB~$$~H`6;;!X=t&o>O-w$fBA@YT zTyST{RR&-N?r;L9!Mg;?ex@5au}G%JplA+RmFSs07Zy|rceMI(Nc)_lvt^A{1MQB|4aC|;E#k&!(j`>k1lvY zPIFmI*g%QximD%;8eXBd=m(1cIl-IJI}bQKEjPoDYVc=)MoLZJ-Nksd}Zq~+Fk2*bvva@P-2!wv`$}`n$XY-1WXM@Srh5xBhjrf zff*hH(D!|X7DcLOlCU<9W}c0IXO)HdK>q%NEjyfDi%v%g8CS|~%C+HFe9AY%^o3$nlpMKb@39K2Xc=CMH=&Z7&)hOCm}s<)GXDDY zLU--=ukS{_pVzl>JhhwqP2-f)V4IElrG*`|qcwLro_>_#jM>chjI(v)xwALvxn%ar z+&_K){fHXZTQ0YQyU(4z&d+tV{_#Uz!&rys9UKz_g16$_Sz7y+Y{MIPE%GWX7-y=F z_RUFZEjr8}?y#h3J?y?tLPOB>$%oDz7s><-fE-dX=@}6HqN4>R)iJFWh_%n)szp)* zL7Sm*DQ$>DX^jf95HK}q=iyzev#lgrT0DA!(&bs;*_EJOBHlQxAhb&3X~cYjM@-p; z`W-4TsByR8_$dvllh;9GhrE>YXsn4M3amlH{4sLDqRdAU08r4Dj19Qq?Tv}V1T7r$ z^P#6JYHGUPxzGjSE`~7`O}y~(T8l&!dicT!o{Ozk3}G^bqPMbQzW{L~W*LwKnW1>J zl{mv9Qn6Jb?pO<-<)ui;a8pMoCnw_lCnhW)i<>rXENF;>W}WPt&z~~~xVC$c27~ev zM+0dGz`avrrWRpOWMsUDNFf24FZq2uwI^^=oE?JyF--)RR->lsLc>m)K?F%`#E_MS z5~z}iPTB2qpA;>#d0_L*%J{HHwBG|YDplPU3WyYmk1H~Rh6`|Df7;2O>C zTWr3kq~BO*TiW!Mg`s%=_H_N9Njk-GLzevmI}#EN{~SAJ6xG)H`I7wgb5Bzo?sBhe zYx#-7^I@IT*9%TzKHnePKa65n`~LB}qozuK`?=wKk~)8W4L0v!>dmi@PfJOa7UTRp zbDZsRRzf!*Q%+S0?t|{SQtRw}n%3J~AEvp;==tESruQa?iJgm5CPA1+h`ET>6ru2NrDr>wxnQbr>{{ zq@jgy*$_d|#^0tm!=ONi5a|MLs0Fcl2_}b_{7x_xUy#f>+w3&|StA3)f-|(OK%$oH z1PJHaMdsK{gWDBbXM7tYNtVMxLiUeJ1nRK3IDCQE#@N_5XrJ6K0A7Ow0GiGL>xHR; zBzuF6guNM>V=OwI)=cxk5bO!-DX*NVPV`8txh3o`N=r+34#2Ku#ObjM{1ee1BZmp` z&(4k4hLyc=q!0E5z_0$$+ak&Jww_)W>RyU7%5-gj3r|b9@CBomh-r&B%P_9HJx$Q6 zId7&?Qt6nqHOhqXg(Ld64t}tl`8v07`Lk?m_WiCmNx_^}4@J4<7m_8F?idOincOt! zZHlq+7OH2@Nf$J1J9X;PrNNxXyav*$s%y^k9n`KXavf>)T=5(m|8k@?o0a|v(@XO+ z?AO#Xk|rMOrT2YjTv*7uPny0FFNOk zW0k78_HaEKl(C!D9MNf8+t&Pov%f?`-Ak_PV#$@q!98;m!HNU>qviI=4ASSj*}dMb zR4rGQLI&>j^z`iK=RdEnzaNQM9`OHAcfNfod*K4IwbxTQw>2YuLB7$9LO~TbefyN% zca>cJ!BgL65-x-V1gB`GW8 z)X7n*y=^jEEgu4lLxHkqI}6Jn3nWXl!n#zw(NY{$jU!*8B~xSMiJoos38t4dsSYpq z40Ewti?jMJT-s>edg-$JDF@lcv?I*N{la`Ym8_0Msd_F1%qdW<95Xt>x;oa+MEjsd z7CAC z>`?p`|4_bk$tO7=4RZSQ$X&!p|3VP$H;6bjqnw#v+TIAN?KW>be~G<+*5bh$?GxtPOZYZ8>zpxuvSdLQrsi3gWy!*!d%qjE zi^t@j(I2{;zwN+=oCFPv^YIIaHW1pbK+!N&^9E#X6lWlLsY8H${2_D!Okz2(2KWrA z?1Ar-`WZr#UHtrh#1Z=K+a}PEzn<5Wl&nclPp3EozPjEBfl(+}v=c3|W04l;y^&8vJKU zS=hva&3@Q57m102a7@T9jXV?6@+9O%#$SW>xbEn0-M7;}y%--0zQ{0l-A66m#k@j@ z;+zqy_fV9X`r_{>tw_y}$@IJPsdnrLwFPoymw^eLlkk7+L}TKs|L=hR-Ei}Y)|MML z7|JU|jf7 zcq>dyP5sBm?-Cm>ArS%NnU8j!BElG;ez*sZV|1+639OgjQe^5UR-j?493)%u&BaJ4 zNos58=*TZ=u_Z<^@Da!QX(;iRf96Whz_#-aE>43qVD~#g;TeicUARC8CIc)^EXd#3D%ivSGe{mcwx2C8RKK(5|~k6TOUJT zZo0E5eNW)EcSo2G*T1#5DEoS$xG9~FtvPfeS96ns5^q26WtIA2iyiF^uhOfF$2$cN zG8`}366*70@2$+KQ;q$9|A-ksElB0n+^oC5D`e3YAOHp==Iz?O{sO{)zZv!(ipa=D8o+AQb4)xWK&{_B_&&gikPY|%%=$%;K^nu?Ok zci`N$GNEhf`qW20Jmd89+IV$4CKTKP)sf%b(_<+tF5W7+XHR!*f-cJLt4CI=%&e1` z8Ey2CUZ`)lvbyitsKayF&_BB;2BGX?Y3N|=o^MfV8kTXHDB(0~Z{%t5z1#09e_4aa z*h2g7UbbsIvo0fpn;Z_=oz;qRaMxH__|wUw`+uOXZi~VoGj3E(Ke)e6*+k$^yJ108 z*$*iXv%G^A)1I?YGCwMlMJF|L?>GGTed%&_Txa(K)}u#Xp7mkf92{_RMQ2OxJEaYK zWBYdR`eVB2vcgC|M`A5drap&&|Cj|n#$Z@Swpm^xxD5`J|NYmZ0n^hb&n?By(1z#F zy(xPeJYgVTs?g$i;jo8q^3v!0V*dOltE1Cy6TAF9PTcTY;na|znVboE)F*PcG4mLm zySsZd>i) z4+x&A8>@S!DqHfWuFBlz^Ur4kZJp%HH!GtK&N-flVGX;<6V&-m5^o5>prwlJxj~(RsO!e_NP)E?Ek74OY{wO z#l-(;*R>eiU9K{ytv#77RI*oTOK?EJyVMV3=`lOgjXKV?1aBN)2Xb=DhP7+QL3LL^ zyS1?S|Mhdq%Hu*+Yr7^El#N9KOzN8xKO}U|DR(^e(J^W*HyV5K@?+}7x;huoP^sQyKNWb)N_s- z4<|0jhNkQD_gfqEhXx(Wo;GX#FjJSds;_X*l;M$G<3p2^p+)s~7?v3&yj4<`nJtgI z1xSNt+mFl?rwY{cnyPAQVvqh$W$e`>=sf2+^P9veKCK#tU1RK zE={!ZGIe8$D!xI7UK~9nG~MxWDJz$5 zV#dv)>*cHCGIBwjo3<>;>3x+!y z4!Uuzg4$EG>?a2H^U>@+hD<#%qR)o|F~3r}1+5UNdO!|uvLwRQG64Ze$(?MBwD<=a z%uSZZiNZFN?JoQ%^8axnj8`F1FV))k%7U8z35WikqrV|A+>9bV#9(!aAVc?I`@uQ( zh3e+zKh771Sj7!Qi}n<2e^aMj&sw*QJU1SQWsibPAAf~gYZ*$v8~6;CH0us+I=y@; zcD<^T>p81)@};NBION7Qwf}yD!;j_n%n!)S5XNQbz+{g*0UtL8xAFB8Dq*n*G@~=> zwk|Zl00>W`VC(AaBxWKk^NBQqbmQbDA3d-p7zof9QHUKJ$rA{;;Rz@27-DqH#6FMC z7y*ei=!FOyg$8y@V;>5N5>$PMz_1g$5N>0-_3Pb1PXR3=$k2~v9Qyd9bD;7eMFwj| z*|5Q1x;qU0OFPH|lLp!=IngMpwMcQl29asWi5r>P&j zC;taw{k#;FQgzdY5mX_9K z2YuCz-c;eLTXavCz z+-ozd8uFJ~gdjrdYlSZeF9gl!1GKF4xwrA|2G2Z88F>!1Qmx@Lq!pvYTa&~c7enj;H?IC(o~6B84UBZE^*9bht1yfsQ;bwIwb**1l&Il&RRw5+6apV(Cj?faE+dJnaBb+fr1NbR(DVTcbQ%XSP<{`f zm-qz{#1~Bcpe~BlY$5;VoyvQ9%?r zgzNY#TFd%hEdbiS54!k1C(oT*jb?=8=G4rhkDx9VB&IP$I)c7;gHvX5xNQfV+phR5 z(BjD}PKtDd6nDJ@!vV_$Ii0rht3O2HOq3A-Z_nV4bZ_7HZ>^5RCdTpqLolB=O3j65 zPtC`c+bF;ALD<%!&uYV8pX_@>m)|&-`RnN4U=)$vb5<*N@4t1r7v&fjZ!2OscX{=y z`;>={!N)WvatxP^kg>xgl?c$sS7q(l@oTVy5-&@-R_cw_sQAcK4;a74)11JuLw!e5 zPhbZ4uwgHylSoUHl}n%iC-!8V2cuJwVpjZ!d85Mq#OE)qU5P%@6CD?fh1T=sWn~nK z=>0+bh4Uv7ErE3}Zj4!%UI3RJ8zk1D8^EyuNm<#EX`Chjh;V}VnT(!@mxB%rCjvm` za;qu;4y!0WJ0)15cc3b8a8@wSLqkEKz}OJs2=^t0@+3I8xS)UniiO;Y2vM|Y#uGb* zpNSZNPP@!Hyk2;#ZlFoCH`jyoUq)siXP6l$0tfH$<1Dzgu=&a7j|GJgQg0IF07w9T zLgQI{3QAfk3K~+9>;MtcJLoJBFhUf%TiY-)YwUhcT~9hcfUT{RS*w@z^w8vRF(^O#UT!-N1)H$=t9P z!4HKEnhFX*$qeKL<8`I>L9Ilji%^^qzHRg7MKNnE#}L`W;8eDeRRn`-Jz{4eW!D{m9d?1_<1KKMq7nlY>;TV(d^ zEyJYCpU!RfuD$6a-~Qo-cy8PddNDaiIoo+zdHz?Ao-E9qgUlnw1BmZl6e`>x();fJ z{jT>cq5EHn`mhSG^@#eRu*U>$`|^&^EO2eE4kEjef(;Gn@>w3osdhQQTh?&*HA6Mq&=OpP!`1Wd|QPnb=2yad48I#`T6+(Al?* zbeqx^bLH$e#I9!lz|FYDG6A{FADLR}`HGtLK8((HTyin~WvX5n^Q}5W>=xE=`we>AC`jGtE7411t*J5$es}9DgBwSSR#JC86AoCbffDPGyR@(N zRi3}g?J4m>D87oRY@|5dj$=qdN+A5&N72sIN7qB^qaU8qK74*<_p1$am749tw`tEf zhDAr8&b5B#@#`07NZQ|rp;kZg4v5WL9Yc;7#5yXrq}Ii`$!wXBpZ4q54ak&sAwi`x z_-D+0oN4OBJ6z$|q|$`us=9im zq;_h0`rEtUhOa5NHh^_NFh{t9;NCySE&G`Oh}z8a^786pZ=X{eqD>19`M(1 z1=8B#G=^8^$O4-;r2qwso5{!&eBiLJQc(mEFhyw{d5ltG|2qdrQ=o^TqM%dIO;SJr zSmD>N^c1U>_dx3C2lI7`i8vYNuS!gsE@?o&u#3`KvkqLeecy)Co4TZBQ5OSo;wq;YSpgE%0tjoLThL}9C* zn)S}hPBw2EGga!k96sdcsvBtzdA{UZwl*6{FA#b*B>KKV^iJ>hNVcw&!n!wnb{pbq zv^0`*rNTwLG+LB(R^G~{iJHht*o62jiVrU&Y1Axp`nm@+*E*HyKPZl0nY-QkWX@`8 za*}naGHdSfp=MR92+20nw2Folfw3tM?@zI{y4p2D7wi`Ml+>&P$C%>Q@6~=&tgTgh zULHffdgXSw57+yq)&;9+W@J?CXA{4F{4}OafYc@<-VCM_#>Ynf= zT`=H>aS4(UFzZsbw%G~AbKPmC9hyU+-knSlVR;UhIosL)G|pui{cSnIYRWk%)9qtF z2L%P`=;}TIR7%t7_*N^;D`#y53J8>1y)s60YuDb$XeFAL8f9*5`;$0K0&%CnQ;A;E zE#4GSamdj^o3|d4qo~*_UR>B5LM|lwK|D4WR}<;rTv}c&>NC&$MGeq%F%fP{H;iYC zRN1ehzgxeDKm*zEFX&p`J%DDl0vHlddpUh7Tn zyU%zK*dEZ`AG{fKl-jzwH0WudEy5DQ_5R^vFgnR%e1|9`l0xs~s>4DYNhE(4@=ja{ z>mV&8act-w2uo074-ZL*xXocuIW$;PsH9)v9wW(2h$#RoKxDyaD}SN1(nqo4oAMPc z+z#Qp`5V9oiHS+U^1ci84$ff2+4L=#YOW^{3HTszx;`f|Bk1`H-suz*tpuhVAi!kp zATJl!7sqF;lJ>{JH(}b%27Ch34h~TE;7U4)QWFv?c~}RCGaj7|iFP9q2c(wMVTq`b z!c#(hNUD6K)VUIaCn#}<6c7>Mii|(p=%c!(jJxyqt5wVk*6X|zHhWMoAr~pGon#DJr!cr^!9dStrqUPEclJN`*r7` zm!7LlG;>x_GHnlRE)ivT<0Gl^cH*wZn~&5F@6gbyEo>R92J39YsS-1Fq?_#U+a2BO_nz&T{11k1=TR^7Fgm<%az6 z>RQSXKK{{4ta4JXG0%ziO>xwx7QDuS*D~wUJDhiJ1(Rs7{|m^I_B>Wb_7~$FrZhHh zG(@+=#&S`Qu;YFxi%6cWQ1^`2O7i#~B5~(G-B%tmi)-Q7lQf z^u99vMJU)!@mA5tb9|ReC1lwLC6{j@M5tf6t$Sf?FJRVJtyV8f_nteq{N6Zf;Xdb_ ziMXhhgH=3Al=SXXKx3GNC~0?<>sN%~lPWMv+|_a~s>P&f3| zw{Df(3c~&=hRqw&c%uG2zhEh99Q`+8Af{e2_dWSgk11_n9q1Z%}~jN>BvY zEXAnW8fQuJ5*5uF!e95XBG0jDtO~~i3(K`T^l!17NDhs7LtBFU9ulW9li%Fii_L=0 z^v2BCDraI=$IbsZXvmcbkU>%>aAFbp`u%l~)u3#ct)5=JmH;2fAI?Jq__jDoiMlhLvsfv z>T$|Pn{)K0I%-c83BN?@ar8mRaeKj@kCF%&%6U{zkb74j>20u}6ID4h(vYIEuLP>- zLX_3!=H?r>Y|+(4UXlx#U$iO$_Xd%6TKMG)DR!IzJV8McfM#GcS&cnnVgz8;IKrS%f=tCW{B=N{%PDd5ete{!fg za{K)JmTN3!x({D5<4N7vtT{fZk#U6STi@lEhC3t}MjGS`K6OgwatOcK7dBKWSYUZ{ z_|tx=yWF($!r!Anpy*9-N9S1T=H;n1@PB^r`>YY2c*&95r&X!INfZyW8`j3X-aIbZ zGd{Jnb+BVVC9;Y~Q_jwwgFE}iajSVczw_<4x}OV~G7s-n_jFj`rfd4}bZFaVNA|DJ z*)nZ0SU}BTk4O+r%7I9y__iIHRB^VC8*`gCBxSJwzGJm~Yt7EKd~Xxu9pe%+2L%Mk z1HyX{4K>uANSvb+XL0|1iIKmx(%B*M6w7g=gO87h9KXx&GGuj-Cy>2ExAh*dMx}|d z*+L1UCWptjmuKtjKc@EkyiDf(mVTX`Y3nHWm{ztKD_-ig!?(q4CwD%?BWTR)3g*}~ zqQK8>ZSUyRWpoM!prCKGWzWB z63ZAB8qfk#dQweggDis@-O^ot;l4e0-=8>*KY&3vH<#_Jm1&pKtu=MWENi2cLU}4W z^S;`;@7oX*7_>wsZK+t>`kA4eCsC$9+2>bq;V18+lb_`Z2*`78wMM${*B?nhev zxeojeI5#b@FfxV4%d6Fi2FW=kID@;O)qz817t~P_v}RkjgH`ZFEgOXlp(_ds#lTr1 ziYAd_r}3aE+Q!D~&zv~}7kL!;hQVzSkh_nLjyi{jvy;e79FSw5L!nrOt5O$;3IdRL z)Ya8DLXeD8ji^0PB%OzSiUh*K?MjFU^g~iG7ny?wAS1v)#FxI;t022KqH!I}e7A3t z!R6OfReu57hjo^^pl!hhyfMM>2o#A7B@}AN2cW^x3w~c`%V7uyJ)uITbhZT_L4gOV z;}mLR;;=VAX%~15fa{L1)HrlJEK5W zz-W~g+}Mwx-SmU(KQ8ni`r1Lv8MS;8s{(;vgY$leCff8x3U2e$Ny3UH4GqD3p_)(m zHL?}wuOH*&Jl#2fu59ZI@lQ~S9n?^@xu~r(BM*ERf8h(z7x(G#qjw$`G(fUwjH4m=jzhx5D1iOzrCIuqu%Z0 z#${2W`!#Hgy2iN4VRLwnNPWM(mvnjNi$%YEI;!fYsn@Bj?raZNvknM*2m<|EbK(&@ z$LgC_x!4K4+_%Ix)Ki^=5OPn+<>H5_KMamt&TtIBaxGv(a8$(TM4#N`$DwxGz7-n} zzvQEvoVwRi3#Sz9AGvNn+r@*ILQ+GPKE&N$ zV*PUHi;CIdS3W)mx2Dja-gkr#uHnw<ccYq5PVSL{3c z;tq-4gYDO9i@)`ZW{uYFW@C)6p9#_nI)pQWL3nj!Vw%-Mha90BJw>nen_HQ5`bI{> zotFAn8}VhWm`8eDJoSpZs>#A4-g?*gp-Q>e6OY4R=G1;WKs-W92!4yGyvn>=+S&!ni!~=!=zKmPwwD;sxi5t{0|daQbxHd!$;`(A zR1q|Z`K-h=f>FElfM(7?cw8f(LdHMf)PV0r9>+)nN!)^nmAFtCHm1<9sBP1N67!VM(Yp;rm2d z%0r?6oSUOpk3u)?j`5=7i{I6Qv@%!jA1Wbg_ghESjFAd$&hIXHkx^MLlAu+TLdK0XUa>57vI29w*dxj?)q1t`C9)JCuU zr>D{i$uW}g<%o9LfRa2)Y3yj)K=-M;obCBNvf8qE+7#_?^}U{@v}U}gZhms4r|5TF zG#h6HUlGsv;8uzCV&~4sn^&LU2t?MD^|b+SHy0~Y&57{*jpl4(LI+Qcw^>I`1P2Gx zOQ^KCX-J*>RlH#38sx`o(Y;bbMODyr*{rA|*lX>&Re9}|nR>o`%c^yMUp1zh9~kb) z;rl3S)6}=%Ff+aW{=jCeqr_vSzQ>2BByB<%bi>Pe6}B>ObuI2vyk7V?Amsqhfg^@i zY&&QB_0H#;Fgj$M4X01~sg>J!(`Wb-?LvBb|6VO6Pd>w~bge-ba~#jS>9C0r(k-6K z)DOe3YXJ)*9FVBfnAhF1{WYQ+ITN#1jL9cUK^(NBcZF4iiYix;p%(|FXF6j>0&I(R zJKl)hFMcuPsxtGjF6dWFO9E<%wq#aPOL+KC7gnxbO!B&GAsLNw^UY|e&!GQEmxOQr zp!$u)W1rirekJWZeCOkl>PQifo(XQ9A;03mf#ie9fnf)%E$b$3zy70R>>SXhCfIDZ zwZmacoLe9o3!7LjlNBaJVl?JH4^802y$cfvdDi%KMShcdVGp(f@h*cXKu^T~PWIf~ z!Bfc63;V6m1veTIj*yv|gM&tS6Ksr9`iC)D=6!+iR+y}z{C+(?(*k>CazogPw~ zp2F??x#aGNm~c4p_FY!gtp5vw?Pkm(rR{K~0sD2X3 zi2?*BNAL-&UL3vS3g}H6rH^(D% z>y(q4Ce?D`DzPRKVA=k@_NP$RR3*NxRbbD`YWiao z=Ue1ze|*EvuPUc@qY4KumH0I4tgJ@&#D~ZqDm$ZfUd5e|@W3J(3(jJMis6u1bCL?t$DnIx|rKD zJpYKn(s+{$zs7NeImi1y_pYIU?>l@rzYj{s#P`wj%O#;>a(n>pCcb!8uv*%P#Fb7V z{^XXW71qesJ~3K~NE63KT+QjpYX3?RulRM!0?!L0Qp!%gVN|2tb~emA@%}=`a|EHP zDGP%L#tQVn?Bs&8J~-V#g+(DP0&TJsXklbGeYLQcqn06ZWDey&G5^&9+`C6Oaw4Ar zMJ%rCl*KV!!->|wWZhvfI(Y+2^BBls?OOsv!p)H;tg$3EC+O4C9DP@gVH>~`u8@Lt zy%k0Ek!$}$XTpaph$~|?1=%x7FCOl$gq~s-(!IbtvWecKBb7A%WE6jcEF7p!2sI6t z2l5nb=C?92bwY(rcm+f|5i$2sjO%XO9K#ui{D4&;`bhkokV)%9q)HI~%{d71$@-k1 zypxcaSOjbeWvXk@Z^8?a!W|QNXJl`)tkEs;=@KKgHs;u%jqe6CCaw!?7lM}Uu&icU z^f6wJ5Pt-H2JitI1Q~yUScM1;On6nYjtWe~xENDdTWfKp%uU=l}VoutS% zb&~x;(??Sn3SKanMBe~eaJ)^VeGdN3rf2%I5*`mpmlbc&_ zr3MBt+iBambM6%9VL#X5nKdp;FIdx^HRMd5I_Cdr9)i21P&i?|iz2##y=7Si)8$r(Y%h z!Q~|nL7}uxv3pb2Gx15+lC_kT7uKb^KMgss;FJ~0aj37MJiqa9-20URndKGl44*gZ zz9rl4`*xL11IBd$oN@K>=GqS<8!J^m?!?_xQeL%ScvNcjxi1)Rw#sizVbTZ%Gd{`Clc$|t}ViHGpJLM9kRI9-Jr!t z)p~P-fPl$dI*v`OuWm6;u4D3OBF13#oxvvB3NVNdq&{KOaQcp?A(fGeQdybEpS73< zH+C@$eBd-80xCsB3kh$-@<2`X5;P_DTWPjs>r&|g3Lbc2*RfTAvp^j|(i7O&*-7+I zW22dj{S4CA&<~N(kK`Oj{2=1Q#I9Qq%~CXaSnJu*-Kf2;=(Pg5{DTRDFNFI{iYo*e z5b0QMg|>6;pw`u^)F27cH|t}E=jbIMZ6_=&Y_$I;FDfgD&KE7O8=FdCidxf3~aAfsGlU*$EQ z?ro1>D&+4j$(-&C{ZVvsXH8_Rj_wtf>d%siX$C{Ll3R3t)^7wvK^HbNm2?S}v^uZF zl74aN?910W>+H*`P60O`JoaYp#-Zsz3;Jt$U8dj$JnChc=iX-g3OQWgpGNT)|vOZp8q(j!oYDN_rZ0;!hw~BA%-uBe6&~`wMnmbYgGDZjD$!0ZQ)lkB1GHstBTgudeoF$``(7h zyQ`W%z7VVaeZ4}k+-_@4R>t)e$B9h6uouCJi$9)+glGdQC&U|Of$xSE4A=r0)dUXK z1;h>*DRVU5rmRjVzKN;9q&1TYx(?(~yo6L6p~O*n2jtQnmu!i$07<3SuOI7JH>om#n*;nc$#qRuTM79ohK;(M1Iz`Z&mI5` z_?bBZ@rS%>xki{mlMn~yJq&WXksbzxkG(OpYJ1+9LfL_1o{ECJiPv{e80fSl)WzXT zNC+jRb7*Ki6#CLF2)=u%+I{})SxP6j9-ZBl=p?mIL*^t<>V!*Ju4^sd@1$S(cQ_V> z={w#p^zJEq#8@mPS2Fr7h?m{2VmRn@&7Fb%-HVSq`v;1SPi|l3Dfz%HBBfvQ=%J&9 zS%!Cxz4&mtaZc=MM2ec6#-CQDFMO9qmWyk1R^GABY}x8*S!$xREcX2H?bUIW*LRPG z#l$cb@hfUP{jK)vSzuuDbbc2=hNtP4_jaz8>NDkK>Fhkp0;o5sO%tF4ukgr9@9Mv9 z_5!r=g|_%(flHD3^Iw~E7ie*FiKTKcaZyg5QHVcfOna;CE90I|w|40dNZ-paSan^* z=8sTq>4%S{s0v;C&T8$w^Y_5`2D zw|6C(Y(T=2KgvRZHvgC(Z@xx3e~m9(WCc1Q8t~xz1q3`JBRd3AWj(`M`=s9>1oGxKC3^udR78vc*WjD%L;qAo z(Cu`c_X)q%IV>_Yy+38U)t=%F&9`sfR9xOQy|NeO`l;9D`$nxIls?t;G{nu$>w2Y} zD3_KAoSNqXsB=Y5BS_+edGBVIrTQU;PUthsv6-86Er+Nn=-Hl66Z>2$KXbVIcz^2EtqZcBmP)yG z>z?)R-LvSW+u*^$-jU+CzgOqd^RTRFnv_6BFlyUM(IW`NUA`AvgSsoNP(v-PrM(clkIYImtvbN-dI|8CVMP zI}m9d5UC4#l4#1IeEbHjJaI05-CWqg4%&~5b0Og&1RVi>O6Vq%s!#OMm=l{fqK9ry z5P4W+_yseVQbVRlsCmd>BIwR_Ad-PFIIJvRDm5+b9CE-&cZm>AR7A?xQ(uYVFQghZ z9Y&wF9$3s-%n7ruhUAqIF#6K4&A-MHm%bHxthh>zJEC`{$14kmHB}ggn|iScC6(%> z7%0$-JNziuII|?5q0e z>_kAr2k-6pqw@TrJiD~zlfZDbsR%6`nF=U%c3`yM{C5E63fPmZ-mry~joe0`O1Hj; zyAF16#ca<^B!=G~X|D=?D32k-ppVHsaG)@K5L6t%|KKyrMaTrojF1;Es3|DN(e4&b zKYjMh16D_TUX8O5L0gvy3k#DO3k73pAN8p`bddAx43ohtXu0uQ?FAk{U<3yD)!c%1 z;aa-UcBm+9ei6JJ`iU`kF(Rg!hj^qtp+=32*SbJw8ubePOLXWQz>md;;Grg}6_Cqy z``e*R!Rxu7lq!Jfs9Q0`0=@(IOPrx0!R~Gn0pO5|m^hd2HMFz<3kmxNwHmk!4>S~V zmEf|$4|ENL0t-hMCdg3y%x|R`-B6R$Iwu!wQF;)lOxFO&!Ua^rpx(HpWS~ma{EHwdflqWZH*qOBfHh2 z4sJOhbNYJRdk@Y7-}53mIF_>a7)NJ+U6>M=kh7?wa;>H zs;kR$^uB=@L&6og?ka$0nwdwDjQvKXLLGR)O{drc|E%Y-R)$gzzx0Ce0(<$|T7^0< zo>t!=R{Dc8aDT_X>R8Ijy^nwG`FkR3E%VL5XD_(_vYg$kgLx)`P@HzBgEx4bvVTwX zT@0q8BC73xw3JL2ms5eIB=He%nCMB7%tr%p(8g6%uDi$p6~vDhL%8!vUP= zc6DBT)9Up$>Y? z@WsL=N!lz&_xDA_fbIKrIs^}1+~Rg0d|l_xpReb!07dSxi%w)78a=PROGt1`OzcSL9KE2V+ z-djv9&46oX2CCvLKk$#5_Tq?0Cu4O0jGJLFD-vP7J2LG5;%3JK$E&NhG=c=|m*F%n zco19zfbe9IxGZ0tOEiAF_()KmkYU7 z5Y;B%dYf2K5Yii9c|`IAu#R)5Pg4VD=c!hCeU%H*k;RbgxbD(Muc&2N4aqv0@lJI! zx$aw{QW9G!WFudawBBgt?}YStHqrb8JGMC1%7Y7gi}2yxZDT;3&;dV=6O6kIFV3PO zXTG!ON)k;FrvQP*JL~87kb#YcVGU&zQV#n_re{Gxge8CgW;HZ|V*_~fgvD@va)Gj6zV zHRaKxu|u)je9NOVHVH(%N>;Ky!^vZXLu)!3NWe3Ud(=mI^^Yt69+4b2{+AA@qPAtN zKtV{k+4io+@TZ$m(`qL0BXjMQ!B3x7O})TDl!)VQ#GLXgR19@irPXpx#)sLf@t4CX z6lmq=(&t(M6MyOGOXKstu-6sArkS$#_AY80dd2c~(E+QivRE9fq-<32Y59pmzh`c~ zZzTd6_1>oQc(ynP%O)E8Z1Sf?x1cWh&oAa+kjDojj+B(f_&pwf-c4(}*7Ztf`_HSC z(F4=PE#DtW=_xh-aFRz3+r`pd(N}S&O~Hgxt%D0W4bmYCrSCQm|CGkU6HSz#WvV^D z02`m{*Gg7BMmY(SXP%4&%?h(EE-!~4%KNzv96g%!Y-l3(`?|*t)s&4e|LcC8%FR># zs4pKQ-jpPLGllMI<1F^~{_nGo5rxFLygV_mIJYGm6co?TOaG2+@-M|@dROOTGPhP) znrg`O)ecu}b=_tE^M?gq_u$wbN&2Oric50kGY8Ic@@V1ymz>5_UoS{h<51e}CJ$v? z2M{H}O#Wpyv54N=4+F>DvA%)~+ zzPuVM&1EfQqlTej1PE_`%+&%eooLDsXsTC{XYdAr3a8N!{~NN2f`(J?TYdc+N-L6= zYr4r;R%E^)g^wJcm6aU2Gp~V;ljvbEW>sUh=ml(NhSyW{f5fea-iu$7q-*{2Ibf4v z&j!94fs|BGyTIAL4;CRQB(GDwENdy;_}}L%KbLMX;ngWQ8SwH?d0y?y{mdlSN9dB)}^IAu0C_Nrv}9fzM~7o z0NQZFBwS0q@izGoy8p2XfqXJ;_SesyJj!n#!p21Y3xUfIU<7&VNPL>WkVrlP+@jLI z7eS?YVvr#gLrI^;h&XZUqAhdx z_m_L?NYbV;a~TE+Fi%0RMo>+Ux7BmEV8QO*{0Q-@(QFPKpX^kw#!LV2I@4=x3i2#k zGh@=G^Yig@kKMaBQ0MD0Y^T6)%E*3XZiCa0XU~*7w(Otm*!A%2)dWNBNr~bo7h8l5 ze$g5T#^e%u%FCAtZb1OYR8pR~1v%uajih0v^V(1_Nd-um6v^_wdKM?f=J5Q5p&%dn7F(Dk?J} zB9$l=l3hl!M@A)CMKok3E0U}-lRZjBRFoZASsB^m_dM#l?)(0J{(|56xF6Tu!g;>m z$NM;5ujlJIe7EJEcY#qd?t#)ZtIwZ51BuX!x5RN>iEVORMi#mNG#D>bjFz2gj#Pwu z@Yh2E>kZI^Aic#VxT?1HPv6^JEggizIIXx4*VTJ5l0x=zmkp1Z6~R&j<|ve|E^jwA z)I8;LM&<@=qsVJDD>uukfGua5%P-0G`JH+YwK;NIPOInh%4(%gk5e^RWUmE>1S%*l zIkooI&qYN%P@jD5YP0lGjJn7|C~VOB()5Czsq?{t`>`h~tU2Da4pB@4d1)x8mvT5I z&w93MUh^J(6}l=r-f~w zKZ~EYkYlF&8dcaM;>@M&How*8)u0R2fv2&%^1m%Q+5PT3v$w$GH<$YgI<%o2v2Cxf zRrz_XbZ5S?+SXR|lI4nlbJUNuH1;>n*GAKHWj0>y(@OJ8l^GnDw$`^kMk7~1JKivK zzNMUt!rZCCcncMS;~{RDpFbzpCVybeSf^Y1upgv%#aK^z7s zoDlQ@1dv$}O3%C|8IO5Vw`gajV^=Ygyc z*e*u3@G7F~V&5j%sUL`@DyxfCr}b6jmtI+Lo4t!GIP%`gF3m*+D zkM>r}9an$lT3Er>yjR3)>GzKGEM7s8%4xQ7p0sna3WHT_lrz!&MXobPT9nm%riOe~ zKSlV4UVOLoFt@+-8`E86KI)_xsktxGht@{7u#GP^PbzsVjP2+6>HG1x@{Kn+-}eXJ zW;gV@Dao%Z>ABwNlAhP?gqU0_1#U*62=kK0=H1GOxpk;Ju1r((3}uEDQ%Gx@iuUR{ zjs2kmqhtGS9{pg^!ZA+EeOT zb`xUt_e;w)9P?`EDM#~%YrYiXTL->Ai@Oz9>Z#%~Jr%h3$dPb&P5oI9;>iv5;SqIp zBxn;B9F#STFpIo_KSUWH9_J3e2)Zm{=?D)B=_`|TP%4s%2Naz2m~5j_LP*g7YcAhP za%cd^Ai0s0t3^d}_;rOon9dAAa!~(3B^3vO6tUEXJ{mQ}b=az*cuo17y2f3Kl{NGV z4!UR101amgmGkin@<5{v+OXW<(D-6pc%eSjYb5F*QrhK2y$0Dt&Z6>U@KeP9oDm`! z--V;Gj$TAqm}N-)8CjTL>1UT;GkE&2$Z&_{?7rA5*0k?+(^pU(TQ54F(~qTgfL^Uk zdgM0CeN%DnBVjMNijGF^@c3}4VHFiEV3~vPs|LvQOTY)d~QS!M&p(rKzce>$s$`Ouw0VX{Bf~Q*& zgGzts&+_zNIHjqE$EsxA)zA>G9jBV&E%d-u~Sf}>)&avT-UuO2vP}pP)0&{D<8pD!w zm6#~MBnR2U8$zmn)Gde_4j!?I@cF*%`ne|x>Uz|Z4Ns;`?l{yOS!!pRO)c6f6t1PP zBj9N6k{~s{m-F-uY^0gf(Jy+>78*o54`yhxVi0+hM=|cn2b%?rBY}Boa{kvSR9!yJ zhC`X(_V)xjbKl$|-YsfYe;m^pHu`gB{&O=vl`;9}tSsm3p60s0i94{T{%*<0T_1zW z>*D)+^5&%3!`RvTzNKz_66CP0zIJf_DRtkc7iz(?$4AdX7f(44u#8BI?`%{Mz0|TV zH!lxL?PYLJ?97H*>IOv5faeHELwuc3x|PB*D+8wOF!Ul4T<7tgwNT&itz3qlo}@QG z_3m4h34sZ6dWc{>!^!fJ-m`4U+npcnmpI_y#N7HX7ht;JW6Z<1ZZoANt(>&|jl1bMI`%wq zv^ge$rTtK{adVZ$Z@ODzmzoZyoR#J-Yxj79=xW-_E+i!-`{`>Q*P8t~6Lsj=+i?e$lj3w-k{dkp z-_x7T+H#SG)HMTd3v&pq=0NnEMg)?>jZ;m@%4^vXO8W!BS zhScqCCVyXMw&q$N6@}k#wMR81~ zb5ADQdlgZZDlwc^K=GrKYQ>;AS^T$}}aqBk~MV`E$QA1f%tau+Nm7%$%K;sBPh zNmy82x&|7}@+!I7u^9*Fi9SYP9{|oT_VxFd6c_U~PUhn`gggR4@70tDuIQkJ{cu2l z#oUXJwf?}fYS&UQm_{NE6|kMLAtrYARp10+wn%w18%8|NQJuTGy0)wf^z}{RTK`MG zt~8Y1#uHoV$@$j>1=pwNYTFx)O?%pl_86PpyQ}?X#$#Zqoz>n$QOnU7ffD^Kt8!$H z4i2nG8Ugj-j?w*g3nts(XZAeb!G}{^LdxQE`H_rrCdzTmMhd08%2Uv`{f_cA*fhS9 z==@P9^>HB1XO*yDSvnVLCe_N3rIBZQa#WDVJI~`Six=(WLUOj2 zZTMd6Ks@jV8ED885B4|-UPrGEcn_Kx^TwLw0K^VZQE*X;D=HEm8(_K^J>sLVJPe@> zB*7PUFgSVE!{Waj{S1YI1x6$_TDy0b~DM%5K zFa_a$AoNSbAcW8kz(ru>#;z(Qur=- zVenNO?^q!HCnH0{;|Z!Plw#7eVuuABN8;dMxpA|osA$@H9Fj;Qm@V$lbU;uO-WZ9j zPPf)?T z9jUj`8cWUSM&+-iP)7JS{hGhH(!9U1)@J@eyWC2;T#XBr)5kJ8zts388PDha44N|w z8p&9@X3dqGt(1qUAi?D#}wnyccf{N=-X)a>r8Up5pLx?|U1!=9>wpi#n%t7bXV8i(fZ;Dq`7r6EHv0@YX{lh&;@l^<3YM(QOtGU~W2vh7~l;%a`9Y%|0Qd7;MWp5RrD> za?n-t@a$B(XIB(nJN5Ek7Fg?V(E4+f1{=c{&IkY&WNcLcxNTFW?+8mup4IiW8SPZE z|9LNK^K;-pkdTBvbH9996ayOhJbJoftlClvOokY&5hWx0}W;lk>L6m^@PX^+VLg}fi$Hy0;muv0n$m&B!W zL$hS^O_Vt;TO0%%Rdv)FhD%?zuQ)%DU$L*iZ1-6m2P%s0q_lT@InCE5dZk}wEhAD_ zQ)m;LvvY3NcH2FiweXDWf606QvtSp>!=nr1lwG@6Y+thaDfXQ|%Uxf;EB&XjS2ag? z?yw!>PJL-A3LS^o#zyxyfvvN}nQ?1Vb?tp_dsF>nXN$|Ys8-<2v21BT`|-KF)n~JZ ze0PLOkG=mI6CnJcCJNRh%rC99SRQa&0pg@i*7{P)`!0uh)SpS?Tp)MLV)^f4M;}@z z>lvdDu_vPCM#Pz@Xr}d>zcytCRE8G?zSlnWR4Y8p_G4Nj#Y1~%F<^|FOW;r6|I7$< z#Nl=_sE6Pkxh8DEYJjWdGO3g8LGOB2{08s?UzeqII~H4Zo~!G`RRMX31~I8$5!kVrGqnSF?wDvzguRVUI1=s41uAin99C(sWmlItV{epHYC7O8o^e?_VGM!VJ1`l-b4=SGjg@3!P4< z9B61ya$zx~T!q6m$rFTPzyJd?mkmZh6OG`5oOdoKE34w(_A@%w0EfhtH?E*)Jw34; z_AJD$pGVsH&WH;h5Ls>f1ujAio&~t7l>-g*PhjiPL16*P1S=Z7VpLhcSU0x$$3MP) z+ZsWRuA|kgI3=rJ+y-j?KtyjfI#WC-rC|NzZG8B03C1dUtHmgff!^YK!l>+ghUr#R zdv8x@ob*a;*Q6XM9}uN!nUJ5DQz7^TfqdGMxtZ?^_B6NWn|Yp|8VuU|eWy)V(`Gkc z7ka(d@Sd=h!h8tpgG%ShRvc>G-#iBjU#J}MtGE$oS#Ns$OnZ&6uq2DKo?p`CP?=*&gWs}y z_Tm9Mx+G?GuiW;B&MhiRp|&*Z%a?r*^UcSoWI*c|%F|TD7`lg9hG_n}^hGI$rW(avp=9 zOm&ROW~oisQZI*w`~3toL+y8$#Ld5Wy9_w8?QuI8XtP7`*+F%^O;x*;W}ZILQsH1azJ=tmQCD4qGnE};_7fu5)L?Jx!2(imq(OV zi$ux!6*cUe`S~_$s zAcq}T_9P^{*G}w#5mQyICinx+Del_sUk(sy{P4ZgpNm=XBz0GMC}MiKDgp3p<5+17Z^eL zYnKVF!bppTg7(9&R%T~)b#>=Rz^vW78vPTEDF1y0j_KsAcq*+fHf{yeNie%K4hS!^##1&4*u@DNUh#IE^)!MZA+ur?c^_5wP&v_IQ%x3 z*G@YX+#?WM(7NW=T>8b|rS0yoWpZwDHXxa8wMZr9>VWo65pfFxMoF``(N_lcUP!s4 z|0+4WDc5!9h{-zEw6<#%&zycp`QRv7uYd2D5xi8k}vZ+V1_jS$@c{}?)^MG}FRz-r;l!sX_Z=6wkmv$*y7|+_) z$>$~;53vhC=MNrOELMlo&qVMiQNMT3F>82Wfrx=(E@+zv2mvClXvI7cd7x@C;?7v zfY?q@-+Lv`9j7vO%uqm#?KSqXdeH45G9lazB)0UyKJw9!D6$I_}N{C5G*e#Hl^9BYQ9W7yDVY$wu{HeNm*Fj&2!{Cp& z&A}o9C73By0`x($D+L2?;*0bp*+fPO4A^UEA!ETDPXZ+5d5o3EoD-CJI)>>IeY;VHJ9uO?DCj%j4CaGayiIC7!RKtKb z?)2W40eyiTgbziG>42;*110B3&V7rmwGHyyFhd^Tw0Ih)K>UG48a7jBe zK(2Qa%g#CAE<)@F+g@{mOF=^+Uk#SP4gvYM5BFO0V89muH8-*1Lt`UDLYXN!3?{d7*9@C{m>;c*y#npKP{%xZ7-<;K}40V8D?vuXiCmCG*slez0t|9F6paX9o&| z(vp~t+FOJ?jj%S5Hw}xgQW29V)0#*>^nQHc1i`NGbe`>ISV9$<}t)mZR^R%!g3L zpnz9Ml>pbK5}Z@yE$t{|UuHa{AqSIFj7)B^i7f-<2}Z_sz}=WL69kEzJrIh%)u0f9 z8+0`^u-qf=t{yYpte{(UV&##bacnx?D49ADDGn4|3^Vp$`1h0pXgKieawB#&Hs1g- zU}dm`xe1Yj0%%{r%{!MdAzKYf2C}!2kuDwGjqFL!x!Z*<<16A0nGERxXwo3G4{#ys{kPD9`gqgHb zpzJ@=`OC=QL>w?PicAG?ad2E1;ABOS8Vl08OO4sZi>s8Cl}8^$=qKyu?IKyt5MUGA3~6rBSd!?0I{3K}L)ZfiBZm+ZO!#8> z5V!@(;NF}!Fn&(CI+0{wZM z7&^CK2-KN<`}TF97$SF?xCel?tN@Z3&cO<-!jGVUNv8!s5e#htard#Y=?`tnwpv53 zhoj}+;o;@gK?)!dJw?vobm0VqX2cQvJP_Dj$=3B3i`U z;mak{Vw*N|d;1oaVrz{aZqBLg2gju_q-Gg~KUif5cY5RG$u;6!+y-e&Z=C@Rw6APao|{s@}$TcqZmogW`k?aG6b*@9klviP7SHs(IqXu2<;NgpG{4>Last z1N{8t3DL#)dG*QT87mfaaVCd=a%m#8vh-7ZWDe@72J-FaX3Q@rYVATxI39yyYb}lZ zn=3mf*Vm^9`*k(An@wq_hP=8%YEN6WW5>!$*^6fLxxal=Vz@zbxOe-JVeycn>qXI1 zF;uM|#5dHk4`{Yr>zmVk?Yvs!SZW4?S08N$xKK}kqGv<4nxc7XwHT^c(uyXXe71o& zirO02Oo?KJkYGDp z?T6no#DDO{jg+ay|W^n5p$|iFk9IR3f@gD04fSO&%nKk1zi0n=KCHqTwbW0raIe=#OA_T zvnBN>9T05%&Mt5Kx?ADqwTz4!8Xy6(&Pw_J{p6~RH6M0(9eXuA#Khscq;IJMa9wGv zHCk&$%fxHxp&Y9GIPOqD&*`Z(s1^cpeW>Rhb2&1<1L=VDd*Nq`eizCC0(#?V z6a!)jdqsiTgLEB5ERxtcmZP{ugN7hNHMc0x;l`s9(=9X@uwW>~U?!sUykmxYzjCyC ztBFAbg>{mlK704fjS zam6`60bdscVia#BZ{BRvZpMjX1oawm9)oR|M$RQYQoaFzDFwAo)OF&*=j4-X0FA;3 zxzhtq@;K8;r2@_}9nJvkRp(*DM*M&Ahl>2c&JnjP5gtcHA$j8%>pt9P!;efRKiEAi zqmbu^9f7a|nUW9P9**ZSUe9?AI_>}Rn>?5s z-9;2{r;qHrFnii3+zI3kHObnk=^iW9F}tC$JQKua=GH_3sXCf(mVPR(qt^VC= zeUmI_roO+P{{5~;sL*vL<(=iHIAbcZ zReRT*;S$~AeowAymJ=VJ8i5DFDTEQA)>Yt>P|#GF!Tq=4I-h_FCa4 zg`<@{a(U6G37?R|vjQi8lIrT8&*n(oiS*X(FZCdjsDoG`jYk_?dSwG;d2n=td+!){ zl>N&E7{DDAm5^Bcx&KpUq{_yDDavq|7Q^Y136KhqfeC-_H!pL1BYVaboR=Z32izd1<8MNB z#WA|0pG+_Q`!YF7C^5XB4snAK)x&2zlBQd8&snUpcYdKjd)evj>-~&{r0jJ*=PAo= zZT3*eQroR?W=e8!yPF7?LfR$nx|BeP@GtCw_^uNyIGxyR;dV8L&3?HR9NS3?_CK%c zNCY>`%U*ogII~+B%<4P*{nDGCY6Xa8uDX6_-T9iOqL1bA73mMGq;B)bnjTt{;9ni$ zDeHpA>nZ{v$xrpu%#7|naMyV`{4x85 zy1opioNsRmU4Jaq$T(Zfo6KTTm!&C{^fc+QnX)G3Nl4%!#r6)IWQ4b@?}o`7E7}SI zPyWwK@UHHZ^%Yy^_J(UEQzI>Hx#orOUH9^LY2O;%EYGluDsp})^dWY%Y>m4c$|eT( ziatFT=jgTJdGkP{xab3DKLmf@!l?9e?UGAluXU?M7VPy9j(_0Vhx%{?|SFp_-R#EY# zbVRn>1gpd+Cu+ab)k!-wAb9c_c;%lyCY{zu$SjBr$pmA%Vs)l1%l&)zOuAW^`F5rq zKY8fTivIqKz5xq(p6vxTm#@O_7;YTdD6{!6q}fWY=BiIZVs)RK%1UNGnXJVq8+}ZZ z{c0u$g>TEO3@|CRu?fE<2Bhp!bC>2g@u z!5;PYcdG>Nf1b(P#K?H~7TmS8K+^KB5NjCX)Hcda)!$P#QeDB_b@R7i;6~32a+YF; z3~1zimtX$Aq^~%^#OS9e2$l=o(5J+po}55fkllpMTcp%qF765c-Ms%k?u>O9KyWR) ztWv+<*SD|Nv?+EdrRRB*@ZAS9ll9knk12$l5_yzg-TZDLXrsHx2BUmlojl=6KK|Ja zz~QEGoLWaqyJU_7Sdb%o+yA^Y&P>P(?lP)N(Rb39b}}?}+Uaz-YP;R(Tk?t%o;$Qt z3p^3`*PNc7ZWdDWFRk>??x3}yqRM@J&k=h`hqvR#`dB&wQHd!bA@&r0{=+ze+V7&s z3QIfl;t3-Py#Iau=^s-36bd3ZlLYmpJ!AK-5nEkpNXI{W5dhmEDB;o?VMOs0JyWAT z^b-I3s^*YqNH-t7)vOmja8kzI-dakYVZA_9zj878`u-M7!^%+QR?%@wYW1RUO#FWc z&3NR6X0LROf#A={Io#4|IEjs6+u`1s;B+=GUk?tj_0>3b8NUfZ-bWH19qV8m<^S`F z&3v;^evK6z&1{_)_74nHRF0>|kI(y(tJQsu6l`c*oZvirb`395m1o(I7;87wp#_5SZCk5#50u3XCNobuKd ziP96dJ+KuwiuKQr4*t~}?OBK*4qah+h7bYn9$Ep+7|p=Va0{_#h#eaSk>vzdLfHC; zXJK1#$e(ZJb()?nvriFJuPTV=mwzd0ZgzGHPAGo?(Yb+AAZCI=t8G4u@v-p#$OU-aJ-w!*K1?Vn!@h#^YjBoj(x1W&@>m*8&Lot*FM z>SEeu`u?E~9H=;mK;_R%>(R-S(;Aur>))zKD!Y{b_iHp!x#OuYa|G6z1W##QOz6i# zYW{hTT0HD*;@my%d-)UWoL=dL)SPJh@B6BbU(F(T9U zxYTn>=B<-m3bk`Ghckb+7(I<2zsLHYTN=Ad9ViF^`y2ovd-mXrL?MXsZl2kO{^r$26SrZ+ks8W7*Yv zx#nBS^|5@*RDZP?QZpWBD7NCC6}NC4CD{+C@AgeWiAHWk(SNo|4VTYN$#!43QUG$_-{sl^TPOZB76WVvQPdo_KPT7{omlmIr zA{ccqGKq!)>U$WJX7w!b37=1B-rZql`ot`LbD}X?BNKtGn~mcWwj^rZ*-)XK93TET zDe0j|zul(4w{o@)$fIgLzJ2TnJJ{GqNXCFpj#aaSHHHjvh&zTs%Gf_2MdJaFqnE{G zI_IE_!A-mLs?mklk7O72YSo8%(BKTZnV)aEDbDms;OuXXJ@pMwHEIhI5*~THzPanf zi8BH2(sZn<(lAW1W@PRNIL6y9ud1Rn#36a1u7wMlxNW0(et%ctHZ_#w_CN`293ikd zhH!XMJ`CIs)QA1EOsgaGw=P`z^{#yX#xjqHo)L%H<2i7UoeHVZdf-PB^CMtGp^u8d z3G3z(S966;cPCFpS#q%NwMyOAe?-4a&zR=YIT@c@KaO3v)WEiuZtZ67e|~OXVmi?D zVgSjL&S+S795&m&9U^s8=#_F!&^7@@>2AZgBqSvz<<@d}XTg4(-=}!$g{mUcC6FEX5^a z`KGEG^CwAK8J~N1KD#vQyz%GnBDRlddj++U`2Jx@Dk^p#g1c)Ee*8L6DQxcW^>5Ym zrwS1BRSk;w7c*~oYW>=(J?}-nxOr&L%Rkq=ee*>7f-ozZY2}?=ega zvXHbz>*=|459f34|2&;qwduj7`m{A4T0O_k9HtGKy~p6%-T+P zoQbOc=RB^CGwreZoXtBiZ_$;?l%^h@v#>pX>5p|DB)WZK58#Fudf8XGpRCn;riQwSlZyvepS4lV; z(UsjYF^;G#G_}}Py*~D2ywJB$Re^Gb%$GZJ#^>UqrNS@Wdn(ei;QDCpNX?AMh}|V= z^kjK^&0vlUymWc`Rranj^CJz28{?4RcZFofRT9bzWa}C)bWTKbMy(9EvhVVA`AYN+!Xwh%{rW7#nM=X*^Nnwz?t%K!?bdzM zk|r$vs!tvSuZnaHROp*=j`Z^lapL-Wv150(VfqX6nNmZXo0l^H0Dk%LZwc3D5eKGg zUO;1$UVi9kLsnx)9Hts)+QO>Kwz3-Sa~kNi>um8d`_v=yLgd{6E!Tz*&BdjuTO9|b z{L$^cm0I`rbJVe_3#|X180}&1i0;T3Uels@yW26H3^n+_m+BF?@IVe*x>00*Lp`F?{ogC4)%;a) zCg?eB;@qQx`G%4@lbZ$O^E#UaD^W^Fe)7jn~PNC7|sW6%tgYRSi z3fCr1f}RuJ2`Zd2tfGwJ-Tj@_<=#c0Ia!LxagILR*nfQ0n)Glwa!aF8yk9I?$%^&$(1coFz2dv5us%moOuOIAiSJ zvS>m))1mM2NI&;j;+$mK+$nWSZmidrvZrquw7&dO%rdZe>%xLnVloShZ}o%laLLHl zEr#QhGXL|=FE*rv4WkW_7>4nM4Mwv0)&E_nNoqmYD(3qWZuv1IzskE3+9eEbf z1xEM$Akc>0nF4$hjy;kNF2smi44Iq6wGuRBIFZ08KeC|9BLf4PEyA`~qIaCQ^1v{O zoHz%5?Y|F>`^^>fJeKR(*qm*^LvI9{sIBwg46wMr zz=4{5;+s7p9d~qz8vGY-j4=QowA#o zG`tXw=h>P-!(R;hs-9f@4M3Qicud~EUtLIxIu0o#UKnJRz(}gP+Ott4;mH#sxF&3O z$fH4ZCh@tksBLytc|sgTyybT9-i`SV6@|Fvq8TSv?-*zRMHKnm3vUBr=?NGAwXiHC z{M5rw-{|)r92*}eBx=~?5Zx$%doobIFL-iw*1HcMlwc+e=hE#m@MfZLb8}O!K6$c* zT+KmuOvDr%9VN^^CsAQcjnV5F(?_^=guqG+o#C}WQcDS~1w%X~gqDElvRE?U(vDh- zDE=@~lo#4i*3hs4Dwc}u+IR2D$czN~2|qM{y}w@n?{4p&2LlmPag*wMv=|A==70}mCeLv0 zozy?jXkDY?;lu1jsb3pBCTpL4YIgc*vk$z;w(EQ=iM<{b9TkD#htYmiJ_GYQ`Q=Am1-3qp*K!sMnsYJ|du*}eA*Zi#;g;KdB6bmX zf88-6>)oRHrgO*yMPzdA>ff(ix2rZuY?4{Kdn?^l@0f$ZqY(|CiUZYmyc>QpuU1_x zIQ=AyiGNe=TPf8%*3V<@nvxC1OTomG!>eTM;1KtGX02UG7hJZQ*^!}5Ej3}y7+*bpnIAol@ zg&WNJHXm7UQ0Wu_Az`;6Jk13z zyU8G)OcK7AdV5#O+4nJ+e@XVn<3RFF;YdK19^8^?!1lrq$5*~m5uvB2C-0X`l!H|m3I0V+O zf?3)3?rv{bLY@bwg!eV!C-L*oN}}m0Zf$*Sa`0g34sGqmyZpg@FPc_7_`3o3Sv*d7 zzG>EIu4LFCl*cT`?q|MUn@gK^pV5TOT+mlB*G{e-F5WKs)$e*&%DAvmK3?g#aKS=b z@HgA07A~7b%WcVh{hK;0rOi1bm?euW-i@5jJ}-DyJmvbgHK&Z;^lewFRPhbErn$z5 zj#G;Jlc%lE^@HD|^a`@mN^&R>ftqsMl(Bir@u84D;1Hu86Muk z$|cWz*ZT4gTN+DSyEV^yer16XSAs5rxSm0`4(Y@h*Gs6m*!J7TzxTjRCWJA-dfs44 zzHz&=1aodE?=WLMm9JVpw~j~pv}Gy@FNSH*=|Z>tFag|R_vI!Aq=Z2O4x@MA$kz;N zxy9*C9*Z+(z{xl;yYfl?*N{k5=p~?@se7`)UgNH;X9yo#;0F_q1l=!c$C-#PmcF|C_~wN9AR(> z&1F4v7$PHD1b_Z99(ZvC)P>j(Q?~IY0P|H{gd8JPP0&0mf@EL7rcn8%fCD z@Xu<2DGFgda6j0o|L7Y+QFqZ%eH+1F zeQqvhA+mlcI_5Nnuzo3=#X4LE$uhH9rx0I_VBi#I%4JpMb z(b}c$PJ2(Bll5uLmJ_u-SoekMN68nfJ(I(FUK^=h-$_*$g&-a`cOKLn0yt; zc6TBpg=Cg6RSc_OLt(b#gfA5ZW>yb8u*e1vVmB_C6e4N_+wRq{^wQ~TA33|+rDu)>r?%1*)Co8ClTFKIcK865H%rr5KC0U0= zQU|qKXdCt4+hvX5Fh;9ynF;Oq0aqOQ>$`(sj$Cuc>CgTk=oz|GvDmuZQQG6)N_AnY zm*%ZJ)XMjUoiHWQvUT_wmYmjZW8e0u%xvp({j6U;Z+;c4k5%&vSN45P=;eyb-xB@d zQ)yG>7KB^eP~AJoV#gpvp>n{pMW z8nG%!;0gV+*B0`?WTP7Ha$!8o%zF^%n@|LAjeO4nRXJ}gBw={>l{4K+yG6qBKKhWB+=lUAZnYntRq6RlcI(%%GgFFNlKX#M z+q5Msx$mtUEcB_%Dv#1qZY<|187-Sr{Zf?b2Pv3*DR+D#%hKhAv-ZrC19}Qf_*tRm zfs#wn)D(|Jt4*<&5}D^D?S|MRC8q?xOm}~1s^rmNO?xYK$lGOREaBy8g116frvUP~ z0g^01SHyIhRaqZg?j($2pq0NkMHWB_HEih^F8 z7%oxx_U)VY+Hb;Qh`EDGk;w45f{_U8!jh29YL`daWafd%Qm%rOMJTZ*HhF&lf(h>z z>FFOa=RVsrG!$iNZ7p~FPgSqtFu6$5KaHR=S#qarr4}o8b8!>T)A=nD1i0Vrctd2eLL=W z5RTXP;o@sR8#sj1Y7`=Qls){SqUpy+*+E~ zhH%hRqZcZMmlEXMCc3$osQ^11KYqLz8q_;cQMa)r#-Xn7`}~v%1Ue0j!=bbP4w4VV zzGYYp#E1c0)qCHvzyKklO@!NcA)t`(i_kfOxV!=aX|#!gFNXrauP5e@h!_JC&K{a) zaM7=0UE+)he)!P0qvL!e5@w(uN`LwC-egv0=5=6(glL5Q&9Jlldh!sqJK`Nnxk}1e zTnHTcH;^id(*H81<i!Z7GRvcJICDrx0KxcwKko`SVkAgmFVOtl!<2n2F;bL zdTtxbIaR+`Q7&e*)_Qt&tyNS$|L#rP9&_1Ums*5B?@ILnTqdvi?3Z~J{czQ$i5~{i zhg~V4B0cC?)jKmDwMR{G~@9FYbMC;dA_E z6s!ED%Pxxva_!&$Ey>7mMd+o4<4`TDAoLQOJgsIAt|))&sByISVY6sMvFhbJouUWOc?vV zz9GNquMwh2CUiL)yMFP{Y#%Pyt?FLGSnr{c=oMf4*pgfFfX>_qJxyD_2XdXFOYX)i z{xCLED0)I=c~;VGA`Gm^TsDW*tZ}8IKf9p|kEI4H+~AaPQ4Pg zc9}7W`qa;x^T?h`+l#83BX=!_QK!U(_Z5D-H=L7mzO3C?H8m_FG_qfbLf1GV=~`Bj zVb?wOXi&4ex3}wRK^F!wx z6*;S@>W9(;+H`EP8E^8?=&{Mguc7e$bX;vO__i&XhSFAWwB-63qXfBq^LLYzIp7Et z=%Uu>IdQgoW3id=iiw4SQ$y1aVG0Qn)nhQ%iEQWT?32XAF3_8ZA_jc71p01BEC@fH zsFq=kEGoY6%6ThHCP?)N$4AU|Vq#)62B*OjLpHn;-~}-H(2ZTgOgs*a69%%CAWVya z)Q5x))j9GUUb}WJ31gs`&TCGDo6_I!hlEc$_Fank&#UK-M((=?RR!3pR3F0ME`FcI zH`)iTBd{W(m4y8ydYuxS+aQ-#Y>2}gos;NPz?+A^TE~wcH*lD+Tx8|s40!yQk96-i zfC*1DBSVaE_t7?PIOFmI^gGDqydfX}fTKGovv3DpAkFgS-$A*3Q&V&Ew9p~&q`r4{ zE^~1BZ8)b_G z+Dwtiwo2$)$GeyMki$>4-jVjK&GjeRV`EI1=s14M0FJsw>*ti=S!-I}TDSt0>-6$- z6?04Ls#*%Jw{b@t&{X3*)ga8^u!YUx$opTny~Qt`d+i&&Ax#PK*$0pElwyKN+E>cs zsjC_c3rkKlaa-moWlatrUl}o#w-hWJKlI@vPG7c$WUu$P>~Ace*(5!^jdC6>?ZemS z9b&G8j`6e0L~(SvjOl;;Tz@ESag#>#SDGN+&HUKv6CE9-?=dZ6UesvH3fw5P zQ5v`tx97KcvyOtX7TK$^){AYNqVf7pE>-`M515%khCct>1M|oFZ-U3<`I75f&`7w~qrrFsimL+ycrk5x zc)*h^pC-+}cC|g^istvqNZU$ajGH%`&~fj6{?*az(9uK2&U_)!GV&97pLPY%KX4tq zH*id zht^MpstDAl$-T@{T)Z-*_2skbeKEPEk;2_Q!pgut(Vi5{N6@pcnVF$bhE(r=XJXJh z6|A}$t&h8;Lr(upjNL&w6sP_V?9XZ=KfS`M=kn~l3tE)=!2oT&Xhyx`Q|#{0tK z+)UaFCPw$$*a=yle{)Q))eqveANiQj$a_uM-RxdP?&_c6;U}vN`DSg|*&5{QyS9!6 zLV5k?wF?phBQ!XAX?m|7d;!Ap{O_d3#O%u!UZgwn?buDteHLF-Hpgq}`TGR(iE{$f zl>7S^G3n|_s5Wa#XFPJ`0~LjKaekKvoyJK`T12xH z2D=oFFO4+6>#e;k_@=1T_VviIT8CVwE7b7O+vhN|>gKIStC=+I`cBn(9o>w?m*ncf zM(khxtG8UqUcga}a;QG7T`#Suod+29{qH9k7~S2?IoP%@6@GKdxBmF$v&4|D=|c19 zJ!U1W&EW?j9Ad^feBX)i&t|9WpY?r5gCi8S zm;H)UibB;XxPyF|uqZA6%mr*&#nqk{yFOf#(0P^a{k0mHVO+`HQ@_?0T$gVh%9syj zpLXvLx@_C;=lZ1QO-;3;;@4Fzg*IqEB`yegoXZ})yK8pExss3PtQdLRn0r?66O`h4 z(>{6S#9gG|X_d-hV02uTcH&&e!ItI|&xbX5nxv+RpYnOq&~hm*i3r!Fm~fU?=qk6D zbai}vDwSe2*x?|szU+!;z^XFcn1ttEHLbKN0Y_1G-c)1dzdTdQZa?_*X7|9N(D<#^ zS-O(hlWT%!C&js}%#D8aZBM#*oHHXQwe(8k3Nku^DJ#lY@7a@epQ^rpjOgoXp_AI1 zR{Chjvja_%XtE}65RU-gPkV+Fn(f8kvUZ-b{7OrCm}4&k6Vh~}PpW&KJ_~eR%Tf9^ zZ>zbl1%@JP@$WkZpWa2q$2#CNTNdLUx~u3Mf9%c=dwXtYNeu5Vw2W~2c#RTgpQ&QsCg7jb&QMzI0FM2#3G|XS z8{a}#fKy@Md}T4yd_SjDV_|9dv%tlv*hXQ~;?D=8 zsdAmx8rkeRbm<;?_-w^z5tCZ9lMuV;33Hde*>BjF6 zaeMP=YtUucU38u@9=auJ%Hx9K-XToq}se(xORZvM?{zjEq| zt5|TV2RyB*sj0a6Vb6Cwj6?cygw!pQ)?npqsY*OPDcX0b`y*Y!2{#%meSMs4-8>8z z9`Co)i}O5jVn4c(LqIB>WzLCg-_CLEZla@Ux5p6?35hJTp;|@j$e;;{Bhl+i{cgy7 zsQz@*N{A;j)8$4?D9dh@sKciRjSggv3OX4*f}yGL0iIjUMkhl;qE{_ESw(d3<<2~; zNfHZ3 z_VvAfd(iItmEV^?Dx3P8^WL&~<_v2~W_Gv>>)%snM>!TA`I@WiaRsN|DhG^3B60kf zn2E2_p0W7L6r(Agn*3z3Kpp+b%;6%JEj+?jU*Enp+(l%zKu*nHsSvA|?_Z*xCFbcz zd60hk6y^31`Jj_^PQkx?wgry6oC*z9ViTvUQpFo~6D+=I!)rIgYnI$65+7Ju$0-Sh zc-T_K4r6cQl=;AApz@Ibozl{F80?DPHEL)zIMBV4laupKPU`EZrScv!mXvaJ= z7ohWo8etV?u`w&^|FQzS%g~MghTd_YF2zJ<1iBo`|L6@xCt!Qr=&P^c8!NE%HJS2( zpi!JMHhMAr*r;jmzWV1W+IBnDSI|pS&-@Y`I=3{j4=H6ZZ+*cFsx4htchVRv-Qp(G! z@&(qAmwqJD-Ny)=_;ryB?R%2*|5F3Ukw#tF)=s6Eu+yqh6rG;5%Xrngm z+o#(evfNtu_lrJ?j0_~G+7kHfs8j%Pm?kZM#njX!hu8exc5gT>_QC5rvx$eiBZ#U3 zgs-c^BguWoWJ+@VHv|p#-5|%`6}jxtnrFp{nys(q)(R0r^RqW1g-%N)1v^G^ntgoS z?qBGfrA6L$lxdIsD;{f)CQWy{8qDSKpZL4Z@2l3(-Vf8CHix4t{eiB#X?{9xH zv^AtNX?bj3cW>kU39YF?wwFrxEc6t%5d$YuvCHWGD-QN+Wf_Yp5 z3Y+IJ^CvF$j}W`$-yzMfq`5YFbUXp%`j>bJ|}%>PcyPIydpYXiSXE zn;uaRDrPclEWqONg~!BL+Ea%z4h$d&cIk(9vVZb`6y6L#qVjA{Z!h7gRW>C9SFOYh zxV>~hN&(ot*wlN0?!tu&5OY3!y95E>i{H?rpSI=!2g!y!6th0l)}v4@PR6Cijh4uQO=Z%2!aU`@eieP~=+TkX7&1P(jV|{IxFEVrLH8(q2lgqp9kb z2s2_RZ^aNF^TbOdh&;9L%8sEfTMWXBv#1DhUSuUsjbE`x^pbb0kQFURBpiWSdOC+c z23lr1=<$IysZ#X3Tip4xPX9ntao~@a&AD!Di-uDhi9gn2hBr6IzGTl+xbVHtJXW>j zea~}?c=Pgt9t+w#$gQj_N!*_|G5>g2$-5;DZYzWz z7(p$0aV#wE8{y~&hmGfoeycEzE(Zcuo7h0PDG5Nc9Bhd?M4S+e8bZ#u_bP5=J9~T7 z9oIpVRJ+IE>jSU6O~S$}t*xyI&jf;Lg1j9fQi=GbU>g9Eowj8aY!L`!9KuQfJUI0$ zcL8ISV{{r|M~9WbLzsU+nsGZOMi3Mnp^PGk-!ap@)b-Drh}O!2ePAX$j)>U2b*qZI z7zNT6!Wo2^$iz`V)TjkJ zfOgrKf}lF!yFdt;f3>){e3WUI8DWqd^yZCY4u;fapJvAh7hmG5G8`CLD0!>uaBEik zsSP(Iij$8lu#!3k8seO8GYzj38PdYAqkHzf?C^mNM9Gt2Z|5uTSKeVZ=1e+t*kfJH z=asY~&5NtB;6 zXzc7QHrjpnN$2}RzEgWQrfYe)$(FYf`{7#^2l#HjWeW}PJBFW~tVC^~6L>)l61Bt|!r`I`IF z=s|Yc%pq{Lu||}bh(H*ns(wv%K5b-^gB*L@O-|oU&#qk)jVn+qk2_UV>g1hpB@$_= zY|U9C{<`60L=-ZVR4jB9=Km;qd+^eErab2X!>%bdMeLK-uz2I-=8myxh$s2r)?$}# zf&(szPE@KYQi=(H>5c|tSVxpYhZ_QD8!RE?0@t+T6-)Oq8h=lr6lwkPL82FYI|LV; zpoV!4p**Lo>mC>~(Xg_TShpQG@K$T{DlM(YsUW_fE(DVHrq)&%kR9PrKv-J<8{!y* zO=}hRC;?m?OM<%7T}J*dUbf!}(eZ&=O{o?A!k#k&uIf zBll3rT3$%GR%uONbA%lZ07f&0SgZrq(``S18xpZo%XX0Po_uzZ$cBPC;sf|mV3)v^ zXtJd0@0JUo;G{34--Y@4*WNGRZU3_QnAx(fmerh$YK{2XlJ8_Plxaq;47&=GztyBJwJRBj!Zj#Wf|NkD3FB|TsLpQ&v550 z)e^$gTkkh*q{p!z-jqqlc)$PQi&H}C|B2;!xu7x_K= zLPz`LNwG_jnz>#?_y9Ne3R1L!_nH1_G6Kp-1hfmN2uSVOWu<;GxB-~!>c$XJT9{90 zCmZYubqacmo_se&BXSMRbe>Xl(mM^TOo&ohE?jQsWyE7;ND~Sjqt-B=V99-3d-n zJ9fyL|M-~9Y(0e+7NX=&+>Hb}P%FJ%UVcEg{64z{WKo0HsVR(+OQ&SV4(c@3t|VRQ z|IU-c&miET{M~giq)uMrVzDAPVYNl->cmK(sWaoP>o$GqQ5W65Ps7v{ddvj_S*l~> zQ#LQmwk2^1xG$uRQCaQ|jb^N&otxaP-5OvCv{p<^3=F4EIJH2^um*2B7&bYCnKky} zvMGHnt@{p9gyRjEkk28d9vdI`4GYtc_<^2c1BTvoBrtP?(QfM5wZeo64HpfG;8A8& z=fBqak@$8U3@G7jDM%_VFORRy#l=u(U?QAuARr-ZGQj8|+$yE7?wkn_1wY3y$AJ&_ zH*upgfV$#Fb7Nz>D^_o(#Yftl?;-{om{|2CB&*m2cfGrf8yoscIuCA7(Kj!UC#k?R83vT zwCP$p7fM_=%P0FA88L_OP{<1}i2d;J!2?DHB|3cG8+b2jp{@4s-^eGENv*ukZ=bhw z-n?Mowb4ndH>_HRZ#GUaF&$idKW=LMblT~I%<5wq@J{T1s=`t3S;*t(UB*LT>7%BygXOmqt!~)`1rXx*)sc1z9-|I zH7+C)zQf30r@qO!y^8lxQ*e6DiV!KR2ht%xHRJ91jW7UPCm?U!`)EDV zdWjG|6qWivj8C5~3D}%v>*>K38fpt$Iq)J0vvkBJy=%S-S3yYdcaaDS1^?jSN6~O` zSp}Du?IuSLM`Is?)mqMBIw9x-uOkN0$Jbs5oLJ*H3OXpFp#!i=XvlHygCANB)cu&` zQvTN+u+%~X;}*H-u&|o|TuUjGCEeZ0U)p;J2Ss6)I}_8;S|ND2mdQZ_j?=!&_6~$s zBHW;9NHFTxwH$g#I86gP`B1stc=}O9#P+Bh+n99+9L}Hb zcmKoIp4I7LpcB~1D(}a1@S`BT?-kLZd5=of(;J!V`nC=p9}9e5$fr`xOQCF(*81fIGjL(j|LO3@Wf(yBF=3 zeZuPPK`B4*lKG6jmZi2+Vodwk&)dHcYb$+Eana1Re41k^mzgEniRmXdSE89K`RcU8 z#dDc^l2_t}vb2u>G~AS_e1Uh}yIaS?l7Z4+Ar|#_JyrL+xcw#uzOYtm3aHPy{$wh{ z%I-cf%xK89{ryKZu0ompCG=9NS6niWG`Mr)_bU0QHWORM&9t%m`O960G&SEGA5}fI zhKY$V-$HO#ATFSKjw6U-N6@R>$qCXWkyuQR9#Paol9C`vBy-_zQqqp9>S`qLZM7F; z4@3l{KUx&KZ|QXCmA8CGR+C2D^ye_qWV&Yq;p_ z9A+H}qYlWgXSCjpxY6+J2r=b$#+JKadEI2O0}$HAsAh-jtaAO4YwgWvlMES3pL^Gkau6(3{&At z=o`u)nL}*u$YG;+!Z;a>D`aSwtgaw-7s4}Sc-Rh;JPSx3>PPQUzqPcK!i5uR)0^*1 zvHbnPyAy&51S-$jj>%=kM_u12N`X*BLAbZHv?P(?LGtn*l|W=>=dI#inpYK{D0A5C z=&;E*GLR8dERF#ghg60t#v>@c9Mig`{&3ajsCJL|ol;ZQIMFiu2G6V@9X`RkLxMt% zzVO1HI?_VI!$ctR9i(D(?ZllC z_mg52sL9vxvepB=NqEOk7UmFHOg=SvWSZSRe|R^NY!bANV(`c!uw8$EOICO-~1Kbgz&<;kI2tm^z!nOqf?xwellWLf(uhGsC*o7^ZRsRt>A3Y{~t$9FzgdN zyeHl98mU+_uqkFKb&Yb%E2CV}&`C5B`{XtuP)dZ~9bB zxp2;>ntj_k!6ku{YrBQJQ2%2wUK!^KfmTaL+H}XJRrAv4WJ%9X$m!bsUZXh4y{i6q zXo2j+C5KxWrV_<7=(g$#6VzVkwU}M3AT_%4m*?;q2L^eUQdCbLzdYdJ{`_@sMn*;) z9apl{sOs&{WQxHrh?;X=YKD%p*FIKq$3 z(xKv)X#-(m4UP_UtG)pN0sp-j%rL+S&+~$BVIRz2TZ1~iq-I~mO8)kujL1#&r+kL>pM2DT ziiU(a(!8E9OT@xtQ|yPM`@C#U|FlV{YAbMG&porfza{rUL2T_g2n5#bGRmGmX#qc? zVlak-SUhK*?C((sS+4n)Br!t0+lqx?1V;BHG;N=j)mz%RXll=YuVM$GlRz7`R?jP2 zqi*GWx<&>biWi5C`@iSgw*&nZ&TuraUNx>hMq>r-blv_LaK|sFTRAWaTy!N*46$cZ zjukZ~#xbz5>Hqj)$q^|oR5(pn3!{<5y$IzjLmsNASJb5v$5DemWXr{oWOmp&+`K0V z@bldJB-1Av0)4&PDn)Z%pGf58ooZi^oEKJ?nV3}&`>$3`68knka-G{?&R=E?oki0h zaaMw$tKBbUtG1-KS7&%zBqjgBXn)`!I6w1K_NojG$J9LMxcTxYN~&f=*G_up@O}8I zuKoBx0S|B$99A2Dnw=M0Eg@OAa^>^pnQCr0emq?x7YvpYEI3UIkAT6h++uBIRhk#f zO~Bo8x2fp*bap`t*N1>-j%S!pilwF4SMj(y7=Md-wjuVc zCves%A0hcdmG%9#L*r%iDc>d{K7IaNN>kIr+4zP5rLpknpQMi?M@(;`rzz-f%^B@j zvvDhDl!CXs-XA5=4jnc*&!>emD{d<}mZl$N2oh*vLlJ;&-^?m52~zah+qo>v#8^9j)tA2-5Bet2V)f3XO=> z)T}eL$p*9zyY~}weBESsFq^K1WMicDc5xHO{#|?>z2Dd2^_nGWRcbGp21Utogm2s{ z?)rtQJiF`6$QrWGc|(cxlA4V2;-qXAZm#X!_iIiJ%UtE}JV}P1H!U!pX9;>&ez{Se zCHH^z7Vffec$Ro)D{&_NGEq5r`AgbL;rOq#cOV)34A*jw{vc) z|K~{P;k((N%)!qeO~_c)6*v6U|4~rBHU4v?%W7UJi6or zR-GC4TzLm2VROGc^a~{&S?jd$^Ojai%bexC*3EW1#cw8JB!9^ z#eFnI%3b>o>%FDk=YQ?IZT)EW0(!OibbtT^;MT?y9ushOZ+S4c;Q#KK0{qyu7Ue;| zveVMEWsp|&cMpyU9guP5^%avnbbdm}O%*=iYsWggJQ}Arn>-?5eyUx6<>dJ1-`G3odb+1W3(SR6mN6{SnJz+mqm4^{jNVd&&oa^5EPuT_kLK*LtlMQD zFs<^6kbkPUVY+i8-c*d3KUn@6el0j^b`mrIyCQyLa#SCvrsbMqwrPdlgxI$dm2b@+R|f4TX@a2`%HF`%o~n#Hl+VpT_HqCk$aP zvt0~#d8(=7AZqb}#iOg^BUkqv&l4VhR^rpEaU9KF4V@JmS7(OvoQgz2&ThZ4LH3<@ zzleKP?Ez2wXnWefH_2KsX5XMMSMX|1H76E>>VJryf514k?RRZ;Ztib1!(YTi{+YfU zHusS;9UvN$S>2Lp>iI7*QC1JE6{dLRqf9*aF-W>uCY{IXV)n2TyCTWf*;2-J)nHX> z1{Q+h->YUC1`$9NV$xrs`k76{K{+1$*HGlhULXl>X`XHMiRUy|k4DJZ+%M>UxF(m? zV!omJLZ1@Njdyby%CTEh7+>4na=AFr*3wv#n8X_!76E{31DE^Cu&jr5Sx&0`g9+xY z-Y4BE{(f+?JK(o!ycb)D?ytcJ$x^Zd$N~t zE?xQE(j!EQQ@<5(L@+CUS4)s&j&0YM+N8uRi;ACT88vnt+`97&Ag&Z;)sB+3NG*d8 zeila8zPviMF)vHcOg=NGp!?6>!tD7KUU++e2MfV+dl<&@(f?t%+S=Z7Zja|$=SQ39 z_!JIgsm^te2kY*Z|8zFUn`Px@2A_&0Qys{}bA#`=ZQH1uu|DhWU<8ozNJB|`{ru|@ zHv^vJspMJ`XnO2w>IRn~mh}2FiZm>Gjg3BqCh8DHt_V1k&pvsbee#o)*WV9h#||ke zB2O=1VruFqaK*g;^XvSnM)h%UkUK59 z1tt|HM#cW#ef{W5_fs)eSdF2t!R!*gCn*PS}hCJovan@bBjs7J^!- ziYz|(5BjZq2H*P->9chP?k*Q$RNSCLiX-_yaQP#W5{y}r4pI98>*G%&l%O<>PMQ~% zE{rao8BFOu5bSM|wR=!7L6otjY*V8~E%#mJ!wSNCLb(qpsc7%FHLQ&9dY1iA*ye=T zSuV*Z7Kw@u(mLuHxjnWF-_J^z%fP`*(cbEC{0kY%?z{k}2BzOm!Sejx#kcze#=3qx zIC>ulQ8R`U`soA-5{YDXUHCy|e~(E`)X#(SQw!Lj6_o2rYL5n5EBE7Rl8-lZDHgAu zJ?r?1HdXS@k*>r2{CIqmzOGXB%sivi8;8~+mo~CeP)`{rEuQ#5@8{>o0gX;4=4xZuWc#f<@!o%&n<#q(^5{rDFD~?aOnw6uQ^YW|bW8J%b?rhFzUHQ}i3r_o zc~|xAPWj|jS;I1&1jZA%#ku&ACt?043I)dDKGM!4$BOb`740h){p(4jN7=1Sl&tO$ z)tp9?OFc#8^-txTE{|v3deWyL*G`?y(3H*eagL~7ep_my7?EFcbDMeI-rgQ_Ah^3$ z4zF=2$?+Y3gxd@2YuyzKZDOnn%^cF(MmT}vkw`Y?kRPEOtw+)X)5B_2-n zDwuxX^Mp^6Z@S;_^S0k}`AU6pyDZk>sZ9x%d%0i&7iDX2UwyB8y6^96e2XxRjbby( zYgeex77y;8sg)aW_;Yp4W^C>3<^`pssPVA#VZ6nOpK{-;x34cIs%Oi#rj&QREzzGF zS11J)RV=>?dh@f89Ois&YN7xQkmi?Og>RTnJ`fz+CfTY`@d6na-<*%6N&)I2R zzLV79#Mj3A_Fn$!(I0*M&jlf#|J0ngae+g9e_Kv&dZz2wZw+KVhPDjW1IstTeYf3M zpx)#M5B70LuY8mTjXRriT~93^f@ZxRM?^=r&o%P`bNIx*MgXQ$RpKO1itd1*Abrx{(IqF5d6{XAB+G z;q1N6T5~=%30Hh4i-t^u3_%duo7Ymx5cI4Df?!k;;lckV#9|5H8%PeXwVWXc!RYB9 zOj3749rz-li?pVTioLmuyOEO_xg*Ss&&=8#9Etg=*>Y6kiuZP~;>b%$Yso)HLW)b& z6I;E1&~!I63fD_smfrd+^!H#a!~3|A%YAG~B!!a7;Xb2~K@f)IOdJxu#aqKnnU6X> zcZ#;Q{g7Q=B4tLqC(Dh>xU*KvhY?l6OmllPenf%Cn(-WZ{q9}o7bBEa7e~4}TcIGh zw=aiiQo=ge<;(nj?A1%{jMM!zX6~TNly|}jd^tiW2FK&R{G$~L5cjo!pqej{s59!- z&!LuWU!&!%k+}e-GWY7$W`}*m~ zY6ws)Iq+V;e=plxrSQFFxJw!m#p;Mredk=Q+@>mdZ=y=rW?;uC$RHgO?EIKsvG_2D znXhuid%?r~1#9Tt3)#*Fhxs+!*wZ)gP)IS$V{0LkR7CyfU-pViY$7fTQAV)KB!x}c zf#;m1@LW|xtd<^qUawzK$u)h8Ve+2}6JOLk`|IXD@$O+u^dPRz>>l zdj!wunt_;`sR9GFU%&Zj&7=H-A2RGY)7#-Ui~6~CSQd@L?))?IWU%npZi#wtul7`t3+DzhmDbpz~*rL zVN2y74{I)%Z?(kRrel2U7+HdFr1Sn`0|Rp}WTtQYSNO zQ7P4xqz#`P_lUPTPwBGR$JmxnR~U6$&Q%ymN#S5)OMb><8QN$0{zI*J@?w8hs~;Jc zvEs2@zg5n4e}LW0O%_y8K32VLY{t5&c$A-?KXkSp z(dwJSH#a!Qpwnn;W%b7WXes;qcN&hXt|ijIk$hBUt+qLSb^j;b*_k@L^Bc>W!c z{)~5s=gcg2mAi!Xl8zum+=xC1@=L58VWvN@NHrpGoj`1Z{Lc4h**>0>Chcj~SWcAb zw|c21TToRSTUv&p5%YFTOkhs`P?NN@v^0t^X>>azg1#BF`w{TjkwB}X>3oG+)%jA7 zsaY2Kp#$wgQ^PMXCHzD%clJ}*GA@kehU|ANxnp11T^hh}*VFqaqb%|1DoPy|HKGK! zQXGq{d^?_aZ?zNl8|yAP@59;V%_$qoZHt2BJjZH3*4`r}EyW4D|E_?fojE z#-ESxA{TIZol_7^DyV#Owz<1GoI(imSmS)tlN(#uKfjQZeu)2OT0iL`U@Dl@+Zp?8 zL@;+aHf-l?b67P`4tH<4nNM0u>L=I-fB(jQ&u16lxIA8Ye|L3U;Q4neuB?nTub==M zbK>vz_+MO+m5*FzM^-6^2i1{pPboiqP_UlOW+?q___p=djY8yfpvg*Dz{i-H_Iq2B z4g5w~|C{64<7KB2U3&vQt)m#T!FXB~=|^lVtU||)K2uA}*VuILzSq=npIu$y|7~~K zRi?TXlnFN7HHx>`;*@%xGVEPwOzZorM2)n`O8^lqT+C1m(c@U8(``W@JnJu~+0g3F zL@t4lheLGMeLA1L$ieScZ|_1eIvGX97m-m>ph)^ZhBP$^jAaOhW=n={F}0|EbY^fi zFK)6-4HGldZ` zFl0y5cqYDwJD@ulT`Gc&fNC+Gne@sh6GrX3M6a9iYq_wc{Q1IwEk;fsaZZiQ4|;Xj zlF@Nkf98VK6XUj0@nq;iB$~({Mw3bc?E|p#N9>b*dY?qnu)b@ggeeYA3pcUf9`LQW;)HTRS4dexU}*d27UI{C$Q1 z?Jd(R!hMW56AcXwER^=EzH@-mSzM}6wTvOhPl}plpNIl?z%b80`L7ou+`SO~^G118 zjV(HDT5b()^QE?42a-8&yzp>X)$%3Y^!8uBRQUx2x&{Z40vyzP>g`wN_Q9fc$5G32 zA}CqU7aUY+ZvFCiNXmd zW?=14JbZe0O2>-nQTf^Gg8UPqMEG5QoR2jG=aMT>jDK*o~y$cggq_Hc_ z?#G$CQw5*#&E?qt&?1ks%hP)8l9Zv&SB3bKBhuW9+X+Eu1v&jfiiL??x=&`Hy^J}fjDrfPan z!Hz$g&#n4#F?rwVo3lyD^nBCKQ|F}pYDB<;<-74JDhWBRuP^U9Hbmx4QCBnpd+}xO zXLnH;h|A08G5-$3-a`be-V*CdXi+LhrbpYy#f)zDpGBW!(9-16c`@aWLP-U6=c_(pSu(m6;U!UAmQQRy?*;v($LUQ-5xcO z{j&j1rqLHUDH`x(M>yD+{yqFt>SPT~+LRuf5TMwzbo zN}KQB{eR`-nv#-|LBYY{jb>w-THNYEnwv#&NpK<2J5kMKbdGmxr$W!jg}rcmM98t} zJnSCa*oW~J+mkNM#te`0B))E-}4Dk6AssVWSpT;hx7S0P3f7UN{{St6uvZf=NZ zXuWN|cd>5~1`QG_`(wyuA0Pg$ZEb}VYm`$73zL!xxO9Aflc1Or~e1=m99- zSP&;4U%|q zE_~mn7X|#eQI|G)@r~Z5WHR>ZN6SH$G3{}%ifW-!uFP&g0Mw|3Z6?}_B|kjq1aY=( zPWtc|8yo*@cDHw9c`qNij`vmLhbj^P3eO<^SFgks6wp|7>P9Box%2PuVat&Iq|QiK z9fhsKQ(#1~RAm|Gy%QSO)oXL{744;I%}>~_+iVbyOdP0fd=_9mQ}V`aIGL6Ttfz1q zw-pv9=KTD;84AWL9(O5Io}$OCQDWE*UeBtKZS0t`-y(@N*k=E>8iP&R*vLOnCqMo> z_BXY*mct+u`UY-w3P3GWAG@2JjzhyYgvGhJlJM~G0MBA#ic3iWMC_EJ;Lka;HXwlf zu=d80LF9aO&}tn!Mg(DnE(U*l=O+F0^8l}NJ6Wy8Qq_(~GX$8Yew?GvQe)3Sa`b-O z^nrU|bGU;Y_1CXo?B*i`CpSKG^YfiOJIqz^1NXovL|7p$Xr z%Qb3qKFR#rEMg5irmNd;_2`g}fW3iOU+Ay6lQL)11rF7X9asX-|KDu0$|0R`)PfJH7S6#3xq#0tIaz zMVUrIq-zyCv$sJ%2q1>n*Ez|R2LC>FvB)nJcglz!DDY&?}2+>F?j-eWeI|}W<3Xk?(_kY8^ zJQRS-MJ}CaiSvkA0JXs3WK~?q^XxeX2M53F9u6!lEChia31yAw+2XjRH}ach&DS}F|169f z-@j^b+k}BAb{5^|N?pagyaYu2kw;$c%bIfU=l5!^7W=bjiugsP&yrES6hPmQW$BYn z(J9HI{aX(~ta=Tfe4FQ5Rbn$U(II|UT}z9dy*zIypq9z)85B(>5CeYf+mAMf45?o+ zmX|ldzA-r7ZaMt?PUgp*7e_ff)cxajt}v>$hU#i-*cjCChe(fX;_N(JcnL5N8qu`$Cm9K9^2d47p3G;(Fi`p|dB++yF(xKi zaGc2p2#OBXW%oaQ?$%ZsUW6%^tQuNZ_tNFUMX}n=6_sEKEx_S94aYM6*30GCF9ZGc zh#2Ze0^RG)}MqH&F9SRY1q6_IVf0Jcs#|*WlFOl=-ZLZQA7kZ^9&j8A12Pm1W^95~j z)N{hHyWY@u%^bEVKHt)xsqryiKX>E9us^pmNviLcPu4W$on&UxD(#>@Y2=!b9Kuxk z$Z0xkjI59M{3N%!pe&P|&uLHZBTLwR!gbOI<@S}dajrWxkoC8D9i4~umN9~Zhl zW+gIT@w;a=2VP6T*w&Nv?h=zhL0c!%{L6lkZ;$j#QoW_mK3>6bJvs-K`#WU!uyJ;h zXVG&fI$!k)BE|=++g}N+Nc^oFuCQI1mxyBtXPufca~}SRe?!aKOShu_@%t;taBe{4 z6S%70;g?rKZ$xy2AMn=V6f5KX2q%I8&M>?PnJ1X^e|g}}*6UA&BxIaU+6? zp;>K?1}bC*`NSD}G6j#LqvOws2_rLv=g(j8ij$}?Sl+)+QGYQ~(S)2F z=!5@m2Oax^gHxh&Xq|C-=guflM--T>-nF$0UjJa;5%!;#JbX74v(##6Hi7SELuzO> zLwcBo(Y)zy{bPHjLpxKB7n<@aP7$GA_+dLwR!mtN;QP&;fV9sxq4 zq7rb~c~MkcJOzNBmcO_-=<~n8s?8i;9xl3U^r2BvHF;fF+i}v1h-8L^8Jn1dbEdZ6 z-Co#KeYK-)H=R>WT+TENG*jsu!INVnpifUf08|?*?&nf_Ru=jG!+I~uB@yOCwZ*vR z-4`MvBGp=JwKR4k*xj9-KlD1LX9g|<#`St~XV+3{RNo z`_7K#-b60K{^e@tC!bLNlhsZx<*wmwhEi+B4@U6`lbH?+az5)yK<@3KBRjpfpD3?! z2OrgbSP5#nuzibPc0?_5T3D`Nv3|2c1vH4`eHK_THXmK9hjpYs7c)sj+&|uyE^1Fz zO-~h>m+sZrUbt`w-w}Awy`#xe2uoJC5;&~N@VYn@tKNQmo5V=`v(v;4{QhYh#dL0- zw+Z5K!h2Kg*r(yLRu@3pmw_SyLPzvSfRGVt&uea*-ioS#n(3)9up z+XYzuz*AF`fcSF0+Coao<@Vf)my9e%mQBCq8@S^am<2W8jpc}miQfWk&eYVj=T_nM zYYMP)nw&O~clY+2Z(w68rE;3R*3n4^2L)(N z=HIikJNpU_;(3RCEwXguvfeRx9(mEzzGBf*4v(gvXk&eNc);pbFI0pP^}8S7c>u60 z2WVwf&&J~w8>0&s>+QRzr*YBI(XpOw8qpC#h87l~KqfP*mHtYp84_JMRq)=VCk)fm zb2wp1AO|_&Yw}LfZ6v+Y`k8h>36@;?(z zR2WHeTmFQYG|GaKlktE68eu*CgOZyY2Z((?r=~s?7c<$~+Da)EeVv5u09SLmH_fD} zsksWIyeDpyDeN5rq~VQUMJn_v`KadOnNekW%{f4OF^*em_a_I-B0d-|AwAdbFDfA+ z@w3I#8NeGIdd*<37!(wxz|?IoZCs57AEip;-n58Bd_uw-t@AA6Go{#7X1U^3p@M;d z0XF2s6hW>1;U&stN}KeV?(R?AJUqLAi2SOzHvs3y{fGqV`Nx)zc;UQ<;9P};&HeTa z6cbBg45#B`XO9gF5^g6K^&8Q1xa9p%DXiM`y|%tyloLp-r=DJ_>yh+j-_>GQ52iOH zjDDW9xsGKuo@G~T;sgoNiF2WY#U&^=m%7Pi_v1>tKYp67S$1yj zC&0u$84)n+HoV|6A2BR{_3BmS^>dl`?-0VdI@yCN?2J9ZSi^Jlb-XfBqQwP-QsP>h**>tFD?Cn(2?z)bAMS6UfQ}9^z$3o--d&!YoCE;z z7f8;LfI(34^5Th2Xl(-N89yjAlo}jgojP0b;bayLyCr_`^wquTVhXDJ>r=44BpngB zOtEQc5ib?eR)I5e6aO!?z0HdmiRvSsNp?%T`yb8pBT&(|tRnU8^@{XAEvV|(lL$tg zO0JS}7Jicj-+KMaWN)q!4isJe-(-z$)A%p%E)Si*35AeJ5fBk=3I2ZNy!FQ;D!uPZ zJ0*D|${6RL+!Ca_?$9gFxDyf_uYj-@DeEe+o^i?(;DV{Btmi5yF)^iOWF|pn{T}hD ze*@065wp>8tt(i!93L*PRHXoYx!peta2Z4>^~;x^K%f}}3OlIkMenmjTrLl^s%mP! zM-lU3VNL)QCM?WyosrsF&i8=F8lvY zmTU0ejG2;C*`!x+J+<&w433MBmjHb-KBTBBZ?U%*P9 zcax0vO!?@78S)86aHAD|Fs(dnqc5eZ-H{_@7Xu5jfz`&kU!LlWRoWdR0&VsrR}O^_ z!wN!^ELhBj3S=j*)cm} zl1tJA6z6vp)f)C3UuJqV`&gNp88PlI6kt$VDi##yB+y7y!9AnZh{2uswAklWb2*&C zb`#7}K;Oy0^4RJ~~oDmmj z#x4GjWaj4Pg*x@D*0W_%8~rgGfEPctsO)S?=o=8OPR`D{Hiy@n^Z2YLzmq{u4i0k^ zn}>KrPe={gNwy1;cIL|sF%lf9Umnn3Y5q$CtqQ-ltlXR}kBph#$^PrO^#_+km}aTc zFXi8rdsGJUAy6|5sWRZaFWQ!aVC z5c{p-xmd%$aIx4RK;l*pii)8Cx6$C?1u5jk?7Rksu`iZ`Iw?JUea1k83yX+=rTF^t zW@mz$h6ZsP4jh{-go=ua92^~uHj1@tf6#C^yogo(zbpW1 z#K{*capVX|^szf0qcI$fV7Lg+Hz-j3QjDn8V%r-OtF9Jr?0|eU=cVupc1)FD>%TBB zI>$vT;ykm6BH{~%aQ5Tzpr%^#DP`=fk+o-If@f!UXM=o}UAk+?!|d`%IBW5>6yLw! zzXam@*DiC#gSXmhK}H16sBSqx=d8QDyd-P9zGEuj_>mKiul7+z8$Fszrm~#O*`W{_ z_Ig6a9$#Po;fZW1QW9xveL-23otc>tcy(iR-LtXRPeQ`@8W3k7r&rhRH^%c~yvrK+ z5|=oyYvFfuEpCZ?0f+z)->dXyjhSiQrPn+jhkYTJW-mNGmk8JRT&XxGe6Ik+Wsq3V zZnyf4#o=y7TS#xbTtfzR{CjX-YG+#No4h%2)pcov6XUm9Ju2--M8{g-i@wDi4dLOy z)h=4DI(@N^M}iZQ>MXMnv9AY-`P{R86?9gR-wK)b?j2h`7L`4JpZ#jK_wU0q2sF@6 z@Oz<-GwDkxz(khSaM3(_z5No8H-QABX*FkaWvbM5M8u z#NYM%ICqa9|0-Oz59H5WvP8drgK78S3=j(Me_OSo4LeRK1z!)93d)e6)rBO46VD7I z`}y8o0jMHDo4<<7{?wy+#AIFff*&kVLu8Z>!fpk_3YHS!LK0`M%i9x&lbr zk#KE(?y_dx$gf|&mYT0x05K5pB4xrE7u1^46WTv8X}^wRjzqQCpX z?@GUaTp7xzU0Z29PMm(#$t3F7i;9ZaSyE)zMKR35D!2oSwSl}HX;`STXk-|b|t!e zDsLD*_9!Ri?!rm&2_O#s)2?m;S%V|ctjls-q=pj=F6_s zMI|Otprc2lvg*1SE~-2jv`K5GUBouLOib?^=**`2<37eSi`ff4`pUoO#Vz`orXfE0 zQ*KyxWa>Mr%m6lCnKn>`YY~1X+?CA^Jx; zBsg{5(PZk1jS-#}48-6!D}>jW*fDIKYYtaiM*D-sUurk$-|71Yh3)VQUEXLaIx-*q z7UCwZ#(@lltvMccU%^3&mBY{FHS5XiGjG3bHf%a2q$J(xx6L`NBX7--%Qn`TK+y1I zMsL_=GX!N6luE)H1OSIC+vl#*DJi1^e|5A#>}$(Im@j-8hW_f(AsrMj+8^xPWH022 z*5?(bYxYcR(7b!@G2sw>YwxP@*rI5iwZ~n=AR@ig>%eV0OtyfPMf>v@7sk(a@4vVT z#Fxo?@S9S8?&HI8f3Gr)qW?MGFV;Wv8vh-%-G6v;l0=@_b{C{1yl8wrB>Z!V;nUQ< z#UR6=L?(H?M3~5_Hst(ENnop*>9db~A`W3-+HA|Uyn*`fp zR70IJ62U$tf2$wf(UDFd3tbrn|GGu?K-<{~t<05msLp%e3Wr$+mk+-o?Ahb~$ELcT z9FBh{4D_gWWRs&r2_csW>KkiJqVhyP32#aJvmG;t2qtpTbje#f3iUsnZkbSgwG&Mi z7{BTHJhqr}@Tb<6moc z5klh!b~id+#Le~YB`s31-(&ul<$7;)mz#9)LdeGr52(+WWS7q>2kjyHe$6XD{<;<9 z?Iz!@CHIScZ{>6@e~HHt^uVgG0df$Sy4%&Gi3wi%a)DQ>xKv z^GOj0I?OYGFO%f#`lCrpf#bCqU;iFGx{I1|g1F5fNCw8PLHPOcG973$!7~b+Ufn7x zQ#9Hy!=nbawRblC;DFQ-P29H(m9A=MtGtL+he-j({HbU#7^Dtmm~V|!{y7#V(FZ<0pfbJ6aO(~llvD(9IH+U$ec%(u8WI@o>C*n< ziZAGYxAiQIw?f6CsRg~*$e}8|2tqL4FppO|J<)BqQhlK%N+eZMrOEfD# zw(94~paCDxXzc5&;3OCHS5v^~V@Lf2A~G-ob31Q!bcg}p5-U>9b6D+wnOj(h2p05E z)RZ%?oy?9HNo%r8lTaqa!ukkg0z*JLUdzh|)z|akvFW2i1h1TEn3;pfpD6oV;#$q} z-(cy1G)~NGy6knn6BtkXHV34!pa2Gypz8Q1{p8`_-IG(*;C@VoiiXBv9_cWnc(_mt zcm*u*p}{$meEhboLKh5a7i^?oJXpWjEg$_PGQd_NFjxs-eZM7FU)?Ifp0T06y+U4DNvd)}+W z#Fd{Dma*HTz{Few`dA_0UUhcM5g<6$-PZ>Xq*N#XlUio=e0S~QAv z?*#?h#X5r4wBnYe8x0(hEm$sbYn_V(r~FX*59z|`3ZFi&9vrYx4u z`@s~MNsjL>R7E@ZgpYwGYdjRh=j5wkdhPQwT+I`SAR({S3)mNo#UMeTtTLrZy+{@8 zdpd%!u#m>@dyfTt8?@@Qd`>zEedP3{EK&b15Hq{GLMf&KK$u=foqu|4bQBHjS4ki| z`uO;q0NM3mu^t0D0a>+%#>O?E!14Rua^K(II{;0W1f&gWt*4E>+h3_-fCK#kGmQuP zax9vF0~Tb-D5y#d+Vw}ui;7M%N;S&KfWwRgv2$^CPvpv;{=0L(zPsz5o6{UAubKe) z6mw^si!f>0Hu+I|j2WQ4DqoBY#+3-yL*Hnd=F0R0fqv|~jZO@BMa;_m1Biou34WEs_+Z z)=}U|-rKs7@HUONH(IsLs`%G@n2>GDO7Hv|#P+1H85o+IzjR(ut@ZK&MHw8Ea1hf#y<6)>0IDP1dqN+7p6k51NN*%b}UpFxevIBq~PjiO<&HHr=G z+6v3eBm-AHw>J%nZznq{=mw!cRDl(1!+lbSp`g~?s(jfv`84iuwKOd(BQJD7JJ*E$Nw<6;)0W zlLq=&H@S|m`iocUF+k7;vWE=Lix*;_;L)fcQxg+Okd`SSS5gyZq7nr*g_M+PkuvSs z`8m#C6`UXaCjHT7kHA?`eEk}+x4(aCsnJ>MN+F$h0-Q&yp+p95p!eK9{1bf2@B+Qn za;d@b6|MM>L0l&p%44A{kDBcc>;9H|2EA9#@&t-rAO+@jb5g)5r!$8Gm&YRY;FZ^U zv$|7#52#%UhdQXO`8lTG3e9U3y3lT3iyIr~%)w$|f?y}TUXzQRI2Xp>q(a5)kKw<% z%%mhTnee53r61O*Wl2a$2WMwfK*ZGQ`eZE;NGjps;RhEWKmrsRkn$Z^Spg|l5x@H{ zCO*)%XM7V-Jr6E`?7jwYiDr`vLmH0_JWvv7^VQoB zpTkN?OB;chV1xgoNKP)-2%{S4D3YP*!Jj@o14)G!1O$aGL3rX15qp*KYd`uU#Pe)1 zm=eCGY%--%nUs9pC|}E2e87I@^`5Zwz~xWy|FCqZgwlD7u@*gV-w+>ZbC3o(tYU4y zX^_-IPcxf%!|5NMC-_)dd}Unzm*sh|?BKL!V;H#Tbs+VD410gGdsn!Sp!<=OoE&l( zA`ty#Kw3+KCnShSv6v5l1 zNkdp_q4i=7>xBJyHM6);O;0~qhHe+exCkPqZGjvd>HNliG-ebPE9sW2JFL067W=5K zWNH@UzM;RwueJJyZhZWHbip~}=@s&CU4Mx5x>g zxEcaMoPfs~2PCTjtZ+R3Ev#CuPYSUcc0fWt*Dt7LB0GRS`7Jv;5S>(jfJ8~vpdYyI{Q9b0V=ZMiY^~DhWM;WG z`346B1}8phfU6FE@R6_=)hl4CN5{kUP)6Ua{X@-X?Y#{zq`= zr1=I2WM(>vc7pc0Ht|La*)1`)t^J#>!$u%{cW~2hFY$T%f*2W}BZWEj@rD32W1w;v zj>g(8zFg{IVrGtH3AFw2;S{u0duCPo>Lc$Bp#U4LrAW+(k z_k{7#|J-RaTgHMv4-kV4=&Db-s>DPbkWH_ceR?0k>8UnXcyfxM6VTWj2h_Mhm=O^T z2?6@*R2Ld-AeF3lbL2`*8!XV6SVYEG#0R^d( zO4Vi1XQ?m&r>E)H%#53uImiaL83Bk71pp`1@K`?^cW}hn&;9uK=mLbL*Y3KH2jYbA z5CFHCUr6E_XLEW$;7&0|3aLc9Ho|qW+9Ku)xsHy`4E!8u?zm|po)ZH@u-oMay|vA< zhMIX)K)d-_;7@n(o2|t@u4``(34c93jj-CtVtjl@lAVJ`BBSXgflv({ubmzQrY&A~ z?Vgo6gcv0XFodsRzn7Ohvzb&1;tcWU>}HST{l~$VFt3Ul({FOfU}1iFF#lu~nU8!4 z1Qw=I=SMhp4vx+z3k851-F81xVeiWxFaa>p>V0Lq)~RLkTu~^*$BC*QEelsXdSSkK z__@&O$XuTHP{U?}rvg9X@1l#>E;gcd9bMAqYRCa|-NU*PZ-@NaL`)yu15Z@y@$_8S35sA!%oh`bou#TiXK z+2Jn7)94_s&{xT&;{doh4eguuerE{@SkN8eLyTS!6MqJg%fU3aJW(MvT41t4FUZMb zz-SCac$rppJAA(;4_E?d8sH$6ehI>(N*d-1%{sOKqfwR%Vsq530QEy1T{XnKC1Z9z zqHL<-MlfQdOW&J&gxOxcCl5JmBCKxky0pT8c9u$UBO}qW%k*KJ@`&Fcr!hTwQ-I|> zSyi?RHQ~!;l5)uRC($IYlz-BzM!Ov@z(9=ZC5TiKfG2C_TB{YSk8b&cbj86Vm~xQN z(<7~`s{>-ZviwJsxRW`HLakv z)p71V;wuvJy5nC$-tJngKDRGE!n-!%sW&-E9<06L%uZ|6JNok{Xr@$W1B^y!w)+X| zbUg*;tId(N3F&BQJAq3Ma;e?Gm*()fb{I+L!vM;FgTp{wDcx+o^r`jKkN7TF|AiW1 zfhe1l7g=A91(d9<)8y!F->H`A5(fNyZj~z&Rbki(3wR_YD=Rv%KM1%jUqEYi)>1)$ zNLIEEvS`;Bx{uoc$Ixi$nJlKnG!EWCm7yUh9nFs!{2nr_52t5V&_d0Q+78liL~U*D zf5wW|a$~Cxj-(^Am70=KAaiT~)XTE8B9Qd&_+948c?d5QVE4Wp>CbMXnZkD$ zU~dQ|#f7x&%$H}J7fYW75+$+21Vt22Vv{5e7;yZd#z^C{*FWmy>Tmd3_846r;k4<3 zlS%`Lf&PnHuGfs0*=?4Gb`u1Oz1QugGYSgF@Z5 zBC2>A_-^(+sl=CSlLqBA?fwPIjqw+a=DIH<{&iocUUTw@BBMZHT#bTN`gBGoyl{@l zicL#711Z|8YgBMts#0BhvC+h~rlwZzHMWdoE{*=35L*e&rc7li79m06tk1s(>wv1M zY(^ZLyk*n&9pkOPR@fSy0D1~_Pg}(Oj(b#XMI{hedR zyXMVdgN&y9>GIzq%**fe{26++t*Xy9RVYGG_rpKkjpbMrIL_7hiGnf58aqy!@d`i! z046~I(niAJ0;mp#APvoC(AF_CgBS2qJA{Ua+cE&8L`!rVIq8%biyNv7$`K&cBfe*k z)q``sCk-3YU(#J9bDJb2t(O;Kq*Lm09rtgkLCj#?pSSu2ut0#&mm}N%wO(d-sn%?9 z^z!rPLjvB1E!U_>3Apm?cEu`6oioeSCHuecxL(P zrP}8pk_7l9Q4+IOJm`@{pn4r#fZ-v)0dm1~k_u>+;bHV3ivvg`6JAVG5^fTkK_-|p zlmxIPUsucye^X&5&y!LqX5Io5^YPGGVRJ_&Ld=tPo4KwL~!JJ;^`F6Rx2|AL1) zzK1W$dY!Do2nh>gVp87Y0qOxrd-dbg)D#Q^0H0~8sD6V33^aGBh)%)}iOrer{%#cn zdJk>BFX+r}Kp1MI2<`vIelTNRZ5=M6M>m9{;h5#1WSeyP^LO<&O!h~9y`^xrtoAkU zbCv5*aQcK+ZOjgcBdwLcec(0vg}&Ej2wLnoaL_59P2OEqhYAw)E;5 z1saP`?`bRuVto*!MUzVWkd6fAF99AOX0pQa1e9$Ns17^>UlS#8zaUvH*3{d3e5*0H zT58=4U*r>3ny-Kr@kJDyCHwHRRF-(DEo(TupNw^c@hA_6zo7<4#l*Z%_*T&i|IVnF;ZhFe&tslqNEa67 z5MboCDT+?W>7?(h9>K7!4E9!JeTuzcUZ z9YT8~rTqTk3TRnK;m&Y)IMjp4#6(5qvweCAeI=oWcy(8XI@*J-S30(1B*Ks4Pi}mK z?=c4m_#hz2nuB)8Ae><+_(9FP{}Vn+OH*tysk6h=!&F?a_4y^4?lEfsInt`E4d191 zm5pvBI0f0s$vndYT!A3&3?9WqW9Xtl36RY+a;0k6Imj~F{fV7j@Z#(FJ%;|7e-kZL0{ zrKGU^mUd7ifJ}7+7?7H4(dXLfx?im1U)(ugw3}^E&OOr2m+AWbVfcLi{ljpd zwOTvzbofXmOnctXlt$>Iw44l9(6{=2rXKs+^6}3foL`(rK`3K13uk_5;g~muE@K)yT;_IZ>x_^9xs=R(&%fsPj*mwwC3=;_(8=asK2_Eg5~-j=9TKH- z^BCK!<<@RZmt%H3a?$5vI-|<#)w_Vn!C$2Zu6|?Z<~@D}5r|mwHq{Y-n)~7rdAlM6 zitID!$q5+ROy2-S*H*Wl{8n3?y>tQQ*wf%!uV#hJJ5bXT8I&-xMBu-HD!=o~iv}{> zyT)}8_D@e@@lwA!7BTbB^^@8-4yCEukr6W`~POfC)j+ zy25^zVdmd_0-wk6HU@|=ojM|wG)K!kTWWbJLcTc{hXRJ?7#9DT^@7zc(XAo{V@qIB zFh(*O#f{UXcAu^dL>yiZyg`p{DbjpnFubuDt_gEb4|+qN0?|+ORo=!wvDii&%@O#$OFIbNucqNbN!*;f2TNa!b|jL@LueD8IS8klqgcFoiiJZBj*pgg>KSF3=s#3T zpMUn^nW%I|skFJUx#($l^NGWGhzJt9dpBC$`)^DtTj>A>7)VX}&>*Npr;%9nMq)#{TlS*&q8Q9}mFLd+>a}lwv9M8dvCml< z)6>ZYIg+o!uDEKCMX2?|R~)Weh`-o*BnIR6<}-VNOwX8wcJ>6lQ&JHJENYm>6o_JROl!=d++;JJ6fxpF*2A9)3mfh!i?$F2{-Ok!Ob);^fw@~Aexed{=V=ki zrNkRx7|3$Q0eZsOgb5nep52{FFTmVSh3j?%LM|P8?W%SSiIb%2T+hq)LATM}8e;Hg z9F;y4*Q@vVhwNx;lVs~xZH*5Vv7@?jQ!7|%`; zV4mJCx5Ei|@HZ9Mizdk)t5e~?wnr+m|C=$xu>rq&^56_)e$Mmr;f)DrI-}x?diLlk z*2Sk;y&4-4RK100nOQfJISF+v$&d>v#qS6pxpMk@TvXN{7z8_Wd2o!0m3>c_4UTzI zJ$gpkM(1*YW~~dt9#3IZ`N}fb$rRV2a(2JRl!C7BcNf8-laK^`Cexg{7CZFoZ|8(; z{=^8dPZ+pDBv-`niDv~m_Dh-ew7*oAu7B;q*;bczZ1+b;SK9zyN6foY*}BnP0gay^ z;shSA2g7sAWa%E8ZFT+boszQwDFqMCxGzU<_^;ZLnh#^%v}OqZ-!n`lRDU8r zAKaaK(9@qC1i`S6J^i>f|7$F%h6MS~G4lplM9RMXuZz$NHMrJr{+B_(vYoFooBs%h za`5Lx7GA5%yF}Rq)u;vopPd@wgpEt1Ks$8R<)314Z zVdXxn@8Lrtm0zY^`sF$*h`4>D?djmQPx-`GhNkAFRu(@@+#8mN1mJNSh3gfFx2fD$ zzR#l5q?V~Y-D0%3q)nx^hd-J+Oaj_#(K`!AUcdBdV~YVf0`X-3^joz|m)>ex?ab|f za5_Ouec$q5O4;*XWGs4cA@FsfIqEc|PaC-IX{yX4mRL?!Y~HjRWA*e~n5V9Est@ap z`~L;>dvMAGuM0gDn)0ZV=4zO{)}sHGFq&@Hp{%^T9?M%MjNFj^`z|~CU%WJYy7~6o zFZxpW$`Qo${1rN4c{x@|U1NI-nc1fO=|cY+zm(&w<3`lvvNeKa8<7waACr__Ym4w{ z{84SYJ*P|6;Bqtn^V^o!3-o+_b?rA1WXUh!hTqgxf1_Di5_jq7CI@~DHsI>J8j z_6EXjcaDa%blXfvr~T6|qO{5k7x|n4h;#tvUt=5$A?=k%^UneL7!o0W{BF0Bc zC-{F=`Z&TgIT(h^KVqYujkV9OKG6KB{sGvu3UTBk5U1uQdF`1EF4cclQ`5&59WgEs zR=s}!_o)f4w4zBW^R@+)DpPb zK48^_^^A51zv-gYc6YA}gc?%SHz*Qf{w7mkAa$2^sf`=tCzTfH2zgp(n7&fB?`Q7I zKxZ5LPtos69WFFfHh3u76E~z-!()s*KdB94ARU*)GfT1jE23{n(0Mf%@7`4z<%-nm zQjKa+@Kx0%DH56qOE0F(nvbg-vsu zRP*X4-` zB_ogVzt^ZphqAJ30?T!{9(B(=DekEzyS4Hb;f?DUl;~(EgMA*_EO($#JH=OorsVgkybD3GeYH(c)>X1Tf(8CT- zU0Y4;F8|fWSz9=-UV5O8HXA{{e`^#Cxic#&=7s(6Tu9fXsWsahY)Tm3ZS9x+Q<_1l zWpC_``04B<6bY}8l49g`pzoA-HW+2wcRv3-Y4lDvI2_-}K(F>jXfaEu29*S%bg6{m zH@}6G3xRvKA9Al8oQmvn^9q}8B)1p+ep}Kwx~w5D|DD}1mK+buPsvHp&U5V*(X}iV z6Lxti^V-G5w-seyxgN{p8OZcpc($5Ny3;i=9i1g)qAyUgRqO0P^hzc$sr)^h;qwBR z^mj8v$CJoW1Yh^mcQ99wtU+docFetaQoxvghJ=Q?W$03N+fhoXh`p`Yn$CZ1VX=I4 z4?ds$dgNRFFZGCd`}M(;h4 zY7h05-VDS`L)&Fi8yK++YEh>!yw^zE=pW&BYyH@Kq1tRy#;mZSW8Wej@pisVt5_{_ zHBHFw8e(g9DY){ws=Ypnz|7b*e4NtiwmU^IFLOy|gxXvGrc-PL^=VCIb>sGNkKa|Y znwm;{Xr2p)UFXV9t1tWl4>q-`^*G|s{-mw@>R&!xk^zg&S{NfJa77wCaA*_m3J8z` z?~7vQj*h1R?!0~bHX87s4-E|Hpwjz*5yE)r!xQXl*L)%)2~PG_F;rDmi}V^Pfz$Pc zFdY3?W(6*_m()7XyrA%{vU02Yv}jQl8D;ferFeBpV||Y z_;qY;y%O_0+S|Bol-=ovSDZr7ks4XeZ5!nxZEDtY)!)6z&zf3hZfZVOW6kU;Xim1r z)-b0$>%z4vEoORJcWVxh?%0&X-Fox)uNhNI_EGgj{lmyL!7$u)2_Jhh8{%@L%Zu?0AxHr=cB)b<0_#D$jEmvx6 z-7n54fVT_y$nf9+O3URR9xk6y0VXz;Vy$@b>o_>R>FIi(_sRAL%9cfG@xG)44g}X9 zbM7+R|62+8$q+b(9~To{{2^L!cI&q2;H($l%Ghh{C9Y%y85rnOl;~o@ANY7IQKa#O z`Xq4fwl<)|!^Qa+2=}EvNnhF*9A#6yZttmB+rJ$qwp3Rq>&8n{%dWrv8}ojd^1(a( zT6}VD1i-Lv@bqC%y;Qd**Q^xr7=g%mO4Ty?x z5L*X_cF-C@uRq-^3wjnpVq$b8iQk6iKqFP4g*>Xf`Ng|fd?0COj3Xn85$e=@xJ;&Cn7?1etOsq0(=w^i6XF8Y5$`QywNZQ z8s9o2EtwDf2Fi?Oi*#7^Xtjalx^FRx2eJXg8wjR{GBQ{Z5fM@GWM1=CW`pQzYHBWf zD?}iUK@e0%MZMDow^2uMOcTD@NSgC?7rG;NwfsYj6QaCEO;*jqwNc^sqP4(`Umi z_&L}44#Y6fK)Tlg9sk0}h~DcDL}6H#P(yZ@y}kXy`g&gmFE@AQ<;7{F_~`I(91Dth zeERrtCgp?IghYdI=fG&Pw=#I&YW%*~G^h`b&*3?vr(dXOblqi=9J3Gn`0*p(;(#?m znaiqe2cD&bN3|lmz)8P|`|N_2DJ57j0rQwZ$v^eaFB=GgApDH6QLFMbEGO&5*w3G_ zAaWxzX}AS9F`g3_6ANny7N>lefsIX0N(#ggcj&X>-`nP(Bv|exA;PD)cZaJCWEcDi z_O45VA^mrzPA8mxzZ%5s*VpY+)@Nbq`C0o;_?-@chy34!!}80oWtz@!v$;U*#iHAh5~ z*QMTvFD6UCiK##Smzyd*c;f*GSUNpIEDb{wSjYAG^UU5QK8appZQ-9s`?Bejgex2iF zz<32OBhfWhrd7e;9HsE5Rp}YV6XGp-dg2%Ef8~T+{`g4U`S3LU1FO&ug(y*7bg=*Z zw^JIS3LZ})_0t%Lu04@d4u`M!OF8mb>X8<}q#=(VyWu-Nfe&gNU!)6dNY5-1G*{{4 zAU7tgDAT4!JT89*nLT}aSMO{u`jklkPo3+P0?bK(;zE*c4H_S;8qx=zj$r--CSrLT54T=Tn15Eu}EiO>PB1P{Bdz5U#7k!Cp- z_*r0nLl6`yi-CpLQfAyeP-&r#MEd^D`a3HGvLFlyJ_BeJae+Y z=K9Kro7^OQW1=_WeQrz)eZ`v!LO}BlZY+@tyNh9+o!_KfJr6J^NoXGsIIT7ODdRCX zK1_Knn^vRA6wA%|RuERDkm1rF9y6=g;qeC(Sp_Tz^7zRU!y$pUc$MkbemP9gv9b9* zw+hTIe5fGiF+5QF`0)`MeOtk8r-uIE{Z9uieIIDObn`ia>^7brU^OTy4T@jkUZ7lk zQ*vxU*&H$Il>O$ELpWjTg|cNt&z6Gi=AP=_tpPHO(`SKewZ+>vn*x<`uu50O@%Q&% zOUmn#nJzKG%T9r`G=z{fq`&V}TuWAb@uH*i(%P^$qH~2`LZtg>)%c1U*L287)p z0<;wJYRR8n9A0a)aSz(s+M51?quK0SY3xk}q%8R+B&dCMMQzTo_DdflCX`9je&1? z0`a0sN=h{@JM?Y`>(`+9uoz{+$5TuUb>Mg$&TzkY=+OLD#|{8(hQD;mBd76M)5 zw)v|c!HpO11?;j}Qgp0W2JM2M>OH;D!&D$4AFuWgo!p+bP0Wk!sZVxu=R-tNCE6;@ zdC4B>w!4RegA)*X_!da_p08h9TU*g!AL@W2?%LZ50VYOZ6h)c%QKSagqYc3>N-pLu zz|PKYV9u&{FE=x@o|l*LtQ;KQFO|_-;Bzj^w2uND}t9MCdZeJpemHq6Jw|fNlP0uk4{Xy zNlHdGSY@RR*67q~AuMkYraeAmqz{&&FZlRz4y8f#W|T{iP*N(Zv6fCYY^%x5gO!3r zz3?9j7nXiFk6+FRP(;=THE36wTgO;*S;G4z={IhrSZjamNS96x)~iZ|bdls@hU2uO zpSs+kQuNmXbO~{I9of0KB2lVmQ0bwhES$P^{@`qv)YqqlP54nHjc8_3Q5bY9#%=!B zk_7DNA4y6Mg6kSWgJz*Lp=6q1ZvOM<4@!3g?FY(Pj*=?d+usYuh8afE)|MOob`Vfb z^N=5a$`%4{kO-vKuw?BJDG5rSP6$NX`L`&i)96M9;?kpki@31#u=XUV>NR^tY6nNf*}vLHqZ#s)2m z4Dd>~nVGMG#TLB4Xh6oI1lQnF$Cs9tPD)9c1F<$r;e4G|EDSiP+hSrga0wuhrTIHb zCR-1x5m68xhC%m)dW|6K0*XigaDAvWP3rgi)V(^~rV6eULVDI)_szcF%lEz7_~ZKv z_7Z&L>Wmk%hYS}>q_&o*IfJ6YOh5QkV_mH~SK6>BVsT+1IQX%wY*27VZ(zEJ>m#_b z+u3*){~RqG=(Dl#DG7;`px^`t_k8~z9vrNqsu~j9Gcp362ap3klahis2PXP#kS;7sJb@krg#JBhQdeJ>g*f@X)CLsU@|zC zW#Es?yW+0@{Nb^NXiAucz`e}&QMaBCetyNK`u0@~>O?G8P3lXut#x#(W-a293A1Xk z>Vw6~#osw5F9YTUTu&&@Jm5b51hTU}H){WUP;|VlB`ixL@<2U_N9CBIgvLsfLG-A8 ztxRr{;c7qU-rZ>TPC1j8CLpo?fz-6!)x`2^H8|6B5p_9uib#HDiDPde-LQEmC4O8O*Q&rmL2WPUq#2jz*PxUa)(evQ$gq5rSF#Z zri$>uqw9h~@T0`>ZZ9l^$IjI=g&W;!>2Br7z^s?w{_c->ucT~~IL)C%l1gY>VURXJ z5CPj61Zf!@1Gra-DIqa8Nznc3M^?Ww6IkJQ8O zZ!a%AJOxufetezqMc8Q_LBJ_H3+viov+W3?*kIu9jN;Ojl%R!DT6&`Y>lO{?fkc(- zuBnLE1r>xvX_OgZmZr+Q(~rTF`nWc&kxh*Pz(BaNQtbuax*&iH_9J^`tzw>GDn3213)5eiN@E0FlX3ZO&w)9)9SU5Y00B+J#FH=?5i zB8*Sw_*oSrof3P$En1DIJpYuh&L)dn9!X12xG#v$k)lniS0^5)A2gB3q3uMB$VAdu zEzHf$so#BM($?d~6S%Ug-%n$dCX@N~t<8r=_#cgD+&gi=es{ZqNNLG?cNr~-B9 zg;Rt;x#ta?Kh{Q$TX-0Bjh@Oxj@A~eRUZsc=`}Dyp&}Dd_7fOIyypcPTz1eQ9Tjd= z7nCZ(n>U$v?rjd`;vy}>!=!K%g2KX{C5L*yh@KI+F69r}QhJuhRMLH|tv-i0MLbR% zhd-*~!c6%HB#NNiMx`klX zcXw}}(0H?3)%>AggkP|KAyHXm>?gL=_};?wxjX98^;V>C_ zDCICkdEcrUzEHuTwo`usj0z6lYlZUNJp1BykxEtcgG_-z2@(=_PX&Tfl{`< z!2Y0>pfUX~O`l)h&ws7j;d3>t=#_79R2u!}PJFxu8eIN2`^{gG*?Tn4g;t;5AUw4+ zJ^TGZTmhgMc#i*d!-I@L=QKeK?54N*EG%~)zlv>Egzg^tlE+WtFNuO7;|Yej1v%;S zv>c9%^sG7Qf+5c+t+;;N{;gor8o=clr+Ufa%@ zQ~csB&|;mfg1GyMgyx)xhi3dBWx03nh06FB?he%YLjW6#6%RRM$M$Ude~u$|lPBM} zIJEez(-3hXTg&$3p@hXqrpZ-1oo~1&Bhg=A$_#~60aiit#LREMwH%oU# zsDDO=K6Y6qIwsEDZ8m6$ncz|XQdyY`G|B)BvPUz>($do2-m3D@=qT(gur>Vjx;&Si znwo++_!&Gx%r-VQl%j6;iHKNPSfT*bx^@K}vR6FM1ZwN*h5)-X2mR&$%-EE|PVBn1 z_5tbQUe8hH1`r8D%{q=r#3W;JQS{MY9j{~~BJ#Mrcn?D65EcfBjvBjp*bddy5@3IG zT7N7hb?@FiDJgfv8@dD%qUi8&{Qp7~l`JjUiHO3f1Y?5l`QG?&l@}!gBqEYe6X2Qt za#6&bn2FBRh3VmyP-zvOGdY(wt{=_Minx&3b1?0qZ?6vws7U%gwj8W|dBLR9=$@$X z^Jkfag|bBjFRgzioAaXfSFh8Z3IlyiMkz3|gf=|K!0;PKR@wq^op<~CdJ-TYF$V-Q z-2D9A0O1aRx2D4KH!*nhxA*s@W0~X?n>{@cqxo>yB-?s>aZwt2C^C$HV-gd)FE0+o z3qHNePhMD904ysKHrw_;f2gR1ouu5|ML=T%a037EXWd(`ex&(;2gxSu8;hs)bW=FQ z+@Q)RV2iELz`HP$04QYZ~e=e zY0h0M#l_X)7VjF>+Vf|m6GpJvCRn{sR+&ihVPCW7X9-{N9S+=#J@ zwmq0fEkSyRad&B+!&6*rpRqH)=?+Zoe1fOx_9sgl4%HI+VXJ57htpD0aY;#!0lLT! z4#tMA{l-Xv64*Sfry8C}N!d&_pd1ED&>o;#5>gtP=TcH2#+JHsVZSk4NP=>=04gFq zJ3H&HQ3ZY9K#LE?*P)>pXwr{Lrm_v-FTM@gH^Uvy;jkc;jZ~A9lbaK`bI0Fl^ACJ` z*a=B*-js!@4jy$_TxF2aX8eL`h)Rb7ObeCgBqasLs66P6iIrp`x2L@>bD^Ca0{NY% zYzO_uV8R2%7MhJg<N)ndpRsKrm>w{rYtigoIJi(LV4tL1YeCJAA;%ahskV9VV-lfh-Jx2*0#6 zDhMdP0!D}i_haI4$_H)GA4jL9;en(Za-(Kom6Jf?2hS8LKL+9@4Y#Hm8{QZzm4OAa z5c(*GJR_XgMAz?zLXWZt;oTREBh+51tEnMf5rSD_&g1&4z<{r8TE+!?)3@o;9n_SS zB>_}#v7;g;At9oo!o$G8xNkmm2jcRLZR0=J#u%rSo#3^Hpu#1>u;u~gj?Xgj%G zwhc5iHCrKG96&6IX3z6G^s(TFkeuV{wks_w^Z8;Qs1OZ!ck93ap>qCnX?QdZ3=Mr| z+k@Ic#0titBmiGZUh?MT=U)ZxsQ|zzZ}4=X48Aaw8A2>QIiDqYx+qEiB}9)I^hB+T zD)W$sh00YsNq%CpmC=y~R;xpwGP!%0jC6r<={oyc=MP)vO^(N1p2X2nT@jb5{cCoI z+|APkc9NcA52YTxP*!KWtX5w=XrFga=McV;ShHvcS5*K&rIgft(2xIx7Xk*9cYAw# zkCl~U!5AnDAj&Yb&qj%i?;)Z#{r1;mAwF5{>#Xn8y5Xh;eD?c3xa1Xa;9ODKVWb6Z4fJdz?(zEf6Cgp)M-u}ArLs7RknKf}YRq{Xc!ETMa!5kCp|3Nj! z&1Xp)x9*(RHOc(>VRp9F3xs>x`rO;G^VaeCH6(KyDT`k!+?{B(qZ)T6?Qgc4to2tP zDjt9NY{N~I#QB?V8wdyGd6S<5FSr*6A;$@gskKu5S5n@KyXAAN?NBD9*X=tV9L zRM`+1%Ne-23E?)%z-~tfja|Pn88d|S^rP8AgRrT;e$zP46%>^S0+qKR*9sh9Xu-0a zF|o0?IXJ@Mffut(lb}QB(wz`*xT|Q0+uvl7-Pz8?=6E)d*i$+5{i51Bes3c6;vbq~QInNE*g-J8dHMM-tqAU}wict!z~+vE z>Yx|8GsHkoKeM$J3AD!!S}B)qUB~h<3LDXH>Lb8}!S{+nda!sA%xT!*jYC?XM>(!= zWIl&p3F5Xf5pbNKUhvr9P)3ygy2 ziZ~As5o9|TXezJkIY6I^!iU$O0^ftQ$1)hlQ3hh5TLy-Ppbp$T5&^|(Ab>`22m%@k z{xedKKR+$sY8Us}9+O49soy4|A#=)|Kbuy28Kv)BK~_cw1APod9jh%P0sXdJTmpHB zve!yp>(*3a2&;d2ol)AP{ld}wqSWEpbpwgPHU+AUZ!YhDv{9kMD zatXTj{mt$(?Ik~|#?xCPYk92RxAR*lo=n%yE*=ZVH@hF)coDKs=WQ6E^u^<3Z*V~% zqPHZ*gqu9y?$2Bh=MFfkPi1C2HdlYuA zba)xKIFiZnx@g0y4{@{5Uwue%{vqy{lh1Fn^{cseW^dl#U+M+f{rI5~cKO0@Oqe)l z>C{rBTBN>lj4*F2OzVRMXAA&OL1CdAeM(pTS_?GJbS`#_D;9gWUU99@y{CuzXfG9~ zlJYOkQ(_dhgJhphG&EK{*yd}!8#c%U_{K1T|UJGC;D%CnM zP70Ijrf>Rs;=ycSZEj;Irq@{aG{Ty=*(1F3w0g&Eu;GY-?%X_#Ms&QVv$KqU-p;;zvOe{Eh%Q>pUCUy-$&xt$&h~kbs4T zcbt^Gm<*^noMo^~E^(Xp*Jbn0^*OP$wmOdP75lA<{lFE_e-$Vy(lI+wb`euEJ= z*zX1NQ_Pmj@869IFBRrqZNCW(3nPuMVh@XvexmtA2+fm}+-+XYytnOW*C4bCJr)ta zXP#}NNc?N4_><2SQkT2s&$9FKm=FozgKh&5lJjz~Ft;!P5wDwHJjsT2Jrjf`;Ni9K zhKwZjblA>K1ocF07}$j)(GW+=2UolfQ$v4#PAoI+@6{o?l=S^knETm9j;2F5d{p6a_yW~Kd`}S$tXkK2 zRy@JC-21>xBM691=B8!y&hg}hidPg5cpuFDyi9)Cp4jNQ_1MRAIhj95ephYTNnuQ_ zi61aKwA%d{4e#U2lsK({rNn-{9O>9L?K{1o-(f9W$0XB)xcgz){oda(#ba!pde6MQ z*vrqZaSsj7r%Do^K)ZbQ#YYcA*K(?q_UEfe5H#A`LNWmH5(i~};{MK!M`~(Vpp%f0 z3ck33yLdO=kl&b^s+zOqVPZM98=3E-MczDUO~}QbT52>~obrTah>&Yj5rAn%3|_7Q zXwlepYcMC>D%vl#_yc|>v>zQjzsIFX+KEw-@>c(qN=Vf!Jd$ir{9H@d5CCf^;d*&e!kNKSeO)nn1$spQ;ssV zrqPo!7XMXuHkxOHY7#}K&naS$1y)A11-8VlA@Cc&8*rxG;atE#W-@y<|7^Q>-=ZO~ zT`#>gUR^QUtNM%3ITU=|J9{C7&}qvCUE(kO+%PR_$B(>Pk^jkq!bh(0N5Z1s2C8GVx$Eq_?gYk>s(x1D0&0-=k2|{4ro#l z1chcJEUc^{TRxQ!5Cez;g|zlqor}8+v9q#zs;Xp2W_C6xNCX|AxTvf1|8jb13atl# z>u}?(`kimfR8ct!f}&oy&ALknY1w{Qj)p*iOcFC^Bo$y_*Kfe@+ON+s-1uRpp}k&K z6l!UVc$?=m)y(c7IB7&Fx5%26CqAQV78;s4sA zmie@VQRa61ylDFV%~byN?l02XhC@=c$js@2MbiSXP*iF~gr{4-Mn}7#H*@Wsfvz46 z0kbu?!_u=0cX+rB;H)s1Y$Y`{DF8ws=P|khDarn*xdRo$QEK{~GK@-)7@}Q(^F%`m z3ZF!LfI$wG0xS&ikv7IFsA^wKY1P+7SSR^8Ay*8lChv#Q?IaL@nn`6h7`I)+EA z$gZ@kY|iVEFQP%qTU)e71_p}E`Ho8zACEFh3@5F(-_Gl;|2mGB)#FO9=BFM0(81Mr zWDNX*56GW(V3}BNsGsu;?hF)-XV-kZ%n1g>SP>r79yVIo9#19j3Qsj$e|PWWbn$Q) z9)%aDp15sar>Uwr|4*SZ2mFw&!00PI-YUtIqpO^PenBm$p}_W zndh*l&hU(`=}97z&wY4f?3>+}hqi`yhK0%P%})qh2>0LoJ0Ml1fjD5*xaerZOF1$; zdSCQccXV}svv(85;9<1{Mo@&gx7wvq5s6ag@(5Cmgn|D zS8sCv|0fe-LX@fu^k?y8(7?e8dGV*ZjJ+)NeMt>PjsNvKVZJS-9DdOXfO>DB*}_XA ztPJSu(;GqYZ@X{|dN#&4wT#qG!v1005;En6{%4+j`!|23-L(ushjZPgHPYy)WNP038Z^Y(O{8WbXE({|_-ijL8S1aH&`}FtmRNa) z=fck?4V66YpOv@ONZ5VyUWsM_7wL&pTkj*%ury;qQlpx3aMQGiL83<;YPC zqiw{7^6*MfUow}G>9V|{W6|97wNB<4S30JI)FV{MmrtR1L>I-y^z~`v12!ka($+9E^^UN*0eSMm%1~ch zXi12G3-;1Ng{5kg-~5yybi17YOVWx!QN?rwi7eO5I;&ljwO6x2NB6s#Sx?&D8&-?E zm*bLC&4-RRG#%H5;{>jIQimRwQxo1i6qTFWTYE3M7*U@8C#yK;8bvam{t;B_?wI3L zI^y%5s>zU$CmQ#i8Pdh~`}suA^@_BrFs`OCHu4F5cIXOaoQO(GYs-+c$pCqz3-mXL zt?;rBfXh3sQrWcIyZCKuna&#Aul%G4M0U@y;m6MW$_frW!xEpSYjC;IQk@uFYS+k{ zz%>;J$}r2V&Puk!fto@z{psow9*uimBnO)!w+F8u$~kW{&ao}^J4vW4RI_i0yU*x* ziZe2jC35{)ij1TY>fbtT!tQ9$T$^?J;U|9cc#&=DO3*BTK1W;AYnp$>O1JLQx8>L_ z&RI!Gd$y1CI+rgzdHNJ}{WCQ`lEs}N3AbqyvHdG0llc(}uW6xQYY$fr1Xo?EF2sr$ zNT@gVEhodHl(MVt)c(yL-OF;)83ztGVu^-4ng^ih@&hH3VjS^d95G3(qIM3)jWLCe zLK3KiTI)OOVQVJ$izX>R0WR&pYb@5>p3>;DzVBPYTTipwVRvwL@srup_O}C$U*Mj9 zpgr!{U+apGq4zGU;a^N)nqhIIJGdH+Usypj!TrjIW0)=VDNBbVK-Y6ytqB4ZmsB7! z?m3tOSS-ikml{e%R8;s0E%^2Ee& z=FK82cEjKG3~_gpU%urE#-5j9M-cF-5}wrQa=V_`!oVQ6=N_ch-_c1H=1u*-Q#JKP z`S5=6h`^KaBx31N7jk!Vm|-M%HO0kOE@VoI1Ftwe@gpDJHjvrbQZd_c)@L>B?W%K5 zb-mY)EE_lEYw`&9F-a`jcDIK>$7)IGN*pHnTUdzV$CtibJCQp(l2_nlj4uy0rd=9qLr+1LDe_OEM$waJX6tg3W4eubUZxQc$dPKc5R@oylo?HQ2vu1U_k4eBHM!aWFvRoCoR-BA3bxV8nx_kiROa- z1F9D)Wfn15>+*x=2R%ran(?J3yqg~jRXf83a~K!)4spAnNZGCVXt-+brv+b(PIA9= zp$++WYjhKp)CPTr5tt8Ak4PLvyMz0U=1s4QJ#uIsdHI9?g^zw93-Sdf;*?>nNEzr3r>E=)}J)@vkn)>jNfJ$W3JMViZ zPL+q;t-d5)N{>_L+1=ryQJfU5zcur3(BtnukyOYhdBpJLAdDhx+GOP4d%th;eUD{U zzd-s>;JGL+Ad4*>g_7kT_^r)CYHGGnnmVt~^(JpM_S3K<=1Z-9J`{0_=IKjvUD4E* zC9&~qH!x74)u(R)vRial z^XswmHSGIGxPF7R+7=-W{plyhE5TxCcJp~y#FB}tEb4`6R`0!E)`#6%owefYGOHbg z7tBAv^AX57k2zyMzeLMS*R%3(IFHJ`swppzS;k{W9IE#4A;_Qapc4{k!t`^s`^}H- zor6y6#CTV|Y8m-(_GMyX&&NHnBqe@RPw>3>tzUabRc|r=b3di@n}1bh(3*x1x}Q!+ zliV#zgj%(kxTT|2`!bbHxgw0d)hQc4PeCgLYI?ba5%9u*SEVg#)+3YM*f%atg-WCGq{T2_8-EWwXO`mH9#CG$#*&MLt|8ww120kR;7Rd<4_eDROZh@BI z4inSE=}gXyR}h6#z&?q+S$%zhH4-pK6=k#QIWlg+*pZJW4vx24E7uILtSU!KDaY_{ z{+Mpr2gmSAxT1pf}Cm1XqW+bY!*;OQN{Jgh+-tk^;NW5X> zmFf`v!85)8HYiKw>y^v%x72Nk4+{#5TgBTsgcBvoLj}<#J7v_Ux=nayjJZ97KzvKB z#xY?C{ba0o_mUY!uFQO3Ld~)V@zbZF$i1aewQ(C+urf5sp0WvkKUceQcJ9mbdz1#| zAXY0Say%I(Juw3Jk!(Bt^Yx)IW~!XwmElSUe4o^(k5A5j8C?g z<;}lOFMZaF-?CAz({PjL%1ODXw1r*kb^MXOAN)<2zvXy--r-`+bLV2e@G&ojdXl|Tv|=vyale;JxN$w}1w;d+ck_KED-{QqhJK0eQN zYR6S=s)^#M{(8_bIocKwx%+mvNpVq*_*m_C5E0(_*{S$fVX=#5-Kq~}1`jwUcFTb2 zW?;R|bIm6!!?WJ~3GvQ@yq+}`OWvZMHFkoI+=j=Tp>8^Jb9y%~!f+oGq^5W2Qo zP1`;ZNhDC_CS)yg(D`(k_vqK)^>3l*d&#vlTCeYpRT}7+^D`?5$!kO;xe(P|mRyT@ z@uGBkgNbsaB%o(S;E&f0?-QXE4Pg^(8O}Ft)U;knD7rlQGrGl#PRO|L2Gn>vj)M;U5LU1^4fu-o_z zYxX+`np(3IUl6vGe&%D+cJ79&t6AmfP|cUlos4rQ+EczQ=1$>%l?#Jsaci^YWE-<8 zy~f|lK_=&O71k)VK%^_f>7%Ei?TI#Jw)OM)-(RaLsXieJdGfWCn^$K0)RwB=p5R^g z0oURUGQ7(uf`ZRl(sO^}_BqE)`GqFV3eC-BHw8-|~^&2`sKNns< z?JdfF)A%=JO*k>LbNkrE(G35&zE|+L)?Zr@dooig8&8qGOD_KKB)39-^*BT;K;_O{q?VTLEa`l3kMyYEi z?(fd8y9!eD(Tv{`kBQmgI1r9eelc^43JX3tT&P5y@N?8?EltVnrxdN~cx21+r?)(g zJA49M=?E+9b%!ddE~k2)&bt<;&x^J64#w6ri*B;Ykg~Bo4U%>XbsW}WJ+r2Ey-wY) zE8?jFOhbhnziyk*8*`4Vm*wprG_6!h;=Q3ac3IG6DN!}S;pHutf#jeD^>KT|JFN(B zST&9@a7avAW_NbPt(rH!M_*Ex%-dyUL(>5J53NX0(X-$8A1ge5hE8`8T3z`!j3Ch& zLn5K%XzyTTOBgf3DkZ)sO8VLNzZCskydxh~u#gWTLTCv0;3tmg^eML2_gthjed@ z9maN~>P>&o2>*s6Je4@IvfVw^qTgQE59*)KO)~G0K1yXWy4hGBv>zz({Dv5sO%@#& zF6ba_J9jnB`Lg;l^VC!cNuAEC8>JQt%-%RUUfF8f>fqAl8Ie{~%WIKS`0(MwK#vMF zk=@kMN#)}Pjq@E1Ev+gsNR?b1N?_o{5UTV)4#rG+If<_-941w7cToFdXIMqOb4ZlA z?C{S%aom@gv&l*TKw~u-Lgv4rySj+Dg`ECi8+4t+DH3*}K@=6VF_3|(!lPVeZDo9f z+oP0G27D)a8z~sbuxe=EKI*5C-%qjc-x8|!|G*}8 z{mdDcpXPK<_64!tmB#je9{CR6FGX(?MV|Z0J+7ORF8)Z|#C*rda{4*>cn&qEbus}- zqn6;3_~H&>e?)Q6L+TBN=a`xQ{#dxvoal73Cs_K? zAg3nZZ@0fi(Q8AIZx7cx^3Bgw3IOva#H!D!k`YEER8;V^_%6`u?ykp`%;VJW zY})MFRqG$#zt;_d1G|>ZeBbB&5<|n^_nL4#Q&_S(8vi3Nl)A@u`^nR!VD`)BG31w+ zX0PMJ*u4l1X9vG7?kC6E+iueEQtqS2#XWsYDf5JTDZ|H@Px!a&vy2MMht|ge^lj_M zRg!8&$$AEfLNI8ziIZ4m{_CAvJ=>nM6X^RcbfZQ`vlDzIE)W^rwmt+W1kdg7?~}=C z<;7$IV-N`TlCPtagxz+kAqDP~lG0L{Zu@EYe?U$qrKPo~JZMjqn8OqI`Lp%=sHiA@ zMh@jC+hAk!hkurjTB$&d_jFMOzDV6LEbB|V-DK11yCv&CG0JtM2zy0_?UD1{jB2wp z8&*w(o9E|Iv3+u_=ef`oUp%%ZNaAnl>nl=x1Kt%mBN2-t$HJX|2cA6AB+Ay z#7iEA)4zm&I#@p;0E(uEiU!zlKu<_m;kU6d7ZIr~_MqFQ5qt}hue^Y^n+AvSwuwS$PK>3@j0lPvLI76AcprnBq#=wnMh%y zP}+B(u*Bj&Y{bT{4eY|_KtpVN+cPyQ8Jncs1Hj`4&?vp2ASU3d#-qa{h_@f-tE}Qt zN}fSG4X8$8f8WFwRKM+x=C&HXDdi8Y3FAM(m^BV-JPVkxW%zh7M;z#pnbz}s)c8Z}^;k&eI6k14DGjt6j#U;Kxq08IV=w!C z>6z|UZNKXTPZKIsyTS9EGCiFlhXf>PC@3BcfraP$2&zis@4<0p5Gr^y(N85(c(*FMq4Ud0qQcTr>q8beWu;Ym8oDe8r1Q|TM zLO#42`ZiMG5e%5fOxZ8ohRBAb~vGjjMl}Pp{PLs?Gf^_8XgXK{CW-47OTF$OWAcNS8FE#`c@!G3+za zsV$?mpkAgs(PoDf0v~=_o5fz*y^rDLHu#jIpLR-?bJ>N?OB6h<`#Y;j@?mPqXs`Nv*BvFS)zI3z++Wr6=f1VJ&L&5r^tPss!Yckn^*hy_O~ z6Ci}xSIpo*Ka{;3ZvB5`C78(YOiXqPHkvOla6#b*K4uK$vrerqyejB~Y}0#gnCNH_ zKW?`=#7N4>ZYu&hmYzOcGh9F?-&i=d(Fz) zV5w^bc>`3aoBG4oZa~qxby*key%i%`lG?=&K>p$XH2xlGl_R6MS)o z{h)t1qM{j-{U-;sx>o_*omBb0!n`#Gl+|LU3k8}N`-h2b3 zdLX`M0xNN2-Cg)V_<(~MK6vsZ8tC^Ew@R=Q{~f6&f&a51Ttx>C<>BKEl!%vN3i$au zi+7(cIGGRxbc%X2mF@EX5kFM0Ih(0iX-_$I=^rO9? zf?WgzQZyvPZT^lD&8KmO>(w71<** zdu9`oy~$o>^FN>W`}6%B|KsR5?z_Ca*7JE?*Lgn9$FQ;a+)=*-ced;(_9uS$4fTkv z_L=n$=@nrvT5*ZTIWj96ZTJ!#OkE8ACEB)gBvLVkQkS|5^z$Dc$7#mx+`UihdG-q( zRpY#kgE~4BRLIxIxQ!`t4ZmaLQytDJGddi7vLwzUUPTL zE`XgloUXr%b%3?5`IyVzD~y*DL{(@gxoFzs7Kb9k_*YnxPIZciMzeeP4h-gafPZCP zcvC=t0*+v}3y}u%ZCH-2VnvX(*RvT<$}nR-8Ry_E7xzFcExnH7da&%iz^sEr6V*)3 z;YQ?7U!$O(`TkrN*C|xaPC2QWqp@a(+Me1ORbcWiKay|_Am+Aw?<<(d$~|Z2{0Zz) zF_q6&mY17>^Z!K!cR?D)$=?T@p9u>KLmCY_3f{s%*>jc2I!_8nc;I*fFXD^~{8J3n zEoSBx5b;3P$W+csZ*mB5CQX4`i#Wl9uBd7si;zB2;a%X5EMVr~gSTY9AxGqx`{}<| z2ir3YP+1+6zx+ku;o;HQ*C&TK`hR7m{RrN#W}RC+6*8|l-4-^uHU2waeC5R;Jw0sRw^PkJ_N!7*n~_&0Ls29Oo# zW0>RnPyRN&137Z#h$9LWqKxBh_lg;}z`^z6J-dFZHS=85;l)BjTc7W~sz~gyMKN%LlQw~{KU7H6mYPE z8XaU9kPXr<`W=LP4!-SR^j<6A;25OT3c@xd#0O+u|4|QbNI^2r$KwW+a-0IXYP61i}diVwje1_VCcjT313KmA3(b1TpRf zE$adVed~V!M(8|PoPdQ&3rIOw51Gzn5}iIgc^|qL{^-mQT+_9B4jPYk7uv)k&dhyV zCo1D9ith*uC&Mznws}^`DFUjIx$_acP-X!IJFe#W>7TgZ0Q2mRko zQBgWj9}%&!!7)BLsgF=vK{PVy!zA_W*)2$4j!jO+_4M?-f{jfvo}E(wmK*k`5ZW07 zdXD#w+kyABH#KK3aykjcMYb=dI*v}qvK-zsuP&dP#(NqzPqtxj-dxAI za{GOL_&j)f0{~{92C2YTo z=^GF7>w)_WmR>Y8jw>MKHinD(L|eN91cC@f1WqfO8D5ABhTIS|B34s)UmIhpb_a35 zQ!2NeF|@wYW6Z4>oRxJOyp`yXwLCV4LV>_p@K8n-MF%AkBCG@1#RK=fThs#NdToVZ z`T&AY31D9$at6@ef^WHIKHcD@2E1WVwF6zg3Fv559*_!D;&bj{J^h^ubkX^gO5I)* zCV^S|&uwP=KhQr^P;N^#VCJ6o;H01QL>Ch$iTenesMK(tDw%9>fNc(f(kY}X&ZZEayOtnig`O3j}0)TWc)HkE?zGt3!r zO}}%!KcFeP#l!^3+rWfIa%j4`yE7o53mA`?KshBL9t5FxSm;Al)mY#Rb6L1o!X*Gx zk)a`0f$D2gN=kVI-e&RIgIXO>H^sewuO0iC^cu`4wK`^Ode*8;Ay-{7s8W^a?&}fz zywUO2FLqW_%M|nK4-}M?NYa{LEbBilIlp%8Z>Y-LfjIU{_(8$%2S$&4@8;blh93`> zidT6j+&qaGIWAb~iT~t%aPbq8=)JCeA2x|c8{z^Ylp};<16gD{e0!xBY@$jj}NF4F;KMbi0K>`%_ zwi&yN*GA}XobUDw>6An#bWa^S;;31f_BOFaJZTy?Pf0IjbqdvKTHDAI*Zrk+Wv_m- zQdm$wqX7NlS!#8sdrN08&G}yMPbGr}`n?;>@|Ki83#HkH3ppYZSKW4wKY+aP3bADB zFY?QJ64oN$8Chu!G5%!|xaa(180KdetwUzmPZ?SFFeo%1wL~AKqi-+D+R&1@9`4FeXC*cZ^qKEhGqc?X(+C!Xa{2>BpKeu+W{Ej$YQB+ z-HryB2YrGCm*mbPOJ|^5Ri!Hd16i(jzGzb(?3Kvw@;~wWg(Z$xny7Cq2#T(c!roEp zeT0&DnsQ;8^&8u9MWuc6__76_#C|^K$%|xNr!L2?)_;{` z?DSTRyT91g*sn0|Do{o;opjUP`!gkLZ7J$qfuFS^dV8gf!1PqMW$S(u3EYSjap|8O zFyMbus>=g<*lWV@8>F|43d&zZ0g3i2h!L{BX%j&`az~?c_!_Rx?tW8t^zY%_!>8tt z=q_cavbaPK{{C9{KKDKW{FM7;|# z-d^YJ%RPIJEB%-C?l6gPYBHOj$*BoW715R1m(~v>>Zk0_t#HV>#{3cr%HO~bV?Vop zR>4|&3>AlCWu>dJ?1)jEV4wbHg^ zl#jBAvR@i{mA{jS*-}V1@UykJ(SeX5(Cl45lZ~>zf&yA4I)m}rVp}4=8Ewz0i+o3) z$HOpfR?U#FH=)eDbaZtU*LG5{zW^34N=h~%K0XM|z8CT%UKee}KwOFpye=^N z*-?#fehlQO-UPE$Oh3EdxG2zf^1v|?Nh1)we`~DR1Or4g_Gd@-;Msp2)OT*cBSy{n zYRSuE1EPc|Bmfo(0j-!L+ADbJtM2aZpwcFVRf8l_z?fWLkm3uq-O=nZ32!!Mn%=V} z+mOocsppTsuFkp77DSNod#jFsifoGMq{+$OlFf3>4G7;LY%*=d0nIKL7W^lgjV&t% z?{!{cVyI8RWCAKlaF&3h{5BI4xB)ORfxGPNn&y)ypT2(=5prC{BPIqnjR@g8XM#k! zy1MqSI=~lVYpOOZ)DkMl#m&vfGBU8^A!ZQc_1?nFN|HZ6TdyTPrt_^W$z3=D3T;Se zs8*rAg3P=jA?S=`4*oX0u(1JUClq*$OzDuX2_$OR3P1|{^r@RW1&7|P(cqF29%AC- zlauSjssUKd4bvbM4P9NYlXeo{+21D-Zh);-MyA~N@&f(TFU@$ZDs$oo?nEKQ=*qT0 z$tqR^h-8))zV+X15aML8nehrU6t$8|tOxOl4NzwbpT2+=$X5)hE((tsVPP8W zLj5@=ZnWb7B|MrtA5XaN-9vvPPF@N(4}e*LaH9d$4+DA7Sww>@siG<=p7g;n)O105 zT;OWI0JRM!0Ir1rWu#l6&LhuEQrZ z24X4{hX~L0TD~nv`5@scC_0)T`*CtGKsWPnL1nuTF*4LLh>RA=zsxTvsBWZ!+YiFX zC7(PYnqOD|QK!UPVbsxf&E4ysScu?$@!v$C%!_eYn25 z$_SSgeBVF{EB#tE&!-iE@iH>h^NW+koBR6-LDxC?QXv$c25dV%N4r7&tug*mZ)9%Q zEh;oeB19Hcvzt&udX*@{>Y0Oj77A8VkXui7eysv&S$wzUOZXdN7Xq;mJdVpf*Hcbm zokzlE(n;YW0?8OMH-}1xz===9eJ@jgjg`<%#E_1Ol^b7gTTC>POaHfhZK?TJoaygA z6kuY}IM1AY9wUXZfQ2clY(3PN^RI@_x0iF1@WF;5JN59Ej#=CW+~XH4xo->BzyTpa z??*xc5-AN%z}z<6{P(4czFl2lqewj8i)jhU?Y$yqPyC#vbq`yQBm_C z8Ab)_sssD_0R}1br)MYYMaS$SRN#0zO%Gppab;x(?9~Wh3LYwykPQrBt$y4GpDHT& z1!{%{2b-WP-+?`ah!`009aMjz?L;TSdO7&(SBH`BIVMD^BC1%hsF;k)1!*(r#t=QK z^<)(>tWih^@UDKO3+ufd?1$!-mM|rbXx2b+3kqnq)9YcX@p0CVA?@eJxBH6ZsPJ!Z zlw>GRYGp%kf4N|ieUqTw+o2VW!QrxNI(vTS{`~_ovVA?D1|IS8Gkx$k@Z`ETMl_hG z&84WI@ELJ$B_4z+gXP}hk2na+kdXl)YKq7%O{0*m60h4b{t~jzuQOx zVq|1wl^Jv-u;u<2d$@Oa$jp=u0}1dK1Si&3F*X*KrFUsyB{4~<`1o;DXuXesj0_n4 zEG{j{LR+2%!Ei)_+AR`*BzeG0CFd~{6ScUH2yE45(3wmS4!jPx<`GBVKc2oodej;bJ5d~SL79=#BRmV)I1H6Lr*ndVeX z1yhrX4IbHhFRt3+3$o{@CdQ|>g3cRVAVM6>au-Rxj^aS^S($K(h@3Hqii+A0tRj05 z+=ip^nUBPInK0O(uK`%;CNp!$DIN|9^~Pj12^>^X5)!EAx5#N|a6uCAr1LpDQ`bI= z$I|{W(qe-;TGLXF1rrk!WrY7w28)HA&$tc5)r7FEz%GTvZch3PI$wfu6m;WlXPq~h z0%KQYZq{$(N64gQWo3cGRtWei-VhLY&&ztkFH9cO(E@(1NUUhWW8Ayrt|`0m$=bFv z?^nKYOb998E*~6r8N_L>C&f{b&Vh5iuxRP2+39cMHcc!)cx??j zx|Ady0mCq_ms9K>T+l7U?k26O%475!0C6yD!$?g{{m3v6>A9Z^3kjH{ASoNnnADBW zne@snU&Ga#eQ)IR`r9|YJCBnB5d$m8&qMja&XEp>vgLR=OE4jW;j}^FZs=%}tk_4S zrTz9T2x3{r%U=W_A*Dojgs=m}Qb9%U9~O4?z56nlg&=C__rA`JerPBZT!lhqiK^_p z^mKn%VDO&pW=g;-iqr6WCxuAraPcDKT9Xs3YgB=f3~Bztq0=lWi$#$ZQ!Z>_b#r(;brvxxi=pY z#3us5iQr5}K5;oSZuxlyoK52Hz9Wh0z$i1zp=Q@9M&)T0bXCqwP0;~B8G0+I_BJY6$U;LM02;*L11b!p6b01f3JTma>A+SFP9-U@&a8VVo+u|4 z`!+|}2Yy|H1fSLea!stm8o!ow2_n4pYI^CK2` z@3+L#Q~*taZX9+8z~Gp#U(;p%t*$6k0N%nXejWPXo2m65j@12y4GTgsl@SYDVfR=M>YvG_0{tV3)pm(s}_~}qNIyxd| zSBEpc9pK)I?Bw9)iUAIWpzJ|R3hbbLz53Trl`_3kKfTi^9r*~54u{0CBe)$H;DIk) zTMCf>LSSj#Wn;^LMGN|=JK*SsPDMrK=HX#_ytiaInoo7`_b-~)W7!VlI0|-SOli|% zKQeaXwtg8^G&V{;3jTBsOc8pO@UFqSm4?JYaW;k4)MaRP@q<~$)po#<F+N{;wIaNQG2Cvw5QNA5sIp%%ma$#nDY(ti(U0N{X2pbg((L{*yEjOJ zk6!TH+Ztn?4K@nRTsooxooO6cDDFZJ;+tf77f;UlCY;+a5r{9R6z98BS4(|-Tx6!a z1gmcR-X{=6(a_L5HFwxGtUJ6lQ=d$jM)suiXjq_x<>ztyk4hU6@Z_7FW@g51NQY(% zB>~>P+FEgCIbKQLmQfpko^)%z44bij%Ve^e8q4uvTHF=*7|jWuH)BGew%6xXJWRZ zDC73=Da!jT7%01~QEAF(Bfr2!4I<*6Z1xsP3Y8yDpW+ZF61Zi8Bp4+?8T1(pQ((9O zvd;UKD40U?pn<#JQ;JvPhQ)$CT~9{+0KSF=xT97_@+iPTlKaKbEqDZzkPx+xj}P)| zP=^g{jiBk+}W{% z&FF@ZP!i1`I}G8|#8(ah4XG6X1ZQ?-1sia4P;67f?$QF{I5ZSkHqQCq;QmSgdc}`L zd6vtP(DBqoM7HcCLi7&a(*Lc_zQz>?Y8I*|7sbe$NCXExAXM*y5iL=3u*Sa>jq zbiuX$wbF(mackD(sak{2-^aqD-v6$AA$z$j2QoLGcbVq7Hl%{-2K5gA4giS}@-Xc! zvyHblS#=$ihq)9=b@QBRcgsPW0xZp2^m#m}!~YHXbs zPaCv$<=rR3LVvjMe7>nnpz8bx^O^bA_mVZ>OAbW`c-KV2kY^4oO(=z&?rLag3{=^h zA>*7P!>|q~R|kh?Fm2#|)=9*!TYBZcU`jh<#ftFs|Hbuwf>Lhspb*lYVW+YOpKeNO z>Ur4L^zp1gIs$gU+(x3{vxrz|xQWn16U7ZUJv>upX{|cPvE%e&RG$(ptbKXsjDgzR z54vBlu$8UDob@cV*A|!QYI1Jy>=QevB}r(0vwxA7%MXS+PP>PP4I#QInPbv-n;iVR^WJjUk zz>xEfV@(^2HUV_)mDe*D@Gu^K!q-1mk{<9YA&K3(0>cIQ9X z>61U;P6)lob%NeeWo%;N(r|K*OXYa+yvL`!qtA$QASOXJ*fjRD&z2gN#E})xag6ur(8@HGOV?qE8Wv7MGgF}Vkw*!(VZUAQh>n+2TX7pG=~s2lALUb z`}%yyP5FPXIANE;Fb#L9KJB^c+e5#iryAuf@G5Lp_&wX#6V^6>H0WUD-SFi9{oYFG ziZE}PGX5xn?`~_mJvudYE@fiAr>DC!So62P%xxb();{lE1gGXM zbi^o7!iMwj+`E6j=ebVH8$6^-0;|zaU%!S!!vb?ZdN{-|8Sh)Yoc=C|6sgcLfMp~m zC|>1tbSOcVhC+d54-D(1z}z)1K3=uRP!u4)Suj6ZR?7 zZDF&uJ@i9RrZX;Hd>iU~%3%-KAJlzL(hA+GQ#=9!EBTKmpcIB#7viTT(>ldHH8q7S zN2p?EVL2kWgMUDPU_&?LSIj~72C~5d3KbR@cm-(^!8Q3!e0&>Nz{4l<2NVdIn<7Wr z-a8LZ&jkPjuCMz(HUrcIW=Rq-3;sKMW;Oc5YfP{8;7vv+U`L00& zj5sBOy67HnP`@i||JbNVN*?r@nwrE1st=kpz=RD^7enn91iu*>6$LCIPEG{%12#7z zQI4eI4QBam$_OqyTvRbSI^U$;p!)?Cs)yqhISblxO(wXtxI~hmBy^`ygj=GLc2DkJ zxF6y9_K{+VTaM;1o0^H4q|GQ~W@p~m+k|+IpWA!;wWnU)aI$VsjEz|;=|!(8!jGxCPv5chpjv2F17HU?*z zhmS}FJolRNMa5!U)Ao3tu9s1fcyj?wfAg|p=6o~oBrP=7 zU7PaWt7eHYpcWUclNKdNRc`lx00^whk7QjqjzXVKUvu|vb$&rEo=6&-36Di6lF z+EQlaA1Xe2?b9)8Ihje^K0zSu9po`^A)7I9(wr6)Ux!WB2&Fq)COAVK*7Nc3ywHI2 zViAmJ$l0_DucYb>Uh+tbA#9?;TgT?U1&*qT@jMhUmCP<&rwQtR?`Ng2WCPI=V*;|G~= zLQ+!n#s=+`X<@vn1ij;EUp8qAi$Os+@#5CLd%0$HwfYv_`&hH;8LTp=F*a&z%oQG5 z^Qf0^1ml*e%9&5Jd79x88)0c08)dKZ9&i8Sbu zz)3q3BD$HF01+Y-^-Vtk>QdKT1L-Zh)g>`_oR?#b4|>>b%XxnHxkn$T$l62zYxp z#W>M!%HcPMss`-bzHt>l9=GCRYasW)UiK6cb_4_jkh}sws4bCms>1pQ9mV5A@DPum zNQ3zeAb#n9C?kb9V64~>1lAzt@9%F1-Y-PN;E#<9_X`xQbFhApX%HaX_TVfJ|FT4p z9r)e*ls}+jL4i14W&SA1A2ubpy{q~G%Aydyb7E7Ua%Xwh4R``8P|`e=4IQ0KeUvlqs03hf9z`Mvogl0BT$e9(`Ji!MC zPIy%JuAsoSHv|y$&)`;pyloUCgucUeuvq#F)lAt8_+rsPJ&#}e<9cNmV&Wt|TmNH6 zt%Ze<2PnUNQ*5s@BBVinlzQ(vEL#bT+d=5-YPMg44eM$pz}-@Wo^Wr^+3w#kUu0!! zckjIx|VK?Hu z&m9LRa(4C|*kVJ#h#y=^C;)I^WCGSnOJ^qzbhzM(DhY?k1GY(6?ttaL4z&Zy4KS)q z0Uom)QA{M1hI$5`7@`Q?>4?9)E+|-yQ+-aj@rq(TEwpTBx?E+W>b!gqr|y!p;lhFd z&ZN6}_ZwGKjEdbC3@NExWez$XTgFfitO-zZIKWPJhy9rhV1% zuS*NnVpj5NU5Bmg3cyz2evCqC6&d0~6L=qJPcV>dfi@8h1!oBa7~$W)f4|tQg8*^e zhXR_C*8&gVyHGe-7+F}Z!YBIADw)_jCpr zd!=Ngq*egsWPU3|9i}4ZNOw2e%mu{Ol&DTu!fhzS?qh8aRsi!ugWeGU$rxH%uI~Hy zncSdMBKP0*th^c9{76~39Zp;%uYiacDybjUjnJ+*%tYjf0Imu@K~@qtB1sw;(7{!2 z2i6BNPXg)|;${hknTjuJ`lPtYoBPTAOR8u^y>{P5{r$+mm2|oI&`XAlgv8+MKExyl zLMRfHl=3j5Li1BEMHSo4Nm*Lrd{Nxk*g&Do+9Scz1=5uKxQ%kb(qDi~5b?3jRmTQ{ zI%K2;PN6^}>(cyz?&t5{Oi0qfAnbyNOK~kg32@}O81W0Df>%u7yN8rv5{$^i7KIa# z)!@D#e9F$m$45p$KmaFpD-=yU7Cn?Ap8xQWL<>7RqOwe~YH?(;3ml$#^kQr*EU;Zq z#a-j<3Pk7*RW&uHa8_YvXPU(^z_2jb-ya>B;evyzo12?4cv%h%4!%xJrPsEB;j2!C zH5H5^VB)N9YygKdV#fouB=PmVks(+oWYF}>IXV5gc#qe97+Fj|VB6hC=6Q=FCILVq$^>xd}d^L|C*sABFO@2~X0bz&oT+JK5v|LuBWBBj&% z9sT9bUb3W__4RdWkkYcf`^Zj9`SQh!*BuKm@QKb-di)q4+U}*HcJGGEiEZ|zo$IFR*D$|{J7^=IW8w?H&I8`?KxK%u?Jmm23I*%;PzlT+Eqo0=;AC)~tH+RR# z{DONPBTzUMbn42`^0I}#eDr#D?N5#Mpf+57rS|cq3keQ)>+9c3jV|B>fOj!BC#RFlyf^F_V`3rR@@aSPW z+zb|NzB;xr)EopXh{v}d&L%f^cYCc-q!9GrnNN%Sj1@=l59W>+oTto>ZoaEH19WL^S|T)%#AnUW^snF4&B;WXXp- zav5^wt}!fx&;{57R2x>#L4*ScuJ$6WH(cmL!?<#Nge!sOWuQJ){(=OwBvpy^s0Y$_ zV^zCh4vhkW{?!74-rim`91M(5nDb*W-d{%Z1BOU!9TZ1lQ?(6PL<{@Jrh9NwtB-@Q zc_e&guITfGB2C-w9YLQ(CLadDPtDKV-7f!-bT_GcYUyB%;N;T$+Dc{J_|$n0I7Gz~*J3U_Xf%F8=dWF`ug(ukb(CeJ6=Y^^K}ISe0yx z_5*)um>LDcv>zr|*x(3jIg|-T=xESke$L1UfXy!h0OqjL?!DiRp7^5hRu#n0#!4p8?EW+6wF5?zT{Ft2zvOx znr5`69}CSoI5$HccU1rUWzTgB4|sZ*0%~$hsSV!0GP*kn$kg2VWULmx(&_iqXcYvZ z-QsuSLPzQ7;-Bku3U(>!wk!<}-^yeCd$G*E4-)N4;tf zzJ0vo=1=#OP_oDX7ieRCE!h1xcG|tSXtO-B2z69xAwmreU?|wxU{-V;tSg%!b|*G9 zl>l}qgc%AVJ3RsZ_cD?uO9i1fOs4VwKYkoJ#2!)5*Y@Vz1q0ua47y+pp)e znz3!(O&f4OOT;WK+kcu$NN*KfICW6{yJK&$tT2vkJ_)InTXz=9;Vrs z>l6D)a4}=6{|?C zMJv>Vv74od_W!HzVeaq3aazRFUd#ccE9E0J~4_ZA2GVX{ZqnY z6rdT}70;X|&TPKQUuL|8J#zySENE;~p8UoauV;8V_e#Ik1wTc|xfyPAdpOddEC}3B z5p`N6LcS0PooRyL9&7^IoF}^3$|>W=u(MxkS>9}G9UrgJS(0g2kL+53GG+DW7eb`p zhi3qKwiFyX@QJQ4;tkw$T76NgLUFtwgAxj85x0RM&!UFK-wQK`DC*2UQ~6u4PT}H@slEfh;1M?O!?re` zzU@!il2CQDCWJHYm@Be^PJ%D>?C}0)0?5{{E?ZLr>9K%qM>Qp=2Mm*1{ zsk<;!5nLCj5Fm_0A$H@z!45Y+!->6BgC?b&4kbWBmT)8!QqF-teaXvToUo}A08tD< z+ki^q>y`?@F;qp|P^cgSEZ{V)unz#37YWEbyM7fB@M&Z9uRi&IT!6`KM4feNfYxkj z1@IU9p$(aAma4SVy`vhufghhZtVZ50TigOMtK<4w#gN>kh@*p&? z8ke6d7Bt=A5{!PO-_evNpW=uNO4U?UxTHLfU17cb-q^}ZI_K+Xq*poAZNnEMH0x}Z-A)| z&3#}{kO?q*WNTa>>J zh%sa>z9oOHit%T1*%dSUnQ!FS_t(jJF%7@k>o|kRIN4ln>g5h?$jLOlE-3MH&O|N zMmMNQAyWyNLIHE8Dz_CJi6aVj!Jl^lCNYx-d>K|u;n&-ZHORFBQj!2Z&IB@*MGM>A z+iU4d(HRE%bc6}drA-d!2MY3Z-L-Vhf0vkrq@|=xwazCLuVaT zcfgnYm~P^+-k8f};k&l}WbFVJyW5k!BOMLzKR=#2s=DGM*`Xw5#mYcsGz?$&-@`+~ zyJ7#?Dz63ar@egi;Vp+)P|`Fj%I9@^a8+%_1;uSJMKt0m*QrlAn#MujK$P3H0Emw;P8xqd`? zu{6;ymi*7Xs&6JyeCuY$!>$eEM>1EJZaj2`ytg7H5*U1+4gsAgDOuw^sEZ@kERNp4 zE;acO8%;xsJ62WDL(blH|luVEp>b`!Dw1AGkiAewjY*~5m* z5!#5+YnhFHWRVOl3%hO_(G<}@mP0+Uvi|J7R`pAU!9Yev7kzqeF5fh)T z@b1|gCSNCn*fSiAGfLH-q$>VvII*&}mUc1ws(W@gV=^NMDFE63GbPNC_&+nm40Q0J zL|7n^*8=~~2a%Ns9R3h*l>a>_94)59jQj-Ny#``GO&S0=iTT+IMpurB*AKT_Z_4o^ z*YLqLMve#RJG8XOYv)EK5VY6(VO7d)xN;*D@O+V`Ol)l3O3KO>0+ZCCr4AUsD|rOo z-R8i~@Ms1XNL6$IEX+Z{`y0O!3&fc>gLVWcGLRpiG~tO*-uCZU$c1L2^5^jI*{v74 ztR!%(Z+XLgMrK&=MKx7wuH<|*T!RDnfI}D|NltIdX<_7>TQGTQmk8B$udv7U@Oi14mhG?BmhFXjjL>C8sX42yKRUzY`g2n=WcWXV!4HPn9d~Ts-==btn{X|tQH^_xi8}8S zI+86SzqfAa z{%ZCakIRb}>#N2&!HXN*t?i?cS?nYv?>K?P@gDgy_DagvW^5;nY&?;_t+kLs2Ul#B z^3&_`-P6nXcXq^uO%YpXdK63v;ut zoR0=a?X(K*9ZJ(IM6=S@wr$3*H@RB1_k7LB$UxEsC=`*GDl;A=tz6pTWqt^M8v!~4 zb&qYsU)5w$+xet$n=fVd>a86eT28#0=s@9c6%eQldz%oTdip1PQ%5w>*>atRtX9v9R7l6{(|9zKbJ!XBFwJ*$I>W7_S1*XLKZDuXs#n;(} z<8$->K}|vAux(||$7~N+iMtJ?g9rcjDsHdr-MKvuBsdhhn{tG}#9R7)?_L}>T*m7Xh>JHcbpvaFB9HU-?JTL7 zSJU5*FJK?{WhvB;?CS8&RT&IYY~sB93q6zb-LsCS994J`jN{&~Dk16j>Z`dX{CyV! zKK)XrIApkt0=dT-zuh&Vb$pmj(B}sWHRoj99{R~UAk{)!c4;TA@yuuGNR-P^>~s!v zcwcV9bdQ>v8Yx^*#(-m(nga_Ibb**CN?;$N5Ll|Dq}ds9-bF~EK!5A_yWWhVM#vy< zZb&0gXBY`H5&(emga2_*9Qpm5isTN?rY_RoL1Adp6~_W8tPdO<9eJQ!gh?wlsu)Ti zs3ggM&j1?u9r(8|G@dn4D`$8FSGk|lTX#H&KblPbg&R$E@5SU9`e6jl0|uP;S+c$% zSxagIpSeWni-s(sC^ad_*Q>DRxnJdLtD1|Q(cJv^+iH~o$H(r9-;=8tjZ?nLvj)TR ziMmeG%F3L}y@pXYnFTJ3r!x=kqCA4=XTH1hM$-~k>>ur#A@VUOHrilNUwscGDhSyW ztaRBRqXhLIO9b|k$$D=Zc(*c81H4H|iDFUD2D1V@sL(nP38GMi&D1p*CL%-ufR~WT z88VdvmP6qy5Su|~j}$G)2oritz^IYh7-(xpM@O?zV9OdPUctg@hvKT(=7A z?T#SvDM3*POaKy-oH0?B0z9(J0Tnvmdx)>5z2kmfBYfdh(L!EyPAQTp07{|x7+rpp zpC(&H^Kd>}%^IOR_W9^TWo6l!3sE9k(r{4IEc`17dGaJDqSm$9Zh43RkGl-XYfw}F zS<=6KYrMiX?BKrG*@aLvVOqfFi5^;S?1Scc^XTGAXUWPfIvCOfkI?w!SK%@}-no^G zK?Y6C zA*OhCl+$XarnR7>gTpmZ3u9wHn2|wtOu_^+2M0bZUKp_Y3%epZ4%Dkxn_kYLa&6(^ z;Xp=bFHQjvP6|j#fK{XX;6}ti`5b3G<821)$2@ESckaA_$FlI4PrT(q^a~M{BMEb# zO;fQX0m`p~tG~r!-fp;nEd&N$J;`-KMxu~QHdg%$V@2601YqOU=`!Bn^Jxmi$y-Z{ zKULfl14U^w!tk7P({G{2e$4k7#zzWa33Vkp=k+c$6gj&?V**|GfQ$i|y?t8jhYvxU zVi#F!Ai*cSenhBCzd%g@ZiVQ_vR|lt&UD+1T>DUL9=wM!2}mfQ1(OUiv@)BQ~sYl;2g7 zJD)W3=2u8&ciq_Sy>2*dSxTsHMCc-79U95^aB~}DSq=wJx z0H{Fu_4KeKa1qRu*oyU^*WD;i7P=OWzlMRL55Bez+}dCm^=RcM1!U&?D*h5gIXHZC zFZpq(178JR_q003k4D}wpFa-bi9(#5;2iXdH~f-XCWX1BfFtC_WOvpB^%EIK8qA>-Ur*#yBBx)1N( z`5ohTs>V>o?%ZlL5%bCHIrO+=)VL|5O_{C~cvKdzed)!dL3`vzOdO_F@C~_}vq5s^{B8@C0?l=MY*Pdm|8U#L~g=;?2)kQTJx2{->5sLBB)STJ-m znMb6YgRx}dlK6m7za^DSZ*6bSCW=K@FXxM?w7rOWmB%}~ayCyD)SMmtlK#4H>D#wo ze~Z_`bf`Yl-1uSe!Ehv0bd5P)R{!XVu+&mVV_di6Y^K)a>%WK#!)C}LBqU@lqmJQz zx;&FuNEcF>0OxC02poFR6L0-V`tn_7E?V{ruUyH9Pz?zu%2j*^b~0;@FtsX6zW<_m zbk&NH;UY@mMINt2-Rj%OG~#T_myDo1nmk?CmPVmkuvKIoM-0cl+jG%wzHc-;)u{-I7w1>+fYC6lDgGS=ZeeN~Ouo-V2^Ram& zztNYW9!U;zI#Wpx)!DX=vmdq~kW-<+(WRCpExntj&obWB;Ko=BwN@{K}?@ zzS!!XOlt*oMM~ZG1*WuSm|;Jg%pif;CiK9}VnHO3v-2RTl1>iBpX4=3PXG3lPhj=J z&qrf&}TTU06WrT{hI+t4N};an3LIJl1 z6B8ufkVCHT4ztq#g6RHBt+)m95|{6wkpdYPXv~GIZ;K8dxEHZRLvQw=4QrFekV$>C zo>R(&KkE@F#LBqD!QgL*w9-?Iy-UGU<~u&${^l4wC13WAj0JMQ#IJ|B=COPDfR~|w zM}+;|^N;|vl9Q`yt+2VxLPk4`*HIDxGqgrWuzSGgw1+8gRdsb39WxWHs0S%zkDw4{ z#8x6?uAQBoAv%0ORpy7Xl#l`jFsiD1z;5aQX;~G~8*xZM)k_GyWfS<1x7hLmtMyLM ztWvx5BU<6Kg+!CIL7oBnLJ;SDe21;_aj-UNLNob;^|h+ z$w|t=ZTa_=PYVrowB-^4n$6XT25c1KAHi)cjt9(S-=0#*kc5D$H&L)Ok7R3QWA}Pt zh67SWNPduUb9+$Z^vD0xC&rR{FoYQZjSPSyzB=!LF9VenF)(*PTVgzzE(>&sHV~Kq zIuZesJuY z+fs^a>lmL+15Vz&E5blMRO+?u>|J?ox3sw41!G9yQV*0x!N5hJ(zI@J?bll4JS{JZ zTYQ{4B4T}5AtFKLGVk7m*WN5|4RsQ!ZeM@D3VXS!vpF;j!!V=hcA{zI#U>xGfK941KGxV#p?nSBe(ESql%Q5{*#1W?%4NQTL7v z!jTE8-`DRiwH~dtOIn!47aIA-0au6jD;+cu#_6UQKZmLFh6^Gq)YOJPXAflQm_w1l zcvHO&8^iZ(H{^9?@)UktNWRQ(Bz6Kpi0TE`O0f?wbIBg$?J+PC&OeZAKsdK5yEbt2r%f*)ce!O+*Oy< z%XfH~K}mB{km6ypJ~wBN&;nW0FdcspOjnOw#%3-VYltSM7tB>vpB#ZP_Hz^25nu5m zQ6bm+g*j_^q@ae4fHcN`NQ1LfW_O`S)Bm@&AL+B8-O?j%A3fvqTv(erMH-kjXoaDn^9nm87Ts)e;NeMA>(=|V zqN2c9PcHO_CO*VcGqvHOf7(D^_S-i!+6(+%#v8ZqUb~C#FZp|Fs^AxA{!7=&vGCUg zzvQe{HP|;AA^#N@SN2Ml$?|Ey`l|jDN!bHO@t4&JAIla}crkcA|4nZLMA!l#GWN0) zZF7I6_3pCnLU6yqo1gYL4g9-uY81XwhVG2?=h$qtxSl}i`sj& ze{_tsp{T5UZ2cSDlWh#~SI*hY4)_#jm(?zq<&w%^B*j@_IXK^cWTXFNzHa2uE{%Sn zW)a@sw&X?zS>PiG%2uf;E`D;lvkz0ZEg?Zc?|qK`i(&8bh730ww)0rL2ll&Co2)6_ ziy9)$+wzf?m^UIIV*2vTbNX4(%&(}IO|EUp`ENAmqqK-hXJ=?CBTmCFUT;m-U*?yq zJMN+>b>4%Ij`IOBUs!A=FG4)dR)5nx4+%a-KG5N8t*XIRE?eXE^_9Ntexxc?UzVsC!Av%1-*#4Dz10-Alhci{{v`&(Pxj ze~(Sm9B3c19_yt4xv(h75@VPep4#y2ObS|KBhx?WlmplPzej#BP3m~qUuT=#pki>b z^6Htj@!koCV}*c?{>Ys#^TZf>l0;Yj_vc1;rqH-m!T9LQ66gN6o+YJE_b}vtVL5I7 z&fn{gl*F^d{C^J(+)htVml|2iV&l~1`70w*xwL{_i5- zAr@AN(AO;>a1Ev>AcnYF@qDQ@JYFV$5q+TpT2VeeKR(?914 zA+uxBY&cTURQz^#)WUAUAB&SL_w%)0?R7jlZfVty&&Hc+MCSiI|K0beQva?~;*>YE zJ74kljUw#_KJTFUv51wTM4mR>qPFCQ+8G0}5T2i0z3qQJ6CeDw4UX9gOEu(76_0qL zzmWG@at7|Vxw4+WI=zE8pYs|#-+Q1EOpw@An|L@gxt(_RWK)>a==+aa*Anskf<(Mj z4Dp$F;lJec^_yJ}|6W=zYjE9()=kVU{Z)i@GycJzc3x9e0(*O;U`ygXMOc(_THUmZ9JEbr~|qk3XhW- z%Ei$Z#!C8S_s3!Hu!)%on}3h+JUt=z$-f=Jua@gXGg>gdJN1ggs3u4;Yoger+O@{f zc}-jEJAbBH!So=$pcEz27i33DehcYm?yeBMlmaUA5 z2ryr=S-cTD|5a))uBIOENc54Q>XBH6!^7LrlnMqvM|eiSI`|dqlTNbS}6=w#0LXwcw%fLE&j* zKB(6XsB;x+bo7r)^~6TrfxfrSx3}^|!xR&*%XO#H`JL}YRy)?3fcl=bE5Y?;kjDJO z#xNztS0XsL+QZchCpv%hkKR#)&k#!z1%8~}&?@gt+^wr@cYI1u>alY?{6Shy;nrKH z)7Vx&Euugph*5}I{r!bQxRSxNFk~-erXwbGu-wOXQjyH;N4P@Ts|-tfVVe*-wH`+A zBY}QLyn4?VHjYc&2liol@xrRLHnQ`Jk}~a>*O>|Dm+Jyh!#M`2ze-;JIkDRoO32E) z!}e|an+2=z_w&2D`Bzb7Z10~9KX8S6+hWy^O7SlS*OckM$oM$U_0}Bwtr@R6bgrr9 zp2@`3%=PdGsq!hzd(Nb?uh#4~yEgkrHwB7s+V3$6x!R&N2ETIrX;3y9eA4;so*11` zjVql}K+@EQ`>$XrGfuY}-OzH0c;HT?%t=s1_Okgi2AWai>3IAEX`fLPoNeDj>p!h> zX}RF8`;&|K9I*e45pPhyl-d?$Vx9Y{wn1q4v30ahPqpvXdzvrGhIi(tZf7}ctJ!?Y zDhSInyE$5Tpzy0Bvjq3gXkNTp${v{k)vztAOvM0_ie{QgquwMj3zKkhY9~=ZYVacQsyC{b)3PZm zx%Q`M2Tk3I{jvrl#_1!pPd*u6TYm(QvN2}&Qok)!9xEe3bw2Ad6478EN#fiLS>UmC zs}XV}k2c*1b&qqgO5nVh{q*GD#sTx$Un<$-5H9r4FdzBc>%9*Zj=h*3kpYu(CIFHo zSW{?(O*!E>f$E&sp_4t!G2m1M!}#}Fc?X>n@0FXmY(CiNZuHKEEdO+Vb3{hs-_=ZK zQKESDh@zz`cHeTev?Bd1q=Di8$JATLRoO1>-zX|7B1%e$f~2%`s-$#>bTp-H0C7STXXnDI&G@rocOwvt;^Z+ z)_T76p|AFtvIeXl7+l80zaIDhi>5q$Dk~>9w;b>YTsUxAPy^H5f3k9Lv=s#X`CF}P z8ZNV@+QJ~k+@;?bKg-l)F;UTWNu%8tZkP~@^D4)-GOnx(Eh#ypJKCmXve3viNnLHpZRV*5p^(7RH+h z2|ZE&H7~eOQ|+c)O2r*51&qT-*q>Qeo_S&~0Zvahc;fzZDcb>5>>Q5Zcs5ic3Q*h&I|mw9^=tF zK2-}l?0Azk9vpx9#=t{auJNu;Ov~Mo5Pugw>i?Yu5Pl~mwKP42roB=9X2ERGS}`1i z9-G;jk&A@*D~&p*P^Ih6B;kKmIht#~qjWV9MpSUuzcSL(7Tzm9EgreaWIB#=fblVW zJW}k1WHgtm+%W>uD4`XrF2s}HmY>&4EGB~}UQ%K2olxIKU%J*i&4(YT#Ot^p=`Z0} z_v1IlCJ#Fq4NY_x(a{ar$+m>)%L#0^bN-GTea+o;5 zVf)fGurJg6RlCxM(>y209X(j#?DjM8y|2oj;AS&e?|lAy(3bld#8R145_O3`HA*JR z2#)7LLH%&`l4*-Reo}Ir?b=2-xX}%Fp?`F*w@v8z%c8|IQhtfw<9uQ5B6Wdpg@xb# zd3%~ESBP3}^-aejB(~o$zQTF?nU}?V1MX!=`1F1aV6CpMtx*SVk0QdpEQggR09MCn8+bPkDLWD&j@~X%Tl|hzBBIV8ZmLh=^s5QhkfK zql#DJoME_T>t@krD}Qy4?cSyRx~kQ3?(|2zOt8j1q};T+oyf0E`gFoOwSFdB;(QKn z8HxNl%V;jgiusuTJYltgoc?}s^X*G~Ecx2gW1E=ewn_U4u@})>o@?!Gs@wUU^>qpv zBL(;MMLV0uhJu%K*JU%sX&kHs8k0N}xITQuAivY|>yx2Y(*|2gZP^V-lRFA7%V$mr z?>W1Ly}3;5&+d4l%y_)K({|{q2Fef&H^3VT7ECps__-C=n{?PO+R#NX;^Xr+4$U25HGpDK`iWr&&nw$Fp36H9DGs&1H37b-8(Q7ZJ!R6h z^L>2PrjEB4GPIIW@7?q9e=>lsk1Z&y##gIhF|pZperyv~kln-5BlU4>`il`gLV#y~ z$&Z7^N?pd(XX~J?L$%xmf)E~Lo+FOOyEOFlQfV#^L>M9!iseQ1e$CLtN46P2hL^)S z5g)v@mD|@VYrOuA!dHm%XW2&a4|{qL%wjT~nk84irlsA%7GczZtfy%gVjyoCy$ao7 z0=xO$_wQpKkS+#W$z*L$FC1*}-uXJ|;!6=dwlo=?%{j&)8-o$gHRK0hwVuLgh*shT zdcG=`rLxl*RkS7aYgY#yo-BxV^%PuUzUnuIPfU5OYi)2TGKBMWtaU{0!9~yBz=5S^ z+o1kSrL1Mr8{4GbZN79=(j`nRkqlw)Fm$ zo&DFhbfAu_`P8ok>tdGa`0Z1@q_uCy5vVhh5kh3TaN(dR(vN7Zv4zLQy-nO&;40X@ zB^5K0qBIe7u01p~v~v*i8fvR0jrSksM{lQf68j60!H79Vkn>;Xi&X-Ojdi=@Zq#K) z0`IaiF)@m}Z@ZzwjpMREJ@|nzR_<$lBcCONeePv9oO4#Ra4bmv8L*UUM2I zun3}M2zU(X7+ZI3+$J|26XBxqsrt?h=?G^vH#G@n)pkqHDf#N&$X#1=E`4LQuecKB z9}Wfu!agv(os)_9k#)_3uG~<0cY*Ra-P_gteOad8*WVcED|v#VsnqmD#}fGalfu6< zX9{S;#mnDl|Gw2^`A-f49#y{dE8X>(wRV+!wLpk@bY35D7 z3d#{J{nI!1%8h5EOd~~WtF(Pr$SD7_(odxLx|LQ0Ggk0vt^AXJ6}Ib*!VeQQdNvCL z#U(fe(hG7a${dEpi#bU-x$ytAHQm4s4|-*Y>h18Hc8-?*{Ya|Sp;gqPt%Al%S=YcF z9MZg(uWxkJ;FOihSLNqAYbpG~%46xVV})bX@_OK&Yj%v2HP>sMUM=LY73^y;aa;0; zHCm<=qR=oLI69?^D#p_%QSrCe$W*GtZf>sJjpERd+mgD#b-Pq41`pD`SjE>Wxtv~m zN`~I$r1<&Ui(|2N;;)t=&=r;zH>VXCR(YVSE;kK#bN_Wc#4D3{}FV5La?t3g{+iH?S z>xKo6)G33?kYGbK8o|$&-qOcD|GRK7HNpQ^x6W>ox&yU^G5V?c7Ei!;tDkdeB z+B4%RSL#C0R4KRMs_e8+Hn(&S1k!X|b6@V_|$a7A4L(_R{cPc}aWAKx?sN0`rfx&a0^x)S-kV^mf}Aj1+k|{!OiX0 zMemSW|5v`zKXcTEtNB|0_P+9*QX|n8FK>v|un8n~ z2V3lccLK8KYI!yVR|fC-Me{oo*x?7HIbI({Z&87{B~J$-uP5H0@QHI4wgI|}>$`RC zq$({UVf5RbeFJNu_x~e#p*&{PraB?vENya(yxysG4MKE_YZiJ#;Uv&sOC_p?aG`<< zb{>LVlUDQ12(hN&n$3@PS&*^d$Da*SF+o8RAECW2*FdC7 zHFrH!`%^UkXp_6jcB8ctd(qZeJf9(ab%^4h>v7YA-2au_zWm&@(!#8)*1g;sOV}Sy zMZ7uXz;gY6gf3EPhDf`kkF%LFq1>0Pf9O6f+C}sJ|2mj}N>>$b_QsK>gN880{(jQ# zu1`^DOZWD4neKH)uDZKm5i{@Wjbz;aDr|2a)d_;+<%B@Jp^mx9!*8)7echUN7;pL> zaV)c@PRE7!fB$093)V%sM&2N{fmAF}whmt0)Y>ZlwJN)G&&8fV!T(o_%dc{}I;*|m zo^~({v9q(xDuJ1N**~~rDU*vJRMnR5|1>y*)gLpN#om|F0a{fi`)%!p#2FfCmeSrS zLNC0q{_h8SNa(eJ0`y<~a7P_WBG4=GEm^QhWp#D_&feboi|gBwx5ZvyZIZuCt6_{9 z5egY7yc>PYz@D)C(t&$(7qcRGs0zzR=z1|`&6-XUqsi`y$j9wGunN^^*%R0j13AOt zVlUEGu6CVt%`y^6lJAWz3NNa+I})0bMRpX&nKb6A7QTJi{ISwFDLeE(|99WMm~-h< z{evaj)AO8R%b}9b|0<5y7}re94Mk1fvfsl9`Z?r4nol0tfo;c)_eKrznFL>?)}{Ev z05~Rak#wy5xRJXv01M-YHkbxdsI93RNNU5|poJEl){`elmF>{cENrIWIZ$ zEMSP@w&VlFoX!WHCX#QD+EbH#>#oWLRwBSR(QaW~+&$jY9=W&mW zSDP&?GJRbiuEIq!;OT=Ow4-2ZVj?y3>fwKZ5_2Yf$3K$0yNb^w;?KR#P2oOjp}4DAIRMn*PN-Bv;7u=KbZe-Nr@oeJ3=f z-N0X~$SWgiH145r>ka0@-HR{P4d1jSCyp@8(7ZN-v}@e@Y2me#$g#jdeg9k(+P`a3 zF|2Lp0M+V&N!Rx@=?kYyqijU=nc07$X?aYhU81jz&x2G{XNR%ca=mTM^Af}fAII@C}T3*@mf~BZrBzi z3+~NT>9DfK&QE2~VE1@0-4eR#YC?hKV^pq{YZgwetI|mwR9O1CE-+P*mJO{kFe@ge ztSlnURPQYdEp1k-fvtQ|??V347MkW7CNJGsy;-W(TF}LM=kvu9oG+Y%G8>K%E?99 zs9%@!(yg40moc{*uM|Gl_({x820Vg${jj}yXIAZ2xQoB>TI&|4syfqj2NAuLeVH_A zD;&m7v*Y@**IM5lOnyWM-#C0(k))Z&cPfcNFi=X6l|@@2qwUj^^L+5-cITgB6)pm& z*yeBvENmi~6(5P6lWIPae=6N;68oN}d*Y-=@%LZwKljP93f$nq6*~-pnx>8eo&DT) zZkW%?)Ew)OA)*^2OEyhacO4_@vm|A@G#8ZvF`tYUwqT9%!<+S0a@u=O2!BdsdD&|z zCpd2)^1 zq!WC1^o^qG2zYbU{qBcPjdSCb zW+u$AfU+kQMLLtgl>o}>N=SdH4V&`D z!OE`Rh>!?kVH$h4%;%)H!=GDKG!o9q>|C9*%z8DVZOw z*rNYwW=iYvbga@xkLmo6_91O>4=eqtlJE){F{d5wr*A1QCXI8Ru_=krh!O!mpevi2 z`Bf~~l5gS$Vt$-yu?srFX=!g*4>Cmz%D)o{HYDyOtDX(3)%Tj!%X0uPh&fT<}@i#(> zteGqXE7sH4sN)s}x@N6;Tz^rXjOOvPOPv+#m>AzF%kEj4WrDjudJMsbq90*3xAoU} zkWL|v2QzVAm0BS7Qo8tB@fikf)lH@rTZQ%C!iOvemP$mQ^w@8!mK>s3nn$+K z=HG)C-tgGR?;FDHt9tMk3$BkM4@^A2RD2V%^7gVb>cYW$xYrGidsW6Hz zd}rlf7{af2Gx1Z2>aD8#>w+S zbZzBcGEDSx;@SG>>_xV=uAd0P0^3M|WErjALJtagLzWB1$;M|Rl z*7~zJWqY#Bkq`C1cute+J~N=Q%I-|?habH)H~niuAUjJmou>Pn6J@X&snVxBJgOsc zSZ&H`ni%hlEni;jSyc%c>YCZ|yk3&c*Gdg%`=>~a_J}mG-pg*p8T%AZBHN9#66cN5 z;R2(%pNP9@UloHT|9;oZGKOoWTsCi-vwz)!Zb~KT-UE#An$dAiH%Ik`d_jiDV%Qwj z1Oqb*3P@ck6hef^f~TiyPik|$)))&b`4)HW=(_y-24$%z*%LVWw!@7!zBDB0Scl8( zzF9SGBzns0SKl=Ke)k3k%NfZZu!0!sZ!Ao@9dqqrM{smN0iodJAmYy4<6skWGwSIl z3)rD|Jqg~;-CRa_@q)tcup4zRnlPki@F$uKc|n`?ofP`7^ajCb7>9`nJ+d=*;wfJC zsuN;PTefU_NIm|1b(i>^(-}}q&iXu7sKzHP=loX3`Eerum5Zq%md}fxSBND>+p{u0 zFhJ#X!uwpS#Q%)`o(4l^gq{%}JAbfo{xr46HHy`De#+-G1m$G6aliHzq%%4nT(H3n zNE0{nz(?qd2E)qmB^4)LXOM4ui-2+Y--g0A>$DR-_s6GrK0mQo528&n+L7HIpulE2!YK|Aa$ z^-fZ}dE%NF@}5wMEMZ&~G;w_T2PyRWxpG-gB9=*K$ZLY){w62g7Di8X&vfRYiz7-7 zGEvCpMM=-Qv*5W)d3%2;StL^rvd&lsd)lA0p@knhZd7q(1|~>%PaQ2^6|0Q0u4hw4 zcFY)-TAin1NH}9yInSw7TFQJrs~ux@rKTXcUuZRR4{m&emYnCW$LTVqLa#K&Sp8}! zE1h@fzKDF~z3Y5IoG%Bh=nUE`d}|iBIgu?s=fLkVgob_-MfPW}ZQqq8R691RQJrYJQMxX%$T{P*3#cGHU&udlNY`5>}B-Dw++?vcba zp^{pgDb^2)u1W*uH&-5Y8zPv6wW zSnI2nD&zanZaCf#+JDtmwFV0V!)L#AQu%Zv1&`oG^#;tJF)_Z8ks~1hlR7%@v%EHg z%E7Mj(^1djR&D;4Sjo*4^*5FphZVCvOOpL z;fX@CtL??AGc==p0s>ar7FIMc9*{9G+{je)qb=R=37?wZ@FEwOWx0pA34Nn8h~FH* z2{Qy|q2G4gPycsjrY=W*NS=ayYsanPe0Fcausy8ryl>Of;q}BXO_G*>j_!ZG1J)}W z>R&TXE%QW6rj>MGe+A(NCPI5JU(mEE2K@nr`z94W+OTCp*JpPsr-Qal8vZ9-26!+a z|63{aYA|;gxsGDHHM2+&+Cu+JWv1+6D3kd}_&$r3t!I`pqu^svqK_X-+O+G5ja5WJ zS@Nk+R8q2Gh!5HsS-D%~PVW~fa-#GuRelKUZlqiR4l)Uxj_Spay#G5)atE;h zg3_f+1d|jBKNx~Epcy=|1+8lE|cUZ;%T=h5t)OmCe^x zx>Bq5qY6qy{QwPHXeginT*mwSnzE_Q*1Me5jM$MvCz@amWFADq=5gBIq@9ev$(jIa4_|Mo>pb=Y#S;@fTQMVT(2rYJS48 z$FV>80M?_$kuP%&cioAzmPMMpDiK(-7 z^&DH~zJLty|J>#py!PEXo{j@c1dk8=xui5Tce%D5v2bt>@=W@EQR-oCCQ>ml@Y^r- z=VFUIjKA!nISMy2xeUzbiwOTimy&wLs1uD3g6+qzYOn`w35Te!udlf($xH8%3rPOZ z@BBL)j~)(h;+@JvP&B1EqHgzc^fsjxr1tCJ)wgmUtDqI5I6TCgbpGHASRvyjT_Zo3 z4R+X?Y%4VrQI>i0l*GZ%1BFWdaiVnEcYblb4CK`3#fLH3dyMo zDPqAlkzXT#|LlrkAlvKz-nqGv-3zr_FlSO^)Z_qd$>%gQYHMzpSW;uP%(059!~ zEsarmZ0#YEp=Jv`31w$XP(x5mCfraucsUHKY810_xphn;Gv|Vh7pSDJtN)cBWIn;& z8ZG~*o=TgVy!{{@oIAv@A|e9Fme4 zh?jU&=XW+HJyxs7PZ)Vz&8w=*ubx|m(@lp#&&Sj55^PoV^cRXXdRkjwo1d5*4;?2E z2M}|msa)m^D&2@;^Bnx?f{|U)Zrg)kjtpVfs0!Kt`W{6m{w^~aF6)?$WT$YZWoG_4;`6&# z^U)8xg@HN{lZX@h`meF`BRuHU**NlND4?4OfM#dh-S23+a|F^4_&o7GLu(_u zrfe5t1Gz^!a;gt{^eD*wzn~@loul~n0CPvD(|I-+3~Fery8$lbnJ_b@*WIZEtye4< z4M?k^xcBh>?Iib(H_-N3NGz>(a*u9zQ5`HEbs1rN$JB8WU+R}FfVD^2?<(CZ_;7BsH=S6S3*ZN;fO{^{~_ z`4K<(;VO-F4$6g!y)ZMrS&QagYqFf0mWG8N`6pa1#P?&z^i>yj<(W8kzHF+O=0@O}wJwZMQlRh*RqhA0mX1g8H`aB+FTyk|n7M z;7^pT4jiBLBEyb4=Yh)qK1x@$5)0k|Dtg0a97Z_af|Q{)Iy3i(?+@wrAAH7nshdvC zW7GK-IdI0${~0u!xBBba;`JT-7QlX!3yzz95ln1knbu>1}`;P){v z{0qyAr}Uls#9xy^MkG>NYF~PGw8`(Q!C*W^OB#5d*rO;AtyGm0I`zHl)z?eu9D$Jv zn*d&d@Zb%BA~VlYLPLRSX)b1FA3|K7q}0@Rn=av;ptp_WO)gL3lI}$jyt16fa@ang z;-ZNLb!>GgogM_$!gg~aDA-VY!C(9B7Jrhaw@4fpZY#`Ta5^@jVPM!34sE@GUCi}$ zv#{*hhCK@PC|><%eCH*GFvb+YFfLXaV@PA5@Cov4z!6F=u~?c;=1`V7DATD`1Ruo{ zzOqonG+-eZdin7geByEYm-EG&&mKRvj$%p`fWM_{XIB$po5;d|;n{e2WeP*YvhZGt z`?J~13jv|!F9dnqmi7pGzz}M#julhE)j`bp;S-BtJF;X*GpbTYS12)&gUxI~AbS?! z^mw~s8V@e#(*md46o74iMFI#*?+QgM%!IevUaUM;3%uqf>)1^cHQ>71zv!fE*`96+ z35k1aC3x8r@lIO$IedRG0^zH_yL%62dn+H?qQo{th#Sr{)wJw{s!hxEz+h=odZfgf zle|0&9pgO`eu2M{I@m!%glkPcPzU!iL45SWC3g;)7ZTQ_U6nP1tF==SzY>aEQ! zR6pb5Lbhy-yy;_)ND^4UoEkX9I0{>|l^;{Q>J&|75vFitdxAfCKH)Cn;W=hZb#Drog56}_-tYr+2wQ5g0-;PtB&X_Wx!(E@wY&MwT1q**a zqM3Bxb|m&0gHH`77}lUjM9}HNzb^25Vnz!xiHZ+%4I}8t-Gv6m=9U&Tb@hq3hp3K@ zj=p&G;?Pb4*nC4%lh@@rBH?5OuqTV{ho^7a0aIf-kk)0~bL|W9rH(VotAt=w2CW!g zQiHRURE>%<1yiQ;WDAk6W0)%}go~yo8J_k`=x!rAc+XTAOe4^Wf8rf`LPq*ssCw`_QR!QejW_dIJ!nwdqaCLL*`cXS58ncp2PZ=K$*&9gs}|y=qewhrY4*;jcVK2 zsHo%BwWWC|KEF-7UNl|7tNZvVp)|Y>uMV9&48hwf4GIN<1fT?Id~XprvEIuR@Panm z7TAQ;NQBec3sj%q$G}+XiY-yxMgnWWjc#UW8d$1o6(61hGyL@NoS_fgqp(2y8!+Z$ z(<5-3`uO{I9(f>pi*sKg0pH&kdj&zB(`Nn_?BKy)Q8vB{Wh}rH{jn&-b@cXji!CiI z=aa@g*?fz$7a~NBFqxWE%YDBb60a?g?|W7 zQa@H@GN$*#zxeBAN4i~oc0AiHZeG<9W~Voq5=7w8bIXc~pCMsMq6|`z){7>6wL%BN zSdI-t7_`pOE)Ip)3ngPFgBG%A!+*{0$c)e4o{jyyGu^h7CDGmOG2@Jm)sbNNBXmSC z6mQ=$71AB7dz0Sm=0{2+?Q6kb>pKD=Q0=h!%XFYb>hPsoRPT@Dwc>r7j5w*~$ix94TQSfC^ zEi$+VM#D`248pogX>A<@!u=ii2yT>tm)SAgp4nMRi_ra&Bp`m7I9HNacQz*GNRGVxqZRkZ*} zAB+qw;Ou)JiDQ7zOQ&5&EJlQL=hm&Hv^3Yon4O`x#8y`haR$07&Yqqi*pLOVhav!FTE5-GMnQi6 zr+Yq(amcfk*aL1rHhzMiKpKL}s09!uxGi9$Cjz)-Bs>pzjN z78M)s$oz%sWgMto`g&$RB8Gb=o|d~*a=@ZMhHB*tPt2`@fwN0&;$0qh+q#w_%9bE) z_le3WSro;m)1J0u#(&$@u$INCz)tKu^N;4%)6O5;+e{Gj|mt#4ivO;j1c> zqz%u)c*^S}uKW*HgykTo!lP47GO-2-EeJ%i)ee?MJqZDll&Csj=>jz=gs88KWn~&g~R@CS_Fe^qPFbUaADrDrFToZfhZB zA2^@IlQTU>b9QP*FcTajCur=c;+Y-kTD+DBYZ=8Sk))mN1}v~Z^~K6)%OO6xuWD^Z zGPdNvyY%=^6FNtpqZwO$kz9O&QXnbf>}bFa>-9oIX*{N1c)|HXKYz0jVqxmv2d~~ zP}+zfxU-SE3#87%$#1=)c%4}QKW6|k41_cJ8Yi2$`1nkf{HO5Le<^0lBhM-X30)nX z*YNI`3}%2IISu^pUJ$CE2a7pfU2u6`UphY2&deDD=v^RW5#D?aGju5?R*FW=g9Ef1edip=H}WE%jJ2Un(d zkx5|D6!Y`vBgk1q0j`JJf{ld*1!t*zBB`6|^2X|F)RqR&7m(ygxN>3prHTK=CrD@Y zjf~Pp)qyzXB}us;h$kN{*$nXFzTV#YzP@0{h(M$L6rp^!cSZ7i@DFi(VS76hm#6+>r1awucSS+e9Oq&AqLk*f)t_Sk%g2>cJR{vBDl` zG@m{NH~08prgk*PFA&1U+2hbf4D8k>v9sdV9WvEH??vF1D4M>*pn)3 zwH8@2OO1xDC`YTAbn_nNL37z?rH)!T1%;+k17?N@bcdKr~~ z_ZJZ~OLA1Jy!<#E_OCSQ4C7)4D%I_LdB%zj=I&T*=BgFx$yc?MJEUUji*y>Uwr;_V zSp?%YgsgeZFeeQJ^5)kAKJ`hpI5uJ(sFRtSOdyl9*lJiqhO7q*LkC2o!8NCnYXTYM zkSOUlFB*#3m#hsJQ&0tJ&NftV$B0NY|jGp%={`au{p|t>I(Ej3?rgoXt zc+bY#%hp&%Or@;IWVwNLkUZMg6|!EOIIR9nO;C@?eP6b;zS5r2wzQYxCDBYzr`%w* z?09&D3bp&3>4`+U(nqCsrla3{_uWNlM*Ksmmr{Vb!34P$xNK&fRDx~^E-rV#zX65Y zajO$@9u%ZiDwsA910QQD!8?$IN6)~Z5wIsHuxhFY7a16tL;#Q-?zp6iAsL7rxQCEe z3xB?O`w6q&t)J`#Hnz5UaL*oWO~nHFeWw~KB_1FR_8kBz-WnvTYqE9NFo>otbYIP=mn8%S#XTSFg@3 z1d!_J&b1k&CTnFIcW3eo;&FM7QoEk9n`mH6+#K?8!IpxdXU>Z2TO!Ss1T4RZP+;j{ z*7OAZ0mh+PA?xP;UM`U%a^(MzDAj#Afmi<#%>FBucy6{^>dmy!j7P0AM_It7>(sSxi zORg%cxb~N?+}h&f65;nNNim$7QV&({h#uZtUs#;q8TF^o6~8$kPBcAF%7*U9 zrbrN0PwZ6OP*$f9d;w<1I#(N|l2K<%Z8^xdt7Ca)1!zb#;)+e?CEfF?{3*_ zt0oJQPOVzE#1Mv|;@rz^xjYRnxTio(xYK`R3BKliDetg_2|yy(LP(z%xq!5lNTLJR z+(RHUKP2VvguDn@`a;+QXCg3gZz$An!Px{5aRMMcZknP-%Y^R2m5VgMfOwdxl#K_> zNNp*HumQ+Y9CP8oMgYPr)8*o1w_H1a*Yy<#2mX25{|r3?Jk_g}Jh6Hjj-IU!ec@nGgcG-T4A{j`erP2UtHl!-HqDF(Iy55)E~IGIJe$p zN=(&mlb*Yf%jw^yJGSdiSeRzn-_Qh0Dx-u<)i$3go*>`tzk=Ihb6HJqZ0&G2MqQ|T zDj}G@;C1b8@eOc~;I=x5+v*OKq6f@`{j7U;aIHlc?0Jo8w|waywwz^fc&u6T;VT}7 z=kw>9Y^OCL3h3kZ77go3B(Osk5+2?)7n;q?I+$K(tnyTdzj?U(K6Jnsytc?*k^3cb zV?f{RF-Xn7e(Q}DGJdk{4vgcm3;$e8CD-}qJJAT)>QFg0>^D$uZ1X%JAOdkytTljy zHG3v>F-4Isx3kiLBJQ>XmCwE8qb=^{Pt&0VjLKoF8LHn;j~X2%t+%G!g;pF8dS+%q zW@g;TF83q?#|~XdrZ--_j}ynfW%Q)WT*UnzApW=j9o;X-$(CT;+j4 z%xXPN46Hn4bqPA{2U}YgbD8-_q`x#K>dl+nd8&o;?dJ*m$%26t+ILJ_caKF0Z7(i^ zzeC(gdEPRUctLFI1`Wb?9v(eI%RiT{a=bE<4PBb06~Yb<57CYjQ<^XjVFL^8*@@F# zxgb50zAOejVMQPPp2XVb_z6tFH0W~_co9TGt3!-m$|&QK?_LB#-e z1!^2(?hi48gCEz~KCH}rZ3(lbql_Tge-Zd9U9mhzDt4rKROrx?gNiCLl!^iClKMMY zgwbG*=3(x?6%o-UEZZw?r1O%KvIjkl)z0pt`2@<|BsmN$EUkyEj`NCN8WE+}qg{I6mtji3i(X6M z5Ly38N=Sg`Z{;f$(}5hu!gtT-J)fZ*w{OC*B+sk&cl@$X8fxjXwtXpeC{ zV~W%Bd-H@n=j9oeovT6sdFSQ9cC|x9b7jo)aTW(2#>u}ES}Rc@g@)x0t4@y`kn8(_ zo*%<*2|B>FBZuS?7}DE->(dTU=y!uL`Ht01dh7N+tQUu+%Tn)M?44n~!x-NV8ce#p z5`%x}8R<`|{kVn&Zk_X4C|)#R%?g!?EjTKf4d=9h9~?SQ3T8+m#``w!vFd+M%&#Y# zA~gN(r_=%BZ_cx$yqkdEX?!(^o?M2O_01!k+?b=xVoH;hqGK*K1&Po}joV38J`rsO z28K&N&JQsTsJCXgNUE-_nJy#3$LGfnBw2Gt zNuM3YKJ0;q84m)sHwK#YLUC`}-P70w-w%dIA z2bLwsf)_d9#%Iu|*pXYQs;tCS)h`1fM>39cH}@P_qd>8`wzj4%q+bTL252C41%H3K zeIp%br~BEQ^XzWU`@%~Z?9Fz<#QrERPc{?n_2Z6{8<#Fpwh}U-0@kC8i7!}6*A@RZ zA0;wu^h-C{e=O8^{q;wnpSzp}#~FI$!AGK)Vc{3{#ylpFn% z@M8r+RuA{lEMs~VUtASKHOt8yJlKCqRl7`Fo7_?f6Q?*N%Cd5sntW;VZ|-rkf0Pu- zF@uxi(>J3tj-c?9Gc1vf;^5@Jtrr} zpHe2_;_N-!X$mL>ozeR;;>SqDsIf5>LoHJIZw?~uDbW23ZgyBJ6bRzIDX_Nr{(#^M zlzae>Gy)%O_;z1`w6y`Tt{l$MS`!#Zi5d0=QL6{6(F&GB$ZH+4)*T1(BdxClY+ZVUCN1-}@m zBB0tGEw?5G3*#k_P7nT#1i{_@{O(QAnZSoS9tvWF-55FYXo&GQ19CTOzJ$+yz%S$_F>1NF))0=ie>?rb3!5_@B&0r1p@LCsf~4a zQ-aA=rA9Z1CrI-%R@N~1f8cQhfg?H)k3X5Y#CUn#28Q_x6q%paryE8`qd;ib*_jh0 zCLsX|yCJx#14ywyN9`2|la#}7L21C$iim>)JIt=6K>3Km>%8|39Bja<8{}!BWLdYs ze2G1bu_Dv!Ic_Vy1GJ;)Q3co)-{R|Ynh0dOo^T*wLfBjme{gnbrsH$h4EPj|Sm8?3 zUs-mZhIyh$*qmM-8y>1IrpV|fa@7#NFE@B(aW&2ylCR?6P*wohwi0aOK70)VFKDmH@34myYoT*jU=eNh$N=2m z#tL*%0p{1e|MNnMj-H;GiK!mcD2v59fFFu!;O#730lu`rdWN4?Ip=OYFinxK5;QHa zx9Kk~FPlVGCBRDRKSt_EB@tMOEa0N>x-_e~!}SpqO%Q~TWM2^AptoN*&^kPf2i*tw zK{HSC7tif!PxQn-U01PJJCX+c>oZ(5et8_9%@}kT|Efm=k(D-+lho(q!^rQX;i?OV z{rO3&R0)n@O7jMX(4$)s&{h!#TPS@9+xqA)10euEe)2>N^rW4mBO{0m6%`fmivNID z0m26*6Vp%706}>A3-W&Bz81+ak`C$O!i@q|88QKFr*|1y^w)3eq(4gA4F5=VRpg?YfL4&CP|mi6k>=( z6zB1~pCPMgWN_%}LQd@ zeevJn@!^k&S{G^Bk&@}2r|?wV-7Ef5g}ATwzXT{(#&nfsvEde*Mpn)F>X*aSH^jb@ z*l`{1Hp%b!`#`#V7(gmew+SyK%Z&Sh_jy!`Y%IGa*l~S@h_XE9G1qV#!nO!p07yv> z21BBvGL|iY2ptOVBJ3ENPF1mOZDra2)YsEP`g=gicXD?23x-)B%#PK$^B}R$Lqp+k z!u@0fAc*?p=H{NrRg~$$))O?r5o;P@UcZ&W8u;&CIJ;SpZ+*B zEUXb;4x}Og5)jb1!v<4K_<%#NXdR6V1aYGBxrA=0JsMKW~7;}we@y+o|n4pyX0GM z05^?hJ(-@JE%D|Bfjn(E+*&{3i$ccTU7G7iG7!rU*H^J1L@$~1$;)SK%eUcV|tB>g4e^1R+AOX1Oxwm5a+ZA}3Wo z6JCZDfsP9x=Ml`e6I`dsQiI>DbG|t>h1VJTRF$&;C6mrnPzRnhe|X}d+lQ}5O73V5 zrqfBf0W<<7=BsIGN8R1iZZ^lm*evq)9~`qasShRgRKWY~2Lj<4J5}xId6dqTe}-2? zj2{8xg}yj64KSVt;y!I}6*_sxE>zwoV?|5mc2RrkuV14;i**s=&h7Wx6Oc84?dk(= zhYi)FLvct~WpoX=z$KXy@&q^)s9AE1NoK?v2#At9_C`z6- zq8l;;_z?AQ0wa&rp$3CL@C&#k&{8l37c2O-KoFJvC3FwzYLcA%9GIfiIQOOFI6p^3 z;DLDC8TyAGE@<^~D`P&#UI(1KFCzB*(c3Ev_E}A)q9HXYcQ(r@*5x|)LA*JB zzlbnXc{6zYF1L5-M(ovkzpjbBd+B`^zUboRPScJvQXbo_lj3jqrz~a9`1rmOsgUj* zr#gkLHT9+PhPSgv5EjoQfdS3T7SxfT_pduII_fL$nS#Fi=vHz)QL%OiL|k2c{l5?q zkzGizvC@H|0q`jV^AHT^B{juHMZPNpp?VBV%Ak|{2Bu1gl{Xg_z5);xzCU6v+i&o2 z9}y6|gD?k8e1Oe{f&D8q0lXk+Lt|zZc5Oaxj6MdIJM*c?Sdn32S(y=puzB7^=&0)J z>iz+hkn_VV3LYL_7>&Yr7+trWZ@dS3eej@d!Kr9lXe(p=01PXz0KjJUTl1gQFSArw6wtn00ta%vryUxQ4cA z0_U*?*=au=ICH`;20(DrkvtNxrv_^BPx#TZOG{opKKCHLq@-jhYdzs{po6)Jw^C98 z@WBe9`2msQK|O@T4CP!xI0nBVzXTBj{Lcha#k1_0Un*sN`~4f)7inl{xCs{=9CtDJ z?8tFGFz!JCsa~$m4LMsZ0xA|<*pE7(;lUC>!~vcc4MT0MKNB)M@YH)b4dS zP9{7nWGsW@30Ic$#R)Ta;SlFQ~E0XHd)snj+%eRN3yspLW zA4247y?yign!?t`ro>`A%f=6|%3&PLY_xgS8QL16q8%cFf`5Bq%yp!*QHvgv-AlhS4O)Xe7kP(+9L&x5HHVeqO@Gj~_oWU>L&% z0_2Wf0AarIp9rx!Md-Bkga%ha^F%U%HyXT=us|w^wX1<95VKk{q=H-kxP}J_&BNYI z1uEF>CIG)~@E{XXS0@4-K1^~Wdnn?mA^)AJGaFIn>jhg{A^<0YC9QI&SuNOUGFP@p zKtoR-QbiPizlXxvg{0(RJ+N4t7GHo3RNEB+2QnGc^HR$F8mlwI>B=&}@J zNFfP&IHD2~Y`x^n%yMz!b4yE29SL*r@cmBj!^nUX9O~WO-RIQQNTXBmMeiJ0pMXmg zI&Jd>`ru>&)sfJL4+ST7gEh`}SuoeWQUgsG1xRR7pcxOHlt+;f5PpiHhTz%w10nmv zZ2b*5i$7pUgeBU)vGM<7>$~H*Zri`VEjtoLDk7AugeWVNgeWs2Au1z@hAkNp*(+sK zlE@~qDl1z_R>;cUdq3~b_4_@q=bz_(-LLz)ZtMFQ=XspR@gB!f_DOa25fA2D;wbl# zDUDk>by8GxfUw6k*kEdNnML2kL_Jnim;EcQ;DDexjvYzfCtZhz%-_73?Uw&NFnq7v zHz7LFZ~B$+{h!+#w)&2-#eS4sJ&By;8TrOqnu+g=e-9ahzj23D*#UFy!&}{~5ITd% z(GbCZ^|vzwXRFw(R~maRaK?sVJ}_QT9~uc94O4Pi9$>oDUA+Fg&(J_sMIhoCWiCcG z4M7o&le^ls5FOx%r4BF1ulJX?p>a3}1o?aSW^P*S0!8-{H~UmsX>DaR?WMSl+#l=f z*AEHUPFrJ(y>s2%u<8ufefljh8uTl)a(L*Fk{`r;5y@G> zO=Zx1&>XBymD1rf0MfixK{gIP&-h>Zh-+s@yeIoGm9gZ%mQ$Ax$DR9=TDekwZ*tWq~pq8%^& z1{Zo>o8YOIIBl68K8IOV>k`sc5&S$n7cdc)|A^C{hj(^k6z6>WeY2Z4sR2CN=Stbn zoJA6GHB?`K33^j*up#=$)c3|nJ0F{HT$+lH(>zwbhzB62doO@=2Oh5Iiwqcs&os&i1!4F)$n^hIzK_Wn&Y3o3K9HEs4v3LZ+6% z*3uF=I;JG%xvYPlLfy*I%3EIQDV+5@=0 zWCs*195>9&s3vzaF&&AFifW@|TYPnOU1BfTSENZy>AR$RB_O|eOJHp^$9#Rv%1yzj@(6|8 z#$uxqr!~^rJs2Ms0uU*&#EM_br(gMPQc~2BX-W{{brzMwBO~gdd8gNtZT$>WFE9N} zrnG?P;ON!dF^hrW(b1CdJsR90Mfd$n#JHqa9#&|YnDn(6yNF&*fCK_T!xgnLmS5d>9NZy$iSYrSH6)`Pjt~_Xd9yE*1ciNJjlm~Fg10+AKi~79SN}HTluQpO zfear2g?P-39 zh!D&1j2)N0qN-Z&cqDL{k&v|G$N~l@WLT|plu_k{2qo%Xm5T{uF1xFgkx`U2HXc{| zH@nvR_b+abgqr<(#EgX9kQ>l-R8!RvGcuo00eP}>QLK#MtmB~gRzMfZm}j50(UO*K z@`&m7_Y9QTjOo~!Au%x<*D8O1(0T?e#932{j!l{|qNj=udzTQ!BR?3pdT@7D%TOho ztJ7q~7~DJWH4^LhFBUcIgbw0pSarbZMdy0^tINbANeE=g#y{1+xGeRM+elTQD!dF? z2XvbiKpup|#BO5nHVL;D1k5H2V@ZH`RU*z;VU#iiK~TA%tly5@iU$kCac2oF1;v+d z-`r95GyyrleDu~#Q)&{x+Lks|Kmr7imiOEFz0!L1q2*y~*E$NtJ zjc(O}RX4`|0hApTDxKLUdqlpSitT2Wn zs*O1oI%G7=q%%m=rZH`~xn~tqf^HynK~1;u@jgsPUU(=WK|$=Pj;o_-CF&W*=U!k; z2TmuD1cP`kq_ycvw%&Xq+rxZGNPc?oyl_*@7!-B0gm!n;r|Lg{ULsO$B~k>O9z=zR zbWI*()Nx?gv~C~;1un?8&$!6tWqq*Yz5-LzH>(a6LArpb{{=%Smt}n zN_13Mc87LmaNj~=Qql$WpE6b3U%h;ZLM2;#`=$6xyAB-iOH3Se4ZoUs;{qlXxtW2b8&C1RX0wA0PxL&uJzU) zyH0#|Wk<(;$UUAPIOUECPuyDoR}Gxt#NEGVNXWT$;Y`8EC!&t#IOhx5J27gU$ZH94 zVoeP-|Hl>?G}=CZa?2C|QEx7)ZRB9?85u%EJZRb$0{8^4`zh`U>o@yOCkw{$LeE)+ zjl!dw$%(`MA0)SSqrZ11s8?a#40jfYVZ|>yHERM3MOb?2Y;4@yM<=Dj9im%aRwn7( z#8MP2*n*(32g&ts_d}dcL;0A4&IM)d?&T4^21AAfb)OgQmGe?-E&0|Ri#+qrsu&(w zbJXB-cGw|`B+rA-@GNF7?8N(?F*sIB_OZW@U}qW@|HQ3d`=2kfzde;5ycP0XJ0R?t z4={@2)ehOE-k)4hEfu|atnpLnO7w-KjS`Ne;V6|W83i+4eTmkH(fYG2M@xeOxa^^Iv^_cFzNq-9C1QkMdjss0Q$cR#8o@ zVSaXY|JBy%{|*-Q#_h6k_Ot&aUDm!H9^?uZl$&P}2-C6Fow~L7^$WxNsE!cN*dxwv zu_Reu9!6MKMnH!Ow;;XrkziQC^Zu1r-Z){1#Uu)t1tq)v ziAZHm!#CV@<}DI;jn|*ah}E_sg?t+n8!X3;oAs>z_D${~g!9RWKf9s2>`1|%Ro9%9 z4!0G%Q__+lJ*(X`x|pzh;73QN>w)tQ3fl3_n;&&Q+woxM{Q7v6$|NzL>V!^Tk=q9v3xBvG6~LP2CZSzy+J zad{^WUjPXl6kBnljQo0xkn%%?1@+yduZ9#p{k)*3Bq6cV1t3ODibnwgPC~GOSjAr& z8t8!gLW5nFV_xaEPafk{3G4z>;rob*yNHD`*ih-HF$lpgY>q&h$*x0A*xKGsL_`Ev zAf6~>?c#Be89Ng-XGH~>ryP;T5)wON_h4Bt3SQGsafPM^6(cb)2D(xVls)!TnS0sy z{zQAGy!k};De4GSnxkV^IJ;w=T!Yn<)VZ3-E+*%sT*|OJ1)zg`+csx%1F3`i1BYKk zGLPIczs<}W7C0xlXXnm~p-Q6LJz129|C0R zg|oyRghKDp=0-*{tEcuIU&*nqRpU3$TKu(#TcA^=Kc<{{cvQV;^$JI9near!1IDb} zI1_og(|#{%rj?CoCVu(76V+!P`Q-ayKq=G7Nk;vrFmut}gHn3UrN0zoFH`@HPfe|O zeyZ5h`F8OI)E{qf{4Fgn6SBTD&5`$QZ4YC>8csJ-eT~nt?+8tLru6?uRhM zud>^9lYfHO;Ijw-utcnI`}*mH1#Vtm<-&}M%F1sbVMWg|lbbge+ho$t&hUSB-IPA8 z;CBeq1C^DVrBqOoH-gdvRH^w;eWM+S)=V>+|1_bYqm$U+ z#zxLroi=AcpMJZVx%-p`^>LA^hhtfyw@gn&t+Za)=DLugRi13jmH0UvASH?2)!LzC zt5T7b_tvL74smh$lRu1&=Vn&_(RQ&gBg9DpNYXR%?c4QfQBJ%3n5MK9nO)=wvTfP2u1CY2*@xV6e2OYzPR@;jwqJXldz@pT9pQU&t96vbjN#4|RN1T^-DyTx$6a z4))d)$nH_bIDK@nva*5;14vC!-bKBxiyX_xxs*b`ball5U=F$bMzf>w>#sEnbHq;Mx###`;#JS1!c!c{`TAoNhAZkbR)!~Xjo$|TG}XRHDK?`&}ZVR)>o zC>j!K^-p&U5bT~oGEd4eZ+?Ampt?3%Y_h0&hOI_UpD-@1*?VPSZQZ6f z-$qvX6HSbW(Z(i~1b5Y^@er15XL7qc?Qu4B-LAJf;y&wt-%Naj957E)bZPS;-FMPN zmxH_Hx%~kn_h*V+E_{WxI1O1-1v!N5(xSjVaxuTs<#MZ#a}tf+{k#XGkC%!s7MZK) zW`tdIr78VbICA0g+UyM#f0{1sGS~LIQ#5_ymYnw%jq$#g>J4jhO_-#W$ASxyTkbn? zgW`x<%vpdb*d#thTnNAz>kTjJE)^9O^h4xem(Ah338_4>evv^R0Zt+NgzQI#$$5lV zQhHVvgX80CRMgZaP;EhS%MntwG@KYaH9c(v;AE`hQvlLC0=U80$l=ELZLm}EwmxGI zasq4f3x+358NNvvC6NkZM}0*yRc=DKLEyZkprz#&77hW=`t)!V(6+V`R~ghn#HK{1 zMzCXJ9l0S-;?VO`^0weRS~*QUVFBtGk$C=lCzt;&0E@@P5QG= z4kQUQbMG&=iN+eQ+@d?&cgZZOS3mWhkvGS3?Z(<{LlJiT-0tn{hTIlJCecx_H}O=P zt_%HWV}+#SN-9DttpE%8SEXyzL26C&pAm00vxSeS=~Nr55BhlEb;v9Q(ZmGe|b zzd5z^jN_%PX9DsDFEHjD-|_dtnr#Zd$|W7%C7$J~+xHym^aekVe+jZ+b4XR;76udT z$9ws;hqg-@cNH5D7s$NprGjEG#^{JeYvLUvO6=2G>gB=3b9+tGjPkzd^I%LrU8)O``k zzns#wN;|h0RjP3I$jiD&X^XN1!3-hlzn6ac1`lK_))&i84LhUc`$l_VCTa~&Rpuz8 zZ!OZDb+v`eJ&QZDc#Myo+;u-EXGX+q%NSps{rm}>g&IFHB3sdl;m6OnuT6vwjDIYz z;B!levWdYzqJkb1-}-flX@3?8uRuH^yYiPJo2Y0MzMP&T!W&wVg4_qyQrh{pnFPx< zCN=4O_ncQhrb6l^ ztNXK`IDOVCUY}BY+3?-)-|+q+=de*F;Fp)PTyDp38}zSqM^H%|sJaQY4j0(YZV$ja zz`x9P5_ukxmGT@#hJm5ObTOvbUN$4Yk|&C9FMdl*7OOjjV@;HYKd1jqQHE^6m}?37 z(sDqFcJ0@N=qrMitz~W}!sbrhcS#HVIJ@5%u#hOWqNiG{^hLltnJtMCOLYM^Hn@52 z*(R`!=s<*fcfi-YIT?A{EJ{L`dr^@8rd<6r5mG}?>UV-II;sPc65qN^_?51@kb zkQg6{;0khjuWF!o^3?s?_Z`0YCMDlE5hdb%Vdm@1Tp^ZQPAB;`wYj0@=^_m>HI~ta z*v$^d`M%o!s|Aqdia5c^m2g*hj8)VEI-N!l%W=&&6l4oAXNAc3qZ#z3?vw7KzM~|o zb}@&_pksGLm2B4k87Z~dU%xT8AbOe0kO^_)4e)=Vp`+4HEVecQ-r{wBt{7$caf#7e(H zXv;#|03_0t&|u>LH3Dn@Qqq1mNOS^TKpwk=sJelN_CX_q#Sn(ke30`{Pwfv>oN0lf z?ETeG0w~Imm)`wXyKnktA}lq8KN5(_y_LD!u%guhmV^yv5E(7;7@(LyL?`fnzzAh6 zIytEir+dqjU}6CoHxC_3A-Z~fTn24oS0x;)T=Es*)d<6zS- zd_>R7%ZpE_0I`4f!GnKDyR$7?o};1+3JEENJbei(4T{Xy$fSY)ettOl9Pl}K8-n>G zPGuyDh-Czah034|u^;dNW)R9@PuRmgQWq_j2ywY%z>(!*aNG_H!&A1OSmAKLDrrn2LWNHWbm$7{{`+A#R33Tce33p`xK%FNjb~o_&-tL9)tm#!C*)zFxnsx>z1Vzt9tnUV)RJ@g8;oh*w*#= zz6Bu_`jxc#)*d;CF|uwrFg4(b1bcV}A6vc-)i`0Y)c&Zsc)Vn7=Gju*FnY?M1Pf&n z+|bRR7--n+SX=4~NV!(TLISLb?j2PiV3S~-BV=RZ;*qVbR|xzAK1`@9j5jyd0c`JL zWIRhiO@JP0VS`D8Fv`uGCmsd}dW;GO>IQ8avPq2P1BJKVs?4+O&RL|$E+Ij#ocyhZ zDm5X2XaDiM(f3QC%F>T(PWrzPH=kIOqgI${-c>OFaKiBOy8QkuYZFLjS!2RZq0>7_ zja7zgBI#I%%RTjD-n1``{|4TXR25eaFl=v1tQ3dlySBKLH-C3-G-b)oZ$DDEBqa@7 z>Xihr%B&FD^0Vc!|1M3%itE@J|1SKIJ>D1z5$)ZDneavP+TwoA-_O$}65sYzvi${D z2S5xE#AG@C!*lfL(KQ4lCctU84FD+=3t~6Q?;`?Z*ZPpG&MHn0H2E)(Y_r}>D40d-l8Sn3C|p%gS>wI3zkyf4vT}{&D#TInYA3+i)|;#2>Qz@)cR@vk zvE`-qQARcqzANC$^{0(m(~SrPA|$xz(=jP&p^==NjPA)mRhT~Q0aySWRw0134nQA) z4pr7UR&s}w(mWrh|0#Mqr}y93=m`hcK7e8 z&YMUmQi1Oh8V9hwgkw+T;zfcQgu@%+18im$V0Sim@5X_pURz%z`rkG|H@jCrA6yot zs;Vm3sf-8XdSE`g^;+;Z@|;&s;-~`_^AaV`WyB`2ljhyO8=dou;F?}r9*h++|3!3y zzAiB?dP-LbK*8&QBAFbwJ|E-?~( z=oD(c^#*|#-FgrOVUMpsuCat?1uDH|cC zIwQKcfiwr^zDPWzc+F$L3gU?;3@(=tC}N}f`cDI1CioH%tX_E5IB*&&lV8bfm;Lg+ zHL(0~Nrw5lWva(yPft^bN>tv{%hFjS#S5xjGcF==BSqOKAKV;%J(mQ&|GV`s8n7wdl(!sZSGEbI_3ClekthZ z!p@_YvjuE7u1r^TYha1e+^l8E=U(fI_lQ62kh|2sm8+ZYBXvuzOb~U?x5~nIwanb4 zt%u-kr_ky=Mllfe6Ntf=bYUN{-eC#J?|Pdd>wFqRdhe*gu3nKUauuPCK{SK z594yH?Y7h!UsnfndpD3*P?M24xW~sAUpKMf8k5LW52Wsq`#yirJt->V`mOhZOM)>| zUo&Jbem?XkppcjGEe-~1LMs6*046IBBy$vCKINR83gM(fBWKnm31a8d2T z`r*RjIrj%A@F{HIGr)HV!(LHQ5#cMg*+lj43DxJ*Nwt^a?AW)&A48bgdT2WtnM*BK zHIq_;`W1+DXwTrR+F0K@2Z6~?DpAEMa%Cl@-Pn3Hl-5hC-Sb;jz_eviaow#tkNwL~ zTU$Hk5wKeS5soA(&ielT8^rx!P&Ffzpn@-DT(3nEcLF7YQyS6Qf9s(Q1Gsv>A+JQd z^LrR0;kw}z4+7k}vpd~L{%b?SWLEi#G};*2ZlUfuTugY~BNP=Ck2XHyik-l&zxU7Bh9!h_FuoBvAT@aUG|EQV{!Q_5SLLg(89YMh+kva)N^nlt8HHFmU^*IWBr_N{b&j)ZlN(P8e!AXm zY01^G<0%a(vt^^t@eRsg515d7eS@;{A=}zM`$v!Bz-4b-jlVRtG;IQk&gTC81O!ro z5rl$05UA}S;cD>RL?^XlP#+q>3#y4G4A3aGJWn$yI7veAzjEcu(@EJdCk~ji-9TXz zLNFnT4-gn4la{?WURU)G@^KGL(RfrCAJ2Y7_2qY4zlfHbijr$lc!;3snkFI)U8;~h zRs{nN+v&2XOVBwsgzp0BWTcd1Zr*rv%m^esJ>%mSK?tCBx(O#g5o58Ef?mDyhw^~X zF`Yl}jgSwU1Jou`5RfE79)UI_G;pHfC=N;Ng67U;bzF>ulbRUTej2qPe0a164<6iD z85JZXIw&D!u{jB)A5to)JBt=h5h*r|aRmK_;{nEzCaABS@{D(1=y~SASejYB|4PQn zc&YP675_3<-Y_M!KHySn_vv7j)*47CbeZRNKNR=9&iNyha0+))dvnG&&&l`2W{Wwj zd`2eeW6Hy*v|aWLcZkm$AwE8pvOKf;gJFq$!daWDej_qRo*Hxn+#RbujrA+^>G;6J zD{s<`n}dZyM2#XBj~SU%R#yC^2V>MraScn-)!~8z>9>Zfdo~kjUY2TEP@hUSI#b+hx@surc1c0#0EBd_cBqgXw~o zb}rY!)B&s#dS#zd4;ez$+lnR#C7T-(@cJ90hFIHJo$PhNIZtFjNRgE6>_qTrZ=y;j zifU-3vye8J)jld&X7#*)ju^yy#w@RVNWk{#v`#zug4FhvZH-Lj)reW7Cm{pshMs}RLkrEFDh|U85E(v!CIuF4i*4DD2 zN-S7H#Xd+*Ms~vDw+vkvO0L^KUhhCrG6=L2Ocz8MUyzs?J`;u_9}NYduTe%ih0t0h z+@is;{9;c|24G$CLG_DCBY$4x@~_yAO`Y2PQ?Mx`Yb4&^CyD+XUmZ#7$7b1D65efmcV8;T3nzcs(@qp5?Wa%k6-Sj4kCK%{3~E zsFCUO@@mzS#|D)gzx7^Bz*Nup-KbK?XQ`(YCFk$cG1VJ%DpazOUH&6iB zpa7Zm8iyw%6Jh%9C+nTl2mj9eU71f-#28iEOoqYV-S^Ti=c^czt6yO~Uul#&=^hoO z`}kK2EC~RHqfT1sJ^poM*%~>|K42&NMQnP@>Hup@o0hybb#z&u*MS997w3>ffEC^e zHn9;jENuD>V4r^c`c?Kva+Bd4Arb*%`2#@E;%UMP0je^z1Zs^k@84V@Rm2I^rxbcL ze*WuYC8~=~PLfEKv(!{n*8Uc7MyZ(XVPL?}m@@rkQfozSgJGoR%nLAT?{y47 ziSPdE6#;-V@~CfzI6ym9|HA4*T4Mc`^y^ztApHZ3M6{+ua=@^E|Fg%Bw-B9lAlOEE zE{tlt!Ku7!;{Oi^^F8f03~m_w|AWDe5y=*c#dKFsPINEZwp@O-UG`Ctvi?k+M$@mx zh~qu!X5XR* zVYAo!?~WZijPBg2M{JFfag~N>fM)+m6-hPhSp)~7_<%mcAEYrr2I8zBlujt2>SJZ- zzMxSfcI+Dz`nBk|8|$({iJgX%-SF8FVPOW;g)7KvQ9R9Zu3iDy8Wa=+V2x1T*r2x* z#QrESV14ocGt!i4stnc9Y(>LqSa&iO*CCF%a`h@;!EcDXx1eo?eBPM53&^krT=h69 z%F&65=;Z_I+E^D?J=R&E<0J#d8j{lOZXkz|Na6H)f=|0p^es589fDZG!G_lYy|xzu z#63t!p%ih4x)>@ExN1(|x8JG0@&zI8GfsgQT&!2#k4cMIV>ohc=fiHp{jzzzK06HRpbL~RI-ixu)A8wxo zXHFO~zSy`kS9bv_V*& z9W4Lc{cqp|W1RVycA%M~f*1RHH-wgGIMRJj27aWBhWhq1R{MNZAIhyU+v6E>kJImRCm-zbQi~ z0RrwHj|I@*-c_9l2ZGKJd`!4!^PLC(7KbFTwoiuYnd>?9cRbL3z~8XcP3Gg@#zp;m z27c*L?a7LVLu_Mh_Ka>Yn6H_ozP)T^&Ha4(X4lxJ4?~4@x5Jn0s%HrHC(qNp+Eeq~ zes?932#O+`==g1k_uzX67HZN^|KvzTUR@lSD}Ipk%}BxkOd1Zjt4iJYlYP(D%{ynbkml|6lkvw4@KB zF%LCVE7Pl!C4E5Ri7<`!YI(V+IhK&AdM?0A2IpW;JFrzJ;i!*P0hQZoTv9JPV9B68 z1}lkh2>@M*j0%g$(wxT;wk{PA%rKN;1`l_e{xC@PR2yejE@e+$ct0cvZ;y?bT#^=VFj z2yY!8^>tYo7&Z8Kt@xnzwUW|!ldH$(_ZTu^E$-Au@(AY{bHwm4BRSbR8(l-=ZTMtBXogxHvZsn#_b#3xq8dY7qmD5Pg0X=owTeMbv^0v2X1_lQD7KK}mn_F(C{ocKqCg*M) zmu2IaeM@(_dv&G%pT8qrZvLu-e#yqo&bs}aGW028}BaBA~_(rQEEN5ch7;M{>$ zfw0nj{qZA1-SFUPyZ7mJ$7w(nX$>&wdl>x_YHp8}-t)JAnmRBntb%3H{*s1Z+LwIG9y33d4mPh>)l8w(l*Nx*e5{2m zy2SZj7Hp|}zD9f4#Q9eC9nS=TrnnEl6P`r;NK3WMhle?lr+5uN1Ve7OeKq z>Vnwm)w0~`vQzxox}yx;z}U@hhBp-p=aoGtB1f{fme}ok9HpVpenjwzM#<{+xxi&i zm}XjY4lwDe%@KE+Qc2hdVUU)Ue{=u7%;zs~LKy*#MB@@~WFY9yPa+|ERDhV69j_gG zk#M42<xWT4cg41nLmfvqD=R$^`uwO#%KEYTnK>} zN1|Et#8oEXHhDW5lx)sXqhmw5QkeG9?G=ucci&HfZz9)}zyp zM6&G2->RhK8+qLHXyN+rU&%L4N>5y@o;ero_(15uuYWDuZk9+#1d1qqBcqyrdVfD;Ot#((~ z1ty$pw3QUAKCd?a{p7%3shu_uw$QOKsb$)2=lv{-|D?AH+S}W+!{i3^13s&5Xy2gKL#gMyhlRz#8ayg3 z@*1|GBsTW9?*O(LqYOn4*fX-STflW;M+Kn8Y3Yw2^k_^ihqsA0bGQFH{ZSsC9`L~g zTn$wG?$lpd6sE>#1Ao5n&{JXK-bC&fLeX61`z_J3BiwfLA!RlAqjXu})Iob2Ynh*ipj1k+u6_iiO&?`XdlUgiI&8TPer znncRH_cJi~j(g!BHBxml55D{BZ{n^LI-c97T#74?Sle|t->cM)wvIi+!qwGnP1ezH zz@@Jp`1n2*1N%Lzd&#W$%`K(f<3Me`$H^UBpB7{(cM?o*w(gzuRX=^Jp>@k}U+G4v<5*Z&+iEKr68pi*;T)E|e3>3o3kdDNgm~VgR{tbJc%pwQ zl@QA#)Z@T-H2ZJo%+Br!4tiUSF@tqtZHO*J1wJz|ktw?(UcCt5JV8 zFj*+x|FbB5pi!loSy@}f)8AnEv(Wxe0u`D^nC8X@wwIoKuqWpC)-PY!i-aCl=v~=7 zK`Yd7t+|e_mmJ8}9_tRf>#1Iu`C|&7I**h%lijZMDEX5G*22^LVQJrY~t~dc1zk0nnO6!mBwZIlq><-aX2)W@~tR zc<|im*_oNV#pUH?q+R_haQpYaiz2s4jw%IJ7eUh0V%zsduPFV=Lbn{ zPEHT<26x`^mdUs$PtJ?!*gx+iQQz3B;5_eO@n_J;f3?v1EWdbpIoDXwwnVYon2Fqb z13v}aX}y0j?AaJF)(y{@6wY5PE;{m%>R4R6-DZQh-LFw<9-iB>-g4%RgmC87t5@V$ z_BnG43#kr%jt61H{R$&O{y3Nbq~vhxZGo2O_{o!AXz~J>#$zwP9_rf}_H`#|HbfN< zj6MZ|u=Cuxb7&Al4xX+I;VA=>8@Oi?v$i68f%@^1y1HD6%X-|~w`e*_J`vCYWhp_{ zUAlZ(7Udk=6Wh?wnhAj4)=Mr;Xd%OlQ;hKBfLkY#K)*ir@**RnW2Heu8@q`=`#{u- zJIo!0T$fn-uWq9CI5hCRgjTg$XFuMrSax=HW1<=xjDy$Qq^|Nv1x}hPD}`N1h9c z86!(e8pw(6!~z2^*7w&zNCLp>f#y1p6?wrAJn^p|XrvA396${w=z%kYNa5+(iXhfA zJnRGA1Uryw;2ZE4y8&HCdgl&c{qB4w5OH7JjT~G+pg?z!pb5B_oW>^Y#Ej$P2{cZS zhc=>7Gg`lx02w146ZAN8_^?J#)^0iT`qKeB3@@6E3QNZZ7<6o-t@gRk_(H1FDj}|q zeV%%D;Ov~B{{ic?661lCn$w>*4Nrb#tx;@HQ1E3woxQo@Tm5$9f|{D(-0W<07VO;4 z&Euozs~0r6&}k6!q|l^)D^LMS2nS%%VHGy3bwh50?oEyPw#%;G1P})A*VJ_1`Pvw% z{pd*eXDwfFeWpf`uqUBi!VVIl0>inLakGInwGMdvWj#G#;MGPD(IWCzAcx88BScE@ zb>+RtCH;jO0!%ivU-#5}uosD@LO|HRwYBX7Ph5)8*SnaRDUjztk7ciAi^Y+G!xT~8 z3t}Jci!aLRCbv~vbUtShr2;NE1S|r=to$D&upwnAoapz=&lh~}ep0<4BJH1G836Ad zY&Yoea&+j@_UFV`HFFbfPCe*fR36n?-Khi`i_wnnt;+|=+@a0qY_#@faUE#`+5yf@ znOU>GBPZU!P{*e45Zjbf-l3snwm%Tbd@(#z<^bV*UMQMb1Vc85b~}WY$nsAfJ$4E# z6~d&@h{7Mge^(%_7lL9A!nt0YTcm2`Idpr_97;|}p*gdBsThCkP6;9kE|E9eA2kIQ z%_n|7eWR1I2`&ZqN67%gVqw};rM!E`jrC22+74hJ&Cj2)=TBfSYd!6Wg_~E8{T-qk z9CNSlkABdhXQHR?kw$8B0#8xNjFNpPdZ4;`(Pc)Z9KZc1(IPjb$A<2)bnM6d%|_pkwXX2y3V{Y+zz zQy3ssIN%8g2ySQh4p#(hL_(s?5s`2~qu$fsza7g8f{4G^y?5>G(C6?N_9T>1`UErp ztm#UQ`5u_e(2)G3Wk>E#9EIg*n1mKJehBRF+Av^(nbUDkym@%vj+KGdsjRCk0M{6H>q7P)o-gy>J4JJdTIQq*;xoKP?McwZ`V~ zsS4?QS69Y=ZyH?Od5Zh~Gp_?;(*CC3g_iw@e-r7?Gjf=g+{o#kh>Lf8aPT>?8jY4m z0{hZVIZV~QG1@lxAjFMT*zxC!iNOZXYkd5B{xp6)a(VjEdQK5X(?|9ZQ*(3#SbZ{s2tEJ z%QKr2m80B9NOpG0R@d%6^si~Rzcs7CE9))Kl5WnGZD?FmDL}rlS#HFvVBB6yzfL3Z zsm1ztO48?0JBIrbO8 zdU`9HeXpbCW#645{RJT;y61G#VUYGGkk;0g~!VvBvj{aftX|{n!xC)o%@uoE-%%Yw1QyZ{>aX%`&b4aaL@!37m|G zj;=w*zc%Hn2B7_%lXR?Mh)_^_SWHYkBO~KlXo>vy1|IyvQE-qO(MjjggXYqNL}!< zobvzv?%QjxJ;Ha%Df(Y$R`^j@H-4b>_?$I0yMNi=ufI=5e-i9GHY85L?$5OTLl!T# z0*OV{J7jSPN>;`0&pmgkuObW!Rkqm}nb)ryGqpr1?>I=u{&qV4fBx!jdit->41<45+8}w z3MH`^)K^18_i$<#G<0vy)cFTI3%@wdJM@TxeJ9(0H*X#W1<0Da z6&9|Eb--Iz(gitIBgp1+zhxB%8%#W-GSXnnRPNvCITCJa@v?{eb zF}1d@@`o}J**S);U}^5J!?M%tEo`ltc+g+fy0t8Q|4G5q+~05PCE2xSdlX;RU_8+< z&C$ahQ!dGPL;S5*TSvztRCj7p_xit|gJa%?SQ<&rr3!63t9c#u3ae~~rM)IJcguZv zjE6!&B4TyZ0@uWC`yj6UxqOtvmVd74Gzw{*Gti*abLjmp;NnNEqxw2KTeFcph=rgG zqp^P7!LqQ$$=s#9z4S2_Iz#ybCfUM#T`uYauE+K}@4{91m!D(?%$ z3slsnV{zs$$@OlG6-=+Iroojuc!pjD4SEv1vPZUiRjz~t zAH3ZEPsla*zojpT2mJFX|NSVl>C^K8FUxDVB`|)4;`B?lZ^vo;3X%_V@oo)>{XP6T z|LtBMU*8izs~=X?*9(AAY&!5ij{;LWUMq)R>D?fJJ|X9czxfuG zLf^lCdl{LS7G0ol`}aThuV;~zWI3aMz-98saO<>&LhgSXGL`|`RfL14^yvZde=cc` z0rg(Z!->fHByw5LGL_2SO1R|(MJ@YOOYC6PEeU7QGVYDXFkZ3@3!;JmIg0YE0>1U) zwl1`Ub6uZjB7G?>-G)3mB}jbxILK(k%G8Vvv~%(Tr@N78i{dC0o#}vOa0&F4W=#`j+wRph&$~p>A<}0w$V1`sG-BX z)B89RI~7_x&j7WJwFrq`rA$=SFU!5SO26A_o2jYkp9~<69PHBjN@DZL(yYAmOd8&7 zx(Bq-ADENzq~qDl|8s+DT;f*mZOdCZt6SWF(HWyL%~L-=56HR2I=WF6wzaj9+)6f9 zsG)5Y-CY>~i4g;{S5NR5TXU*`1k}i{ynVIIg zCbB^a<82`k%o0vp=5nmA8y~0O-D`bLRkiL9^Qu^p%K1{;*^No_tD2gA?(W~r4Ftzc z$QYxUjfUMXwr2h)(Q|H2VEw1Somg*e?>hGBU<7h+E+V4$51ygq$FUHzZDK;ZsNkyB zC$?^b^G2PC5-!8H)d!|`*!~f6>F1z1_=66{WjQ)q69R-$oZ3lp!~OdmkYp;da=6|l zcZB5jPD!oMIoeluw!-LJX#8E>%$+q%R!Mi>58e93vdGqzqRj>j5|ZrmFeVTb4{4|+ zjSb$typ!}v8Y{S4auNaZ;XiOZe*uel-OS7jObpsvkdcVlOD{3@aaA7xb}Pm=qDwIej5r#m z1r>ZHI$r_~_)=cJ1K9w{4YaojlnEp^=m>fUu@#1jL0ja7(7%NQQ3AU8dSg-kh5ni7 zV+0t7L;|vh7*Ph|KLh$qj36xOTlehJk{r(Cru9@Slx1*eA-TiBq5bfKhn@?O_QNcEPwurLv!P(_JhvB^okJw5- zPJO-_YTXzp?fh+$+-YXU;NbV;ewSVuMDCig1%`8N?zPS~*BtBa;^C$UrmWevTe+Xp zKwj#Wn_X-}DAr7-U5*S3OyEFYl73jRn6iclDwmzCB*=Vg#ufymmdqLXMVsx3_@LJTB+W4e>6 zZn)_2-XfLh%7KFCA%X5&kganC!rzFn@!A;qB|Y5_|DYtnus%LydLHnyeCM8yB7 zmk(B>OOD(*Pln4nFDUmxas!W$mxH5L`sv224l$Q+hjKq%q&pewZ{}K(9bwAswexhr zGl}8~XV!<3_@eb6>1riqF(X^$Sj~}=#B)p!YMz@~O7E{*nJ}!ZuD*~(AJCAF7qfqQ ziG~_u)@=FoPx~AcYJV=V*!89<#m|p%RiKYHx~Us})@3fA zkm>z8GCZ1YZ>rYjbn=CBnB9{-d-obMba^*_pPZO5p$zy8gb+)l~gkP_>6H|#5rK0kkdFrrhC$)vft=q|3 z5?y9>1M~;?par~Ea%W37psl?8Y>*V_60({)i+biRJa)k02|`0wRyHs^weWj301o(P zjz5~Zj;iD@v`uJq;0+NIvJJw*p%Qgs$C+h@;BVh%dc4tX1|w|GBWeC}oCfowsDy=u zVSkWSRHVR`@`NOe6o*wYH9zkOCk-+m0ZgletA66b)F*IsjOc)cxvh46^A-!~lDc-k zv|*OnjQDDY);a);;axw9TZu0c(Hb80~v>&|K^dbzi1Do*co+r@F0rY6(< z`Qu&cH3HtORAQ6O2|vQ6AMFlvOMNxwvb2U?qH&O$1O_cTsr?0-^=M&i-`xB>GI39M z^4>rVPpW28Ge`3uz^%Y=XjlVuj3$PG-9y7nDJYocS~LMt3fg(H`}gOmRC$72WVsi318(beMm9HoXk0b~%kKN&4^Tg@ucTCgKN(8KT9aMFb3m zz=&RhvjH(L4+X_(yP+w6>%W!#sNuJ!I8{T*-+zM?i!#4~pzsUsZu@`Ew1FpEr#?aPU1^=3sTC zvwCkg?h?*W&CqY(hto%7VM6+ON02P3+M4PD_5QzKv#w`br1{5vp^lU=;Sc|;a!x{u ze=bMV+rNA}>h|lkkH}Hov5)oyzc_qEg03f?UVOfw!FBzGIlvq3$0d`~Q5HJmlAMiaDA@0Np`2lPJoU8i z0=EH;9_49TS$p~rw}tj1R2;{KF5TT&vnRGFk{kxRqy7qddU{`xw_;hG&tXC%FPLlGocUlo8z0By(+7f zvmzgPVSUf8T~mbs8OM*te>6nt(`>WW5}3?TAuxQHDF1W3wNG)seQct8Qs&5@#=E$_ zU2j7d_g&I{$N4V9VfNp}6)=ITIPP>QPVbdd?0iJe5FviE;&Xjy?e3pbz#0?}2gWWA zC+|>XYFPYI9+;kXFxAG>^W1Su6Ytc6+M?%!76sc-2O6(8+i$uR**^!0PvJ`#8r#vMBb zc>lR5UXWLPAkdN;7q@Hn=11?Qp>il#dPX?5p1FN{UvFRVr7Kr7<0sW(sR;TC4}|kb zLR?S4dbS%+gSCV4vHId3Pzx%6yey+>E=Ii>t^S;k}-=F7x9(kP| zV3+Ifs+*2JS{sL&iSgr;;WTb+sB4ZS_uCz1#4GW9N{LsrAezmB9>%|djjjMS^bcs4 zIifi%zNBPY8II9^!1-k}-b701@_*cI=rdP*l)7#x!%U-VC})aW(6oxN^-toqCFp9J z>s|jXt|H^kC5?OIf8ypKM8b;NnqcIuQyKKeDLXZA@@vHFIF#1KYO;Fm9FF@pDbLxU(B zlu-C%FsS6~FH$oNO+Ml&$d>=RAsS#$@2WLbP(2*)HFw%GO9*vp9Cqr(<6 z#js+aw3f{AetEU0NVK-bRjvxB?(nHBd)Mf+y|C~-Y< z{PErk2nb!C_4>7X>BZUX*W|KG?@edUO%L{&>Z^}lnhBXrTuE0%qHtfpB@BIm}0@ z5a72T)jO$gcx538v>}Vl1aL zkZDpN5^Smv2#uDC;OuM_K$2&FavdPyJ%9arDXvyE=vm>r)fPb(6x;;_zo;B*>+EPJ zF|BizhQ=~f+YEFZl90K-7^6`H4p`sa2P_45XG?>Xh>{`J7P#Lh`T3UtuODBUhT;c- zgjy(Uh{Q;Vwl@bRnE~1gp|H&B>+( zr)5c%NW{ZgTLTlL5-cH-#{f_hybVU50DN2tb@;>RsMVbR2}fNJh9K+-vKM|_=IpyC zn?!7EdMW{PN7MVZzFLG6{g;iUOpTJ22j zFY-ysPtUzp1Np!Z=40^w|dQpqTlW2xFi?KL%x^SuFgoeZ@d-l`~!^Ytn!#9$eeYq%N9$w|Jf z|L?!vd|ckH$HfVfE=+L#z!StIC5gL#&EQ?&#Ug5qX!n&y#(z8weL?a=Au*jF`zANm zYH3%#*dtGBHaC)d<9Mz5t4$LRK zTEl=qba5pHvJ|4AlShK;3?KXyWQj@w0kubsJ^S2A=mn4Rf5dXH4 zl9CfjRBT8D=Y>t1pP%0rmb+ya!uD-}HABbXBf+q(Lc^8s&@aHuM7D@LEr5aeXMg|# zL@wKBlN-=k2v!y&)q?2K2ytJ6?|3bq2YYM~w2$3JMk07pG>BKDY{;Bhj+&Cy(<6_s z80Hpj$Mz`dImvzW9X@h9lAcg$^5Mmx3zQCW!d6-UXDQL>f(%1d7|57%D~uF(j5ytf zj1@I>JQJA)1LJEU7}-If5yK_fB|RGv{5Dbt8__x8qqBe9rnDFlyZQvRee)EcX}$(&b^tJT7Wf1p5pzz z%e!xGoB6{5XkE7&giS3iyQcv*$9ba{Wi)ziE-IF>f2Gdtj*}@~7k!t#KjBHSRg(4o zckAO78Gd9!M?D1M{AK)3e>Zl8wW3BiZRz_8V^$Yhdc6x!!=?(9A%71ONIcWf(IA3k zOB$g@2WvPv$v<;SU&~Q-X|Z|yzSN@Jsf%Jo6?66-`H(I}6nV#m zHy=sCaHpIl=iyesN)3?LuodVKiy6OGB;6zKG& zNG8|(E&VTAwAYW)6xfn+#c`>e%#7c>*A|d+^qPF-3y#m-=djvJk&V$5@vv{%Y(vWc15aMY;f$jVV9qE)Tr?gonBngx&-x~S-(ZkXT&J{bi&ClhV$Xf(E3fJ2}XW- z#!Crv7)nuA0V*b+KBW#uCz@AD^nca%JWZKodJQzaE6r({eQ^)6S^wL;mW-I%5W-+m z*cMEeu0g#qg_~A}`u!-N$-uX!mcXSB^OTj@CFJIRG?5Yew!D|+=DHww2PI(iLiIa# zwEqrqQ9aj9tbZ$t-(g3cT5KKTQp2HU#Y$7}yvxd8KyV9=V z&ZO%?m?W@)F%Hi#C^E8s?Y3=sjyQ*I&%f{QHM5hyLcWbm`fCr9o4W^Oq!s_)2t9(j zDgvur9R;L;xA)Y--&du-{PdfwhKIHsvMEX8%8lC}|LxP>+dltE{47;r Date: Fri, 19 Jan 2024 10:08:59 +0100 Subject: [PATCH 202/240] new image --- docs/_static/images/docu_field_comp_flow.png | Bin 250938 -> 249417 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/images/docu_field_comp_flow.png b/docs/_static/images/docu_field_comp_flow.png index f3b8fb6f33332240a9bc4446b1799bc8e02b88bf..11ff11666480f34dcee1967900b580ed72d89d31 100644 GIT binary patch literal 249417 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

Nr{`FF0`WoT2%By> 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

qxzUD@ec*k4pD{RX~ii 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%0Dp?#Efz2m$@uX%3aH9#I5 z1JrbDxjFBbSh2K9kvJXlXeDHyY&!*d?&frhuK7)2qN( z)0ZI2v$*gxkMPdLrmel|Y8CVmB3yp{(>ikCxY2PU?w|qt{YMlqbUwE*LYYOf%+;3r zf2KKBwl;~6fKf$d;!W##qdevbha5_S^@GYiyB*chq*jiVUSQJ)d>#)6P~R*`t6`Nz zWLFg!@g{KcVmQB{(B&=$s{r&Lx7!3r&O#&Vw5-CJc1Zn(=H~10GlZY5*rMhwCjpF87f|$SI^_fmrpj-yVHE%(~_4 z1qyV$m2n!06^u&Mv1M-`&!a`;0MNd(E?2{5=0pN|rgg7CjQB}#%ujpMy+ zKDGLHY(O?80Rrj2h58bB-YRFXE<}+0qN6d&QS0!;09h}trq;943p6k#U=)8ARR9L4 zYw3esezG$`tJe;-fjgY69l5zOF#}<&&Xf1e<{%i}6M#wrU(b=A?u`@Po)e`M$VUND z(cj;Hw9eibxCCHg%D)aI$R6NRa|M+3zz6rEjk8y6AEmQd^Iu?EFctXwGRd+Q*t#CT zmj-5LX3l`MQHWMq1Y=3AGEiyHzP7%u&Ooi(H|!)0)ccd$3rk=tK*FF9>H@>d=CrLO zm(GU=wEZq%>&F5+2K))dkmvy+K+42~3J}>raER!XG&Bet=EKjQx%nDRE*Mr(!3sQ6 zm~aTFZ9p$sU0sC%L|Io?mjTFdWA z38AoeQ<1j81a;BaEq`=WlvE14>B{P=AMij501+L)Ffhi50^(nk9zGWrSks?X$mOc7 zkKzSaPU0sD)9=e1J8HlsoiN{MO1 zPt~}h<>26DpOPbIz7$jA;7iQnfj5C}p%U3eV5vo`X>6`6S>N`Xo}}^Ei?J#v|Mwp( z0Rh1{uxC1kcCo^0y<SL?=Gz&5zxKC z=CZE|e3@G5e(ik-pf2^_>h$_spbQtU;&iw`1VRvEE-pL@u85r->zBVJ^lrl%WqtXD z^=F`u#!sc6<%T9ugeJ~HhYYm0wXxu744SJ3+^$aNDg3=!VukaFUdC*i_da-7Ukmqp zibBI^&B(oVBK~7QmqRErAW|$@awGb0CN^TfjZ|2@Q;NL=pX)zEc)K?&#fgJ;za37h zcjx*o1LRpv;c9U~0AMBj+Z7&i{IeE~VN<_;hP5(#kUZjTeg`UQrwIK6#wmHs)Pn_4 zcOSkM(XSu8j(K_;AQdfjB|%-c2l-$wr*)(F-kpz8Nny;&%F027@A6N!#}W%%IB1Tm zpumDk!8SQIHUJnV`o^7sf8)#9%>9?)JYZ1C)`B1Vg{pox{(ckfK;*WIQjN}x%Vj1IaY(GqY&<^pkUr7dwloS>Eir8mJ7+kDT z4jaRN39~}Rl_B5`3jwA#dd@N~ZiCGZ?6qeQfKEpbfUIc7LiRHFu9;c878NXDLN=op zttDrVUZ#l6I`t0iOH0pAU;q64d;{bHfC`{3$wcLbs20xgzB;bx8hm-c|9vk2X|+(u zAW5M8z%=Mm1b&jG7Ow#hpT>rUHL&sg&-(g4Ko0O5Yis`CNg3QLaKuJ|{;$*Egsf5+ zuV2~rkO3SkNFaj~2TonP)ed-F?zH&=xAQ=q8^$I-cA=oDJlHeKPH?sG&J4w z^ZdX4imEpevp(KniuHrO<-hd}R5C#*=~ACnnm~%05^Gq)`sNMdx^8}Zpfw`%$OP?Z zB6G#ge^!+E;bZ{1(t+pyjpKmP9;U6ZV|beZVY>A^yAe8;7|==jGM4KdgQG1#%X|<3 z6ex!O*!%ieR0)Iopx$}!qYx8N-+{!=mbe{3Dwd2p83WUGuGhh5uzfv|L(4Q;M>apE zJ8&W!U8a@95Dd_3RE9kz0v%|W>y^g62+7*y+Iikj^mV`N@qtYel)Nv#Nw&>Tz&YP3 zbX*KMUZ2Bl*{lKk;f-T7dMB2>8@&FAqA|gGTQ-|n6V3zh;eas(&?G?N4Fpp8R4!ys zyDlR(&|jveH{#_m|AB+?rHza#OE7=jmaZ?w4%Q*9%l0qNYW<1P;`W zTDTNv9KY7RsTe%Nj!in|vz_1Z;3)|U)y*|(Q{ue(nE*_4O0;^D~kTHya-o(=o z4T<~U4+!EGVFm20lL@Ap_zN6ssHlf&jvZjFmh^A+fj6g3gb)lk;R0f-K!u){mzP-- zE}j^HmLNogmRhS@VRlz(Kbxokf0@w<;@hywik2i5_N=Om^8x;+szh@kqN2(_=EI%< zL~2ITB26?AT?5LyFF$K2mN(bOd$I%Kst4qbl%)oeT`K0r>bA-sa>qHlkJkZBXNXd_ zQ_ig0kgSCr^iFxPST7cQW5tz_xb5CoG-2KGOzGlTOV_kC6VkEx(A;4V*Ymy%m)Hd% zgku*Wu)MukroZkoP=D?R_P9FiUhND9TNku9nPq}n?A|5~c(tW~L?%lA4VY_)xvWA! z5xc(B?D1Tq054I;$PS1da%ntRKKCbx+1c5w=EI~`bJfVeYXf}d?V$Q$^S*a;I+()) z;#em@2T`PUV0=-^kwObt#1ZFshIHVMfgs@P0{ZN~jMe^xw`^83tp7$bC_+O+pRGyc zLY{OpRA>Xng>hCs3d0n z3$4vSn-IUBk7#u|ycUx;J%D^MRc7K55PG+ftA06qffmXN&^2HEWvX8U`wucQGTUb_ zQm{dO#iB`l{2(VM0hDDn=UughdPhXz$Llvis6?@tAT20JBOxH6l#_Q|_XbgYL}_)(#y3!n-$=EDNBP#lgX8 zfE73vh<|7JJYToERd^mGxCGRPp~}fZn8=^8A1uhX$+yX7z)C{ALB840)+T#L?B2Z? z!l?rgMI(;44V48U)mbFx%-V94{!WfILfW8o2AvJ}NQaa;I*o`t|EqB++GpSUrQ|(+!ct zkM}0Sxp1OTCqO0Cy#J(fV+_pz zFe0BCAm$+rkO-g|vkpQ2h12OD5P;i@*3kPz)T#5Xn|1%^cafVn){NWYtl-kn?5*@9 z#Vu8RXn|9tO$^?u4rzbg`iT$fQ(??e`@ZPY?wWFWklaQ?D| z+-u~V=uoyAt-HznyaE@xR(gzGP>*dlw&mZW6mZ!|?LD?JF2;${Xdkym|+x@8k6I7^}IMv&(hW9kutUT<1C_n)*p#>w9pQhJkw#aUp!^AYiu zGGoBmO)4=jP<7obPSpNU}qN&dOx=5 zNhp^-o7~p0Dl;8hB^e%DL5#dxW2M37YzNfttn9zH^b z9P!c7kpb|T&xYcGG6lKYk=`9yIG$1T7YPr@SeoFv^@F?!X~@-f)c;J(A~UzYHflbi zSZNk99&QMs-~9IUudv+rw>!t8F(D|Bk9A`;3?BY7Sageq-=fEB z0B~B?=QTF~h=z+vD0qmA-gt7LA|hzQio+D~IFst0B^hw4fF^UWH5>jPJ_mmXe~(ar zv2WgHGlH*;B_%w?N-SuS8Q<#Rh^4peY|9oRH4tfqG`qNW)3} zN}$_&adhSDf3n#0iFSSs)lru%hQI~EReQW}kL*WB<#TmW>g5dZKtMS*NUXku9Au#7 zw(p*Qa7aiqK!dk~R#aeHL3ycor`NZ(Rs_B>lpfx2_)b;7B7~S*;dYpBa2X3!fuAic zjU63V0qS1X{Dd}l?)3i=ls%WeBtb$*4FX4!Oi@V=W-6+QA1yR65~@fO^LuS0&4tpj z(K$E^XE7CD0iRMr4u4eHwwM|t9WcsTkRk_DVEBIVUtBbnP#%VNkP5(4oorr=J#jdu z&HE}}oalu~ zL;L#xGLG#Anj@D@gLbyNv9l0J79k0GvE~r0bYRb6bE7$;FcNRLaQLQ1h*G%m7`pa& zr259{$K$868oOlgfW23if64wZM{;Abi0V-g9#F(@;e`Sjw+rX&7SS36B!3vN zdFq>NzvDZ^OiDK$E>w}W`Khe?CAt+oX!v=5=iC7aGS+Ga+2WV0kN;94eLZ)vr9@_2 zXtuW;g5$~3qCH2R>1k?l=UX?#rvKMCc}~?|+Y({-t^^O(0*k7nP(9Q@XDs7~B-E-J zZ@0+V%=ZP#J5No`{Y0Ds9+5aN?7ABbb;+Qf3+-eoOjA^U0{(g==5|OwB0bWAb|lD8 z{Nu9~6EPaOs)}?Ydgp|SGx;lB@>d)9lM~lsOAkvAXIcVk3&t>8&?(4t9587mOfZGj zR!&)F@u)vWAXnen&#de)@c~tlp1C7I_*L&PE-o())A&oOueLfmbT0sXV%IJACsM0W ziIBiiRRGmNS4G8r56>%65zJksKiY2CDBk^;{w`IIzRE_c?r(#E`qC5ni2yy_JCx*1 zC*p8A#a4>@rsOh9U)sM5xaDY!uzhqW-PtU%YH5|K-F+LO%s^_WyX>Zy!{kRM{s3R# zy(4e_S&HAsh?-8T!Y_rleNUyde(b2|tuDu~*}VICd1*l&6Fh)(hwYo0FVQ*q9ki|@ z4!V}drjgGixNoIja@S8zgq4tLE8HlGop^YKNgDh2@84rUj>pq2_a#cx#%pF&LqGU8 z?Df&Hii^Qm`f)>8YSp)Qrb%CQY%xB7&BSk@Akx_4Gf9(jaB#%KcLgk~w9jRlLNds7 zd}5gYL$o)&sON}>%OMZqU0)!k@Kn8Dm1F&m#q+F(1SfXCN8KvbTgDni<&+hgzpC#o zb+Ug<5{EV_dpKDdT8?t+*O4Z$HGXodGaHd0Iyz=!ro zBS!b92c&DG1uNy#|1}(dJ1s4%@N}m@yI4kLtI2=xr$y0T5|LW;*~wbL2t)vD2t~?& ztM2*{_0R~o%1p~$9UBpI(a!yzi-tV=k!Skt`{ysJ%iABI24v=$7jxctfFq!unH{BH zAKF;-0nL2)GcMMLPd-6f`aB{d+U_S{3r<{#(9zK$2uLiJ$`j(_A)6?*^glh!Y@FJjZ^qmgupR?d$>wH?FCT0J z>#2$#9-D9v$AFt7+GSaB}ylI|DbUaVc%S=ApIIz?HeAa?|+-#L=ev5ZUeRM=-If%l(gHb@xgqt zB7a59Lpu{bl?40XNxlz*uq&9s2z&D;tu>Xketk*7dTRpdl9P=|ZO`W*AtUaR;Odsh z#KiBvT!|#fNQ0$Y|HZR2ohS>$3xHzCC1|~MFN^RRZtDFK+M=t~di6oAYm@`#f7Pg8 zjIgFsmq`^rkLP<6Kjm4VFKskl6MXj8#f=ALiSgpa3uMrnnVDI#P?tHfVSGG6f7vI@ z+Z$=*J^Tf7xk8-+JrQK^U8cv$KjNWU(6Cfg%2)4JI6%Dt<>ZA+l7P)o6<||W+7#sF zRoCdQ4#)!C*T&4kqN9)_$I<%%Q-=NCf&Dk8KOKmlF0Bbi=T%o$y(zqafB<*pmJ@QP zxg|Y(GrO}8wamMV1pQe~e9qSML4zipFDHClzUi0V57^(4FS1s4?9 zd1q$c+ylR;dbp$2Dso>4TvJ$9MEPIrM;W}>()W^QAw+{wSlz>zBr0=f2Bws2`~C%lFFiC zF}-ac9Q>$yyMfV<%$B38N#cr3GJ>H(WNa^U>Pjpe)Nntjfg@gY_pvR_-tjMN(ZbJj zK1An6uoe1AS>Wy?VCH_1%&L^f9zOj-qqJQ;MA`(2?ikMZi|~187+(dd3~o&{ObS0T z`Zo!i$yI_^nJm-inC*6V4V8ifDg_SWOV;H2(Z^j9k1iieU@=K&LhnK|C?q6g)I}JF zX<~)dR8c!tX+&E0ahoxbB+zm=PQB6bB>(BB_V#$X61^LE=8eYs+AjwFjpQgh_XdCLld2GasbLS^rOM=(J>8m_! zMCa_3ilT=m%TfQfm67MA%^Ky(AFb6~u3TPT-ffxP$D=(F3(p=o_?XJ_upG(Cd8(Qj zuwqjCEq*K_bkX$~w%YvyNcsh35L zJk4-+3@PW#o8Ta=STC-t9m>2IQsk|q@WDP34i2i3=PSQ1sI;CY)XQkkI|bj==cjBZ zK58STPjuE5V)k!&oU)a3iin6PyP&V8YRbq2G`YMVar&e?&Eqmd(^&P&?&jmJ`f#rt zjTc3GCyz>+_WxQ~jBirz_enh?(Y`Z%alF^|ur!3o<@Tb7HuG}!zgGq}p;B%F@O}oH(P9V!hki@H-p0W6 zB>tozN91Jcf?DkrWG*%6)W>PD(?iZZocvnW_Nno?Zuhy~l=pgiF#J}3;0iAHg)1}jS)$+8agEHsr(5jJJ+&uUE)8psuoOk8#;A8q}9Ij|S^)#p-H$NOGqdGX*Z6I129R#x4cj4QbzFN$ra zglLZBBg1{vqm^irr29Qie5_u-Smbd1zzf!@EdPZcc@ja#MnUgZ=-yToPAFaf zRWqOTBd+ZrEsEf)mxd*FX6p~#)h*8XZXNwzXp_zza}Jtae%68mrP1R_mg-zwrc>silqi9ga3FtgSLO5&)d{`hhA_e=ek1Gx@bwHxtMu5qh3 z7h5fIXIw)WxVF=sZg#(@OP>A8y3!M&`qW{0Oca4r{wnvqyD2tFiXll?S5J?aB=Vdp z-Gr&cP5UI_hA=j+tc7L#NXQ)>9O7{Yha0gN6`?dh8X^XM9bKo>mXT?7_l{I|NZ3P% zTJDn)MaMqtVw<{~i<@js(3?&!P3V)0^_M((gZgqy7t^N~Uv>njX=o%})w;`OM2`;k zuh9&#%A;Wod~5g>_!3Qg$KFITXkt@LdAYu%BF9=AcdghZklzsM#XC7n1Lyw{+ z#1^yjLtg2bP70@U#+slGfU}uH!YU<6{$7bIt&N&*yCUE6;tUWy(tqvkygic@9_&N7 zH@E%Dr;UJcYqHZW$H*k8-ZV@?*NT*i#zYT$%5(Q>*h{Q=u3`o}9Ay5`_DgM`2$u6+ z_i!cN)*&D0xca}DT2$Id5YGHQB>3QP@<%UIUj;7nXQ!mMW2J8}SH}~7durh{>3>B% zlVE+u+t8yXG9@;EWcjAkP`-!o$oxud=|FnAyK?8h9SqgJ5i%FusUsJ!(4?zbY%)~B zV}bgIq8E8hSx>?kCTt8&$>3grpn!M1z842Qkvc{}rtFonvOZF9Ih5)$DB(}!#Sw>Z=i_knr zC6%w^frJp(RJahQ1FRGxpCpE#xnI3Bj+OVyT-9@l4e8cPWXg136tMqkY@U-YbQFMH)_SR%N+_-2a0&>KTW?5 z-IAZZf&rEY(we==c&|lG3&ZoiJN_>YN!zS;Yd0CL4%yw;N&0{aeR7S5=P%UOj3}Fh z*(OCN%J&Xn-<_^xJPF7Q0~sDuM!S>6_CoY@TR~4(m1y<5rmMb-Tf&n zDg=&mh;bdij~aEk=%oz+aX#VT+E;1)@7K2Jqz&j~_DAG&hoU4<(Xr4-CgN_WEg&VeuU+^B?LkslQAChO9O6*j*jYW>M6hb7 znW&(-l681g4tgmnI;}lxs z&31xQ`Sr{U)l@EfU4jj%V3Vt|G^4S$_anO4G{7sKa)B2-N{q&99mX#8Xl1&24}Xmy zWnmPf;rPWo>*fqbXbvHjCPTEv3?-uSWxql4vW?>P9@=kGvH8JdStNtuIyRRdh^QHA zgJk}`q2zwkqvu(#s!+C9U}h)kS8++Q_hY#7)!$wgcq=(21?+`Y-%c@nG^Bgd z^$NVan%XY}fmdm0Jte#lDd>ML)Z(77>~U?wjnggpp6U1#WN>eBW-Anr;@1cRg19To zw;V=4b3aM+)6FV)c|7y$td4k~fZJ{|^33Y>b<7#}hWaN8eKkg>f2~L^g;=Ck7-WyX z7hW)M{^?eTH4W2yz5SqIf6;vIb~Crh;iro0YwN#lzvp(3r&kZ0YxdQY<%mbg-Edrk z3SCPE z^w4-}rc_)rEAnjY-zhCK|mj8rBnLxkFf63_7VB!QLp09+q{=)GOOE>id{_^!&dyh4pp(671 zXTiV!@EPUJy;{&3#tl=?+g{ypqfdF%n^1Mwa7gQ>vav$^+SUDDecel1{Tlo!j-yl2 z5_7ux?(;>C2^YHaniqe5lR4&{aIy67WikACJ$lt0e+C~33iAkgZ_+il5NAilZ^!Z^ zFa8Rw)t73@rc4cWxaL6*eo)wvihhm{=J{kb)_%@e@}}&P}Zu=YQyk1*L(;^h5;) za=q;*^Pd@`PZbpJ9W#4MXXWm8o}`$mH>QyxpEtKick`4c1}jZikcNf$QJ?)hnh`X_ z{pv^0s|i%>bty&W1BAh;b-%mHx1L*PF54uqZIr$b^I+H7n%5zX(%5PoLW2twNb!Nx zxKBM_H|W-Q_&L#TZ%o z6S@J8qg9gMK3}1+xrh(>7m7EOXNV7%;lqhyN7-4I;N@T4@^mMB+EBeJ4C+?|pCz_B zsLJdW%9dEt^cUdD-KX4ToOHx|G!oxYSoyBF)|M^9k2`=s7 z#Zxr7dTBxWN^je!II|ZsLqDlplVz4=f0bBv57HwUBEr{X|EWB<)g|-n`nfPg{&hU{ zWr9y5*fX|0P*2?1JBSLWUcRZo7xwb+ z?eP-Kwe1&MUm*Sk1@%nbUMu1?3*V4Ad~)JJ>mQYv_#Qk|zj>_r?Hd2N(;4+ei`b*@ zj#*vIv)TxxzE*kx{qCn1(25h?#!b7JCF4>zj_jP=$Sxpdb@s;;!FP zOFpzul<*nx9ZQ_Ne4$q5zWK=2jjAw?GZ{pcSQcu)_b=9vG5rc7I=(} z2lgoxh)RyD2#0n(1H80{i%dan2^uUEUrBaI{ug*#g_Z?7BJpn&=(m69`7@NqkXU{oqjDr%a#+~5_lH&bXOX#17rG#WSu2+a z*5y@3PCGMgIbiH%$;{Sy_`0r-%}3fuWDy(>rMZ8dbI?cg5V3RlvsRfU)_!qOU6)dh zYeyn_>n}V|%ik`V9+#5xN|l1R>lWJgEdLeXPz5Hglsnn1lO`r>%Gf4)4P97NpQJ}f zC=>^9_$fHF`b&VYFbN#*No5PBh2O_xY3{}u%wxl&dm4dtDbfYolYrjAnCjCy0G|W6LAIY)y zr;x5y*OiF@pvZ89!$Q;>$bp~1MF8>cisNc-Z4KaB0b^Vsro1!Je(mPg+|_jrEFdI+ zNvPjCIvVFC0<;1|{P}Yl04;ZCR1nbVK7U^Cd2ULR9SUi|7t%}4FfYre2VK>f3MPoR zLVCCG+TGQVou%S`Plli)Hvok-(vX$R-bz5Re2?bng_YvNk-h^)w~I?cD8* zG*mE6b2r-&Ar0l#l%`5_J_~`a9m6ZrQ)rEGmsp7V#Xq!}9(Auv6H44mzpx&^NmCG$Z`ncBUp_ zR*XZDW}KRunhBbz)74hDq^4kx6a=;WX5d$&My+t>Y{ZV^nwr{2hdb-{9)4nDT)1=^ zgq^5UM+*h%T{I+B%(DTi^E1&d0n=Obdr%JEJ?%4U?9{C|I^;pIk@qy$5oD^)f%@Ww zrs3ZJjQCP>Qqcbni+3McVF2_^G6b-V{qEx%?-L%|*_B%8kl!va)(bL|)&6WaH~6QG z=e+JSU2RSi8mYkJkoY5%I{9ZOh7^y$XCv7Jk0c~v@zLbkG^wbm4S`Z~CSYY{^`ANhxLExe@}&Oz6XWA3 z4GLT~8jIVq%#j&6oR4srq*IUZx86;};9nUpdND+qo?jYl5V}yaG&#^w%z2r-3b4Yb z?{cZb+}zyW#Y)S@ro=xuR<)>`0Qd-Z;9Cl-9Jqvz242n7cQ)&&FOkHnns>trz3Q8K zzHoSbA`NtHp8lZ|*2`t}xl#HjF3Wr3bFao#o3@4`4!F4UP?U%3Dfosyhzk5-Bg?>V zZx!~hgrACrdZ+#yfJCp4*pl^aRnl*4N_(_sMAjDC58HZrjPNgJg|AEZ_1ZW(lRPjn zGYbTo4KpJnA}T5hJqsVbaA-rYyIlarlGfJN7SkGP-I(2*${^RTeJv!T z7@vHo@t3Hp$ev6g%B;hK$X>pjDxfi6X*cSc61^aTeJ(iP?rA8Q_qAsdH@4p6+{gDK zX>L$OD9EJSxaC)%pyg^zl2d!+f5aen_Y8~|MNfwFa}W&H`=sB=Z;%9E!E62uhk@fH zYR{BMr1E2(QbTNqK-8N$kF6Byb3}3&5K@5I}G1=_+!m09H@l z7p~{fJ8;;ZlRz4P-Q)n|JurCo20%sx0X{n}@VK!N_e;*&4MC$;JWt_6eiEh|uVlin z_vk<5#`6V1!A^k*7aJ@dT&<-thl0u_$W?p{sT%V(b4@B-u3vA`3C3$SU(aqz>#Q-p zgB2m)+TySm(HmWLXpDnwmp?}SD&~qWXPAqdriAWSD_@JHt+g(Z%yO6wx$c!u$gQq{4NB1~|;`;<)&*Ra`a_9}q^w z#E=8I-51!=&6AU~_pC-18Inz@usFJ20!5zczWwI_&ue~Ucg^}zmds>hYTLQ__?kxx zpoL`IWlS&DUc@2ld4_|S|N8pjGe@~DMY#>oAkT!rZoBRi=Ud)?m~k0hbI@!NvXN_= zG|wBRXW*jCPD;@$Y<-TRb1s2j@|nwYn9nkF8P3)q%4`>q*Lpz%0*mo{d#ui*y0Z%2 z`U4ngqyhWchy4>wjWMofc(ufGY8V$&R_%CozznR|NI0Cz6(GLgf=f~5Zg%$j+f~c9 z*`BAjlIQrW48F9lk0+F~g8%fel|AR;!QW2clNtTh5yLN+aWgLVrr}6fDVL4=@OMVN zn4ue*VNMn;U8p3T#w&@%~aPP4}}S)&6_Wv!2j{R1SbZ zxS~xhEtoPgGFnB(!8+5x4xVXwNreULqA@lD-9AH54<9YX>F>T>^&@+uv*o*P6VVpF zWLHW{OAF73ogJg!#*K$ky~ttzj4)f7*&E#^)^5^+L_F0Al4lsT~@M8b1JQb#!V~;A;re6~Zyl(I>uhYV&8BB(!KA zJk9_kFCUkwKG0L4M}#G}SIq+BuA{*1Nad}WbOF1`TSr@`QYB@!ngH`tOD#o~_qzk|1zo1ENCw4PuygkZ%r9gtR8~aK)Agl#K9cLlv=(FC=xL`h;Qy{o z1F6GKmyS1@+^@*ME4mGvMvabbNjsx5r4#B+H5ocdL&IebA=0MkXCN3s^A}wxh&EfUJ z1oVmb-H*7nV*P`IuYv<28gQn7gNyn-8XJ9QXJ<9|GP?r4w17_4^5&NF7ca-&yQT_t zLnzI+3bTJ*E6V1hKWOXMjcr2ryD}H#-K*Uid+P*Zx!T>?O0;;Mo{>5*z``H7FiA9R zO@GMAco(WHtT|MC&;Lz_TH&j2A%!m_f?hNB2y}Ae<$n^(E;+9ix@WP!yQC1DW`jq0 z?x0Ba>O!zJ?Gw&ryS(a$%Mxo6gGD7>T`{K>1Hwb|%A0->pZbSBcfq)+c`%x!$yU%` z5@-NOc17G34U3V~czwL=Yi5yT5yQ~*DZVV+6g{Wt%5dDo4mIA zzsW1C3pwIL@-*@Gw4t?CC@||Oh-sK{xt*n_+hU8|_|>UzXQd_0`bX4QPFLM~ULO0T ziW;0Ci%k6KnZRuULD4kF#>Te8zXtgEeFhbe<9<(y2ral{24rR3uJ47IW6~A-2#)YV zyIIj^-&Ees#zH*|+K)8D<^VYRFd(nM6cZE6)%t<%?fhGD^L~-dO*N-1Ji_x+;|9C$ zojlsuUB-f3HDAdxCvU{Y#3MlU51jTO&>iJHFO7h47Z5#gcGd8x zd9MJwe(o?E1etI!vB>>oNooi*q;kJFOYnN`Ke@cH;0qmXo!YnLU~V?Y5F7$80loX} zt@=x5&`6b)mDxKwN`h(?1fnoshK5y0uu3;gD{@^ZYw~8N$I&(#P%4SfY8{=Pn4CYH z`}n6L7Bi!x9Byb}CEywv8F`+=q$Lzzu5sZzN7rvCBnpd*qjJR7NCUw>Gvs@Qj{k)# zSZqyHAJ6_>@Yr}3_iWo)e#A`Y-UoENG~C0+fzJ{cC@VyiTnp5Y7h)FrxXAHO%_3hn zw04*9FyPBV39OI!tm^9=jddg9ZW#65PMfwEXIwm9tZ2xSa?*QgP-2cXrH?X zqYilI?~kch^ZJWpDoSAwj)H3=@>!MK_wJEjAp&I!?x7JC*7EYQd($Iz5EsMSE<3Mc zI;gdt9f)4&zI|nPxc)YGv3;5|a)0g-u9bD}5q?*2M2^=&*77c&OvS! z8<+5F8HS-@BOJO_(aOw%#_dF+zVm61Li+0=1$!uHWgUJ!p~F)al0NVd#E9^80RP3S zklIBFV<9lGMz!xlfc?7`C;`cP+1O4ey1Nw%yKBW@SNVZ(0%6@)_|ppMhCp17PO$EblovXoDKTTJWgB z@UNPMt6qR1>U2N`soAmdX|BLehRs+%TjVdXKxy9|PK}EFBL({5U!($G0+NCfSR;Q+ z4vYBu2!NOkT5U?Y1&}o(jI}2-RW4TVjs^z=i|!N`f9+VoqH?OGG-?aOWW?tMV?^9o zlYz24=T;wDL2#?j%*ZeR!PyZk@bS)LnDmAXC6tQ`W>?Wzy_cB2C<_aQg@whLw;ZFB z4k_i&h%^>1Db~fWZ~LB%mgTpY#8C(QC@Ri5zSi}%mQ^+Fr`foTTlwEdgprbfEAdso z-T!WYbuUuRSR;OL;$zc0+QUYVY}VCQQ*8>cs{RSt&tjPahNg*w{iR9CwxXY18t7+k=|o<0t`&vn52%*L&9U6dxGuE9DL%M0EM1zRe=? zCbqT&;uiuTRjK+9YCSZfYs0;2Cs(ps30wBH6+ln-EqkL(_f(RmJ2zEdj4^}HfC%Y(n zHk3UC=yG<}z;d$ND_ghbH8C8$KY^tF!?cG6{}X-(0i}VTzrO?r#on5UU8;Q}`U;Te z|IE}1U-DlOJkD4bEfRxGa0ln5=jHh;n3s_yQu+mG#@3D_!{TNS2{RGqrPNkDc-r!;|oN)yS-N4D0q_WMxvsTgoF$yJ~i*+6qMBG&eH_YxRQR# zpM0BL*@*XjVIOQNyPP$dKNz&MNw=zOeaq~_eM!`Y24>&PtMrWwsr-^+Wo>WAjKwN4 z?*_$(-JdFae|*7LkDsW+?gkB+W!&hqkGDU*>YNDyAMCLF_YbP~#RK^#PoS~=?`5Xf<;C`16ao9m*)d<8 zSKRUhyK0)>$b_Qf3VdP28w7GJA|muqRRU`WTn`?B=E%U<_)4q^rnc?_V0b=wKwWIs zw;Wfg7YP+6E{QF1ngy9LnIxz`W0I15lBhV=V!FY-Jhagl``QSUGXww-&c{`9-4qwn z-A&+2P#z!7ekxnY6&ET$ju&v_5}#H;vCw@Xpv0^SE;P#7!C(^fbw>}efw9>JN^vwf z(t94qyO=PZ%zf~d+P0RK7OM^bS@_7{8BHf^r=3J2=70cAlqyaSOcw>3DN1Y%Prk=|yD+g9qRyO6J3=|>M2G0uKzfT3C`UBa(`?}`NLJ=Z}o?|I#q)n*Z>gQ3;pRRKe&Ei`dwie zZ{K+FqcpRs)r1?WZT=aEdHU`>2k8a(bh$98-+7F9JQpZ7QBf$@1^8{6qJ% z+|$I|D4)=WY2s2|Jo%zkw&+YXVA+ileiv>6JMnA{)@`o<<})_dKLIOtSXm+pid$oX zaH<2@2gyi~B41Kxdem3=gKxF$v~qGb@q4%r9{y+Xbb~REWtxjO*F&IXiynb>)obG+ z0TN#~kM#OKoYOx{tV^6N_;u8BW;I($yTiEpNVH67%NTpdu^{H0A6T+#Y9!q~J!l9d zDO7^CbTFQ4sq=Wm^rI7WVo+kb_mo@&$kEXbOO@*e%BdINrK6J*^oY7qa41-EvW!ni z_yU@A_d2@GOG%*WfUw3F8A?fK^ z=*+KCgVFVQAIJZCykK>8zseZB_3o^HrqNrYy`e^PF-x>31)n}`ZS>^<5ZTnE=Hf4%b?mDwS4&_94w0_Bxle(>?r9@M)KvC)iY*zV|7@gs8}yaXT* zWL+o$78(L4rF$QypvpsjelpO%NkbYBMk=y(P|x&FZ?J=;midFRFZP+@G9%yX5Y;_{Xq6VJz8UKa=SW(BitOZ`Fa)|sXz z9z*xn`asBt8w<^j)LEq)mC^eHPUD}sIiUq}KKJflnE`+`Xb7ygW<~gV`X>DJ%i1V- z=Kk9UXFSc^%N$*vJv|qpMLBWUTxwcIMo|y%JsJ7XW!7J3BJyEaGs48!Se%0*2gn?F zMvX<*J_3^&>>%YIzvO&xo`M%(Xj#N@sx^Mlvb5(WZO70PXgwp%ktv%ZC}t5d;luU+|2LWKX1mU>Z%Y@QBg6n zv9U>wZlD;a{7mKV(sUxe^aTAHD!HH1LTj0Gf}RNwQa50lVrx|}8eTH!*5WNuqAS$5 z^QAq1={S+GpGsloKCjoE47@`G9jflpT-f4yFPSnR!6N93LOg z_+Q*MXpdlWr-$nh&ufBBE$4a9*9gqlGK zKzuq_)WA?)G)XBb6edpwSq%~aEh_+E-A@l7HL#Az*@8Uk=s@YMS+t2ZO1}Z|go@V~ z2ar#dI`=mS$^#utL9QS$^^y4DE%}nQJ+r^|`7q!5H#CnBZz@lm8dQ-Kno zTF3@WUkEmHf78fIG;lRVw0^wA)X~)?rlq|WYjVxz^E_B*+Y&rxa8~V5J;%l%-z4?i z(#Q|r0IsC!<-_j%i({o^@n!@mW2md*F2D|XOP7+q`&8zzW$)+9O3@U+;=$epbi=81 z9D*gPw-InJa(fS#Ynp-a!CwG`XpdP#gvU8IRsU-44 z{jU6t8iK=APEL+!ahx?*U&vn-hH)bZq-TLppCJ~@Px24Z5tv8JG`0d~Vi4%9YTu5} zCL}`*)cYw?RG!@}Cj(#?=VqSDUn_K2qU$k@0!GR3Sl`vHpYGhb{TtW~$`Xd+5l|`Vc8g zOWgXD90r8BiEkxRUh#-flj~Cx5EREew|Omxa(x9qY93vP1amnpSTbG;LFZY$-p8Gs zILU)T3?L5OwJDJzA=@B+6PpjupFEi05)r}A>{=9^xPlN85{kBzgFPPfC=f<&ZX!@R zK|TZBGWULN%OO_JO6bR&odEEFnx_Yt%-K&#fGT0SX<&J|AQ*Nd2rAAvA&zjyctb0D zd?}kmEdxL%G&stAp3Q-zTIS_TW{}7GpbTleF6f{!+z5isqjMBI0Wp>d;ts+{5yE3J zctc_ufEu_L-4g)f++HiU9&d^R?M6~BpU>vl52+2sui89#z7%(#H$a+UOM6(!^g?44 zY&@=s3pr2=eqC;Ohk@{tS~^@ZR4k!P`2jhoc(rvMCA5bd;aZuK7|bZfg@yq%)USng zOnVj=6*UDBvmgj$qoci7Ee8^ePPv`ZoKs?B^9K*hEa)w^J{2qRK>xA#@ZrPr6=La_ zykifAn6f!Hi7C3yv^Q3Eb-6j|Ys+T$ zKw*$Ez|j71TriV?01JOQsY_|F&?(CKItC~1qG$G0ACGSpE;IkQjb?AEwBzop8c%9p zz2t|CZ_a%P4fI6gW-z+j`xSAo0%E*6oS7hfljN$8udl<=wxQ=ZG(@1M2-7&^5gN z7M6^LG-R$o2PQM2(rnM9^mG<$YwIH$Iu;fd)II>A_?gh7D1$d|-t3rI4t;AZV7PD3 zcv-t@U^<1Dk*XssJ#xyyC#XuwKzw>Q{mYw_<&m;^Mi=Mu{1~f zg@wq|Wx2Mppgd~?sXrwCYOyu-_4fCqf4~6 zC5AhBox~aKJTUwEP*N)JqUROuqnomx;>#$fh>2v8bIv*wz5nP!FIIA`=Y?B30OJ2P z7ts(R^|FB3&`c!5?tPM!HqbbBPuIu2?q>zA$loCG+#ZC4%h1N=uI8<~ca>~wzkG4e zCxCm&U)L8k2ok)9@+ZHAN;Au=>AojI7w4`I`0|BmcGAGg>KbG5p&<;fMtE23JKnRm z={f^uBV$WDz*U~FuXf~tiMZtA{jeS9@O;zrk@{>+p2dx6)#3h(oLIcH&YW(wmH1BW_ND*QVs75 zlP39Qj)H)?6`6kVp5K&uMPzYE^%VJjpYG|L+n`{HbE%|st~-|;WxTghQ=}Hh^Ys~jCim%`nRn(M{jC%1```%cT znjd0~`B)*77&$M(4-;RUmb)e3j4%XAG;%Z_$Z+NK@eLBK7cbHQywEB%Km%c3IIQO0 zn%n-WKZN035t>BFy-%zw;9=F@ui`mp=qdmBG0E8_6Xqn~pqO2?Zig~p+k_Y_$h2q{ z!<_B^05lwiy=P{4*X|q{yU|r_6`b}(fwL~d`E9MsZS^Xh@%aU_tFg`cU(t|OCEXiZ z`sJQR7o}-i^0w1scdo_6c2v#GeBneshdD`kp8HY`dLM1kCP)M;un_vY{C(avLVq_f zJc+#0^e-N^Eq%IWe?S>vJ0Yv&?_IHMBjuizwfE|#8C<^>V7Rz>31Uc0sC%#xaIabb z0hsyFH)3%92(TRH-Fl-3y&mujNjI3?-nPI0;6cZ*$JUI9zwe@&nwr7Omra+7ii&=f zl@Fkv1ul!Iha`+1J4$^&(UttZDC%pag5xa#0Sd_Qno&J=u=`Yu=YGc!%67^J$Q2Cm z{H9l1^|@`Zy&VToGxtgWtPD?GE!4!J`b72K{|72SYSYzqM4DJFjh0xQVV=NS)mLeR zCOnC5`D8y|sr6Q3GD{{mOfC87dfjY*6nF=KDxQ6Q0WdSaFv>&av765NN6pYvOH-BmE)TZCR=@(2(D63520 zfXYMdb#Z(fFj&S&x$VUc(z@Fc39wCn!%69rbq8h`1VbzW)O~NQ%yzmHn?qGq)w0wA z6xrW&9yq};zOFdTNlMOcr|;B~ICuWhwKNeADQ3-{Y{XYP(~3zSfG?~0@;H+qd3u_O zi4JZILvT_uZvNIMy1$ebd3nB)tA8u6XmajDPA#U~%R8B!VlR zxFspD(lWlL4E9lMayc}wPlzs0wxZA+KMcBh!^zh3s{HWpuRyQ6Ng3ecb2VSB)CM>V z<&^*;hY7&daSpG;iBNhdgCi?I9JlT`GPIEV{K34(*$X%+BnxZp;+Btx)TW;b7$!~) zw}K1_3^rt_U0_8rRy*HYsykhE; z68c@;#>t+ol7%FAd#Q^O865h_92u0FDt+J@oaWq>_zye_GGG1am)O62mZlB~l3I=q z5MxTGNYX+7Gk+OCDB~?emLbjPk|2bgv_3Bk+CdZWW z|A;Jt=oj+LpWMWQ5BXfaW&ZDgzK5&J^rga^R}9~@RC}Iwh>YunMfwE+P?SijiiEvA|V~3 zfV6@%NUO96NSAbXqm#@tM<(P>Vm1y!qkre|(5IE#r22N<6T6t&&Fl|}?RT~Y;a$DY&^Ws+f{8Br zEBwiJS7(-_4`r5EYL(o!4Rj7rnw zbHyei!zTIwexbpYhzHfK!l*XcFHbZjRDVL!H&LK^>09|;=*=AVT zMYH;Is4SCdMa{h?z_?6;{3WkAL!V zLirpclbjz#fy7YKEhyMLhjW*0;D3MWm*!UZ)LAKe4rGka`Qxc-j7 zdCwl-kstb)5tdRDiRtTXJ~H@3t*SrNvwy6_B>AQ^4s+$R0^PH|)1w{B1qWB}`t^t8 zqH<}(%$=t-iGmA}Gj)PP9%kvf?}Z^fyE+DrX=$hzUDrqaEWc388nYGsy3m-?lEq=R zgDR792?VT5(8+uFdQm>9q1?P=Xv*`RvU<5Nd2zJ7E@SJ2SlQ?D_?!?z6~En$Nl#QW z;bh6Kf`0U!f$C)aZh8>229D)vhA>y50f{;G-n{Q7b2&u#Wg^ z>X6qaKOIVFXpAR(lM`+Ii&i|Gz86RkJ=~6#r0?u^rT_zcbV5-)76Q#{BSV!ZJ8WHT zqm~4YWz$fsy+3gcN`^m#ZU-zEBx2pfLI{bvKi3|Ede&3?#QE2gw|Q;U)(*9Oki{!i zJDG!WT!O(m>ejV4+uH4*3%%ce?co*CEBA1I=<#2^H~0utyLaggKRMfox?M*4`|F9Z z4y=MP2oOgQKS(r$ybcL1lZ!_ zwziwvNu+eg0hg{m+>NV#86+;aX_KIKBLvL08p*=ucWgwD0>aNtXRR3OzF#BiPM2o8 z{j}^$AixLo_cPO22&RhKWu3?F%xrEFU}Iw=VsIG4j4nHXjUkQDdDpkH!h_#K@-zpF z3-N<~iXmXuppi$A1$7MsYFJ7mo`2zxZ#ss&8mIgKq7I-^ofgBSGY^mI)G*L>Ky}l> z%O+r%0u9#Lx(;e%TDC=TqvGsC2(eR|m@SaJ$Zx(2_;ZLNaaj9BUhLipwUdUpACI ztI~ru>QH^?>zL1C{%>BZ`39A;S&|seY{?;hf^_Oz3p=+<&3+OA15f($PLIsII!AmE zk&olx=bx|Ms2*%VbGHX{-ZhznON!o)So8fdJ`j10I(BRNCJU_*fJLgvr{(C9CvW0)m^AS%0?32q?N z@`BW=88&YE+pBSPGLs=V9$O?}p9jzj6AtgA_vHu@Aj!0~P(_=ukAq>bpifD%pGNw4L8OmaL+$J$kA%Lv+7ZX5e>U)(VU5~%-(2m4k|neH zNmbSTfv4AT=rA>&Jn1=re0fQj?kZho~iyvTZ-*9+ulAod}RA-``CqklAZ-@L};oyFefG>!AhU=!r#kX8jj zh7SY3cQ>c?^6$zpUZZ1TTJ&9;bk(@7Ns2u)-7hYtOe9a7$s#F^xf*f<*X1dp)S4mb zw~&;4=XI0yy03M^YE5cu&Q18wB>ORlq=+Psy?l*%I2m`Z_3wxfM0WOogq!=}i{1%O z_KL3{WBwrE%mR~90ztrlMB!^%gHQ@dg~(yT1*8GeP{Xf}jEhPM|M-#o7Ax&MDb)VM zc=3PzGTHnUbg=orv%M+Qv4jMVq$EdG2NPfZx!tDJL#*?Phefta0!ZVTC*OP!HfE^i zS1l0=#O`3oM3+PFLMx4IjPIc6yJntt)isD}QH7o&{iaJWV;VOtEv=Rt)r$eRZgHMO z?7>6&XfawseP}#_ucNWd&*)T`SLit|9dwcsCUt{m&o8C|&n5bv-Zq3Nb3WDt)?vU>H zKL-Z7a>l1c6`i{WBB4xd1P1_={IScS5S>SggHff+2!;N@&_tNHySw}Y{rU6fprH$b zE<`!VXR^x)w@cD+@l*yu@zbfw-?gi`ZH2K-m7<2wGRt|8>ejzPvo*m)H$T5Je6G9v zqnw@ed(>4y7Td-?+ajwopVt9+)w{b0a_YTD?wW2g?m@8m?Y*n(>c{LLfXq`ulj4K0 z)=j!97z#j7&#jWw%`S#dBN7G!xRFL60yY)jg+r?K%j5kKBRDUS%$5}pUZT{2$jm@Q zH@syKquHO>Ilos>ZLl0>&+mbu>#6y@Qo)ryDSr2gQ4DAys@rs;*Q*<8G3l;x!@QW^ z^@2>{Yb|WPM{8UnzQ?aO|JoB?CZ`mw^kL8Wiyn3w)OkoZmKFE-NmV`qZUX ziT6`@GPtoQyhNK{?Y_Cfof<6pkcuJb1BTLxR?*yy}+97*8yP`y4_tm#)Wqio>CHpON^5k>8G$)kunCv zFvDdoHPGCzDF&-3vyXgUx4NMwuW-RaNm5O#G{ST|7hIZd7K3*EO>_%3iIiGEluGNb zUu74dD@b9kp$drlJzU~_wg`>SqxfRp_R9m$hrWD%_93V3+gyTvqa=xw<3m|yprsY6 zLHA%Jdkp|#A=9#b_!)4z+VM1uFPM`&qd+60K>HBb;F;2uXVrLQ#Hmkj&?wnlrhaf* z9!#i$g))FN!};MI0|B?}-yP~9$M&+v=M=cbY_>S8bOFXU$jLiT7+{8dS37JvCEfX6 z)TceZPG&f)N4Y%-A}hyDlN4ISPA5WAz(m48PS4J61=a#5Du!r$h{Ij^11_E)J$XQs zb_YE<>{O!he75WzkFg}jG0-g2`xCy+07N`5J3BsD zO{DfPk9jVBq=n+CFqzbMypEB~EOq9LO$n+)mhCUoG0NQSCRxD5qRS)(3YoOBjy^&R zM5mbAl%q&|ov>(bIg-vhE5?baUxHb)s7Fsa8U?ICB~^5QM_8jM|4_6vSSAXHC12;h z{rh<_lhjhn=9mBX2*QVMUP}e$K&b%iFitLQ8d#!|1KLj#b9YyRxUzN~!Dg--vL0 zC<8dSQjk;)9}Xaad=v@_>ysr}A=OwmPW$)PRMJh^brnbkrJk>pB!Uk_DL4>Ex*QQ7e~@e zEQqLgp4h#%TK~&1D_Cx;5rXw%Y}ftzL!alo<@nmg{U>Rczk2_S_1@OP{&!_!?r26f zM^Ewm-CX~o(O{?$u^{8_BHcPZ{J?FW9r^RFJ4LhbgH&PVp|lrF4#hz1e*b1b#9FfQ z5Cl$R;*)y0A1qxn59{LNM~CkFesei)P{uoUF~1~sENDADLWI!f*t|;1$Yv?g1k6`@ zX{mGJ(#S9WV&gQnB!MO7|9w4ozE$S#C^w~EgDJhC1aDohbSDbc;tpTxH8>Ah@Bdkl zTG(@N_~2gYz?{tr_l2ZXeH`n>pE>M66BE(zt2Z4?c&6`EKN#(NAAYnO%_{gpi4U&i z5PQ<6!X)dpFOG`3j&H}m|H)8j!%x{SzMsL%XLnEX2)pVMtkamJ-$FuE3T zUr@786|OL`agLQ~`*R2DKhqgi+EsrgbcoAO3fNdn*l-aeY}X}UrjN;mrjxD|!8aY8 zlZ1ux^(sptgO{Z8m4w~zTU+Ext{q*E#|6+?? znJ-V8`9%T$EGEXds=E063H1W|aCy}2gB!8dMtVB4MeqT#BK8_jw)h;zpOF7loaOy^ zi|O1?wj))+`RK96sA=AE;^Boi20R7-PwA~#MT%iPgOeMW8m5Zmz1|yC@gE&c+Tpk_ z`feF_)+^ZH?@^)Rc22qOuxXMMAAbq^u};2p>C` zC{m8S`6wb$==Po3Y5kU{lN6)z{MeG27G{KtGb8QK+pGPZG-pww!)Dbdus=VQg!f_tat$`%%H_`10}wE{BH!OPHR=}3J;1G#gT z6#|FH@(XNVY(E2>mvO@V{Dz0e!a|qr^E%q$+k4`!1x8|hB>e+J(X5uH>CtoLAuB(A zvT~>^YAoY!>_MD4^XE5?ygXZ#!{zC@AN`LA9xo|$?ekP#EMnigU*^_L>}~Vrji|nD ze#X`XHtp33N+Hj|C1k>>xV!~N8I3@}T|0;B{Sa>_2u**l_{&x;oO#|0*2)eV zao**I%2A+b1tli!dNf$JzZT}-CfIDMb2Q4#`g>v9vF|%4cMp%{*<#*1%-$F0Mfl#> z;j?*yo`#}MU4OPk<3fFnb{q47o^*FZB=Yc}MgE!p3$`)))fdNj zXb9XqG7}XAwKLg$IDANB;;jTU#8y{WV)z-alCpBN;38a{LB3zjfHo^R6Lo0iz~ObK zFdkPByE$*aZbsne8{tA#*Zk|9vyQ?T@4YtcPQfR;Ispbo**imQAm?lNm5Z10`5^{| z9D1d{j6z$le0Pz{=q4GT3AK8;sw3;h-k#r%oz+}Vo$aI;RCyh5F)CWq!`VGA=qvwt zw3V@)U;`)NzIhHv?f)Qqs0}~exH$Cit0H#7@cn?0kSne~Qm8beF2QUCZ$F;){ofr^ zY&j-IxAQLS2`faHmE*+}B_!Ud%Q(gC1}W~}6RhEV;c`|~$|t?3d%4&_)3!JGz?y(| z4y!a3%i}0jp)RP_q@ir3u}(ozk>lx^rgv1WgoDGi-Cb7wZ?AGc@K|GPW`{(>Htci2 z+wOt2YXvA-54IGHU1c|i~8l% zM&p^WAN3tU^O@P%j>5ymSX_wy@q{QEqD<2|QQTP)i-z!?@uz6Hu+30{4iERwlF&fO z^@@!x>OcHPM!$L=R>VON^<43QF009qmre3gxs>Hn(!aAX-AQZz2zuSOv`Ge)oZdyh zUo$_NTDR2K$rh>JUm<`@qwuSaOukiU`9SFYpDFZ1owK;!);iMZnE_L~v`8ruH~Y;A zGCwB`BlNn30@vspqtqQX5TCNz# z*C`J?#BOm!7ipYsKWNH%?mT$N@%kNU@zm{wD_gz+24uQ^`v(2%Uc=F9;-aRS1&O^8 z17gzw&YO&*(muztEJUH>+sdejXgH>Y(PUPMttf*Zuy_r1&wh^ETMZVRtzw-Tiao z6!Y^}cezb;RCM0Nis^`Mv&*mJRVkVBO?>|M` z@)Rfjk3~|Wk7J>5ww40F40fbx6NU@7WsUm zw9%5{TWc~Tv8J-v+E@N2>!;5ZiWNTp?n#%22_=v`Sf1s-+Q>)Wak^V|8OJLa^3;x7 zw}c#TVYnuHGr#pV-rMySU_YZk<1@t$lq0J6s`%(Q{^&X#h z?I;5Dxp0NgwjI{pwr8lr!07R0e}fMXUOH=WTTZ;DBG;cnG5v97aF6WfN4Kp_`Kj{* zE3Ku{h?Ync)H(7x?U(Y}AN2vQOHt#J9+LbmR7l?+uijC#v_?y`?w?)i}cQG=~ z$F;D1+t~T9?t@b2t~4!6B6X1p$*=G_tb8=mp&n`rB1_8GePs$id{^gqefoB98?ujWywZ%2RBbg0C&iFWoM`}s6z+$L9oK) z-2?ypWD)A%Y%xHP>4g^`hkl8}K&3*mnhU~ThtGX`sdf;*JWI#T z(OjH=XW73r1QIKl6wx%Jn|B-Q%yX_U{qws^|8Rf*vM6TmS%ZRWbrVUe^_~aS8pR?! zRks}I|I5G#A9fTp)f zw;z^9+Tg!oPd_<13@+q~@&C)i7$a+gtJdN$Im*B4j7XAOr=nK;|Nct+NM8P>Ohs0U zNmHnAZ_<{MOM=<4+JA2z(l5KIdiReHiuINRh=>XvqB#rU)aI=*{Z%UdM<@LE95Lkg zO8@WKaOh01uCmeH3Nj!QMnK@g#9G4^!ilFk+h1&+0*UlnQLduDJsn4@Pa%zN;rvCw zmat#`Izci-(|DJ(&9igyRr1dB;8=NF_7&df-#3^dnoHFLah<-a)O%G0X_YN$K z9Nhd*VhnX3DpD1WTVvx+Z~yKExq;i|Q+NI-~MX z0WdVp53cnhPVV8x%g9|T?|^jt&SXb40~`2_M!_Mcqw1m+lh&ST;?CavpmG)(o1sPb zB|mOEqIW*1q~l~PCDVFW;!kSBVm)o5?3(iFzlZ(k-ZHehsvKl)xu51Rp61S8(Bn7o z>x6E^hegc%w>4Of*f1h(0tgd zVa9^NU-w}HxsHBm`hAF3@U3_TU|@X53iqL~)%=p5l%`x3FHn7~#!B{dP#MiKCNZ&t ziOI|K`t{#G8`{0)ekw`4{S>IFB3B+sT!2EIRQiA{G~xpDUirz%2?1-`i}x(y*u9N) zGsAA4FNB|bCQo0_ajzsjy=gz}!g`m6=?JI9#;6I48Vfm`_6dlXJD-{2!M9OTQx;#T z^Pk|=jN`($(crR2yo9Q0Md*l9jOO%Jz7rz){T`eX<9sdB)3e{)Mr89lGl?gCiu#B}3^lUxG$1 zg83>Gv{cdas3hGkxUeGe@1oDmV7;>l$}yq$Q~tI+;<$0MUUxKbyIP>~LI|(n>c=}xI^C#!ugIX5Rdj(WA@v#j59aT>4tY@ca}ERRw= za8p?FDR51x3wc>GyyMeXcK?2qog+uZRd_Y|tImv5W9@a*g1(eOiBv>4u7h4)xl9Gj z2+w#3p3n@UJJxE4?i9+Ag!xlgkg2^2*K{UZ{_}e#FyVDWJ@!KB{iTNxskz1ZDzl`N<9oYMw!jA9GJK>t& zBcEz;-g(WRPnVL3P{6M7L7F02Xn z0b4FYq;f|o_d8*0+wD{pOkuCEH{ZTox71h8U{O!W`*fMQ;YzTPx25*D04cof+7p-N z74)S_?~WCZQx-}W?`$zu2`afx=33`WH)qNNY?6pS!Dl{vg@&8t_e$~t69gC0k)E2! z77Baf05z6}syb9@*+Z9*bVEafB@uFEt|B}0Nu2gL`Nb6;r<);#KUbCOe~Fb^>HGW1 zmneUlJ1JYEj$GC`HuiF+*6r5@bPnB|Ugk%Ttav{8w(3e# zxhLK?MSQ3sJ9{tR=yF5Z1v-Ma`h(=fWSe1=nX6UuY#>`ZhhdvO>4T9E=*UxwJ@Gx3 z_gl8Ich0X~4upk5xz*5#$&GB;IGIc(cgk)j$Gm-YUNZE%=d-z)(l~LE`bBdP!#U1n z&&c>o5>L(@`G%=g9_&c(zf5~6g3NSWqtU1i>ns22zvQ%RX1;jnnZG>I^@k^8p;mdF2$R2o7MX)Eeo<(%Oit)QwX>MyB zB;S6HhKzhr3wA%SpZLBE>+y>16F^qDd%m)_Yk|*;T0i;LNuivbGGF=KsK??5PRIM2 zTMKz_kc|I5UlH*QU+Qho{c`cJ-yUP6u|*%9z~A0ZV(nK$s-A+!5p1IdvmffsrduP% zrv^-fHYr5~Xs0uF`sDn#mNLbmqhH6QKNjz;!99gu9?LXQu2=6JymJ^xR^fy_q(G&j z9sKxw=Xer(O~4V;G7GP5`*JeQ;Q=%3`-Lxqi99UW;HN}g=CdhGzafUj^lD`(ugb#r zUnHJUBO>165>{T91~#aE56So*1&thHOq^K)w{aHH^Ey5b_awaE-u)i;S&f3$oBZo> zGw=o6ayb1arK6)-r)8_iU{W!sKJ+zEb%F5mh)%+@0QMxl&h*R!J;s-K1@=4}PquVM zH1>VYL#(Bg^B%%9Nnnm1aWK6F(`!S^>g|!lw^{{gUcb{s1SUWE^U@zu)ocDq{(29#0&QY%yo3_(D z)pQ3o;}{1i!E|vU_Le?6ZtbijxF+2a_k@0w zW^N|vaMiQ0%XG$f(>!SG?NbFUpSI4y(t8`u?mASkw=_S-W6WQ7V)nW2YjSk?Adv54r$&T2_g?2IL+o6ci^%g)o0j3KH+a9w_efs5^uW7 zw|<*ar0EjEbjaRQWEENNY`;D5?$rj14EW`i=S3oLttdx}FmxrbiM#7xI{>Ij2k5|s z<>mRr6ck!HRlERV)Ke&ytqx0$;I;K0qGQ76U3Ad1DSquUR9&J~Zs5^&C;1&~*4B`T zPkMrmcjhvs=I@reDSpp;Pd=+HPO0;KZe4xeg+<-w(?Fr&IznJHDe${L{SAz_Bej`r z3gtv?32{T=mD|Cjiaqwe$kxf$)I{L!&lc9LyHCP2DG1VhZ_8NiQbq9n&Y@go80|i- zx$fd2Y?~w%{<(Hf)3;CH?3td8=DX_H(OC7;wdDc1iPdqziv5R1n*0`xIQ9*(a_a6hBFcs^;S-9=AZ|YCmy&tlS-pI3> zETo+~X>M)b-uMlH#g$Cqb7Nl#U1`Z?S0w`NN$xm`a?VUGOLl#QE7IyRp{^yG-I9Kv z-?Q3nva0v{=cZmc5E<~rh5cXRkp;`|g3YF3)|#yuDKeHa@>d^h{}yuR2U(X$Hy71I z+grumr@&M*>7(XNb$@%cKL;JzkH>#_E#KwDw^<%8;+NjDA&}c8Mf_s31U{?r-l_J# zeZ)OANxpyfsg4155ZT%?f*=KnBBul9r+H7&XheJ$hHkW`=I3p-N6c~l;l4>kBVu57 zz$-Q6@{l{B%uE7SipHjauAY5%3V&??ezaHd7ms#WfVhS&njW)LQ zh5U=WeW`=6s8w;DdcQpTJUHJHC_gxv8di0_d}H;2vGmL8tEEYoo?^S^ zAZGo6h0V)e_W}j$cB%wX?j>WZ%t3ySJ)h^cDN>jog;R~Q6@NVyr!$!c9~I4>35HdX z&*)j1U)tVnZ%!*RYJgj&M;vNAr991fGkw~G=!iwU<}4Ie04f@%9-Q|^fHyxr*uRDk zYGPZQTO<_*S=|J!=Pem0wpTpO*8sbFyl{19Oj!Wy z>Ep)~lg~#RFZL;%ENlWp51N(b&ET{BVUYLYb?UkIRr>*zM0>~WJSbpC7Tk$UKr{XE zN!~vZKQ4xDXlSS!EO2XpdbkCYwDcb1|7B?^U6c=e51vD>M|E}keFON-`48B(nZ~Ih zp19gTeD{Uw4a{4{i8XuT-l|8&Qc(UUnmhWqu1ND;R)x3E^`1rOiiy_dsAV52IfhaR zA#pr%F+O0m@_bA{qpTy2H7=H!S;fZ3a*|wc!~(}#bxZPf=I);#8NsyN+;8{({&i1X zgPy~Gy~bGp`M4(go;YnvSqZdoO1-t#Fo+B@BBgHY)2mmj{~|@w%}3q39gZCx8y`ZU zewvz<6*2wXIcn7$cD?HwL;3!3eJQlt-Vb`UFL#zJjFzi&oL{|hL+upGZcm?Pw%^=6 z7OYTEFA!Se*;BbpQi4vzY5bTqp0}lU@(ZUMVq$*r5rh?=bd8M}1z+@h zXaKatR9WMV(HatiFS2h4LvbsFlXi+_Hce*BvmJH!IS+7!r0=nFaiOxlchtURjXznu zUcWgLU#eY`i#GFSTQ0ZFsP{>OhcXH)o@cJ{M!o&?MLikbf%$EAkq}q=;&*+|6A2uC zM2*vN{eeoy!p+u>Cu;In*^;VfDxOgh=U05y!AA7eYS=kaq`QxQLAPeaol(ptAyz?W zW~A*SuA#AxX@P!DxVKR|lz6#?g>D<}M4VN{GN@d7>9>0J7W6+L#-6GkFb21d4l23B zrxKVKdXN8KmN_36$FG+`U+fjm=n0**#V=lL6oQj*^ZXi*Xy8~DaaVqIX1dWDSD;iR zrrCsxYGPo7YTmOLs?K!jw5JdPAcERCu_MTlf*~<2M?-rl<_DlYqm{wjTNB(On$1r^ zAdv$`*fKWvA`p8_L+V-%J|5_&ll-qX(pl}vgTV62At0}F%@BDFt)EJ_4Y%agAA>Q9 zpQ7}tFdxU{6|SFLQ!VVhzb>#x3>k66{>L+zP3Q13%txY)y1;arQBRnwtJ;Qo>UAh9 z)jPe3T6b0U4|Ie$gn-vmF9$z23P}JG9%X=tfQK91@7@S4{FRC-9Ig%y#wC#Sd{j>n zqeac^MVSoYV}KFSZk4-lLj|Vxp->Vw)4*@Fb6#+rypDr`61w*$3**A$3iNO%cnUkH z_#Nf}o5E-b!&g83zm@%{jj*B|YFeD1{@jJF zrO*G7KAiL_!h|J3EOkHI@4bG!v<);^Pe_qNqpeoNpkFrU4Xf_#3@~K@6T12$B`P z8tHNWzWeIhlY4ex<dO?3AKU2$2kw~>sH`)#WD>A@dwPAYOUuEVi`9UV36gQP06d0S0+ zQrDea^-tDyw;El-KHJ9nz~@a|;j2!-@paPv8hVgojOBRh)Bq8_V^%mcb7wZ^PEdk3 zn#f%Ku1b#4X;Gf&CT%lP|$062J%Fc4F10FSrZ`JSZ+1!tZZYVhUm`+e2jBs{h*f} zvC-C0Q9vT66Bxb?|1f@CfYnm4)>m&PjbF+FL}iD6=dkaxv882X_;VSx0hAKZPC(qU z`}N)ByM=V>~&cW&5k|`;PPi9ISE}g#N8y83jjtdAZqmIU5-4f-!MP zfVNVrcdcQzi|%%Eq5i-D5k}L!MXP?jDrvHDV$Iwb72*;qzUl)B|rvbKW%U zlwBJfR_Hf+!zn^1EXcvfIP@KKm&nBwk>v^>>9G}7nK$H?v(zSZ_d`r92D zK@QUW-W#ZIVkaG)Z4JFo2%`P0*G9^1T};TpHAOF;Zx93b2lQjZEFKz3hA_;`uf6&; zo@o>cLXW26tJs|GF-}ZTP~$%rj?aiKV}n{Zf-FyE;Wz7w%1j=I|AkN@tMBZ3a!W0m zEcy*-+BUt^S*a$$rN=!yPpk0uB31tJbpyAMYp0Lge7^o#1{chP;t%_N1br zA-I44zL|Nas{$eHPJe%6?(S~KN%GRsq2aZNX=&9UG8vTM_dOW^+iFPh0bUDP;a!T1 z9^U9v@IVq?!6jKYCdwIzUT@H6NW_&)V zEFG>k_+I!FC7X79=(Ae)5m?-p96012y#wo+*B{WSf17yRRZ>y0 zNkdFrn*HyeKq{CHP^Ql$#^YG{4dK(C$BQequ>`o@Xgb2)WY3~E*V(T_C*k*8eOi3~ z2d&sc1~kyT%x~OkY0(vjXh{0kXVwXI=r*Ngt0z~S{%j}6Q7XDNI9S}rM+W~+#3yRr z{1UE|M(nqpnPAScoU3ekr;~BtypD0|wr`~7=QpvPP*MfL-LG$&I%LZQLIG;HCOZXDXjn@872YP_$iGx77kAt;-r!N1P;m$&IW$JCV*4`{D6rK?Mg$PEL;9o=N`rkR%nfP}(Y)cQ~y# zSi(}u{X)y{7g*x-8C-`#=idkPH->3^d-e`{@~F1Axnfu}{mK3fE`_Td*d6!-D1Yp) zqd$W1w3#*t1oW^q#%$7JA@f%of=g8LAHJmd}=^#Qgz``fYk}7xg6BFnoLl#*Z-*7ha7r9RX;sA51UQC{#B!tEUGPZkm4c$ySf zZnh$sOKsH(XI|J`|Md{zEk5PGr$W|>_*p!hyLQ6iB zz20+t2h;17HGHd$|u1)LE{QaW>M!#c_6a-h;}v>>pw( zs;fc&=z=z*unQJJokzPu9tO^XzoGH~h8R4AtCu#`!ifidw&ytlSKwf>BbE5Z#>>mA zn8JaIo`~Vj(btT9yzC1a-dhzpR+qhQMBd&UP%3h`VZs-PD5(#N6@ECg=a)^qG|v?U z2g;9PYtvF|Gdxd6s=JBzso_L|ekH(DzCX|Kxavb4QoVPxd7zpd(VyD3esE)pIGH^H z&fZ-sb%Nnb+#aWItk&Jf$4167%5$g>eA;Ipqq3VuUAC%yAs;(Z5YWK)@+~vk8))J3 zV^s(9SD(aJ5r7@RHM*=a^532F{0ClQ81`9?Ad|BzB zom5g)of|@#v%&Ms3EV93eG*YqM*trM4MCB00Kwh0ZV5}3@JirFohKiP6bQ@p7M;)e zDgWTFUqGY;u;`UEC7c`Y!wOPa)$;++1j2MuK&|-)21@0p(7-pQ62tW5L!-B8m($AN z<-HY?iMYW6JsL3EfTcn3U(7mD-6PE2m&or=%At!y`PjbjAuKeKkroMpkN z$QWcI=5^-b_{Rxsru4rJ=Kp3GS4?>A+9hD~!sJ-}-b8*kxa?o^^4!N?oNJf(w9$zA z1VED6?5!LTrbn>Qak!@hKPZ}aL~|K5V?Z!|{o?$Dqaw@zcur(nU20(61VR-y0*lN5 zzG~>Xi9HG12ee~whu|*XriwVqw z-2$JJ)o=>`yCr6~l3Rp0TQHFQ;jrpWfQe9jewKHW0oRc5?K|MU{`Fw(5b{1I1~+z` zmWboxlc#y{CkMrFM^wXVZ)_CXka2m5!kGatEm2*UcV0nW{wgS#-EBm>ivfAbbMOBB zs%$RUWE@D9|Ni|8JjUZxf>2V9#UQWUbyiVke;7SI zyqXU9=o~#gfg8(Uq(bc!z%FpGuJXO8Z>E*D0g&Cy@^Xh@{teb=9q+YZ6fn$w2!jwF z)INeGrGnMAmdV{A0eI=6(j*qX=pN;bFB^Cp9Y46{5*TKF$|CW2*8er6TI6G}ctyh) zPNVTvL*!zwBL~0^-}i2(_=lQon)soqR$f$o1J@-Q3}(qu`$7W4sO`>A-7BlB4fia} zXOw9PQvrTi>9BBRWhgHcR`YM55TKARL){LBfIVpG?Zt--SK`SNJV+O|_Sdyw%88vx zcvu)J6M<+IhNq{(egeoK8K5Jkrl-FG*yJ2YyHU|=TLe`oJSG*8mbHvQCQZQp*#vs7 z+-wEjUdNq;M-D@8!9|lOoSY8xvbql^8!pZ%AU&B-dY9-Co+#pdv#F_Rd9sENVsQ6W z@Z<->tKgAwBEPlQA9(x)rF+Gapuhw(%&fe30}C&*m`P{!UA(MVWH3~?(;z&GizDe8S30B-p-_*|%I4#0++`Td)|f?y^3?`j$~c>^`- z2j;87+>g+ugt>j5eH&$vU^K3z41s8i)1vwA*XQ%Ns-a`3`^8g1TgG4d`;Wma`ucA^ zFCpXMWMQeKqOZGAc>kGyQSL3P72d|t>nBAXfI@L{kAqZJCp(SzJ1)$%`8`GX_tku_H4o-f6!kqo<_akM1w1O z-k9?@5T7oHC?X#~uFKrnsnM`{?|{53EBn27P$1C`vp`-|odldzyPgCqlW^lb~q?})ds;h5a#sF?wmy?y?uM>`t|F9 zmxaME!ug^H<~qT+S&!Z2m+|*e$uG*91XKeu0jE2*39v#XXbBsqr)gj=CW6GU-cAZW z1p7m6KFqlX*X2WRZ*MkU>#@wWtSIiD)>e$Esi`Z&nwpx-Jrrnuq?Jm8R~>;7FuDPy zN!8iy9<-N$8=ikKfjVYKUnuqo={UXs@|$ucc=We{aB5h7VgYD2)Ul7$-j=kmuyANf zL6uQ+V8+EJ&r{-7luf|r1?<`Wxw+;Wm~!>YaeOm2trvCPHy~JnC|SFIwx`T=;wuw9 z9r}`ux4w?d*!3((lxl639dq8mOd%pC zsFL1wefHpQU|!KprYCwGpYRKd%7QbjZ&VduOx3?q3-xmEHWHEP#^fL_%DL4X-cDm+ zna)mX7gb9ThwF1Hdx&qH*F?iiPNq3eV2FI*VT z)8=2;s_oiSd>3A>E!I0Q;Z&T_*lzP%g<(qX!e)lx?4ReyugJL3(9lr3gRSP`@*0?w z0Pu(*cKr1IZNOIt10MT8m-plqNWCGdp(X8zl6%tgU=zTe+NtyNo$ttmJ)H7+((Y*K z>(9i&clE3%w?>H0WHO3QA_njVkOvc^jkd_Bcn$tq2=-w#sb1o`Ux0Z-O%0Fg9ak9B z&T4Jcp1v8BmT|Zdh&ot%oHZUB2a{L}*l)JWf4r?%k>@|beZdo4g*{X4aqO_lV+5Wa zvSXHvmd|%btrls64kxv6EC(@YzhkRRj)YZsKYCO=v72}FuP3O_RG{C2Rd&{a0gxdK8r7{z`mO?Ae&C4iosLW(VI;g+dm{d2L3&%8~Q+K=r9< zUU_4=e-IBI3PQvQmvDoc+4Cn>r_O@JjpOWhIC7M)gIUaBGo|)%%QmJe=YZ->vBQ%w z$GaL;=$SKCW?CF2#*bYTl|IV zR)?yPxY#_8NM2u|J5;89!7&vuw9X@dA>@^SkwRocCa7UQ!*si4GD*l9Lm!-Y=MO~3 zxilH5@miKKLPs}?4{9pT-+ccUJ8+iM0-_iun>9k!>HTD)*U{(; zHs)^F*RRgSFMg=-(t5N0)6=a_?IKZlL$!Z>f&Gd`tb8Gn-w`!UT=o?RYyR$8PHmof zomXI(7BtLELC<1mEBd|f6A9IXum8IZC}FjwiSMl9P6@v!vDe7dfp)#Zh2xQ2%wp2-zN ze#}n@(Uk~0qRh=cwLhVp^|&YcKKJwI^u&B>$%jD{B->Cs*n`blLg-&DnP#!7_YDup zoYoqt&G;Cv%S81@+~LPHS4=(&ifm8>+_#{o-J-Isg1cRKky)BRHE|>&MhEH&XJ6F5 zw82sEHo5q%Axxy4h}#Jx=TWM1FhLq2;Dsa$s9-=*;FG^e`<(|;;{z1kNK#VLZm%;I zeSpsXV>eU`AVha`20S8)p;pfCv3m!S6nE!Np9o6+=Jy6hA{azYgYu1i=TLxU0tv z++Ni50Kc$JUTbS>n9XA})A+J>71Pwz6e`i_kPN+k^X4Ul$D1|xR?7l|gR#ddZ2QUx z5O$sF*1BF4;ubaBLd5H^F>iC=6D|bqMC9aeAWO^1&9y(;H9mo!77!+uvYwleVL?+K z?7}V38iA__WT`^q&T9~4VgsdhR^m_HIOM@ID=Q(uXqA+bdTBsLDd-Y5JOzO|ixmf* zW!%L%7$O&8s$-G^N0MMw+O3d8e7EDtbeE({&YP@P_pQ&-jbv1~Yg4{M_4ZkkuxGha zbZw8BncV186^X|{%tEK3p@9KZFlr8}QIJCh1O$kI6`YeZigi3siw`v>1>%`L`{#pS zSBc+$uf1Ed2j=HjaN%u7MK4a#0UZTnE~p``{9RT6#rBa>^XtG{YWo7nWC=T0*FG2D zpr9ZWfEEZ4s66MM)ksu@?&HU}kWRM3P^Zg)`$55{b{6H{Jq-*DZ0hL=W3{!k3{^-H zF!=lX8{!A}GH4VTb-uj?4)V{m!llL4Ao$2i>GSQDvU(<$8$4%83alD5P zJU*vxX6EJw0Ad5X&1{h4(*j*81O_Ygoq;L8CnY6XNN&i8o`buOI7Y9tvvWc!VC{04 zRe5^P`eS+d_qc}hf%*CDu}210{PBUjc1Dx%N&hO z!<3%NVW{174wqL!I@$#J49e*a09m+{H_XrGm$vu6chm4*;`~O`ig3#}#FHlfe{8*X zIG6qZKK?>Up&?}xsT2gk-M>8BtcU_xhgK z{k}i{{Epvo^nM@refM&?Ue|a&ACL1qALn^03SPT*j*g}KSavz^1X1qWU;Dx7&Z0PM zO1!5Nc|E7a^x%Iy2xRU13u(8@-&HwFQwS4cMn>bWp}fMPqU0ptD0cTL^lmXDMg^9+ zEU+ef3a}Csb1QIyKa@v!)o&UCAadZU6^nfrE2Dy=<9#5Y814W>E_KizKLJ?Mz z6dXpLYtM1hi?+754gU->h}w^7ZP~KL81}nR+Gso2Q7ymu;@i*rbBs?$?93gYTm+N&9@CSQ*uXvn#_Il>$+fI~*QJBRmj`20G2 z$MQSSVUs-3WCBSdsx8}YA~@;9i4$|De=;m!fRvTrzFjX|wZ8c7r(c#9=P&pM&lexR zQKa0c&MtlmA@?y^JPuVP?0P% z81WK@FkmfqeCQ_sn4PTv0pfHuhezm$`^RT}w<;8Th}Rk)m1o*4oB#qZ{J| z3V}Db7~{|7E~XuexAbQ}@Gyrzo144Kx@vWZ=D8xH@RO8R%nD}$;?ylHMuOoJEOKZM zdYYl&2M^@lRXqtx($`~=TU5j>h85qU3Svji}W@FQ*Dk?98cPdg6p+qaK~ z?L??cRGlylZc_imL?HN1EcxPA-v}!%cv_QH2>U&L)}RGbM@d+An`~~Z>*n5K0|11Y z7p71ZfIjj$kVw=^8=RWTccUXC5cp&~8vPfkH>2gNKiKY`FF|QNV@G*tpC^h$W*Rp-yl-u^Ts&8~?Duw!XgD7oQZ~Y_fvy zQq*@)FIsf*vQ^8g?URdnS1TVC$$y2-$FKHC=W9}Ww$(k-59=~vZuzAbc#w|_z zt<&VJSkcUNyRkOj9alq)yv{8qy;YF)aEdv&{>w z8CzrYS1_k@sAQ6h*34|U`9WbfX~^#CL$TCX+)CO`sA4sX)QvVK4{;u z+q}AM;ZMwLm;Ly`wrv+Xn&#FvY>8Z!ES$@CWTt*IlN6%sLvOQ}s9rrP{$bKX_*P2o z6XjvGJVR>rpIbEeYg=}$hkn^baswI5w*n(ai%UDYu9rO(9me*`uRmpXMQ*$5VV}95 zmDJ$^OOly}A#P{So%;xR(QkoO;MGH4@eBnR=Gh&~ zVIuTDEdYqwNSy`rOg-bUq;)_d=^w1?&gO*UO4aAjQ{k1>n{mZb!D*7Ko1~2sPmaCf z&tE+2m!f=#Ztqo#5a9)ZLiuoE2QgEQ@s5%p5h_#RgH{fQ&4pYzWt)q?(X}KsOa`wO za=7&zv(9yr;Rs&6Ei)X`xLj0t`LCNt!%Qrmv>m z8^4T450+Rob8U>ZA7N);sD|KNVb})ArExMIv33p1=!BfLv_ey)d3gMU4Dv#Z z3LR9nh!4A?mA^Sy4<(2qbK|egd@Nw5L4vZ6LcttNQV{QFM$0Q-D*!}(uhx3&`&q89pTFArutNJrA zWPxUFmC@#_ag}=o7M3TPtFFe$7h1e;v`48a{mD1-wt+>ip#ZIvN zZG1jt{<<7ZvU-1>?I!idlx}U(>b|o(p-R$& z+%rd&=URIDqaM}!T>rpM>fSxcE^dCE79j~GDh)Ikp5`lt3p;k3(rlGHRrGST(FRLV zH`&b&2iBq5xe4XPqmNTLn016^oXMG={2Oxh4j!4Py@jeLWxrrhRsy7YKX3U*+1ky2iyH)^zo+)m`4))t6&YRv5g;r5P3PEm3-ogM@B4Oy?S*3CeyGSqJJTSC7iU| zU4|#@>&?t=*zKZl9K@@Ir^WLLGQGdOJu>EqQiSAr2A8YFpp z`xfj}ciRmI>k|^g@TeynX2l~^iK^gZQ5E5XjIH!`!;_E@QiCM&0JxRH*A}{M5+>?M zW#7K-kFmqxKm4{RgMxw>#jdHGDW>;!4NFf?zl{`nqS{11sHmc{h4jTt6j!bs2=37v zZ6s3Gtsl(rtJek9LVAf+Q@6xb@|C7#PKP8aBSOUndLCu;0cmM=yeA}2Y*QHAw&|rg z9cHXoih%2WWEv9-9HVI-NQ79td!<4oAdUx=%yw&1F`B-6_wF=G$AQU7Ay+3wqeez7 zBrNZs&l9ftH)(`O%sxuDZ_Lz3_C9VSiG*ph2RIQ+gH==&#sMbqdX;dV;Z}+eeB0as z7DB{n%DCA+R@!Yx6=TMg;zyAA;rsdfeiE$`#>F&1ye2Anh0~`!Q~J;Y2v-q&B8eb+ zKtofJbPMi|MihH`yZ*uvA;}Bn??z;tW$01ssQC|apoHK7wLfg@wrz>v*vjp3>S=dk zv0x{uM3R$`adM{Sse*edKWC*9OvR^{rcDRHP%bljoU0CGc?h5LgXh*KdW-d)O2BLn zEE7o!r?Yx1QS#jR`FXomnl>%MCi-Irv9jbZ!#+mdrKQg&e%q;%U)xA~W7XJCI+b0L zxx$B0Fpo_ERt%6?wIw61<)2W%=_-lR>(ii-j*iLLxF9=HWn5ZjCpGon*z0rsF?Q&i zd!Z;$S#4`=9mI~ZKddkB&I5{mEDbU)3;eB4rETaqB!$TPWafC-caiM#!Vy+*#_86z zYgMLKuX+yxEkS$jI3R%qGlK8ZBb`*O^E^mGU=h+t*s`q*wMpEl{jpm;JS)q}ii-WP zJs%`FfgD@{b|60ilCB=MRuZNGnU}Mbcx+IVY%2pLRRiK+87X{{!!f1Kv%b-hk-vWK z`%)Yd5+WX3yJ-iJU9~K2_|W73&*B@yf)170eAMIkP@;7^n^qyCoKvHF1emSK)08x+mn|5 z7Ea==r$`6+)h$7_d^u&ffuamWle|5ckg0`*{XvLOL{tp-jlqEdvR%7^Z)S?!9Z^RC zt|jrUqoW%2t(Y~EZXEAU_8KM{oZ|aKBy%xO@0GPZhkWrwmGxY4nH43W#23EOMq&>! zcT7#J?iGwjsHwW&xwnJ2BiiHv3A~BGKg#bw(?CSTHA})zP8Mw#_G^DcwNd|qWi#b4 zt4@Z&eJwtpj~_q2lVBjVrRnU7v5Pt7vLCTIt%{mN{toH({MaQ(ddhlxucRn@u#x;8 zKi=zbkca^!0i=?9qxG|JvSe>B_Zu0WQwXBl;X{s)wsICB`#7Ri9%e=0j_}b-)@R%% zx{eb0b}*flbQhqeGfh{TTJX{63VPrgnQ}d|X*pRTYlWNGm=vYoOm>>JWFKMJZz&ZN zlORvYtF^<2>n|c~ih#}>%3E1j)Ae*L{yrwCc z6TAQ2KTG%!zT39_qNh=ztzeGLKi6CTG&Tud{)6dzU^SvXvQHe4Warow^g3nftKA8l z^q1qoc0pL|Eu`RARio*4?)o#~txM?&Ealm3%Gx|n&SWKlD}5cNbjaxAwR%(QYK(-7 z3i#HyIZ#0el8!55f+KeA#H_gOtHOsmZ-uuy^_@xd%3@PKOZ;>O?27j*y?*NA?CfJ9 zwO=;N|LC7-1?vYDNJNfG-)hj_vmb=P0^;#0$zB~qb5-C80hUFLy0+6O^cvi~0I3o|E(=$w1B)f4 z_EDfjqwC_=eRo=S#13!)7VA%5TP@mC!%{MFM=M%eu>&h|KV5S(q2jFkHs_r7qJqBv zZI?su>j~YKBW*`T8BN&eZ7}* z70(B3Q&yIJRuz=xxUMqWY_a>fj|y8`KP-|*7f~)I9z=^;prQZ3~z;(6`fmk z#msr6xev#0W~SXSnLR0_WfHkZZ|O5N9)iELD0w7d80AtN&46cpmI ztpXa}+x2SFC|>;~`fJ(m*u6hVp0l+TCA3cn#~#2Ao1*veFu)W7CB`v81}mArU(1i^ zJ~kp{*F4#s&9;rR@_XC&BOlX^=97(rg}2@VEp?2u|JCN^4&HQ?UoPREB1s*6c7Hbf zOE)qDgMysO&RN{;^$g%KXtvln6{ra$Hka?0!X z#EhGUN8AWK+kJQ;rS}fEwkmVL*2LAT1K-ve+Y<$tyL4fn$?B3&bcbmzf0JgT$Aj|M zSH^+Qho-}$IfORf-U;kT-lUI}32+bMwEH&L|Don~)_eI*nOb4D`U@wWW@w)7IQ)Eh z^%kq)(PC2RPn(F92Nl>jfi7>m-GGXX?9{1K8ifFXwsUxiUKU=mixg;!eln+jyzBe- zeYLy9ye%%mFv@4`?HZ5A6jXP>uy$%b+{y>3hH#b@Ci83s#g`gevTpL}9$ zosK*?+51kU^#=L!v-B@Y?Kh5UWOwm=PAaT3D&6#ba^=B}xq??y%PT7vbojzrp;PJQ z<>49eB(7n!E@gCJRmo$9*hQN@)?~rHv!6FE<(E=Q8_M^Zo{VbySk6%jlbX9P=43wq z!~MI!=4n34;OEiN)YXVoBY6Ulu%tP)2vi$S2I(axD3K`{qh5OR`-hvE^C)^WJh1EO ze!NC2G0dZ%!{1xHnngJae0#cd({kOHnzQmO@YpiRYPeStwVtk?9)Y|+KW@WLI<2O* zi%7_UEihm>Z&CBFessPN!F?u-uR#Dc=S*Oyf?x%s6h5f92__a9z~t)I4>z34_mVbN z#)ax)B==rSQaOWl%G)$mwV6iNI4F&-#X>zCHG>?f;*Epbd!PZ(SF*Ygz>Fc3LjjXf9o^IAaX}h zRr$dyuNy9XIySwZChN@aTD3F!uDqTatC#x1_r_uHgvoPs2tXJ&Wn)( zq^7E3{rg)&b=)3q2I%fAa*dY{oZ+wx49xrj?wlQK%ct^k**Lq{R})TDH8x}1l%%$h zNXs^l5^0~u4+77!Trcdpos;%fW5bz_!js3Jj1;C4s8GG{YuNqk!`kKVPxzCawmOgP z6-9z(7Gs#;gC>jEpB(hD{omU7bk2_mg(ViU)a&O5?!4%k_KdOeOr;WC(GiWULQiW4 z2dib3X`YZLrKM40^JZjaRWarGk9vRQm!e(q^?(Xl$-UW)5w8;MaDSSr=kCwRUDuCo zrbh@1l;5>i9x(LhldvC%&5`3=Ry_P{lrKyP?kIm{F*^GNQxl2W=rU1~8%Fx#}V{1OSxcv8rRFs!06|Z{YKMffSNhHVB_Z{!o z{z_H!T{+j#rxta>%K6P&iLQovcI=U3H!ss*gy~peZ5Gg`E>%KO9jwnZYcJ0_f?CvIJX5N5{%i6SxVxe~(zujL( z*4fo{6~h{`%TU@+11kY|Z(J3?G_|Pn)(Cr;96)|{bePYNSx2R0qIzcfSJG-waCbY0 zw5unQQcy^1^Ylp9svoaCulS@~@*&76$j-<}`2BgVdf7oYvFS}Co+y3- z-HtkbDd{wF_YkM%!={96O8=oDGfpyGbhKK~T<-^SP6y87ED>1fvUrQMdqqR}!i6f7 z@ZP3XjA-w?fF+Pf1jLQCl$?Y`rMYN1IadLBM%8ILnf>U()?(GGv0Y;6x@&Lmqx<&l z+dcdDQE-6+s*FRNyME^1H(79L zWpFG!F67hE-;0uv9vU6rn+uDJ9BYS$n=%enUW|1V7%2Mk=d!Hr;gcs>3E$Nl9Z#`5 zmetkqR~%mm5Ybr1y#kg`+aL+t@*xzrTqp)oj)RPP{FvHdvNsR|G5rXA+4mw7!#1dv zO)w&d!F+X%T$6wS2&hA9%y0vl5fl*!DGRx}+e=Yl;V*L(iYvNEsHZ{dUHawMT`h}5 zXaA!hd&HoM5f>hlJBicwzyImV9_qeG?pae$8jZ=okBw?pHr!DdP$vp#n*h;BD(xQ8I4&+>x;E1$(? z9gh+{;fz976uH1|M~L) zUP$fp5e@afJte-;9fI4d9py3^M&?QFyS@ha$Bqn%>_@}A_1@b60%2E+JxRy}-&wXE zMj0W;7xt)&o16OpC+F-TC3IwmP6P!8*4sLUH#)wUzNq z*;Qxv(I8L=*C@wVBm|NNYsZ8p)&8hRh0^SR;*$w}X=r2>BU z6>cAPv@m(_aPiW?d%>|Dp3XACXZuGFMx7Dy?N+-q;JWtQD`}o@;D>&@P;;f#hF5oR zDlaE4a+%+q(yI%hdz{vUSwHZ#-(LHfJaFqr_p#mXFAL|H9_c+kPOg4PbZ(wj*lPN$ zyfn?jV)IBLw;wZRkPlH|cg73=8Od#B?hIHDaC}}s)d9j%S-;lg1w2Xt;SfaqtQ6aJ z%Hsp*1<6uG|DHt%_yRB*QvcxK18_W4)hN+B(K!klmC%5}CBUEJd-HT-I0!;feY|}z z)OxKU#vd~_1K#tP9^T$j-g<-r}lhYV%|ZZDDhjVVLXl zW7iL@>|_^cJN{N@kKo%MA~xTz-MwY~FiD`RKRNDFqJ6B)ias_NWZ z2A@U~S8t2)&9XH16?6z0y*l=G8&v`}^(wWbXIUGm|WTOJ?{MPfOz7P^N z_?R^#!aq%&K;2GL$|<(;+n}_b!!GIGpz0s1HJkNhOZKsVOY%;PCvrj`H?W*qI``-M z`#sT%F=tV-WL()Lc0>1$eAGR;^cJZR);$M0|nY6C*57i zP+8T^`0yzb+0j#L(4~y@cM3`wN?W5rZjR*Buvpzpi-MGK$YNYedJOq_u7k0#GMFU*)FT-ZrReauXx+grphQBv`5Asoofu^c{0Dcz_&kq zY4Y}h&>HfiS zd8?zfT^1ehlW+eydhK4&YU$m-;*^UPhd#3{{%$>`mUi=+!W^3@<;n`BUJhqmXr}HP z`RC+o_rCZXcRL|2Qj<}q)lmaGIZKeK9uG_M|(%dLxiS)S@Z@ZWP-^(LX&jY{!*!Dn@toro*++8 zl2*Fz9^mW5u16H?s6)#E+=@rNO5wYhM01gc{tc>a_v8s(s+vHL&2^n_&fT)2Po({y z^a5gPPQ7&Gc7snf#FaqW>o}1ql6- z9a#mhFYBhLNQHZOA0H$jPkb=XWl<2qjLc>ov=&VzZZ7d0^M4tl>1D~^|;_R(j0p~?GdZ?z^{qJ9LDqE6pY8yrS9cPUaj@r$C(Hk7 z0WKF>#k{*$LzAw5j3r$An8fYF^g-I@w+|W2J2y2X(BG0jObu6B#{RJiwegcqYn}8R z88Hu>+s8UR4r_f;&$dl}bp6pk68497guTAhgpz=B4+kmbgU^f0-xP)tS5wsAh%UsH z1lk@>eM4jV(Eb6wkZkzOyFR0-?rbKHH8*ln)BD#3M^h8^Wrz_StGc^wDY&*`)o0ZD zNT$halq9M`Ci3fr0UaI(j7lK`mEXmFvJ+boy_(O_Z295>7vQ0QLMcIOB%%||bDHKt zVz;f`vcc+8lU`>gx)S^)q*&&C!oeT9&kx%DyQ3NWyO+Jh4^UAxF^)V!b3||@b8Z{Z zQ4nI8{ut_mqAeY@zidI_8tFdfJmRr7y$#Vdg6R?V7%9W3=L5SSq$UxPvdE2q*KwCy z@Ze*rl2bxLHzgUHoG<~sD4yM4zcD%!KEG+VQgpK7_||H{lXb#CNr;WcLcIFESUl+f zw+PAaDI-b#JPoO{M2ITBnH8U+r~N32%NpN|RFBkWjGlZKe{;|W0q*j&QsTq`YCSg3 ziMOS?4NJ??QIK}VY*)bH*7lX?=G~CG(e?p$$Fcb;idPY2lUBYC&M6$LZw(rt#ksjU zrvL#?&vyzhFda!lLt}jX`a^V!JI)6XRDuX4!bBlne)RVP^dR)W;mA#C(6?xeqR0MP04GQ?B(ZoxGWk@P;Zx2LUUJ(%tjHY1x%b*iF`jc?i-BJ6>XtRSyg{(J6F1WNc zg5kwhnyiub$Jax2N3QpUxMp9@y*06W|Ney~Y>vr$nB*VaE6lG`iILa?^%1k<<<`DR z*Qx&dDI2p=F>xME9_uUFXV)P*5YI47)#ByXow5{p1CBERq#mBg?cQ@z#wm{B>JUPr zZ7~V@o8!JTb;crDo#()_8hL&Hq&_dCH5P)H|B_>b1_KQjipwGA!ZWgu0D4d~Tlmi1 zy?a;mhix%Zs>*L28$F6(=}slFBiJGdX{FB~cs9!zMRz^eO!sLYcALu`UOPe&+}~`zc_8#0qkP5rE(U0u!py=ua`*pM1ts0w-zh` zNM-o)!RYlLk4X#zhAl_4C;tnLxDYzL5PgUb%4gGau2Oo!jKFXIWW7bN%RRJ7_lt{* zZx`MC9?{rcXW>#0pdk?t4$Quzk5kt$&A;faOUWhY#!Ud#{#H(mCC$(8-73=$##S;< zHjj)VqY(}BgL7wv>Gn#S^$Bx42T~khQ;#7J9msARfP~3pfmvdg7?yBb5yx-#Cb(qa zr=UtHOlk1kPzoE$G>oXW)~ro zim#M@9PzwEsJSlZ>Ft}hG$^VY&XGv5XHUsSA3iD{H{2FC8N}M@v;E*b|CJqqzhT6= zzx8*J`9_ZBizxWN$hffR>c$;BzriB;#jQ8e^r3sh zd3`p*4ajM3(#S*p+P9Hkc0LwIF6IY(Dv4sR+BxofHoLYw-uK2pL!Ud-gQjRsi9d@B zz6-Vrnwngsml^pxiO*bm?0(;%YWE&?%G23GHo*?GoDm{-Z}JiVq3?pGb)Q&P#E}c^ z(+`5L>xk!O24E+SfBk@Fp?%I<0-NvygQ}@+A=4&&(RcL?RhMQpIt;c+<))t**3P+| zv1&P;*0_~@L+qc-i%;51`brHOj_-cr6TqJ z&Q`X#roSB+$E&61 z5+5edu<+_$c*c0Tz);yeQaGzC!r;%!AUFT!I)_2YqPC*0O7urM+iuG8znu5(t=vvE zrSVVaqgP`;I90dji#5wjQ|0_7wa&L`<$^pwLv}l`Ee}rawfK=CrhRjbVLXSuyfF7p z%H&8uXs8lv1;E45#_ulfC=PWqG-OR?r)?R)EurqkY4=+U-W zy3(#Xt_NLU>nRzk6|@Dc`13vrQ?@QSZ`Dt=Jd520>-wNzJnCuQ+f|B8Y6}3d6b+JC$jTVymS^G|ZcvCbZ2bl_6NG?I z0S-;QPJUWmeoH`Lpw9;&QWPY-AKNj3flx4|l#6i};UyNo3GLN(dpdvlyNPl?dV6{d zI-mIOp0QS}F(&I@T2hnJV~#lT+sitGt!|rfz))Ozbh3@8Q<;Hyi3%n5`KftLx zjW7=Q8mdq?uI3S1xX9)^_;B;n--Aq<{V&)6(R3xstK{hOu_iMt9;U4NrIpz9*XPG+ zYnJw(_oQ|iezhgb$7*raICE$Iqc=SHD53z9LURn!IAIyHw6cP!;+H6O%A@y_FiUVL zY5k=8ny&8oAJESN>8k=7OfA0Pg&e`e&yyvq4aQhWL1I7`B7)>!F$e^;2UX_)`As#- zZ2CEDv&1+>l3H94cG828;(!`B2cZkNaeqiAwzs}(b9)^ZUMWe~y?PtUW8Dk+mGu^| zKLk8GhTVI=wX|q<21%}Gqo~;mRGVM#!xo_bd)t#e+(%mu1^#$Ov@%LzS`{_G`2x|d zkdw{^vHF9Sq)L?9&S{iMd7*yyZB^bCgWpH56$mpYsC>6&Jas2Wz0|4RC}Fl%M_lfZ z81IlMWnzo@?d4wm_=Yz@BE6+9kLeC6^^Y!^8D2YR8dVd3UNgbQud!6J1&sDO2Psv- zKq-Rv@%B>wDsQ9x;JoOI?oHF6n)c4Q(9k!y&?@dKm}pg5RpjMw=|eM))zS;)w=A8U z;l|H=@?R=n6Sdx`wQAr!Z=bzwsebBN#49I648Nc1>FCgLifv}`pIfa3TX&XVRELo# z-#0qAef#!zSy^qrVm#_fj&dqQ050O<;+knY_gX28knt0~TVt-6_JRu9q$|r(R?j5E z=ZvBT>{NC-I@bTFhY^dshYT6)5HRDUBJr)geP1jfTFGv!*L{1wGzse{%R?ZzG+DYS ze^9YRex;e|23Vxaeuk%Z(5na5!CFcoBSQ{(eSBJ}G3MpQOPxl-u78Be-**T9+*;mf z5ZEwPr0h5-$}enO48EYn4+J!&GbZ6mFvaK1|QRW z_gqIvT#Ux(<=w~+34>1$*(;n65Hi0SwS0o%_a(6lN?M}NZg7h@td;bqoxi$U1ek^% zMqU^Xi71+w2Kf6L$ariR+`ayhD2t~4aJVm>xtnp1O43xNvq0;V+Nt&1HY8%$(1Ouv z<<&t`8nm?uc0iK1LYhoU{j9iR*De+0@>d&8Tm*Xw~i6->UJwbphJ+|5Ua@cYkMX=}vQ_*S&uZzR}|2i%@!9PF3BL2iuGk znR!2IlI}a>`xdDWq4pZ-nR`z<4XI8O$_n8saJPTF$}&>JKkK}hB#)~ z7x%0#HAw#L)0q$sbARrMiQ#we-vi`)dHV;nDgF)Tg}AuLW9-lrlz;kk z=DAL(!^CBcIM|PoJYT)y#(z$LR3u?#j?ok<=Iv+x%IST@{Ieq!K3(&-SG zX4fMvEiF?oR3$GapZImxNqX04z3q{YAYf*4o_P5f>P2{=zq0%ge`;q2_T~U9euD-M)PGPD3*Q5bKA8Bzs6N zcp;Fv{b+{?CWd%J3}k~A%qBbXBi{tl{~3BxTsaoB*bq2tQXa~9;)`5&e26Q?Dc>0q zdh!$YH{ziumluF^1)HNRAQu2ELZNmI8YQmICKfqCK|2!?6MYJR=TO_Top8PE=>)f> z1Wt(aF&ub_M-28#N=lD}*|s+IG#_w_ycbJI?|m!}kc2Io4geF>+i_s6=2c(Y*4%@RB~^MrrFJ>sQ9PnT92rAsr_9&aI6Utw#n6v>=8%bHw!NI(!&Nk+MWaJ| zN@cqjM0IR-Twt9aH$AxZ;}fs?Ska$m?%U>HgZ{X^Jmm0P_z+*%J=OC8nRO^v*kOvK z9rB+d0yT%xwMS~3r1n37<;qt{`6_0w7IP<0=-6w@37Yz*?Qf)N2rm0D!?|&4%{8Lj zn-_O8gt6!hONaJWH$BZSK>JM((QS_wHX%1p`cKpF%C9r)+JGSoZ|D6}B?5?%eER)( zjbwu}IVrI4w`in~-I3geLF=0=CvrF6=SJSU<)D4_O7ZEY(eBy<*W5qOZde6?mg#Gkln|lXe_>az@bsiO)B2J13ihw*7Y!n&PMh zUfzElFFkzneE0iT5yMACscIghDt5sY-p|f{p^)l-rz@+tXP~R-+NFogl>d!Z>=u}w zn_G*+TYBcq8F8AB|33G~&)rb}E{ZD!8K)`v?)PTCbWE?*345jpMeaW+arUBRB*d=7 zMc6la|F7N6SH_$S8&W9~x*sYmRLP0IaD9vMCDy^gK_M%toipa-0) zU;((3o1Ja_61Xh!m-C7LJNir|MwPtazy$@H`F~%)w&7Eav#4o$=g_uCsxOIe#Ji#t z%0uQOJADk624NyqS!}zAQ~QuyaaY$uTy0*;e)dIAM+EM zpPB#`tB8gVP{hQU#>~2orzhDLPx{it5wzlu`Dw#84fXp+nH9dhdR! z_8Zrz2-^MX@`Bj^(^Hifu-9+^i`kAz317&XcgvRSqh&cB{j>1+ZTD;Q7tgw|N>5EX zES3lG9&uJO6Rx6(ST7m=v}>a9{ESGF(86Mp0;5P)+%Y-g-T1-)y}=i050A};t!`{e zX7p;#=gqXz^a_9UZ4+7cn@O3!+{GQ7ulRrQJrQyI{`-sI zscn}8Je_84R{J_L9(8?}Q$ZY8aVPAYj2kdc;USQ+!TUtxr`*l{K1xr)Y;oV83T^BtW+6i66d{mIG;F+fRfBvn>~nrI zq5kg$4a6eJ4v}Y1OUs8Zz5K)-^6LamyLjvD>)Q}xnOr$uD_=G6(Q)ruV;@`>KGAz$ zw!gWj_Ket(bHszZ$3GoqIg%y#89k4{%m4Z z{i$;L{&uTxylv@9XJ)3N9_er#u_RR6NKcl-&=92NB7jNsy5AFr;J#kR{o$#}jw!a) z#!C9RY;iiK;%z}EbId2dTW@tUvF5?p!`1vPC?(n*cyJn2HNGl`TQBj2xdHTPw!8)d14PIdfQc4gC3e3ZYX#2J0~OmK&{pX-DXXoeMj_SVK#YGq@&p#7 zqs14t2Kc!o@qaJyQ?XO4w3$pvq)90YEp_)r0d7%)9|h^rbK_5woF~);P4gJpx6&gcGd5BAZ#{+SMO~6*9QN#QV13BY zxX+n(56M__U25ps_;;a8KpP1s36RYA1K8`|stltAYKph>3Ja-+drI6`-~vzRX?Z<}4(KC`<{NtWn<+y53?S2c0dHDD9ulbq+v*|hBoR`y zwsgI{C}X{}_!QbxE@#3$ z`&S)2fQh(-1w51`syGFEZM+m1NQ&OcA$?M-d-~tE%O!22r|+Q3efEPZ`KmM$ze>4R z#YB@SNXp0mG(@O9Y`>9`6SBS}GFYjUYjc^bb;r-G$6a=@`cD<+%9{s>3@j&Zb8OBq zRU62QIlt_B9u$SS<-WCI7n_o~ut;FTS20I6wYd0DHvTkl7O1Tm0KV?auD#*yhnFcX zE-r=NZFMd)^39u3hDr&?zXmW?=c=C2N!Ps(Hd6d+5(?(X9BYgz(eKy_xLeq=h1hhg2*nrC^CI~7lTK4462Ve@`8C5@jr_Zz@j2KE&}cxj4vF+ZVhxaAWo{GqGN=+x zFmlN&)I~w&l)=B0J$*AR{B!D(=a?6}|ZrkD6Qc zVFBCjhdW7GcP6iJmU< zxawUEq9S`{4zRDEIR0SKb7$wrbw`!CzqI(-<$Z#Gq8+|sK;Anpjsqd^oMB3w&uW^m zobz|15n=@ayBSO;*t$(%evS}+w~FJH{oA9VDs>D>LO9o0+^E5`fghEhfJsD5 zq7_t@7bLFZViTH9M79>Y_yq*e&yQk`F(&u+fGk{r6%}2fXgw$Xb_j+z`5P-lcVq}} zfqd;ZS~GZ{rEUO0^K*cX=Ul^&bD*LMtRLRt>EDUBsg66fxwu)4oqBw+#+*j(p8LH? zw^KvpQQbOcp*q49A-}*zNm@H$lG4=Mkf4=xobBSpiy94D^LK8nCWTG0H<1;0No^m# zQfRxA1clJ!x1~arPbK#PkX@SSq5(l@e~G?3FfFck7z&N|BAMO=OwW9QM@6d{&E7Z6 zv*kQ}x8!=^r;1PPiFiFzqkyS&j_*%GpC9PzKkfDcr@5orJW%z+A2n#%N}P_hT7ki{jCmIGJY~8- zo$i6f#Zgi7J^qAVIaj}V2E~?~`6k2E=V`Gu@C#87l^h0MYK|?x{l|}W4nl{` z>je1N{43+*B&7iMwS%OWizJ&rCtUTkB4R?lqcu3hCME%-^_1-4KXHA=BKYRuk}GXq zR1+RTo}3*4VXWo%PwL zt1&mUaHsQITJnVO{|ZrAS=nCfs?kktPk;I+g=0-6^USWj)@M+>q*K^fYk%uXp{wA> zXW!Or4o7|e<*rfFqb{^_4ELrfLl5)f*kCctn(zTktH~$je@mbVC{^E}v(GOq^q4(( z0WW9B&jy#C%pBA}4s0&$7mPCO#=ewHN4^lBNsgo{nm&1s0rY5ng!LogW0e1=1@L57 z=#j&#`?~Os^WP)?$l6d2>l;M~8p?uWZ_2jPF(Bsb9kGGq`M(~cTzs7`s`r>2yw zrsR9S`Wm;!^^oawOxi7p{x*Q2OmmU59bprL!A(y{dBBN={gT8pfx->o--8DaD6s9p zcN`5j`Z><5<<-ej;%7)@=#51j{^)8D4rm}M|6wKOc@-HkH_or0Lxba$fm_AUg8qPk z{VL`MUIJo+Hk;Usi;GY7mU`?ua^#3c_n(rTBx3d(u-tCoYHewIPhq~kE+-fwf&|QZ z%6b6SBT8(-P96LerhR@{9w9<-E927Atk$8IHfl@>`k%azc%7|d?8M(6%Yff*cL!7Z zIH?R`)3>%=@t7SUy;d5vOb=Pj|CqckV)uao(w*{Me}AY~mTRVnrSb1QBqGWcA%fYK z{fU(J<*SP})8b>OYoi)TosxdX4W2~l_s=Ygp|JVrL6_0}!<13;>9eCNCASxxN)Cw- zvt5xYhXdG!j~)g2M@PE!!36p^DjFJWqm?j7`tTDW!i@^Y;{hN>!`+>9OQE+?4Kjl<8s92q->hD2vrax2CzfT@Kka6I(E^+}GY z>A_@{N73K-KL^QF9{)+`sY$xRru+qYU5Uz8QUQx&oIPu7J&BN8cWjqy?(Ev)k)h8Y z6CE1r7hh~}bVYf6X^$G$L3Drn3_VhUr>~q6w$$n)U%t1!`LJ5>v4cQ0kKZ!bQI{d0 zTw483KRH%EJiYtq%hQQRxRg8szRo;Y&OMO$=1?5mwVCiN#*SJ9l+P^FGvU7Xv2$?5 zC8CG9wTS7F|LqeY3C5{v9$Sn~KHeL_PHj%hPurZ`s7~;(*pfbdNuk-ub&pVwbB|i< zH2D#NjH|HdHTn5!A!)C4rK2Rz$trU~>xzQxXYc2JtWJS%Rxr5|Z)5ED>Xp1ioA8&r z%+j2dVVIeYR$H26!`RBmE19XjO^-i69JTi9u~1H3AK5SePFCylBg)F4pIz@3cP?aD z*`#HdPZTfq9yzkm`|e2Ro;~vw8O96>^73C@J-6cr*QE{R&^VQYha;&(%kkos^Wmett`_L+)WvPo=D#vmSl_$>U&;zdOUl zYMMN)7D`!7kk~Qulq3Ubm#am}`9-B>^PiK0|5j)^(Na)kN2p@HWwZ{EzmdMaJ~g-s zfj5)7ej|&6zc%+gbg=O;IQ&v!0(JWqKM|46tW{Q$apiA03744zN({nYa?%uaF~ah{ zmhO9~R*ep>4roeq9Lv-@U_IHF&C5E&^wJ<+#X_%ok7MkSq_pSJ`UldSk9{#GPYunI zt-Si1ZyhKa8R_f3udkl9bNSDi4~Op~J7j$z&*lp97~E1+1pKXjriX3XjklUh0-hIDdTA_e;U$m1(5>HJJb~UD#A`PQ|`|*F2r=@yp7$eJaBYnaqQ)!x}TpP z4_N|Fc}>lE?G$y+GxU`_Zq;(Qvb+pT%M`Ye#=kW=;~J>7ZW!{*(kr|_ANc=>y6U*7 zx~+XsLP8MfR=Sar4yB|!q*F>jxBl#0R^PHyJ6^|`S!f;z4tqR_=9mc zwQH@to_PFPep;MZB*|3{EFuJ6&BE5fv2$=yVZ5 z#macNxT+yT)w+L7qig8{8N+c4hx{vx3@K-8YGh@}9)Q;tNjm5lA8jTVH;^H^>_36i zq2>{#-$kWTTaqpM^C>8BvbpR|9s;>Y+a*9cvOJ|gpisA@C$z@&5~|-HoFBG?7J7cg z=U8y`SUB77$qC`AIJ_8Ffm!? z*VNPiIhGWG%V==#A6?AE;q)Wm3G4mVI^I|F8wD08ZK`R>_j2*t@a*c<7x)&w?35#9 z(aLgBdVL!R_CJByDf&fFJCX9?xqs^;Q)*+=M^Vxbw*j9{IQA=$>tSpm1EMa8AW_qj z-8ps|i;D~^PI`oEfklJnBJ5(maQ4R9*D$+UaA-m~!~XsznZtcD`ENmVDsB3@!Jru= znP?vbu7G&QzPrvJRBB=XR!GwY{9sK&2p%Fg>q1>-{&kyE*-EVM%&F5YzA_u)g@x~c z7nOk!(*c%e1Y_>3;7mDWq40eV)nU|*%S7i>-88Ph(&m7OK2#0^vEC5ij6Syk4c97> zxiDHKb0>rKzVN$6QA+PGei$Qu>~5d zFA^UU3Z(5;k>%9UqP7E|9q8HjvKTM7g-{VEqL}DPWS%>1a1)`Wm0ZaD6+y!VnhA0E zIkyOn!i>P<7-ZN^oKnb}BCFz`QrrjEK^(dFAAV_Upc1OCYH3~hh!al?PK-E^p#yIc zHa`zIy^Z*Wv=V&(Vt&|N*wIlAaf66M_DH8U(#`vF{@VFq(k1!V;wvn0BZ%YPH{vi8 zD=S8knCS4Ab+zo-_CVN8JH|xz^Di*0|2d#-S*%=c2ApQf(U#zJ(AT{l$aief{&QvU zU{8|0{+8Z+_yzkr<2IK)z|a5q!@m#n5&9+en5MR_{KJXXc{~l6LH{{k+4gi~NObod zDDF7_GwA<0k?gqT;N-9$N7k=!O`B-BVrM78|K2>(puJ!iy|w1KdcpeeW`+8gu58Sw z|D4Q^Y3}yASL;!bIh9zJ|5dsSkbWL9dBq%5p`I)Zn>15-KPrgy?>oS~5uI=IBp9pH z8|i_=W}6FlT3_S+_f}o#wGGL#MYu zu3HD2>?3hX3E(R?%sZb=5EPEt`#J6!aY`ZGs6A2mgqe_gqS5!f?irZ<<<|H#e5iK# zU$WypNk%Vtn?V7GUFMipOG;jH`>*xC*>GqHujbG!d+8KyiMY98pp^c8m}<)@1yaSw z6Oq0MZ9Q5jFLA%}(h+pOw=GDZP~#lAyFdwjzpJLQ7sdE_PyvMe*ldzJ;->D5W3DbW zg!n8JB)n=mO2Q;?uMb;VB`$I`7*h$L!5mE)ecwlcLIX4vPCK}h1Qip`=8}?y9EhfA zVX!x#x`dz565X2rdmC4E zMSy0szMue6VQPyDS!Jr_hs;>qq9&feQUg`rP|)SyTw;-EU)mc->J&Qt{WDGZ6VV59 z8^RN9A*dhZx7B;S8!;wHOwKl;3GAZ|!-TglsJ%|eQkZYV{5)_Vm7=$FHNIr->F@nN zJ&4%VDs-|RW?N&-1JVfTrzA#3$uPh6xpw08^w5Cczo{SyOv;xJgFZ4+Ake;fEgST# zGall_|8~<@LybZ$H0a&_Xr{+ZUq+rzlx^=krtay*nd_66Z9d~gdvMs&gFC`JO}*UD zpNW0I93k7ClhnSU9Ou&)^YH-`P-uOG@j*t?r@N`HJkGTQ|7XktAB@{iS)vKmrP+sv zH+nt~Z{l)Vv$L}f&J;g&`7Zoyqr0fWJ2`qCH@!1_C)86L5$CrQkXpjGhd#HG`Le9k)V|Jz3&Q38pNQuRq^^)L z{kag+r&on(geiX4;@3lEqsi}1?2x!&qffKl8)Z`VvEFxupQsY$bwE!YY?ZZ$+Vid} zM{7_JzvxI&&9u@=^dhUUwP&<-B%g0W^x;=1=Qas7As}MXZfaz{q1ULQp{JKFPNigs z*#(~(|2Sz|u~Q#=$^b^wX*#9^JDy5oK&)1c6!_ghk43tje5y`j&y_(V-`pzWIp`$` zED7i}J%7L`VYIbO%85{h|7FQpxn)em1S1VFW37Fghf8gfb98LAS)Yht`y6WI8VvSY z4!3Sw9CiGef64s0V2iiM;@|(Mb|BDS1(hc|7{W2Wv=@&!$h%s=_e>b{aWoO_YwX4hiVAaL| zT<~gXE^E}SCvr7UJr+XGTe+c}3^d$W(Z702Uyc8rXX0kngsQUpC#ZNTF}) z13wPWo`zG`mKb~#_OzJvTjaX_Ao*789SQWVd!r};S(Q|zp1YYnEpWEt9{8X0mdKEn zW-REN^^VeK9`BlI0qp&$@aLz!PqpoBOBS}6Y7SdB!$8$XZVwFZ`_OFbQto*7iDS4f zky$=_7#z4xv*R6rV5?83nPkV^3&cJ5fCBX}irveQm~@m0CI5TS?wSR_h_NR6uTNFp z512+VUa{$P-2P38U&RqTGeEsaMQhq_6o0TH6Qs3)f*%%YZH5LxA<WBONUFb5ATfOg!Q~-LDZdVlYWDkyf%EO zpWgi?1g&Oe^&qd`DCQw$zR6cMAplST%@I3Z{yKii%+h4?a|(oi%A5dOy4Doq(AiKT zL2`D+1a_bIz{GWy-g=*g#;h6J>wWBf!Ge=q)_og$`^>WjmHm-lL*vMG4jwcf0VlV+JH>oqgfGvuOD< zvmtt@z2bw=(sV)AsMdwIO_$h*sSg9-6x zsUzKJ!5lG5EqT}eo-L=N2GwZ62r|p_VkH(PZALt5>eioPuMTBuj`qJDHN-K7KSCZ8 z3KjE{{8o}qg8V`yrI*TxDz!G$A2VJHhgj5&&-IpvQZ67!{W{5i|&Dd1Eq5IzILCPyeij%0!Ai`o|G%zBClwlIWL`& zt^Q7wu~ zo_Sqh@|}#e9`L*ZfeGTS-t8Vq)R(&p zt0R3wOvbfDmxn-pL#h2Fm~5z{h79WbcwZI2V&xMbd2eC#w3D3%?rkgIV1bl(pFyj{ zUi0h9)RpPG(R_w6SLTn}?+0@0nF$6$zL#fi^1iE1a8bJ?y+i?XAESN=E%w(_?J2%* zdfaJ3t`C}&XJ4X+RzmoH@Ydq`92c4C^Vn3;kRWYsG*i`^0L6Yi&Bw84oEM5t578j8yPJr}r|zj0E-?u#3|g@b)A|P-2`C zLi3u!D>%}sRlgbICL*JLtG_BeSqaVRmu!&phXQn4&@7O|)?P&=eDr>SCEoa<$!dKP zA5Jx4boWqo!B554CG4mf`1*}DwoRgAiB~+9GYunBT&8~py+V(i9Dxu&84ncBKmmJX zNjAGeZujm(A(5%5(C-wXa=Cl=15U1or&dU>hEMz+p4&c#jE71bNn?fx6w$E^Qu);m z=f)DmE0S)S`OesOi>GgG@2;}WL^Pt`Hn{GoRW7UgVLyEM5SM}=x6+~WUS|QC#l8FI z7fny?!BuWWd?J{Zi5|C(Vh3_j@VsncV@3Hod1u>e+=8$xus{R#H@XqdEtuHYEz+y%PunX{XENR$?!T7!5HtHue@-P!9fX$XjZl*QV|ARzI zI5EE|2GqU)52@zZcfSQ)3+!rDDv@4kM@!0;fyMsm!3AEu>wa-OGAgtSp9Fw_VcUYP z8+!wM+31mOxcm(LU)AP?5mQC7hJ9a3Y3Z)>jW$WC-3)*kaYv%P5qc?^pryafH_$Y+ z;-b<+Z}AvJz=L^n8!DwP_E=kE819CQ``?&hZ#Syg5(YCN{7$jN;?J~sirw>*Exv52 zqm1wO(Aa4NnK9UQm~BX1c5&}!9Tj&mh`K>~asGIR+V3LoLxBnp%k4!mT58P})}rOh z>?5k0_|djn3_^D7C$1Zn3@(SZ@P&(&Z2hI5<7HOirXHsY+msDQIt-Ia4HzG}{v^!f zvua=kP;Vur3dihsewr^;KP5-6xZtopJ^U$DI!#7wCHB(hmzeW+i@leupG8Rxyl{SQ zD2H1=_mBD#1rkxy!(GSEw*yah`Bxt<=xIZcZ|F4*1&6QnTeS^tF3){Zp37`vH+zF% z^eV0G9Ayh?di8ZyQaDR|(2okg`5Lln2&$Q;^$orKaHJGI&4pou8pP|na32JlhN*w?5!3coF*Ifjtzt=DVm0KtU5;VO zZLS}P8xtPu&$PyFtxZq|R#}=EZlCab3Z)#3Q0OPwwe6+iXc!M(hxaGz8s5SiR0*q) z{glGO!?_<)H{3H4PJB!}aSNAPtBTcPQD!uO$&_Xl z!g6wcr|@d!3ldLI(wd*1CXh;=m#DDVqvoGZZHHgrq@PldlHLQsMa>?^ot~3`edQo< zR!KF@RVX+k;nAATS1@Ev%RDDh()<3xvf?rBRu4#9slcvgP7u5^9@0T1?88VF=>ac9 zbYG2dTt58rY16Ssldx>|E+?*ikoH=SI4e5~vD}}dg)!eH5Zv-o65T4UxUC%pIfmx2 z81)`q#2>)FDM0~=dNH(@x^cEw(o%LnSWA$0m}^Tei5cz5g)4?B{7`T+xa;QW(~RnE z83B={=z%fh877#hk*NF2mD|Ob@T_9l(x@yJy>Z46ShrtWr5J*6{s_$P}$ zDZ6_orP_L&Pe63#h$-I1u-pb@;W>g@*wfT(iPifdAz~4WSfzg4FHX6U`*sE{!$vA;LJJX= zG4Wc(uM~E3erDfY6z^%X7m|9{`?lW~t8sgnMnGiE@|;R*Cf}AIqLATHLiYRH@R~~n zRc|z?$dw%GwA50kY9DBPm>MV10}FOa4IZvyKtk^#D_I-GS+rCg3DF$ONB1;3(Fo(@ zRY+;WhH`Sf$?C3C@YTk>b+}{>TkJ-g(|al^R)(u-o#yUD<*Sn3YR)9%_h^PUb9l+M z&BWRE2zTy8T{S&W85L2z?K30dK*5)9x)&b!6*k|hR;ZY*fX^aRoHN0sBYEPT%3lOO z1p9vvLENs^#PZ{xq#v`#nhyHD^2rTFL`G2;f(i_titV54cZRw<4Cb5NKe0DbE-jUE zW6^Lb3og$zQdwwtw#xNlEyL;0Xgz@DR(Sn!(O@?T1H;+$WU+s93*_?Ock0)L8Y(z) z$ZD+0$83kX;0F#M{RHp|^qFyv*gVwVM?^t|g(?00*%~?Z@Aeh> zDuUh~%q_#2<8BMmH?3@*E=Bc7!b0sSY|J;2UGL}g7R?>r^UA;ZX1MZPx4Y`yUyx!u z6QN;k&5?$lz5V(7b`Ru#%;<3sDZ^>i5gSZxw(4nc;mCA7?MxRWnjqhYQvI`@hccz8 zvgfk219jia!L1>pD7|s^gn5ppj?OV@Y$k0yCiOvlG&j4hU#h+;R(TJZLc;$VpV!+9gFWw&h{edi|<8hQoLxTFtgb`^|hTw$GzzH_>l5gB5 zXUsJ#i3@We>CFQBQ{i-q?j;|xOVI5f?pjSZs)w&MZ(b2XgKhHh9x|m?Kbd9N z3#C7S-GS4%t%JsQ80y_8q1mO&Row2u>th-L8M69oMK9Y2%=@EQh#+XBzJwWU%1$)* zqxM<^c9z-Ml66ghngunr9MSVVQ3jFP>ftn%kHrSbeg&#coRX@NSM-Uh*(JQ}8A9*p z80@Xzmg^@j?v_4DJMhAaG2{Dg_1Av2qCXUr46-!*PMJ`H!za!Do~=B%IU>2kg1wjU zu0?{h-ip#HCVsT-T+d+P>brE{`M^F4Vo8vqmFPSp;by=>`u4fs>8M9NJ)Nd3!hJpy zwDcn>ToAFm1Uc$ZxwDxER8kstc6tCKlv)}DX_zLNW7gZpEoALusxi?5EPa*)DmCp^P6MB<>le2VH!32wAiO>o0$CL=pdac^m ziVxE5_#Ef3tOub!sLVG*Fq(Z!P;vRVoXpXhClnmvOM8_F8UI4b!58idMk$I3#L z`d!TzfLL+HhwiN$F|gI2WfWHV&QcAJ7?J@S;6o0kAhv#le~uLjx1XqAT`CAW1|{AD z{EY66OAF}LsTYLUxhMI{TVC~JZl-aPP76-`v;?t>*OtriG~)>|u@&njQIq0x%wAeJ z#ThUD>q;9#(s(~#kRCsw7bJa?!nGrW4=(GHt=;6s4VE7AFT;NP=|P|lB0t5-il<=A zI75quN8HATho?^e3_Rg&mBn1v6{xOlQol!f_rD6kmW~sqJAKIiml!P7)ato816!IE z9L&`5)D7Gf5Vr~rlHRi$LwS&C#JiTjr3Mxa_qGY~r8qUZO=Ll(MZMVJKQ)gY@oPir z2w6o_u&*y_?`o;vx|*`R)9d`dOVafwxkl`RQU8e$9V;MM1kpy|U<`XHiWYGr^nBQJ zssGnI#1iq(VQsfuFyCFAP%W3(ksh}6zw(RR=$O1^pm+UPEuPe2M62ol7iPkf8N)Z1 zbS>YwYbTRmDi}lmD@v#eKQA{}WJyNvNRbf#-AyKpf0;W=@5@IyY$>c$3N*UEAreGh zi1e0q9QkXFvZp2jknKx&9X zpu7L7*wO`QN1qi+`cWU1W}txI$?!+OZR>OEkf)RRhWA&*r)=Q8**QfU2sys14)%Q0 zxyF(pV#uPX3PmiD7}K|C&RaZB&h{hDneUA*#EYi+QxW;!xDf2wB~~b zF@5~u7V@XJUe$2zaGzB-nt@My^9ugYYezYC=2oPCk(lhUoMj!kUNO_ zjk^=;GR^utqA#Dyg*{`WAg(bdE4#?=9SYtW9&5tEy{0q6HyY3_<<{Dh9q-^c!Q46@ zc zLw3nVZXp<{u=eql5to4buj*6p6N(V6@qMAFVt1--p-8Ns4c%foRTJvPwJ%_Ss;zkP zs5jw37-g5(FKmUPjfUMHkQg2>IltTaq3#=+nOHI=xKcl1=NtOM>(kF%G`xwN8j^_T z6Apu;r$)ZrNT61`74YN%u+PJIjQq5?)l7Dvql0FV>_C6PM1>g_>7#DhJO4Xtm5|0K z)O)^I=zpIPi6(e$?$Jqz z$`1nY(hD&^OMZu7!&B7TBW%vv?qSkaV?ew=+OYuqf&{uA4 z)v}cZD`E3Im%gE1(P7TxI*PbjOalE1tM1rpkExs=As;M}AbEGnpOb6*!)G`d5{?~u zj@@sQ^ZEMq+gSFz$;bnjFrS;br45O=Q;K%4wAa<&@jQ@Nw?l@SZx7H7-ETub6XRwl z+jl#~;Ms7Jw|d~+SeC9BkW%0=*PHIOjts;5>>^FawsM7HepKANDvU~$cZUKP!+-08 zT)h=uJ*H-4K+pO!p{B`XeOC4IYx$(>J<#ehHHLUOX3hK8%7Sl}ZEzA{7vtGP!`rzd z{)R1gLAJLskML+rcb(iz=G=9s-7izA=6$XsHyq~8lJo0&G;+8nV2Jm?|L(R?&i@Ne z2HH1bI$rnQ7f}2A;|bhDn#Z7zaC+=Z>$kym*rExVl#CB%qv|)E#driQTv<;lWc3RAHY}DFyBeZ z--S`9-b3~H?bKTZMef(;{b>SiK%5lRx#EdQa4GR__vHKPI5M)r@oR6AfX_*gzq5LX z8W2?|4mz{!Nk9j#XbZ7j8NDZOwi`trIe0vwmku(aMHAym{$q z4R7f%(u!AgN6@?-#K`WWWOyjm(i05@tWh$2_wF~c+FPcjSUGOX-<)b@h-RhyU}h+4 z#p97Ty3w@AF}woWIks3y8EbElztm8$bJ)2@?j)`9v4jv$R|DDY=LScrUu2XXWY;9~ zen=2BVC(m3tujzv3N6xLKU&}mj`7`fEWiH_B^X}A-VKvCjEI+yH{V*YqJaUuJ6~pF zopmIJXP}zM8S!Bm)sw_e>~}P&??8;A@@1~Y-z-Yy&TW?nu~Ms$VzuXZ(d1=ow*~KvkSX+{j_dZH@~^UV(D=0yEAq^Ser0cstK-t!L``F(m<<3NkIm$M`RpP~upnL^3mo>Cc0 z%`&a4vBZe{x;!Zb$K35s(_p!=kkNvPYWE4zpQ~WUvHsnLqG>3jzi}Q^3AKZgy5sRM z=k?O+M%4pW$kWxO8U`%Iqp8d2z5rt|B>y(%>K1j>y*`>%o8FB%UnX6bdz|6++B zcJEy&8$IDjpo+h&?^sFT(~8-nsYwSzC#!rf=>7Wu`mvssgHFA3?>}x^A|hT>GHV^g z5T&p;ogLXkHUSpP4fzsYlUzdC8Ci_H=uqVI5P9c(!4us{e zPcc5%_uNM#jJyYFFuvBI1HsvP3U{XOO%W7euV|ZfwMsDDx^*)nAG7z?X=iav#pztk z^WykwqKY6g9Us=4&}v`r1;;(odHrhP&|CT5;g&pc-~gkv8t*&y`o$5ucjM*CQ|^>suPkiuF4QmYUS9~g zuD4?s=oc~SUh`_Ni4+d|lDFzlVjcITJFcI`Bao_B+t?Milxr@ZuDXm*^=jJbb^NcratOU z7hLMYixEegPO;%TxRfqdpdvd9V!{yM!Ah!Wq*=XwW8NEwK48?=Z3@pWP%9#~f194W zdH2)ut*8%W9i_15iI4|sUz(6yMn0Gfmd~OE7CEzgygO;=_{{YM&j=U?W35 zk4?yzd&fo~2)jjnT2SzUS8{L_U7(I|jvpUetY~=NtY= z6?c!cj+)m)@#gCeuTMjB(3E8#A88)LugzXDLZ>j#h4IszZrPUX+PhiTYI07#Fx_;JT)|R3rA@q|bR;!mslxO@o?+ncQ&v zPy4MCWg2o88ArlK9UZ(%FEU37ToqL@E5`i!lD@Wjp$UDN{hchs`KkfL!lP%7i+W5? zQ|5#*w*d_dscOPxC*cJIy?~Eu}Y0<3n=99_GV zja4p}9CxQV(LyUNVDqQ4BirqQ@}{5=Yc(C&snoVM4+A?3s9R{|8*`#ot47 z1)C}m3dX8(=}0UkP~s510upM5r<+47b^0~5ObPDmEpZ(dzeCZelrP?;XT!$kE|@O{NVq^=-*!F~8X?jb8252R+Wj z6{A+o9-u8_mM-jC$%#=0?r+B}J{Me!rTr<(w9-?RN)%fA{H=CtMZ%(#@xqd2<~|`uwexi2MGmB_QIl7Ql3dKQs$e7Te(=BY_dCPIB*>j+K0>lGguu z^ihX&##kkjNs5A^##fd*kFe;l_nGhQw|Ql{e=S~*v*!2rQZ?vQ|jzPb(n z_9?5%BV=b){5JY|!L_)XiLxgTP}SEVTl?F@aBC*cDE{n!Q@21xv*#=m0iw`XD^>Pxemr7s#ZO*|j06R{sL z*LU3fvUpgrxmT=IfnWUx1=||zS%ebV;KhC1VuD@UI=KyiLYx4Zh11oj*&z za)Sz@2A;O}H?MN^MvU~h%(NB?sFUItF~-})11e&lrg>k})J(aZP=nwmXH)n+4F6A2 z78_cA0o8E2u+}zT0n~Y-YM@5{Ow<>B#JWvV4mi7kViN0LtyG0^S!PW~#}+c;D)ld7 zJ!iK~NT48XI=nj;R;w5m?H?VSyr8I2J%D1ivbvC-bz%wZInIEIS4$(H?4DvIlB88G zrmD^MdCzWS^cT_ZAirpzh)9jNH$XQ1+D^}wC?9!%)AAidAIYq&nSTrp>sWod%gW|5 zC!+~N{8#Tj(!9#M_CVDFMh;dHe^(z~M8c^@=)vYi60glN)SH*ypQ80IjwF|Zh}syw#WLI~;(Qw@VWPA)qIFV} z&OCs3^|kwJ8y2JVv=4l`tOCZSj#^~5Gm%FLLbd!#wljh8C|HE+%2koDb~p@PqGhP@N=T;P~KWBN(S~=PBnr(8mGO*4FO4Z`P0Y-f_6v_5t?XmK8DueVy9cI4t zZUYyXXDAlTH+vak7|~L8xf>SYM88@+!|8mgEdB%yV(LA!9r4phAd;@1aN7;zy<~S| zO%Rqk+6Z=4>Vz4;c=V}T&J`n6kzL+e!=QCmU@+^A)MtVyzAHm1n6*o*9_Xj9 z>++IMxJ-~54aO+@9rp*{h{Ha3BE*_;%8j^BA=1RyIKW|nX0guMukvo8rY{Kv1g)Pq z>Ig(ba{eefT`Hzn^h8Fq!iLRrWuIo&6!JRm&B8izrdW!ZGT?i&4~PcHX98}KGKwu&pn8WaLR)%@+j;M^yzg@`7Vs!J-totWWJU`LX6a&S?6<|V5gw>7GASANrSjvKMKQ|_P!!j6UPjHRjWXnl0d990 z(3z`B538g7XFn#Z`i-sj=-ck6Fy%r|?9zDRomu5(+qp7J&b4CzDhDW7k!Bc5akYAM zeZ7+`FXtiC`Ta|mw*l5>n#s^ig(J!Sc>&?0G$9M*#_FQzmb;<_eb0S+vC(?FM)?lSs6falRfCL_A5 zN<}ooT&+7@I?nO8Lbv@5(!Pm*+;@@&qU(HMQ$Jh9S$(ySsJ$) zG-rHesrg(KQtWufiQIf|Sf?Wq1tsghal@_QoqZM7r#>fCqDb28>0KZF_KZ!eQNZ>s z(C`J;O^;t!UZ>qnG%R?O===@LXiEj2by!6PO=niV-C*Asact{v^VWTBFz<~Y-4q(i zB3pUG>OWaA6QJ){C)NxLZGN7L*@w@z9{|`1xf>ykontH^0f(d}wou^P|bs?zh%XNFEi-iGj^`LY6f=xv=jGM6O ztW0`F>+fUl!i4vT4Yj%_qn(kN;T{H)WQ0gd{lRf_Z5a`slV1-ZNFvl_&TUY*vw$V=Ayq5DXZq@B%Ua5 zogDh22d91$WV`!CA$Dg@rMxu)sCDB40s_3_2!;aU$SGSGJfv%c-|W~dv(*qeJkIt> zB(=x0EG;8gxYa#frkXlnJ`m7tbX7k11%OFlX~)7EY_`+(SinrWy+FIE^OD0LDyyp~ zPsv(g@||w;zcXpjH`H2i{1@CF+Zcw6F* zhHck+tF-|1&3eiNLvDHx4#bXEAe;>=*?lMd9l$6J|I6je{cl_7qTH_U( z7ZI^IGiOFG^9lWpXAkxnmsAMh`?Hb&1_sJWTGku-*^~e~%DeNtvq3#nr9$pu)XhUu z0TkOLh0ypG6y16^iFRKx{bNDQyC=9QT$V2kzg&a5Bm%idG(ag)_1xf@?~*i+^&EOJ zq4Lj+bkZ!3uwi0RCFh}`)Kfi^yC1GrLy_E%{CI%{i!alIzy`)}qxcyabIJ?L1p7FN zWJlF)Zwg_NDVV2AE0+nQ0YfqSO6DkB)WclTDTV-3KqkwD&di`oVoSe*G~k2&f~tyc zr90B@?g<4-Zf@LZz^o13^+(3^*WYcDZTBwO=D&(o_`j0_#!#jS%&<|Xjm%=*n{^Jo z3T6a)^=iI7Vk2g0Q3vV54zsv=OU#EFAktsw_3?X}^U)8y{*cyiEO!O)*uam5S_JM* z5|unawNeIqSiLih<0iG6>ziNRhA5TRyWVgSmd!m>R|lL^gQewzqq~o`$pb}^&`^%< z3MDjeb9}^jpA&c(0No@Blo!tkr8t=dd_ST}b3W_O*%@R^l#&YL{3D{p^9=Xm>)o43 zg?9KgpP6|N{My~jybJMfA3TjQTc^?S?n%B)jhJDJhnJJnm5RJ+$*hN!yP8m}kXFEK zMPQdNXp>mV3ENp|ySo{_7oU>Z%PZO1>`ZFRKdMpg_*hN$DcUtjxfM%-+7s|f2pA)) zT^XRHz_XJlkq7K)V2MS9)c$O!>)#%fu>O2sSmaNGbUL5mhKM%9hDTy5v8L_lO*gaU zKC(9J5FJ=`01~u7x5`g9{V}vS?JJr3m4ikIP}DGpZc|je?rvL7>VeOMPMfH2>AO@K z8SMK&+3DSXC!wtwP!@cR&cUfk^jwrK;>tN@-P|kS1Day0NkO`OHsP3bGU=i(o}qaIYpDjLhw%~&lK8n z9ws`@-S9u@N7JiUb|Fi3`-1ulPhSCp@d%*PTws^MWr1NcUXpT#M7Ict`-6F##jU{F zx4&a6e#D2{Bd@3;#=d)CJJZjj_4e7Ww$7H2TK=fgGg^}K!|u4|Me34KJ*=Ua{)R0 zLOY`x8v@`wl~t*TFNI*?gZ#PxI3UlrYPKj*@!7YQeIEHQ7JxjQkS!eW=R5xXedv41 z*!m2?>IZg82;&AIZ;db?9p(he!w6FtjZ5p%g4iw{G9IgMwRXVW>w8TNLRq}V`-gST zo4=_IKu_RO<3BT^>U4we2w4`R=|-$A^Nf2&Jqq-|V==$-27u>AS#3Y~rE6O9Vcm@o z07z)gMr^FuV5y>_21h(mF`=TS5Uk#0P8DF^09FYZdUliXqTYRj-pAg<Yb23+$eKmHhgR;>dllqV1F z0k%7XU6bcAJ^0nwJV)2N229!rGjm{o9u9hL*4wF!YzNR!!hrPo3NQmKP|L*tf|1cc z&g~uqydlGZhQiP8ZZsg*RPTHBe0Pt4r48)QVFHC0H8CK2+qpMW$J^z#(NB@M0c<&c z%r&_Ky@m*&Mf6QF>@m}o$)?Vi=XMy;lp>d{{}~5a)fM(rtN?t>Fn{CiRuWGMTqz!4 z=Ea0uEwOkUog>Mu-3bUR{aP~cxK?olm6wbA78t3fzZXUv&!nG3@4CzM3P!8x0r0zy z4kLD2Lxh8gQH9mGPPNU%{pS~NW5M>$wA=QF4k>w?$b7%VmpY;j)U!B7KdMp45G`9` z#3?iF55RYTM`64>O81OML!Zja=-N}lZ)+AEII*B(r?7zmZ`EzU zYDK5g9KF(V02_EZ`&?vw{sNSXbrWNWfp^kN5c<@FHvt9y&tm?fi0=TBd{9u36+Ccc z5SR$=UY+i^vmGY=GZQBaiTIvC!Qob|4XHHRl+B)}YF>sppUL+_%iDFeAi!BbP;OQn zfy0b=HsPLXtto;)w=~$`{h+?Q{zoynNd-W>=&bcc6&_ERZElhv*8QncCu++wz%ePs zt~BSJm~>x^$gf|8Q#Kq=A`mM&JARKb^X^KA*VnV3A-GkFx`pMH4S*!4?|DE%PoMYw zk7dM*?7S?adFc z^)D*@__OTr=x78+>u?|5u=gco=vO(g`~ip?aC`*+6GJ7YQmn-mWhWCwicp8H3Cu`J zO8U%eO9(9j=R=$Bsp)CCn1@(?{V3c(vOr)RsIMVpcC{Rd7*)j0GPeE(q2n<(HZ-J< z{wFd_Xspy*Zy)K#;n~N$J;1KKb{jqhXur{ED`pMPzdNka`|jFI_(d+aqb8qIEQXnD zC%a*j^;mv&W3U=Q?egN;pzAX&b`^wHZIN?j()8Hz`jZ1nL_3kE)eNeI~`g zXEj~-JC_UWZ6Jm|y6eb0kq{Gqv!$9Vxg7%IUV(f{kHObEt}ej0=rmme)ZRP5a`+C| ztd!_fyfhie8$bKJ_Tf(eYOt4VAD7Q0%thDWomP( ziUF{e^IiKeiP#e;y}j~cUKXOi20j#eL`2DB|INqLLXUJ$+U>`r9;3qFqC<>21)r<; zP7ZH|S7KlU5uhMZr?yiCP-K(M(#KY(T6^9_c1_+_g3uFhHx{6#EN9oIb{H4lNNQpv zk!!aS;u^yj`#58=oQ8>Y`PasHZg~$^b?R)}4vmOmR`&AO3W22Jx2^p*=FKdMq(D@g zAi}{?{|D7P9Y62N-j4XzAwc?!orU^fD?O7S01FfVc1?$TXeT=45YF5?NRiXDwe@Ws z1zt(dA@MwgLma6}-~bX8^>Dh@uK0O+4p3M@cr}CIC&#}hmH<)#_0P#kATJvB81XuU zg*DLVfT|oYzd!>*k^$qKG2{Yj+f_i{&BixR2I#TC)WLW;b?W$MjyaK^^jszF!rsCF z6M63P6rkVnJ1;}mhDC-BwXo+9;Gv=ROR2Od7-ErgzBcn;u`EN7oe=DiayM5*y2W$# z#GR6CmBkoA7Bt8Y1c|}<)f_V9vMWFg2(pskqjw9{`eqBhs4-ID3WGU~B0WTZ0%@+? z7MIiAb=$@PBF^~TKvIE01teHkf(i(G0ODUGSSwM7Dn`)~W;N&*DG zZZKEVm8Y0aJH-M)=Bh3uu8{jqCc^hWk_UbvDh5M7IS~#_QXA-O>!B zKL9<6Gs{Q_P`rR1nggIh1Oq0l$@xMLVDdsBhL@D|ktu(l&H1)Zp_2^0@F)omGt5Tt z{1OU3ZYWbO+gd%?M`gyyd#fu#1)dAR8TbSW9)Le3v2!NEX)!OzKc%;5yEza`3$*b< zv=eIX-qx19gZ{U;7@lrB`@is7=r1rnP zw(%{~sknE%{#SmO2R|2R02YjP|9$|hGVeAToHufUGem@i=NlRu*M}_?5?p|<1F!B>C(kL`7+4;=qiXOPg7`~kTORwk{ zIM2%`GUx7mVoXF2RFTB*H2-_yd$by+Q}F4M++A(~`NelaDb?fiJIQ8mxKdv_&R0Eu+!V>S-v}|<7-0k+90wY(<9!F% z*-? zm~F(4>mVk;KZ?H(lr{1=nTm%JahU^5nRIQxT-zKX3L~lkV&PNyw|C_r!EI}^8s|^6 zFu=kfV2Ry)6$!X?OWPkFr8SUz1pQ-4V+>`icU9Ax+hkl01mwie-{egLa_F@2PHO~W zae!%VG~gc5bvq?$e*Kb~`oqvr+U(&AGc&v>aHcRoK>7xFXMdE8p>XOie`Jjz z6?tPwobyVJbOn@xULmL~CnUmUl5js7jiL-vFv+}Ta2fntOoRlz{#zwGlgBap5nyYJ zs%@91*?z>g`NK&WuwflxKbsbHTDiconSAzot?wbY9whEJ(}8|gT^pAGRs0z0I5w|6@{b>uz%iF zxi2`1?D>}S6OCmPLDCi;vFx-(Plef>%djT*N_vL6zP;*shLCD zlsR{CbJ((wJp!Nkv`scY(48F$6UcBBWf+BD31kdhm417&2`eXe1bs08Uipfvg)Wus~3Wg&l4V(d+_SB7B*PW1TvCijJSh3#hLU`l{Eid(#9U>saK+ z260#?(9-e$_~d#mIJ}6I+Em~zyk5t{LXAGDWNNO$j3{&j`u2pO{L|fvCzMp`7tg=p zdV9`iRJ{PBX=`{0J);lpi!5h)Q=;qMK81Icdj^~u7#!L6HJ6l+;^Nf8d7mF01Apnu z-uCu(_ex4Mri>!Z;t*i@OvBBMhk=2C&;kJ{zY9Bk3npG(0vj8fVCg9Khmo9Mj%**V zy+)R#2a;nSbAY^@3D7mYcGS`Nj3p!S>d)l&D;F4cy0=~I{rY<&@2#}Qfu$KX#a}Ss zqMPdOKbx_8gzY`gdelkESM$<29^;kVB^GoJa5W&aG|H)sO4aQ+?tAKUdxBJMW|XBZ zrVX_p-6`ICqPu;(kx+j3A~7LhXsVGA7?*H5cfcN;oHB#`T{--fJ?%P>KW{`S+OU_cBmn1LAza9sb@-U_PXyf2IYbO*17z<#IUKBY^-GD6@tDn+Dm0&dyE~AOY*+ zwF*2GcIR8jAYh<|4YYB;znYk=ccclyrU)M#RQYiMCVyk_Ma0aYrWO?6@P#iT%QA+P z7X#tKh44^87@fa-U*U`}83$K7TEU-bHAt8`2azQ)5Y_^F>a^1!_oM^~y@d4tk@es4 zT=wr9IDR2zg=Ch!X&?<{WrjjaQMQy4Ldss*WJIWx$f%G=Lb9@=NGY;Il$DXazQ=Wc z-rvXf_jvsNxbM3=FR$12x~}K*d>-d<9Orq~z8TgOBOAAISTS5`O2U2Gwk@@&cm8iL zM|SnU>7}I~mLC&kR@@xDWE>nE%09OKvprU|u;5zX*f?X2x$nOk(Ub2MnZ4H31WGl( zy;Fj&ah^urGJh~LWcn990K63Ea-77eg+suCEO9k;!{vKSPMwO)od<;R9NluVV~?!t zwejiV4^H>E?kx^0tm-dhaqpG3rEDy@pQvmnSL&eg#PQUQvQmp!b{4yjPJvupnz=!}V>;p_JzqRqopi zzkYsh;r^7>qQ4RBP*Bf?n(7cD^$-HpUP+5FID1bKk<=J`$~EeFx+&T)6NUfYGxX zE6;C0?izP+TW;X_$+`%x_X&!a&l5Gn%Ec90P_Vabr+@&(*<5d3b!PkpC_lV9EiKI) zB#@S#-bSL}>JJuocX!Y88X6kpEVO=JxyW1h?{C9Q((1O?FJJb>a+P{5vkPfP^fdzU zKh6|WJeG={wtM&OTP!JG6)o0Dj?S-e0SsA)uhZeevQ&^N63|Qoz52^||+!JPeQ#eQn<}gYJkm z$j=v?BV@???lRK4rC>3BT(~&5lJh+*&o3d@5*5A_RLnd)w;w;|S8)GFgFztC3#P8_ z?&P>CmerGC-^`fO4OeH6;}807T6qneYI=H%d__)Z<;W>x8DKsuV#R5z-g z_xB4w6ING~#gaayr-iaTc+<_RkKu-j!dH#GQ% zh0&EO4j^l!)}!IvzMVeb#o*b=K)!J7vM^cg?c27|A$Wx@H|TqJo(cIJs`j?e=PspR zd=Q06PDdEv*Wro9CzGAj`}lt~m#?pdqdWib`JVKn9B8inis8t=fza^q#)8+c)4kNw zj)Uf@dMJOhtk^{5{Bhsh%eT>{b1^q5i5pYEo*NE)j0z6k^yTZF5{G>ewXK`J=d>KXo zDs1gDX-eovCgc9k#@i#v&rii~7GsJmEG#Pd%H3u(rQ?_9%e`XmqjzX_cDC?D)?=9! zEpOY4zD+q-68`M_#K76a-5D;l{3ia7ix0Kh(%R6qzdgIwyM_lRRy{ltvNxXQ4Y_rD z?HFLC()N2c1ySj3KzJ_c@7uQzJw}hE znoKNg{eljVxu0!8%*^Csh8iVt{Nzc=0uxgm9i1Y#Q>+m`nwyz$gvUO8<+!Honyx~@OSWKXJswx#tJerGNKpr4ks_%jcIAb_7N zqkqSyyljl0OwLEw$n?)Q4cK=V-4Oljb}(|FLFtJ3L8tNlH<{tPcHFyn4+Cl~{gjh1 z7DU*8%m9xK4M(>o9X)oe8jW+)y@?Nau4tk*IrM4!T1h|Cv1DZ*%_}2q?YW>f{GWbQ zzW?##F|sfJ-2plGv7=$?h!%in!8tj*Ft-DfTsL7{37URW5tf#g*U+jLZ8IuvXiipE)|ZwwbYwqWNlCNe?_~Ty!+y)^kS#VC&L(-9d3-r8Az>5$ zQlFHmYKil(US&g27x`OPf9#3ikw*z%VUBooK3E7&D~!52gt>O1=-$aMAdpfagSkzq zX64?T@;9>8Z?HQ`pE`T?Y_{pUb5|NhS3=%=`8_gXi4pPU5h*vu^S%7>6a%!J*Vnu- zY&W@g&B9tl$Nq6@D&?+SyVh3F-kfgi$co1yOn$|}!5msFHqHa~N!5QR0V|D1QxkxUaw8+RxQjsmqB}XT+w`4#Wqo~!va&;u!V2c|t>od}@7%SEoXQ5wfiB2* zrEse7JU^QoiB`%fE~DQugR2sUf;3n%A~i<>DgXu9{D14$4Bhuw=-%}P+qmme(A@9t z!N?uSK7Z1>wjFd5mWzCkQ=}zRpRc;EZsz=+>dG8kNeGh6ekecwcaH<420eVpgFZo- zdYED`)M#4f8H$iDuww^3#>}x)_ZYr&-)CCxjar*4<{$2e_}|^Et*wm-YCMsA9J*B- zH*Tbhzdtm$w(23iW5*L93V$W+h)Ilnv9Yy%f`!v3r#C6AeeM_&hR`J`xUoQ8iZ8ix zYe25Rwv7>6hN{ldzg_XsqenJq^Y5boM!s_UE5y_6bFb-8mxCY6-5BEp*C93gh{QBs z`0dy%o@m})$l>Fz`w7vFP-miba&lUjDHfxco}0mykW+ap+U7Uo=O9SQnGF2)Z2U_+ zJ6~W1jg&Zgu|6_az;Tk`E+ru$u|magxZsN=&d`-9fKuw!F5vCnsqj~?1_uWZtEoLv zFMJ%I@j+_$lTk?@j^zSL(RO2`Pt$bqOo!D+} zpBA8WLw6y6NR+M5>!+~KnDf-$`wv2RgI?PUH&{sfEYTP~xFR%rlumh$kv-_QT_;Vf zrd-v>KHF<;I2x*Iv-A6wa@OQ~3hlSBf8q)r8@r0v59Hnxd9|y<&L&y0tyE$CW3Dj; zvHOwY^|C;hw`(F!HGId)l@u>1ZlJ~FlOxxzN%!~nFF501!?fE2O-YL-_eDiTgM1{j za&x1wno^MF`IiX_387rHW`3W?+||kr)xgW-|7Pwzrmd|KNXK@(*vT3wk_LmEJiGt> zyKY=^ot9Y8j^xvZwl(67DW3l_H-FI2@>yT;IX3i$O zx1$g+HSvJMqiQ!77nfuA_l7F>phlQn@~gFk<=;3v%Mg3yU(b*XH}@5)VhWGZ~c zjh%^SYrek@m$q0^KUS>If9}!y_oF>E@{s85 zYt>68)MBxmikEr+FuZ$F-aA*tTAMqSIabZ-@r z*V74?9nh70CC$Gb*o~U+oNsPp1I6TJ?ip|XAJ`L;7#t{aG@hoZ+;xib+_`fEe^I>a zhmyV9wr#@%g(~Is6)IWTR}OC_xG>_-b`_S}%)mC`!8(@h+Z&bH$RXSHQCR>y?fDub zBW}|VtG9Am=9+4zxNrWTPQ#~MN=1xymIWziUOG;7_FdC7uAbW`!A9)Ut(R{@+@`qM zb{N!Vt}k`JvfJlA$0K}V{`Z<44nwaQy2?d3jghaZAGcK~CTn zz$`tg+<;u9qQ3t31LXx2sc|AjQOVSM_!udJ*8J&5vq(K|y1KeP&aP9@LXqF|gXH8S zZw4%4PT7kQ*}+ps>!lP-8+@i@k8M9N+ix^_cbH@@TP#$96KnHQJsMB_B=>Q2K9eKgH%CcLcg3t8hr)}{gD(xnFuYBo$yCfd= z(#L_l$VD~2+T%B1S`QT?*0ZpY8Y^CG8TbfWr?kTP5$i2DaPK2e$$U86wra1~Gu+&nx}7&gRx z&}jp);T~(l6Kt7X#onujL~KdIiv(CUHoKR)+}<~DN81S$zI;6p@cP;QM~ab$TW9JP zdLv?76PV2gm2$2gs_<&mN%PkBTOMNb-zl8+hF8VYnBmOs#4vszz5@HSTVhdF=5xJt z48!*EAyMI#SZ30t8=`MJ029S?tElJtR-Rp~QzFP#VWRKIig%h{w((SCki)P1tt z@tbFo@}J60v~0Ha-!|fOp1W-;=X}*6rd;3hQ6}R%<5w3hu2$K4*XUbby*$?+KWv|p_ofyjuZ|Fd@(YRYrhR(S3fXfXXT_$Jxcocm-tecgO?Tv;0RPyom0Xi*?gM8TwjbE~hR;XWOaA)D>mOHacYWXeVDp0;53lvi z3vh4p+Ai7$rSeiPpJXG9`$Oc-(TK` zN_~}m>L=lR)*Nu>>GOnj9iPkoE4if)WV|1r__;68a6*nQLS{C^aJR%wYFVn**mo(&Nvb)YYv|% z)x_42bN{)2x0#%u#FHfEHJyTN>!!5I$+u1v6c!E?gkx^1#YJ(3q(xaYzNg2I4gfFu z!CichjQ7gIjF|qToqPF@nPS}>#dt5tj(nk$_Q>kyNf3HL>9ZiJsCCA=ieG8|+^6kk z3Eg))50BStIlEi_{(I1_yU|Sj(n#DjH?^Wk6j@7x)&!$w|IBAL@ll)Fw*O0N@> zpI=8WiG?VoC0;1 zn^vCto*6%+-C8{RTNcQ8dNZ%!qY9#;**8=XEli`&4gC&8f4j?8OICkMyjB*|N9XAJ z-95v$Owcdg`#m~3fC72LQrEofV8N}YGL99WNTU63=pVBx9f{eEnEY;H8o6{Woi1@$ z%WdY+g!M&7F==0gSWYc8uGQ*&eGIvo3B%7MCaguu2ce6^AK2BhGb$iC-{eTs2)E`rc zh@`WOM#%&{efkvtMR1jHfR#81R8-Xtoh?vjB@Ld(O@^d3*4J0O4`z{HrM_xsH#PP{ z)^lMpE`T_IBO)Q}WK=HtbFO^~+}m4{n>L`hl?V1T5ZXv)zjpm^DJR z<4m@AsU^j>y*9+9OT$$=*iT^k@NqIV8_ngnzE}7F7ro_GK|wY0yzp<|(c-iCO@BfL z(m8OX1)tq9(tS5a<($~DL|+)s8r?b5k{UmLJlweP?&;zcr!MxGYD|_io;`G+Wu~?A zY;T9rJ}))I@e00mhsFI}lQZnCi<4{PZ*GK38mK!w7n-1^Z_0Z5)Y6>EcFnb4dJ|C* zLifQHYm3$2S-h@jHw5GJ-x(HNv!F7Hx)tqE>DYBM^9iz9UPf-YSJ}tjUOuFC9IbC&z9fjT-f}+B+|>LH=ZefL zDOKJfr%-y3f@~K@F9QME%@4D1;^N{ymw9vL5FnBIiZ#9T> zzkP=J`CzrW+TPYnW(iwtuBMUM0QIXnng;*mbpDkdo-&7nZf-(&Yi=LoXHZFfeSH-P;M5tBo2`Te!FsFG z*|mcQ`1f&I+6HTDYw6l)6U_(9k2E7V{eHZX|p8tNgK=hoRp0?>M+_h)V9(a$b z@Ws4;X(79Xj1`5E3zner(V97mZvbN%gR-)AKuo)apargr@6e$`+b6M{8)ub=@uz~(0s^xp!=xS?-?86HlLF)V>b zuP#y$@jH$D5j|m*zMX8i`0vkF(|0Vp3vri*tsJ^aT<9@o(?2w-i;3MSm%sb9EeBY1 z3L+yTgY;fwXxB84)Z*)dF0Tqar}{N6XwN46)nOpONP5~1Rx~-&JwMfxE-bd%`scKd z0lHw^L&S*N78@6e_)p2Kdzd(kNo|vuAxlYM=z7pS*-L`L!lWSh6hU;o(FaQ$vqsqe z-Su$Mzz8OL77w$!+uMWPW`?Hf@TFEZXnOB9Ee(N?YxS$)?uz5Y6r7TRIPSFOyI+{d zd(-z|Ux_q0bQM*W_aOeoEPafEjPU$S4j9+!svTQ_Lq!G$Kg9m-c%9Xk1SRE#aq-Et zInbFaqqs(xXF2?xreGS8lb1hwOfW7E5ucWKFOkZ|)6--2X6}t$=NV`O7l^=g(IGiT zu#Lf`r3yLsg*Apo+FzVqD1Y;p)%4YGI%7-*qiA~<|9Z4Nr{nu7hba4*H>IG?RW#U{ z4jyzdVhW6VjJWXvqdXg|*$hLO5C7AA3`;Z(4b9=}*Y~pTI#-?0UFvpfGDD+O3@(y0 zlS3K&Fx|0iWyP44g&~@exw$!(KfSnoe>%3ZjJ&np^|g6jFZRGBV?XEqKsqL-x?Z34 z%^;|;npowX6o9{{cL4zidpneQua=yqG!YXPo-CJQV`bIZtM&Bx^XJuGU{f)xxV-v> zfSA~(n>TNIdr0VBh^~s@YL6cbwbi#j)68)VD9U#z(H;&nZ{kA0D%NSlq89U#1)oi;a!F1+vaSGBl(xz;nlx zPS8a6TPs+uK^zS?TR~KCU0vEo@XN@=tW$$>0!x){vA(uS_(J}f*~*6fx3c|qYwPKi z)%jj6ZS6|vq67gV8Or#^-Nr@7)21{uH4&mIx9`7t_`(HYK_QJ!My$(?I2$~d{?QVX za0e;X*3Rb_6p(XR!B$ux>Vx)ES^VAdgn}S-;GLOl9&0NHQ&Urk!=Lx#TKxT?+)1#J zygP21_r{y!WCUz+|c%YV`4}hruwhJx0V(ayvv4x z76?<3k&HykofbUdDTy|sMYt01=y7^FH71W#UT5*Ago#(OIi&LZYIA@gao{B(||9ZwHRB{zljk=mBw{_zJ;4hlJC! z@D64jPF^1McA1)*f}hMcFHeS%Pig1qPv+@UZd3i#_Pu4hUT4E(YdtOf5p%mS$d7__ z`d8G}X0+xm?kc(YHxjzyV^GQYygH#=`>wF!Yzssfoyp<>#|^UrRzesnhiZrIVb;NO zr6a04_v~STTMbY}EnVlX@KRV*)Db+X4Ot2lld-(;dQ0!!yYu=^Uz@-SXU?!g%M&hq z%B$ddJarXA@sK0Bt{LLKrZ#ugAt2O${8$afU_<#=ib^U$B{;eqh@^$+He;wYEst9+QjoFik21 zJTSK4Q>SQ&SDa#50~WBka;56yM+RS;%RUp?r_(h|O?f9KCO8#buRSx%k_NZ<`P(

QJEb7GjUW;Qh3c{G$1&8LEFi(HIEDr*en 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 z℘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%0D7D0Se?XY9x(w-XX+1^?bXPoZ zylI9D71rdlmENu!B!Y@XLF|8{C}IvFp1K?c4M8vvViI~EJY#psVr6UhTU%QX3|6bU z|ChA@J58|oJn1h>_peewV`HomFjm!Cn)|I5IwX9eZt4kqQG1reHo1A_e9W?FE4-55 zp6j*s?2@1G*vF`=@PixvXuXPC%Yv~d0!@cpP5fEx$Ee&iuE{(Qe6~{ZBss=lsVV=; zbU{e++O9k3_GEu&f8|6YwOaFhF!v8|vhf|yRZ)qqmts2$-9J3h{=aL=x+1wa0o4D(G zN&t7w=J*gEbC-zOxUSaGI- z`p%VW*EoxdpT@_mQ{XOJ#_T)4ml^u|7xtyy6y;Z_XC&m>ih@2Xq4+GtWqE=$|U|j ze24ujYK67EO%4-YNs>*lsr7HTux)ZuIe6wRZKe)iN{!?FpI$I{lc;`nKCP;v66f4_Z9H#l)cTs9 zgr6U5t}HP&y;%|P`ktnY?b3stJx1%-9fwbAYm4)#rfhrX=hbpiPEmENhPkwHVat7-EKOvic(=*!$rBK`haR^z=&`G z^k_*vcRgsl_t`}3n~fF6Ct+b9*3J}2unZQwb?6x&H`Pl07A-kzmpV1&s9s^kwlhMA zWY)X=hpAfXk3t9j`--1qGTmM$Uw*n+xVtp)K_HhLpW}oZXUnLZJ#R!xr-4bV53r?R ztoX;WE)iIoUp#eY1E5eFgBJS%W*ueuIxz>B$;#GX1xKq@<`e|mAP*eO z552ZuuY5r!)1Y7!uwvSJ4^0o%FO^S!_EVqYm>2h%Hyy?wqLn0BJ&~h7cmfy72RRnd z?~cvO&5N-7IX3)K%S)cmz+0lQGB7pD9-e8a19Szv=K@7{9u;n!P1d{!&R8Tz_ZC zMw{|!l~L{cV`pVs^h<*Vl*hd{C-bdQ+&4AtK5(FcJ3cKf4Htn3V54&J@yQRzf|tgG z5;!Yvof4#_rG??H9!>X8;6^CcjDQvmiDaxZFLmY#ynbM!(b3cb)+kRxR8*9T`Q_7T zBnlbA`3^Q{`tM)m(=Ov3LTFUcsc`8oNjyFH7A5uS(r7&2hVBk#Mr!KNzblg!O}(H# zAw!J*CpG*p0N}}fam`sEiOIt!&MhH_o5n9e~-dF8Do6%=eKf7 zxCIP9T%1c8*s0b1DdtL+qZ^kT>nX>mz%tu)_9IWfebf5#+@e|Eq}xs^x??J9uB55?&z(CNHzi~ZcYOPE)8yW%?B5+bHH6b% zw;gFHysI$Qn@10dDV0khmcmcT@lEe);yl+uE~UI%f_awPqZtgpf2=fas#b_ftsPm5 z`nha0rDod ze+l%uw~#Ehsx9HVJ%?vyK&jV@M{hSJiq8QC!=u8VDtkg$v`S}#PKP@exsEbG%NaoN z{$%J=;-!6XJXhgue0^Q~UtQ)v49AH)c9J!gS1Xq(wCS0JZu^BFr7phMz`yO{U-_9H zj##ngKNX^w{u{cw?6cCPy9i$wJ|67eS@b?(mIqXbAv`FBCuWuAr`bAu#k<EZ9c1BO%d zj(M6eu(2K9jWW3&T_ByGvSVGA!Ugt2UT=sI>BV8VBzfS{9!~+xHgb=N(d>dCWnSOt zz%9q;0u!PpwC9iCPiO95W$Y<)1g95u0r+`2zd~l+gG-LSFx!vYJRVTnppp~6nL73r^!~I6h|6NkTDBJl~@7ygI_Ub-m zvxdjbc6NSaerwXwU$m`gCu&Zq+CApM+q8C_l4HpM`zYJE!q)*u8Ei~rCGj>fK#r^M_ngQXR0<2SiO@&h4j{T?V83$ zw-;Z29~<--J@WmTv}UY@bA$o*8sibf({UWNA%eI$aynquH(mt26Tb)w|&UEM{Vg&GtRz zuAf!rH(e!3mtZ>8CL);DZ_D<7264;2?%Bz&$kuxaS z+1WSHH<%Y`xb*uTphpZ5#i90e;Y8l0VLsjSnd zf2)1oykEy%I$+_?tE|hdEHZgPvx&h*VmgJ7PKS@AHMW+HIWSN+e&Zi?pqUR@n0!S> z;cCNvc4_p4W*a}BJNmw&UYrlmeDA(RTXY9+#$AgeR|5Z7vFt@Mi*#EOdZzJTFTinG zl`x_F%xIZR!JE+)yd1KiL>{3Z`+0hL11xfoM0dcrbDAwOWGA6!;%_4$$(Il?`m$op z0iqTx1GoPoJw0xs?e^(0oEk`vy1Kd{EieAxrzIJJE8CRbTw(^Fs|dw1TEzUOqR-zR z`^71a2tE}~LBU;U!z3II`uff*Fj zb)==mNX5*b?Jt$6x}ljusq+1yp8U>EQLqguK^4KiBY)jb+AJ{mGD*G!UHTQ%8)A2z ztGx|wxFQSwEmhFKyVWwZqW{nKfND)cO)Uj;)X@{4i)KP#N*aKDW3w)Dz76cMHTp}5 z5R$TB%_6~ea{;z5>r*e&(1WUo21L+%44=~fyAfA^)AK61!2;PgIx1V?vX*Un6CO5* z`EaaDAkTjno-D}6h!B@`^{lN<`ubqLnIDwC(Fe7MZjolVH}XyE+t}_^INrQ@)1)(( z651QczBqi;53V2G`e~VtI!P<~gboi3N*sM~F}RHU{u-iV{ioB+{;C&PBucGkuWe+c z%U@=f{5w-X#T$q)E-LExCvZ@r%Gji(BYh|7PS6TWpX_j`e|1jacD|LDRrz3A_}9sQ zh1xZyo@J_6kp>4~B7FHo=59cX+$PIqos$8Ww`6(vXkX5(a-Q|`kh@n&iZEG!x_22E zn8xB^)IwA9I%BXvv4+&1@&s(wZnSEr3V}MWBOdlYJD{iQEBN|gMeAYJi9QY=4xAH_ z!;^f}n{}aF)pe{NhDQ*aH4#Jz8bl8zp8KDcpv94|4Da5&d2{G37d>bGW*DJ9x8)Mc}bT)-y@$qu_IA2 zi^m1^kkhOQ`u#GrYAL$xoS&gnR}}7Z>b?adV89zYSrO3F*^XVE<}`Qwh1p0Hr>q~7 z^=Me5Q*+aiXL|FubkJGz7X2ZCYY5OX(RW*a^tj))J*+NvM~p>vd3xpTvyLbcFzfo@ zHR>cBulOi+zohCC$#`!3NelQR#>26|0>5m2ay9()^$TCK^$2gL{@MWLi4>K6nq4Wq zL&aOAYDJM~9&}>2ffEYU;klQnK%v%(D@qs88}$Kzt!7}{YxjP#e}gUD6FbNIL_HG z|2;$k9fXlS&4rc*H_M$Uc~h=pyvq}mUxUAX$)7*(+1bIP82hfi-XGfB^UTaEFbfbJ zx#JyKr4Fwg$dd_a*xbAc{RCt;O6`Twc**TF9;tt++MTGbN3=vzQEyT zXtLpS^6pp961&NihLx%QbFfA!Kvl&o6dn|s=v2tYNyR^3%UqwrmlX}D-2dxW$~;Fc zz%-mY1}Hr1ew(%?Q6r8HHN-HYj{@iVjb-=Llm$ExQ}grN=vD@o7Z=gZau;&-+qZ8C zhg}`+r3E`J1ta5EZE-q!dM{{Zs9pOp;f2Hn$MD32B4|6xojbSc&W<$Ht&9x}yu2@Y zCH9mQ{!{4WNf|Y@C_IHPpm@O3M`y9{{x=?nfDe)m;i8lad*tW0Nf)Y7ok18EH4V*6 zllaGv&x3DLY227qfjbU|BO>_}^g$nZwb{<74%-_UC0TW&TMy=y6w5{k^C2N2&xOtK zB}2J<;kj*w8OO#zaV2imfDrAH+cM-pQiO8JCA_SGg$3_=B(owl`1jb@p;0RcJmrH^P`|iHf8(A;2|KMR8b-eB8>D>(<73YBiErHIfi_^q}r-Umwy%B##n&P2x zp19W*0ABP}YHF&mkGnhBoyJ%Wv><{ixr?j2GH)6S5xN!#m!#a|u!t`anum%+BJa)8 zQ(;_L4zssOU|AEtx}cRezO({C_4&o0PfxoTSy&FA9{pqCU-rpuwoG#ad@Tes2OV8wQEcNKswW<_#B$=>e2Qr z3wxc(1L;zP`-|*1KdZ`F6dBlw5&91qudok1-XQBP-S=>H)#=ogSg)1LoaT0y*I)8W zY$nMUic*gnxG&!F4ciuM;l9FpBp_z!P183dW4;p=gEk%f*+X{duMZ8a*Y>RlHZUXJHIX-01Om5h~<##b;1X%%9`H zL5n{aW=PGtzWvI8`uOY$<4K!2>ict>9xAea2$!|fyHUS|_Mqd3UP)58cAV>tlY68u zetQ%UaLLxf;>pjUA>~<|>qKt0-5j1y{Dy{xHosaa5zimO0it9jC5PhoJlx4JMEGGF zl1M+wop<2oRJ0Kie$hh6yV=>X%v6<7 zZ>QGR98G%5+2CVIQqMUE&)bWaFCT(4*8|C}q$-%PzwO02^3P9C?!c{-9}uxH6{*P- zaC1qQX&WRfZ%nBoPE=f6;Lo2%0nEp@p-S+_2mc1$GrFMMVO16MjT=zMjP_0#I7?E|LI~mKZvrRce2gD|8UkzX}}}1hura z-yfwS95^^LgZLkuRXkw&eKNF5q;bxMs=oW|W+%q{K;}CqB}E2Z+B+y{SH{gUKR5vO zq40GS+wvQ?Cb^?y_)(SZa2-8HX!SL}lGPu?vp5c+4JaiDXpoFoLQ2?m-HCW)@V1lO zz?oUs*h0-Xip!@-x9;3ICnrZqJh`Hx(-0{`5yMN-<@Y$T%l!i>2-ySsYh$rGm9PoMhx-g{Ws*)bX)u*WAJgfY*kyHE(s_0_g7 z5p4*2n7u&}sHUVu0wsb^`dD4X04B4Ls3=(}DR-p}d?{%>GCXScoE+@Dcy z^2x|BOCA)w@s>0_JzZN{+qYc(@uOcZumbBY)>CePz!ZU5aB4ure-30wnB(EF@M^26 zu6D<^fcnTOcLcW-!MYfxjXE%jgCP-#=zwuDT?bzCP;8%$+Wvp}sr%XO@MvCW|IY(cG+w zhr(^zb={3_FAVCmba3@&tX7?*Lt3gih0j&mdQ%>yM6=DO+D(;S7S95-O8F5l0H+$q};v1)`gmA~BCLKxgrapUSW@Th$ z?;O#{HR^#PIx;uWqxrQT=R1PPx@ga_9BpJHtf8+(KlHk81fmP468@yFeACTJTHjv6 zFt&&d)s~^X!){??qp@mqmQX4nZk)Y(m72)#a6=b`AHvU9B1XI)ot?%Jo{&C6>gecr z_wnNeqt*L={3;SZefmU3f-*w1w}-CXC@^Cq%&MsD;4|C>?yFlA0Rx>2AHIE~hk>%t zZgO9g?o6JiCq{KS@8#Zz&%EfMx-ZxVX=2A=773?QFvsWtt)O`i`Y;)3YOI9^S&2qQ zM&^{u>M0mqYUeNw?&CZ6&CO^UX-!k7MduvR=ukB=TR?P%;iPl$^Vc87m>>!ui5*+F zZgs+#LpV&Cg!DZx_NJu?Y}vZimz5a0_8c|V+w5#V{V7wB|%fmx1D=Q0R;$kQ8f6mcNs)e=nIXpX13R(C6 zXk?j=|M}BDG_(a3?&o;P3VD}Tl(A>*cA0i<*RkuLUBXX$z!`$Xc->eBzij+RndU7A z^fC#-W-FkPOM#qDJo%#DhZV)XwEmgq#4IGFSK6-I2nB@03_ig?BG3R$ahk%P7K9iW zb>3{}UxeMSAGpA(0?v{hjR9F)D*EkoYmEmVTwOP7>_!ZgO^guB($aGZXC0 zz3Wj}cnU>J1PeQd=i|}wPnDMCDr26qOxt`CC|7eZcQ7JWus} z(SFApYM@j`$5rFDv`=}AB4wWnzP7Z`%2(|P2@gMq#x&B|@i>u94~^+`kR#Xt5=kUf zeXHwd`QKw$C*1q)5OL8ZT@`WN*3$AlezA-3|E&EhN|zjrn@Rk99~vOZFR@EkeO}h5 zn)?dHo29h0unLa~r=@G$LTg6cscMfDZu81b#gyENXb9Ufs#P9Q*t@I~9vVtU3QfK| zJ3HHqCZma1Pnf|)ZGMt?cz6m63;Ph=d+Av?JW20xM}1fvx!qog7SkJl$U_S)kroLL$%c7y%jpI zFMqORmvqsZn5OMEoaHxAk954U_4;=TAO%uin6=9mwAukpZWpM3`S$G|B+8H24SL4L z+tt;-%QuBzBC-`yRv2d?FQmEfO>J$}5e2~6XoFSybsxP+WF$15^g6$&NV$df(qhG< zaTnpI_jRF&`Q>b8^W(u_l6V*J5Pk&YQ zZBU>K3lz%xYylb?nmv2>ZpR5)Tv`I|-ZgCYDy>3Bx;FmL_i%D%TJMF}VX>DVROEJPb;dUY7-1(6u zlb+&}B5}2FAo0(6bfHO+G&M7P8Q77~G^xou@$18nAN#Okbm|hA z&oneLLgez}w<)3+L1Y{v5glusuKf6c^YIeSb*$H?epEOvWJn9m*o)zilcUT1EZ|>g zRFn@aQ7TDC{XujvbaW zLvlvO#sld4F#4844>-pW%|=a(QWV>_8%R<5!2?lJA7UU3$Tf&42>kxX?=(`(L5ks5 zBZFq5hPZz{c(mSsm zjD=%rm_y};R#kQ;A&%Fg_VH5(T{{=u?~!*3UoXGE6kG6mFgcA6cWW2UPkt8{4C=A< z?#TO-dv9=tM<d?wR?mr_O-2Du(w&p$zcpMvj2O zw}%uQu8(1+JcoW21tXRCMG6zt49-fza%@hdWkbN!uFV0!;VeiI73c-;O`ubI38RW*g~s zV&W^fVmYq|Q!yXga@I2YENl$8T*w0%%R`R|K<+s{0r43DCEktBzCRkEBon4Xl-Zy? zcY5nRPfn&p_`#bXonoF8JAgW4^uZA{Xc7B{C`|Cp@O7;xeKRry4_V1V7?m-jz_6HIy!E^I8A}SU9%+j`Ui_wm5HYa zkfC?`wl_FOeH6#~iTk-;7jLJee2@AB7-XBawl+Qxj>|1WLo(9q*RQ7lw9utYh9>AI zXy{sC(fUul@B}k@kZp*90lK@2{W#3$(r~)I9(Yk&Du!Ow=WpI@Q&CaLa>f|i7fDIA zmxDQjaQ;zvggkjtyOv=NjQ%){rkdb``Cg9_QG%PCOC_TL871y$Ppr_adP zD=->U5nCF#|NY=}p*1Hwur=`L;SiJS=rpr@09029T>EWVCt8lf`lY4C!_QAXS!6TV z1}S`_Ara`HHJ0dIB^C+x`EGL)fExjwa#qv=#Q01o#xnV1V`G>or1%aq6F_y7R~{wi zA7Q7|b+b#?PV%qS-?_iMC?#8>(H2%_KT;Jiq6ujRT@bt)G#7 zimL&b3GJmxYMJ{0DW1Q2<$)boiKa;i`C+>zqZsz?^~0~*M1q(Xh>R;I{G5-5j_z@1 z;uZGek%q3nOc(KKD)A1kKYzd8%6Sus50n0=u$NY6nO^Aal+NY|4sI&sf+^z>H5nZiz>DC^-&HBUow-@j2RL_!@~#P z7%efN@HEV5Z*5JR>aB@CC?rIUxzL=~tBFzxn+vFzhOd{u@7J&Gko@E6M|O+AFatoY zhp8A9%0(kO?$A_0od{u8kAPO8l1U5(3BhsZ+kHrmf@7#g6YW@$g4%r9-Jd^Gq0QR^ zbS4IJ9dWO9d4}wchT=^mti~7AO{B9b2g{VzQ2uHin*qI0l;Xr~`|}H#UJk0!xE7GuqbZWE3ZKew;&h5{Ri{8&-F_koN z5_jR2*IpBRCRXYpYqu`Len#_2Ynrd`ptpi3m-NN0a9JG&;nUD!-d))Ldt~I`m<8S_ zL?^QEwP5AZUTxEd7lU5B;1DUWk^>7sG>l>n@_X&>#W?iV?>TE(y_-`2Z3VE>Kl1fG zl-B^A#zp@;AYqEMi;DyRGPy+jZm25=#(LmN3!PVl(TQ^u@CR%TiB}@j1#AlFW6aBJCip}3y+$?_jt-^RFQ_fL?RJM49x(bf$Jz=yzw*ej1XvyA2 zymezELHWR5a@3;lEbpO1zJUfnsUTb2Q4YEd9HIdokE9ofg!lr9o8*Q-NiaYb_wN&p zT|~PEc)31|g2I#x863nZbi^Wglg{40eGtoQfv!uWB^0rslk;1X_mTFCiLpirY7@oK z6pE{))Kp@8Jrg>yl^)LE6lXaum;r`b*TX>-i1Pkg!C@XAAL;`a9&3nC0aX!&_cIA+ zzuepth)^#uR0DRzBPYp-n*j+KEH*CgYO3mCARb+eWU)sBAaV*Zu9_eK!9XW`lyNaf z=(_-)dbH~Tv1IY4zoW!NnJu2%t)iVRh^>=j%p!&E%2%?t0-AdsjHR{fH~G#r1ipSE zsA~JmzEE`4dVZ+i^F!uD{<5~1GaaRC{G7cd&z@hO9dC#nud13>Jeq_lssm*Ib@v8B&H0ugp2QU(=zkL{w5@%Ln0bmR;HHp*NL2Mu91sxX72XZ-wls*0gcykL0W4mKD z=fRMW-eH{!4N*`IH!XeA*QdUxOL%UPQ4G0co-g#1zO+sd zHcZ~{F*m{z6H-yVD%@hl^1)%pDK5^2Ek>iB34q?$ryI(Yos%PkF*jz7Qg&BglLIy9_H8%DG;o4ndx100@g(+$^bjoi+ zj}&2!LQeoh?41}xhJE}3bu(wzZongqPZ2V#qr*PK6Qak=0dLHH0r(8Y5NEXIF(C{t z{v8Wf>c!4>(_^}!t}Z1jOOB@dYD~}|#^IyTvWk#E1S)KjaR=Dkk35ERuf-{igZa6k z0VJ!a-9!MG98@fHSY!|27>qc0k(NewlQ;S0jf zcIR#BH_P@C9T5;ru=>z3x8m5rbV!Y$Lu>_mv3)2}$dQzZ`U8jHXInbUq7w?CCuI1; zzi^2U*(+kUF;%55Y+XdD`v`>zh;O355T(gO+~tMlw8HI-7K!Gg|Mdb0)DK|ez+y9; z9~vF~i7h6R9oEGs=;1*9D)VvQNn<+TY?w~=fXl{k4P7*65S7pUiEc`KK&&!y`=h_w zIk$Hr(r*X${15I%x*@VFZiBV3v~&lkgzR;GIZV<6XBPheQggFyN3`YZTWHEq+gzOY zwmbWQRLf#}W-s^y_WmgTbJG$B4>G)?cz@4ZkM60aM#)o=dw09WGRsW`y6!8{-`cE3 z?n`M-?J`WYXd)<@XYQ|&7ohts_h8!7T@f}$o+0K4eeo4&{=5v0D4Mvh;9q>lS&u_~ z^a{Og_6rki#x4;d>ln<0Cf42P)BttQpkW6-JojGvE{vpqk3vsDoH7&I06H3ps z2HMHP%WEs?IS2{lg?AuY3rkcG$^@0Kt>U~QhBDzmyJIYv9=uB3#lUX4W<7g>ntg~d zM)uHB34WdMd;_lws&fNJl63FgBP0j(5^V>j&^O4>$*BZ7JO8HSQ*G^8bPr5T56YrJ z@&LNxz>-Uwq5$#6*&*&Aq1d(S0wC)7eAmS(J*W@V?(XhJAgl_Uz!wsA267rT6VoH; z2ag|UQG&>ufa;6vLaq5{93rg#3Lyc2$Uqc7JT+Od!3o*U#48zmKam@O#u+urF)kO&=f5kg7PFgiFr&@{ME3?ButGJt)BO-W$DwqMRty>a6a zviBgcnZ9H_j+PhJ5`ys#pzRB|D}~@Hh!W=+gHDC)*EtfwRUMgYWdi*)ULL6Mh=n$!1`l!E*BC z)KmjBO%O){bry*PNnAhi5SbgKQ(x^L2x6E^@)DmLVs~=w)>$y&{YVoS=<^==3?U={ z2;bL)M?{1XV{C{xSYU$6p%6{=p#MC91oT1LYDEx9#Z&o6)R8R*$9wZ`f9Q#6_h48P zzxlO)Rf#4u;Of3e!<>npqVoB78FM{x<7ZM%rhGbjds)|{t1a)D=>5LPIsQ>CamSg9 zUq@T744aRWe`;!~TszZl`D^O_7PjvJkM`*B4pmFXIRty3Oz0CUWski;C6%u6?cTbX zbnS)$TzWMD%!B54?;di5VVyZF$cLz0Ul&{5zkk8TCif!eHWG0&1!_@FN@@}T`Z7K* z;?v9btOGj~rBH7IJ&)|Qi`0h#Px?<_U?569f^WyHg##QMQbuKk*M1p-H6^dl2KZo* zt0IvIz*ynJhG|ULQtz=0)njQgXP;~iRUjv9U0 zpHX%SA{h#*Y$bdl2nkk06_TsF}UsS^idV zWG|ON5vX>^05g0NR0T0D#5+2g*=-=@U_#!Z=tms7ckg}@cby)nUhpP}&5-!<_dQds zS-V^>KRHos-cTZv=^rVvwsXxYLWqw~VnT(9l$9sL>>YYdP0jp9s+mXQg9jIEzn#d< zRaR-urF$Ham+;y!LRsY-eXi|aI+brHhnSCk-M-xq0ai*w!;(2acAUy-{c*sNyN7rM zYRNT~l~#E_lLsG?zAVnh#L`YJDrESQea2S4VZcYwcI)L&N56W^K zhXdqf!;uQ|e>~nH|3cc|n?NxTIxFVv1p*B4v8i3ZPD}EX58qsU{U;p*!vNR-{QV21 zu!{i5Z-FzR9jh7zV2VV4L(D+R(}(AMzQSWb_5S^P`?+5c9dii{=X9>p7cCyJKe9#T zuzV+`?;sERK<1tFdvy$>gmyn)HE4%x1bXBFc-o-ugoLFmChwG@56j%xMI6aMB0}Ir z;6Od2q8M?u$3J&h-ia@W}SflLI92Feq&vnP>oQj z_?besbpuEszDFO34TKq#rt0py_VzpV8NMwILa%WP;is3|xF8xRvBY$b9; zy@?$t9+Dfp2MIJnc<8G)FlcH>q8A{;5M8Nm>^uI4W`+L-k@vCSf+L zp317Ts%^*TSJp~2F1#7{GcsIyy!yf-E7AW-?NYdbqn((xSF;Lx#p($>=9wIHvrk;b zlfU<;r=|@F2Q_T6hPo&5ndpD~0E8hU zVN6pME>-iKh~4UjQ4E!}wG`kdAvHdXifR|Rus%qkzb-nD5eJ{2iI;pnPxU^R4@ZD= ztv<(~W*0#@qtb;fN!rP&^vg1k1JJ-Ay6~~3s}SAz@|+RsB~=k}ijt3`!XzO95}m}f z2QRrHVL^f{+AgD#fQ$;Z4?G=O7%Neiyftd(?C9)#3I71eFI8NHi+VF}M)owso)hLj zSTPw6>?lE}ehg;c4Ny-UOsdL0ppKv4QW?RQaU3{sJz?c<8Ge5=v{2Gn%&iatkx0BX zN^TGMF`~ryc2yp=u&^L0iIqhK2JS>0CmQ4-!De-5;TrkA@$mo{!3e4u7h|C?sX}0B zPEcS(X#u6{Idl+Wmx?$u5n;yV2=kau-^8(>msW0zW^>>Q3XSMgN#-7DeE$t%EUwzd z>TGbO>&6;m(Stc-IUh)jo_nsDQtxy%YX)@Ah&$@jn zkH^>nl>GeGTF}Np>-GbeJrT7{>9{tJ5epdF{IpiPoV!4?t(PN0gvWCa3`!osIAA2ABXJhNqiJNCQyZ(Af!=_(){`j zF&e#v07|b~nF>t1yboP}gFU*f`xxYU1RD+%n}C)d5ED=@G-Dzd+#QeN_N4yWF{&1@GOJgidnJHv_FVOaO{hlW#*GsO>hsM_tZG-UHqU_5 zvXd{-m7z!$MOFG8y!?qf>hp6=tOeMl;^=&EZS3e+yt7N8mi-XERvxnIZGrlO>z^$T z3kZ1Quh*5-eW5X?^#A@Ha{}k|dW!eui`B}vpVCAcX5IguR*QbcIVa4(u_|I)-+$sV zCC5-OJ$jFj5WY}iA%&lxr@|8=($4?whL+0paQ4!NZ^O6E?Zv96uZ&69tvX25( zeusxggP?E&K^MpUA4K|py*lLAz-PJM8#U#oXVNwMzfV>B%QkVdHD&sRA&0mkEL45K zEiw}Y`T43yKm(ip>C*q#>)~#(rFQxA)u;JfG1W>}LssM3qZl>fHz>+~CEFui8yxxU zS)*c%@Zl*i3G)K=4DzD;{;$ve?^~q!Ck9X6F}Zot&hlr)Xj=fAF)G!RohboQ6icSV z*fo`v2VN8MD2l(|SA3cN*Zbo)D1TmAEL=?%oKO9;VVuFbJXv%yEGV(~MI94V+pico zbX_&=M)N5NiSJ6Ekbdt8%L z7Z&6ZBI%C%lu%D`vu*pgRH1HQM=-trJ;r!myY_8TS*$*rD8L^-evAuz1;;F06p#hh z$lpo+=hZ$w z`c5oJBSa7I62R>WqO5q>6>qUoLF*u%gK?G@jpb`WbjMs(16WML&|rWbx_8KjCyiS} zY3Mpk9Z1w@{lI8LRahTnWav9cu6!`*8YG{|z8>%8pOG~^0 zxQPx;f&B|UwFeDJ?)B+VDZ_y2yq`C|B{;E@_Yi7Z`@ zRImwD9~HX<>pu-2mLloPmvAi2T9hXigFjN012iinMu zVoy=Svg{6?2b2{?sR{wib)(P9XM6!{b>GjQ6v)0<3uvPR34=1}P{C7K3=0B8R7zB2 zL->>e)rt5`#80(mX&5?~QKd@*{1D6Ft+$__e+NDz;Tu3Ml)SZpXS2$Tpa>ccWEHaJ zmtuTIEIHxxs;>6B$Aecu9+@DfdlX5)9Iz?4W^?6H+_=h3B6QW}sB(4+D&EXE%|pHDNsE_E$0kaw9R4p|GUnfUvOY*M8-D z;3A=$YUZ1S+VC3A{QbKHTBL|b)W40H;tu+u4}lqi1GUC8%rL60y%>ASaubMG&;?Qm z_|&wt9zfrOosJOIpxarZpvZ^^m5&|~j2_)eBK)pIzlbb>e)BOJ4?2%D62mm~d6-Hp zuiGHa?g!Ol+L=pMc@)MjlZf$2tRRY10+9nGqM?Pb#DLxFzz{6Ko23-sF^NQ0?awIf zOiM!p37-sFHhyj(-gR9Hb}me)fk)mCoW~2Sy4DJS)^jNE1hgXH3sQxN&8P3{$-X3s|i3I#CX09z$cZ?#+MVHK3^2JWjsxG$5ECi1c3Cd_1T09gNIHO#?XG87l;cCaK1OAiKL_4`3(4Y znEU`xVc{VmCt-R`sq+d0n7S%Llq!c62=z2ULJ`iZKX+7B85kY0cLf9l3MIg7)HXGZ zw()Rtb70R3)N}Ik;-l$fh#EAAX}d}IrHF#b^)W&_$7>Cc6jkuqXCb!t{yM*b(}5@a zAC$pGMCm;isx4(7JJ>>mbk>^bT^;cz03wz0Aza-8kXIwGbqtK*N2A z;*TRD$cU~9!bzy7$AOD7{4sm#9VAc^-hnXIfe)+%@FaanRXYM#j%#~4;eN!-Cz2cf zY7n(Vx7R^DG~Bqb0~!(7dqTrUgy1<5=noK$zOa7aW}=dSX9?^If~Ok{0%*|ig-gxi z4;w3MNJNAeVb#J`?3-O8kx)xvV?8G%r2Y9CL~y;PLKKV0>(WqRjT|5e6*j#$kG^IC zCkCFRkFT#>vdH0F<8RylX&RX0&LJsT@pbU=39$L3vp~f{xb!Q_G&VN&%PZO4q<0|t zi-ysnGLT}3Dv_M3^3mUP?Gj(>V*D#-SOMuSHbgEKCGGb|d03u`T?jPu{I z)OiKu0A=POtTtpM#FlQSf`2O}B!AO{SI&Yg@zy43nTWJELb!zCTG!lUZ@lAr8F%AW z16YsAfob{dKBG4JCr%JEmLbb+CfT_sauU`Z>|jJn?6q@ARK{JMCgJy*xe7 zs;e_oRi1?>98NQMT(;pzd4ZXAsTcg0ze2V@|M!)?-Gn4*Cj=J-IRXWB@!9?MgtUm5 z$hn4^X@|ESF>{n?2ZKCDh)U1CtmbS2{XEq9SoOO;Jn;xhsQ(5c)3u6ds459Bb9Xk* zfJN6aVd0FabzJ{Kf(WC(5M48| z-g*5?hzrmKkU5=;@ZCwovG{qyi`JRIe&Pp8EKsmNg5 zisL%4_O-5vG;cj!-6?GYct|#pFdU@bL~W+TSN9T_K{IunWUr4bm}iIjrw<+voOupz zZkeRdjsUv+B|EUXwO0-#v0l~qs*8O?BH{eNhHRbOb=q=ZOZ!c2?X?npng9ok3V-oS zg`1v^Za=(UZWh<>U^F0!?uwcfID4y_?YnpTKvKeD5mVCfUi-PYwvg{)eFp?U9Pi0a z#(!wynyCE8{W#Z}cbi{s#W(j9+yg%caY71he=~?omYjn4a>{Cm0$yjHR!P*{SPp{V z7P9Dj$LEWQ<==JH8Q=sOjI6Gzq6k!{Mg@RAwFV3$<$+b^60cdL;q>_T;~4?Pg=={T z(PJ%ePT?B$EhywToe6UTc8Ju?oBI&EIi1VO$}BM_J4w8f(D18b{HH>j*+tPug$ z_?4Lf6NM#Vv`1{z|03_5vJ+C%E0nZD5if1qV^D z7Ftu{dwGHS*tT=;7Sd5+;gL7I>J={HB8oVOxE^IY++dSG*#GSc&VB#Tz*0KvX{#-e zAn7zdj?NMTVM*00T=EAgAGc;1$acfyU-A=VmLeO`*+Sa0XO9~!^c!xq{FC4c2qKWA z3(mvn%X9aOG59hB(W}3Lc{9wIa=1rb0@XjM+HMLk&Z-`lHuntf(VHL`|Ffb7|GVj-~xj+f<^6@={LjkTw(*6SnJQf%238fTw$PmLW zk@G-y63Q)%b&%Td5}+DsBaLu#LtNAWZ14h(qK-005g;nU-9kkLSLSwaJ!0}WFF*eQ z*qOj*(EdV$#34dpCA8Uqhim#w+t7bcFfE+~#Q_Y4qv8RV6wD4pqaTU};FcGs#~@3E zm=nf6{3qs|Urh3&B@xa+JS;8F@>2+W578*p;rB>3=>K8sJ-~AA-@oyzLPfHQq(l@- z!zxW%lq4l`(5JdioVpBZRy?gh{C8n`#=_uE(UE76#mBDPB z%1>>BT%2EeDcnIpQoJO2(AU+K8WD$GfB#;;|L*CP_uro!hsvLTLsm2w^zeYS@rAgC z&u1A!mjThI4<>?mYvkj7r&i=x4shS&cm8)TD>DA4!CX;LW?o&W@dj%_9|Dk!+?!k^ zY!~so-jh`o6-l+TIFFgvGgA<5%m`Ns)-J*&)}x_)qqtIT0NM47J^W1w!anJ~fCyzK zOqJ%qY$B+%vqbE8%OS-kQVXdqo!GN=h=U?0L3jWJ5G*G za>>3p>hj&779noqig^P`<@{?% zY;KO)ZScX@-rsz5fm#ODKWeR1Ot6UR!Cn6V#6AFH9ew?WIHD{DuqWSP`U6Nq%)ES8 zH-_&{?oFJ06s!xuqenlZ%Fdc1`yB_$7`V77M?__I`gv!*R{x)O`rr4Q$f|@}eeoHK zhrIT6A(`*y0f~Fi9V)rN$+1PXRFDB58tiTY=^uqCCL)nF0%B21EHFme=enGxjr=G+ zfpyVP{_D80Q^##fE^c&Vx}ZI0F{~of67>-a)Ep2wa@R=-<{R6TN(-*oUqoDG$YH&; zLPm|mSD}q^=JY_%@82Y08XA9D{Vd#JsH)5nQ($dv{SvfktLRRDT1czav+vO(o4=<- z1Xg>(sNg>uKi=8G$RX*26a3y#Gc&4M!NZ5)=6PFt--D6e3-ig~bW|IZT)D&I(M6@d z=H*>3X}aAV{f8>vXY~mGFARe7sP6{Ld8(5_G8VqdBBW?qypN9;H|UZKEv;uTXwIRM z_eQtoK9!|Z*VewP%j%4ci75|X8w1fa^NSDafb#LfpZe9n=7qqP8x4SHpNgcvetlpW z<&?Dl0e}c?S+d?k>l8#&pkx*Mijv=y(j2BnJ~`zyl=sKp_#P!{YI{=Koq=baP%&fsZER zKJ9Ds*UF7-kCLe=?|5|a^FNr=R2EnXII@p+sN?*?!`pTOqjZ$T%>!5MXMmS`iOlAI zHz$7g@NCF2leZt2YO{XY`DscU82|Wk>id|Y6=_Q6_8_}*VrHG2gN=_r2l|Pl;fsRDo&-%c`m{!a6;QE=Q{?i`3GnI z19e}6V(7=FZ0haq-ePKO+$QyZe#8Iy$az`CzJ54W(BoNClDW(t6(1kZ_t!dc-M>}z z4Fd+_Mn*>n!p@aAguJYVDGZ5!Dm_x`=&$lD)&{=pp)mQerj|3EMQ zeb)h|c(u1GXH0Cmrb9P4tntk)67-3V(VhP(e;7a~1(v?EZCHbC=2litiVz{{{=keS(Tw=*JZbS{;A1~@%Mdy7LPF=Gb;Vn6FQ@mZ44VUDIcKcL8VXi9{?t0kvlr9Iz? zz;b&4LZIB{9J>DO3;vT-Cj>I`=QzP{6$zF2CX8Hy2%Qss4W3rdG+8Z)pa(XQN*7@& zCe51);n^rIETqB#xZg?)w+GHgrhWlo zoDNVmyff{POof)}FCO(fB$KFrvSmiNuKEd?ieC$Egn~f_cl)+V|1$iekg-BK8YW?< ztltJwnB-FiX4d`uNu~{@w(Nks3HT{M6Cz9m=3%yk`90SR*vJZ>o4_?+Y}j4smk}8s zYc7B6u`w64l_+{~pkEn353~sY94V4fe?sB~$$~H`6;;!X=t&o>O-w$fBA@YT zTyST{RR&-N?r;L9!Mg;?ex@5au}G%JplA+RmFSs07Zy|rceMI(Nc)_lvt^A{1MQB|4aC|;E#k&!(j`>k1lvY zPIFmI*g%QximD%;8eXBd=m(1cIl-IJI}bQKEjPoDYVc=)MoLZJ-Nksd}Zq~+Fk2*bvva@P-2!wv`$}`n$XY-1WXM@Srh5xBhjrf zff*hH(D!|X7DcLOlCU<9W}c0IXO)HdK>q%NEjyfDi%v%g8CS|~%C+HFe9AY%^o3$nlpMKb@39K2Xc=CMH=&Z7&)hOCm}s<)GXDDY zLU--=ukS{_pVzl>JhhwqP2-f)V4IElrG*`|qcwLro_>_#jM>chjI(v)xwALvxn%ar z+&_K){fHXZTQ0YQyU(4z&d+tV{_#Uz!&rys9UKz_g16$_Sz7y+Y{MIPE%GWX7-y=F z_RUFZEjr8}?y#h3J?y?tLPOB>$%oDz7s><-fE-dX=@}6HqN4>R)iJFWh_%n)szp)* zL7Sm*DQ$>DX^jf95HK}q=iyzev#lgrT0DA!(&bs;*_EJOBHlQxAhb&3X~cYjM@-p; z`W-4TsByR8_$dvllh;9GhrE>YXsn4M3amlH{4sLDqRdAU08r4Dj19Qq?Tv}V1T7r$ z^P#6JYHGUPxzGjSE`~7`O}y~(T8l&!dicT!o{Ozk3}G^bqPMbQzW{L~W*LwKnW1>J zl{mv9Qn6Jb?pO<-<)ui;a8pMoCnw_lCnhW)i<>rXENF;>W}WPt&z~~~xVC$c27~ev zM+0dGz`avrrWRpOWMsUDNFf24FZq2uwI^^=oE?JyF--)RR->lsLc>m)K?F%`#E_MS z5~z}iPTB2qpA;>#d0_L*%J{HHwBG|YDplPU3WyYmk1H~Rh6`|Df7;2O>C zTWr3kq~BO*TiW!Mg`s%=_H_N9Njk-GLzevmI}#EN{~SAJ6xG)H`I7wgb5Bzo?sBhe zYx#-7^I@IT*9%TzKHnePKa65n`~LB}qozuK`?=wKk~)8W4L0v!>dmi@PfJOa7UTRp zbDZsRRzf!*Q%+S0?t|{SQtRw}n%3J~AEvp;==tESruQa?iJgm5CPA1+h`ET>6ru2NrDr>wxnQbr>{{ zq@jgy*$_d|#^0tm!=ONi5a|MLs0Fcl2_}b_{7x_xUy#f>+w3&|StA3)f-|(OK%$oH z1PJHaMdsK{gWDBbXM7tYNtVMxLiUeJ1nRK3IDCQE#@N_5XrJ6K0A7Ow0GiGL>xHR; zBzuF6guNM>V=OwI)=cxk5bO!-DX*NVPV`8txh3o`N=r+34#2Ku#ObjM{1ee1BZmp` z&(4k4hLyc=q!0E5z_0$$+ak&Jww_)W>RyU7%5-gj3r|b9@CBomh-r&B%P_9HJx$Q6 zId7&?Qt6nqHOhqXg(Ld64t}tl`8v07`Lk?m_WiCmNx_^}4@J4<7m_8F?idOincOt! zZHlq+7OH2@Nf$J1J9X;PrNNxXyav*$s%y^k9n`KXavf>)T=5(m|8k@?o0a|v(@XO+ z?AO#Xk|rMOrT2YjTv*7uPny0FFNOk zW0k78_HaEKl(C!D9MNf8+t&Pov%f?`-Ak_PV#$@q!98;m!HNU>qviI=4ASSj*}dMb zR4rGQLI&>j^z`iK=RdEnzaNQM9`OHAcfNfod*K4IwbxTQw>2YuLB7$9LO~TbefyN% zca>cJ!BgL65-x-V1gB`GW8 z)X7n*y=^jEEgu4lLxHkqI}6Jn3nWXl!n#zw(NY{$jU!*8B~xSMiJoos38t4dsSYpq z40Ewti?jMJT-s>edg-$JDF@lcv?I*N{la`Ym8_0Msd_F1%qdW<95Xt>x;oa+MEjsd z7CAC z>`?p`|4_bk$tO7=4RZSQ$X&!p|3VP$H;6bjqnw#v+TIAN?KW>be~G<+*5bh$?GxtPOZYZ8>zpxuvSdLQrsi3gWy!*!d%qjE zi^t@j(I2{;zwN+=oCFPv^YIIaHW1pbK+!N&^9E#X6lWlLsY8H${2_D!Okz2(2KWrA z?1Ar-`WZr#UHtrh#1Z=K+a}PEzn<5Wl&nclPp3EozPjEBfl(+}v=c3|W04l;y^&8vJKU zS=hva&3@Q57m102a7@T9jXV?6@+9O%#$SW>xbEn0-M7;}y%--0zQ{0l-A66m#k@j@ z;+zqy_fV9X`r_{>tw_y}$@IJPsdnrLwFPoymw^eLlkk7+L}TKs|L=hR-Ei}Y)|MML z7|JU|jf7 zcq>dyP5sBm?-Cm>ArS%NnU8j!BElG;ez*sZV|1+639OgjQe^5UR-j?493)%u&BaJ4 zNos58=*TZ=u_Z<^@Da!QX(;iRf96Whz_#-aE>43qVD~#g;TeicUARC8CIc)^EXd#3D%ivSGe{mcwx2C8RKK(5|~k6TOUJT zZo0E5eNW)EcSo2G*T1#5DEoS$xG9~FtvPfeS96ns5^q26WtIA2iyiF^uhOfF$2$cN zG8`}366*70@2$+KQ;q$9|A-ksElB0n+^oC5D`e3YAOHp==Iz?O{sO{)zZv!(ipa=D8o+AQb4)xWK&{_B_&&gikPY|%%=$%;K^nu?Ok zci`N$GNEhf`qW20Jmd89+IV$4CKTKP)sf%b(_<+tF5W7+XHR!*f-cJLt4CI=%&e1` z8Ey2CUZ`)lvbyitsKayF&_BB;2BGX?Y3N|=o^MfV8kTXHDB(0~Z{%t5z1#09e_4aa z*h2g7UbbsIvo0fpn;Z_=oz;qRaMxH__|wUw`+uOXZi~VoGj3E(Ke)e6*+k$^yJ108 z*$*iXv%G^A)1I?YGCwMlMJF|L?>GGTed%&_Txa(K)}u#Xp7mkf92{_RMQ2OxJEaYK zWBYdR`eVB2vcgC|M`A5drap&&|Cj|n#$Z@Swpm^xxD5`J|NYmZ0n^hb&n?By(1z#F zy(xPeJYgVTs?g$i;jo8q^3v!0V*dOltE1Cy6TAF9PTcTY;na|znVboE)F*PcG4mLm zySsZd>i) z4+x&A8>@S!DqHfWuFBlz^Ur4kZJp%HH!GtK&N-flVGX;<6V&-m5^o5>prwlJxj~(RsO!e_NP)E?Ek74OY{wO z#l-(;*R>eiU9K{ytv#77RI*oTOK?EJyVMV3=`lOgjXKV?1aBN)2Xb=DhP7+QL3LL^ zyS1?S|Mhdq%Hu*+Yr7^El#N9KOzN8xKO}U|DR(^e(J^W*HyV5K@?+}7x;huoP^sQyKNWb)N_s- z4<|0jhNkQD_gfqEhXx(Wo;GX#FjJSds;_X*l;M$G<3p2^p+)s~7?v3&yj4<`nJtgI z1xSNt+mFl?rwY{cnyPAQVvqh$W$e`>=sf2+^P9veKCK#tU1RK zE={!ZGIe8$D!xI7UK~9nG~MxWDJz$5 zV#dv)>*cHCGIBwjo3<>;>3x+!y z4!Uuzg4$EG>?a2H^U>@+hD<#%qR)o|F~3r}1+5UNdO!|uvLwRQG64Ze$(?MBwD<=a z%uSZZiNZFN?JoQ%^8axnj8`F1FV))k%7U8z35WikqrV|A+>9bV#9(!aAVc?I`@uQ( zh3e+zKh771Sj7!Qi}n<2e^aMj&sw*QJU1SQWsibPAAf~gYZ*$v8~6;CH0us+I=y@; zcD<^T>p81)@};NBION7Qwf}yD!;j_n%n!)S5XNQbz+{g*0UtL8xAFB8Dq*n*G@~=> zwk|Zl00>W`VC(AaBxWKk^NBQqbmQbDA3d-p7zof9QHUKJ$rA{;;Rz@27-DqH#6FMC z7y*ei=!FOyg$8y@V;>5N5>$PMz_1g$5N>0-_3Pb1PXR3=$k2~v9Qyd9bD;7eMFwj| z*|5Q1x;qU0OFPH|lLp!=IngMpwMcQl29asWi5r>P&j zC;taw{k#;FQgzdY5mX_9K z2YuCz-c;eLTXavCz z+-ozd8uFJ~gdjrdYlSZeF9gl!1GKF4xwrA|2G2Z88F>!1Qmx@Lq!pvYTa&~c7enj;H?IC(o~6B84UBZE^*9bht1yfsQ;bwIwb**1l&Il&RRw5+6apV(Cj?faE+dJnaBb+fr1NbR(DVTcbQ%XSP<{`f zm-qz{#1~Bcpe~BlY$5;VoyvQ9%?r zgzNY#TFd%hEdbiS54!k1C(oT*jb?=8=G4rhkDx9VB&IP$I)c7;gHvX5xNQfV+phR5 z(BjD}PKtDd6nDJ@!vV_$Ii0rht3O2HOq3A-Z_nV4bZ_7HZ>^5RCdTpqLolB=O3j65 zPtC`c+bF;ALD<%!&uYV8pX_@>m)|&-`RnN4U=)$vb5<*N@4t1r7v&fjZ!2OscX{=y z`;>={!N)WvatxP^kg>xgl?c$sS7q(l@oTVy5-&@-R_cw_sQAcK4;a74)11JuLw!e5 zPhbZ4uwgHylSoUHl}n%iC-!8V2cuJwVpjZ!d85Mq#OE)qU5P%@6CD?fh1T=sWn~nK z=>0+bh4Uv7ErE3}Zj4!%UI3RJ8zk1D8^EyuNm<#EX`Chjh;V}VnT(!@mxB%rCjvm` za;qu;4y!0WJ0)15cc3b8a8@wSLqkEKz}OJs2=^t0@+3I8xS)UniiO;Y2vM|Y#uGb* zpNSZNPP@!Hyk2;#ZlFoCH`jyoUq)siXP6l$0tfH$<1Dzgu=&a7j|GJgQg0IF07w9T zLgQI{3QAfk3K~+9>;MtcJLoJBFhUf%TiY-)YwUhcT~9hcfUT{RS*w@z^w8vRF(^O#UT!-N1)H$=t9P z!4HKEnhFX*$qeKL<8`I>L9Ilji%^^qzHRg7MKNnE#}L`W;8eDeRRn`-Jz{4eW!D{m9d?1_<1KKMq7nlY>;TV(d^ zEyJYCpU!RfuD$6a-~Qo-cy8PddNDaiIoo+zdHz?Ao-E9qgUlnw1BmZl6e`>x();fJ z{jT>cq5EHn`mhSG^@#eRu*U>$`|^&^EO2eE4kEjef(;Gn@>w3osdhQQTh?&*HA6Mq&=OpP!`1Wd|QPnb=2yad48I#`T6+(Al?* zbeqx^bLH$e#I9!lz|FYDG6A{FADLR}`HGtLK8((HTyin~WvX5n^Q}5W>=xE=`we>AC`jGtE7411t*J5$es}9DgBwSSR#JC86AoCbffDPGyR@(N zRi3}g?J4m>D87oRY@|5dj$=qdN+A5&N72sIN7qB^qaU8qK74*<_p1$am749tw`tEf zhDAr8&b5B#@#`07NZQ|rp;kZg4v5WL9Yc;7#5yXrq}Ii`$!wXBpZ4q54ak&sAwi`x z_-D+0oN4OBJ6z$|q|$`us=9im zq;_h0`rEtUhOa5NHh^_NFh{t9;NCySE&G`Oh}z8a^786pZ=X{eqD>19`M(1 z1=8B#G=^8^$O4-;r2qwso5{!&eBiLJQc(mEFhyw{d5ltG|2qdrQ=o^TqM%dIO;SJr zSmD>N^c1U>_dx3C2lI7`i8vYNuS!gsE@?o&u#3`KvkqLeecy)Co4TZBQ5OSo;wq;YSpgE%0tjoLThL}9C* zn)S}hPBw2EGga!k96sdcsvBtzdA{UZwl*6{FA#b*B>KKV^iJ>hNVcw&!n!wnb{pbq zv^0`*rNTwLG+LB(R^G~{iJHht*o62jiVrU&Y1Axp`nm@+*E*HyKPZl0nY-QkWX@`8 za*}naGHdSfp=MR92+20nw2Folfw3tM?@zI{y4p2D7wi`Ml+>&P$C%>Q@6~=&tgTgh zULHffdgXSw57+yq)&;9+W@J?CXA{4F{4}OafYc@<-VCM_#>Ynf= zT`=H>aS4(UFzZsbw%G~AbKPmC9hyU+-knSlVR;UhIosL)G|pui{cSnIYRWk%)9qtF z2L%P`=;}TIR7%t7_*N^;D`#y53J8>1y)s60YuDb$XeFAL8f9*5`;$0K0&%CnQ;A;E zE#4GSamdj^o3|d4qo~*_UR>B5LM|lwK|D4WR}<;rTv}c&>NC&$MGeq%F%fP{H;iYC zRN1ehzgxeDKm*zEFX&p`J%DDl0vHlddpUh7Tn zyU%zK*dEZ`AG{fKl-jzwH0WudEy5DQ_5R^vFgnR%e1|9`l0xs~s>4DYNhE(4@=ja{ z>mV&8act-w2uo074-ZL*xXocuIW$;PsH9)v9wW(2h$#RoKxDyaD}SN1(nqo4oAMPc z+z#Qp`5V9oiHS+U^1ci84$ff2+4L=#YOW^{3HTszx;`f|Bk1`H-suz*tpuhVAi!kp zATJl!7sqF;lJ>{JH(}b%27Ch34h~TE;7U4)QWFv?c~}RCGaj7|iFP9q2c(wMVTq`b z!c#(hNUD6K)VUIaCn#}<6c7>Mii|(p=%c!(jJxyqt5wVk*6X|zHhWMoAr~pGon#DJr!cr^!9dStrqUPEclJN`*r7` zm!7LlG;>x_GHnlRE)ivT<0Gl^cH*wZn~&5F@6gbyEo>R92J39YsS-1Fq?_#U+a2BO_nz&T{11k1=TR^7Fgm<%az6 z>RQSXKK{{4ta4JXG0%ziO>xwx7QDuS*D~wUJDhiJ1(Rs7{|m^I_B>Wb_7~$FrZhHh zG(@+=#&S`Qu;YFxi%6cWQ1^`2O7i#~B5~(G-B%tmi)-Q7lQf z^u99vMJU)!@mA5tb9|ReC1lwLC6{j@M5tf6t$Sf?FJRVJtyV8f_nteq{N6Zf;Xdb_ ziMXhhgH=3Al=SXXKx3GNC~0?<>sN%~lPWMv+|_a~s>P&f3| zw{Df(3c~&=hRqw&c%uG2zhEh99Q`+8Af{e2_dWSgk11_n9q1Z%}~jN>BvY zEXAnW8fQuJ5*5uF!e95XBG0jDtO~~i3(K`T^l!17NDhs7LtBFU9ulW9li%Fii_L=0 z^v2BCDraI=$IbsZXvmcbkU>%>aAFbp`u%l~)u3#ct)5=JmH;2fAI?Jq__jDoiMlhLvsfv z>T$|Pn{)K0I%-c83BN?@ar8mRaeKj@kCF%&%6U{zkb74j>20u}6ID4h(vYIEuLP>- zLX_3!=H?r>Y|+(4UXlx#U$iO$_Xd%6TKMG)DR!IzJV8McfM#GcS&cnnVgz8;IKrS%f=tCW{B=N{%PDd5ete{!fg za{K)JmTN3!x({D5<4N7vtT{fZk#U6STi@lEhC3t}MjGS`K6OgwatOcK7dBKWSYUZ{ z_|tx=yWF($!r!Anpy*9-N9S1T=H;n1@PB^r`>YY2c*&95r&X!INfZyW8`j3X-aIbZ zGd{Jnb+BVVC9;Y~Q_jwwgFE}iajSVczw_<4x}OV~G7s-n_jFj`rfd4}bZFaVNA|DJ z*)nZ0SU}BTk4O+r%7I9y__iIHRB^VC8*`gCBxSJwzGJm~Yt7EKd~Xxu9pe%+2L%Mk z1HyX{4K>uANSvb+XL0|1iIKmx(%B*M6w7g=gO87h9KXx&GGuj-Cy>2ExAh*dMx}|d z*+L1UCWptjmuKtjKc@EkyiDf(mVTX`Y3nHWm{ztKD_-ig!?(q4CwD%?BWTR)3g*}~ zqQK8>ZSUyRWpoM!prCKGWzWB z63ZAB8qfk#dQweggDis@-O^ot;l4e0-=8>*KY&3vH<#_Jm1&pKtu=MWENi2cLU}4W z^S;`;@7oX*7_>wsZK+t>`kA4eCsC$9+2>bq;V18+lb_`Z2*`78wMM${*B?nhev zxeojeI5#b@FfxV4%d6Fi2FW=kID@;O)qz817t~P_v}RkjgH`ZFEgOXlp(_ds#lTr1 ziYAd_r}3aE+Q!D~&zv~}7kL!;hQVzSkh_nLjyi{jvy;e79FSw5L!nrOt5O$;3IdRL z)Ya8DLXeD8ji^0PB%OzSiUh*K?MjFU^g~iG7ny?wAS1v)#FxI;t022KqH!I}e7A3t z!R6OfReu57hjo^^pl!hhyfMM>2o#A7B@}AN2cW^x3w~c`%V7uyJ)uITbhZT_L4gOV z;}mLR;;=VAX%~15fa{L1)HrlJEK5W zz-W~g+}Mwx-SmU(KQ8ni`r1Lv8MS;8s{(;vgY$leCff8x3U2e$Ny3UH4GqD3p_)(m zHL?}wuOH*&Jl#2fu59ZI@lQ~S9n?^@xu~r(BM*ERf8h(z7x(G#qjw$`G(fUwjH4m=jzhx5D1iOzrCIuqu%Z0 z#${2W`!#Hgy2iN4VRLwnNPWM(mvnjNi$%YEI;!fYsn@Bj?raZNvknM*2m<|EbK(&@ z$LgC_x!4K4+_%Ix)Ki^=5OPn+<>H5_KMamt&TtIBaxGv(a8$(TM4#N`$DwxGz7-n} zzvQEvoVwRi3#Sz9AGvNn+r@*ILQ+GPKE&N$ zV*PUHi;CIdS3W)mx2Dja-gkr#uHnw<ccYq5PVSL{3c z;tq-4gYDO9i@)`ZW{uYFW@C)6p9#_nI)pQWL3nj!Vw%-Mha90BJw>nen_HQ5`bI{> zotFAn8}VhWm`8eDJoSpZs>#A4-g?*gp-Q>e6OY4R=G1;WKs-W92!4yGyvn>=+S&!ni!~=!=zKmPwwD;sxi5t{0|daQbxHd!$;`(A zR1q|Z`K-h=f>FElfM(7?cw8f(LdHMf)PV0r9>+)nN!)^nmAFtCHm1<9sBP1N67!VM(Yp;rm2d z%0r?6oSUOpk3u)?j`5=7i{I6Qv@%!jA1Wbg_ghESjFAd$&hIXHkx^MLlAu+TLdK0XUa>57vI29w*dxj?)q1t`C9)JCuU zr>D{i$uW}g<%o9LfRa2)Y3yj)K=-M;obCBNvf8qE+7#_?^}U{@v}U}gZhms4r|5TF zG#h6HUlGsv;8uzCV&~4sn^&LU2t?MD^|b+SHy0~Y&57{*jpl4(LI+Qcw^>I`1P2Gx zOQ^KCX-J*>RlH#38sx`o(Y;bbMODyr*{rA|*lX>&Re9}|nR>o`%c^yMUp1zh9~kb) z;rl3S)6}=%Ff+aW{=jCeqr_vSzQ>2BByB<%bi>Pe6}B>ObuI2vyk7V?Amsqhfg^@i zY&&QB_0H#;Fgj$M4X01~sg>J!(`Wb-?LvBb|6VO6Pd>w~bge-ba~#jS>9C0r(k-6K z)DOe3YXJ)*9FVBfnAhF1{WYQ+ITN#1jL9cUK^(NBcZF4iiYix;p%(|FXF6j>0&I(R zJKl)hFMcuPsxtGjF6dWFO9E<%wq#aPOL+KC7gnxbO!B&GAsLNw^UY|e&!GQEmxOQr zp!$u)W1rirekJWZeCOkl>PQifo(XQ9A;03mf#ie9fnf)%E$b$3zy70R>>SXhCfIDZ zwZmacoLe9o3!7LjlNBaJVl?JH4^802y$cfvdDi%KMShcdVGp(f@h*cXKu^T~PWIf~ z!Bfc63;V6m1veTIj*yv|gM&tS6Ksr9`iC)D=6!+iR+y}z{C+(?(*k>CazogPw~ zp2F??x#aGNm~c4p_FY!gtp5vw?Pkm(rR{K~0sD2X3 zi2?*BNAL-&UL3vS3g}H6rH^(D% z>y(q4Ce?D`DzPRKVA=k@_NP$RR3*NxRbbD`YWiao z=Ue1ze|*EvuPUc@qY4KumH0I4tgJ@&#D~ZqDm$ZfUd5e|@W3J(3(jJMis6u1bCL?t$DnIx|rKD zJpYKn(s+{$zs7NeImi1y_pYIU?>l@rzYj{s#P`wj%O#;>a(n>pCcb!8uv*%P#Fb7V z{^XXW71qesJ~3K~NE63KT+QjpYX3?RulRM!0?!L0Qp!%gVN|2tb~emA@%}=`a|EHP zDGP%L#tQVn?Bs&8J~-V#g+(DP0&TJsXklbGeYLQcqn06ZWDey&G5^&9+`C6Oaw4Ar zMJ%rCl*KV!!->|wWZhvfI(Y+2^BBls?OOsv!p)H;tg$3EC+O4C9DP@gVH>~`u8@Lt zy%k0Ek!$}$XTpaph$~|?1=%x7FCOl$gq~s-(!IbtvWecKBb7A%WE6jcEF7p!2sI6t z2l5nb=C?92bwY(rcm+f|5i$2sjO%XO9K#ui{D4&;`bhkokV)%9q)HI~%{d71$@-k1 zypxcaSOjbeWvXk@Z^8?a!W|QNXJl`)tkEs;=@KKgHs;u%jqe6CCaw!?7lM}Uu&icU z^f6wJ5Pt-H2JitI1Q~yUScM1;On6nYjtWe~xENDdTWfKp%uU=l}VoutS% zb&~x;(??Sn3SKanMBe~eaJ)^VeGdN3rf2%I5*`mpmlbc&_ zr3MBt+iBambM6%9VL#X5nKdp;FIdx^HRMd5I_Cdr9)i21P&i?|iz2##y=7Si)8$r(Y%h z!Q~|nL7}uxv3pb2Gx15+lC_kT7uKb^KMgss;FJ~0aj37MJiqa9-20URndKGl44*gZ zz9rl4`*xL11IBd$oN@K>=GqS<8!J^m?!?_xQeL%ScvNcjxi1)Rw#sizVbTZ%Gd{`Clc$|t}ViHGpJLM9kRI9-Jr!t z)p~P-fPl$dI*v`OuWm6;u4D3OBF13#oxvvB3NVNdq&{KOaQcp?A(fGeQdybEpS73< zH+C@$eBd-80xCsB3kh$-@<2`X5;P_DTWPjs>r&|g3Lbc2*RfTAvp^j|(i7O&*-7+I zW22dj{S4CA&<~N(kK`Oj{2=1Q#I9Qq%~CXaSnJu*-Kf2;=(Pg5{DTRDFNFI{iYo*e z5b0QMg|>6;pw`u^)F27cH|t}E=jbIMZ6_=&Y_$I;FDfgD&KE7O8=FdCidxf3~aAfsGlU*$EQ z?ro1>D&+4j$(-&C{ZVvsXH8_Rj_wtf>d%siX$C{Ll3R3t)^7wvK^HbNm2?S}v^uZF zl74aN?910W>+H*`P60O`JoaYp#-Zsz3;Jt$U8dj$JnChc=iX-g3OQWgpGNT)|vOZp8q(j!oYDN_rZ0;!hw~BA%-uBe6&~`wMnmbYgGDZjD$!0ZQ)lkB1GHstBTgudeoF$``(7h zyQ`W%z7VVaeZ4}k+-_@4R>t)e$B9h6uouCJi$9)+glGdQC&U|Of$xSE4A=r0)dUXK z1;h>*DRVU5rmRjVzKN;9q&1TYx(?(~yo6L6p~O*n2jtQnmu!i$07<3SuOI7JH>om#n*;nc$#qRuTM79ohK;(M1Iz`Z&mI5` z_?bBZ@rS%>xki{mlMn~yJq&WXksbzxkG(OpYJ1+9LfL_1o{ECJiPv{e80fSl)WzXT zNC+jRb7*Ki6#CLF2)=u%+I{})SxP6j9-ZBl=p?mIL*^t<>V!*Ju4^sd@1$S(cQ_V> z={w#p^zJEq#8@mPS2Fr7h?m{2VmRn@&7Fb%-HVSq`v;1SPi|l3Dfz%HBBfvQ=%J&9 zS%!Cxz4&mtaZc=MM2ec6#-CQDFMO9qmWyk1R^GABY}x8*S!$xREcX2H?bUIW*LRPG z#l$cb@hfUP{jK)vSzuuDbbc2=hNtP4_jaz8>NDkK>Fhkp0;o5sO%tF4ukgr9@9Mv9 z_5!r=g|_%(flHD3^Iw~E7ie*FiKTKcaZyg5QHVcfOna;CE90I|w|40dNZ-paSan^* z=8sTq>4%S{s0v;C&T8$w^Y_5`2D zw|6C(Y(T=2KgvRZHvgC(Z@xx3e~m9(WCc1Q8t~xz1q3`JBRd3AWj(`M`=s9>1oGxKC3^udR78vc*WjD%L;qAo z(Cu`c_X)q%IV>_Yy+38U)t=%F&9`sfR9xOQy|NeO`l;9D`$nxIls?t;G{nu$>w2Y} zD3_KAoSNqXsB=Y5BS_+edGBVIrTQU;PUthsv6-86Er+Nn=-Hl66Z>2$KXbVIcz^2EtqZcBmP)yG z>z?)R-LvSW+u*^$-jU+CzgOqd^RTRFnv_6BFlyUM(IW`NUA`AvgSsoNP(v-PrM(clkIYImtvbN-dI|8CVMP zI}m9d5UC4#l4#1IeEbHjJaI05-CWqg4%&~5b0Og&1RVi>O6Vq%s!#OMm=l{fqK9ry z5P4W+_yseVQbVRlsCmd>BIwR_Ad-PFIIJvRDm5+b9CE-&cZm>AR7A?xQ(uYVFQghZ z9Y&wF9$3s-%n7ruhUAqIF#6K4&A-MHm%bHxthh>zJEC`{$14kmHB}ggn|iScC6(%> z7%0$-JNziuII|?5q0e z>_kAr2k-6pqw@TrJiD~zlfZDbsR%6`nF=U%c3`yM{C5E63fPmZ-mry~joe0`O1Hj; zyAF16#ca<^B!=G~X|D=?D32k-ppVHsaG)@K5L6t%|KKyrMaTrojF1;Es3|DN(e4&b zKYjMh16D_TUX8O5L0gvy3k#DO3k73pAN8p`bddAx43ohtXu0uQ?FAk{U<3yD)!c%1 z;aa-UcBm+9ei6JJ`iU`kF(Rg!hj^qtp+=32*SbJw8ubePOLXWQz>md;;Grg}6_Cqy z``e*R!Rxu7lq!Jfs9Q0`0=@(IOPrx0!R~Gn0pO5|m^hd2HMFz<3kmxNwHmk!4>S~V zmEf|$4|ENL0t-hMCdg3y%x|R`-B6R$Iwu!wQF;)lOxFO&!Ua^rpx(HpWS~ma{EHwdflqWZH*qOBfHh2 z4sJOhbNYJRdk@Y7-}53mIF_>a7)NJ+U6>M=kh7?wa;>H zs;kR$^uB=@L&6og?ka$0nwdwDjQvKXLLGR)O{drc|E%Y-R)$gzzx0Ce0(<$|T7^0< zo>t!=R{Dc8aDT_X>R8Ijy^nwG`FkR3E%VL5XD_(_vYg$kgLx)`P@HzBgEx4bvVTwX zT@0q8BC73xw3JL2ms5eIB=He%nCMB7%tr%p(8g6%uDi$p6~vDhL%8!vUP= zc6DBT)9Up$>Y? z@WsL=N!lz&_xDA_fbIKrIs^}1+~Rg0d|l_xpReb!07dSxi%w)78a=PROGt1`OzcSL9KE2V+ z-djv9&46oX2CCvLKk$#5_Tq?0Cu4O0jGJLFD-vP7J2LG5;%3JK$E&NhG=c=|m*F%n zco19zfbe9IxGZ0tOEiAF_()KmkYU7 z5Y;B%dYf2K5Yii9c|`IAu#R)5Pg4VD=c!hCeU%H*k;RbgxbD(Muc&2N4aqv0@lJI! zx$aw{QW9G!WFudawBBgt?}YStHqrb8JGMC1%7Y7gi}2yxZDT;3&;dV=6O6kIFV3PO zXTG!ON)k;FrvQP*JL~87kb#YcVGU&zQV#n_re{Gxge8CgW;HZ|V*_~fgvD@va)Gj6zV zHRaKxu|u)je9NOVHVH(%N>;Ky!^vZXLu)!3NWe3Ud(=mI^^Yt69+4b2{+AA@qPAtN zKtV{k+4io+@TZ$m(`qL0BXjMQ!B3x7O})TDl!)VQ#GLXgR19@irPXpx#)sLf@t4CX z6lmq=(&t(M6MyOGOXKstu-6sArkS$#_AY80dd2c~(E+QivRE9fq-<32Y59pmzh`c~ zZzTd6_1>oQc(ynP%O)E8Z1Sf?x1cWh&oAa+kjDojj+B(f_&pwf-c4(}*7Ztf`_HSC z(F4=PE#DtW=_xh-aFRz3+r`pd(N}S&O~Hgxt%D0W4bmYCrSCQm|CGkU6HSz#WvV^D z02`m{*Gg7BMmY(SXP%4&%?h(EE-!~4%KNzv96g%!Y-l3(`?|*t)s&4e|LcC8%FR># zs4pKQ-jpPLGllMI<1F^~{_nGo5rxFLygV_mIJYGm6co?TOaG2+@-M|@dROOTGPhP) znrg`O)ecu}b=_tE^M?gq_u$wbN&2Oric50kGY8Ic@@V1ymz>5_UoS{h<51e}CJ$v? z2M{H}O#Wpyv54N=4+F>DvA%)~+ zzPuVM&1EfQqlTej1PE_`%+&%eooLDsXsTC{XYdAr3a8N!{~NN2f`(J?TYdc+N-L6= zYr4r;R%E^)g^wJcm6aU2Gp~V;ljvbEW>sUh=ml(NhSyW{f5fea-iu$7q-*{2Ibf4v z&j!94fs|BGyTIAL4;CRQB(GDwENdy;_}}L%KbLMX;ngWQ8SwH?d0y?y{mdlSN9dB)}^IAu0C_Nrv}9fzM~7o z0NQZFBwS0q@izGoy8p2XfqXJ;_SesyJj!n#!p21Y3xUfIU<7&VNPL>WkVrlP+@jLI z7eS?YVvr#gLrI^;h&XZUqAhdx z_m_L?NYbV;a~TE+Fi%0RMo>+Ux7BmEV8QO*{0Q-@(QFPKpX^kw#!LV2I@4=x3i2#k zGh@=G^Yig@kKMaBQ0MD0Y^T6)%E*3XZiCa0XU~*7w(Otm*!A%2)dWNBNr~bo7h8l5 ze$g5T#^e%u%FCAtZb1OYR8pR~1v%uajih0v^V(1_Nd-um6v^_wdKM?f=J5Q5p&%dn7F(Dk?J} zB9$l=l3hl!M@A)CMKok3E0U}-lRZjBRFoZASsB^m_dM#l?)(0J{(|56xF6Tu!g;>m z$NM;5ujlJIe7EJEcY#qd?t#)ZtIwZ51BuX!x5RN>iEVORMi#mNG#D>bjFz2gj#Pwu z@Yh2E>kZI^Aic#VxT?1HPv6^JEggizIIXx4*VTJ5l0x=zmkp1Z6~R&j<|ve|E^jwA z)I8;LM&<@=qsVJDD>uukfGua5%P-0G`JH+YwK;NIPOInh%4(%gk5e^RWUmE>1S%*l zIkooI&qYN%P@jD5YP0lGjJn7|C~VOB()5Czsq?{t`>`h~tU2Da4pB@4d1)x8mvT5I z&w93MUh^J(6}l=r-f~w zKZ~EYkYlF&8dcaM;>@M&How*8)u0R2fv2&%^1m%Q+5PT3v$w$GH<$YgI<%o2v2Cxf zRrz_XbZ5S?+SXR|lI4nlbJUNuH1;>n*GAKHWj0>y(@OJ8l^GnDw$`^kMk7~1JKivK zzNMUt!rZCCcncMS;~{RDpFbzpCVybeSf^Y1upgv%#aK^z7s zoDlQ@1dv$}O3%C|8IO5Vw`gajV^=Ygyc z*e*u3@G7F~V&5j%sUL`@DyxfCr}b6jmtI+Lo4t!GIP%`gF3m*+D zkM>r}9an$lT3Er>yjR3)>GzKGEM7s8%4xQ7p0sna3WHT_lrz!&MXobPT9nm%riOe~ zKSlV4UVOLoFt@+-8`E86KI)_xsktxGht@{7u#GP^PbzsVjP2+6>HG1x@{Kn+-}eXJ zW;gV@Dao%Z>ABwNlAhP?gqU0_1#U*62=kK0=H1GOxpk;Ju1r((3}uEDQ%Gx@iuUR{ zjs2kmqhtGS9{pg^!ZA+EeOT zb`xUt_e;w)9P?`EDM#~%YrYiXTL->Ai@Oz9>Z#%~Jr%h3$dPb&P5oI9;>iv5;SqIp zBxn;B9F#STFpIo_KSUWH9_J3e2)Zm{=?D)B=_`|TP%4s%2Naz2m~5j_LP*g7YcAhP za%cd^Ai0s0t3^d}_;rOon9dAAa!~(3B^3vO6tUEXJ{mQ}b=az*cuo17y2f3Kl{NGV z4!UR101amgmGkin@<5{v+OXW<(D-6pc%eSjYb5F*QrhK2y$0Dt&Z6>U@KeP9oDm`! z--V;Gj$TAqm}N-)8CjTL>1UT;GkE&2$Z&_{?7rA5*0k?+(^pU(TQ54F(~qTgfL^Uk zdgM0CeN%DnBVjMNijGF^@c3}4VHFiEV3~vPs|LvQOTY)d~QS!M&p(rKzce>$s$`Ouw0VX{Bf~Q*& zgGzts&+_zNIHjqE$EsxA)zA>G9jBV&E%d-u~Sf}>)&avT-UuO2vP}pP)0&{D<8pD!w zm6#~MBnR2U8$zmn)Gde_4j!?I@cF*%`ne|x>Uz|Z4Ns;`?l{yOS!!pRO)c6f6t1PP zBj9N6k{~s{m-F-uY^0gf(Jy+>78*o54`yhxVi0+hM=|cn2b%?rBY}Boa{kvSR9!yJ zhC`X(_V)xjbKl$|-YsfYe;m^pHu`gB{&O=vl`;9}tSsm3p60s0i94{T{%*<0T_1zW z>*D)+^5&%3!`RvTzNKz_66CP0zIJf_DRtkc7iz(?$4AdX7f(44u#8BI?`%{Mz0|TV zH!lxL?PYLJ?97H*>IOv5faeHELwuc3x|PB*D+8wOF!Ul4T<7tgwNT&itz3qlo}@QG z_3m4h34sZ6dWc{>!^!fJ-m`4U+npcnmpI_y#N7HX7ht;JW6Z<1ZZoANt(>&|jl1bMI`%wq zv^ge$rTtK{adVZ$Z@ODzmzoZyoR#J-Yxj79=xW-_E+i!-`{`>Q*P8t~6Lsj=+i?e$lj3w-k{dkp z-_x7T+H#SG)HMTd3v&pq=0NnEMg)?>jZ;m@%4^vXO8W!BS zhScqCCVyXMw&q$N6@}k#wMR81~ zb5ADQdlgZZDlwc^K=GrKYQ>;AS^T$}}aqBk~MV`E$QA1f%tau+Nm7%$%K;sBPh zNmy82x&|7}@+!I7u^9*Fi9SYP9{|oT_VxFd6c_U~PUhn`gggR4@70tDuIQkJ{cu2l z#oUXJwf?}fYS&UQm_{NE6|kMLAtrYARp10+wn%w18%8|NQJuTGy0)wf^z}{RTK`MG zt~8Y1#uHoV$@$j>1=pwNYTFx)O?%pl_86PpyQ}?X#$#Zqoz>n$QOnU7ffD^Kt8!$H z4i2nG8Ugj-j?w*g3nts(XZAeb!G}{^LdxQE`H_rrCdzTmMhd08%2Uv`{f_cA*fhS9 z==@P9^>HB1XO*yDSvnVLCe_N3rIBZQa#WDVJI~`Six=(WLUOj2 zZTMd6Ks@jV8ED885B4|-UPrGEcn_Kx^TwLw0K^VZQE*X;D=HEm8(_K^J>sLVJPe@> zB*7PUFgSVE!{Waj{S1YI1x6$_TDy0b~DM%5K zFa_a$AoNSbAcW8kz(ru>#;z(Qur=- zVenNO?^q!HCnH0{;|Z!Plw#7eVuuABN8;dMxpA|osA$@H9Fj;Qm@V$lbU;uO-WZ9j zPPf)?T z9jUj`8cWUSM&+-iP)7JS{hGhH(!9U1)@J@eyWC2;T#XBr)5kJ8zts388PDha44N|w z8p&9@X3dqGt(1qUAi?D#}wnyccf{N=-X)a>r8Up5pLx?|U1!=9>wpi#n%t7bXV8i(fZ;Dq`7r6EHv0@YX{lh&;@l^<3YM(QOtGU~W2vh7~l;%a`9Y%|0Qd7;MWp5RrD> za?n-t@a$B(XIB(nJN5Ek7Fg?V(E4+f1{=c{&IkY&WNcLcxNTFW?+8mup4IiW8SPZE z|9LNK^K;-pkdTBvbH9996ayOhJbJoftlClvOokY&5hWx0}W;lk>L6m^@PX^+VLg}fi$Hy0;muv0n$m&B!W zL$hS^O_Vt;TO0%%Rdv)FhD%?zuQ)%DU$L*iZ1-6m2P%s0q_lT@InCE5dZk}wEhAD_ zQ)m;LvvY3NcH2FiweXDWf606QvtSp>!=nr1lwG@6Y+thaDfXQ|%Uxf;EB&XjS2ag? z?yw!>PJL-A3LS^o#zyxyfvvN}nQ?1Vb?tp_dsF>nXN$|Ys8-<2v21BT`|-KF)n~JZ ze0PLOkG=mI6CnJcCJNRh%rC99SRQa&0pg@i*7{P)`!0uh)SpS?Tp)MLV)^f4M;}@z z>lvdDu_vPCM#Pz@Xr}d>zcytCRE8G?zSlnWR4Y8p_G4Nj#Y1~%F<^|FOW;r6|I7$< z#Nl=_sE6Pkxh8DEYJjWdGO3g8LGOB2{08s?UzeqII~H4Zo~!G`RRMX31~I8$5!kVrGqnSF?wDvzguRVUI1=s41uAin99C(sWmlItV{epHYC7O8o^e?_VGM!VJ1`l-b4=SGjg@3!P4< z9B61ya$zx~T!q6m$rFTPzyJd?mkmZh6OG`5oOdoKE34w(_A@%w0EfhtH?E*)Jw34; z_AJD$pGVsH&WH;h5Ls>f1ujAio&~t7l>-g*PhjiPL16*P1S=Z7VpLhcSU0x$$3MP) z+ZsWRuA|kgI3=rJ+y-j?KtyjfI#WC-rC|NzZG8B03C1dUtHmgff!^YK!l>+ghUr#R zdv8x@ob*a;*Q6XM9}uN!nUJ5DQz7^TfqdGMxtZ?^_B6NWn|Yp|8VuU|eWy)V(`Gkc z7ka(d@Sd=h!h8tpgG%ShRvc>G-#iBjU#J}MtGE$oS#Ns$OnZ&6uq2DKo?p`CP?=*&gWs}y z_Tm9Mx+G?GuiW;B&MhiRp|&*Z%a?r*^UcSoWI*c|%F|TD7`lg9hG_n}^hGI$rW(avp=9 zOm&ROW~oisQZI*w`~3toL+y8$#Ld5Wy9_w8?QuI8XtP7`*+F%^O;x*;W}ZILQsH1azJ=tmQCD4qGnE};_7fu5)L?Jx!2(imq(OV zi$ux!6*cUe`S~_$s zAcq}T_9P^{*G}w#5mQyICinx+Del_sUk(sy{P4ZgpNm=XBz0GMC}MiKDgp3p<5+17Z^eL zYnKVF!bppTg7(9&R%T~)b#>=Rz^vW78vPTEDF1y0j_KsAcq*+fHf{yeNie%K4hS!^##1&4*u@DNUh#IE^)!MZA+ur?c^_5wP&v_IQ%x3 z*G@YX+#?WM(7NW=T>8b|rS0yoWpZwDHXxa8wMZr9>VWo65pfFxMoF``(N_lcUP!s4 z|0+4WDc5!9h{-zEw6<#%&zycp`QRv7uYd2D5xi8k}vZ+V1_jS$@c{}?)^MG}FRz-r;l!sX_Z=6wkmv$*y7|+_) z$>$~;53vhC=MNrOELMlo&qVMiQNMT3F>82Wfrx=(E@+zv2mvClXvI7cd7x@C;?7v zfY?q@-+Lv`9j7vO%uqm#?KSqXdeH45G9lazB)0UyKJw9!D6$I_}N{C5G*e#Hl^9BYQ9W7yDVY$wu{HeNm*Fj&2!{Cp& z&A}o9C73By0`x($D+L2?;*0bp*+fPO4A^UEA!ETDPXZ+5d5o3EoD-CJI)>>IeY;VHJ9uO?DCj%j4CaGayiIC7!RKtKb z?)2W40eyiTgbziG>42;*110B3&V7rmwGHyyFhd^Tw0Ih)K>UG48a7jBe zK(2Qa%g#CAE<)@F+g@{mOF=^+Uk#SP4gvYM5BFO0V89muH8-*1Lt`UDLYXN!3?{d7*9@C{m>;c*y#npKP{%xZ7-<;K}40V8D?vuXiCmCG*slez0t|9F6paX9o&| z(vp~t+FOJ?jj%S5Hw}xgQW29V)0#*>^nQHc1i`NGbe`>ISV9$<}t)mZR^R%!g3L zpnz9Ml>pbK5}Z@yE$t{|UuHa{AqSIFj7)B^i7f-<2}Z_sz}=WL69kEzJrIh%)u0f9 z8+0`^u-qf=t{yYpte{(UV&##bacnx?D49ADDGn4|3^Vp$`1h0pXgKieawB#&Hs1g- zU}dm`xe1Yj0%%{r%{!MdAzKYf2C}!2kuDwGjqFL!x!Z*<<16A0nGERxXwo3G4{#ys{kPD9`gqgHb zpzJ@=`OC=QL>w?PicAG?ad2E1;ABOS8Vl08OO4sZi>s8Cl}8^$=qKyu?IKyt5MUGA3~6rBSd!?0I{3K}L)ZfiBZm+ZO!#8> z5V!@(;NF}!Fn&(CI+0{wZM z7&^CK2-KN<`}TF97$SF?xCel?tN@Z3&cO<-!jGVUNv8!s5e#htard#Y=?`tnwpv53 zhoj}+;o;@gK?)!dJw?vobm0VqX2cQvJP_Dj$=3B3i`U z;mak{Vw*N|d;1oaVrz{aZqBLg2gju_q-Gg~KUif5cY5RG$u;6!+y-e&Z=C@Rw6APao|{s@}$TcqZmogW`k?aG6b*@9klviP7SHs(IqXu2<;NgpG{4>Last z1N{8t3DL#)dG*QT87mfaaVCd=a%m#8vh-7ZWDe@72J-FaX3Q@rYVATxI39yyYb}lZ zn=3mf*Vm^9`*k(An@wq_hP=8%YEN6WW5>!$*^6fLxxal=Vz@zbxOe-JVeycn>qXI1 zF;uM|#5dHk4`{Yr>zmVk?Yvs!SZW4?S08N$xKK}kqGv<4nxc7XwHT^c(uyXXe71o& zirO02Oo?KJkYGDp z?T6no#DDO{jg+ay|W^n5p$|iFk9IR3f@gD04fSO&%nKk1zi0n=KCHqTwbW0raIe=#OA_T zvnBN>9T05%&Mt5Kx?ADqwTz4!8Xy6(&Pw_J{p6~RH6M0(9eXuA#Khscq;IJMa9wGv zHCk&$%fxHxp&Y9GIPOqD&*`Z(s1^cpeW>Rhb2&1<1L=VDd*Nq`eizCC0(#?V z6a!)jdqsiTgLEB5ERxtcmZP{ugN7hNHMc0x;l`s9(=9X@uwW>~U?!sUykmxYzjCyC ztBFAbg>{mlK704fjS zam6`60bdscVia#BZ{BRvZpMjX1oawm9)oR|M$RQYQoaFzDFwAo)OF&*=j4-X0FA;3 zxzhtq@;K8;r2@_}9nJvkRp(*DM*M&Ahl>2c&JnjP5gtcHA$j8%>pt9P!;efRKiEAi zqmbu^9f7a|nUW9P9**ZSUe9?AI_>}Rn>?5s z-9;2{r;qHrFnii3+zI3kHObnk=^iW9F}tC$JQKua=GH_3sXCf(mVPR(qt^VC= zeUmI_roO+P{{5~;sL*vL<(=iHIAbcZ zReRT*;S$~AeowAymJ=VJ8i5DFDTEQA)>Yt>P|#GF!Tq=4I-h_FCa4 zg`<@{a(U6G37?R|vjQi8lIrT8&*n(oiS*X(FZCdjsDoG`jYk_?dSwG;d2n=td+!){ zl>N&E7{DDAm5^Bcx&KpUq{_yDDavq|7Q^Y136KhqfeC-_H!pL1BYVaboR=Z32izd1<8MNB z#WA|0pG+_Q`!YF7C^5XB4snAK)x&2zlBQd8&snUpcYdKjd)evj>-~&{r0jJ*=PAo= zZT3*eQroR?W=e8!yPF7?LfR$nx|BeP@GtCw_^uNyIGxyR;dV8L&3?HR9NS3?_CK%c zNCY>`%U*ogII~+B%<4P*{nDGCY6Xa8uDX6_-T9iOqL1bA73mMGq;B)bnjTt{;9ni$ zDeHpA>nZ{v$xrpu%#7|naMyV`{4x85 zy1opioNsRmU4Jaq$T(Zfo6KTTm!&C{^fc+QnX)G3Nl4%!#r6)IWQ4b@?}o`7E7}SI zPyWwK@UHHZ^%Yy^_J(UEQzI>Hx#orOUH9^LY2O;%EYGluDsp})^dWY%Y>m4c$|eT( ziatFT=jgTJdGkP{xab3DKLmf@!l?9e?UGAluXU?M7VPy9j(_0Vhx%{?|SFp_-R#EY# zbVRn>1gpd+Cu+ab)k!-wAb9c_c;%lyCY{zu$SjBr$pmA%Vs)l1%l&)zOuAW^`F5rq zKY8fTivIqKz5xq(p6vxTm#@O_7;YTdD6{!6q}fWY=BiIZVs)RK%1UNGnXJVq8+}ZZ z{c0u$g>TEO3@|CRu?fE<2Bhp!bC>2g@u z!5;PYcdG>Nf1b(P#K?H~7TmS8K+^KB5NjCX)Hcda)!$P#QeDB_b@R7i;6~32a+YF; z3~1zimtX$Aq^~%^#OS9e2$l=o(5J+po}55fkllpMTcp%qF765c-Ms%k?u>O9KyWR) ztWv+<*SD|Nv?+EdrRRB*@ZAS9ll9knk12$l5_yzg-TZDLXrsHx2BUmlojl=6KK|Ja zz~QEGoLWaqyJU_7Sdb%o+yA^Y&P>P(?lP)N(Rb39b}}?}+Uaz-YP;R(Tk?t%o;$Qt z3p^3`*PNc7ZWdDWFRk>??x3}yqRM@J&k=h`hqvR#`dB&wQHd!bA@&r0{=+ze+V7&s z3QIfl;t3-Py#Iau=^s-36bd3ZlLYmpJ!AK-5nEkpNXI{W5dhmEDB;o?VMOs0JyWAT z^b-I3s^*YqNH-t7)vOmja8kzI-dakYVZA_9zj878`u-M7!^%+QR?%@wYW1RUO#FWc z&3NR6X0LROf#A={Io#4|IEjs6+u`1s;B+=GUk?tj_0>3b8NUfZ-bWH19qV8m<^S`F z&3v;^evK6z&1{_)_74nHRF0>|kI(y(tJQsu6l`c*oZvirb`395m1o(I7;87wp#_5SZCk5#50u3XCNobuKd ziP96dJ+KuwiuKQr4*t~}?OBK*4qah+h7bYn9$Ep+7|p=Va0{_#h#eaSk>vzdLfHC; zXJK1#$e(ZJb()?nvriFJuPTV=mwzd0ZgzGHPAGo?(Yb+AAZCI=t8G4u@v-p#$OU-aJ-w!*K1?Vn!@h#^YjBoj(x1W&@>m*8&Lot*FM z>SEeu`u?E~9H=;mK;_R%>(R-S(;Aur>))zKD!Y{b_iHp!x#OuYa|G6z1W##QOz6i# zYW{hTT0HD*;@my%d-)UWoL=dL)SPJh@B6BbU(F(T9U zxYTn>=B<-m3bk`Ghckb+7(I<2zsLHYTN=Ad9ViF^`y2ovd-mXrL?MXsZl2kO{^r$26SrZ+ks8W7*Yv zx#nBS^|5@*RDZP?QZpWBD7NCC6}NC4CD{+C@AgeWiAHWk(SNo|4VTYN$#!43QUG$_-{sl^TPOZB76WVvQPdo_KPT7{omlmIr zA{ccqGKq!)>U$WJX7w!b37=1B-rZql`ot`LbD}X?BNKtGn~mcWwj^rZ*-)XK93TET zDe0j|zul(4w{o@)$fIgLzJ2TnJJ{GqNXCFpj#aaSHHHjvh&zTs%Gf_2MdJaFqnE{G zI_IE_!A-mLs?mklk7O72YSo8%(BKTZnV)aEDbDms;OuXXJ@pMwHEIhI5*~THzPanf zi8BH2(sZn<(lAW1W@PRNIL6y9ud1Rn#36a1u7wMlxNW0(et%ctHZ_#w_CN`293ikd zhH!XMJ`CIs)QA1EOsgaGw=P`z^{#yX#xjqHo)L%H<2i7UoeHVZdf-PB^CMtGp^u8d z3G3z(S966;cPCFpS#q%NwMyOAe?-4a&zR=YIT@c@KaO3v)WEiuZtZ67e|~OXVmi?D zVgSjL&S+S795&m&9U^s8=#_F!&^7@@>2AZgBqSvz<<@d}XTg4(-=}!$g{mUcC6FEX5^a z`KGEG^CwAK8J~N1KD#vQyz%GnBDRlddj++U`2Jx@Dk^p#g1c)Ee*8L6DQxcW^>5Ym zrwS1BRSk;w7c*~oYW>=(J?}-nxOr&L%Rkq=ee*>7f-ozZY2}?=ega zvXHbz>*=|459f34|2&;qwduj7`m{A4T0O_k9HtGKy~p6%-T+P zoQbOc=RB^CGwreZoXtBiZ_$;?l%^h@v#>pX>5p|DB)WZK58#Fudf8XGpRCn;riQwSlZyvepS4lV; z(UsjYF^;G#G_}}Py*~D2ywJB$Re^Gb%$GZJ#^>UqrNS@Wdn(ei;QDCpNX?AMh}|V= z^kjK^&0vlUymWc`Rranj^CJz28{?4RcZFofRT9bzWa}C)bWTKbMy(9EvhVVA`AYN+!Xwh%{rW7#nM=X*^Nnwz?t%K!?bdzM zk|r$vs!tvSuZnaHROp*=j`Z^lapL-Wv150(VfqX6nNmZXo0l^H0Dk%LZwc3D5eKGg zUO;1$UVi9kLsnx)9Hts)+QO>Kwz3-Sa~kNi>um8d`_v=yLgd{6E!Tz*&BdjuTO9|b z{L$^cm0I`rbJVe_3#|X180}&1i0;T3Uels@yW26H3^n+_m+BF?@IVe*x>00*Lp`F?{ogC4)%;a) zCg?eB;@qQx`G%4@lbZ$O^E#UaD^W^Fe)7jn~PNC7|sW6%tgYRSi z3fCr1f}RuJ2`Zd2tfGwJ-Tj@_<=#c0Ia!LxagILR*nfQ0n)Glwa!aF8yk9I?$%^&$(1coFz2dv5us%moOuOIAiSJ zvS>m))1mM2NI&;j;+$mK+$nWSZmidrvZrquw7&dO%rdZe>%xLnVloShZ}o%laLLHl zEr#QhGXL|=FE*rv4WkW_7>4nM4Mwv0)&E_nNoqmYD(3qWZuv1IzskE3+9eEbf z1xEM$Akc>0nF4$hjy;kNF2smi44Iq6wGuRBIFZ08KeC|9BLf4PEyA`~qIaCQ^1v{O zoHz%5?Y|F>`^^>fJeKR(*qm*^LvI9{sIBwg46wMr zz=4{5;+s7p9d~qz8vGY-j4=QowA#o zG`tXw=h>P-!(R;hs-9f@4M3Qicud~EUtLIxIu0o#UKnJRz(}gP+Ott4;mH#sxF&3O z$fH4ZCh@tksBLytc|sgTyybT9-i`SV6@|Fvq8TSv?-*zRMHKnm3vUBr=?NGAwXiHC z{M5rw-{|)r92*}eBx=~?5Zx$%doobIFL-iw*1HcMlwc+e=hE#m@MfZLb8}O!K6$c* zT+KmuOvDr%9VN^^CsAQcjnV5F(?_^=guqG+o#C}WQcDS~1w%X~gqDElvRE?U(vDh- zDE=@~lo#4i*3hs4Dwc}u+IR2D$czN~2|qM{y}w@n?{4p&2LlmPag*wMv=|A==70}mCeLv0 zozy?jXkDY?;lu1jsb3pBCTpL4YIgc*vk$z;w(EQ=iM<{b9TkD#htYmiJ_GYQ`Q=Am1-3qp*K!sMnsYJ|du*}eA*Zi#;g;KdB6bmX zf88-6>)oRHrgO*yMPzdA>ff(ix2rZuY?4{Kdn?^l@0f$ZqY(|CiUZYmyc>QpuU1_x zIQ=AyiGNe=TPf8%*3V<@nvxC1OTomG!>eTM;1KtGX02UG7hJZQ*^!}5Ej3}y7+*bpnIAol@ zg&WNJHXm7UQ0Wu_Az`;6Jk13z zyU8G)OcK7AdV5#O+4nJ+e@XVn<3RFF;YdK19^8^?!1lrq$5*~m5uvB2C-0X`l!H|m3I0V+O zf?3)3?rv{bLY@bwg!eV!C-L*oN}}m0Zf$*Sa`0g34sGqmyZpg@FPc_7_`3o3Sv*d7 zzG>EIu4LFCl*cT`?q|MUn@gK^pV5TOT+mlB*G{e-F5WKs)$e*&%DAvmK3?g#aKS=b z@HgA07A~7b%WcVh{hK;0rOi1bm?euW-i@5jJ}-DyJmvbgHK&Z;^lewFRPhbErn$z5 zj#G;Jlc%lE^@HD|^a`@mN^&R>ftqsMl(Bir@u84D;1Hu86Muk z$|cWz*ZT4gTN+DSyEV^yer16XSAs5rxSm0`4(Y@h*Gs6m*!J7TzxTjRCWJA-dfs44 zzHz&=1aodE?=WLMm9JVpw~j~pv}Gy@FNSH*=|Z>tFag|R_vI!Aq=Z2O4x@MA$kz;N zxy9*C9*Z+(z{xl;yYfl?*N{k5=p~?@se7`)UgNH;X9yo#;0F_q1l=!c$C-#PmcF|C_~wN9AR(> z&1F4v7$PHD1b_Z99(ZvC)P>j(Q?~IY0P|H{gd8JPP0&0mf@EL7rcn8%fCD z@Xu<2DGFgda6j0o|L7Y+QFqZ%eH+1F zeQqvhA+mlcI_5Nnuzo3=#X4LE$uhH9rx0I_VBi#I%4JpMb z(b}c$PJ2(Bll5uLmJ_u-SoekMN68nfJ(I(FUK^=h-$_*$g&-a`cOKLn0yt; zc6TBpg=Cg6RSc_OLt(b#gfA5ZW>yb8u*e1vVmB_C6e4N_+wRq{^wQ~TA33|+rDu)>r?%1*)Co8ClTFKIcK865H%rr5KC0U0= zQU|qKXdCt4+hvX5Fh;9ynF;Oq0aqOQ>$`(sj$Cuc>CgTk=oz|GvDmuZQQG6)N_AnY zm*%ZJ)XMjUoiHWQvUT_wmYmjZW8e0u%xvp({j6U;Z+;c4k5%&vSN45P=;eyb-xB@d zQ)yG>7KB^eP~AJoV#gpvp>n{pMW z8nG%!;0gV+*B0`?WTP7Ha$!8o%zF^%n@|LAjeO4nRXJ}gBw={>l{4K+yG6qBKKhWB+=lUAZnYntRq6RlcI(%%GgFFNlKX#M z+q5Msx$mtUEcB_%Dv#1qZY<|187-Sr{Zf?b2Pv3*DR+D#%hKhAv-ZrC19}Qf_*tRm zfs#wn)D(|Jt4*<&5}D^D?S|MRC8q?xOm}~1s^rmNO?xYK$lGOREaBy8g116frvUP~ z0g^01SHyIhRaqZg?j($2pq0NkMHWB_HEih^F8 z7%oxx_U)VY+Hb;Qh`EDGk;w45f{_U8!jh29YL`daWafd%Qm%rOMJTZ*HhF&lf(h>z z>FFOa=RVsrG!$iNZ7p~FPgSqtFu6$5KaHR=S#qarr4}o8b8!>T)A=nD1i0Vrctd2eLL=W z5RTXP;o@sR8#sj1Y7`=Qls){SqUpy+*+E~ zhH%hRqZcZMmlEXMCc3$osQ^11KYqLz8q_;cQMa)r#-Xn7`}~v%1Ue0j!=bbP4w4VV zzGYYp#E1c0)qCHvzyKklO@!NcA)t`(i_kfOxV!=aX|#!gFNXrauP5e@h!_JC&K{a) zaM7=0UE+)he)!P0qvL!e5@w(uN`LwC-egv0=5=6(glL5Q&9Jlldh!sqJK`Nnxk}1e zTnHTcH;^id(*H81<i!Z7GRvcJICDrx0KxcwKko`SVkAgmFVOtl!<2n2F;bL zdTtxbIaR+`Q7&e*)_Qt&tyNS$|L#rP9&_1Ums*5B?@ILnTqdvi?3Z~J{czQ$i5~{i zhg~V4B0cC?)jKmDwMR{G~@9FYbMC;dA_E z6s!ED%Pxxva_!&$Ey>7mMd+o4<4`TDAoLQOJgsIAt|))&sByISVY6sMvFhbJouUWOc?vV zz9GNquMwh2CUiL)yMFP{Y#%Pyt?FLGSnr{c=oMf4*pgfFfX>_qJxyD_2XdXFOYX)i z{xCLED0)I=c~;VGA`Gm^TsDW*tZ}8IKf9p|kEI4H+~AaPQ4Pg zc9}7W`qa;x^T?h`+l#83BX=!_QK!U(_Z5D-H=L7mzO3C?H8m_FG_qfbLf1GV=~`Bj zVb?wOXi&4ex3}wRK^F!wx z6*;S@>W9(;+H`EP8E^8?=&{Mguc7e$bX;vO__i&XhSFAWwB-63qXfBq^LLYzIp7Et z=%Uu>IdQgoW3id=iiw4SQ$y1aVG0Qn)nhQ%iEQWT?32XAF3_8ZA_jc71p01BEC@fH zsFq=kEGoY6%6ThHCP?)N$4AU|Vq#)62B*OjLpHn;-~}-H(2ZTgOgs*a69%%CAWVya z)Q5x))j9GUUb}WJ31gs`&TCGDo6_I!hlEc$_Fank&#UK-M((=?RR!3pR3F0ME`FcI zH`)iTBd{W(m4y8ydYuxS+aQ-#Y>2}gos;NPz?+A^TE~wcH*lD+Tx8|s40!yQk96-i zfC*1DBSVaE_t7?PIOFmI^gGDqydfX}fTKGovv3DpAkFgS-$A*3Q&V&Ew9p~&q`r4{ zE^~1BZ8)b_G z+Dwtiwo2$)$GeyMki$>4-jVjK&GjeRV`EI1=s14M0FJsw>*ti=S!-I}TDSt0>-6$- z6?04Ls#*%Jw{b@t&{X3*)ga8^u!YUx$opTny~Qt`d+i&&Ax#PK*$0pElwyKN+E>cs zsjC_c3rkKlaa-moWlatrUl}o#w-hWJKlI@vPG7c$WUu$P>~Ace*(5!^jdC6>?ZemS z9b&G8j`6e0L~(SvjOl;;Tz@ESag#>#SDGN+&HUKv6CE9-?=dZ6UesvH3fw5P zQ5v`tx97KcvyOtX7TK$^){AYNqVf7pE>-`M515%khCct>1M|oFZ-U3<`I75f&`7w~qrrFsimL+ycrk5x zc)*h^pC-+}cC|g^istvqNZU$ajGH%`&~fj6{?*az(9uK2&U_)!GV&97pLPY%KX4tq zH*id zht^MpstDAl$-T@{T)Z-*_2skbeKEPEk;2_Q!pgut(Vi5{N6@pcnVF$bhE(r=XJXJh z6|A}$t&h8;Lr(upjNL&w6sP_V?9XZ=KfS`M=kn~l3tE)=!2oT&Xhyx`Q|#{0tK z+)UaFCPw$$*a=yle{)Q))eqveANiQj$a_uM-RxdP?&_c6;U}vN`DSg|*&5{QyS9!6 zLV5k?wF?phBQ!XAX?m|7d;!Ap{O_d3#O%u!UZgwn?buDteHLF-Hpgq}`TGR(iE{$f zl>7S^G3n|_s5Wa#XFPJ`0~LjKaekKvoyJK`T12xH z2D=oFFO4+6>#e;k_@=1T_VviIT8CVwE7b7O+vhN|>gKIStC=+I`cBn(9o>w?m*ncf zM(khxtG8UqUcga}a;QG7T`#Suod+29{qH9k7~S2?IoP%@6@GKdxBmF$v&4|D=|c19 zJ!U1W&EW?j9Ad^feBX)i&t|9WpY?r5gCi8S zm;H)UibB;XxPyF|uqZA6%mr*&#nqk{yFOf#(0P^a{k0mHVO+`HQ@_?0T$gVh%9syj zpLXvLx@_C;=lZ1QO-;3;;@4Fzg*IqEB`yegoXZ})yK8pExss3PtQdLRn0r?66O`h4 z(>{6S#9gG|X_d-hV02uTcH&&e!ItI|&xbX5nxv+RpYnOq&~hm*i3r!Fm~fU?=qk6D zbai}vDwSe2*x?|szU+!;z^XFcn1ttEHLbKN0Y_1G-c)1dzdTdQZa?_*X7|9N(D<#^ zS-O(hlWT%!C&js}%#D8aZBM#*oHHXQwe(8k3Nku^DJ#lY@7a@epQ^rpjOgoXp_AI1 zR{Chjvja_%XtE}65RU-gPkV+Fn(f8kvUZ-b{7OrCm}4&k6Vh~}PpW&KJ_~eR%Tf9^ zZ>zbl1%@JP@$WkZpWa2q$2#CNTNdLUx~u3Mf9%c=dwXtYNeu5Vw2W~2c#RTgpQ&QsCg7jb&QMzI0FM2#3G|XS z8{a}#fKy@Md}T4yd_SjDV_|9dv%tlv*hXQ~;?D=8 zsdAmx8rkeRbm<;?_-w^z5tCZ9lMuV;33Hde*>BjF6 zaeMP=YtUucU38u@9=auJ%Hx9K-XToq}se(xORZvM?{zjEq| zt5|TV2RyB*sj0a6Vb6Cwj6?cygw!pQ)?npqsY*OPDcX0b`y*Y!2{#%meSMs4-8>8z z9`Co)i}O5jVn4c(LqIB>WzLCg-_CLEZla@Ux5p6?35hJTp;|@j$e;;{Bhl+i{cgy7 zsQz@*N{A;j)8$4?D9dh@sKciRjSggv3OX4*f}yGL0iIjUMkhl;qE{_ESw(d3<<2~; zNfHZ3 z_VvAfd(iItmEV^?Dx3P8^WL&~<_v2~W_Gv>>)%snM>!TA`I@WiaRsN|DhG^3B60kf zn2E2_p0W7L6r(Agn*3z3Kpp+b%;6%JEj+?jU*Enp+(l%zKu*nHsSvA|?_Z*xCFbcz zd60hk6y^31`Jj_^PQkx?wgry6oC*z9ViTvUQpFo~6D+=I!)rIgYnI$65+7Ju$0-Sh zc-T_K4r6cQl=;AApz@Ibozl{F80?DPHEL)zIMBV4laupKPU`EZrScv!mXvaJ= z7ohWo8etV?u`w&^|FQzS%g~MghTd_YF2zJ<1iBo`|L6@xCt!Qr=&P^c8!NE%HJS2( zpi!JMHhMAr*r;jmzWV1W+IBnDSI|pS&-@Y`I=3{j4=H6ZZ+*cFsx4htchVRv-Qp(G! z@&(qAmwqJD-Ny)=_;ryB?R%2*|5F3Ukw#tF)=s6Eu+yqh6rG;5%Xrngm z+o#(evfNtu_lrJ?j0_~G+7kHfs8j%Pm?kZM#njX!hu8exc5gT>_QC5rvx$eiBZ#U3 zgs-c^BguWoWJ+@VHv|p#-5|%`6}jxtnrFp{nys(q)(R0r^RqW1g-%N)1v^G^ntgoS z?qBGfrA6L$lxdIsD;{f)CQWy{8qDSKpZL4Z@2l3(-Vf8CHix4t{eiB#X?{9xH zv^AtNX?bj3cW>kU39YF?wwFrxEc6t%5d$YuvCHWGD-QN+Wf_Yp5 z3Y+IJ^CvF$j}W`$-yzMfq`5YFbUXp%`j>bJ|}%>PcyPIydpYXiSXE zn;uaRDrPclEWqONg~!BL+Ea%z4h$d&cIk(9vVZb`6y6L#qVjA{Z!h7gRW>C9SFOYh zxV>~hN&(ot*wlN0?!tu&5OY3!y95E>i{H?rpSI=!2g!y!6th0l)}v4@PR6Cijh4uQO=Z%2!aU`@eieP~=+TkX7&1P(jV|{IxFEVrLH8(q2lgqp9kb z2s2_RZ^aNF^TbOdh&;9L%8sEfTMWXBv#1DhUSuUsjbE`x^pbb0kQFURBpiWSdOC+c z23lr1=<$IysZ#X3Tip4xPX9ntao~@a&AD!Di-uDhi9gn2hBr6IzGTl+xbVHtJXW>j zea~}?c=Pgt9t+w#$gQj_N!*_|G5>g2$-5;DZYzWz z7(p$0aV#wE8{y~&hmGfoeycEzE(Zcuo7h0PDG5Nc9Bhd?M4S+e8bZ#u_bP5=J9~T7 z9oIpVRJ+IE>jSU6O~S$}t*xyI&jf;Lg1j9fQi=GbU>g9Eowj8aY!L`!9KuQfJUI0$ zcL8ISV{{r|M~9WbLzsU+nsGZOMi3Mnp^PGk-!ap@)b-Drh}O!2ePAX$j)>U2b*qZI z7zNT6!Wo2^$iz`V)TjkJ zfOgrKf}lF!yFdt;f3>){e3WUI8DWqd^yZCY4u;fapJvAh7hmG5G8`CLD0!>uaBEik zsSP(Iij$8lu#!3k8seO8GYzj38PdYAqkHzf?C^mNM9Gt2Z|5uTSKeVZ=1e+t*kfJH z=asY~&5NtB;6 zXzc7QHrjpnN$2}RzEgWQrfYe)$(FYf`{7#^2l#HjWeW}PJBFW~tVC^~6L>)l61Bt|!r`I`IF z=s|Yc%pq{Lu||}bh(H*ns(wv%K5b-^gB*L@O-|oU&#qk)jVn+qk2_UV>g1hpB@$_= zY|U9C{<`60L=-ZVR4jB9=Km;qd+^eErab2X!>%bdMeLK-uz2I-=8myxh$s2r)?$}# zf&(szPE@KYQi=(H>5c|tSVxpYhZ_QD8!RE?0@t+T6-)Oq8h=lr6lwkPL82FYI|LV; zpoV!4p**Lo>mC>~(Xg_TShpQG@K$T{DlM(YsUW_fE(DVHrq)&%kR9PrKv-J<8{!y* zO=}hRC;?m?OM<%7T}J*dUbf!}(eZ&=O{o?A!k#k&uIf zBll3rT3$%GR%uONbA%lZ07f&0SgZrq(``S18xpZo%XX0Po_uzZ$cBPC;sf|mV3)v^ zXtJd0@0JUo;G{34--Y@4*WNGRZU3_QnAx(fmerh$YK{2XlJ8_Plxaq;47&=GztyBJwJRBj!Zj#Wf|NkD3FB|TsLpQ&v550 z)e^$gTkkh*q{p!z-jqqlc)$PQi&H}C|B2;!xu7x_K= zLPz`LNwG_jnz>#?_y9Ne3R1L!_nH1_G6Kp-1hfmN2uSVOWu<;GxB-~!>c$XJT9{90 zCmZYubqacmo_se&BXSMRbe>Xl(mM^TOo&ohE?jQsWyE7;ND~Sjqt-B=V99-3d-n zJ9fyL|M-~9Y(0e+7NX=&+>Hb}P%FJ%UVcEg{64z{WKo0HsVR(+OQ&SV4(c@3t|VRQ z|IU-c&miET{M~giq)uMrVzDAPVYNl->cmK(sWaoP>o$GqQ5W65Ps7v{ddvj_S*l~> zQ#LQmwk2^1xG$uRQCaQ|jb^N&otxaP-5OvCv{p<^3=F4EIJH2^um*2B7&bYCnKky} zvMGHnt@{p9gyRjEkk28d9vdI`4GYtc_<^2c1BTvoBrtP?(QfM5wZeo64HpfG;8A8& z=fBqak@$8U3@G7jDM%_VFORRy#l=u(U?QAuARr-ZGQj8|+$yE7?wkn_1wY3y$AJ&_ zH*upgfV$#Fb7Nz>D^_o(#Yftl?;-{om{|2CB&*m2cfGrf8yoscIuCA7(Kj!UC#k?R83vT zwCP$p7fM_=%P0FA88L_OP{<1}i2d;J!2?DHB|3cG8+b2jp{@4s-^eGENv*ukZ=bhw z-n?Mowb4ndH>_HRZ#GUaF&$idKW=LMblT~I%<5wq@J{T1s=`t3S;*t(UB*LT>7%BygXOmqt!~)`1rXx*)sc1z9-|I zH7+C)zQf30r@qO!y^8lxQ*e6DiV!KR2ht%xHRJ91jW7UPCm?U!`)EDV zdWjG|6qWivj8C5~3D}%v>*>K38fpt$Iq)J0vvkBJy=%S-S3yYdcaaDS1^?jSN6~O` zSp}Du?IuSLM`Is?)mqMBIw9x-uOkN0$Jbs5oLJ*H3OXpFp#!i=XvlHygCANB)cu&` zQvTN+u+%~X;}*H-u&|o|TuUjGCEeZ0U)p;J2Ss6)I}_8;S|ND2mdQZ_j?=!&_6~$s zBHW;9NHFTxwH$g#I86gP`B1stc=}O9#P+Bh+n99+9L}Hb zcmKoIp4I7LpcB~1D(}a1@S`BT?-kLZd5=of(;J!V`nC=p9}9e5$fr`xOQCF(*81fIGjL(j|LO3@Wf(yBF=3 zeZuPPK`B4*lKG6jmZi2+Vodwk&)dHcYb$+Eana1Re41k^mzgEniRmXdSE89K`RcU8 z#dDc^l2_t}vb2u>G~AS_e1Uh}yIaS?l7Z4+Ar|#_JyrL+xcw#uzOYtm3aHPy{$wh{ z%I-cf%xK89{ryKZu0ompCG=9NS6niWG`Mr)_bU0QHWORM&9t%m`O960G&SEGA5}fI zhKY$V-$HO#ATFSKjw6U-N6@R>$qCXWkyuQR9#Paol9C`vBy-_zQqqp9>S`qLZM7F; z4@3l{KUx&KZ|QXCmA8CGR+C2D^ye_qWV&Yq;p_ z9A+H}qYlWgXSCjpxY6+J2r=b$#+JKadEI2O0}$HAsAh-jtaAO4YwgWvlMES3pL^Gkau6(3{&At z=o`u)nL}*u$YG;+!Z;a>D`aSwtgaw-7s4}Sc-Rh;JPSx3>PPQUzqPcK!i5uR)0^*1 zvHbnPyAy&51S-$jj>%=kM_u12N`X*BLAbZHv?P(?LGtn*l|W=>=dI#inpYK{D0A5C z=&;E*GLR8dERF#ghg60t#v>@c9Mig`{&3ajsCJL|ol;ZQIMFiu2G6V@9X`RkLxMt% zzVO1HI?_VI!$ctR9i(D(?ZllC z_mg52sL9vxvepB=NqEOk7UmFHOg=SvWSZSRe|R^NY!bANV(`c!uw8$EOICO-~1Kbgz&<;kI2tm^z!nOqf?xwellWLf(uhGsC*o7^ZRsRt>A3Y{~t$9FzgdN zyeHl98mU+_uqkFKb&Yb%E2CV}&`C5B`{XtuP)dZ~9bB zxp2;>ntj_k!6ku{YrBQJQ2%2wUK!^KfmTaL+H}XJRrAv4WJ%9X$m!bsUZXh4y{i6q zXo2j+C5KxWrV_<7=(g$#6VzVkwU}M3AT_%4m*?;q2L^eUQdCbLzdYdJ{`_@sMn*;) z9apl{sOs&{WQxHrh?;X=YKD%p*FIKq$3 z(xKv)X#-(m4UP_UtG)pN0sp-j%rL+S&+~$BVIRz2TZ1~iq-I~mO8)kujL1#&r+kL>pM2DT ziiU(a(!8E9OT@xtQ|yPM`@C#U|FlV{YAbMG&porfza{rUL2T_g2n5#bGRmGmX#qc? zVlak-SUhK*?C((sS+4n)Br!t0+lqx?1V;BHG;N=j)mz%RXll=YuVM$GlRz7`R?jP2 zqi*GWx<&>biWi5C`@iSgw*&nZ&TuraUNx>hMq>r-blv_LaK|sFTRAWaTy!N*46$cZ zjukZ~#xbz5>Hqj)$q^|oR5(pn3!{<5y$IzjLmsNASJb5v$5DemWXr{oWOmp&+`K0V z@bldJB-1Av0)4&PDn)Z%pGf58ooZi^oEKJ?nV3}&`>$3`68knka-G{?&R=E?oki0h zaaMw$tKBbUtG1-KS7&%zBqjgBXn)`!I6w1K_NojG$J9LMxcTxYN~&f=*G_up@O}8I zuKoBx0S|B$99A2Dnw=M0Eg@OAa^>^pnQCr0emq?x7YvpYEI3UIkAT6h++uBIRhk#f zO~Bo8x2fp*bap`t*N1>-j%S!pilwF4SMj(y7=Md-wjuVc zCves%A0hcdmG%9#L*r%iDc>d{K7IaNN>kIr+4zP5rLpknpQMi?M@(;`rzz-f%^B@j zvvDhDl!CXs-XA5=4jnc*&!>emD{d<}mZl$N2oh*vLlJ;&-^?m52~zah+qo>v#8^9j)tA2-5Bet2V)f3XO=> z)T}eL$p*9zyY~}weBESsFq^K1WMicDc5xHO{#|?>z2Dd2^_nGWRcbGp21Utogm2s{ z?)rtQJiF`6$QrWGc|(cxlA4V2;-qXAZm#X!_iIiJ%UtE}JV}P1H!U!pX9;>&ez{Se zCHH^z7Vffec$Ro)D{&_NGEq5r`AgbL;rOq#cOV)34A*jw{vc) z|K~{P;k((N%)!qeO~_c)6*v6U|4~rBHU4v?%W7UJi6or zR-GC4TzLm2VROGc^a~{&S?jd$^Ojai%bexC*3EW1#cw8JB!9^ z#eFnI%3b>o>%FDk=YQ?IZT)EW0(!OibbtT^;MT?y9ushOZ+S4c;Q#KK0{qyu7Ue;| zveVMEWsp|&cMpyU9guP5^%avnbbdm}O%*=iYsWggJQ}Arn>-?5eyUx6<>dJ1-`G3odb+1W3(SR6mN6{SnJz+mqm4^{jNVd&&oa^5EPuT_kLK*LtlMQD zFs<^6kbkPUVY+i8-c*d3KUn@6el0j^b`mrIyCQyLa#SCvrsbMqwrPdlgxI$dm2b@+R|f4TX@a2`%HF`%o~n#Hl+VpT_HqCk$aP zvt0~#d8(=7AZqb}#iOg^BUkqv&l4VhR^rpEaU9KF4V@JmS7(OvoQgz2&ThZ4LH3<@ zzleKP?Ez2wXnWefH_2KsX5XMMSMX|1H76E>>VJryf514k?RRZ;Ztib1!(YTi{+YfU zHusS;9UvN$S>2Lp>iI7*QC1JE6{dLRqf9*aF-W>uCY{IXV)n2TyCTWf*;2-J)nHX> z1{Q+h->YUC1`$9NV$xrs`k76{K{+1$*HGlhULXl>X`XHMiRUy|k4DJZ+%M>UxF(m? zV!omJLZ1@Njdyby%CTEh7+>4na=AFr*3wv#n8X_!76E{31DE^Cu&jr5Sx&0`g9+xY z-Y4BE{(f+?JK(o!ycb)D?ytcJ$x^Zd$N~t zE?xQE(j!EQQ@<5(L@+CUS4)s&j&0YM+N8uRi;ACT88vnt+`97&Ag&Z;)sB+3NG*d8 zeila8zPviMF)vHcOg=NGp!?6>!tD7KUU++e2MfV+dl<&@(f?t%+S=Z7Zja|$=SQ39 z_!JIgsm^te2kY*Z|8zFUn`Px@2A_&0Qys{}bA#`=ZQH1uu|DhWU<8ozNJB|`{ru|@ zHv^vJspMJ`XnO2w>IRn~mh}2FiZm>Gjg3BqCh8DHt_V1k&pvsbee#o)*WV9h#||ke zB2O=1VruFqaK*g;^XvSnM)h%UkUK59 z1tt|HM#cW#ef{W5_fs)eSdF2t!R!*gCn*PS}hCJovan@bBjs7J^!- ziYz|(5BjZq2H*P->9chP?k*Q$RNSCLiX-_yaQP#W5{y}r4pI98>*G%&l%O<>PMQ~% zE{rao8BFOu5bSM|wR=!7L6otjY*V8~E%#mJ!wSNCLb(qpsc7%FHLQ&9dY1iA*ye=T zSuV*Z7Kw@u(mLuHxjnWF-_J^z%fP`*(cbEC{0kY%?z{k}2BzOm!Sejx#kcze#=3qx zIC>ulQ8R`U`soA-5{YDXUHCy|e~(E`)X#(SQw!Lj6_o2rYL5n5EBE7Rl8-lZDHgAu zJ?r?1HdXS@k*>r2{CIqmzOGXB%sivi8;8~+mo~CeP)`{rEuQ#5@8{>o0gX;4=4xZuWc#f<@!o%&n<#q(^5{rDFD~?aOnw6uQ^YW|bW8J%b?rhFzUHQ}i3r_o zc~|xAPWj|jS;I1&1jZA%#ku&ACt?043I)dDKGM!4$BOb`740h){p(4jN7=1Sl&tO$ z)tp9?OFc#8^-txTE{|v3deWyL*G`?y(3H*eagL~7ep_my7?EFcbDMeI-rgQ_Ah^3$ z4zF=2$?+Y3gxd@2YuyzKZDOnn%^cF(MmT}vkw`Y?kRPEOtw+)X)5B_2-n zDwuxX^Mp^6Z@S;_^S0k}`AU6pyDZk>sZ9x%d%0i&7iDX2UwyB8y6^96e2XxRjbby( zYgeex77y;8sg)aW_;Yp4W^C>3<^`pssPVA#VZ6nOpK{-;x34cIs%Oi#rj&QREzzGF zS11J)RV=>?dh@f89Ois&YN7xQkmi?Og>RTnJ`fz+CfTY`@d6na-<*%6N&)I2R zzLV79#Mj3A_Fn$!(I0*M&jlf#|J0ngae+g9e_Kv&dZz2wZw+KVhPDjW1IstTeYf3M zpx)#M5B70LuY8mTjXRriT~93^f@ZxRM?^=r&o Date: Fri, 19 Jan 2024 12:12:12 +0100 Subject: [PATCH 203/240] cast right types --- magpylib/_src/fields/field_BH_circle.py | 2 +- magpylib/_src/fields/field_BH_cuboid.py | 2 +- magpylib/_src/fields/field_BH_cylinder.py | 2 +- magpylib/_src/fields/field_BH_cylinder_segment.py | 4 ++-- magpylib/_src/fields/field_BH_dipole.py | 2 +- magpylib/_src/fields/field_BH_polyline.py | 2 +- magpylib/_src/fields/field_BH_triangle.py | 2 +- magpylib/_src/fields/field_BH_triangularmesh.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index 44dcdc338..cf85ec182 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -78,7 +78,7 @@ def current_circle_field( check_field_input(field) if field in "MJ": - return np.zeros_like(observers) + return np.zeros_like(observers, dtype=float) r, phi, z = cart_to_cyl_coordinates(observers) r0 = np.abs(diameter / 2) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index bcee038d9..7aaf78901 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -244,7 +244,7 @@ def magnet_cuboid_field( # dealing with special cases ----------------------------------- # allocate B with zeros - B = np.zeros((len(pol_x), 3)) + B = np.zeros_like(observers, dtype=float) # SPECIAL CASE 1: polarization = (0,0,0) mask_pol_not_null = ~( diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 8e44c8414..fa73b092e 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -343,7 +343,7 @@ def magnet_cylinder_field( z0 = np.copy(z0 / r0) # allocate field vectors ---------------------------------------- - Br, Bphi, Bz = np.zeros((3, len(r))) + Br, Bphi, Bz = np.zeros((3, len(r)), dtype=float) if in_out == "auto": # inside/outside diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 286e887ec..3b92c2eed 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2312,7 +2312,7 @@ def magnet_cylinder_segment_field_internal( """ n = len(polarization) - BHfinal = np.zeros((n, 3)) + BHfinal = np.zeros_like(observers, dtype=float) r1, r2, h, phi1, phi2 = dimension.T @@ -2425,7 +2425,7 @@ def magnet_cylinder_segment_field( """ check_field_input(field) - H_all = np.zeros((len(observers), 3)) + H_all = np.zeros_like(observers, dtype=float) r1, r2, h, phi1, phi2 = dimension.T r1 = abs(r1) diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index d210096de..dd9aac158 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -63,7 +63,7 @@ def dipole_field( """ check_field_input(field) if field in "MJ": - return np.zeros_like(observers) + return np.zeros_like(observers, dtype=float) x, y, z = observers.T r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index abe9d5cf7..a0a37f297 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -139,7 +139,7 @@ def current_polyline_field( # pylint: disable=too-many-statements check_field_input(field) if field in "MJ": - return np.zeros_like(observers) + return np.zeros_like(observers, dtype=float) # allocate for special case treatment ntot = len(current) diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index 67aeb813b..191a6824e 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -141,7 +141,7 @@ def triangle_field( # pylint: disable=too-many-statements check_field_input(field) if field in "MJ": - return np.zeros_like(observers) + return np.zeros_like(observers, dtype=float) n = norm_vector(vertices) sigma = np.einsum("ij, ij->i", n, polarization) # vectorized inner product diff --git a/magpylib/_src/fields/field_BH_triangularmesh.py b/magpylib/_src/fields/field_BH_triangularmesh.py index 47cf90bdb..02bb602e9 100644 --- a/magpylib/_src/fields/field_BH_triangularmesh.py +++ b/magpylib/_src/fields/field_BH_triangularmesh.py @@ -583,7 +583,7 @@ def magnet_trimesh_field( b_split = np.split(B, split_indices) B = np.array([np.sum(bh, axis=0) for bh in b_split]) else: - B = np.zeros_like(observers) + B = np.zeros_like(observers, dtype=float) if field == "H": return B / MU0 From cf74b73924daf690b4f795fef6db10f22ec337ac Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 19 Jan 2024 12:34:31 +0100 Subject: [PATCH 204/240] add HBMJ consistency tests --- tests/test_core_misc.py | 218 ++++++++++++++-------------------------- 1 file changed, 78 insertions(+), 140 deletions(-) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index 3a9c7526a..c6c1dae4e 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -11,7 +11,9 @@ from magpylib.core import current_line_field from magpylib.core import current_loop_field from magpylib.core import current_polyline_field +from magpylib.core import dipole_field from magpylib.core import magnet_cuboid_field +from magpylib.core import magnet_cylinder_field from magpylib.core import magnet_cylinder_segment_field from magpylib.core import magnet_sphere_field from magpylib.core import magnet_tetrahedron_field @@ -25,6 +27,16 @@ # NEW V5 BASIC FIELD COMPUTATION TESTS +def helper_check_HBMJ_consistency(func, **kw): + B = func(field="B", **kw) + H = func(field="H", **kw) + M = func(field="M", **kw) + J = func(field="J", **kw) + np.testing.assert_allclose(M * MU0, J) + np.testing.assert_allclose(B, MU0 * H + J) + return H, B, M, J + + def test_magnet_cuboid_field_BH(): """test cuboid field""" pol = np.array( @@ -57,21 +69,12 @@ def test_magnet_cuboid_field_BH(): (0, 0, 0), ] ) - - B = magnet_cuboid_field( - field="B", - observers=obs, - polarization=pol, - dimension=dim, - ) - H = magnet_cuboid_field( - field="H", - observers=obs, - polarization=pol, - dimension=dim, - ) - J = np.array([(0, 0, 0)] * 5 + [(1, 2, 3)]) - np.testing.assert_allclose(B, MU0 * H + J) + kw = { + "observers": obs, + "polarization": pol, + "dimension": dim, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_cuboid_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -120,20 +123,13 @@ def test_magnet_cylinder_field_BH(): (0, 0, 0), ] ) - B = magpy.core.magnet_cylinder_field( - field="B", - observers=obs, - polarization=pol, - dimension=dim, - ) - H = magpy.core.magnet_cylinder_field( - field="H", - observers=obs, - polarization=pol, - dimension=dim, - ) - J = np.array([(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)]) - np.testing.assert_allclose(B, MU0 * H + J) + + kw = { + "observers": obs, + "polarization": pol, + "dimension": dim, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_cylinder_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -171,20 +167,13 @@ def test_magnet_sphere_field_BH(): (1, -1, 0.5), ] ) - B = magnet_sphere_field( - field="B", - observers=obs, - diameter=dia, - polarization=pol, - ) - H = magnet_sphere_field( - field="H", - observers=obs, - diameter=dia, - polarization=pol, - ) - J = np.array([(0, 0, 0), (0, 0, 0), pol[2], pol[3]]) - np.testing.assert_allclose(B, MU0 * H + J) + + kw = { + "observers": obs, + "polarization": pol, + "diameter": dia, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_sphere_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -229,20 +218,13 @@ def test_field_cylinder_segment_BH(): (1, -1, 0.5), ] ) - B = magnet_cylinder_segment_field( - field="B", - observers=obs, - dimension=dim, - polarization=pol, - ) - H = magnet_cylinder_segment_field( - field="H", - observers=obs, - dimension=dim, - polarization=pol, - ) - J = np.array([(0, 0, 0)] * 3 + [pol[3]]) - np.testing.assert_allclose(B, MU0 * H + J) + + kw = { + "observers": obs, + "polarization": pol, + "dimension": dim, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_cylinder_segment_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -287,19 +269,13 @@ def test_triangle_field_BH(): (2, 3, 1), ] ) - B = triangle_field( - field="B", - observers=obs, - vertices=vert, - polarization=pol, - ) - H = triangle_field( - field="H", - observers=obs, - vertices=vert, - polarization=pol, - ) - np.testing.assert_allclose(B, MU0 * H) + + kw = { + "observers": obs, + "polarization": pol, + "vertices": vert, + } + H, B, *_ = helper_check_HBMJ_consistency(triangle_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -344,20 +320,12 @@ def test_magnet_tetrahedron_field_BH(): (2, 0, 0), ] ) - B = magnet_tetrahedron_field( - field="B", - observers=obs, - vertices=vert, - polarization=pol, - ) - H = magnet_tetrahedron_field( - field="H", - observers=obs, - vertices=vert, - polarization=pol, - ) - J = np.array([(0, 0, 0)] * 2 + [pol[2]] + [(0, 0, 0)]) - np.testing.assert_allclose(B, MU0 * H + J) + kw = { + "observers": obs, + "polarization": pol, + "vertices": vert, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_tetrahedron_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -423,23 +391,15 @@ def test_magnet_trimesh_field_BH(): [-0.47620221972465515, -0.0791524201631546, 0.8757661581039429], ], ] - meshes = np.array([mesh1, mesh2]) + mesh = np.array([mesh1, mesh2]) pol = np.array([(1, 2, 3), (3, 2, 1)]) obs = np.array([(1, 2, 3), (0, 0, 0)]) - B = magnet_trimesh_field( - field="B", - observers=obs, - mesh=meshes, - polarization=pol, - ) - H = magnet_trimesh_field( - field="H", - observers=obs, - mesh=meshes, - polarization=pol, - ) - J = np.array([(0, 0, 0), (3, 2, 1)]) - np.testing.assert_allclose(B, MU0 * H + J) + kw = { + "observers": obs, + "polarization": pol, + "mesh": mesh, + } + H, B, *_ = helper_check_HBMJ_consistency(magnet_trimesh_field, **kw) Btest = [ [1.54452002e-03, 3.11861149e-03, 4.68477835e-03], @@ -456,19 +416,12 @@ def test_magnet_trimesh_field_BH(): def test_current_circle_field_BH(): """Test of current circle field core function""" - B = magpy.core.current_circle_field( - field="B", - observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), - diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]) * 1e3, - ) - H = magpy.core.current_circle_field( - field="H", - observers=np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), - diameter=np.array([2, 4, 6]), - current=np.array([1, 1, 2]) * 1e3, - ) - np.testing.assert_allclose(B, MU0 * H) + kw = { + "observers": np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), + "current": np.array([1, 1, 2]) * 1e3, + "diameter": np.array([2, 4, 6]), + } + H, B, *_ = helper_check_HBMJ_consistency(current_circle_field, **kw) Btest = ( np.array( @@ -498,21 +451,14 @@ def test_current_circle_field_BH(): def test_current_polyline_field_BH(): """Test of current polyline field core function""" vert = np.array([(-1.5, 0, 0), (-0.5, 0, 0), (0.5, 0, 0), (1.5, 0, 0)]) - B = magpy.core.current_polyline_field( - field="B", - observers=np.array([(0, 0, 1)] * 3), - segment_start=vert[:-1], - segment_end=vert[1:], - current=np.array([1, 1, 1]), - ) - H = magpy.core.current_polyline_field( - field="H", - observers=np.array([(0, 0, 1)] * 3), - segment_start=vert[:-1], - segment_end=vert[1:], - current=np.array([1, 1, 1]), - ) - np.testing.assert_allclose(B, MU0 * H) + + kw = { + "observers": np.array([(0, 0, 1)] * 3), + "current": np.array([1, 1, 1]), + "segment_start": vert[:-1], + "segment_end": vert[1:], + } + H, B, *_ = helper_check_HBMJ_consistency(current_polyline_field, **kw) Btest = ( np.array( @@ -541,21 +487,13 @@ def test_current_polyline_field_BH(): def test_dipole_field_BH(): """Test of dipole field core function""" - obs = np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]) pol = np.array([(0, 0, 1), (1, 0, 1), (-1, 0.321, 0.123)]) - mom = pol * 4 * np.pi / 3 / MU0 - B = magpy.core.dipole_field( - field="B", - observers=obs, - moment=mom, - ) - H = magpy.core.dipole_field( - field="H", - observers=obs, - moment=mom, - ) - np.testing.assert_allclose(B, MU0 * H) + kw = { + "observers": np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]), + "moment": pol * 4 * np.pi / 3 / MU0, + } + H, B, *_ = helper_check_HBMJ_consistency(dipole_field, **kw) Btest = [ [4.09073329e-03, 8.18146659e-03, 5.90883698e-03], From 3b1a4f669152db37d9613fde858b892060cc0e0f Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 19 Jan 2024 13:00:12 +0100 Subject: [PATCH 205/240] pylint --- tests/test_core_misc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index c6c1dae4e..a86df8aff 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -28,6 +28,8 @@ def helper_check_HBMJ_consistency(func, **kw): + """helper function to check H,B,M,J field consistencies + returns H, B, M, J""" B = func(field="B", **kw) H = func(field="H", **kw) M = func(field="M", **kw) From 306f450219fa77c99b24d8def6e3e3a056fcd785 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 19 Jan 2024 13:08:34 +0100 Subject: [PATCH 206/240] pylint --- magpylib/_src/fields/field_BH_cylinder_segment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 3b92c2eed..fc92f18d0 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2310,7 +2310,6 @@ def magnet_cylinder_segment_field_internal( Falls back to magnet_cylinder_field whenever the section angles describe the full 360° cylinder. """ - n = len(polarization) BHfinal = np.zeros_like(observers, dtype=float) From 6d3ccabde745c615dabdcdf2d54ee06db6b9c8d6 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 23 Jan 2024 09:52:20 +0100 Subject: [PATCH 207/240] update changelog --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c43c6237..8f4e665e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ All notable changes to magpylib are documented here. # Changelog ## [5.0.0dev] +- The Magpylib inputs and outputs are now in **SI Units**. The `magnetization` term has also been redefined and is now codependent with the `polarization` term ([#712](https://github.com/magpylib/magpylib/issues/712)) +- Added `getM` (magnetization) and `getJ` (polarization) top level functions. Also an `in_out` (inside/ouside) parameter is added to specify the location of the observers relative to the magnet body in order to increase performance ([#717](https://github.com/magpylib/magpylib/issues/717)) +- ## [4.5.1] - 2023-12-28 -- Fixed a field computatio issue where H-field resulting from axial magnetization is computed incorrectly inside of Cylinders ([#703](https://github.com/magpylib/magpylib/issues/703)) +- Fixed a field computation issue where H-field resulting from axial magnetization is computed incorrectly inside of Cylinders ([#703](https://github.com/magpylib/magpylib/issues/703)) ## [4.5.0] - 2023-12-13 -- Add optional handedness parameter for Sensors ([#687](https://github.com/magpylib/magpylib/pull/687)) +- Added optional handedness parameter for Sensors ([#687](https://github.com/magpylib/magpylib/pull/687)) - Renaming classes: `Line`→`Polyline`, `Loop`→`Circle`. Old names are still valid but will issue a `DeprecationWarning` and will eventually be removed in the next major version ([#690](https://github.com/magpylib/magpylib/pull/690)) - Rework CI/CD workflows ([#686](https://github.com/magpylib/magpylib/pull/686)) @@ -29,7 +32,7 @@ All notable changes to magpylib are documented here. - Many minor graphic improvements ([#663](https://github.com/magpylib/magpylib/pull/663), [#649](https://github.com/magpylib/magpylib/issues/649), [#653](https://github.com/magpylib/magpylib/issues/653)) - `legend` style option ([#650](https://github.com/magpylib/magpylib/issues/650)) - Changed unit naming in text to comply with DIN Norm 641 ([#614](https://github.com/magpylib/magpylib/issues/614)) -- Improving the documentation now boasting a contribution guide, a news-blog, an example and tutorial gallery, a getting started section and many other improvements ([#621](https://github.com/magpylib/magpylib/issues/621), [#596](https://github.com/magpylib/magpylib/issues/596), [#580](https://github.com/magpylib/magpylib/issues/580)) +- Improved documentation now boasting a contribution guide, a news-blog, an example and tutorial gallery, a getting started section and many other improvements ([#621](https://github.com/magpylib/magpylib/issues/621), [#596](https://github.com/magpylib/magpylib/issues/596), [#580](https://github.com/magpylib/magpylib/issues/580)) - Improved numerical stability of `CylinderSegement`, ([#648](https://github.com/magpylib/magpylib/issues/648), [#651](https://github.com/magpylib/magpylib/issues/651)) From ee393863a47338f4b6920cbf4a79166d720d68c7 Mon Sep 17 00:00:00 2001 From: mortner Date: Tue, 23 Jan 2024 16:08:56 +0100 Subject: [PATCH 208/240] minifix --- tests/test_core_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index a86df8aff..a5e24b47d 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -39,7 +39,7 @@ def helper_check_HBMJ_consistency(func, **kw): return H, B, M, J -def test_magnet_cuboid_field_BH(): +def test_magnet_cuboid_field_BHMJ(): """test cuboid field""" pol = np.array( [ From 97d237386e5a09aceab33cdfdbecce2d79278e09 Mon Sep 17 00:00:00 2001 From: mortner Date: Wed, 24 Jan 2024 08:40:36 +0100 Subject: [PATCH 209/240] core getM getJ tests --- tests/test_core_misc.py | 75 ++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index a5e24b47d..35aef486a 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -28,8 +28,10 @@ def helper_check_HBMJ_consistency(func, **kw): - """helper function to check H,B,M,J field consistencies - returns H, B, M, J""" + """ + helper function to check H,B,M,J field consistencies + returns H, B, M, J + """ B = func(field="B", **kw) H = func(field="H", **kw) M = func(field="M", **kw) @@ -68,7 +70,7 @@ def test_magnet_cuboid_field_BHMJ(): (1, -1, 0), (1, -1, 0), (1, 2, 3), - (0, 0, 0), + (0, 0, 0), # inside ] ) kw = { @@ -76,7 +78,7 @@ def test_magnet_cuboid_field_BHMJ(): "polarization": pol, "dimension": dim, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_cuboid_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_cuboid_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -98,6 +100,9 @@ def test_magnet_cuboid_field_BHMJ(): ] np.testing.assert_allclose(H, Htest, rtol=1e-5) + Jtest = [(0, 0, 0)] * 5 + [(1, 2, 3)] + np.testing.assert_allclose(J, Jtest, rtol=1e-5) + def test_magnet_cylinder_field_BH(): """test cylinder field computation""" @@ -122,7 +127,7 @@ def test_magnet_cylinder_field_BH(): (1, 2, 3), (1, -1, 0), (1, 1, 1), - (0, 0, 0), + (0, 0, 0), # inside ] ) @@ -131,7 +136,7 @@ def test_magnet_cylinder_field_BH(): "polarization": pol, "dimension": dim, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_cylinder_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_cylinder_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -149,6 +154,9 @@ def test_magnet_cylinder_field_BH(): ] np.testing.assert_allclose(H, Htest) + Jtest = [(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)] + np.testing.assert_allclose(J, Jtest) + def test_magnet_sphere_field_BH(): """test magnet_sphere_field""" @@ -165,8 +173,8 @@ def test_magnet_sphere_field_BH(): [ (1, 2, 3), (1, -1, 0), - (0, -1, 0), - (1, -1, 0.5), + (0, -1, 0), # inside + (1, -1, 0.5), # inside ] ) @@ -175,7 +183,7 @@ def test_magnet_sphere_field_BH(): "polarization": pol, "diameter": dia, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_sphere_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_sphere_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -193,6 +201,9 @@ def test_magnet_sphere_field_BH(): ] np.testing.assert_allclose(H, Htest) + Jtest = [(0, 0, 0), (0, 0, 0), (2, 3, -1), (2, 3, -1)] + np.testing.assert_allclose(J, Jtest) + def test_field_cylinder_segment_BH(): """CylinderSegmetn field test""" @@ -217,7 +228,7 @@ def test_field_cylinder_segment_BH(): (1, 2, 3), (1, -1, 0), (0, -1, 0), - (1, -1, 0.5), + (1, -1, 0.5), # inside ] ) @@ -226,7 +237,7 @@ def test_field_cylinder_segment_BH(): "polarization": pol, "dimension": dim, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_cylinder_segment_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_cylinder_segment_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -244,6 +255,9 @@ def test_field_cylinder_segment_BH(): ] np.testing.assert_allclose(H, Htest, rtol=1e-6) + Jtest = [(0, 0, 0), (0, 0, 0), (0, 0, 0), (2, 3, -1)] + np.testing.assert_allclose(J, Jtest) + def test_triangle_field_BH(): """Test of triangle field core function""" @@ -277,7 +291,7 @@ def test_triangle_field_BH(): "polarization": pol, "vertices": vert, } - H, B, *_ = helper_check_HBMJ_consistency(triangle_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(triangle_field, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -295,6 +309,9 @@ def test_triangle_field_BH(): ] np.testing.assert_allclose(H, Htest, rtol=1e-06) + Jtest = [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)] + np.testing.assert_allclose(J, Jtest) + def test_magnet_tetrahedron_field_BH(): """Test of tetrahedron field core function""" @@ -302,8 +319,9 @@ def test_magnet_tetrahedron_field_BH(): [ (0, 0, 0), (1, 2, 3), - (-1, 0.5, 0.1), + (-1, 0.5, 0.1), # inside (2, 2, -1), + (3, 2, 1), # inside ] ) vert = np.array( @@ -312,6 +330,7 @@ def test_magnet_tetrahedron_field_BH(): [(0, 0, 0), (0, 1, 0), (1, 0, 0), (0, 0, 1)], [(-1, 0, -1), (1, 1, -1), (1, -1, -1), (0, 0, 1)], [(-1, 0, -1), (1, 1, -1), (1, -1, -1), (0, 0, 1)], + [(-10, 0, -10), (10, 10, -10), (10, -10, -10), (0, 0, 10)], ] ) obs = np.array( @@ -320,6 +339,7 @@ def test_magnet_tetrahedron_field_BH(): (1, 1, 1), (0, 0, 0), (2, 0, 0), + (1, 2, 3), ] ) kw = { @@ -327,13 +347,14 @@ def test_magnet_tetrahedron_field_BH(): "polarization": pol, "vertices": vert, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_tetrahedron_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_tetrahedron_field, **kw) Btest = [ [0.0, 0.0, 0.0], [0.02602367, 0.02081894, 0.0156142], [-0.69704332, 0.20326329, 0.11578416], [0.04004769, -0.03186713, 0.03854207], + [2.09887014, 1.42758632, 0.8611617], ] np.testing.assert_allclose(B, Btest, rtol=1e-06) @@ -342,9 +363,13 @@ def test_magnet_tetrahedron_field_BH(): [20708.97827326, 16567.1826186, 12425.38696395], [241085.26350642, -236135.56979233, 12560.63814427], [31868.94160192, -25359.05664996, 30670.80436549], + [-717096.35551784, -455512.33538799, -110484.00786285], ] np.testing.assert_allclose(H, Htest, rtol=1e-06) + Jtest = [(0, 0, 0), (0, 0, 0), (-1, 0.5, 0.1), (0, 0, 0), (3, 2, 1)] + np.testing.assert_allclose(J, Jtest, rtol=1e-06) + def test_magnet_trimesh_field_BH(): """Test of magnet_trimesh_field core-like function""" @@ -371,7 +396,7 @@ def test_magnet_trimesh_field_BH(): [-0.5703949332237244, -0.25462886691093445, 0.7809056639671326], ], ] - mesh2 = [ + mesh2 = [ # inside [ [0.9744000434875488, 0.15463787317276, 0.16319207847118378], [-0.12062954157590866, -0.8440634608268738, -0.522499144077301], @@ -401,7 +426,7 @@ def test_magnet_trimesh_field_BH(): "polarization": pol, "mesh": mesh, } - H, B, *_ = helper_check_HBMJ_consistency(magnet_trimesh_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(magnet_trimesh_field, **kw) Btest = [ [1.54452002e-03, 3.11861149e-03, 4.68477835e-03], @@ -415,6 +440,9 @@ def test_magnet_trimesh_field_BH(): ] np.testing.assert_allclose(H, Htest) + Jtest = [(0, 0, 0), (3, 2, 1)] + np.testing.assert_allclose(J, Jtest, rtol=1e-06) + def test_current_circle_field_BH(): """Test of current circle field core function""" @@ -423,7 +451,7 @@ def test_current_circle_field_BH(): "current": np.array([1, 1, 2]) * 1e3, "diameter": np.array([2, 4, 6]), } - H, B, *_ = helper_check_HBMJ_consistency(current_circle_field, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(current_circle_field, **kw) Btest = ( np.array( @@ -449,6 +477,9 @@ def test_current_circle_field_BH(): ) np.testing.assert_allclose(H, Htest) + Mtest = [(0, 0, 0)] * 3 + np.testing.assert_allclose(M, Mtest, rtol=1e-06) + def test_current_polyline_field_BH(): """Test of current polyline field core function""" @@ -460,7 +491,7 @@ def test_current_polyline_field_BH(): "segment_start": vert[:-1], "segment_end": vert[1:], } - H, B, *_ = helper_check_HBMJ_consistency(current_polyline_field, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(current_polyline_field, **kw) Btest = ( np.array( @@ -486,6 +517,9 @@ def test_current_polyline_field_BH(): ) np.testing.assert_allclose(H, Htest, rtol=0, atol=1e-7) + Mtest = [(0, 0, 0)] * 3 + np.testing.assert_allclose(M, Mtest, rtol=1e-06) + def test_dipole_field_BH(): """Test of dipole field core function""" @@ -495,7 +529,7 @@ def test_dipole_field_BH(): "observers": np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]), "moment": pol * 4 * np.pi / 3 / MU0, } - H, B, *_ = helper_check_HBMJ_consistency(dipole_field, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(dipole_field, **kw) Btest = [ [4.09073329e-03, 8.18146659e-03, 5.90883698e-03], @@ -511,6 +545,9 @@ def test_dipole_field_BH(): ] np.testing.assert_allclose(H, Htest) + Mtest = [(0, 0, 0)] * 3 + np.testing.assert_allclose(M, Mtest, rtol=1e-06) + ####################################################################################### ####################################################################################### From a94ce6501fa39d36e1cacf626d6c871e4dcec42a Mon Sep 17 00:00:00 2001 From: mortner Date: Wed, 24 Jan 2024 15:51:42 +0100 Subject: [PATCH 210/240] intermediate commit --- magpylib/_src/fields/field_BH_cuboid.py | 58 +++++++++++------------ magpylib/_src/fields/field_BH_cylinder.py | 40 +++++++--------- tests/test_core_misc.py | 14 ++++-- 3 files changed, 58 insertions(+), 54 deletions(-) diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 7aaf78901..25c45411c 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -111,9 +111,8 @@ def _magnet_cuboid_field_B(observers, dimension, polarization): ) # contributions from x-polarization - bx_pol_x = ( - pol_x * ff1x * qsigns[:, 0, 0] - ) # the 'missing' third sign is hidden in ff1x + # the 'missing' third sign is hidden in ff1x + bx_pol_x = pol_x * ff1x * qsigns[:, 0, 0] by_pol_x = pol_x * ff2z * qsigns[:, 0, 1] bz_pol_x = pol_x * ff2y * qsigns[:, 0, 2] # contributions from y-polarization @@ -146,48 +145,44 @@ def magnet_cuboid_field( polarization: np.ndarray, in_out="auto", ) -> np.ndarray: - """Magnetic field of homogeneously magnetized cuboids. + """Field (B, H, J, or M) of homogeneously magnetized cuboids. The cuboid sides are parallel to the coordinate axes. The geometric center of the cuboid lies in the origin. - SI units are used for all inputs and outputs. + SI units are used by default for all inputs and outputs. Parameters ---------- - field: str, default=`'B'` - If `field='B'` return B-field in units of T, if `field='H'` return H-field - in units of A/m. + field: str, {'B', 'H', 'J', 'M'} + Select which field to compute: 'B' computes the B-field in units of tesla, + 'H' computes the H-field in units of A/m, 'J' computes the magnetic + polarization in units of tesla, and 'M' the magnetization in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of m. + Observer positions (x,y,z) in Cartesian coordinates in units of meter. dimension: ndarray, shape (n,3) - Length of Cuboid sides in units of m. + Length of Cuboid sides in units of meter. polarization: ndarray, shape (n,3) - Magnetic polarization vectors in units of T. - - in_out: {'auto', 'inside', 'outside'} - Specify the location of the observers relative to the magnet body, affecting the calculation - of the magnetic field. The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for each - observer. - - 'inside': All observers are considered to be inside the cuboid; use this for performance - optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for performance - optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. + Magnetic polarization vectors in units of tesla. + + in_out: str, {'auto', 'inside', 'outside'} + Give additional information about observer position relative to the magnet body, + affecting the computation speed. With 'auto' (default) the inside-outside + evaluation is done by Magpylib which adds to the computation overhead. + With 'inside' all observers are assumed inside, with 'outside' all observers + are assumed outside. Returns ------- - B-field or H-field: ndarray, shape (n,3) - B- or H-field of source in Cartesian coordinates in units of T or A/m. + Field (B, H, J, or M): ndarray, shape (n,3) + B-field in tesla, H-field in A/m, J-field in tesla, or M-field in A/m. Examples -------- - Compute the field of three different instances. + Compute the B-field of three different instances. >>> import numpy as np >>> import magpylib as magpy @@ -258,6 +253,9 @@ def magnet_cuboid_field( mask_inside = None if in_out == "auto" and field != "B": + # generate in-out mask for auto-input. This is not needed when + # "B" is computed, because the explicit expressions compute B + # SPECIAL CASE 3: observer lies on-edge/corner # -> EPSILON to account for numerical imprecision when e.g. rotating # -> /a /b /c to account for the "missing" scaling (EPSILON is large when @@ -268,11 +266,13 @@ def magnet_cuboid_field( mask_surf_y = abs(y_dist := abs(y) - b) < RTOL_SURFACE * b # on surface mask_surf_z = abs(z_dist := abs(z) - c) < RTOL_SURFACE * c # on surface - mask_inside_x = x_dist < RTOL_SURFACE * a # within cuboid dimension - mask_inside_y = y_dist < RTOL_SURFACE * b # within cuboid dimension - mask_inside_z = z_dist < RTOL_SURFACE * c # within cuboid dimension + # inside-outside + mask_inside_x = x_dist < RTOL_SURFACE * a + mask_inside_y = y_dist < RTOL_SURFACE * b + mask_inside_z = z_dist < RTOL_SURFACE * c mask_inside = mask_inside_x & mask_inside_y & mask_inside_z + # on edge mask_xedge = mask_surf_y & mask_surf_z & mask_inside_x mask_yedge = mask_surf_x & mask_surf_z & mask_inside_y mask_zedge = mask_surf_x & mask_surf_y & mask_inside_z diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index fa73b092e..50f3c1312 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -258,44 +258,40 @@ def magnet_cylinder_field( polarization: np.ndarray, in_out="auto", ) -> np.ndarray: - """Magnetic field of homogeneously magnetized cylinders. + """Field (B, H, J, or M) of homogeneously magnetized cylinders. The cylinder axis coincides with the z-axis and the geometric center of the cylinder lies in the origin. - SI units are used for all inputs and outputs. + SI units are used by default for all inputs and outputs. Parameters ---------- - field: str, default=`'B'` - If `field='B'` return B-field in units of T, if `field='H'` return H-field - in units of A/m. + field: str, {'B', 'H', 'J', 'M'} + Select which field to compute: 'B' computes the B-field in units of tesla, + 'H' computes the H-field in units of A/m, 'J' computes the magnetic + polarization in units of tesla, and 'M' the magnetization in units of A/m. observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of m. + Observer positions (x,y,z) in Cartesian coordinates in units of meter. dimension: ndarray, shape (n,2) - Cylinder dimension (d,h) with diameter d and height h in units of m. + Cylinder dimension (d,h) with diameter d and height h in units of meter. polarization: ndarray, shape (n,3) - Magnetic polarization vectors in units of T. - - in_out: {'auto', 'inside', 'outside'} - Specify the location of the observers relative to the magnet body, affecting the calculation - of the magnetic field. The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for each - observer. - - 'inside': All observers are considered to be inside the cuboid; use this for performance - optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for performance - optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. + Magnetic polarization vectors in units of tesla. + + in_out: str, {'auto', 'inside', 'outside'} + Give additional information about observer position relative to the magnet body, + affecting the computation speed. With 'auto' (default) the inside-outside + evaluation is done by Magpylib which adds to the computation overhead. + With 'inside' all observers are assumed inside, with 'outside' all observers + are assumed outside. Returns ------- - B-field or H-field: ndarray, shape (n,3) - B- or H-field of source in Cartesian coordinates in units of T or A/m. + Field (B, H, J, or M): ndarray, shape (n,3) + B-field in tesla, H-field in A/m, J-field in tesla, or M-field in A/m. Examples -------- diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index 35aef486a..d6e790357 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -100,11 +100,15 @@ def test_magnet_cuboid_field_BHMJ(): ] np.testing.assert_allclose(H, Htest, rtol=1e-5) - Jtest = [(0, 0, 0)] * 5 + [(1, 2, 3)] + Jtest = np.array([(0, 0, 0)] * 5 + [(1, 2, 3)]) np.testing.assert_allclose(J, Jtest, rtol=1e-5) + H_inout = magnet_cuboid_field(field="H", in_out="outside", **kw) + Htest_inout = Htest + Jtest / MU0 + np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) -def test_magnet_cylinder_field_BH(): + +def test_magnet_cylinder_field_BHJM(): """test cylinder field computation""" pol = np.array( [ @@ -154,9 +158,13 @@ def test_magnet_cylinder_field_BH(): ] np.testing.assert_allclose(H, Htest) - Jtest = [(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)] + Jtest = np.array([(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)]) np.testing.assert_allclose(J, Jtest) + H_inout = magnet_cylinder_field(field="H", in_out="outside", **kw) + Htest_inout = Htest - Jtest / MU0 + np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) + def test_magnet_sphere_field_BH(): """test magnet_sphere_field""" From ffad5e0b7798f9f4d2a6565d7ec4a32db4e339fe Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 26 Jan 2024 15:55:51 +0100 Subject: [PATCH 211/240] add M and J to show2D --- magpylib/_src/display/traces_generic.py | 7 +++---- magpylib/_src/display/traces_utility.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index e90d9078c..d6543cd32 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -280,9 +280,9 @@ def get_generic_traces_2D( field_str, *coords_str = out if not coords_str: coords_str = list("xyz") - if field_str not in ("B", "H") and set(coords_str).difference(set("xyz")): + if field_str not in "BHMJ" and set(coords_str).difference(set("xyz")): raise ValueError( - "The `output` parameter must start with 'B' or 'H' " + "The `output` parameter must start with 'B', 'H', 'M', 'J' " "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Hz') )" f"\nreceived {out!r} instead" ) @@ -319,8 +319,7 @@ def get_obj_list_str(objs): def get_label_and_color(obj): style = obj.style - label = getattr(style, "label", None) - label = repr(obj) if not label else label + label = get_legend_label(obj, style=style) color = getattr(style, "color", None) return label, color diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index e6734e230..d21f4edae 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -26,7 +26,8 @@ def get_legend_label(obj, style=None, suffix=True): desc = style.description.text if not desc: desc = getattr(obj, "_default_style_description", "") - suff = f" ({desc})" + if desc: + suff = f" ({desc})" return f"{name}{suff}" From f7af42a71324f54084e6deef3bb60d0f0d63d76d Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 27 Jan 2024 01:02:18 +0100 Subject: [PATCH 212/240] fix subplots test --- tests/test_display_plotly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index f6e5256ef..b9f51abfd 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -317,7 +317,7 @@ def test_subplots(): # bad output value with pytest.raises( - ValueError, match=r"The `output` parameter must start with 'B' or 'H'.*" + ValueError, match=r"The `output` parameter must start with 'B', 'H', 'M', 'J'.*" ): magpy.show(*objs, canvas=fig, output="bad_output") From 3129c5ed9b5b852ac118b4213b9ec7152bc554b7 Mon Sep 17 00:00:00 2001 From: mortner Date: Sat, 27 Jan 2024 22:22:00 +0100 Subject: [PATCH 213/240] adding mu_0 to top level using scipy mu0 --- magpylib/__init__.py | 2 ++ magpylib/_src/utility.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/magpylib/__init__.py b/magpylib/__init__.py index 90fcb50b7..531c90da2 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -49,9 +49,11 @@ "__credits__", "core", "graphics", + "mu_0", ] # create interface to outside of package +from scipy.constants import mu_0 from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS from magpylib import magnet, current, misc, core, graphics from magpylib._src.defaults.defaults_classes import default_settings as defaults diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index c166f9241..7847aec0e 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -10,6 +10,7 @@ from typing import Sequence import numpy as np +from scipy.constants import mu_0 as MU0 from magpylib._src.exceptions import MagpylibBadUserInput @@ -25,8 +26,6 @@ def get_allowed_sources_msg(): - string {srcs}""" -MU0 = 4 * np.pi * 1e-7 - ALLOWED_OBSERVER_MSG = """Observers must be either - array_like positions of shape (N1, N2, ..., 3) - Sensor object From 6b34ee4aabb9b2f3c1983070930f3736947f7bcd Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 28 Jan 2024 00:02:46 +0100 Subject: [PATCH 214/240] cuboid and cylinder core fixes --- docs/_pages/docu/docu_magpylib_api.md | 4 +- magpylib/_src/fields/field_BH_cuboid.py | 272 ++++++++-------- magpylib/_src/fields/field_BH_cylinder.py | 297 ++++++++---------- .../_src/fields/field_BH_cylinder_segment.py | 6 +- .../_src/obj_classes/class_magnet_Cuboid.py | 4 +- .../_src/obj_classes/class_magnet_Cylinder.py | 4 +- magpylib/core/__init__.py | 28 +- tests/test_core_misc.py | 42 +-- tests/test_core_physics_consistency.py | 29 +- tests/test_field_cylinder.py | 5 +- tests/test_obj_Cuboid.py | 10 +- 11 files changed, 324 insertions(+), 377 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index a7dd66ba1..3efdc7df3 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -729,10 +729,10 @@ At the heart of Magpylib lies a set of core functions that are our implementatio **current_circle_field(** `field`, `observers`, `current`, `diameter`**)** ::: :::{grid-item} -**magnet_cuboid_field(** `field`, `observers`, `polarization`, `dimension`**)** +**XXX(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} -**magnet_cylinder_field(** `field`, `observers`, `polarization`, `dimension`**)** +**BHJM_magnet_cylinder(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} **magnet_cylinder_segment_field(** `field`, `observers`, `polarization`, `dimension`**)** diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 25c45411c..f5e582221 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -5,18 +5,68 @@ import numpy as np from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import convert_HBMJ +from magpylib._src.utility import MU0 # pylint: disable=too-many-statements -RTOL_SURFACE = 1e-15 # relative distance tolerance to be considered on surface +# CORE +def magnet_cuboid_Bfield( + *, + observers: np.ndarray, + dimensions: np.ndarray, + polarizations: np.ndarray, +): + """B-field of homogeneously magnetized cuboids -def _magnet_cuboid_field_B(observers, dimension, polarization): - """Magnetic B-field of homogeneously magnetized cuboids - see core `magnet_cuboid_field` for parameter definitions""" - pol_x, pol_y, pol_z = polarization.T - a, b, c = dimension.T / 2 + The cuboid sides are parallel to the coordinate axes. The geometric center of the + cuboid lies in the origin. The output is proportional to the polarization magnitude + and independent of the length units chosen for observers and dimensions. + + Parameters + ---------- + observers: ndarray, shape (n,3) + Observer positions (x,y,z) in Cartesian coordinates. + + dimensions: ndarray, shape (n,3) + Length of cuboid sides. + + polarizations: ndarray, shape (n,3) + Magnetic polarization vectors. + + Returns + ------- + B-Field: ndarray, shape (n,3) + B-field generated by Cuboids at observer positions. + + Notes + ----- + Field computations via magnetic surface charge density. Published + several times with similar expressions: + + Yang: Superconductor Science and Technology 3(12):591 (1999) + + Engel-Herbert: Journal of Applied Physics 97(7):074504 - 074504-4 (2005) + + Camacho: Revista Mexicana de Fisica E 59 (2013) 8-17 + + Avoiding indeterminate forms: + + In the above implementations there are several indeterminate forms + where the limit must be taken. These forms appear at positions + that are extensions of the edges in all xyz-octants except bottQ4. + In the vicinity of these indeterminate forms the formula becomes + numerically instable. + + Chosen solution: use symmetries of the problem to change all + positions to their bottQ4 counterparts. see also + + Cichon: IEEE Sensors Journal, vol. 19, no. 7, April 1, 2019, p.2509 + + + """ + pol_x, pol_y, pol_z = polarizations.T + a, b, c = dimensions.T / 2 x, y, z = np.copy(observers).T # avoid indeterminate forms by evaluating in bottQ4 only -------- @@ -136,96 +186,21 @@ def _magnet_cuboid_field_B(observers, dimension, polarization): return B -# CORE -def magnet_cuboid_field( +def BHJM_magnet_cuboid( *, field: str, - observers: np.ndarray, - dimension: np.ndarray, - polarization: np.ndarray, + observers: np.ndarray[np.float64], + dimension: np.ndarray[np.float64], + polarization: np.ndarray[np.float64], in_out="auto", ) -> np.ndarray: - """Field (B, H, J, or M) of homogeneously magnetized cuboids. - - The cuboid sides are parallel to the coordinate axes. The geometric center of the - cuboid lies in the origin. - - SI units are used by default for all inputs and outputs. - - Parameters - ---------- - field: str, {'B', 'H', 'J', 'M'} - Select which field to compute: 'B' computes the B-field in units of tesla, - 'H' computes the H-field in units of A/m, 'J' computes the magnetic - polarization in units of tesla, and 'M' the magnetization in units of A/m. - - observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of meter. - - dimension: ndarray, shape (n,3) - Length of Cuboid sides in units of meter. - - polarization: ndarray, shape (n,3) - Magnetic polarization vectors in units of tesla. - - in_out: str, {'auto', 'inside', 'outside'} - Give additional information about observer position relative to the magnet body, - affecting the computation speed. With 'auto' (default) the inside-outside - evaluation is done by Magpylib which adds to the computation overhead. - With 'inside' all observers are assumed inside, with 'outside' all observers - are assumed outside. - - Returns - ------- - Field (B, H, J, or M): ndarray, shape (n,3) - B-field in tesla, H-field in A/m, J-field in tesla, or M-field in A/m. - - Examples - -------- - Compute the B-field of three different instances. - - >>> import numpy as np - >>> import magpylib as magpy - >>> B = magpy.core.magnet_cuboid_field( - ... field='B', - ... observers=np.array([(1,2,0), (2,3,4), (0,0,0)]), - ... dimension=np.array([(2,2,2), (3,3,3), (4,4,4)]), - ... polarization=np.array([(0,0,1), (1,0,0), (0,0,1)]), - ... ) - >>> print(B) - [[ 0. 0. -0.05227894] - [-0.00820941 0.00849123 0.011429 ] - [ 0. 0. 0.66666667]] - - Notes - ----- - Advanced unit use: The input unit of magnetization and polarization - gives the output unit of H and B. All results are independent of the - length input units. One must be careful, however, to use consistently - the same length unit throughout a script. - - Field computations via magnetic surface charge density. Published - several times with similar expressions: - - Yang: Superconductor Science and Technology 3(12):591 (1999) - - Engel-Herbert: Journal of Applied Physics 97(7):074504 - 074504-4 (2005) - - Camacho: Revista Mexicana de Fisica E 59 (2013) 8-17 - - Avoiding indeterminate forms: - - In the above implementations there are several indeterminate forms - where the limit must be taken. These forms appear at positions - that are extensions of the edges in all xyz-octants except bottQ4. - In the vicinity of these indeterminate forms the formula becomes - numerically instable. - - Chosen solution: use symmetries of the problem to change all - positions to their bottQ4 counterparts. see also - - Cichon: IEEE Sensors Journal, vol. 19, no. 7, April 1, 2019, p.2509 """ + Return BHMJ fields + - treat special cases + - inside-outside checks + """ + + RTOL_SURFACE = 1e-15 # relative distance tolerance to be considered on surface check_field_input(field) @@ -233,13 +208,8 @@ def magnet_cuboid_field( a, b, c = np.abs(dimension.T) / 2 x, y, z = observers.T - # This implementation is completely scale invariant as only observer/dimension - # ratios appear in equations below. - - # dealing with special cases ----------------------------------- - - # allocate B with zeros - B = np.zeros_like(observers, dtype=float) + # allocate for output + BHJM = polarization.astype(float) # SPECIAL CASE 1: polarization = (0,0,0) mask_pol_not_null = ~( @@ -249,50 +219,62 @@ def magnet_cuboid_field( # SPECIAL CASE 2: 0 in dimension mask_dim_not_null = (a * b * c).astype(bool) - mask_gen = mask_pol_not_null & mask_dim_not_null - - mask_inside = None - if in_out == "auto" and field != "B": - # generate in-out mask for auto-input. This is not needed when - # "B" is computed, because the explicit expressions compute B - - # SPECIAL CASE 3: observer lies on-edge/corner - # -> EPSILON to account for numerical imprecision when e.g. rotating - # -> /a /b /c to account for the "missing" scaling (EPSILON is large when - # a is e.g. EPSILON itself) - - # on-surface is not a special case - mask_surf_x = abs(x_dist := abs(x) - a) < RTOL_SURFACE * a # on surface - mask_surf_y = abs(y_dist := abs(y) - b) < RTOL_SURFACE * b # on surface - mask_surf_z = abs(z_dist := abs(z) - c) < RTOL_SURFACE * c # on surface - - # inside-outside - mask_inside_x = x_dist < RTOL_SURFACE * a - mask_inside_y = y_dist < RTOL_SURFACE * b - mask_inside_z = z_dist < RTOL_SURFACE * c - mask_inside = mask_inside_x & mask_inside_y & mask_inside_z - - # on edge - mask_xedge = mask_surf_y & mask_surf_z & mask_inside_x - mask_yedge = mask_surf_x & mask_surf_z & mask_inside_y - mask_zedge = mask_surf_x & mask_surf_y & mask_inside_z - mask_not_edge = ~(mask_xedge | mask_yedge | mask_zedge) - - mask_gen = mask_gen & mask_not_edge - elif in_out != "auto": - mask_inside = np.full(len(observers), in_out == "inside") - - # continue only with general cases - if np.any(mask_gen) and field in "BH": - B[mask_gen] = _magnet_cuboid_field_B( - observers[mask_gen], - dimension[mask_gen], - polarization[mask_gen], - ) - return convert_HBMJ( - output_field_type=field, - polarization=polarization, - input_field_type="B", - field_values=B, - mask_inside=mask_inside, + # SPECIAL CASE 3: observer lies on-edge/corner + # EPSILON to account for numerical imprecision when e.g. rotating + # /a /b /c to account for the missing scaling (EPSILON is large when + # a is e.g. EPSILON itself) + + # on-surface is not a special case + mask_surf_x = abs(x_dist := abs(x) - a) < RTOL_SURFACE * a # on surface + mask_surf_y = abs(y_dist := abs(y) - b) < RTOL_SURFACE * b # on surface + mask_surf_z = abs(z_dist := abs(z) - c) < RTOL_SURFACE * c # on surface + + # inside-outside + mask_inside_x = x_dist < RTOL_SURFACE * a + mask_inside_y = y_dist < RTOL_SURFACE * b + mask_inside_z = z_dist < RTOL_SURFACE * c + mask_inside = mask_inside_x & mask_inside_y & mask_inside_z + + # on edge (requires on-surface and inside-outside) + mask_xedge = mask_surf_y & mask_surf_z & mask_inside_x + mask_yedge = mask_surf_x & mask_surf_z & mask_inside_y + mask_zedge = mask_surf_x & mask_surf_y & mask_inside_z + mask_not_edge = ~(mask_xedge | mask_yedge | mask_zedge) + + mask_gen = mask_pol_not_null & mask_dim_not_null & mask_not_edge + + # @alex: + # 1. previous computation did not remove edge cases from general mask + # --> mask inside must also be computed for B + # 2. Use already allocated array for outputs, no copy, I changed the name to BHJM + # 3. How the field is translates to BHMJ should be right here! It might be more code, + # and even be repetitive with other core functions, but this computation + # is slightly different for almost all classes, and it must be understood what + # is happening from a physics point of view. The argument change once for all does + # not hold here - its many individual cases. + # 4. This new layer is now called BHJM_whatever_whatever + + if field == "J": + BHJM[~mask_inside] *= 0 + return BHJM + + if field == "M": + BHJM[~mask_inside] *= 0 + return BHJM / MU0 + + BHJM *= 0.0 # return (0,0,0) for all special cases + BHJM[mask_gen] = magnet_cuboid_Bfield( + observers=observers[mask_gen], + dimensions=dimension[mask_gen], + polarizations=polarization[mask_gen], + ) + if field == "B": + return BHJM + + if field == "H": + BHJM[mask_inside] -= polarization[mask_inside] + return BHJM / MU0 + + raise ValueError( # pragma: no cover + "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" ) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 50f3c1312..87c9cc0c9 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -10,30 +10,36 @@ from magpylib._src.fields.special_cel import cel from magpylib._src.input_checks import check_field_input from magpylib._src.utility import cart_to_cyl_coordinates -from magpylib._src.utility import convert_HBMJ from magpylib._src.utility import cyl_field_to_cart from magpylib._src.utility import MU0 -def fieldB_cylinder_axial(z0: np.ndarray, r: np.ndarray, z: np.ndarray) -> list: +def magnet_cylinder_Bfield_axialM( + *, z0: np.ndarray, r: np.ndarray, z: np.ndarray +) -> list: """ - B-field in Cylindrical CS of Cylinder magnet with homogenous axial unit - magnetization. The Cylinder axis coincides with the z-axis of the - CS. The geometric center of the Cylinder is in the origin. + B-field of axially magnetized cylinders. - Implementation from [Derby2009]. + The cylinder axis coincides with the z-axis and the geometric center of the + cylinder lies in the origin. The computation is performed in Cylinder coordinates. + Length inputs are made dimensionless by division over the cylinder radii. + Unit polarization is assumed. Implementation based on Derby, American Journal + of Physics 78.3 (2010): 229-235. Parameters ---------- - dim: ndarray, shape (n,2) - dimension of cylinder (d, h), diameter and height, in units of m - pos_obs: ndarray, shape (n,2) - position of observer (r,z) in cylindrical coordinates in units of m + z0: ndarray, shape (n) + Ratio of cylinder height over cylinder radius. + + r: ndarray, shape (n) + Ratio of radial observer position over cylinder radius. + + z: Ratio of axial observer position over cylinder radius. Returns ------- - B-field: ndarray - B-field array of shape (n,2) in cylindrical coordinates (Br,Bz) in units of T. + B-Field: tuple, (Br, Bz) + B-field generated by Cylinders at observer positions in units of tesla. """ n = len(z0) @@ -63,40 +69,43 @@ def fieldB_cylinder_axial(z0: np.ndarray, r: np.ndarray, z: np.ndarray) -> list: / np.pi ) - return Br, Bz + return np.row_stack((Br, np.zeros(n), Bz)) + # return Br, Bz -def fieldH_cylinder_diametral( +def magnet_cylinder_Hfield_diametralM( + *, z0: np.ndarray, r: np.ndarray, - phi: np.ndarray, z: np.ndarray, -) -> np.ndarray: + phi: np.ndarray, +) -> list: """ - H-field in Cylindrical CS of Cylinder magnet with homogenous - diametral unit magnetization. The Cylinder axis coincides with the z-axis of the - CS. The geometric center of the Cylinder is in the origin. - - Implementation from [Rauber2021]. - - H-Field computed analytically via the magnetic scalar potential. Final integration - reduced to complete elliptic integrals. + B-field of diametrally magnetized cylinders. - Numerical Instabilities: See discussion on GitHub. + The cylinder axis coincides with the z-axis and the geometric center of the + cylinder lies in the origin. The computation is performed in Cylinder coordinates. + Length inputs are made dimensionless by division over the cylinder radii. + Unit magnetization is assumed. Implementation partially based on Caciagli: + Journal of Magnetism and Magnetic Materials 456 (2018): 423-432, and + [Ortner, Leitner, Rauber] (unpublished). Parameters ---------- - dim: ndarray, shape (n,2) - dimension of cylinder (d, h), diameter and height, in units of m - tetta: ndarray, shape (n,) - angle between magnetization vector and x-axis in [rad]. M = (cos(tetta), sin(tetta), 0) - obs_pos: ndarray, shape (n,3) - position of observer (r,phi,z) in cylindrical coordinates in units of m and rad + z0: ndarray, shape (n) + Ratio of cylinder height over cylinder radius. + + r: ndarray, shape (n) + Ratio of radial observer position over cylinder radius. + + z: Ratio of axial observer position over cylinder radius. + + phi: Azimuth angle between observer and magnetization direction. Returns ------- - H-field: ndarray - H-field array of shape (n,3) in cylindrical coordinates (Hr, Hphi, Hz) in units of A/m. + B-Field: tuple, (Hr, Hphi, Hz) + H-field generated by Cylinders at observer positions in units of A/m. """ # pylint: disable=too-many-statements @@ -246,11 +255,11 @@ def fieldH_cylinder_diametral( ) ) - return Hr, Hphi, Hz + return np.row_stack((Hr, Hphi, Hz)) + # return Hr, Hphi, Hz -# CORE -def magnet_cylinder_field( +def BHJM_magnet_cylinder( *, field: str, observers: np.ndarray, @@ -258,73 +267,11 @@ def magnet_cylinder_field( polarization: np.ndarray, in_out="auto", ) -> np.ndarray: - """Field (B, H, J, or M) of homogeneously magnetized cylinders. - - The cylinder axis coincides with the z-axis and the geometric center of the - cylinder lies in the origin. - - SI units are used by default for all inputs and outputs. - - Parameters - ---------- - field: str, {'B', 'H', 'J', 'M'} - Select which field to compute: 'B' computes the B-field in units of tesla, - 'H' computes the H-field in units of A/m, 'J' computes the magnetic - polarization in units of tesla, and 'M' the magnetization in units of A/m. - - observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of meter. - - dimension: ndarray, shape (n,2) - Cylinder dimension (d,h) with diameter d and height h in units of meter. - - polarization: ndarray, shape (n,3) - Magnetic polarization vectors in units of tesla. - - in_out: str, {'auto', 'inside', 'outside'} - Give additional information about observer position relative to the magnet body, - affecting the computation speed. With 'auto' (default) the inside-outside - evaluation is done by Magpylib which adds to the computation overhead. - With 'inside' all observers are assumed inside, with 'outside' all observers - are assumed outside. - - Returns - ------- - Field (B, H, J, or M): ndarray, shape (n,3) - B-field in tesla, H-field in A/m, J-field in tesla, or M-field in A/m. - - Examples - -------- - Compute the B-field of two different cylinder magnets at position (1,0,0). - - >>> import numpy as np - >>> import magpylib as magpy - >>> B = magpy.core.magnet_cylinder_field( - ... field='B', - ... observers=np.array([(1,0,0), (1,0,0)]), - ... dimension=np.array([(1,1), (1,3)]), - ... polarization=np.array([(0,0,1), (.5,0,.5)]), - ... ) - >>> print(B) - [[ 0. 0. -0.05185272] - [ 0.06821654 0. -0.01576545]] - - Notes - ----- - Advanced unit use: The input unit of magnetization and polarization - gives the output unit of H and B. All results are independent of the - length input units. One must be careful, however, to use consistently - the same length unit throughout a script. - - Axial implementation based on - - Derby: American Journal of Physics 78.3 (2010): 229-235. - - Diametral implementation based on - - Caciagli: Journal of Magnetism and Magnetic Materials 456 (2018): 423-432. - - Leitner/Rauber/Orter: WIP + """ + Prepare BHJM returns + - Merge axial B with diametral H field + - special cases + - inside-outside checks """ check_field_input(field) @@ -334,41 +281,37 @@ def magnet_cylinder_field( r0, z0 = dimension.T / 2 # scale invariance (make dimensionless) - r = np.copy(r / r0) - z = np.copy(z / r0) - z0 = np.copy(z0 / r0) - - # allocate field vectors ---------------------------------------- - Br, Bphi, Bz = np.zeros((3, len(r)), dtype=float) - - if in_out == "auto": - # inside/outside - mask_between_bases = np.abs(z) <= z0 # in-between top and bottom plane - mask_inside_hull = r <= 1 # inside Cylinder hull plane - mask_inside = mask_between_bases & mask_inside_hull - - # special case: on Cylinder edge - mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane - mask_on_bases = np.isclose( - abs(z), z0, rtol=1e-15, atol=0 - ) # on top or bottom plane - mask_not_on_edge = ~(mask_on_hull & mask_on_bases) - else: - mask_inside = np.full(len(observers), in_out == "inside") - mask_not_on_edge = np.full(len(observers), True) - - if field in "MJ": - return convert_HBMJ( - output_field_type=field, - polarization=polarization, - mask_inside=mask_inside, - ) + r = r / r0 + z = z / r0 + z0 = z0 / r0 + + # allocate for output + BHJM = polarization.astype(float) + + # inside/outside + mask_between_bases = np.abs(z) <= z0 # in-between top and bottom plane + mask_inside_hull = r <= 1 # inside Cylinder hull plane + mask_inside = mask_between_bases & mask_inside_hull + + if field == "J": + BHJM[~mask_inside] *= 0 + return BHJM + + if field == "M": + BHJM[~mask_inside] *= 0 + return BHJM / MU0 + + # SPECIAL CASE 1: on Cylinder edge + mask_on_hull = np.isclose(r, 1, rtol=1e-15, atol=0) # on Cylinder hull plane + mask_on_bases = np.isclose(abs(z), z0, rtol=1e-15, atol=0) # on top or bottom plane + mask_not_on_edge = ~(mask_on_hull & mask_on_bases) + # axial/transv polarization cases pol_x, pol_y, pol_z = polarization.T mask_pol_tv = (pol_x != 0) | (pol_y != 0) mask_pol_ax = pol_z != 0 - # special case: pol = 0 + # SPECIAL CASE 2: pol = 0 mask_pol_not_null = ~((pol_x == 0) * (pol_y == 0) * (pol_z == 0)) # general case @@ -379,42 +322,78 @@ def magnet_cylinder_field( mask_pol_ax = mask_pol_ax & mask_gen mask_inside = mask_inside & mask_gen + # allocate + BHJM *= 0 + # Br, Bphi, Bz = np.zeros((3, len(r)), dtype=float) # transversal polarization contributions ----------------------- if any(mask_pol_tv): pol_xy = np.sqrt(pol_x**2 + pol_y**2)[mask_pol_tv] tetta = np.arctan2(pol_y[mask_pol_tv], pol_x[mask_pol_tv]) - Br_tv, Bphi_tv, Bz_tv = fieldH_cylinder_diametral( - z0[mask_pol_tv], - r[mask_pol_tv], - phi[mask_pol_tv] - tetta, - z[mask_pol_tv], - ) + # Br_tv, Bphi_tv, Bz_tv = magnet_cylinder_Hfield_diametralM( + # z0=z0[mask_pol_tv], + # r=r[mask_pol_tv], + # z=z[mask_pol_tv], + # phi=phi[mask_pol_tv] - tetta, + # ) + # Br[mask_pol_tv] += pol_xy * Br_tv + # Bphi[mask_pol_tv] += pol_xy * Bphi_tv + # Bz[mask_pol_tv] += pol_xy * Bz_tv # add to H-field (inside pol_xy is missing for B !!!) - Br[mask_pol_tv] += pol_xy * Br_tv - Bphi[mask_pol_tv] += pol_xy * Bphi_tv - Bz[mask_pol_tv] += pol_xy * Bz_tv + BHJM[mask_pol_tv] = ( + magnet_cylinder_Hfield_diametralM( + z0=z0[mask_pol_tv], + r=r[mask_pol_tv], + z=z[mask_pol_tv], + phi=phi[mask_pol_tv] - tetta, + ) + * pol_xy + ).T - # axial polarization contributions ----------------------------- + # axial polarization contributions ---------------------------- if any(mask_pol_ax): - Br_ax, Bz_ax = fieldB_cylinder_axial( - z0[mask_pol_ax], r[mask_pol_ax], z[mask_pol_ax] - ) - Br[mask_pol_ax] += pol_z[mask_pol_ax] * Br_ax - Bz[mask_pol_ax] += pol_z[mask_pol_ax] * Bz_ax + # Br_ax, _, Bz_ax = magnet_cylinder_Bfield_axialM( + # z0=z0[mask_pol_ax], r=r[mask_pol_ax], z=z[mask_pol_ax] + # ) + # Br[mask_pol_ax] += pol_z[mask_pol_ax] * Br_ax + # Bz[mask_pol_ax] += pol_z[mask_pol_ax] * Bz_ax + + BHJM[mask_pol_ax] += ( + magnet_cylinder_Bfield_axialM( + z0=z0[mask_pol_ax], + r=r[mask_pol_ax], + z=z[mask_pol_ax], + ) + * pol_z[mask_pol_ax] + ).T + + # transform field to cartesian CS + # Bx, By = cyl_field_to_cart(phi, Br, Bphi) + BHJM[:, 0], BHJM[:, 1] = cyl_field_to_cart(phi, BHJM[:, 0], BHJM[:, 1]) - # transform field to cartesian CS ------------------------------- - Bx, By = cyl_field_to_cart(phi, Br, Bphi) + # Bx = BHJM[:,0] + # By = BHJM[:,1] + # Bz = BHJM[:,2] - # add/subtract Mag when inside for B/H -------------------------- + # add/subtract Mag when inside for B/H if field == "B": mask_tv_inside = mask_pol_tv * mask_inside if any(mask_tv_inside): # tv computes H-field - Bx[mask_tv_inside] += pol_x[mask_tv_inside] - By[mask_tv_inside] += pol_y[mask_tv_inside] - return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T - - mask_ax_inside = mask_pol_ax * mask_inside - if any(mask_ax_inside): # ax computes B-field - Bz[mask_ax_inside] -= pol_z[mask_ax_inside] - return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 + # Bx[mask_tv_inside] += pol_x[mask_tv_inside] + # By[mask_tv_inside] += pol_y[mask_tv_inside] + BHJM[mask_tv_inside, 0] += pol_x[mask_tv_inside] + BHJM[mask_tv_inside, 1] += pol_y[mask_tv_inside] + return BHJM + # return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T + + if field == "H": + mask_ax_inside = mask_pol_ax * mask_inside + if any(mask_ax_inside): # ax computes B-field + # Bz[mask_ax_inside] -= pol_z[mask_ax_inside] + BHJM[mask_ax_inside, 2] -= pol_z[mask_ax_inside] + return BHJM / MU0 + # return np.concatenate(((Bx,), (By,), (Bz,)), axis=0).T / MU0 + + raise ValueError( # pragma: no cover + "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" + ) diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index fc92f18d0..d495afe15 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -7,7 +7,7 @@ from scipy.special import ellipeinc from scipy.special import ellipkinc -from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_field +from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.fields.special_el3 import el3_angle from magpylib._src.input_checks import check_field_input from magpylib._src.utility import convert_HBMJ @@ -2327,7 +2327,7 @@ def magnet_cylinder_segment_field_internal( # case2: full cylinder mask1x = ~mask1 - BHfinal[mask1x] = magnet_cylinder_field( + BHfinal[mask1x] = BHJM_magnet_cylinder( field=field, observers=observers[mask1x], polarization=polarization[mask1x], @@ -2336,7 +2336,7 @@ def magnet_cylinder_segment_field_internal( # case2a: hollow cylinder <- should be vectorized together with above mask2 = (r1 != 0) & mask1x - BHfinal[mask2] -= magnet_cylinder_field( + BHfinal[mask2] -= BHJM_magnet_cylinder( field=field, observers=observers[mask2], polarization=polarization[mask2], diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 2e3e4646d..665dd85e0 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -1,6 +1,6 @@ """Magnet Cuboid class code""" from magpylib._src.display.traces_core import make_Cuboid -from magpylib._src.fields.field_BH_cuboid import magnet_cuboid_field +from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet from magpylib._src.utility import unit_prefix @@ -74,7 +74,7 @@ class Cuboid(BaseMagnet): [-0.03557183 0.00646436 0.14943466]] """ - _field_func = staticmethod(magnet_cuboid_field) + _field_func = staticmethod(BHJM_magnet_cuboid) _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_Cuboid diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 61b289334..2af97d5ce 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -1,6 +1,6 @@ """Magnet Cylinder class code""" from magpylib._src.display.traces_core import make_Cylinder -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_field +from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet from magpylib._src.utility import unit_prefix @@ -85,7 +85,7 @@ class Cylinder(BaseMagnet): [1.25715233e-04 2.01445027e-04 1.31238931e-05]] """ - _field_func = staticmethod(magnet_cylinder_field) + _field_func = staticmethod(BHJM_magnet_cylinder) _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_Cylinder diff --git a/magpylib/core/__init__.py b/magpylib/core/__init__.py index 03aed10d3..b1c4022f8 100644 --- a/magpylib/core/__init__.py +++ b/magpylib/core/__init__.py @@ -3,27 +3,11 @@ """ __all__ = [ - "dipole_field", - "current_circle_field", - "current_loop_field", - "current_line_field", - "current_polyline_field", - "magnet_sphere_field", - "magnet_cuboid_field", - "magnet_cylinder_field", - "magnet_cylinder_segment_field", - "triangle_field", - "magnet_tetrahedron_field", + "magnet_cuboid_Bfield", + "magnet_cylinder_Bfield_axialM", + "magnet_cylinder_Hfield_diametralM", ] -from magpylib._src.fields.field_BH_dipole import dipole_field -from magpylib._src.fields.field_BH_circle import current_circle_field -from magpylib._src.fields.field_BH_circle import current_loop_field -from magpylib._src.fields.field_BH_polyline import current_line_field -from magpylib._src.fields.field_BH_polyline import current_polyline_field -from magpylib._src.fields.field_BH_sphere import magnet_sphere_field -from magpylib._src.fields.field_BH_cuboid import magnet_cuboid_field -from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_field -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field -from magpylib._src.fields.field_BH_triangle import triangle_field -from magpylib._src.fields.field_BH_tetrahedron import magnet_tetrahedron_field +from magpylib._src.fields.field_BH_cuboid import magnet_cuboid_Bfield +from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_Bfield_axialM +from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_Hfield_diametralM diff --git a/tests/test_core_misc.py b/tests/test_core_misc.py index d6e790357..a4fa261d9 100644 --- a/tests/test_core_misc.py +++ b/tests/test_core_misc.py @@ -4,20 +4,20 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibDeprecationWarning +from magpylib._src.fields.field_BH_circle import current_circle_field +from magpylib._src.fields.field_BH_circle import current_loop_field +from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid +from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder +from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field +from magpylib._src.fields.field_BH_dipole import dipole_field +from magpylib._src.fields.field_BH_polyline import current_line_field +from magpylib._src.fields.field_BH_polyline import current_polyline_field from magpylib._src.fields.field_BH_polyline import current_vertices_field +from magpylib._src.fields.field_BH_sphere import magnet_sphere_field +from magpylib._src.fields.field_BH_tetrahedron import magnet_tetrahedron_field +from magpylib._src.fields.field_BH_triangle import triangle_field from magpylib._src.fields.field_BH_triangularmesh import magnet_trimesh_field from magpylib._src.utility import MU0 -from magpylib.core import current_circle_field -from magpylib.core import current_line_field -from magpylib.core import current_loop_field -from magpylib.core import current_polyline_field -from magpylib.core import dipole_field -from magpylib.core import magnet_cuboid_field -from magpylib.core import magnet_cylinder_field -from magpylib.core import magnet_cylinder_segment_field -from magpylib.core import magnet_sphere_field -from magpylib.core import magnet_tetrahedron_field -from magpylib.core import triangle_field ####################################################################################### @@ -41,7 +41,7 @@ def helper_check_HBMJ_consistency(func, **kw): return H, B, M, J -def test_magnet_cuboid_field_BHMJ(): +def test_BHJM_magnet_cuboid(): """test cuboid field""" pol = np.array( [ @@ -78,7 +78,7 @@ def test_magnet_cuboid_field_BHMJ(): "polarization": pol, "dimension": dim, } - H, B, _, J = helper_check_HBMJ_consistency(magnet_cuboid_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(BHJM_magnet_cuboid, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -103,12 +103,12 @@ def test_magnet_cuboid_field_BHMJ(): Jtest = np.array([(0, 0, 0)] * 5 + [(1, 2, 3)]) np.testing.assert_allclose(J, Jtest, rtol=1e-5) - H_inout = magnet_cuboid_field(field="H", in_out="outside", **kw) - Htest_inout = Htest + Jtest / MU0 - np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) + # H_inout = BHJM_magnet_cuboid(field="H", in_out="outside", **kw) + # Htest_inout = Htest + Jtest / MU0 + # np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) -def test_magnet_cylinder_field_BHJM(): +def test_BHJM_magnet_cylinder(): """test cylinder field computation""" pol = np.array( [ @@ -140,7 +140,7 @@ def test_magnet_cylinder_field_BHJM(): "polarization": pol, "dimension": dim, } - H, B, _, J = helper_check_HBMJ_consistency(magnet_cylinder_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(BHJM_magnet_cylinder, **kw) Btest = [ [0.0, 0.0, 0.0], @@ -161,9 +161,9 @@ def test_magnet_cylinder_field_BHJM(): Jtest = np.array([(0, 0, 0), (0, 0, 0), (0, 0, 0), (1, 1, 1)]) np.testing.assert_allclose(J, Jtest) - H_inout = magnet_cylinder_field(field="H", in_out="outside", **kw) - Htest_inout = Htest - Jtest / MU0 - np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) + # H_inout = BHJM_magnet_cylinder(field="H", in_out="outside", **kw) + # Htest_inout = Htest - Jtest / MU0 + # np.testing.assert_allclose(H_inout, Htest_inout, rtol=1e-5) def test_magnet_sphere_field_BH(): diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 610d90c72..b380b3b69 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -1,6 +1,9 @@ import numpy as np import magpylib as magpy +from magpylib._src.fields.field_BH_circle import current_circle_field +from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid +from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.utility import MU0 # PHYSICS CONSISTENCY TESTING @@ -18,6 +21,8 @@ # Geometric approximation testing should give similar results for different # implementations when one geometry is constructed from another # +# Scaling invariance of solutions +# # ----------> Circle # webpage numbers # Circle <> Dipole # mom = I*A (far field approx) # Polyline <> Dipole # mom = I*A (far field approx) @@ -221,7 +226,7 @@ def test_core_physics_long_solenoid(): BHz_long *= MU0 # SOLENOID TEST constructed from circle fields - BH = magpy.core.current_circle_field( + BH = current_circle_field( field=field, observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), diameter=np.array([2 * R] * N), @@ -238,7 +243,7 @@ def test_core_physics_long_solenoid(): obs = np.array([(0, 0, 0)]) # cylinder - BHz_cyl = magpy.core.magnet_cylinder_field( + BHz_cyl = BHJM_magnet_cylinder( field=field, observers=obs, dimension=np.array([(2 * R, L)]), @@ -249,7 +254,7 @@ def test_core_physics_long_solenoid(): np.testing.assert_allclose(BHz_long, BHz_cyl, rtol=1e-5) # cuboid - BHz_cub = magpy.core.magnet_cuboid_field( + BHz_cub = BHJM_magnet_cuboid( field=field, observers=obs, dimension=np.array([(2 * R, 2 * R, L)]), @@ -275,7 +280,7 @@ def test_core_physics_current_replacement(): obs = np.array([(1.5, -2, -1.123)]) Jz = 1 - Hz_cyl = magpy.core.magnet_cylinder_field( + Hz_cyl = BHJM_magnet_cylinder( field="H", observers=obs, dimension=np.array([(2 * R, L)]), @@ -284,7 +289,7 @@ def test_core_physics_current_replacement(): N = 1000 # current discretization I = Jz / MU0 / N * L - H = magpy.core.current_circle_field( + H = current_circle_field( field="H", observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N) + obs, diameter=np.array([2 * R] * N), @@ -303,7 +308,7 @@ def test_core_physics_geometry_cylinder_from_segments(): obs = np.array([(1, 2, 3), (0.23, 0.132, 0.123)]) pol = np.array([(2, 0.123, 3), (-0.23, -1, 0.434)]) - B_cyl = magpy.core.magnet_cylinder_field( + B_cyl = BHJM_magnet_cylinder( field="B", observers=obs, dimension=np.array([(2 * r, h)] * 2), @@ -339,7 +344,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dim = np.array([(2, 2, 2)] * 2) vol = 8 pol = mom / vol * MU0 - Bcub = magpy.core.magnet_cuboid_field( + Bcub = BHJM_magnet_cuboid( field="H", observers=obs, dimension=dim, @@ -350,7 +355,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dim = np.array([(0.5, 0.5)] * 2) vol = 0.25**2 * np.pi * 0.5 pol = mom / vol * MU0 - Bcyl = magpy.core.magnet_cylinder_field( + Bcyl = BHJM_magnet_cylinder( field="H", observers=obs, dimension=dim, @@ -429,7 +434,7 @@ def test_core_physics_cube_current_replacement(): Jz = 1.23 dim = np.array([(2, 2, h)] * 2) pol = np.array([(0, 0, Jz)] * 2) - Hcub = magpy.core.magnet_cuboid_field( + Hcub = BHJM_magnet_cuboid( field="H", observers=obs, dimension=dim, @@ -486,7 +491,7 @@ def test_core_physics_triangle_cube_geometry(): obs = np.array([(3, 4, 5)]) mag = np.array([(0, 0, 333)]) dim = np.array([(2, 2, 2)]) - bb = magpy.core.magnet_cuboid_field( + bb = BHJM_magnet_cuboid( field="B", observers=obs, dimension=dim, @@ -595,13 +600,13 @@ def test_core_physics_Tetrahedron_VS_Cuboid(): obs1 = np.reshape(obs, (1, 3)) mag1 = np.reshape(mag, (1, 3)) dim = np.array([(2, 2, 2)]) - bb = magpy.core.magnet_cuboid_field( + bb = BHJM_magnet_cuboid( field="B", observers=obs1, polarization=mag1, dimension=dim, )[0] - hh = magpy.core.magnet_cuboid_field( + hh = BHJM_magnet_cuboid( field="H", observers=obs1, polarization=mag1, diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index 8aa13eb5c..4b9206430 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -5,6 +5,7 @@ import pytest import magpylib as magpy +from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_core from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field @@ -468,7 +469,7 @@ def test_cylinder_diametral_small_r(): test if the transition from Taylor series to general case is smooth test if the gneral case fluctuations are small """ - B = magpy.core.magnet_cylinder_field( + B = BHJM_magnet_cylinder( field="B", observers=np.array([(x, 0, 3) for x in np.logspace(-1.4, -1.2, 1000)]), polarization=np.array([(1, 1, 0)] * 1000), @@ -490,7 +491,7 @@ def test_cyl_vs_cylseg_axial_H_inside_mask(): dims = np.array([(1, 1)]) dims_cs = np.array([(0, 0.5, 1, 0, 360)]) - Bc = magpy.core.magnet_cylinder_field( + Bc = BHJM_magnet_cylinder( field=field, observers=obs, dimension=dims, diff --git a/tests/test_obj_Cuboid.py b/tests/test_obj_Cuboid.py index ce8c7ef33..32b85af1a 100644 --- a/tests/test_obj_Cuboid.py +++ b/tests/test_obj_Cuboid.py @@ -4,9 +4,9 @@ import numpy as np import magpylib as magpy +from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.obj_classes.class_Sensor import Sensor - # # # """data generation for test_Cuboid()""" # # N = 100 @@ -99,12 +99,8 @@ def test_cuboid_object_vs_lib(): pol = np.array([(10, 20, 30)]) dim = np.array([(a, a, a)]) pos = np.array([(2 * a, 2 * a, 2 * a)]) - B0 = magpy.core.magnet_cuboid_field( - field="B", observers=pos, polarization=pol, dimension=dim - ) - H0 = magpy.core.magnet_cuboid_field( - field="H", observers=pos, polarization=pol, dimension=dim - ) + B0 = BHJM_magnet_cuboid(field="B", observers=pos, polarization=pol, dimension=dim) + H0 = BHJM_magnet_cuboid(field="H", observers=pos, polarization=pol, dimension=dim) src = magpy.magnet.Cuboid(polarization=pol[0], dimension=dim[0]) B1 = src.getB(pos) From acfa53a2c9f669163af9e0462246157b4dbacff5 Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 28 Jan 2024 20:46:07 +0100 Subject: [PATCH 215/240] BHJM dipole --- docs/_pages/docu/docu_magpylib_api.md | 2 +- magpylib/_src/fields/field_BH_dipole.py | 92 +++++++++---------- .../_src/obj_classes/class_misc_Dipole.py | 4 +- .../{test_core_misc.py => test_BHMJ_level.py} | 6 +- tests/test_core_physics_consistency.py | 19 ++-- tests/test_input_checks.py | 6 +- 6 files changed, 65 insertions(+), 64 deletions(-) rename tests/{test_core_misc.py => test_BHMJ_level.py} (99%) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index 3efdc7df3..246290292 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -744,7 +744,7 @@ At the heart of Magpylib lies a set of core functions that are our implementatio **magnet_tetrahedron_field(** `field`, `observers`, `polarization`, `vertices`**)** ::: :::{grid-item} -**dipole_field(** `field`, `observers`, `moment`**)** +**BHJM_dipole(** `field`, `observers`, `moment`**)** ::: :::{grid-item} **triangle_field(** `field`, `observers`, `polarization`, `vertices`**)** diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index dd9aac158..d5f955b58 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -8,81 +8,81 @@ # CORE -def dipole_field( +def dipole_Hfield( *, - field: str, observers: np.ndarray, - moment: np.ndarray, + moments: np.ndarray, ) -> np.ndarray: """Magnetic field of a dipole moments. - The dipole moment lies in the origin of the coordinate system. - - SI units are used for all inputs and outputs. + The dipole moment lies in the origin of the coordinate system. SI units are used + for all inputs and outputs. Returns np.inf for all non-zero moment components + in the origin. Parameters ---------- - field: str, default=`'B'` - If `field='B'` return B-field in units of T, if `field='H'` return H-field - in units of A/m. - observers: ndarray, shape (n,3) Observer positions (x,y,z) in Cartesian coordinates in units of m. - moment: ndarray, shape (n,3) + moments: ndarray, shape (n,3) Dipole moment vector in units of A·m². Returns ------- - B-field or H-field: ndarray, shape (n,3) - B- or H-field of source in Cartesian coordinates in units of T or A/m. - - Examples - -------- - Compute the B-field of two different dipole-observer instances. - - >>> import magpylib as magpy - >>> import numpy as np - >>> B = magpy.core.dipole_field( - ... field="B", - ... observers=np.array([(1,2,3), (-1,-2,-3)]), - ... moment=np.array([(0,0,1e6), (1e5,0,1e7)]) - ... ) - >>> print(B) - [[0.00122722 0.00245444 0.00177265] - [0.01212221 0.02462621 0.01784923]] + H-field: ndarray, shape (n,3) + H-field of dipole in Cartesian coordinates in units of A/m. Notes ----- - Advanced unit use: The input unit of magnetization and polarization - gives the output unit of H and B. All results are independent of the - length input units. One must be careful, however, to use consistently - the same length unit throughout a script. - The moment of a magnet is given by its volume*magnetization. """ - check_field_input(field) - if field in "MJ": - return np.zeros_like(observers, dtype=float) x, y, z = observers.T r = np.sqrt(x**2 + y**2 + z**2) # faster than np.linalg.norm with np.errstate(divide="ignore", invalid="ignore"): # 0/0 produces invalid warn and results in np.nan # x/0 produces divide warn and results in np.inf - B = ( - 3 * np.sum(moment * observers, axis=1) * observers.T / r**5 - - moment.T / r**3 - ).T * 1e-7 - - # when r=0 return np.inf in all non-zero moment directions + H = ( + ( + 3 * np.sum(moments * observers, axis=1) * observers.T / r**5 + - moments.T / r**3 + ).T + / 4 + / np.pi + ) + + # when r=0 return np.inf in all non-zero moments directions mask1 = r == 0 if np.any(mask1): with np.errstate(divide="ignore", invalid="ignore"): - B[mask1] = moment[mask1] / 0.0 - np.nan_to_num(B, copy=False, posinf=np.inf, neginf=np.NINF) + H[mask1] = moments[mask1] / 0.0 + np.nan_to_num(H, copy=False, posinf=np.inf, neginf=np.NINF) + + return H + + +def BHJM_dipole( + *, + field: str, + observers: np.ndarray, + moment: np.ndarray, +) -> np.ndarray: + check_field_input(field) + + if field in "MJ": + return np.zeros_like(observers, dtype=float) + + H = dipole_Hfield( + observers=observers, + moments=moment, + ) + + if field == "H": + return H if field == "B": - return B + return H * MU0 - return B / MU0 + raise ValueError( # pragma: no cover + "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" + ) diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index ae18537df..192cc8928 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -2,7 +2,7 @@ import numpy as np from magpylib._src.display.traces_core import make_Dipole -from magpylib._src.fields.field_BH_dipole import dipole_field +from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseSource from magpylib._src.style import DipoleStyle @@ -81,7 +81,7 @@ class Dipole(BaseSource): [0.0100802 0.01720799 0.00712778]] """ - _field_func = staticmethod(dipole_field) + _field_func = staticmethod(BHJM_dipole) _field_func_kwargs_ndim = {"moment": 2} _style_class = DipoleStyle get_trace = make_Dipole diff --git a/tests/test_core_misc.py b/tests/test_BHMJ_level.py similarity index 99% rename from tests/test_core_misc.py rename to tests/test_BHMJ_level.py index a4fa261d9..b32b191b7 100644 --- a/tests/test_core_misc.py +++ b/tests/test_BHMJ_level.py @@ -9,7 +9,7 @@ from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field -from magpylib._src.fields.field_BH_dipole import dipole_field +from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.fields.field_BH_polyline import current_line_field from magpylib._src.fields.field_BH_polyline import current_polyline_field from magpylib._src.fields.field_BH_polyline import current_vertices_field @@ -529,7 +529,7 @@ def test_current_polyline_field_BH(): np.testing.assert_allclose(M, Mtest, rtol=1e-06) -def test_dipole_field_BH(): +def test_BHJM_dipole(): """Test of dipole field core function""" pol = np.array([(0, 0, 1), (1, 0, 1), (-1, 0.321, 0.123)]) @@ -537,7 +537,7 @@ def test_dipole_field_BH(): "observers": np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]), "moment": pol * 4 * np.pi / 3 / MU0, } - H, B, M, _ = helper_check_HBMJ_consistency(dipole_field, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(BHMJ_dipole, **kw) Btest = [ [4.09073329e-03, 8.18146659e-03, 5.90883698e-03], diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index b380b3b69..ec2a1905e 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -4,6 +4,7 @@ from magpylib._src.fields.field_BH_circle import current_circle_field from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder +from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.utility import MU0 # PHYSICS CONSISTENCY TESTING @@ -49,26 +50,26 @@ def test_core_phys_moment_of_current_circle(): curr = np.array([1e3, 1e3]) mom = ((dia / 2) ** 2 * np.pi * curr * np.array([(0, 0, 1)] * 2).T).T - B1 = magpy.core.current_circle_field( + B1 = current_circle_field( field="B", observers=obs, diameter=dia, current=curr, ) - B2 = magpy.core.dipole_field( + B2 = BHJM_dipole( field="B", observers=obs, moment=mom, ) np.testing.assert_allclose(B1, B2, rtol=1e-02) - H1 = magpy.core.current_circle_field( + H1 = current_circle_field( field="H", observers=obs, diameter=dia, current=curr, ) - H2 = magpy.core.dipole_field( + H2 = BHJM_dipole( field="H", observers=obs, moment=mom, @@ -97,7 +98,7 @@ def test_core_phys_moment_of_current_square(): current=curr4, ) B1 = np.sum(B1, axis=0) - B2 = magpy.core.dipole_field( + B2 = magpy.core.BHJM_dipole( field="B", observers=obs1, moment=mom, @@ -112,7 +113,7 @@ def test_core_phys_moment_of_current_square(): current=curr4, ) H1 = np.sum(H1, axis=0) - H2 = magpy.core.dipole_field( + H2 = magpy.core.BHJM_dipole( field="H", observers=obs1, moment=mom, @@ -182,7 +183,7 @@ def test_core_physics_dipole_sphere(): diameter=dia, polarization=pol, ) - B2 = magpy.core.dipole_field( + B2 = magpy.core.BHJM_dipole( field="B", observers=obs, moment=mom, @@ -195,7 +196,7 @@ def test_core_physics_dipole_sphere(): diameter=dia, polarization=pol, ) - H2 = magpy.core.dipole_field( + H2 = magpy.core.BHJM_dipole( field="H", observers=obs, moment=mom, @@ -335,7 +336,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): obs = np.array([(100, 200, 300), (-200, -200, -200)]) mom = np.array([(1e6, 2e6, 3e6)] * 2) - Bdip = magpy.core.dipole_field( + Bdip = magpy.core.BHJM_dipole( field="H", observers=obs, moment=mom, diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 1def8caf1..95ca17363 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -6,7 +6,7 @@ from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.exceptions import MagpylibMissingInput - +from magpylib._src.fields.field_BH_dipole import BHJM_dipole # pylint: disable=unnecessary-lambda-assignment @@ -936,7 +936,7 @@ def test_input_getBH_field_good(field): """good getBH field inputs""" moms = np.array([[1, 2, 3]]) obs = np.array([[1, 2, 3]]) - B = magpy.core.dipole_field(field=field, observers=obs, moment=moms) + B = BHJM_dipole(field=field, observers=obs, moment=moms) assert isinstance(B, np.ndarray) @@ -960,7 +960,7 @@ def test_input_getBH_field_bad(field): moms = np.array([[1, 2, 3]]) obs = np.array([[1, 2, 3]]) with pytest.raises(MagpylibBadUserInput): - magpy.core.dipole_field(field=field, observers=obs, moment=moms) + magpy.core.BHJM_dipole(field=field, observers=obs, moment=moms) def test_sensor_handedness(): From 1806372569eced855fd8fed4f2bb1c67460ec104 Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 28 Jan 2024 21:54:25 +0100 Subject: [PATCH 216/240] BHJM circle --- docs/_pages/docu/docu_magpylib_api.md | 2 +- magpylib/_src/fields/field_BH_circle.py | 170 ++++++++---------- magpylib/_src/fields/field_BH_cylinder.py | 8 +- .../_src/obj_classes/class_current_Circle.py | 6 +- magpylib/core/__init__.py | 4 + tests/test_BHMJ_level.py | 40 ++--- tests/test_core_physics_consistency.py | 66 +++---- tests/test_obj_Circle.py | 2 +- 8 files changed, 137 insertions(+), 161 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index 246290292..b0e9218d6 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -726,7 +726,7 @@ At the heart of Magpylib lies a set of core functions that are our implementatio **current_polyline_field(** `field`, `observers`, `current`, `segment_start`, `segment_end`**)** ::: :::{grid-item} -**current_circle_field(** `field`, `observers`, `current`, `diameter`**)** +**BHJM_circle(** `field`, `observers`, `current`, `diameter`**)** ::: :::{grid-item} **XXX(** `field`, `observers`, `polarization`, `dimension`**)** diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index cf85ec182..6943418cd 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -14,66 +14,88 @@ from magpylib._src.utility import MU0 -# CORE -def current_circle_field( +def current_circle_Bfield( *, - field: str, - observers: np.ndarray, - diameter: np.ndarray, - current: np.ndarray, + r0: np.ndarray, + r: np.ndarray, + z: np.ndarray, ) -> np.ndarray: - """Magnetic field of a circular (line) current loops. - - The loop lies in the z=0 plane with the coordinate origin at its center. + """ + B-field of a circular line-current loops. - SI units are used for all inputs and outputs. + The loop lies in the z=0 plane with the coordinate origin at its center and carries + a unit current of 1A. The output is independent of the length units chosen for observers + and radius r0. Implementation based on "Numerically stable and computationally + efficient expression for the magnetic field of a current loop.", M.Ortner et al, + Magnetism 2023, 3(1), 11-31 Parameters ---------- - field: str, default=`'B'` - If `field='B'` return B-field in units of T, if `field='H'` return H-field - in units of A/m. + r0: ndarray, shape (n) + Radii of loops. - observers: ndarray, shape (n,3) - Observer positions (x,y,z) in Cartesian coordinates in units of m. + r: ndarray, shape (n) + Radial positions of observers. - diameter: ndarray, shape (n,) - Diameter of loop in units of m. - - current: ndarray, shape (n,) - Electrical current in units of A. + z: ndarray, shape (n) + Axial positions of observers. Returns ------- - B-field or H-field: ndarray, shape (n,3) - B- or H-field of source in Cartesian coordinates in units of T or A/m. - - Examples - -------- - Compute the field of three different circular loops at three different positions. - - >>> import numpy as np - >>> import magpylib as magpy - >>> H = magpy.core.current_circle_field( - ... field='H', - ... observers=np.array([(0,0,0), (1,1,1), (2,2,2)]), - ... diameter=np.array([1,2,3]), - ... current=np.array([1,1,2]) - ... ) - >>> print(H) - [[0. 0. 1. ] - [0.0496243 0.0496243 0.02124542] - [0.02833835 0.02833835 0.00654999]] - - Notes - ----- - Advanced unit use: The input unit of magnetization and polarization - gives the output unit of H and B. All results are independent of the - length input units. One must be careful, however, to use consistently - the same length unit throughout a script. - - Implementation based on "Numerically stable and computationally efficient expression for - the magnetic field of a current loop.", M.Ortner et al, Submitted to MDPI Magnetism, 2022 + B-Field: tuple, (Hr, Hz) + B-field generated by Cylinders at observer positions in units of tesla. + """ + n5 = len(r) + + # express through ratios (make dimensionless, avoid large/small input values, stupid) + r = r / r0 + z = z / r0 + + # field computation from paper + z2 = z**2 + x0 = z2 + (r + 1) ** 2 + k2 = 4 * r / x0 + q2 = (z2 + (r - 1) ** 2) / x0 + + k = np.sqrt(k2) + q = np.sqrt(q2) + p = 1 + q + pf = k / np.sqrt(r) / q2 / 20 / r0 * 1e-6 + + # cel* part + cc = k2 * k2 + ss = 2 * cc * q / p + Br = pf * z / r * cel_iter(q, p, np.ones(n5), cc, ss, p, q) + + # cel** part + cc = k2 * (k2 - (q2 + 1) / r) + ss = 2 * k2 * q * (k2 / p - p / r) + Bz = -pf * cel_iter(q, p, np.ones(n5), cc, ss, p, q) + + return Br, Bz + + +print( + current_circle_Bfield( + r0=np.array([1]), + r=np.array([1e-10]), + z=np.array([0]), + ) +) + + +def BHJM_circle( + *, + field: str, + observers: np.ndarray, + diameter: np.ndarray, + current: np.ndarray, +) -> np.ndarray: + """ + Return BHMJ fields + - treat special cases + + """ check_field_input(field) @@ -97,7 +119,7 @@ def current_circle_field( if np.any(mask3): mask4 = mask3 * ~mask1 # only relevant if not also case1 Bz_tot[mask4] = ( - 0.6283185307179587 + 0.6283185307179587e-6 * r0[mask4] ** 2 / (z[mask4] ** 2 + r0[mask4] ** 2) ** (3 / 2) ) @@ -105,40 +127,14 @@ def current_circle_field( # general case mask5 = ~np.logical_or(np.logical_or(mask1, mask2), mask3) if np.any(mask5): - r0 = r0[mask5] - r = r[mask5] - z = z[mask5] - n5 = len(r0) - - # express through ratios (make dimensionless, avoid large/small input values, stupid) - r = r / r0 - z = z / r0 - - # field computation from paper - z2 = z**2 - x0 = z2 + (r + 1) ** 2 - k2 = 4 * r / x0 - q2 = (z2 + (r - 1) ** 2) / x0 - - k = np.sqrt(k2) - q = np.sqrt(q2) - p = 1 + q - pf = k / np.sqrt(r) / q2 / 20 / r0 - - # cel* part - cc = k2 * k2 - ss = 2 * cc * q / p - Br_tot[mask5] = pf * z / r * cel_iter(q, p, np.ones(n5), cc, ss, p, q) - - # cel** part - cc = k2 * (k2 - (q2 + 1) / r) - ss = 2 * k2 * q * (k2 / p - p / r) - Bz_tot[mask5] = -pf * cel_iter(q, p, np.ones(n5), cc, ss, p, q) + Br_gen, Bz_gen = current_circle_Bfield(r0=r0[mask5], r=r[mask5], z=z[mask5]) + Br_tot[mask5] = Br_gen + Bz_tot[mask5] = Bz_gen # transform field to cartesian CS Bx_tot, By_tot = cyl_field_to_cart(phi, Br_tot) B_cart = ( - np.concatenate(((Bx_tot,), (By_tot,), (Bz_tot,)), axis=0) * current * 1e-6 + np.concatenate(((Bx_tot,), (By_tot,), (Bz_tot,)), axis=0) * current ).T # ugly but fast # B or H field @@ -146,17 +142,3 @@ def current_circle_field( return B_cart return B_cart / MU0 - - -def current_loop_field(*args, **kwargs): - """current_loop_field is deprecated, see current_circle_field""" - - warnings.warn( - ( - "current_loop_field is deprecated and will be removed in a future version, " - "use current_circle_field instead." - ), - MagpylibDeprecationWarning, - stacklevel=2, - ) - return current_circle_field(*args, **kwargs) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index 87c9cc0c9..4feadfcaf 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -38,7 +38,7 @@ def magnet_cylinder_Bfield_axialM( Returns ------- - B-Field: tuple, (Br, Bz) + B-Field: np.ndarray, (Br, Bphi, Bz) B-field generated by Cylinders at observer positions in units of tesla. """ n = len(z0) @@ -70,7 +70,6 @@ def magnet_cylinder_Bfield_axialM( ) return np.row_stack((Br, np.zeros(n), Bz)) - # return Br, Bz def magnet_cylinder_Hfield_diametralM( @@ -98,13 +97,14 @@ def magnet_cylinder_Hfield_diametralM( r: ndarray, shape (n) Ratio of radial observer position over cylinder radius. - z: Ratio of axial observer position over cylinder radius. + z: ndarray, shape (n) + Ratio of axial observer position over cylinder radius. phi: Azimuth angle between observer and magnetization direction. Returns ------- - B-Field: tuple, (Hr, Hphi, Hz) + H-Field: np.ndarray, (Hr, Hphi, Hz) H-field generated by Cylinders at observer positions in units of A/m. """ # pylint: disable=too-many-statements diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index a4830722f..b57b0530f 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -3,7 +3,7 @@ from magpylib._src.display.traces_core import make_Circle from magpylib._src.exceptions import MagpylibDeprecationWarning -from magpylib._src.fields.field_BH_circle import current_circle_field +from magpylib._src.fields.field_BH_circle import BHJM_circle from magpylib._src.input_checks import check_format_input_scalar from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent from magpylib._src.utility import unit_prefix @@ -83,7 +83,7 @@ class Circle(BaseCurrent): [-9.85948764e-24 -4.45190261e-05 4.45190261e-05]] """ - _field_func = staticmethod(current_circle_field) + _field_func = staticmethod(BHJM_circle) _field_func_kwargs_ndim = {"current": 1, "diameter": 1} get_trace = make_Circle @@ -136,7 +136,7 @@ class Loop(Circle): def _field_func(*args, **kwargs): """Catch Deprecation warning in getBH_dict""" _deprecation_warn() - return current_circle_field(*args, **kwargs) + return BHJM_circle(*args, **kwargs) def __init__(self, *args, **kwargs): _deprecation_warn() diff --git a/magpylib/core/__init__.py b/magpylib/core/__init__.py index b1c4022f8..425b3f0da 100644 --- a/magpylib/core/__init__.py +++ b/magpylib/core/__init__.py @@ -6,8 +6,12 @@ "magnet_cuboid_Bfield", "magnet_cylinder_Bfield_axialM", "magnet_cylinder_Hfield_diametralM", + "dipole_Hfield", + "current_circle_Bfield", ] from magpylib._src.fields.field_BH_cuboid import magnet_cuboid_Bfield from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_Bfield_axialM from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_Hfield_diametralM +from magpylib._src.fields.field_BH_dipole import dipole_Hfield +from magpylib._src.fields.field_BH_circle import current_circle_Bfield diff --git a/tests/test_BHMJ_level.py b/tests/test_BHMJ_level.py index b32b191b7..0f9af23ed 100644 --- a/tests/test_BHMJ_level.py +++ b/tests/test_BHMJ_level.py @@ -4,8 +4,7 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibDeprecationWarning -from magpylib._src.fields.field_BH_circle import current_circle_field -from magpylib._src.fields.field_BH_circle import current_loop_field +from magpylib._src.fields.field_BH_circle import BHJM_circle from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field @@ -452,14 +451,14 @@ def test_magnet_trimesh_field_BH(): np.testing.assert_allclose(J, Jtest, rtol=1e-06) -def test_current_circle_field_BH(): +def test_BHJM_circle(): """Test of current circle field core function""" kw = { "observers": np.array([(1, 1, 1), (2, 2, 2), (3, 3, 3)]), "current": np.array([1, 1, 2]) * 1e3, "diameter": np.array([2, 4, 6]), } - H, B, M, _ = helper_check_HBMJ_consistency(current_circle_field, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(BHJM_circle, **kw) Btest = ( np.array( @@ -537,7 +536,7 @@ def test_BHJM_dipole(): "observers": np.array([(1, 2, 3), (-1, -2, -3), (3, 3, -1)]), "moment": pol * 4 * np.pi / 3 / MU0, } - H, B, M, _ = helper_check_HBMJ_consistency(BHMJ_dipole, **kw) + H, B, M, _ = helper_check_HBMJ_consistency(BHJM_dipole, **kw) Btest = [ [4.09073329e-03, 8.18146659e-03, 5.90883698e-03], @@ -580,21 +579,6 @@ def test_line_deprecation(): np.testing.assert_allclose(B1, B2) -def test_loop_deprecation(): - """test loop deprecation""" - kw = { - "field": "B", - "observers": np.array([(0, 0, 1)]), - "diameter": np.array([1]), - "current": np.array([1]), - } - B1 = current_circle_field(**kw) - with pytest.warns(MagpylibDeprecationWarning): - B2 = current_loop_field(**kw) - - np.testing.assert_allclose(B1, B2) - - def test_field_loop_specials(): """test loop special cases""" cur = np.array([1, 1, 1, 1, 0, 2]) @@ -602,7 +586,7 @@ def test_field_loop_specials(): obs = np.array([(0, 0, 0), (1, 0, 0), (0, 0, 0), (1, 0, 0), (1, 0, 0), (0, 0, 0)]) B = ( - current_circle_field( + BHJM_circle( field="B", observers=obs, diameter=dia, @@ -744,7 +728,7 @@ def test_field_loop2(): curr = np.array([1]) dia = np.array([2]) obs = np.array([[0, 0, 0]]) - B = current_circle_field( + B = BHJM_circle( field="B", observers=obs, current=curr, @@ -754,7 +738,7 @@ def test_field_loop2(): curr = np.array([1] * 2) dia = np.array([2] * 2) obs = np.array([[0, 0, 0]] * 2) - B2 = current_circle_field( + B2 = BHJM_circle( field="B", observers=obs, current=curr, @@ -887,7 +871,7 @@ def test_triangle5(): ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]] * n) b1 = ( - magpy.core.triangle_field( + triangle_field( field="H", observers=obs1, polarization=mag, @@ -897,7 +881,7 @@ def test_triangle5(): ) np.testing.assert_allclose(btest1, b1) b2 = ( - magpy.core.triangle_field( + triangle_field( field="H", observers=obs2, polarization=mag, @@ -915,19 +899,19 @@ def test_triangle6(): obs3 = np.array([(5, 0, 0)]) mag = np.array([(1, 2, 3)]) ver = np.array([[(0, 0, 0), (0, 5, 0), (5, 0, 0)]]) - b1 = magpy.core.triangle_field( + b1 = triangle_field( field="B", observers=obs1, polarization=mag, vertices=ver, ) - b2 = magpy.core.triangle_field( + b2 = triangle_field( field="B", observers=obs2, polarization=mag, vertices=ver, ) - b3 = magpy.core.triangle_field( + b3 = triangle_field( field="B", observers=obs3, polarization=mag, diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index ec2a1905e..5347ba696 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -1,12 +1,18 @@ import numpy as np import magpylib as magpy -from magpylib._src.fields.field_BH_circle import current_circle_field +from magpylib._src.fields.field_BH_circle import BHJM_circle from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder +from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field from magpylib._src.fields.field_BH_dipole import BHJM_dipole +from magpylib._src.fields.field_BH_polyline import current_polyline_field +from magpylib._src.fields.field_BH_sphere import magnet_sphere_field +from magpylib._src.fields.field_BH_tetrahedron import magnet_tetrahedron_field +from magpylib._src.fields.field_BH_triangle import triangle_field from magpylib._src.utility import MU0 + # PHYSICS CONSISTENCY TESTING # # Magnetic moment of a current loop with current I and surface A: @@ -50,7 +56,7 @@ def test_core_phys_moment_of_current_circle(): curr = np.array([1e3, 1e3]) mom = ((dia / 2) ** 2 * np.pi * curr * np.array([(0, 0, 1)] * 2).T).T - B1 = current_circle_field( + B1 = BHJM_circle( field="B", observers=obs, diameter=dia, @@ -63,7 +69,7 @@ def test_core_phys_moment_of_current_circle(): ) np.testing.assert_allclose(B1, B2, rtol=1e-02) - H1 = current_circle_field( + H1 = BHJM_circle( field="H", observers=obs, diameter=dia, @@ -90,7 +96,7 @@ def test_core_phys_moment_of_current_square(): curr4 = np.array([curr1] * 4) mom = (4 * curr1 * np.array([(0, 0, 1)]).T).T - B1 = magpy.core.current_polyline_field( + B1 = current_polyline_field( field="B", observers=obs4, segment_start=vert[:-1], @@ -98,14 +104,14 @@ def test_core_phys_moment_of_current_square(): current=curr4, ) B1 = np.sum(B1, axis=0) - B2 = magpy.core.BHJM_dipole( + B2 = BHJM_dipole( field="B", observers=obs1, moment=mom, )[0] np.testing.assert_allclose(B1, -B2, rtol=1e-03) - H1 = magpy.core.current_polyline_field( + H1 = current_polyline_field( field="H", observers=obs4, segment_start=vert[:-1], @@ -113,7 +119,7 @@ def test_core_phys_moment_of_current_square(): current=curr4, ) H1 = np.sum(H1, axis=0) - H2 = magpy.core.BHJM_dipole( + H2 = BHJM_dipole( field="H", observers=obs1, moment=mom, @@ -132,13 +138,13 @@ def test_core_phys_circle_polyline(): obs99 = np.array([(1, 2, 3)] * 299) dia = np.array([2]) - H1 = magpy.core.current_circle_field( + H1 = BHJM_circle( field="H", observers=obs, diameter=dia, current=curr, )[0] - H2 = magpy.core.current_polyline_field( + H2 = current_polyline_field( field="H", observers=obs99, segment_start=vert[:-1], @@ -148,13 +154,13 @@ def test_core_phys_circle_polyline(): H2 = np.sum(H2, axis=0) np.testing.assert_allclose(H1, -H2, rtol=1e-4) - B1 = magpy.core.current_circle_field( + B1 = BHJM_circle( field="B", observers=obs, diameter=dia, current=curr, )[0] - B2 = magpy.core.current_polyline_field( + B2 = current_polyline_field( field="B", observers=obs99, segment_start=vert[:-1], @@ -177,31 +183,31 @@ def test_core_physics_dipole_sphere(): pol = np.array([(1, 2, 3), (0, 0, 1), (-1, -2, 0), (1, -1, 0.1)]) mom = np.array([4 * (d / 2) ** 3 * np.pi / 3 * p / MU0 for d, p in zip(dia, pol)]) - B1 = magpy.core.magnet_sphere_field( + B1 = magnet_sphere_field( field="B", observers=obs, diameter=dia, polarization=pol, ) - B2 = magpy.core.BHJM_dipole( + B2 = BHJM_dipole( field="B", observers=obs, moment=mom, ) np.testing.assert_allclose(B1, B2, rtol=0, atol=1e-16) - H1 = magpy.core.magnet_sphere_field( + H1 = magnet_sphere_field( field="H", observers=obs, diameter=dia, polarization=pol, ) - H2 = magpy.core.BHJM_dipole( + H2 = BHJM_dipole( field="H", observers=obs, moment=mom, ) - np.testing.assert_allclose(H1, H2, rtol=0, atol=1e-11) + np.testing.assert_allclose(H1, H2, rtol=0, atol=1e-10) # -> Circle, Cylinder @@ -227,7 +233,7 @@ def test_core_physics_long_solenoid(): BHz_long *= MU0 # SOLENOID TEST constructed from circle fields - BH = current_circle_field( + BH = BHJM_circle( field=field, observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N), diameter=np.array([2 * R] * N), @@ -290,7 +296,7 @@ def test_core_physics_current_replacement(): N = 1000 # current discretization I = Jz / MU0 / N * L - H = current_circle_field( + H = BHJM_circle( field="H", observers=np.linspace((0, 0, -L / 2), (0, 0, L / 2), N) + obs, diameter=np.array([2 * R] * N), @@ -319,7 +325,7 @@ def test_core_physics_geometry_cylinder_from_segments(): Bseg = np.zeros((2, 3)) for phi1, phi2 in zip(sections[:-1], sections[1:]): - B_part = magpy.core.magnet_cylinder_segment_field( + B_part = magnet_cylinder_segment_field( field="B", observers=obs, dimension=np.array([(0, r, h, phi1, phi2)] * 2), @@ -336,7 +342,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): obs = np.array([(100, 200, 300), (-200, -200, -200)]) mom = np.array([(1e6, 2e6, 3e6)] * 2) - Bdip = magpy.core.BHJM_dipole( + Bdip = BHJM_dipole( field="H", observers=obs, moment=mom, @@ -367,7 +373,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): vert = np.array([[(0, 0, 0), (0, 0, 0.1), (0.1, 0, 0), (0, 0.1, 0)]] * 2) vol = 1 / 6 * 1e-3 pol = mom / vol * MU0 - Btetra = magpy.core.magnet_tetrahedron_field( + Btetra = magnet_tetrahedron_field( field="H", observers=obs, vertices=vert, @@ -378,7 +384,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dim = np.array([(0.1, 0.2, 0.1, -25, 25)] * 2) vol = 3 * np.pi * (50 / 360) * 1e-3 pol = mom / vol * MU0 - Bcys = magpy.core.magnet_cylinder_segment_field( + Bcys = magnet_cylinder_segment_field( field="H", observers=obs + np.array((0.15, 0, 0)), dimension=dim, @@ -401,7 +407,7 @@ def test_core_physics_circle_VS_webpage_numbers(): Hz = [500, 176.8, 44.72, 15.81] Htest = [(0, 0, hz) for hz in Hz] - H = magpy.core.current_circle_field( + H = BHJM_circle( field="H", observers=obs, diameter=dia, @@ -418,7 +424,7 @@ def test_core_physics_circle_VS_webpage_numbers(): ] Btest = [(0, 0, bz) for bz in Bz] - B = magpy.core.current_circle_field( + B = BHJM_circle( field="B", observers=obs, diameter=dia, @@ -457,7 +463,7 @@ def test_core_physics_cube_current_replacement(): Hcurr = np.zeros((2, 3)) for i, obss in enumerate([obs1, obs2]): - h = magpy.core.current_polyline_field( + h = current_polyline_field( field="H", observers=obss, segment_start=start, @@ -481,7 +487,7 @@ def test_core_physics_triangle_cube_geometry(): [(1, 1, -1), (1, -1, -1), (-1, 1, -1)], # bott2 ] ) - b = magpy.core.triangle_field( + b = triangle_field( field="B", observers=obs, vertices=fac, @@ -511,7 +517,7 @@ def test_core_physics_triangle_VS_itself(): [(0, 0, 0), (10, 0, 0), (0, 10, 0)], ] ) - b = magpy.core.triangle_field( + b = triangle_field( field="B", observers=obs, polarization=mag, @@ -529,7 +535,7 @@ def test_core_physics_triangle_VS_itself(): [(6, 0, 0), (10, 0, 0), (0, 10, 0)], ] ) - bb = magpy.core.triangle_field( + bb = triangle_field( field="B", observers=obs, polarization=mag, @@ -583,13 +589,13 @@ def test_core_physics_Tetrahedron_VS_Cuboid(): for obs in obss: obs6 = np.tile(obs, (6, 1)) mag6 = np.tile(mag, (6, 1)) - b = magpy.core.magnet_tetrahedron_field( + b = magnet_tetrahedron_field( field="B", observers=obs6, polarization=mag6, vertices=ver, ) - h = magpy.core.magnet_tetrahedron_field( + h = magnet_tetrahedron_field( field="H", observers=obs6, polarization=mag6, diff --git a/tests/test_obj_Circle.py b/tests/test_obj_Circle.py index 711474b75..5875560f8 100644 --- a/tests/test_obj_Circle.py +++ b/tests/test_obj_Circle.py @@ -15,7 +15,7 @@ def test_Circle_basic_B(): assert np.allclose(B, Btest) -def test_current_circle_field(): +def test_BHJM_circle(): """test explicit field output values""" s = magpy.current.Circle(current=1, diameter=1) From f838aae4cf9a939a3f6c4635a54f3331110a9900 Mon Sep 17 00:00:00 2001 From: mortner Date: Sun, 28 Jan 2024 22:26:26 +0100 Subject: [PATCH 217/240] BHJM cylinder segment --- docs/_pages/docu/docu_magpylib_api.md | 2 +- .../_src/fields/field_BH_cylinder_segment.py | 171 ++++++++++-------- .../class_magnet_CylinderSegment.py | 4 +- magpylib/core/__init__.py | 4 + tests/test_BHMJ_level.py | 4 +- tests/test_core_physics_consistency.py | 6 +- tests/test_field_cylinder.py | 6 +- tests/test_input_checks.py | 2 +- tests/test_obj_Sphere.py | 7 +- tests/test_obj_Triangle.py | 5 +- 10 files changed, 117 insertions(+), 94 deletions(-) diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md index b0e9218d6..99a9ccb0e 100644 --- a/docs/_pages/docu/docu_magpylib_api.md +++ b/docs/_pages/docu/docu_magpylib_api.md @@ -735,7 +735,7 @@ At the heart of Magpylib lies a set of core functions that are our implementatio **BHJM_magnet_cylinder(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} -**magnet_cylinder_segment_field(** `field`, `observers`, `polarization`, `dimension`**)** +**BHJM_cylinder_segment(** `field`, `observers`, `polarization`, `dimension`**)** ::: :::{grid-item} **magnet_sphere_field(** `field`, `observers`, `polarization`, `diameter`**)** diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index d495afe15..fac23bfa0 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2133,11 +2133,19 @@ def case235(r, r_i, r_bar_i, phi_bar_j, phi_bar_M, phi_bar_Mj, theta_M, z_bar_k) return results -def magnet_cylinder_segment_core( +def magnet_cylinder_segment_Hfield( mag: np.ndarray, dim: np.ndarray, obs_pos: np.ndarray ) -> np.ndarray: """ - Put all solutions properly together (see paper Florian). + Magnetic field of homogeneously magnetized (in cartesian coordinates) cylinder + ring segments. + + The cylinder axis coincides with the z-axis of the global coordinate + system. The geometric center of the cylinder lies in the origin. + Implementation based on F.Slanovc, Journal of Magnetism and Magnetic + Materials, Volume 559, 1 October 2022, 169482 + + SI units are used for all inputs and outputs. Parameters ---------- @@ -2152,11 +2160,6 @@ def magnet_cylinder_segment_core( ------- H-field: ndarray H-field in cylindrical coordinates (Hr, Hphi, Hz), shape (n,3) in units of A/m. - - Notes - ----- - Field computed in the surface charge picture. Final integrals reduced to incomplete - elliptic integrals. """ # tile inputs into 8-stacks (boundary cases) @@ -2298,14 +2301,14 @@ def magnet_cylinder_segment_core( return result.T -def magnet_cylinder_segment_field_internal( +def BHJM_cylinder_segment_internal( field: str, observers: np.ndarray, polarization: np.ndarray, dimension: np.ndarray, ) -> np.ndarray: """ - internal version of magnet_cylinder_segment_field used for object oriented interface. + internal version of BHJM_cylinder_segment used for object oriented interface. Falls back to magnet_cylinder_field whenever the section angles describe the full 360° cylinder. @@ -2318,7 +2321,7 @@ def magnet_cylinder_segment_field_internal( # case1: segment mask1 = (phi2 - phi1) < 360 - BHfinal[mask1] = magnet_cylinder_segment_field( + BHfinal[mask1] = BHJM_cylinder_segment( field=field, observers=observers[mask1], polarization=polarization[mask1], @@ -2346,8 +2349,7 @@ def magnet_cylinder_segment_field_internal( return BHfinal -# CORE -def magnet_cylinder_segment_field( +def BHJM_cylinder_segment( *, field: str, observers: np.ndarray, @@ -2402,7 +2404,7 @@ def magnet_cylinder_segment_field( >>> import numpy as np >>> import magpylib as magpy - >>> B = magpy.core.magnet_cylinder_segment_field( + >>> B = magpy.core.BHJM_cylinder_segment( ... field='B', ... observers=np.array([(1,1,1), (1,1,1)]), ... dimension=np.array([(0,1,2,-90,90), (1,2,4,35,125)]), @@ -2424,7 +2426,7 @@ def magnet_cylinder_segment_field( """ check_field_input(field) - H_all = np.zeros_like(observers, dtype=float) + BHJM = polarization.astype(float) r1, r2, h, phi1, phi2 = dimension.T r1 = abs(r1) @@ -2444,68 +2446,85 @@ def magnet_cylinder_segment_field( # determine when points lie inside and on surface of magnet ------------- - mask_inside = None - if in_out == "auto": - # phip1 in [-2pi,0], phio2 in [0,2pi] - phio1 = phi - phio2 = phi - np.sign(phi) * 2 * np.pi - - # phi=phi1, phi=phi2 - mask_phi1 = close(phio1, phi1) | close(phio2, phi1) - mask_phi2 = close(phio1, phi2) | close(phio2, phi2) - - # r, phi ,z lies in-between, avoid numerical fluctuations (e.g. due to rotations) by including 1e-14 - mask_r_in = (r1 - 1e-14 < r) & (r < r2 + 1e-14) - mask_phi_in = (np.sign(phio1 - phi1) != np.sign(phio1 - phi2)) | ( - np.sign(phio2 - phi1) != np.sign(phio2 - phi2) - ) - mask_z_in = (z1 - 1e-14 < z) & (z < z2 + 1e-14) - - # on surface - mask_surf_z = ( - (close(z, z1) | close(z, z2)) & mask_phi_in & mask_r_in - ) # top / bottom - mask_surf_r = ( - (close(r, r1) | close(r, r2)) & mask_phi_in & mask_z_in - ) # in / out - mask_surf_phi = (mask_phi1 | mask_phi2) & mask_r_in & mask_z_in # in / out - mask_not_on_surf = ~(mask_surf_z | mask_surf_r | mask_surf_phi) - - # inside - mask_inside = mask_r_in & mask_phi_in & mask_z_in - else: - mask_inside = np.full(len(observers), in_out == "inside") - mask_not_on_surf = np.full(len(observers), True) + # mask_inside = None + # if in_out == "auto": + # phip1 in [-2pi,0], phio2 in [0,2pi] + phio1 = phi + phio2 = phi - np.sign(phi) * 2 * np.pi + + # phi=phi1, phi=phi2 + mask_phi1 = close(phio1, phi1) | close(phio2, phi1) + mask_phi2 = close(phio1, phi2) | close(phio2, phi2) + + # r, phi ,z lies in-between, avoid numerical fluctuations (e.g. due to rotations) by including 1e-14 + mask_r_in = (r1 - 1e-14 < r) & (r < r2 + 1e-14) + mask_phi_in = (np.sign(phio1 - phi1) != np.sign(phio1 - phi2)) | ( + np.sign(phio2 - phi1) != np.sign(phio2 - phi2) + ) + mask_z_in = (z1 - 1e-14 < z) & (z < z2 + 1e-14) + + # on surface + mask_surf_z = ( + (close(z, z1) | close(z, z2)) & mask_phi_in & mask_r_in + ) # top / bottom + mask_surf_r = (close(r, r1) | close(r, r2)) & mask_phi_in & mask_z_in # in / out + mask_surf_phi = (mask_phi1 | mask_phi2) & mask_r_in & mask_z_in # in / out + mask_not_on_surf = ~(mask_surf_z | mask_surf_r | mask_surf_phi) + + # inside + mask_inside = mask_r_in & mask_phi_in & mask_z_in + # else: + # mask_inside = np.full(len(observers), in_out == "inside") + # mask_not_on_surf = np.full(len(observers), True) + # ACHTUNG @alex + # 1. inside and not_on_surface are not the same! Cant just put to true. # return 0 when all points are on surface -------------------------------- if not np.any(mask_not_on_surf): - return H_all - - if field in "BH": - # redefine input if there are some surface-points ------------------------- - pol = polarization[mask_not_on_surf] - dim = dim[mask_not_on_surf] - pos_obs_cy = pos_obs_cy[mask_not_on_surf] - phi = phi[mask_not_on_surf] - - # transform mag to spherical CS ----------------------------------------- - m = np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2 + pol[:, 2] ** 2) - phi_m = np.arctan2(pol[:, 1], pol[:, 0]) - th_m = np.arctan2(np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2), pol[:, 2]) - mag_sph = np.concatenate(((m,), (phi_m,), (th_m,)), axis=0).T - - # compute H and transform to cart CS ------------------------------------- - H_cy = magnet_cylinder_segment_core(mag_sph, dim, pos_obs_cy) - Hr, Hphi, Hz = H_cy.T - Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) - Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) - H = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 - H_all[mask_not_on_surf] = H - - return convert_HBMJ( - output_field_type=field, - polarization=polarization, - input_field_type="H", - field_values=H_all, - mask_inside=mask_inside & mask_not_on_surf, - ) + return BHJM * 0 + + if field == "J": + BHJM[~mask_inside] *= 0 + return BHJM + + if field == "M": + BHJM[~mask_inside] *= 0 + return BHJM / MU0 + + # redefine input if there are some surface-points ------------------------- + pol = polarization[mask_not_on_surf] + dim = dim[mask_not_on_surf] + pos_obs_cy = pos_obs_cy[mask_not_on_surf] + phi = phi[mask_not_on_surf] + + # transform mag to spherical CS ----------------------------------------- + m = np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2 + pol[:, 2] ** 2) + phi_m = np.arctan2(pol[:, 1], pol[:, 0]) + th_m = np.arctan2(np.sqrt(pol[:, 0] ** 2 + pol[:, 1] ** 2), pol[:, 2]) + mag_sph = np.concatenate(((m,), (phi_m,), (th_m,)), axis=0).T + + # compute H and transform to cart CS ------------------------------------- + H_cy = magnet_cylinder_segment_Hfield(mag_sph, dim, pos_obs_cy) + Hr, Hphi, Hz = H_cy.T + Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) + Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) + BHJM[mask_not_on_surf] = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 + + if field == "H": + return BHJM + + if field == "B": + BHJM[mask_inside] += polarization[mask_inside] / MU0 + return BHJM * MU0 + + raise ValueError( # pragma: no cover + "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" + ) + + # return convert_HBMJ( + # output_field_type=field, + # polarization=polarization, + # input_field_type="H", + # field_values=H_all, + # mask_inside=mask_inside & mask_not_on_surf, + # ) diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index e5445c6a4..42a0bdb54 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -3,7 +3,7 @@ from magpylib._src.display.traces_core import make_CylinderSegment from magpylib._src.fields.field_BH_cylinder_segment import ( - magnet_cylinder_segment_field_internal, + BHJM_cylinder_segment_internal, ) from magpylib._src.input_checks import check_format_input_cylinder_segment from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet @@ -100,7 +100,7 @@ class CylinderSegment(BaseMagnet): [ 0.00025439 0.00074332 0.00011683]] """ - _field_func = staticmethod(magnet_cylinder_segment_field_internal) + _field_func = staticmethod(BHJM_cylinder_segment_internal) _field_func_kwargs_ndim = {"polarization": 2, "dimension": 2} get_trace = make_CylinderSegment diff --git a/magpylib/core/__init__.py b/magpylib/core/__init__.py index 425b3f0da..7e286f7c6 100644 --- a/magpylib/core/__init__.py +++ b/magpylib/core/__init__.py @@ -8,6 +8,7 @@ "magnet_cylinder_Hfield_diametralM", "dipole_Hfield", "current_circle_Bfield", + "magnet_cylinder_segment_Hfield", ] from magpylib._src.fields.field_BH_cuboid import magnet_cuboid_Bfield @@ -15,3 +16,6 @@ from magpylib._src.fields.field_BH_cylinder import magnet_cylinder_Hfield_diametralM from magpylib._src.fields.field_BH_dipole import dipole_Hfield from magpylib._src.fields.field_BH_circle import current_circle_Bfield +from magpylib._src.fields.field_BH_cylinder_segment import ( + magnet_cylinder_segment_Hfield, +) diff --git a/tests/test_BHMJ_level.py b/tests/test_BHMJ_level.py index 0f9af23ed..6a7e97a9f 100644 --- a/tests/test_BHMJ_level.py +++ b/tests/test_BHMJ_level.py @@ -7,7 +7,7 @@ from magpylib._src.fields.field_BH_circle import BHJM_circle from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field +from magpylib._src.fields.field_BH_cylinder_segment import BHJM_cylinder_segment from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.fields.field_BH_polyline import current_line_field from magpylib._src.fields.field_BH_polyline import current_polyline_field @@ -244,7 +244,7 @@ def test_field_cylinder_segment_BH(): "polarization": pol, "dimension": dim, } - H, B, _, J = helper_check_HBMJ_consistency(magnet_cylinder_segment_field, **kw) + H, B, _, J = helper_check_HBMJ_consistency(BHJM_cylinder_segment, **kw) Btest = [ [0.0, 0.0, 0.0], diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 5347ba696..6325a4a31 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -4,7 +4,7 @@ from magpylib._src.fields.field_BH_circle import BHJM_circle from magpylib._src.fields.field_BH_cuboid import BHJM_magnet_cuboid from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field +from magpylib._src.fields.field_BH_cylinder_segment import BHJM_cylinder_segment from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.fields.field_BH_polyline import current_polyline_field from magpylib._src.fields.field_BH_sphere import magnet_sphere_field @@ -325,7 +325,7 @@ def test_core_physics_geometry_cylinder_from_segments(): Bseg = np.zeros((2, 3)) for phi1, phi2 in zip(sections[:-1], sections[1:]): - B_part = magnet_cylinder_segment_field( + B_part = BHJM_cylinder_segment( field="B", observers=obs, dimension=np.array([(0, r, h, phi1, phi2)] * 2), @@ -384,7 +384,7 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dim = np.array([(0.1, 0.2, 0.1, -25, 25)] * 2) vol = 3 * np.pi * (50 / 360) * 1e-3 pol = mom / vol * MU0 - Bcys = magnet_cylinder_segment_field( + Bcys = BHJM_cylinder_segment( field="H", observers=obs + np.array((0.15, 0, 0)), dimension=dim, diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index 4b9206430..7eb1d379e 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -6,8 +6,8 @@ import magpylib as magpy from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder +from magpylib._src.fields.field_BH_cylinder_segment import BHJM_cylinder_segment from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_core -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_field # pylint: disable="pointless-string-statement" # creating test data @@ -207,7 +207,7 @@ def test_cylinder_field1(): eins = np.ones(N) d, h, _ = dim.T dim5 = np.array([nulll, d / 2, h, nulll, eins * 360]).T - B1 = magnet_cylinder_segment_field( + B1 = BHJM_cylinder_segment( field="B", observers=poso, polarization=magg, dimension=dim5 ) @@ -497,7 +497,7 @@ def test_cyl_vs_cylseg_axial_H_inside_mask(): dimension=dims, polarization=pols, ) - Bcs = magpy.core.magnet_cylinder_segment_field( + Bcs = BHJM_cylinder_segment( field=field, observers=obs, dimension=dims_cs, diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 95ca17363..f84cf0f4d 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -960,7 +960,7 @@ def test_input_getBH_field_bad(field): moms = np.array([[1, 2, 3]]) obs = np.array([[1, 2, 3]]) with pytest.raises(MagpylibBadUserInput): - magpy.core.BHJM_dipole(field=field, observers=obs, moment=moms) + BHJM_dipole(field=field, observers=obs, moment=moms) def test_sensor_handedness(): diff --git a/tests/test_obj_Sphere.py b/tests/test_obj_Sphere.py index a72915650..7735c8935 100644 --- a/tests/test_obj_Sphere.py +++ b/tests/test_obj_Sphere.py @@ -4,6 +4,7 @@ import numpy as np import magpylib as magpy +from magpylib._src.fields.field_BH_sphere import magnet_sphere_field # # """data generation for test_Sphere()""" @@ -94,9 +95,9 @@ def test_sphere_object_vs_lib(): pol = np.array([(10, 20, 30)]) dia = np.array([1]) pos = np.array([(2, 2, 2)]) - B1 = magpy.core.magnet_sphere_field( - field="B", observers=pos, polarization=pol, diameter=dia - )[0] + B1 = magnet_sphere_field(field="B", observers=pos, polarization=pol, diameter=dia)[ + 0 + ] src = magpy.magnet.Sphere(polarization=pol[0], diameter=dia[0]) B2 = src.getB(pos) diff --git a/tests/test_obj_Triangle.py b/tests/test_obj_Triangle.py index 7cecd60d3..bbea0c5ae 100644 --- a/tests/test_obj_Triangle.py +++ b/tests/test_obj_Triangle.py @@ -2,6 +2,7 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibMissingInput +from magpylib._src.fields.field_BH_triangle import triangle_field def test_Triangle_repr(): @@ -46,9 +47,7 @@ def test_triangle_input3(): [(6, 0, 0), (10, 0, 0), (0, 10, 0)], ] ) - b = magpy.core.triangle_field( - field="B", observers=obs, polarization=pol, vertices=vert - ) + b = triangle_field(field="B", observers=obs, polarization=pol, vertices=vert) b = np.sum(b, axis=0) tri1 = magpy.misc.Triangle(polarization=pol[0], vertices=vert[0]) From f5a01ca5603d7d3b47de461ca35dfc275826bcbe Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Mon, 29 Jan 2024 22:35:43 +0100 Subject: [PATCH 218/240] draft defaults --- magpylib/_src/defaults/defaults_classes.py | 381 +++++++-------------- magpylib/_src/defaults/defaults_utility.py | 208 ++++++----- magpylib/_src/defaults/defaults_values.py | 2 +- pyproject.toml | 1 + 4 files changed, 258 insertions(+), 334 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 8bf4f6d8a..c855cf1a2 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -1,272 +1,147 @@ +import param + from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import get_defaults_dict -from magpylib._src.defaults.defaults_utility import MagicProperties +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.defaults.defaults_utility import validate_property_class from magpylib._src.style import DisplayStyle - -class DefaultSettings(MagicProperties): - """Library default settings. - - Parameters - ---------- - display: dict or Display - `Display` class containing display settings. `('backend', 'animation', 'colorsequence' ...)` - """ - - def __init__( - self, - display=None, - **kwargs, - ): - super().__init__( - display=display, - **kwargs, - ) - self.reset() - - def reset(self): - """Resets all nested properties to their hard coded default values""" - self.update(get_defaults_dict(), _match_properties=False) - return self - - @property - def display(self): - """`Display` class containing display settings. - `('backend', 'animation', 'colorsequence')`""" - return self._display - - @display.setter - def display(self, val): - self._display = validate_property_class(val, "display", Display, self) - - -class Display(MagicProperties): - """ - Defines the properties for the plotting features. - - Properties - ---------- - backend: str, default='matplotlib' - Defines the plotting backend to be used by default, if not explicitly set in the `display` +# pylint: disable=missing-class-docstring + + +class Animation(MagicParameterized): + fps = param.Integer( + default=30, + bounds=(0, None), + inclusive_bounds=(False, None), + doc="""Target number of frames to be displayed per second.""", + ) + + maxfps = param.Integer( + default=50, + bounds=(0, None), + inclusive_bounds=(False, None), + doc="""Maximum number of frames to be displayed per second before downsampling kicks in.""", + ) + + maxframes = param.Integer( + default=200, + bounds=(0, None), + inclusive_bounds=(False, None), + doc="""Maximum total number of frames to be displayed before downsampling kicks in.""", + ) + + time = param.Number( + default=5, + bounds=(0, None), + inclusive_bounds=(False, None), + doc="""Default animation time.""", + ) + + slider = param.Boolean( + default=True, + doc=""" + If True, an interactive slider will be displayed and stay in sync with the animation, + will be hidden otherwise.""", + ) + + # either `mp4` or `gif` or ending with `.mp4` or `.gif`" + output = param.String( + doc="""Animation output type""", + regex=r"\b(?:mp4|gif|(?:\S*\.)(?:mp4|gif))\b", + ) + + +class Display(MagicParameterized): + def __setattr__(self, name, value): + if name == "colorsequence": + value = [ + color_validator(v, allow_None=False, parent_name="Colorsequence") + for v in value + ] + return super().__setattr__(name, value) + + backend = param.Selector( + default="matplotlib", + objects=list(SUPPORTED_PLOTTING_BACKENDS), + doc=""" + Plotting backend to be used by default, if not explicitly set in the `display` function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS + Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", + ) - colorsequence: iterable, default= - ['#2E91E5', '#E15F99', '#1CA71C', '#FB0D0D', '#DA16FF', '#222A2A', - '#B68100', '#750D86', '#EB663B', '#511CFB', '#00A08B', '#FB00D1', - '#FC0080', '#B2828D', '#6C7C32', '#778AAE', '#862A16', '#A777F1', - '#620042', '#1616A7', '#DA60CA', '#6C4516', '#0D2A63', '#AF0038'] + colorsequence = param.List( + doc=""" An iterable of color values used to cycle trough for every object displayed. A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color - - animation: dict or Animation - Defines the animation properties used by the `plotly` plotting backend when `animation=True` - in the `show` function. - - autosizefactor: int, default=10 + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + ) + + animation = param.ClassSelector( + class_=Animation, + default=Animation(), + doc=""" + Animation properties used when `animation=True` in the `show` function, + if applicaple to the chosen plotting backend.""", + ) + + autosizefactor = param.Number( + default=10, + bounds=(0, None), + inclusive_bounds=(False, True), + softbounds=(5, 15), + doc=""" Defines at which scale objects like sensors and dipoles are displayed. - Specifically `object_size` = `canvas_size` / `AUTOSIZE_FACTOR`. - - styles: dict or DisplayStyle - Base class containing display styling properties for all object families. - """ - - @property - def backend(self): - """plotting backend to be used by default, if not explicitly set in the `display` - function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""" - return self._backend - - @backend.setter - def backend(self, val): - backends = [*SUPPORTED_PLOTTING_BACKENDS, "auto"] - assert val is None or val in backends, ( - f"the `backend` property of {type(self).__name__} must be one of" - f"{backends}" - f" but received {repr(val)} instead" - ) - self._backend = val - - @property - def colorsequence(self): - """An iterable of color values used to cycle trough for every object displayed. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""" - return self._colorsequence - - @colorsequence.setter - def colorsequence(self, val): - if val is not None: - name = type(self).__name__ - try: - val = tuple( - color_validator(c, allow_None=False, parent_name=f"{name}") - for c in val - ) - except TypeError as err: - raise ValueError( - f"The `colorsequence` property of {name} must be an " - f"iterable of colors but received {val!r} instead" - ) from err - - self._colorsequence = val - - @property - def animation(self): - """Animation properties used by the `plotly` plotting backend when `animation=True` - in the `show` function.""" - return self._animation + -> object_size = canvas_size / AUTOSIZE_FACTOR""", + ) - @animation.setter - def animation(self, val): - self._animation = validate_property_class(val, "animation", Animation, self) + style = param.ClassSelector( + class_=DisplayStyle, + default=DisplayStyle(), + doc="""class containing styling properties for any object family.""", + ) - @property - def autosizefactor(self): - """Defines at which scale objects like sensors and dipoles are displayed. - Specifically `object_size` = `canvas_size` / `AUTOSIZE_FACTOR`.""" - return self._autosizefactor - @autosizefactor.setter - def autosizefactor(self, val): - assert val is None or isinstance(val, (int, float)) and val > 0, ( - f"the `autosizefactor` property of {type(self).__name__} must be a strictly positive" - f" number but received {repr(val)} instead" - ) - self._autosizefactor = val +class DefaultSettings(MagicParameterized): + """Library default settings. All default values get reset at class instantiation.""" - @property - def style(self): - """Base class containing display styling properties for all object families.""" - return self._style + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._declare_watchers() + with param.parameterized.batch_call_watchers(self): + self.reset() - @style.setter - def style(self, val): - self._style = validate_property_class(val, "style", DisplayStyle, self) - - -class Animation(MagicProperties): - """ - Defines the animation properties used by the `plotly` plotting backend when `animation=True` - in the `display` function. - - Properties - ---------- - fps: str, default=30 - Target number of frames to be displayed per second. - - maxfps: str, default=50 - Maximum number of frames to be displayed per second before downsampling kicks in. - - maxframes: int, default=200 - Maximum total number of frames to be displayed before downsampling kicks in. - - time: float, default=5 - Default animation time. - - slider: bool, default = True - If True, an interactive slider will be displayed and stay in sync with the animation, will - be hidden otherwise. - - output: str, default = None - The path where to store the animation. Must end with `.mp4` or `.gif`. If only the suffix - is used, the file is only store in a temporary folder and deleted after the animation is - done. - """ - - @property - def maxfps(self): - """Maximum number of frames to be displayed per second before downsampling kicks in.""" - return self._maxfps - - @maxfps.setter - def maxfps(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `maxfps` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._maxfps = val - - @property - def fps(self): - """Target number of frames to be displayed per second.""" - return self._fps - - @fps.setter - def fps(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `fps` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._fps = val - - @property - def maxframes(self): - """Maximum total number of frames to be displayed before downsampling kicks in.""" - return self._maxframes - - @maxframes.setter - def maxframes(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `maxframes` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._maxframes = val - - @property - def time(self): - """Default animation time.""" - return self._time - - @time.setter - def time(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `time` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._time = val - - @property - def slider(self): - """show/hide slider""" - return self._slider - - @slider.setter - def slider(self, val): - assert val is None or isinstance(val, bool), ( - f"The `slider` property of {type(self).__name__} must be a either `True` or `False`" - f" but received {repr(val)} instead." - ) - self._slider = val - - @property - def output(self): - """Animation output type""" - return self._output + def reset(self): + """Resets all nested properties to their hard coded default values""" + self.update(get_defaults_dict(), _match_properties=False) + return self - @output.setter - def output(self, val): - if val is not None: - val = str(val) - valid = val.endswith("mp4") or val.endswith("gif") - assert val is None or valid, ( - f"The `output` property of {type(self).__name__} must be a either `mp4` or `gif` " - "or a valid path ending with `.mp4` or `.gif`" - f" but received {repr(val)} instead." - ) - self._output = val + def _declare_watchers(self): + props = get_defaults_dict(flatten=True, separator=".").keys() + for prop in props: + attrib_chain = prop.split(".") + child = attrib_chain[-1] + parent = self # start with self to go through dot chain + for attrib in attrib_chain[:-1]: + parent = getattr(parent, attrib) + parent.param.watch(self._set_to_defaults, parameter_names=[child]) + + @staticmethod + def _set_to_defaults(event): + """Sets class defaults whenever magpylib defaults parameters instance are modifed.""" + event.obj.param[event.name] = event.new + + display = param.ClassSelector( + class_=Display, + default=Display(), + doc=""" + `Display` defaults-class containing display settings. + `(e.g. 'backend', 'animation', 'colorsequence', ...)`""", + ) default_settings = DefaultSettings() diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 77e33dabf..740028c52 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -5,6 +5,7 @@ from copy import deepcopy from functools import lru_cache +import param from matplotlib.colors import CSS4_COLORS as mcolors from magpylib._src.defaults.defaults_values import DEFAULTS @@ -64,20 +65,29 @@ def __repr__(self): # pragma: no cover _DefaultValue = _DefaultType() -def get_defaults_dict(arg=None) -> dict: - """returns default dict or sub-dict based on `arg`. - (e.g. `get_defaults_dict('display.style')`) +def get_defaults_dict(arg=None, flatten=False, separator=".") -> dict: + """Return default dict or sub-dict based on `arg` (e.g. get_default_dict('display.style')). Returns ------- dict default sub dict + + flatten: bool + If `True`, the nested dictionary gets flatten out with provided separator for the + dictionary keys + + separator: str + the separator to be used when flattening the dictionary. Only applies if + `flatten=True` """ dict_ = deepcopy(DEFAULTS) if arg is not None: - for v in arg.split("."): + for v in arg.split(separator): dict_ = dict_[v] + if flatten: + dict_ = linearize_dict(dict_, separator=separator) return dict_ @@ -317,59 +327,133 @@ def validate_style_keys(style_kwargs): return style_kwargs -class MagicProperties: +def update_with_nested_dict(parameterized, nested_dict): + """updates parameterized object recursively via setters""" + # Using `batch_call_watchers` because it has the same underlying + # mechanism as with `param.update` + # See https://param.holoviz.org/user_guide/Dependencies_and_Watchers.html?highlight=batch_call + # #batch-call-watchers + with param.parameterized.batch_call_watchers(parameterized): + for pname, value in nested_dict.items(): + if isinstance(value, dict): + if isinstance(getattr(parameterized, pname), param.Parameterized): + update_with_nested_dict(getattr(parameterized, pname), value) + continue + setattr(parameterized, pname, value) + + +def get_current_values_from_dict(obj, kwargs, match_properties=True): """ - Base Class to represent only the property attributes defined at initialization, after which the - class is frozen. This prevents user to create any attributes that are not defined as properties. + Returns the current nested dictionary of values from the given object based on the keys of the + the given kwargs. + Parameters + ---------- + obj: MagicParameterized: + MagicParameterized class instance - Raises - ------ - AttributeError - raises AttributeError if the object is not a property - """ """""" + kwargs, dict: + nested dictionary of values - __isfrozen = False + same_keys_only: + if True only keys in found in the `obj` class are allowed. - def __init__(self, **kwargs): - input_dict = {k: None for k in self._property_names_generator()} - if kwargs: - magic_kwargs = magic_to_dict(kwargs) - diff = set(magic_kwargs.keys()).difference(set(input_dict.keys())) - for attr in diff: - raise AttributeError( - f"{type(self).__name__} has no property '{attr}'" - f"\n Available properties are: {list(self._property_names_generator())}" + """ + new_dict = {} + for k, v in kwargs.items(): + try: + if isinstance(v, dict): + v = get_current_values_from_dict( + getattr(obj, k), v, match_properties=False ) - input_dict.update(magic_kwargs) - for k, v in input_dict.items(): - setattr(self, k, v) + else: + v = getattr(obj, k) + new_dict[k] = v + except AttributeError as e: + if match_properties: + raise AttributeError(e) from e + return new_dict + + +class MagicParameterized(param.Parameterized): + """Base Magic Parametrized class""" + + __isfrozen = False + + def __init__(self, arg=None, **kwargs): + super().__init__() self._freeze() + self.update(arg=arg, **kwargs) - def __setattr__(self, key, value): - if self.__isfrozen and not hasattr(self, key): + def __setattr__(self, name, value): + if self.__isfrozen and not hasattr(self, name) and not name.startswith("_"): raise AttributeError( - f"{type(self).__name__} has no property '{key}'" - f"\n Available properties are: {list(self._property_names_generator())}" + f"{type(self).__name__} has no property '{name}'" + f"\n Available properties are: {list(self.as_dict().keys())}" ) - object.__setattr__(self, key, value) + p = getattr(self.param, name, None) + if p is not None: + # pylint: disable=unidiomatic-typecheck + if isinstance(p, param.Color): + value = color_validator(value) + elif isinstance(p, param.List) and isinstance(value, tuple): + value = list(value) + elif isinstance(p, param.Tuple) and isinstance(value, list): + value = tuple(value) + if type(p) == param.ClassSelector: + if isinstance(value, dict): + self.update({name: value}) + return + if value is None: + value = type(getattr(self, name))() + super().__setattr__(name, value) def _freeze(self): self.__isfrozen = True - def _property_names_generator(self): - """returns a generator with class properties only""" - return ( - attr - for attr in dir(self) - if isinstance(getattr(type(self), attr, None), property) - ) + def update( + self, arg=None, _match_properties=True, _replace_None_only=False, **kwargs + ): + """ + Updates the class properties with provided arguments, supports magic underscore notation + + Parameters + ---------- + + _match_properties: bool + If `True`, checks if provided properties over keyword arguments are matching the current + object properties. An error is raised if a non-matching property is found. + If `False`, the `update` method does not raise any error when an argument is not + matching a property. + + _replace_None_only: + updates matching properties that are equal to `None` (not already been set) + - def __repr__(self): - params = self._property_names_generator() - dict_str = ", ".join(f"{k}={repr(getattr(self,k))}" for k in params) - return f"{type(self).__name__}({dict_str})" + Returns + ------- + self + """ + if arg is None: + arg = {} + elif isinstance(arg, MagicParameterized): + arg = arg.as_dict() + if kwargs: + arg.update(kwargs) + if arg: + arg = magic_to_dict(arg) + current_dict = get_current_values_from_dict( + self, arg, match_properties=_match_properties + ) + new_dict = update_nested_dict( + current_dict, + arg, + same_keys_only=not _match_properties, + replace_None_only=_replace_None_only, + ) + update_with_nested_dict(self, new_dict) + return self - def as_dict(self, flatten=False, separator="."): + def as_dict(self, flatten=False, separator="_"): """ returns recursively a nested dictionary with all properties objects of the class @@ -383,7 +467,7 @@ def as_dict(self, flatten=False, separator="."): the separator to be used when flattening the dictionary. Only applies if `flatten=True` """ - params = self._property_names_generator() + params = (v[0] for v in self.param.get_param_values() if v[0] != "name") dict_ = {} for k in params: val = getattr(self, k) @@ -395,42 +479,6 @@ def as_dict(self, flatten=False, separator="."): dict_ = linearize_dict(dict_, separator=separator) return dict_ - def update( - self, arg=None, _match_properties=True, _replace_None_only=False, **kwargs - ): - """ - Updates the class properties with provided arguments, supports magic underscore notation - - Parameters - ---------- - - _match_properties: bool - If `True`, checks if provided properties over keyword arguments are matching the current - object properties. An error is raised if a non-matching property is found. - If `False`, the `update` method does not raise any error when an argument is not - matching a property. - - _replace_None_only: - updates matching properties that are equal to `None` (not already been set) - - - Returns - ------- - self - """ - arg = {} if arg is None else arg.copy() - arg = magic_to_dict({**arg, **kwargs}) - current_dict = self.as_dict() - new_dict = update_nested_dict( - current_dict, - arg, - same_keys_only=not _match_properties, - replace_None_only=_replace_None_only, - ) - for k, v in new_dict.items(): - setattr(self, k, v) - return self - def copy(self): """returns a copy of the current class instance""" - return deepcopy(self) + return type(self)(**self.as_dict()) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 8b7ddb2fb..99c0cf573 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -9,7 +9,7 @@ "maxframes": 200, "time": 5, "slider": True, - "output": None, + "output": "", }, "backend": "auto", "colorsequence": ( diff --git a/pyproject.toml b/pyproject.toml index 717c8fa71..53d543287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "scipy>=1.7", "matplotlib>=3.3", "plotly>=5.3", + "param>2.0", ] requires-python = ">=3.8" authors = [ From eb93d5b177787a8c4f83e94eeb78fb9b95184e3b Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 30 Jan 2024 21:59:48 +0100 Subject: [PATCH 219/240] rewrite style classes --- magpylib/_src/defaults/defaults_classes.py | 6 +- magpylib/_src/defaults/defaults_utility.py | 15 - magpylib/_src/defaults/defaults_values.py | 4 +- magpylib/_src/display/traces_generic.py | 4 +- magpylib/_src/display/traces_utility.py | 16 +- magpylib/_src/style.py | 2753 +++++--------------- 6 files changed, 720 insertions(+), 2078 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index c855cf1a2..5eb5e5a21 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -48,7 +48,7 @@ class Animation(MagicParameterized): # either `mp4` or `gif` or ending with `.mp4` or `.gif`" output = param.String( doc="""Animation output type""", - regex=r"\b(?:mp4|gif|(?:\S*\.)(?:mp4|gif))\b", + regex=r"^(mp4|gif|(.*\.(mp4|gif))?)$", ) @@ -63,7 +63,7 @@ def __setattr__(self, name, value): backend = param.Selector( default="matplotlib", - objects=list(SUPPORTED_PLOTTING_BACKENDS), + objects=["auto", *SUPPORTED_PLOTTING_BACKENDS], doc=""" Plotting backend to be used by default, if not explicitly set in the `display` function (e.g. 'matplotlib', 'plotly'). @@ -133,7 +133,7 @@ def _declare_watchers(self): @staticmethod def _set_to_defaults(event): """Sets class defaults whenever magpylib defaults parameters instance are modifed.""" - event.obj.param[event.name] = event.new + event.obj.param[event.name].default = event.new display = param.ClassSelector( class_=Display, diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 740028c52..a7332e3ca 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -296,21 +296,6 @@ def color_validator(color_input, allow_None=True, parent_name=""): return color_new -def validate_property_class(val, name, class_, parent): - """validator for sub property""" - if isinstance(val, dict): - val = class_(**val) - elif val is None: - val = class_() - if not isinstance(val, class_): - raise ValueError( - f"the `{name}` property of `{type(parent).__name__}` must be an instance \n" - f"of `{class_}` or a dictionary with equivalent key/value pairs \n" - f"but received {repr(val)} instead" - ) - return val - - def validate_style_keys(style_kwargs): """validates style kwargs based on key up to first underscore. checks in the defaults structures the generally available style keys""" diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 99c0cf573..2b150032b 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -47,8 +47,8 @@ "frames": None, "numbering": False, }, - "description": {"show": True, "text": None}, - "legend": {"show": True, "text": None}, + "description": {"show": True, "text": ""}, + "legend": {"show": True, "text": ""}, "opacity": 1, "model3d": {"showdefault": True, "data": []}, "color": None, diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index b267f4ff6..9e973360e 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -28,7 +28,7 @@ from magpylib._src.display.traces_utility import group_traces from magpylib._src.display.traces_utility import place_and_orient_model3d from magpylib._src.display.traces_utility import slice_mesh_from_colorscale -from magpylib._src.style import DefaultMarkers +from magpylib._src.style import MarkersStyleSpecific from magpylib._src.utility import format_obj_input @@ -36,7 +36,7 @@ class MagpyMarkers: """A class that stores markers 3D-coordinates.""" def __init__(self, *markers): - self._style = DefaultMarkers() + self._style = MarkersStyleSpecific() self.markers = np.array(markers) @property diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index a77139ed8..e981f664b 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -213,7 +213,7 @@ def draw_arrow_on_circle(sign, diameter, arrow_size, scaled=True, angle_pos_deg= return vertices -def get_rot_pos_from_path(obj, show_path=None): +def get_rot_pos_from_path(obj, frames): """ subsets orientations and positions depending on `show_path` value. examples: @@ -222,19 +222,13 @@ def get_rot_pos_from_path(obj, show_path=None): """ # pylint: disable=protected-access # pylint: disable=invalid-unary-operand-type - if show_path is None: - show_path = True pos = obj._position orient = obj._orientation path_len = pos.shape[0] - if show_path is True or show_path is False or show_path == 0: - inds = np.array([-1]) - elif isinstance(show_path, int): - inds = np.arange(path_len, dtype=int)[::-show_path] - elif hasattr(show_path, "__iter__") and not isinstance(show_path, str): - inds = np.array(show_path) - else: # pragma: no cover - raise ValueError(f"Invalid show_path value ({show_path})") + if frames.mode == "indices": + inds = np.array(frames.indices) + else: + inds = np.arange(path_len, dtype=int)[:: -frames.step] inds[inds >= path_len] = path_len - 1 inds = np.unique(inds) if inds.size == 0: diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py index fa5aa56c2..8f1e0af7c 100644 --- a/magpylib/_src/style.py +++ b/magpylib/_src/style.py @@ -3,18 +3,20 @@ # pylint: disable=too-many-instance-attributes # pylint: disable=cyclic-import import numpy as np +import param from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import get_defaults_dict -from magpylib._src.defaults.defaults_utility import MagicProperties +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.defaults.defaults_utility import validate_property_class from magpylib._src.defaults.defaults_utility import validate_style_keys ALLOWED_SIZEMODES = ("scaled", "absolute") +# pylint: disable=missing-class-docstring + def get_families(obj): """get obj families""" @@ -89,336 +91,336 @@ def get_style(obj, default_settings, **kwargs): return style -class Line(MagicProperties): - """Defines line styling properties. +class Description(MagicParameterized): + show = param.Boolean( + default=True, + doc="if `True`, adds legend entry suffix based on value", + ) + text = param.String(doc="Object description text") - Parameters - ---------- - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - color: str, default=None - Line color. +class Legend(MagicParameterized): + show = param.Boolean( + default=True, + doc="if `True`, overrides complete legend text", + ) + text = param.String(doc="Object legend text") - width: float, default=None - Positive number that defines the line width. - """ - def __init__(self, style=None, color=None, width=None, **kwargs): - super().__init__(style=style, color=color, width=width, **kwargs) +class Marker(MagicParameterized): + """Defines the styling properties of plot markers""" - @property - def style(self): - """Line style.""" - return self._style + color = param.Color( + default=None, + allow_None=True, + doc=""" + The marker color. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - @style.setter - def style(self, val): - assert val is None or val in ALLOWED_LINESTYLES, ( - f"The `style` property of {type(self).__name__} must be one of " - f"{ALLOWED_LINESTYLES},\n" - f"but received {repr(val)} instead." - ) - self._style = val - - @property - def color(self): - """Line color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val) - - @property - def width(self): - """Positive number that defines the line width.""" - return self._width - - @width.setter - def width(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `width` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._width = val + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, True), + softbounds=(1, 5), + doc="""Marker size""", + ) + symbol = param.Selector( + objects=list(ALLOWED_SYMBOLS), + doc=f"""Marker symbol. Can be one of: {ALLOWED_SYMBOLS}""", + ) -class BaseStyle(MagicProperties): - """Base class for display styling options of `BaseGeo` objects. - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. +class Line(MagicParameterized): + color = param.Color( + default=None, + allow_None=True, + doc="""A valid css color""", + ) - description: dict or `Description` object, default=None - Object description properties. + width = param.Number( + default=1, + bounds=(0, 20), + inclusive_bounds=(True, True), + softbounds=(1, 5), + doc="""Line width""", + ) - legend: dict or `Legend` object, default=None - Object legend properties when displayed in a plot. Legend has the `{label} ({description})` - format. + style = param.Selector( + default=ALLOWED_LINESTYLES[0], + objects=ALLOWED_LINESTYLES, + doc=f"""Line style. Can be one of: {ALLOWED_LINESTYLES}""", + ) - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - opacity: float, default=None - object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. +class Arrow(Line): + show = param.Boolean( + default=True, + doc="Show/hide Arrow", + ) - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(False, True), + softbounds=(1, 5), + doc="""Arrow size""", + ) - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """ + sizemode = param.Selector( + default=ALLOWED_SIZEMODES[0], + objects=ALLOWED_SIZEMODES, + doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", + ) - def __init__( - self, - label=None, - description=None, - legend=None, - color=None, - opacity=None, - path=None, - model3d=None, - **kwargs, - ): - super().__init__( - label=label, - description=description, - legend=legend, - color=color, - opacity=opacity, - path=path, - model3d=model3d, - **kwargs, - ) + offset = param.Magnitude( + bounds=(0, 1), + inclusive_bounds=(True, True), + doc="""Defines the arrow offset. `offset=0` results in the arrow head to be coincident with + the start of the line, and `offset=1` with the end.""", + ) - @property - def label(self): - """Label of the class instance, e.g. to be displayed in the legend.""" - return self._label - @label.setter - def label(self, val): - self._label = val if val is None else str(val) +class Frames(MagicParameterized): + indices = param.List( + default=[], + item_type=int, + doc="""Array_like shape (n,) of integers: describes certain path indices.""", + ) - @property - def description(self): - """Description with 'text' and 'show' properties.""" - return self._description + step = param.Integer( + default=1, + bounds=(1, None), + softbounds=(0, 10), + doc="""Displays the object(s) at every i'th path position""", + ) - @description.setter - def description(self, val): - if isinstance(val, str): - self._description = Description(text=val) - else: - self._description = validate_property_class( - val, "description", Description, self - ) + mode = param.Selector( + default="indices", + objects=["indices", "step"], + doc=""" + The object path frames mode. + - step: integer i: displays the object(s) at every i'th path position. + - indices: array_like shape (n,) of integers: describes certain path indices.""", + ) - @property - def legend(self): - """Legend with 'show' property.""" - return self._legend + @param.depends("indices", watch=True) + def _update_indices(self): + self.mode = "indices" + + @param.depends("step", watch=True) + def _update_step(self): + self.mode = "step" + + +class Path(MagicParameterized): + def __setattr__(self, name, value): + if name == "frames": + if isinstance(value, (tuple, list, np.ndarray)): + self.frames.indices = [int(v) for v in value] + elif ( + isinstance(value, (int, np.integer)) + and value is not False + and value is not True + ): + self.frames.step = value + else: + super().__setattr__(name, value) + return + super().__setattr__(name, value) + + show = param.Boolean( + default=True, + doc=""" + Show/hide path + - False: shows object(s) at final path position and hides paths lines and markers. + - True: shows object(s) shows object paths depending on `line`, `marker` and `frames` + parameters.""", + ) - @legend.setter - def legend(self, val): - if isinstance(val, str): - self._legend = Legend(text=val) - else: - self._legend = validate_property_class(val, "legend", Legend, self) - - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - @property - def opacity(self): - """Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.""" - return self._opacity - - @opacity.setter - def opacity(self, val): - assert val is None or (isinstance(val, (float, int)) and 0 <= val <= 1), ( - "The `opacity` property must be a value betwen 0 and 1,\n" - f"but received {repr(val)} instead." - ) - self._opacity = val + marker = param.ClassSelector( + class_=Marker, + default=Marker(), + doc=""" + Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent + key/value pairs""", + ) - @property - def path(self): - """An instance of `Path` or dictionary of equivalent key/value pairs, defining the - object path marker and path line properties.""" - return self._path + line = param.ClassSelector( + class_=Line, + default=Line(), + doc=""" + Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent + key/value pairs""", + ) - @path.setter - def path(self, val): - self._path = validate_property_class(val, "path", Path, self) + numbering = param.Boolean( + doc="""Show/hide numbering on path positions. Only applies if show=True.""", + ) - @property - def model3d(self): - """3d object representation properties.""" - return self._model3d + frames = param.ClassSelector( + class_=Frames, + default=Frames(), + doc=""" + Show copies of the 3D-model along the given path indices. + - mode: either `step` or `indices`. + - step: integer i: displays the object(s) at every i'th path position. + - indices: array_like shape (n,) of integers: describes certain path indices.""", + ) - @model3d.setter - def model3d(self, val): - self._model3d = validate_property_class(val, "model3d", Model3d, self) +class Trace3d(MagicParameterized): + def __setattr__(self, name, value): + validation_func = getattr(self, f"_validate_{name}", None) + if validation_func is not None: + value = validation_func(value) + return super().__setattr__(name, value) -class Description(MagicProperties): - """Defines properties for a description object. + backend = param.Selector( + default="generic", + objects=list(SUPPORTED_PLOTTING_BACKENDS) + ["generic"], + doc=f""" + Plotting backend corresponding to the trace. Can be one of + {list(SUPPORTED_PLOTTING_BACKENDS) + ['generic']}""", + ) - Parameters - ---------- - text: str, default=None - Object description text. + constructor = param.String( + doc=""" + Model constructor function or method to be called to build a 3D-model object + (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend.""" + ) - show: bool, default=None - If True, adds legend entry based on value. - """ + args = param.Parameter( + default=(), + doc=""" + Tuple or callable returning a tuple containing positional arguments for building a + 3D-model object.""", + ) - def __init__(self, text=None, show=None, **kwargs): - super().__init__(text=text, show=show, **kwargs) + kwargs = param.Parameter( + default={}, + doc=""" + Dictionary or callable returning a dictionary containing the keys/values pairs for + building a 3D-model object.""", + ) - @property - def text(self): - """Description text.""" - return self._text + coordsargs = param.Dict( + default=None, + doc=""" + Tells Magpylib the name of the coordinate arrays to be moved or rotated. + by default: + - plotly backend: `{"x": "x", "y": "y", "z": "z"}` + - matplotlib backend: `{"x": "args[0]", "y": "args[1]", "z": "args[2]}"`""", + ) - @text.setter - def text(self, val): - assert val is None or isinstance(val, str), ( - f"The `show` property of {type(self).__name__} must be a string,\n" - f"but received {repr(val)} instead." - ) - self._text = val - - @property - def show(self): - """If True, adds legend entry suffix based on value.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val + show = param.Boolean( + default=True, + doc="""Show/hide model3d object based on provided trace.""", + ) + scale = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, False), + softbounds=(0.1, 5), + doc=""" + Scaling factor by which the trace vertices coordinates should be multiplied by. + Be aware that if the object is not centered at the global CS origin, its position will also + be affected by scaling.""", + ) -class Legend(MagicProperties): - """Defines properties for a legend object. + updatefunc = param.Callable( + doc=""" + Callable object with no arguments. Should return a dictionary with keys from the + trace parameters. If provided, the function is called at `show` time and updates the + trace parameters with the output dictionary. This allows to update a trace dynamically + depending on class attributes, and postpone the trace construction to when the object is + displayed.""" + ) - Parameters - ---------- - text: str, default=None - Object description text. + def _validate_coordsargs(self, value): + assert isinstance(value, dict) and all(key in value for key in "xyz"), ( + f"the `coordsargs` property of {type(self).__name__} must be " + f"a dictionary with `'x', 'y', 'z'` keys" + f" but received {repr(value)} instead" + ) + return value - show: bool, default=None - If True, adds legend entry based on value. - """ + def _validate_updatefunc(self, val): + """Validate updatefunc.""" + if val is None: - def __init__(self, show=None, **kwargs): - super().__init__(show=show, **kwargs) + def val(): + return {} - @property - def text(self): - """Legend text.""" - return self._text + msg = "" + valid_keys = self.param.values().keys() + if not callable(val): + msg = f"Instead received {type(val)}" + else: + test_val = val() + if not isinstance(test_val, dict): + msg = f"but callable returned type {type(test_val)}." + else: + bad_keys = [k for k in test_val.keys() if k not in valid_keys] + if bad_keys: + msg = f"but invalid output dictionary keys received: {bad_keys}." - @text.setter - def text(self, val): - assert val is None or isinstance(val, str), ( - f"The `show` property of {type(self).__name__} must be a string,\n" - f"but received {repr(val)} instead." - ) - self._text = val - - @property - def show(self): - """If True, adds legend entry based on value.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." + assert msg == "", ( + f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " + f"dictionary with a subset of following keys: {list(valid_keys)}.\n" + f"{msg}" ) - self._show = val + return val -class Model3d(MagicProperties): - """Defines properties for the 3d model representation of magpylib objects. +class Model3d(MagicParameterized): + def __setattr__(self, name, value): + if name == "data": + value = self._validate_data(value) + return super().__setattr__(name, value) - Parameters - ---------- - showdefault: bool, default=True - Shows/hides default 3d-model. + showdefault = param.Boolean( + default=True, + doc="""Show/hide default 3D-model.""", + ) - data: dict or list of dicts, default=None + data = param.List( + item_type=Trace3d, + doc=""" A trace or list of traces where each is an instance of `Trace3d` or dictionary of equivalent key/value pairs. Defines properties for an additional user-defined model3d object which is positioned relatively to the main object to be displayed and moved automatically with it. This feature also allows the user to replace the original 3d representation of the object. - """ - - def __init__(self, showdefault=True, data=None, **kwargs): - super().__init__(showdefault=showdefault, data=data, **kwargs) - - @property - def showdefault(self): - """If True, show default model3d object representation, else hide it.""" - return self._showdefault - - @showdefault.setter - def showdefault(self, val): - assert isinstance(val, bool), ( - f"The `showdefault` property of {type(self).__name__} must be " - f"one of `[True, False]`,\n" - f"but received {repr(val)} instead." - ) - self._showdefault = val - - @property - def data(self): - """Data of 3d object representation (trace or list of traces).""" - return self._data - - @data.setter - def data(self, val): - self._data = self._validate_data(val) + """, + ) - def _validate_data(self, traces, **kwargs): + @staticmethod + def _validate_trace(trace, **kwargs): + updatefunc = None + if trace is None: + trace = Trace3d() + if not isinstance(trace, Trace3d) and callable(trace): + updatefunc = trace + trace = Trace3d() + if isinstance(trace, dict): + trace = Trace3d(**trace) + if isinstance(trace, Trace3d): + trace.updatefunc = updatefunc + if kwargs: + trace.update(**kwargs) + trace.update(trace.updatefunc()) + return trace + + def _validate_data(self, traces): if traces is None: traces = [] elif not isinstance(traces, (list, tuple)): traces = [traces] new_traces = [] for trace in traces: - updatefunc = None - if not isinstance(trace, Trace3d) and callable(trace): - updatefunc = trace - trace = Trace3d() - if not isinstance(trace, Trace3d): - trace = validate_property_class(trace, "data", Trace3d, self) - if updatefunc is not None: - trace.updatefunc = updatefunc - trace = trace.update(kwargs) - new_traces.append(trace) + new_traces.append(self._validate_trace(trace)) return new_traces def add_trace(self, trace=None, **kwargs): @@ -431,8 +433,7 @@ def add_trace(self, trace=None, **kwargs): pairs, or a callable returning the equivalent dictionary. backend: str - Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`. + Plotting backend corresponding to the trace. Can be one of `['matplotlib', 'plotly']`. constructor: str Model constructor function or method to be called to build a 3D-model object @@ -451,7 +452,7 @@ def add_trace(self, trace=None, **kwargs): by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. show: bool, default=None - Shows/hides model3d object based on provided trace. + Show/hide model3d object based on provided trace. scale: float, default=1 Scaling factor by which the trace vertices coordinates are multiplied. @@ -463,1875 +464,537 @@ def add_trace(self, trace=None, **kwargs): depending on class attributes, and postpone the trace construction to when the object is displayed. """ - self._data += self._validate_data([trace], **kwargs) + self.data = list(self.data) + [self._validate_trace(trace, **kwargs)] return self -class Trace3d(MagicProperties): - """Defines properties for an additional user-defined 3d model object which is positioned - relatively to the main object to be displayed and moved automatically with it. This feature - also allows the user to replace the original 3d representation of the object. - - Parameters - ---------- - backend: str - Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`. - - constructor: str - Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - - args: tuple, default=None - Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object. - - kwargs: dict or callable, default=None - Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object. - - coordsargs: dict, default=None - Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - - show: bool, default=True - Shows/hides model3d object based on provided trace. - - scale: float, default=1 - Scaling factor by which the trace vertices coordinates are multiplied. +class BaseStyle(MagicParameterized): + label = param.String(doc="Label of the class instance, can be any string.") - updatefunc: callable, default=None - Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed. - """ - - def __init__( - self, - backend="generic", - constructor=None, - args=None, - kwargs=None, - coordsargs=None, - show=True, - scale=1, - updatefunc=None, - **params, - ): - super().__init__( - backend=backend, - constructor=constructor, - args=args, - kwargs=kwargs, - coordsargs=coordsargs, - show=show, - scale=scale, - updatefunc=updatefunc, - **params, - ) - - @property - def args(self): - """Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object.""" - return self._args - - @args.setter - def args(self, val): - if val is not None: - test_val = val - if callable(val): - test_val = val() - assert isinstance(test_val, tuple), ( - "The `trace` input must be a dictionary or a callable returning a dictionary,\n" - f"but received {type(val).__name__} instead." - ) - self._args = val - - @property - def kwargs(self): - """Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object.""" - return self._kwargs - - @kwargs.setter - def kwargs(self, val): - if val is not None: - test_val = val - if callable(val): - test_val = val() - assert isinstance(test_val, dict), ( - "The `kwargs` input must be a dictionary or a callable returning a dictionary,\n" - f"but received {type(val).__name__} instead." - ) - self._kwargs = val - - @property - def constructor(self): - """Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - """ - return self._constructor - - @constructor.setter - def constructor(self, val): - assert val is None or isinstance(val, str), ( - f"The `constructor` property of {type(self).__name__} must be a string," - f"\nbut received {repr(val)} instead." - ) - self._constructor = val - - @property - def show(self): - """If True, show default model3d object representation, else hide it.""" - return self._show - - @show.setter - def show(self, val): - assert isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be " - f"one of `[True, False]`,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def scale(self): - """Scaling factor by which the trace vertices coordinates are multiplied.""" - return self._scale - - @scale.setter - def scale(self, val): - assert isinstance(val, (int, float)) and val > 0, ( - f"The `scale` property of {type(self).__name__} must be a strictly positive number,\n" - f"but received {repr(val)} instead." - ) - self._scale = val + description = param.ClassSelector( + class_=Description, + default=Description(), + doc="Object description properties such as `text` and `show`.", + ) - @property - def coordsargs(self): - """Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - """ - return self._coordsargs - - @coordsargs.setter - def coordsargs(self, val): - assert val is None or ( - isinstance(val, dict) and all(key in val for key in "xyz") - ), ( - f"The `coordsargs` property of {type(self).__name__} must be " - f"a dictionary with `'x', 'y', 'z'` keys,\n" - f"but received {repr(val)} instead." - ) - self._coordsargs = val - - @property - def backend(self): - """Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`.""" - return self._backend - - @backend.setter - def backend(self, val): - backends = ["generic"] + list(SUPPORTED_PLOTTING_BACKENDS) - assert val is None or val in backends, ( - f"The `backend` property of {type(self).__name__} must be one of" - f"{backends},\n" - f"but received {repr(val)} instead." - ) - self._backend = val + legend = param.ClassSelector( + class_=Legend, + default=Legend(), + doc="Object description properties such as `text` and `show`.", + ) - @property - def updatefunc(self): - """Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed.""" - return self._updatefunc + color = param.Color( + default=None, + allow_None=True, + doc="A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.", + ) - @updatefunc.setter - def updatefunc(self, val): - if val is None: + opacity = param.Number( + default=1, + bounds=(0, 1), + doc="Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", + ) - def val(): - return {} + path = param.ClassSelector( + class_=Path, + default=Path(), + doc=""" + An instance of `Path` or dictionary of equivalent key/value pairs, defining the object path + marker and path line properties.""", + ) - msg = "" - valid_props = list(self._property_names_generator()) - if not callable(val): - msg = f"Instead received {type(val)}" - else: - test_val = val() - if not isinstance(test_val, dict): - msg = f"but callable returned type {type(test_val)}." - else: - bad_keys = [k for k in test_val.keys() if k not in valid_props] - if bad_keys: - msg = f"but invalid output dictionary keys received: {bad_keys}." + model3d = param.ClassSelector( + class_=Model3d, + default=Model3d(), + doc=""" + A list of traces where each is an instance of `Trace3d` or dictionary of equivalent + key/value pairs. Defines properties for an additional user-defined model3d object which is + positioned relatively to the main object to be displayed and moved automatically with it. + This feature also allows the user to replace the original 3d representation of the object. + """, + ) - assert msg == "", ( - f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " - f"dictionary with a subset of following keys: {valid_props} keys.\n" - f"{msg}" - ) - self._updatefunc = val +class MagnetizationColor(MagicParameterized): + _allowed_modes = ("tricolor", "bicolor", "tricycle") -class Magnetization(MagicProperties): - """Defines magnetization styling properties. + north = param.Color( + default="red", + doc=""" + The color of the magnetic north pole. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - Parameters - ---------- - show : bool, default=None - If True show magnetization direction. + south = param.Color( + default="green", + doc=""" + The color of the magnetic south pole. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - color: dict or MagnetizationColor object, default=None - Color properties showing the magnetization direction (for the plotly backend). - Only applies if `show=True`. + middle = param.Color( + default="grey", + doc=""" + The color between the magnetic poles. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - arrow: dict or Arrow object, default=None, - Arrow properties. Only applies if mode='arrow'. + transition = param.Number( + default=0.2, + bounds=(0, 1), + inclusive_bounds=(True, True), + doc=""" + Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + ) - mode: {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`. - """ + mode = param.Selector( + default=_allowed_modes[0], + objects=_allowed_modes, + doc=""" + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + ) - def __init__(self, show=None, size=None, color=None, mode=None, **kwargs): - super().__init__(show=show, size=size, color=color, mode=mode, **kwargs) - @property - def show(self): - """If True, show magnetization direction.""" - return self._show +class Magnetization(MagicParameterized): + _allowed_modes = ("auto", "arrow", "color", "arrow+color", "color+arrow") - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - "The `show` input must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Deprecated (please use arrow.size): Arrow size property.""" - return self.arrow.size - - @size.setter - def size(self, val): - if val is not None: - self.arrow.size = val - - @property - def color(self): - """Color properties showing the magnetization direction (for the plotly backend). - Applies only if `show=True`. - """ - return self._color + show = param.Boolean( + default=True, + doc="""Show/hide magnetization based on active plotting backend""", + ) - @color.setter - def color(self, val): - self._color = validate_property_class(val, "color", MagnetizationColor, self) + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, True), + softbounds=(1, 5), + doc=""" + Arrow size of the magnetization direction (for the matplotlib backend only), only applies if + `show=True`""", + ) - @property - def arrow(self): - """`Arrow` object or dict with `show, size, width, style, color` properties/keys.""" - return self._arrow + color = param.ClassSelector( + class_=MagnetizationColor, + default=MagnetizationColor(), + doc=""" + Color properties showing the magnetization direction, only applies if `show=True` + and `mode` contains 'color'""", + ) - @arrow.setter - def arrow(self, val): - self._arrow = validate_property_class(val, "magnetization", Arrow, self) + arrow = param.ClassSelector( + class_=Arrow, + default=Arrow(), + doc=""" + Arrow properties showing the magnetization direction, only applies if `show=True` + and `mode` contains 'arrow'""", + ) - @property - def mode(self): - """One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + mode = param.Selector( + default=_allowed_modes[0], + objects=_allowed_modes, + doc=""" + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`.""" - return self._mode - - @mode.setter - def mode(self, val): - allowed = ("auto", "arrow", "color", "arrow+color", "color+arrow") - assert val is None or val in allowed, ( - f"The `mode` input must None or be one of `{allowed}`,\n" - f"but received {repr(val)} instead." - ) - self._mode = val - - -class MagnetizationColor(MagicProperties): - """Defines the magnetization direction color styling properties. (Only relevant for - the plotly backend) - - Parameters - ---------- - north: str, default=None - Defines the color of the magnetic north pole. + can display both and `auto` is chosen, the priority is given to `color`.""", + ) - south: str, default=None - Defines the color of the magnetic south pole. - middle: str, default=None - Defines the color between the magnetic poles. +class MagnetSpecific(MagicParameterized): + magnetization = param.ClassSelector( + class_=Magnetization, + default=Magnetization(), + doc=""" + Magnetization styling with `'show'`, `'size'`, `'color'` properties or a dictionary with + equivalent key/value pairs""", + ) - transition: float, default=None - Sets the transition smoothness between poles colors. Can be any value - in-between 0 (discrete) and 1(smooth). - mode: str, default=None - Sets the coloring mode for the magnetization. - - `'bicolor'`: Only north and south pole colors are shown. - - `'tricolor'`: Both pole colors and middle color are shown. - - `'tricycle'`: Both pole colors are shown and middle color is replaced by a color cycling - through the default color sequence. - """ +class ArrowCoordSysSingle(MagicParameterized): + show = param.Boolean( + default=True, + doc="""Show/hide single CS arrow""", + ) - _allowed_modes = ("bicolor", "tricolor", "tricycle") - - def __init__( - self, north=None, south=None, middle=None, transition=None, mode=None, **kwargs - ): - super().__init__( - north=north, - middle=middle, - south=south, - transition=transition, - mode=mode, - **kwargs, - ) + color = param.Color( + default=None, + allow_None=True, + doc=""" + The color of a single CS arrow. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - @property - def north(self): - """Color of the magnetic north pole.""" - return self._north - - @north.setter - def north(self, val): - self._north = color_validator(val) - - @property - def south(self): - """Color of the magnetic south pole.""" - return self._south - - @south.setter - def south(self, val): - self._south = color_validator(val) - - @property - def middle(self): - """Color between the magnetic poles.""" - return self._middle - - @middle.setter - def middle(self, val): - self._middle = color_validator(val) - - @property - def transition(self): - """Sets the transition smoothness between poles colors. Can be any value - in-between 0 (discrete) and 1(smooth). - """ - return self._transition - - @transition.setter - def transition(self, val): - assert ( - val is None or isinstance(val, (float, int)) and 0 <= val <= 1 - ), "color transition must be a value between 0 and 1" - self._transition = val - - @property - def mode(self): - """Sets the coloring mode for the magnetization. - - `'bicolor'`: Only north and south pole colors are shown. - - `'tricolor'`: Both pole colors and middle color are shown. - - `'tricycle'`: Both pole colors are shown and middle color is replaced by a color cycling - through the default color sequence. - """ - return self._mode - - @mode.setter - def mode(self, val): - assert val is None or val in self._allowed_modes, ( - f"The `mode` property of {type(self).__name__} must be one of" - f"{list(self._allowed_modes)},\n" - f"but received {repr(val)} instead." - ) - self._mode = val +class ArrowCoordSys(MagicParameterized): + x = param.ClassSelector( + class_=ArrowCoordSysSingle, + default=ArrowCoordSysSingle(), + doc=""" + `Arrowsingle` class or dict with equivalent key/value pairs for x-direction + (e.g. `color`, `show`)""", + ) -class MagnetProperties: - """Defines styling properties of homogeneous magnet classes. + y = param.ClassSelector( + class_=ArrowCoordSysSingle, + default=ArrowCoordSysSingle(), + doc=""" + `Arrowsingle` class or dict with equivalent key/value pairs for y-direction + (e.g. `color`, `show`)""", + ) - Parameters - ---------- - magnetization: dict or `Magnetization` object, default=None - `Magnetization` instance with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ + z = param.ClassSelector( + class_=ArrowCoordSysSingle, + default=ArrowCoordSysSingle(), + doc=""" + `Arrowsingle` class or dict with equivalent key/value pairs for z-direction + (e.g. `color`, `show`)""", + ) - @property - def magnetization(self): - """`Magnetization` instance with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ - return self._magnetization - @magnetization.setter - def magnetization(self, val): - self._magnetization = validate_property_class( - val, "magnetization", Magnetization, self - ) +class Pixel(MagicParameterized): + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, None), + softbounds=(0.5, 2), + doc=""" + The relative pixel size. + - matplotlib backend: pixel size is the marker size + - plotly backend: relative size to the distance of nearest neighboring pixel""", + ) + sizemode = param.Selector( + default=ALLOWED_SIZEMODES[0], + objects=ALLOWED_SIZEMODES, + doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", + ) -class DefaultMagnet(MagicProperties, MagnetProperties): - """Defines styling properties of homogeneous magnet classes. + color = param.Color( + default=None, + allow_None=True, + doc=""" + The color of sensor pixel. Must be a valid css color or one of + `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", + ) - Parameters - ---------- - magnetization: dict or Magnetization, default=None - """ + symbol = param.Selector( + default="o", + objects=list(ALLOWED_SYMBOLS), + doc=f""" + Marker symbol. Can be one of: + {ALLOWED_SYMBOLS}""", + ) - def __init__(self, magnetization=None, **kwargs): - super().__init__(magnetization=magnetization, **kwargs) +class SensorSpecific(MagicParameterized): + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, True), + softbounds=(1, 5), + doc="""Sensor size relative to the canvas size.""", + ) + sizemode = param.Selector( + default=ALLOWED_SIZEMODES[0], + objects=ALLOWED_SIZEMODES, + doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", + ) + arrows = param.ClassSelector( + class_=ArrowCoordSys, + default=ArrowCoordSys(), + doc="""`ArrowCS` class or dict with equivalent key/value pairs (e.g. `color`, `size`)""", + ) -class MagnetStyle(BaseStyle, MagnetProperties): - """Defines styling properties of homogeneous magnet classes. + pixel = param.ClassSelector( + class_=Pixel, + default=Pixel(), + doc="""`Pixel` class or dict with equivalent key/value pairs (e.g. `color`, `size`)""", + ) - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - description: dict or `Description` object, default=None - Object description properties. +class CurrentLine(Line): + show = param.Boolean( + default=True, + doc="""Show/hide current direction arrow""", + ) - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. +class CurrentSpecific(MagicParameterized): + arrow = param.ClassSelector( + class_=Arrow, + default=Arrow(), + doc=""" + `Arrow` class or dict with equivalent key/value pairs""", + ) - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. + line = param.ClassSelector( + class_=CurrentLine, + default=CurrentLine(), + doc=""" + Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent + key/value pairs""", + ) - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ +class DipoleSpecific(MagicParameterized): + _allowed_pivots = ("tail", "middle", "tip") - def __init__(self, **kwargs): - super().__init__(**kwargs) + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, True), + softbounds=(0.5, 5), + doc="""The dipole arrow size relative to the canvas size""", + ) + sizemode = param.Selector( + default=ALLOWED_SIZEMODES[0], + objects=ALLOWED_SIZEMODES, + doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", + ) -class MarkerLineProperties: - """Defines styling properties of Markers and Lines.""" + pivot = param.Selector( + default="middle", + objects=_allowed_pivots, + doc="""The part of the arrow that is anchored to the X, Y grid. The arrow rotates about + this point. Can be one of `['tail', 'middle', 'tip']`""", + ) - @property - def show(self): - """Show/hide path. - - False: Shows object(s) at final path position and hides paths lines and markers. - - True: Shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters. - """ - return self._show - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val +class MarkerLineSpecific(MagicParameterized): + show = param.Boolean( + default=True, + doc=""" + Show/hide path + - False: shows object(s) at final path position and hides paths lines and markers. + - True: shows object(s) shows object paths depending on `line`, `marker` and `frames` + parameters.""", + ) - @property - def marker(self): - """`Markers` object with 'color', 'symbol', 'size' properties.""" - return self._marker + marker = param.ClassSelector( + class_=Marker, + default=Marker(), + doc=""" + Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent + key/value pairs""", + ) - @marker.setter - def marker(self, val): - self._marker = validate_property_class(val, "marker", Marker, self) + line = param.ClassSelector( + class_=Line, + default=Line(), + doc=""" + Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent + key/value pairs""", + ) - @property - def line(self): - """`Line` object with 'color', 'type', 'width' properties.""" - return self._line - @line.setter - def line(self, val): - self._line = validate_property_class(val, "line", Line, self) +class GridMesh(MarkerLineSpecific): + ... -class GridMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of GridMesh objects +class OpenMesh(MarkerLineSpecific): + ... - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. +class SelfIntersectingMesh(MarkerLineSpecific): + ... - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ +class DisconnectedMesh(MarkerLineSpecific): + def __setattr__(self, name, value): + if name == "colorsequence": + value = [ + color_validator(v, allow_None=False, parent_name="Colorsequence") + for v in value + ] + return super().__setattr__(name, value) -class OpenMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of OpenMesh objects + colorsequence = param.List( + doc=""" + An iterable of color values used to cycle trough for every disconnected part of + disconnected triangular mesh object. A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + ) - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. +class Mesh(MagicParameterized): + grid = param.ClassSelector( + class_=GridMesh, + default=GridMesh(), + doc="""`GridMesh` properties.""", + ) - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ + open = param.ClassSelector( + class_=OpenMesh, + default=OpenMesh(), + doc="""`OpenMesh` properties.""", + ) + disconnected = param.ClassSelector( + class_=DisconnectedMesh, + default=DisconnectedMesh(), + doc="""`DisconnectedMesh` properties.""", + ) -class DisconnectedMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of DisconnectedMesh objects + selfintersecting = param.ClassSelector( + class_=SelfIntersectingMesh, + default=SelfIntersectingMesh(), + doc="""`SelfIntersectingMesh` properties.""", + ) - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. +class Orientation(MagicParameterized): + _allowed_symbols = ("cone", "arrow3d") - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. + show = param.Boolean( + default=True, + doc="Show/hide orientation symbol.", + ) - colorsequence: iterable, default=["red", "blue", "green", "cyan", "magenta", "yellow"] - An iterable of color values used to cycle trough for every disconnected part of - disconnected triangular mesh object. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color - """ + size = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, True), + softbounds=(1, 5), + doc="""Size of the orientation symbol""", + ) - @property - def colorsequence(self): - """An iterable of color values used to cycle trough for every disconnected part of - disconnected triangular mesh object. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""" - return self._colorsequence - - @colorsequence.setter - def colorsequence(self, val): - if val is not None: - name = type(self).__name__ - try: - val = tuple( - color_validator(c, allow_None=False, parent_name=f"{name}") - for c in val - ) - except TypeError as err: - raise ValueError( - f"The `colorsequence` property of {name} must be an " - f"iterable of colors but received {val!r} instead" - ) from err - - self._colorsequence = val - - -class SelfIntersectingMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of SelfIntersectingMesh objects - - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ + color = param.Color( + default=None, + allow_None=True, + doc="""A valid css color""", + ) + offset = param.Magnitude( + bounds=(0, 1), + inclusive_bounds=(True, True), + doc=""" + Defines the orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", + ) + symbol = param.Selector( + objects=_allowed_symbols, + doc=f"""Orientation symbol for the triangular faces. Can be one of: {_allowed_symbols}""", + ) -class TriMesh(MagicProperties): - """Defines TriMesh mesh properties. - Parameters - ---------- - grid: dict or GridMesh, default=None - All mesh vertices and edges of a TriangularMesh object. +class TriangleSpecific(MagnetSpecific): + orientation = param.ClassSelector( + class_=Orientation, + default=Orientation(), + doc="""`Orientation` properties.""", + ) - open: dict or OpenMesh, default=None - Shows open mesh vertices and edges of a TriangularMesh object, if any. - disconnected: dict or DisconnectedMesh, default=None - Shows disconnected bodies of a TriangularMesh object, if any. +class TriangularMeshSpecific(TriangleSpecific): + mesh = param.ClassSelector( + class_=Mesh, + default=Mesh(), + doc="""`Mesh` properties.""", + ) - selfintersecting: dict or SelfIntersectingMesh, default=None - Shows self-intersecting triangles of a TriangularMesh object, if any. - """ - @property - def grid(self): - """GridMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._grid +class MagnetStyle(BaseStyle, MagnetSpecific): + ... - @grid.setter - def grid(self, val): - self._grid = validate_property_class(val, "grid", GridMesh, self) - @property - def open(self): - """OpenMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._open +class CurrentStyle(BaseStyle, CurrentSpecific): + ... - @open.setter - def open(self, val): - self._open = validate_property_class(val, "open", OpenMesh, self) - @property - def disconnected(self): - """`DisconnectedMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._disconnected +class DipoleStyle(BaseStyle, DipoleSpecific): + ... - @disconnected.setter - def disconnected(self, val): - self._disconnected = validate_property_class( - val, "disconnected", DisconnectedMesh, self - ) - @property - def selfintersecting(self): - """`SelfIntersectingMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._selfintersecting +class SensorStyle(BaseStyle, SensorSpecific): + ... - @selfintersecting.setter - def selfintersecting(self, val): - self._selfintersecting = validate_property_class( - val, "selfintersecting", SelfIntersectingMesh, self - ) +class TriangleStyle(BaseStyle, TriangleSpecific): + ... -class Orientation(MagicProperties): - """Defines Triangle orientation properties. - Parameters - ---------- - show: bool, default=True - Show/hide orientation symbol. +class TriangularMeshStyle(BaseStyle, TriangularMeshSpecific): + ... - size: float, default=1, - Size of the orientation symbol - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. +class MarkersStyleSpecific(MagicParameterized): + marker = param.ClassSelector( + class_=Marker, + default=Marker(), + doc=""" + Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent + key/value pairs""", + ) - offset: float, default=0.1 - Defines the orientation symbol offset, normal to the triangle surface. Must be a number - between [0,1], 0 resulting in the cone/arrow head to be coincident to the triangle surface - and 1 with the base. - symbol: {"cone", "arrow3d"}: - Orientation symbol for the triangular faces. +class DisplayStyle(MagicParameterized): + """ + Base class containing styling properties for all object families. The properties of the + sub-classes get set to hard coded defaults at class instantiation. """ - _allowed_symbols = ("cone", "arrow3d") - - @property - def show(self): - """Show/hide arrow.""" - return self._show + def reset(self): + """Resets all nested properties to their hard coded default values""" + self.update(get_defaults_dict("display.style"), _match_properties=False) + return self - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Positive float for ratio of sensor to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val + base = param.ClassSelector( + class_=BaseStyle, + default=BaseStyle(), + doc="""Base properties common to all families""", + ) - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color + magnet = param.ClassSelector( + class_=MagnetSpecific, + default=MagnetSpecific(), + doc="""Magnet properties""", + ) - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") + current = param.ClassSelector( + class_=CurrentSpecific, + default=CurrentSpecific(), + doc="""Current properties""", + ) - @property - def offset(self): - """Defines the orientation symbol offset, normal to the triangle surface. `offset=0` results - in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the - base. - """ - return self._offset + triangularmesh = param.ClassSelector( + class_=TriangularMeshSpecific, + default=TriangularMeshSpecific(), + doc="""Triangularmesh properties""", + ) - @offset.setter - def offset(self, val): - assert val is None or (isinstance(val, (float, int))), ( - "The `offset` property must valid number\n" - f"but received {repr(val)} instead." - ) - self._offset = val - - @property - def symbol(self): - """Pixel symbol. Can be one of `("cone", "arrow3d")`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in self._allowed_symbols, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{self._allowed_symbols},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val + triangle = param.ClassSelector( + class_=TriangleSpecific, + default=TriangleSpecific(), + doc="""Triangularmesh properties""", + ) + dipole = param.ClassSelector( + class_=DipoleSpecific, + default=DipoleSpecific(), + doc="""Dipole properties""", + ) -class TriangleProperties: - """Defines Triangle properties. + sensor = param.ClassSelector( + class_=SensorSpecific, + default=SensorSpecific(), + doc="""Sensor properties""", + ) - Parameters - ---------- - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - """ - - @property - def orientation(self): - """`Orientation` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._orientation - - @orientation.setter - def orientation(self, val): - self._orientation = validate_property_class( - val, "orientation", Orientation, self - ) - - -class DefaultTriangle(MagicProperties, MagnetProperties, TriangleProperties): - """Defines styling properties of the Triangle class. - - Parameters - ---------- - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation of triangles styling with `'show'`, `'size'`, `'color', `'pivot'`, `'symbol'`` - properties or a dictionary with equivalent key/value pairs.. - """ - - def __init__(self, magnetization=None, orientation=None, **kwargs): - super().__init__(magnetization=magnetization, orientation=orientation, **kwargs) - - -class TriangleStyle(MagnetStyle, TriangleProperties): - """Defines styling properties of the Triangle class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - """ - - def __init__(self, orientation=None, **kwargs): - super().__init__(orientation=orientation, **kwargs) - - -class TriangularMeshProperties: - """Defines TriangularMesh properties.""" - - @property - def mesh(self): - """`TriMesh` instance with `'show', 'markers', 'line'` properties - or a dictionary with equivalent key/value pairs. - """ - return self._mesh - - @mesh.setter - def mesh(self, val): - self._mesh = validate_property_class(val, "mesh", TriMesh, self) - - -class DefaultTriangularMesh( - MagicProperties, MagnetProperties, TriangleProperties, TriangularMeshProperties -): - """Defines styling properties of homogeneous TriangularMesh magnet classes. - - Parameters - ---------- - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None - Orientation of triangles styling with `'show'`, `'size'`, `'color', `'pivot'`, `'symbol'`` - properties or a dictionary with equivalent key/value pairs. - - mesh: dict or TriMesh, default=None - TriMesh styling properties (e.g. `'grid', 'open', 'disconnected'`) - """ - - def __init__(self, magnetization=None, orientation=None, mesh=None, **kwargs): - super().__init__( - magnetization=magnetization, orientation=orientation, mesh=mesh, **kwargs - ) - - -class TriangularMeshStyle(MagnetStyle, TriangleProperties, TriangularMeshProperties): - """Defines styling properties of the TriangularMesh magnet class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - - mesh: dict or TriMesh, default=None, - mesh styling of triangles. - """ - - def __init__(self, orientation=None, **kwargs): - super().__init__(orientation=orientation, **kwargs) - - -class ArrowCS(MagicProperties): - """Defines triple coordinate system arrow properties. - - Parameters - ---------- - x: dict or `ArrowSingle` object, default=None - x-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - - y: dict or `ArrowSingle` object, default=None - y-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - - z: dict or `ArrowSingle` object, default=None - z-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - """ - - def __init__(self, x=None, y=None, z=None): - super().__init__(x=x, y=y, z=z) - - @property - def x(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._x - - @x.setter - def x(self, val): - self._x = validate_property_class(val, "x", ArrowSingle, self) - - @property - def y(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._y - - @y.setter - def y(self, val): - self._y = validate_property_class(val, "y", ArrowSingle, self) - - @property - def z(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._z - - @z.setter - def z(self, val): - self._z = validate_property_class(val, "z", ArrowSingle, self) - - -class ArrowSingle(MagicProperties): - """Single coordinate system arrow properties. - - Parameters - ---------- - show: bool, default=True - Show/hide arrow. - - color: color, default=None - Valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - """ - - def __init__(self, show=True, color=None): - super().__init__(show=show, color=color) - - @property - def show(self): - """Show/hide arrow.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - -class SensorProperties: - """Defines the specific styling properties of the Sensor class. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of sensor to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in meters. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - @property - def size(self): - """Positive float for ratio of sensor to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the sensor.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def pixel(self): - """`Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`).""" - return self._pixel - - @pixel.setter - def pixel(self, val): - self._pixel = validate_property_class(val, "pixel", Pixel, self) - - @property - def arrows(self): - """`ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`).""" - return self._arrows - - @arrows.setter - def arrows(self, val): - self._arrows = validate_property_class(val, "arrows", ArrowCS, self) - - -class DefaultSensor(MagicProperties, SensorProperties): - """Defines styling properties of the Sensor class. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of sensor to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in meters. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - def __init__( - self, - size=None, - sizemode=None, - pixel=None, - arrows=None, - **kwargs, - ): - super().__init__( - size=size, - sizemode=sizemode, - pixel=pixel, - arrows=arrows, - **kwargs, - ) - - -class SensorStyle(BaseStyle, SensorProperties): - """Defines the styling properties of the Sensor class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - size: float, default=None - Positive float for ratio of sensor size to canvas size. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Pixel(MagicProperties): - """Defines the styling properties of sensor pixels. - - Parameters - ---------- - size: float, default=1 - Positive float for relative pixel size. - - matplotlib backend: Pixel size is the marker size. - - plotly backend: Relative distance to nearest neighbor pixel. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the pixel size. If 'absolute', the `size` parameters - becomes the pixel size in meters. - - color: str, default=None - Defines the pixel color@property. - - symbol: str, default=None - Pixel symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`. - Only applies for matplotlib plotting backend. - """ - - def __init__(self, size=1, sizemode=None, color=None, symbol=None, **kwargs): - super().__init__( - size=size, - sizemode=sizemode, - color=color, - symbol=symbol, - **kwargs, - ) - - @property - def size(self): - """Positive float for relative pixel size. - - matplotlib backend: Pixel size is the marker size. - - plotly backend: Relative distance to nearest neighbor pixel.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"the `size` property of {type(self).__name__} must be a positive number" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the pixel.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def color(self): - """Pixel color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - @property - def symbol(self): - """Pixel symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in ALLOWED_SYMBOLS, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{ALLOWED_SYMBOLS},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val - - -class CurrentProperties: - """Defines styling properties of line current classes. - - Parameters - ---------- - arrow: dict or `Arrow` object, default=None - `Arrow` object or dict with `show, size, width, style, color` properties/keys. - - line: dict or `Line` object, default=None - `Line` object or dict with `show, width, style, color` properties/keys. - """ - - @property - def arrow(self): - """`Arrow` object or dict with `show, size, width, style, color` properties/keys.""" - return self._arrow - - @arrow.setter - def arrow(self, val): - self._arrow = validate_property_class(val, "current", Arrow, self) - - @property - def line(self): - """`Line` object or dict with `show, width, style, color` properties/keys.""" - return self._line - - @line.setter - def line(self, val): - self._line = validate_property_class(val, "line", CurrentLine, self) - - -class DefaultCurrent(MagicProperties, CurrentProperties): - """Defines the specific styling properties of line current classes. - - Parameters - ---------- - arrow: dict or `Arrow`object, default=None - `Arrow` object or dict with 'show', 'size' properties/keys. - """ - - def __init__(self, arrow=None, **kwargs): - super().__init__(arrow=arrow, **kwargs) - - -class CurrentStyle(BaseStyle, CurrentProperties): - """Defines styling properties of line current classes. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - arrow: dict or `Arrow` object, default=None - `Arrow` object or dict with `'show'`, `'size'` properties/keys. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Arrow(Line): - """Defines styling properties of current arrows. - - Parameters - ---------- - show: bool, default=None - Show/Hide arrow - - size: float, default=None - Positive number defining the size of the arrows. Effective value depends on the - `sizemode` parameter. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the arrow size. If 'absolute', the `size` parameters - becomes the arrow length in meters. - - offset: float, default=0.5 - Defines the arrow offset. `offset=0` results in the arrow head to be coincident to start - of the line, and `offset=1` with the end. - - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - - color: str, default=None - Line color. - - width: float, default=None - Positive number that defines the line width. - """ - - def __init__(self, show=None, size=None, **kwargs): - super().__init__(show=show, size=size, **kwargs) - - @property - def show(self): - """Show/hide arrow showing current direction.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False," - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Positive number defining the size of the arrows.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the arrows.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def offset(self): - """Defines the arrow offset. `offset=0` results in the arrow head to be coincident to start - of the line, and `offset=1` with the end. - """ - return self._offset - - @offset.setter - def offset(self, val): - assert val is None or (isinstance(val, (float, int))) and 0 <= val <= 1, ( - "The `offset` property must valid number between 0 and 1\n" - f"but received {repr(val)} instead." - ) - self._offset = val - - -class CurrentLine(Line): - """Defines styling properties of current lines. - - Parameters - ---------- - show: bool, default=None - Show/Hide arrow - - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - - color: str, default=None - Line color. - - width: float, default=None - Positive number that defines the line width. - """ - - @property - def show(self): - """Show/hide current line.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False," - f"but received {repr(val)} instead." - ) - self._show = val - - -class Marker(MagicProperties): - """Defines styling properties of plot markers. - - Parameters - ---------- - size: float, default=None - Marker size. - color: str, default=None - Marker color. - symbol: str, default=None - Marker symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`. - """ - - def __init__(self, size=None, color=None, symbol=None, **kwargs): - super().__init__(size=size, color=color, symbol=symbol, **kwargs) - - @property - def size(self): - """Marker size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def color(self): - """Marker color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val) - - @property - def symbol(self): - """Marker symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in ALLOWED_SYMBOLS, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{ALLOWED_SYMBOLS},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val - - -class DefaultMarkers(BaseStyle): - """Defines styling properties of the markers trace. - - Parameters - ---------- - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ - - def __init__(self, marker=None, **kwargs): - super().__init__(marker=marker, **kwargs) - - @property - def marker(self): - """`Markers` object with 'color', 'symbol', 'size' properties.""" - return self._marker - - @marker.setter - def marker(self, val): - self._marker = validate_property_class(val, "marker", Marker, self) - - -class DipoleProperties: - """Defines styling properties of dipoles. - - Parameters - ---------- - size: float - Positive value for ratio of dipole size to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in meters. - - pivot: str - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - _allowed_pivots = ("tail", "middle", "tip") - - @property - def size(self): - """Positive value for ratio of dipole size to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the dipole.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def pivot(self): - """The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - return self._pivot - - @pivot.setter - def pivot(self, val): - assert val is None or val in (self._allowed_pivots), ( - f"The `pivot` property of {type(self).__name__} must be one of " - f"{self._allowed_pivots},\n" - f"but received {repr(val)} instead." - ) - self._pivot = val - - -class DefaultDipole(MagicProperties, DipoleProperties): - """ - Defines styling properties of dipoles. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of dipole size to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in meters. - - pivot: str, default=None - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - def __init__(self, size=None, sizemode=None, pivot=None, **kwargs): - super().__init__(size=size, sizemode=sizemode, pivot=pivot, **kwargs) - - -class DipoleStyle(BaseStyle, DipoleProperties): - """Defines the styling properties of dipole objects. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - size: float, default=None - Positive float for ratio of dipole size to canvas size. - - pivot: str, default=None - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Path(MagicProperties, MarkerLineProperties): - """Defines styling properties of an object's path. - - Parameters - ---------- - show: bool, default=None - Show/hide path. - - False: Shows object(s) at final path position and hides paths lines and markers. - - True: Shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters. - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - frames: int or array_like, shape (n,), default=None - Show copies of the 3D-model along the given path indices. - - integer i: Displays the object(s) at every i'th path position. - - array_like, shape (n,), dtype=int: Displays object(s) at given path indices. - - numbering: bool, default=False - Show/hide numbering on path positions. - """ - - def __init__( - self, show=None, marker=None, line=None, frames=None, numbering=None, **kwargs - ): - super().__init__( - show=show, - marker=marker, - line=line, - frames=frames, - numbering=numbering, - **kwargs, - ) - - @property - def frames(self): - """Show copies of the 3D-model along the given path indices. - - integer i: Displays the object(s) at every i'th path position. - - array_like shape (n,) of integers: Displays object(s) at given path indices. - """ - return self._frames - - @frames.setter - def frames(self, val): - is_valid_path = True - if hasattr(val, "__iter__") and not isinstance(val, str): - val = tuple(val) - if not all(np.issubdtype(type(v), int) for v in val): - is_valid_path = False - elif not (val is None or np.issubdtype(type(val), int)): - is_valid_path = False - assert is_valid_path, f"""The `frames` property of {type(self).__name__} must be either: -- integer i: Displays the object(s) at every i'th path position. -- array_like, shape (n,), dtype=int: Displays object(s) at given path indices. -but received {repr(val)} instead""" - self._frames = val - - @property - def numbering(self): - """Show/hide numbering on path positions. Only applies if show=True.""" - return self._numbering - - @numbering.setter - def numbering(self, val): - assert val is None or isinstance(val, bool), ( - f"The `numbering` property of {type(self).__name__} must be one of (True, False),\n" - f"but received {repr(val)} instead." - ) - self._numbering = val - - -class DisplayStyle(MagicProperties): - """Base class containing styling properties for all object families. The properties of the - sub-classes are set to hard coded defaults at class instantiation. - - Parameters - ---------- - base: dict or `Base` object, default=None - Base properties common to all families. - - magnet: dict or `Magnet` object, default=None - Magnet properties. - - current: dict or `Current` object, default=None - Current properties. - - dipole: dict or `Dipole` object, default=None - Dipole properties. - - triangle: dict or `Triangle` object, default=None - Triangle properties - - sensor: dict or `Sensor` object, default=None - Sensor properties. - - markers: dict or `Markers` object, default=None - Markers properties. - """ - - def __init__( - self, - base=None, - magnet=None, - current=None, - dipole=None, - triangle=None, - sensor=None, - markers=None, - **kwargs, - ): - super().__init__( - base=base, - magnet=magnet, - current=current, - dipole=dipole, - triangle=triangle, - sensor=sensor, - markers=markers, - **kwargs, - ) - # self.reset() - - def reset(self): - """Resets all nested properties to their hard coded default values.""" - self.update(get_defaults_dict("display.style"), _match_properties=False) - return self - - @property - def base(self): - """Base properties common to all families.""" - return self._base - - @base.setter - def base(self, val): - self._base = validate_property_class(val, "base", BaseStyle, self) - - @property - def magnet(self): - """Magnet default style class.""" - return self._magnet - - @magnet.setter - def magnet(self, val): - self._magnet = validate_property_class(val, "magnet", DefaultMagnet, self) - - @property - def triangularmesh(self): - """TriangularMesh default style class.""" - return self._triangularmesh - - @triangularmesh.setter - def triangularmesh(self, val): - self._triangularmesh = validate_property_class( - val, "triangularmesh", DefaultTriangularMesh, self - ) - - @property - def current(self): - """Current default style class.""" - return self._current - - @current.setter - def current(self, val): - self._current = validate_property_class(val, "current", DefaultCurrent, self) - - @property - def dipole(self): - """Dipole default style class.""" - return self._dipole - - @dipole.setter - def dipole(self, val): - self._dipole = validate_property_class(val, "dipole", DefaultDipole, self) - - @property - def triangle(self): - """Triangle default style class.""" - return self._triangle - - @triangle.setter - def triangle(self, val): - self._triangle = validate_property_class(val, "triangle", DefaultTriangle, self) - - @property - def sensor(self): - """Sensor default style class.""" - return self._sensor - - @sensor.setter - def sensor(self, val): - self._sensor = validate_property_class(val, "sensor", DefaultSensor, self) - - @property - def markers(self): - """Markers default style class.""" - return self._markers - - @markers.setter - def markers(self, val): - self._markers = validate_property_class(val, "markers", DefaultMarkers, self) + markers = param.ClassSelector( + class_=MarkersStyleSpecific, + default=MarkersStyleSpecific(), + doc="""Markers properties""", + ) From 7933f9077ec4af6b67cdb92af06290582357c08a Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Fri, 2 Feb 2024 20:51:34 +0100 Subject: [PATCH 220/240] fix magic_to_dict recurisive kw propagation --- magpylib/_src/defaults/defaults_utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index a7332e3ca..3a59b1853 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -164,7 +164,7 @@ def magic_to_dict(kwargs, separator="_") -> dict: new_kwargs[keys[0]] = val for k, v in new_kwargs.items(): if isinstance(v, dict): - new_kwargs[k] = magic_to_dict(v) + new_kwargs[k] = magic_to_dict(v, separator=separator) return new_kwargs From 114f68788b66790e1de2c3db028092d29c0d9f94 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sun, 4 Feb 2024 21:49:22 +0100 Subject: [PATCH 221/240] draft --- magpylib/_src/defaults/defaults_values2.py | 760 +++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 magpylib/_src/defaults/defaults_values2.py diff --git a/magpylib/_src/defaults/defaults_values2.py b/magpylib/_src/defaults/defaults_values2.py new file mode 100644 index 000000000..a83d23261 --- /dev/null +++ b/magpylib/_src/defaults/defaults_values2.py @@ -0,0 +1,760 @@ +"""Package level config defaults""" +import param + +from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES +from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS +from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS + +ALLOWED_SIZEMODES = ("scaled", "absolute") + +DEFAULTS = { + "display.autosizefactor": { + "$type": "Number", + "default": 10, + "bounds": (0, None), + "inclusive_bounds": (False, True), + "softbounds": (5, 15), + "doc": """ + Defines at which scale objects like sensors and dipoles are displayed. + -> object_size = canvas_size / AUTOSIZE_FACTOR""", + }, + "display.animation.fps": { + "$type": "Integer", + "default": 20, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": """Target number of frames to be displayed per second.""", + }, + "display.animation.maxfps": { + "$type": "Integer", + "default": 30, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": """Maximum number of frames to be displayed per second before downsampling kicks in.""", + }, + "display.animation.maxframes": { + "$type": "Integer", + "default": 200, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": """Maximum total number of frames to be displayed before downsampling kicks in.""", + }, + "display.animation.time": { + "$type": "Number", + "default": 5, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": """Default animation time.""", + }, + "display.animation.slider": { + "$type": "Boolean", + "default": True, + "doc": """If True, an interactive slider will be displayed and stay in sync with the animation, Ïwill be hidden otherwise.""", + }, + "display.animation.output ": { + "$type": "String", + "doc": """Animation output type""", + "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", # either `mp4` or `gif` or ending with `.mp4` or `.gif`" + }, + "display.backend": { + "$type": "Selector", + "default": "auto", + "objects": ["auto", *SUPPORTED_PLOTTING_BACKENDS], + "doc": """ + Plotting backend to be used by default, if not explicitly set in the `display` + function (e.g. 'matplotlib', 'plotly'). + Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", + }, + "display.colorsequence": { + "$type": "List", + # "item_type": "Color", + "default": [ + "#2E91E5", + "#E15F99", + "#1CA71C", + "#FB0D0D", + "#DA16FF", + "#B68100", + "#750D86", + "#EB663B", + "#511CFB", + "#00A08B", + "#FB00D1", + "#FC0080", + "#B2828D", + "#6C7C32", + "#778AAE", + "#862A16", + "#A777F1", + "#620042", + "#1616A7", + "#DA60CA", + "#6C4516", + "#0D2A63", + "#AF0038", + "#222A2A", + ], + "doc": """ + An iterable of color values used to cycle trough for every object displayed. + A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + }, + "display.style.base.path.line.width": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Path line width.", + }, + "display.style.base.path.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Path line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.base.path.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Path line color.", + }, + "display.style.base.path.marker.size": { + "$type": None, + "default": 3, + "doc": "Path marker size.", + }, + "display.style.base.path.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Path marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.base.path.marker.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Path marker color.", + }, + "display.style.base.path.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide path.", + }, + "display.style.base.path.frames.indices ": { + "$type": "List", + "default": [], + "item_type": int, + "doc": """Array_like shape (n,) of integers: describes certain path indices.""", + }, + "display.style.base.path.frames.step ": { + "$type": "Integer", + "default": 1, + "bounds": (1, None), + "softbounds": (0, 10), + "doc": """Displays the object(s) at every i'th path position.""", + }, + "display.style.base.path.frames.mode ": { + "$type": "Selector", + "default": "indices", + "objects": ["indices", "step"], + "doc": """ + The object path frames mode. + - step: integer i: displays the object(s) at every i'th path position. + - indices: array_like shape (n,) of integers: describes certain path indices.""", + }, + "display.style.base.path.numbering": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide numbering on path positions. Only applies if show=True.", + }, + "display.style.base.description.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide object description in legend (shown in parentheses).", + }, + "display.style.base.description.text": { + "$type": "String", + "default": "", + "doc": "Object description text.", + }, + "display.style.base.legend.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide legend.", + }, + "display.style.base.legend.text": { + "$type": "String", + "default": "", + "doc": "Custom legend text. Overrides complete legend.", + }, + "display.style.base.opacity": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": "Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", + }, + "display.style.base.model3d.showdefault": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide default 3D-model.", + }, + "display.style.base.model3d.data": { + "$type": "List", + "default": [], + "doc": """ + A trace or list of traces where each is an instance of `Trace3d` or dictionary of + equivalent key/value pairs. Defines properties for an additional user-defined model3d + object which is positioned relatively to the main object to be displayed and moved + automatically with it. This feature also allows the user to replace the original 3d + representation of the object.""", + }, + "display.style.base.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Object explicit color", + }, + "display.style.magnet.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization indication (arrow and/or color).", + }, + "display.style.magnet.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization arrow.", + }, + "display.style.magnet.magnetization.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", + }, + "display.style.magnet.magnetization.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.magnet.magnetization.arrow.offset": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Defines the arrow offset. `offset=0` results in the arrow head to be coincident with + the start of the line, and `offset=1` with the end.""", + }, + "display.style.magnet.magnetization.arrow.width": { + "$type": None, + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Line width", + }, + "display.style.magnet.magnetization.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.magnet.magnetization.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Magnetization explicit arrow color. Takes the object color by default.", + }, + "display.style.magnet.magnetization.color.north": { + "$type": "Color", + "default": "#E71111", + "doc": "The color of the magnetic north pole.", + }, + "display.style.magnet.magnetization.color.middle": { + "$type": "Color", + "default": "#DDDDDD", + "doc": "The color between the magnetic poles.", + }, + "display.style.magnet.magnetization.color.south": { + "$type": "Color", + "default": "#00B050", + "doc": "The color of the magnetic south pole.", + }, + "display.style.magnet.magnetization.color.transition": { + "$type": "Number", + "default": 0.2, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + }, + "display.style.magnet.magnetization.color.mode": { + "$type": "Selector", + "default": "tricolor", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + }, + "display.style.magnet.magnetization.mode": { + "$type": "Selector", + "default": "auto", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", + }, + # TODO next -> + "display.style.current.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.current.arrow.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.current.arrow.sizemode": { + "$type": None, + "default": "scaled", + "doc": "", + }, + "display.style.current.arrow.offset": { + "$type": None, + "default": 0.5, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.current.arrow.width": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.current.arrow.style": {"$type": None, "default": "solid", "doc": ""}, + "display.style.current.arrow.color": {"$type": None, "default": None, "doc": ""}, + "display.style.current.line.show": {"$type": "Boolean", "default": True, "doc": ""}, + "display.style.current.line.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.current.line.style": {"$type": None, "default": "solid", "doc": ""}, + "display.style.current.line.color": {"$type": None, "default": None, "doc": ""}, + "display.style.sensor.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.sensor.sizemode": {"$type": None, "default": "scaled", "doc": ""}, + "display.style.sensor.pixel.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.sensor.pixel.sizemode": { + "$type": None, + "default": "scaled", + "doc": "", + }, + "display.style.sensor.pixel.color": {"$type": None, "default": None, "doc": ""}, + "display.style.sensor.pixel.symbol": {"$type": None, "default": "o", "doc": ""}, + "display.style.sensor.arrows.x.color": {"$type": None, "default": "red", "doc": ""}, + "display.style.sensor.arrows.y.color": { + "$type": None, + "default": "green", + "doc": "", + }, + "display.style.sensor.arrows.z.color": { + "$type": None, + "default": "blue", + "doc": "", + }, + "display.style.dipole.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.dipole.sizemode": {"$type": None, "default": "scaled", "doc": ""}, + "display.style.dipole.pivot": {"$type": None, "default": "middle", "doc": ""}, + "display.style.triangle.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.triangle.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.triangle.magnetization.arrow.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.magnetization.arrow.sizemode": { + "$type": None, + "default": "scaled", + "doc": "", + }, + "display.style.triangle.magnetization.arrow.offset": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.magnetization.arrow.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.magnetization.arrow.style": { + "$type": None, + "default": "solid", + "doc": "", + }, + "display.style.triangle.magnetization.arrow.color": { + "$type": None, + "default": None, + "doc": "", + }, + "display.style.triangle.magnetization.color.north": { + "$type": None, + "default": "#E71111", + "doc": "", + }, + "display.style.triangle.magnetization.color.middle": { + "$type": None, + "default": "#DDDDDD", + "doc": "", + }, + "display.style.triangle.magnetization.color.south": { + "$type": None, + "default": "#00B050", + "doc": "", + }, + "display.style.triangle.magnetization.color.transition": { + "$type": None, + "default": 0.2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.magnetization.color.mode": { + "$type": None, + "default": "tricolor", + "doc": "", + }, + "display.style.triangle.magnetization.mode": { + "$type": None, + "default": "auto", + "doc": "", + }, + "display.style.triangle.orientation.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.triangle.orientation.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.orientation.color": { + "$type": None, + "default": "grey", + "doc": "", + }, + "display.style.triangle.orientation.offset": { + "$type": None, + "default": 0.9, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangle.orientation.symbol": { + "$type": None, + "default": "arrow3d", + "doc": "", + }, + "display.style.triangularmesh.orientation.show": { + "$type": "Boolean", + "default": False, + "doc": "", + }, + "display.style.triangularmesh.orientation.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.orientation.color": { + "$type": None, + "default": "grey", + "doc": "", + }, + "display.style.triangularmesh.orientation.offset": { + "$type": None, + "default": 0.9, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.orientation.symbol": { + "$type": None, + "default": "arrow3d", + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.show": { + "$type": "Boolean", + "default": False, + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.line.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.line.style": { + "$type": None, + "default": "solid", + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.line.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.marker.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.marker.symbol": { + "$type": None, + "default": "o", + "doc": "", + }, + "display.style.triangularmesh.mesh.grid.marker.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.triangularmesh.mesh.open.show": { + "$type": "Boolean", + "default": False, + "doc": "", + }, + "display.style.triangularmesh.mesh.open.line.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.open.line.style": { + "$type": None, + "default": "solid", + "doc": "", + }, + "display.style.triangularmesh.mesh.open.line.color": { + "$type": None, + "default": "cyan", + "doc": "", + }, + "display.style.triangularmesh.mesh.open.marker.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.open.marker.symbol": { + "$type": None, + "default": "o", + "doc": "", + }, + "display.style.triangularmesh.mesh.open.marker.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.show": { + "$type": "Boolean", + "default": False, + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.line.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.line.style": { + "$type": None, + "default": "solid", + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.line.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.marker.size": { + "$type": None, + "default": 5, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.marker.symbol": { + "$type": None, + "default": "o", + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.marker.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.triangularmesh.mesh.disconnected.colorsequence": { + "$type": None, + "default": ("red", "blue", "green", "cyan", "magenta", "yellow"), + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.show": { + "$type": "Boolean", + "default": False, + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.width": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.style": { + "$type": None, + "default": "solid", + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.color": { + "$type": None, + "default": "magenta", + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.size": { + "$type": None, + "default": 1, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.symbol": { + "$type": None, + "default": "o", + "doc": "", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.color": { + "$type": None, + "default": "black", + "doc": "", + }, + "display.style.markers.marker.size": { + "$type": None, + "default": 2, + "bounds": (), + "inclusive_bounds": (), + "softbounds": (), + "doc": "", + }, + "display.style.markers.marker.color": {"$type": None, "default": "grey", "doc": ""}, + "display.style.markers.marker.symbol": {"$type": None, "default": "x", "doc": ""}, +} + + +def convert_to_param(dict_, parent=None): + parent = "" if not parent else parent[0].upper() + parent[1:] + params = {} + for key, val in dict_.items(): + if not isinstance(val, dict): + raise TypeError(f"{val} must be dict.") + typ = val.get("$type", None) + if typ: + params[key] = getattr(param, typ)( + **{k: v for k, v in val.items() if k != "$type"} + ) + else: + name = parent + key[0].upper() + key[1:] + val = convert_to_param(val, parent=name) + params[key] = param.ClassSelector(class_=val, default=val()) + class_ = type(parent, (param.Parameterized,), params) + return class_ From 330df79b2f8a044d1d95fad368c37644eb6c6d93 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 5 Feb 2024 13:49:18 +0100 Subject: [PATCH 222/240] update defaults dict --- magpylib/_src/defaults/defaults_values2.py | 604 +++++++++++++-------- 1 file changed, 363 insertions(+), 241 deletions(-) diff --git a/magpylib/_src/defaults/defaults_values2.py b/magpylib/_src/defaults/defaults_values2.py index a83d23261..a18c31911 100644 --- a/magpylib/_src/defaults/defaults_values2.py +++ b/magpylib/_src/defaults/defaults_values2.py @@ -6,6 +6,8 @@ from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS ALLOWED_SIZEMODES = ("scaled", "absolute") +ALLOWED_ORIENTATION_SYMBOLS = ("cone", "arrow3d") +ALLOWED_PIVOTS = ("tail", "middle", "tip") DEFAULTS = { "display.autosizefactor": { @@ -16,44 +18,46 @@ "softbounds": (5, 15), "doc": """ Defines at which scale objects like sensors and dipoles are displayed. - -> object_size = canvas_size / AUTOSIZE_FACTOR""", + -> object_size = canvas_size / autosizefactor""", }, "display.animation.fps": { "$type": "Integer", "default": 20, "bounds": (0, None), "inclusive_bounds": (False, None), - "doc": """Target number of frames to be displayed per second.""", + "doc": "Target number of frames to be displayed per second.", }, "display.animation.maxfps": { "$type": "Integer", "default": 30, "bounds": (0, None), "inclusive_bounds": (False, None), - "doc": """Maximum number of frames to be displayed per second before downsampling kicks in.""", + "doc": "Maximum number of frames to be displayed per second before downsampling kicks in.", }, "display.animation.maxframes": { "$type": "Integer", "default": 200, "bounds": (0, None), "inclusive_bounds": (False, None), - "doc": """Maximum total number of frames to be displayed before downsampling kicks in.""", + "doc": "Maximum total number of frames to be displayed before downsampling kicks in.", }, "display.animation.time": { "$type": "Number", "default": 5, "bounds": (0, None), "inclusive_bounds": (False, None), - "doc": """Default animation time.""", + "doc": "Default animation time.", }, "display.animation.slider": { "$type": "Boolean", "default": True, - "doc": """If True, an interactive slider will be displayed and stay in sync with the animation, Ïwill be hidden otherwise.""", + "doc": """If True, an interactive slider will be displayed and stay in sync with the + animation, will be hidden otherwise.""", }, - "display.animation.output ": { + "display.animation.output": { "$type": "String", - "doc": """Animation output type""", + "default": "", + "doc": "Animation output type", "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", # either `mp4` or `gif` or ending with `.mp4` or `.gif`" }, "display.backend": { @@ -67,7 +71,7 @@ }, "display.colorsequence": { "$type": "List", - # "item_type": "Color", + "item_type": "Color", "default": [ "#2E91E5", "#E15F99", @@ -121,11 +125,14 @@ "$type": "Color", "default": None, "allow_None": True, - "doc": "Path line color.", + "doc": "Explicit Path line color. Takes object color by default.", }, "display.style.base.path.marker.size": { - "$type": None, + "$type": "Number", "default": 3, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), "doc": "Path marker size.", }, "display.style.base.path.marker.symbol": { @@ -145,18 +152,18 @@ "default": True, "doc": "Show/hide path.", }, - "display.style.base.path.frames.indices ": { + "display.style.base.path.frames.indices": { "$type": "List", "default": [], "item_type": int, - "doc": """Array_like shape (n,) of integers: describes certain path indices.""", + "doc": "Array_like shape (n,) of integers: describes certain path indices.", }, "display.style.base.path.frames.step ": { "$type": "Integer", "default": 1, "bounds": (1, None), "softbounds": (0, 10), - "doc": """Displays the object(s) at every i'th path position.""", + "doc": "Displays the object(s) at every i'th path position.", }, "display.style.base.path.frames.mode ": { "$type": "Selector", @@ -252,28 +259,28 @@ "inclusive_bounds": (True, True), "softbounds": (0, 1), "doc": """ - Defines the arrow offset. `offset=0` results in the arrow head to be coincident with - the start of the line, and `offset=1` with the end.""", + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", }, "display.style.magnet.magnetization.arrow.width": { - "$type": None, + "$type": "Number", "default": 2, "bounds": (0, 20), "inclusive_bounds": (True, True), "softbounds": (1, 5), - "doc": "Line width", + "doc": "Magnetization arrow line width", }, "display.style.magnet.magnetization.arrow.style": { "$type": "Selector", "default": "solid", "objects": ALLOWED_LINESTYLES, - "doc": f"Arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + "doc": f"Magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.magnet.magnetization.arrow.color": { "$type": "Color", "default": None, "allow_None": True, - "doc": "Magnetization explicit arrow color. Takes the object color by default.", + "doc": "Explicit magnetization arrow color. Takes the object color by default.", }, "display.style.magnet.magnetization.color.north": { "$type": "Color", @@ -322,422 +329,537 @@ that the chosen backend determines which mode is applied by its capability. If the backend can display both and `auto` is chosen, the priority is given to `color`.""", }, - # TODO next -> "display.style.current.arrow.show": { "$type": "Boolean", "default": True, - "doc": "", + "doc": "Show/hide current arrow.", }, "display.style.current.arrow.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow size.", }, "display.style.current.arrow.sizemode": { - "$type": None, + "$type": "Selector", "default": "scaled", - "doc": "", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the current arrow size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", }, "display.style.current.arrow.offset": { - "$type": None, + "$type": "Number", "default": 0.5, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Current arrow offset. `offset=0` results in the arrow head to be coincident + with the start of the line, and `offset=1` with the end.""", }, "display.style.current.arrow.width": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow line width", + }, + "display.style.current.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current arrow color. Takes the object color by default.", + }, + "display.style.current.line.show": { + "$type": "Boolean", + "default": True, "doc": "", }, - "display.style.current.arrow.style": {"$type": None, "default": "solid", "doc": ""}, - "display.style.current.arrow.color": {"$type": None, "default": None, "doc": ""}, - "display.style.current.line.show": {"$type": "Boolean", "default": True, "doc": ""}, "display.style.current.line.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current line width.", + }, + "display.style.current.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current line color. Takes object color by default.", }, - "display.style.current.line.style": {"$type": None, "default": "solid", "doc": ""}, - "display.style.current.line.color": {"$type": None, "default": None, "doc": ""}, "display.style.sensor.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor size.", + }, + "display.style.sensor.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", }, - "display.style.sensor.sizemode": {"$type": None, "default": "scaled", "doc": ""}, "display.style.sensor.pixel.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor pixel size.", }, "display.style.sensor.pixel.sizemode": { - "$type": None, + "$type": "Selector", "default": "scaled", - "doc": "", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor pixel size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.sensor.pixel.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Sensor pixel color.", + }, + "display.style.sensor.pixel.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Pixel symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.sensor.arrows.x.color": { + "$type": "Color", + "default": "#ff0000", + "allow_None": True, + "doc": "Sensor x-arrow color.", }, - "display.style.sensor.pixel.color": {"$type": None, "default": None, "doc": ""}, - "display.style.sensor.pixel.symbol": {"$type": None, "default": "o", "doc": ""}, - "display.style.sensor.arrows.x.color": {"$type": None, "default": "red", "doc": ""}, "display.style.sensor.arrows.y.color": { - "$type": None, - "default": "green", - "doc": "", + "$type": "Color", + "default": "#008000", + "doc": "Sensor y-arrow color.", }, "display.style.sensor.arrows.z.color": { - "$type": None, - "default": "blue", - "doc": "", + "$type": "Color", + "default": "0000FF", + "doc": "Sensor z-arrow color.", }, "display.style.dipole.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Dipole size.", + }, + "display.style.dipole.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the dipole size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.dipole.pivot": { + "$type": "Selector", + "default": "middle", + "objects": ALLOWED_PIVOTS, + "doc": f""" + The part of the arrow that is anchored to the X, Y grid. The arrow rotates about + this point. Can be one of `{ALLOWED_PIVOTS}`""", }, - "display.style.dipole.sizemode": {"$type": None, "default": "scaled", "doc": ""}, - "display.style.dipole.pivot": {"$type": None, "default": "middle", "doc": ""}, "display.style.triangle.magnetization.show": { "$type": "Boolean", "default": True, - "doc": "", + "doc": "Show/hide magnetization indication (arrow and/or color).", }, "display.style.triangle.magnetization.arrow.show": { "$type": "Boolean", "default": True, - "doc": "", + "doc": "Show/hide magnetization arrow.", }, "display.style.triangle.magnetization.arrow.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", }, "display.style.triangle.magnetization.arrow.sizemode": { - "$type": None, + "$type": "Selector", "default": "scaled", - "doc": "", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", }, "display.style.triangle.magnetization.arrow.offset": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", }, "display.style.triangle.magnetization.arrow.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow line width", }, "display.style.triangle.magnetization.arrow.style": { - "$type": None, + "$type": "Selector", "default": "solid", - "doc": "", + "objects": ALLOWED_LINESTYLES, + "doc": f"Triangle magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangle.magnetization.arrow.color": { - "$type": None, + "$type": "Color", "default": None, - "doc": "", + "allow_None": True, + "doc": "Explicit triangle magnetization arrow color. Takes the object color by default.", }, "display.style.triangle.magnetization.color.north": { - "$type": None, + "$type": "Color", "default": "#E71111", - "doc": "", + "doc": "The color of the magnetic north pole.", }, "display.style.triangle.magnetization.color.middle": { - "$type": None, + "$type": "Color", "default": "#DDDDDD", - "doc": "", + "doc": "The color between the magnetic poles.", }, "display.style.triangle.magnetization.color.south": { - "$type": None, + "$type": "Color", "default": "#00B050", - "doc": "", + "doc": "The color of the magnetic south pole.", }, "display.style.triangle.magnetization.color.transition": { - "$type": None, + "$type": "Number", "default": 0.2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, }, "display.style.triangle.magnetization.color.mode": { - "$type": None, + "$type": "Selector", "default": "tricolor", - "doc": "", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", }, "display.style.triangle.magnetization.mode": { - "$type": None, + "$type": "Selector", "default": "auto", - "doc": "", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", }, "display.style.triangle.orientation.show": { "$type": "Boolean", "default": True, - "doc": "", + "doc": "Show/hide orientation symbol.", }, "display.style.triangle.orientation.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", }, "display.style.triangle.orientation.color": { - "$type": None, + "$type": "Color", "default": "grey", - "doc": "", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", }, "display.style.triangle.orientation.offset": { - "$type": None, + "$type": "Number", "default": 0.9, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0.1, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", }, "display.style.triangle.orientation.symbol": { - "$type": None, + "$type": "Selector", "default": "arrow3d", - "doc": "", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", }, "display.style.triangularmesh.orientation.show": { "$type": "Boolean", "default": False, - "doc": "", + "doc": "Show/hide orientation symbol.", }, "display.style.triangularmesh.orientation.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", }, "display.style.triangularmesh.orientation.color": { - "$type": None, + "$type": "Color", "default": "grey", - "doc": "", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", }, "display.style.triangularmesh.orientation.offset": { - "$type": None, + "$type": "Number", "default": 0.9, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0.1, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", }, "display.style.triangularmesh.orientation.symbol": { - "$type": None, + "$type": "Selector", "default": "arrow3d", - "doc": "", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", }, "display.style.triangularmesh.mesh.grid.show": { "$type": "Boolean", "default": False, - "doc": "", + "doc": "Show/hide mesh grid", }, "display.style.triangularmesh.mesh.grid.line.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid line width.", }, "display.style.triangularmesh.mesh.grid.line.style": { - "$type": None, + "$type": "Selector", "default": "solid", - "doc": "", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh grid line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangularmesh.mesh.grid.line.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", }, "display.style.triangularmesh.mesh.grid.marker.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid marker size.", }, "display.style.triangularmesh.mesh.grid.marker.symbol": { - "$type": None, + "$type": "Selector", "default": "o", - "doc": "", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh grid marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, "display.style.triangularmesh.mesh.grid.marker.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Mesh grid marker color.", }, "display.style.triangularmesh.mesh.open.show": { "$type": "Boolean", "default": False, - "doc": "", + "doc": "Show/hide mesh open", }, "display.style.triangularmesh.mesh.open.line.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open line width.", }, "display.style.triangularmesh.mesh.open.line.style": { - "$type": None, + "$type": "Selector", "default": "solid", - "doc": "", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh open line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangularmesh.mesh.open.line.color": { - "$type": None, - "default": "cyan", - "doc": "", + "$type": "Color", + "default": "#00FFFF", + "doc": "Explicit current line color. Takes object color by default.", }, "display.style.triangularmesh.mesh.open.marker.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open marker size.", }, "display.style.triangularmesh.mesh.open.marker.symbol": { - "$type": None, + "$type": "Selector", "default": "o", - "doc": "", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh open marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, "display.style.triangularmesh.mesh.open.marker.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Mesh open marker color.", }, "display.style.triangularmesh.mesh.disconnected.show": { "$type": "Boolean", "default": False, - "doc": "", + "doc": "Show/hide mesh disconnected", }, "display.style.triangularmesh.mesh.disconnected.line.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected line width.", }, "display.style.triangularmesh.mesh.disconnected.line.style": { - "$type": None, + "$type": "Selector", "default": "solid", - "doc": "", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh disconnected line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangularmesh.mesh.disconnected.line.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", }, "display.style.triangularmesh.mesh.disconnected.marker.size": { - "$type": None, - "default": 5, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected marker size.", }, "display.style.triangularmesh.mesh.disconnected.marker.symbol": { - "$type": None, + "$type": "Selector", "default": "o", - "doc": "", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh disconnected marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, "display.style.triangularmesh.mesh.disconnected.marker.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Mesh disconnected marker color.", }, "display.style.triangularmesh.mesh.disconnected.colorsequence": { - "$type": None, - "default": ("red", "blue", "green", "cyan", "magenta", "yellow"), - "doc": "", + "$type": "List", + "item_type": "Color", + "default": ["FF0000", "0000FF", "008000", "00FFFF", "FF00FF", "FFFF00"], + "doc": """ + An iterable of color values used to cycle trough for every disconnected part to be + displayed. A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", }, "display.style.triangularmesh.mesh.selfintersecting.show": { "$type": "Boolean", "default": False, - "doc": "", + "doc": "Show/hide mesh selfintersecting", }, "display.style.triangularmesh.mesh.selfintersecting.line.width": { - "$type": None, + "$type": "Number", "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting line width.", }, "display.style.triangularmesh.mesh.selfintersecting.line.style": { - "$type": None, + "$type": "Selector", "default": "solid", - "doc": "", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh selfintersecting line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangularmesh.mesh.selfintersecting.line.color": { - "$type": None, - "default": "magenta", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", }, "display.style.triangularmesh.mesh.selfintersecting.marker.size": { - "$type": None, + "$type": "Number", "default": 1, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting marker size.", }, "display.style.triangularmesh.mesh.selfintersecting.marker.symbol": { - "$type": None, + "$type": "Selector", "default": "o", - "doc": "", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh selfintersecting marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, "display.style.triangularmesh.mesh.selfintersecting.marker.color": { - "$type": None, - "default": "black", - "doc": "", + "$type": "Color", + "default": "#000000", + "doc": "Mesh selfintersecting marker color.", }, "display.style.markers.marker.size": { - "$type": None, - "default": 2, - "bounds": (), - "inclusive_bounds": (), - "softbounds": (), - "doc": "", + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Markers marker size.", + }, + "display.style.markers.marker.color": { + "$type": "Color", + "default": "#808080", + "doc": "Markers marker color.", + }, + "display.style.markers.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Markers marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, - "display.style.markers.marker.color": {"$type": None, "default": "grey", "doc": ""}, - "display.style.markers.marker.symbol": {"$type": None, "default": "x", "doc": ""}, } From fa44e554adad9ac2fc9e821258d32a87fff9c77e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 5 Feb 2024 21:32:09 +0100 Subject: [PATCH 223/240] refactor --- magpylib/__init__.py | 2 +- magpylib/_src/defaults/defaults_classes.py | 347 ++++-- magpylib/_src/defaults/defaults_utility.py | 99 +- magpylib/_src/defaults/defaults_values.py | 1001 ++++++++++++++--- magpylib/_src/defaults/defaults_values2.py | 882 --------------- magpylib/_src/display/traces_generic.py | 8 +- magpylib/_src/display/traces_utility.py | 2 +- magpylib/_src/input_checks.py | 2 +- .../_src/obj_classes/class_BaseExcitations.py | 4 +- magpylib/_src/obj_classes/class_BaseGeo.py | 2 +- magpylib/_src/obj_classes/class_Sensor.py | 2 +- .../class_magnet_TriangularMesh.py | 4 +- .../_src/obj_classes/class_misc_Dipole.py | 2 +- .../_src/obj_classes/class_misc_Triangle.py | 2 +- magpylib/_src/style.py | 1000 ---------------- magpylib/graphics/__init__.py | 2 +- magpylib/graphics/style/__init__.py | 2 +- tests/test_defaults.py | 8 +- 18 files changed, 1196 insertions(+), 2175 deletions(-) delete mode 100644 magpylib/_src/defaults/defaults_values2.py delete mode 100644 magpylib/_src/style.py diff --git a/magpylib/__init__.py b/magpylib/__init__.py index fa518fc42..509acc3df 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -50,7 +50,7 @@ ] # create interface to outside of package -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS from magpylib import magnet, current, misc, core, graphics from magpylib._src.defaults.defaults_classes import default_settings as defaults from magpylib._src.fields import getB, getH diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 5eb5e5a21..00a5fc6d8 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -1,147 +1,288 @@ import param from magpylib._src.defaults.defaults_utility import color_validator -from magpylib._src.defaults.defaults_utility import get_defaults_dict +from magpylib._src.defaults.defaults_utility import magic_to_dict from magpylib._src.defaults.defaults_utility import MagicParameterized -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.style import DisplayStyle +from magpylib._src.defaults.defaults_values import DEFAULTS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS # pylint: disable=missing-class-docstring -class Animation(MagicParameterized): - fps = param.Integer( - default=30, - bounds=(0, None), - inclusive_bounds=(False, None), - doc="""Target number of frames to be displayed per second.""", +class Trace3d(MagicParameterized): + _allowed_backends = (*SUPPORTED_PLOTTING_BACKENDS, "generic") + + def __setattr__(self, name, value): + validation_func = getattr(self, f"_validate_{name}", None) + if validation_func is not None: + value = validation_func(value) + return super().__setattr__(name, value) + + backend = param.Selector( + default="generic", + objects=_allowed_backends, + doc=f""" + Plotting backend corresponding to the trace. Can be one of + {_allowed_backends}""", ) - maxfps = param.Integer( - default=50, - bounds=(0, None), - inclusive_bounds=(False, None), - doc="""Maximum number of frames to be displayed per second before downsampling kicks in.""", + constructor = param.String( + doc=""" + Model constructor function or method to be called to build a 3D-model object + (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend.""" ) - maxframes = param.Integer( - default=200, - bounds=(0, None), - inclusive_bounds=(False, None), - doc="""Maximum total number of frames to be displayed before downsampling kicks in.""", + args = param.Parameter( + default=(), + doc=""" + Tuple or callable returning a tuple containing positional arguments for building a + 3D-model object.""", ) - time = param.Number( - default=5, - bounds=(0, None), - inclusive_bounds=(False, None), - doc="""Default animation time.""", + kwargs = param.Parameter( + default={}, + doc=""" + Dictionary or callable returning a dictionary containing the keys/values pairs for + building a 3D-model object.""", ) - slider = param.Boolean( + coordsargs = param.Dict( + default=None, + doc=""" + Tells Magpylib the name of the coordinate arrays to be moved or rotated. + by default: + - plotly backend: `{"x": "x", "y": "y", "z": "z"}` + - matplotlib backend: `{"x": "args[0]", "y": "args[1]", "z": "args[2]}"`""", + ) + + show = param.Boolean( default=True, + doc="""Show/hide model3d object based on provided trace.""", + ) + + scale = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, False), + softbounds=(0.1, 5), doc=""" - If True, an interactive slider will be displayed and stay in sync with the animation, - will be hidden otherwise.""", + Scaling factor by which the trace vertices coordinates should be multiplied by. + Be aware that if the object is not centered at the global CS origin, its position will also + be affected by scaling.""", ) - # either `mp4` or `gif` or ending with `.mp4` or `.gif`" - output = param.String( - doc="""Animation output type""", - regex=r"^(mp4|gif|(.*\.(mp4|gif))?)$", + updatefunc = param.Callable( + doc=""" + Callable object with no arguments. Should return a dictionary with keys from the + trace parameters. If provided, the function is called at `show` time and updates the + trace parameters with the output dictionary. This allows to update a trace dynamically + depending on class attributes, and postpone the trace construction to when the object is + displayed.""" ) + def _validate_coordsargs(self, value): + assert isinstance(value, dict) and all(key in value for key in "xyz"), ( + f"the `coordsargs` property of {type(self).__name__} must be " + f"a dictionary with `'x', 'y', 'z'` keys" + f" but received {repr(value)} instead" + ) + return value + + def _validate_updatefunc(self, val): + """Validate updatefunc.""" + if val is None: + + def val(): + return {} + + msg = "" + valid_keys = self.param.values().keys() + if not callable(val): + msg = f"Instead received {type(val)}" + else: + test_val = val() + if not isinstance(test_val, dict): + msg = f"but callable returned type {type(test_val)}." + else: + bad_keys = [k for k in test_val.keys() if k not in valid_keys] + if bad_keys: + msg = f"but invalid output dictionary keys received: {bad_keys}." + + assert msg == "", ( + f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " + f"dictionary with a subset of following keys: {list(valid_keys)}.\n" + f"{msg}" + ) + return val -class Display(MagicParameterized): + +class Model3d(MagicParameterized): def __setattr__(self, name, value): - if name == "colorsequence": - value = [ - color_validator(v, allow_None=False, parent_name="Colorsequence") - for v in value - ] + if name == "data": + value = self._validate_data(value) return super().__setattr__(name, value) - backend = param.Selector( - default="matplotlib", - objects=["auto", *SUPPORTED_PLOTTING_BACKENDS], - doc=""" - Plotting backend to be used by default, if not explicitly set in the `display` - function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", + showdefault = param.Boolean( + default=True, + doc="""Show/hide default 3D-model.""", ) - colorsequence = param.List( + data = param.List( + item_type=Trace3d, doc=""" - An iterable of color values used to cycle trough for every object displayed. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""", + A trace or list of traces where each is an instance of `Trace3d` or dictionary of equivalent + key/value pairs. Defines properties for an additional user-defined model3d object which is + positioned relatively to the main object to be displayed and moved automatically with it. + This feature also allows the user to replace the original 3d representation of the object. + """, ) - animation = param.ClassSelector( - class_=Animation, - default=Animation(), - doc=""" - Animation properties used when `animation=True` in the `show` function, - if applicaple to the chosen plotting backend.""", - ) + @staticmethod + def _validate_trace(trace, **kwargs): + updatefunc = None + if trace is None: + trace = Trace3d() + if not isinstance(trace, Trace3d) and callable(trace): + updatefunc = trace + trace = Trace3d() + if isinstance(trace, dict): + trace = Trace3d(**trace) + if isinstance(trace, Trace3d): + trace.updatefunc = updatefunc + if kwargs: + trace.update(**kwargs) + trace.update(trace.updatefunc()) + return trace - autosizefactor = param.Number( - default=10, - bounds=(0, None), - inclusive_bounds=(False, True), - softbounds=(5, 15), - doc=""" - Defines at which scale objects like sensors and dipoles are displayed. - -> object_size = canvas_size / AUTOSIZE_FACTOR""", - ) + def _validate_data(self, traces): + if traces is None: + traces = [] + elif not isinstance(traces, (list, tuple)): + traces = [traces] + new_traces = [] + for trace in traces: + new_traces.append(self._validate_trace(trace)) + return new_traces - style = param.ClassSelector( - class_=DisplayStyle, - default=DisplayStyle(), - doc="""class containing styling properties for any object family.""", - ) + def add_trace(self, trace=None, **kwargs): + """Adds user-defined 3d model object which is positioned relatively to the main object to be + displayed and moved automatically with it. This feature also allows the user to replace the + original 3d representation of the object. + + trace: Trace3d instance, dict or callable + Trace object. Can be a `Trace3d` instance or an dictionary with equivalent key/values + pairs, or a callable returning the equivalent dictionary. + backend: str + Plotting backend corresponding to the trace. Can be one of `['matplotlib', 'plotly']`. -class DefaultSettings(MagicParameterized): - """Library default settings. All default values get reset at class instantiation.""" + constructor: str + Model constructor function or method to be called to build a 3D-model object + (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._declare_watchers() - with param.parameterized.batch_call_watchers(self): - self.reset() + args: tuple, default=None + Tuple or callable returning a tuple containing positional arguments for building a + 3D-model object. - def reset(self): - """Resets all nested properties to their hard coded default values""" - self.update(get_defaults_dict(), _match_properties=False) + kwargs: dict or callable, default=None + Dictionary or callable returning a dictionary containing the keys/values pairs for + building a 3D-model object. + + coordsargs: dict, default=None + Tells magpylib the name of the coordinate arrays to be moved or rotated, + by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. + + show: bool, default=None + Show/hide model3d object based on provided trace. + + scale: float, default=1 + Scaling factor by which the trace vertices coordinates are multiplied. + + updatefunc: callable, default=None + Callable object with no arguments. Should return a dictionary with keys from the + trace parameters. If provided, the function is called at `show` time and updates the + trace parameters with the output dictionary. This allows to update a trace dynamically + depending on class attributes, and postpone the trace construction to when the object is + displayed. + """ + self.data = list(self.data) + [self._validate_trace(trace, **kwargs)] return self - def _declare_watchers(self): - props = get_defaults_dict(flatten=True, separator=".").keys() - for prop in props: - attrib_chain = prop.split(".") - child = attrib_chain[-1] - parent = self # start with self to go through dot chain - for attrib in attrib_chain[:-1]: - parent = getattr(parent, attrib) - parent.param.watch(self._set_to_defaults, parameter_names=[child]) + +class ColorSequence(param.List): + def __set__(self, obj, val): + self._validate_value(val, self.allow_None) + val = [color_validator(v) for v in val] + super().__set__(obj, val) + + +class Model3dData(param.List): + def __set__(self, obj, val): + self._validate_value(val, self.allow_None) + val = [self._validate_trace(v) for v in val] + super().__set__(obj, val) @staticmethod - def _set_to_defaults(event): - """Sets class defaults whenever magpylib defaults parameters instance are modifed.""" - event.obj.param[event.name].default = event.new + def _validate_trace(trace, **kwargs): + updatefunc = None + if trace is None: + trace = Trace3d() + if not isinstance(trace, Trace3d) and callable(trace): + updatefunc = trace + trace = Trace3d() + if isinstance(trace, dict): + trace = Trace3d(**trace) + if isinstance(trace, Trace3d): + trace.updatefunc = updatefunc + if kwargs: + trace.update(**kwargs) + trace.update(trace.updatefunc()) + return trace + + +def convert_to_param(dict_, parent=None): + """Convert nested defaults dict to nested MagicParameterized instances""" + parent = "" if not parent else parent[0].upper() + parent[1:] + params = {} + for key, val in dict_.items(): + if not isinstance(val, dict): + raise TypeError(f"{val} must be dict.") + if "$type" in val: + typ = None + typ_str = str(val["$type"]).capitalize() + args = {k: v for k, v in val.items() if k != "$type"} + if typ_str == "Color": + typ = param.Color + elif typ_str == "List": + it_typ = str(args.get("item_type", None)).capitalize() + if it_typ == "Color": + args.pop("item_type", None) + typ = ColorSequence + elif it_typ == "Trace3d": + args.pop("item_type", None) + typ = Model3dData + else: + typ = getattr(param, typ_str) + if typ is not None: + params[key] = typ(**args) + else: + name = parent + key[0].upper() + key[1:] + val = convert_to_param(val, parent=name) + params[key] = param.ClassSelector(class_=val, default=val()) + class_ = type(parent, (MagicParameterized,), params) + return class_ - display = param.ClassSelector( - class_=Display, - default=Display(), - doc=""" - `Display` defaults-class containing display settings. - `(e.g. 'backend', 'animation', 'colorsequence', ...)`""", - ) +default_settings = convert_to_param( + magic_to_dict(DEFAULTS, separator="."), parent="Settings" +) -default_settings = DefaultSettings() +default_style_classes = { + k: v.__class__ + for k, v in default_settings.display.style.param.values().items() + if isinstance(v, param.Parameterized) +} +locals()["BaseStyle"] = base = default_style_classes.pop("base") +for fam, klass in default_style_classes.items(): + klass_name = f"{fam.capitalize()}Style" + locals()[klass_name] = type(klass_name, (base, klass), {}) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 3a59b1853..84706c5f6 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -10,25 +10,6 @@ from magpylib._src.defaults.defaults_values import DEFAULTS -SUPPORTED_PLOTTING_BACKENDS = ("matplotlib", "plotly", "pyvista") - - -ALLOWED_SYMBOLS = (".", "+", "D", "d", "s", "x", "o") - -ALLOWED_LINESTYLES = ( - "solid", - "dashed", - "dotted", - "dashdot", - "loosely dotted", - "loosely dashdotted", - "-", - "--", - "-.", - ".", - ":", - (0, (1, 1)), -) COLORS_SHORT_TO_LONG = { "r": "red", @@ -82,7 +63,7 @@ def get_defaults_dict(arg=None, flatten=False, separator=".") -> dict: `flatten=True` """ - dict_ = deepcopy(DEFAULTS) + dict_ = deepcopy(DEFAULTS_VALUES) if arg is not None: for v in arg.split(separator): dict_ = dict_[v] @@ -312,6 +293,79 @@ def validate_style_keys(style_kwargs): return style_kwargs +def get_families(obj): + """get obj families""" + # pylint: disable=import-outside-toplevel + # pylint: disable=possibly-unused-variable + # pylint: disable=redefined-outer-name + from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet as Magnet + from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid + from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder + from magpylib._src.obj_classes.class_magnet_Sphere import Sphere + from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment + from magpylib._src.obj_classes.class_magnet_Tetrahedron import Tetrahedron + from magpylib._src.obj_classes.class_magnet_TriangularMesh import TriangularMesh + from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent as Current + from magpylib._src.obj_classes.class_current_Circle import Circle + from magpylib._src.obj_classes.class_current_Polyline import Polyline + from magpylib._src.obj_classes.class_misc_Dipole import Dipole + from magpylib._src.obj_classes.class_misc_CustomSource import CustomSource + from magpylib._src.obj_classes.class_misc_Triangle import Triangle + from magpylib._src.obj_classes.class_Sensor import Sensor + from magpylib._src.display.traces_generic import MagpyMarkers as Markers + + loc = locals() + obj_families = [] + for item, val in loc.items(): + if not item.startswith("_"): + try: + if isinstance(obj, val): + obj_families.append(item.lower()) + except TypeError: + pass + return obj_families + + +def get_style(obj, default_settings, **kwargs): + """Returns default style based on increasing priority: + - style from defaults + - style from object + - style from kwargs arguments + """ + obj_families = get_families(obj) + # parse kwargs into style an non-style arguments + style_kwargs = kwargs.get("style", {}) + style_kwargs.update( + {k[6:]: v for k, v in kwargs.items() if k.startswith("style") and k != "style"} + ) + + # retrieve default style dictionary, local import to avoid circular import + # pylint: disable=import-outside-toplevel + + default_style = default_settings.display.style + base_style_flat = default_style.base.as_dict(flatten=True, separator="_") + + # construct object specific dictionary base on style family and default style + for obj_family in obj_families: + family_style = getattr(default_style, obj_family, {}) + if family_style: + family_dict = family_style.as_dict(flatten=True, separator="_") + base_style_flat.update( + {k: v for k, v in family_dict.items() if v is not None} + ) + style_kwargs = validate_style_keys(style_kwargs) + + # create style class instance and update based on precedence + style = obj.style.copy() + style_kwargs_specific = { + k: v for k, v in style_kwargs.items() if k.split("_")[0] in style.as_dict() + } + style.update(**style_kwargs_specific, _match_properties=True) + style.update(**base_style_flat, _match_properties=False, _replace_None_only=True) + + return style + + def update_with_nested_dict(parameterized, nested_dict): """updates parameterized object recursively via setters""" # Using `batch_call_watchers` because it has the same underlying @@ -467,3 +521,8 @@ def as_dict(self, flatten=False, separator="_"): def copy(self): """returns a copy of the current class instance""" return type(self)(**self.as_dict()) + + +DEFAULTS_VALUES = magic_to_dict( + {k: v["default"] for k, v in DEFAULTS.items()}, separator="." +) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 2b150032b..1461ac23b 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -1,18 +1,89 @@ -"""Package level config defaults""" +SUPPORTED_PLOTTING_BACKENDS = ("matplotlib", "plotly", "pyvista") + +ALLOWED_PLOTTING_BACKENDS = ("auto", *SUPPORTED_PLOTTING_BACKENDS) +ALLOWED_SIZEMODES = ("scaled", "absolute") +ALLOWED_ORIENTATION_SYMBOLS = ("cone", "arrow3d") +ALLOWED_PIVOTS = ("tail", "middle", "tip") +ALLOWED_SYMBOLS = (".", "+", "D", "d", "s", "x", "o") +ALLOWED_LINESTYLES = ( + "solid", + "dashed", + "dotted", + "dashdot", + "loosely dotted", + "loosely dashdotted", + "-", + "--", + "-.", + ".", + ":", + (0, (1, 1)), +) DEFAULTS = { - "display": { - "autosizefactor": 10, - "animation": { - "fps": 20, - "maxfps": 30, - "maxframes": 200, - "time": 5, - "slider": True, - "output": "", - }, - "backend": "auto", - "colorsequence": ( + "display.autosizefactor": { + "$type": "Number", + "default": 10, + "bounds": (0, None), + "inclusive_bounds": (False, True), + "softbounds": (5, 15), + "doc": """ + Defines at which scale objects like sensors and dipoles are displayed. + -> object_size = canvas_size / autosizefactor""", + }, + "display.animation.fps": { + "$type": "Integer", + "default": 20, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Target number of frames to be displayed per second.", + }, + "display.animation.maxfps": { + "$type": "Integer", + "default": 30, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Maximum number of frames to be displayed per second before downsampling kicks in.", + }, + "display.animation.maxframes": { + "$type": "Integer", + "default": 200, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Maximum total number of frames to be displayed before downsampling kicks in.", + }, + "display.animation.time": { + "$type": "Number", + "default": 5, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Default animation time.", + }, + "display.animation.slider": { + "$type": "Boolean", + "default": True, + "doc": """If True, an interactive slider will be displayed and stay in sync with the + animation, will be hidden otherwise.""", + }, + "display.animation.output": { + "$type": "String", + "default": "", + "doc": "Animation output type", + "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", # either `mp4` or `gif` or ending with `.mp4` or `.gif`" + }, + "display.backend": { + "$type": "Selector", + "default": "auto", + "objects": ALLOWED_PLOTTING_BACKENDS, + "doc": """ + Plotting backend to be used by default, if not explicitly set in the `display` + function (e.g. 'matplotlib', 'plotly'). + Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", + }, + "display.colorsequence": { + "$type": "List", + "item_type": "Color", + "default": [ "#2E91E5", "#E15F99", "#1CA71C", @@ -37,141 +108,773 @@ "#0D2A63", "#AF0038", "#222A2A", - ), - "style": { - "base": { - "path": { - "line": {"width": 1, "style": "solid", "color": None}, - "marker": {"size": 3, "symbol": "o", "color": None}, - "show": True, - "frames": None, - "numbering": False, - }, - "description": {"show": True, "text": ""}, - "legend": {"show": True, "text": ""}, - "opacity": 1, - "model3d": {"showdefault": True, "data": []}, - "color": None, - }, - "magnet": { - "magnetization": { - "show": True, - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 1, - "width": 2, - "style": "solid", - "color": None, - }, - "color": { - "north": "#E71111", - "middle": "#DDDDDD", - "south": "#00B050", - "transition": 0.2, - "mode": "tricolor", - }, - "mode": "auto", - } - }, - "current": { - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 0.5, - "width": 1, - "style": "solid", - "color": None, - }, - "line": {"show": True, "width": 2, "style": "solid", "color": None}, - }, - "sensor": { - "size": 1, - "sizemode": "scaled", - "pixel": { - "size": 1, - "sizemode": "scaled", - "color": None, - "symbol": "o", - }, - "arrows": { - "x": {"color": "red"}, - "y": {"color": "green"}, - "z": {"color": "blue"}, - }, - }, - "dipole": {"size": 1, "sizemode": "scaled", "pivot": "middle"}, - "triangle": { - "magnetization": { - "show": True, - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 1, - "width": 2, - "style": "solid", - "color": None, - }, - "color": { - "north": "#E71111", - "middle": "#DDDDDD", - "south": "#00B050", - "transition": 0.2, - "mode": "tricolor", - }, - "mode": "auto", - }, - "orientation": { - "show": True, - "size": 1, - "color": "grey", - "offset": 0.9, - "symbol": "arrow3d", - }, - }, - "triangularmesh": { - "orientation": { - "show": False, - "size": 1, - "color": "grey", - "offset": 0.9, - "symbol": "arrow3d", - }, - "mesh": { - "grid": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "black"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - "open": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "cyan"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - "disconnected": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "black"}, - "marker": {"size": 5, "symbol": "o", "color": "black"}, - "colorsequence": ( - "red", - "blue", - "green", - "cyan", - "magenta", - "yellow", - ), - }, - "selfintersecting": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "magenta"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - }, - }, - "markers": {"marker": {"size": 2, "color": "grey", "symbol": "x"}}, - }, + ], + "doc": """ + An iterable of color values used to cycle trough for every object displayed. + A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + }, + "display.style.base.label": { + "$type": "String", + "default": "", + "doc": "Object label.", + }, + "display.style.base.description.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide object description in legend (shown in parentheses).", + }, + "display.style.base.description.text": { + "$type": "String", + "default": "", + "doc": "Object description text.", + }, + "display.style.base.legend.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide legend.", + }, + "display.style.base.legend.text": { + "$type": "String", + "default": "", + "doc": "Custom legend text. Overrides complete legend.", + }, + "display.style.base.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Object explicit color", + }, + "display.style.base.opacity": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": "Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", + }, + "display.style.base.path.line.width": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Path line width.", + }, + "display.style.base.path.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Path line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.base.path.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit Path line color. Takes object color by default.", + }, + "display.style.base.path.marker.size": { + "$type": "Number", + "default": 3, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Path marker size.", + }, + "display.style.base.path.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Path marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.base.path.marker.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Path marker color.", + }, + "display.style.base.path.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide path.", + }, + "display.style.base.path.frames.indices": { + "$type": "List", + "default": [], + "item_type": int, + "doc": "Array_like shape (n,) of integers: describes certain path indices.", + }, + "display.style.base.path.frames.step": { + "$type": "Integer", + "default": 1, + "bounds": (1, None), + "softbounds": (0, 10), + "doc": "Displays the object(s) at every i'th path position.", + }, + "display.style.base.path.frames.mode": { + "$type": "Selector", + "default": "step", + "objects": ("indices", "step"), + "doc": """ + The object path frames mode. + - step: integer i: displays the object(s) at every i'th path position. + - indices: array_like shape (n,) of integers: describes certain path indices.""", + }, + "display.style.base.path.numbering": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide numbering on path positions. Only applies if show=True.", + }, + "display.style.base.model3d.showdefault": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide default 3D-model.", + }, + "display.style.base.model3d.data": { + "$type": "List", + "default": [], + "item_type": "Trace3d", + "doc": """ + A trace or list of traces where each is an instance of `Trace3d` or dictionary of + equivalent key/value pairs. Defines properties for an additional user-defined model3d + object which is positioned relatively to the main object to be displayed and moved + automatically with it. This feature also allows the user to replace the original 3d + representation of the object.""", + }, + "display.style.magnet.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization indication (arrow and/or color).", + }, + "display.style.magnet.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization arrow.", + }, + "display.style.magnet.magnetization.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", + }, + "display.style.magnet.magnetization.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.magnet.magnetization.arrow.offset": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", + }, + "display.style.magnet.magnetization.arrow.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow line width", + }, + "display.style.magnet.magnetization.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.magnet.magnetization.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit magnetization arrow color. Takes the object color by default.", + }, + "display.style.magnet.magnetization.color.north": { + "$type": "Color", + "default": "#E71111", + "doc": "The color of the magnetic north pole.", + }, + "display.style.magnet.magnetization.color.middle": { + "$type": "Color", + "default": "#DDDDDD", + "doc": "The color between the magnetic poles.", + }, + "display.style.magnet.magnetization.color.south": { + "$type": "Color", + "default": "#00B050", + "doc": "The color of the magnetic south pole.", + }, + "display.style.magnet.magnetization.color.transition": { + "$type": "Number", + "default": 0.2, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + }, + "display.style.magnet.magnetization.color.mode": { + "$type": "Selector", + "default": "tricolor", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + }, + "display.style.magnet.magnetization.mode": { + "$type": "Selector", + "default": "auto", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", + }, + "display.style.current.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide current arrow.", + }, + "display.style.current.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow size.", + }, + "display.style.current.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the current arrow size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.current.arrow.offset": { + "$type": "Number", + "default": 0.5, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Current arrow offset. `offset=0` results in the arrow head to be coincident + with the start of the line, and `offset=1` with the end.""", + }, + "display.style.current.arrow.width": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow line width", + }, + "display.style.current.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current arrow color. Takes the object color by default.", + }, + "display.style.current.line.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.current.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current line width.", + }, + "display.style.current.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.sensor.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor size.", + }, + "display.style.sensor.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.sensor.pixel.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor pixel size.", + }, + "display.style.sensor.pixel.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor pixel size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.sensor.pixel.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Sensor pixel color.", + }, + "display.style.sensor.pixel.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Pixel symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.sensor.arrows.x.color": { + "$type": "Color", + "default": "#ff0000", + "allow_None": True, + "doc": "Sensor x-arrow color.", + }, + "display.style.sensor.arrows.y.color": { + "$type": "Color", + "default": "#008000", + "doc": "Sensor y-arrow color.", + }, + "display.style.sensor.arrows.z.color": { + "$type": "Color", + "default": "#0000FF", + "doc": "Sensor z-arrow color.", + }, + "display.style.dipole.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Dipole size.", + }, + "display.style.dipole.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the dipole size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.dipole.pivot": { + "$type": "Selector", + "default": "middle", + "objects": ALLOWED_PIVOTS, + "doc": f""" + The part of the arrow that is anchored to the X, Y grid. The arrow rotates about + this point. Can be one of `{ALLOWED_PIVOTS}`""", + }, + "display.style.triangle.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization indication (arrow and/or color).", + }, + "display.style.triangle.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization arrow.", + }, + "display.style.triangle.magnetization.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", + }, + "display.style.triangle.magnetization.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.triangle.magnetization.arrow.offset": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", + }, + "display.style.triangle.magnetization.arrow.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow line width", + }, + "display.style.triangle.magnetization.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Triangle magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangle.magnetization.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit triangle magnetization arrow color. Takes the object color by default.", + }, + "display.style.triangle.magnetization.color.north": { + "$type": "Color", + "default": "#E71111", + "doc": "The color of the magnetic north pole.", + }, + "display.style.triangle.magnetization.color.middle": { + "$type": "Color", + "default": "#DDDDDD", + "doc": "The color between the magnetic poles.", + }, + "display.style.triangle.magnetization.color.south": { + "$type": "Color", + "default": "#00B050", + "doc": "The color of the magnetic south pole.", + }, + "display.style.triangle.magnetization.color.transition": { + "$type": "Number", + "default": 0.2, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + }, + "display.style.triangle.magnetization.color.mode": { + "$type": "Selector", + "default": "tricolor", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + }, + "display.style.triangle.magnetization.mode": { + "$type": "Selector", + "default": "auto", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", + }, + "display.style.triangle.orientation.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide orientation symbol.", + }, + "display.style.triangle.orientation.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", + }, + "display.style.triangle.orientation.color": { + "$type": "Color", + "default": "grey", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", + }, + "display.style.triangle.orientation.offset": { + "$type": "Number", + "default": 0.9, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0.1, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", + }, + "display.style.triangle.orientation.symbol": { + "$type": "Selector", + "default": "arrow3d", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", + }, + "display.style.triangularmesh.orientation.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide orientation symbol.", + }, + "display.style.triangularmesh.orientation.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", + }, + "display.style.triangularmesh.orientation.color": { + "$type": "Color", + "default": "grey", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", + }, + "display.style.triangularmesh.orientation.offset": { + "$type": "Number", + "default": 0.9, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0.1, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", + }, + "display.style.triangularmesh.orientation.symbol": { + "$type": "Selector", + "default": "arrow3d", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", + }, + "display.style.triangularmesh.mesh.grid.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh grid", + }, + "display.style.triangularmesh.mesh.grid.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid line width.", + }, + "display.style.triangularmesh.mesh.grid.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh grid line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.grid.line.color": { + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.grid.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid marker size.", + }, + "display.style.triangularmesh.mesh.grid.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh grid marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.grid.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh grid marker color.", + }, + "display.style.triangularmesh.mesh.open.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh open", + }, + "display.style.triangularmesh.mesh.open.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open line width.", + }, + "display.style.triangularmesh.mesh.open.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh open line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.open.line.color": { + "$type": "Color", + "default": "#00FFFF", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.open.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open marker size.", + }, + "display.style.triangularmesh.mesh.open.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh open marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.open.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh open marker color.", + }, + "display.style.triangularmesh.mesh.disconnected.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh disconnected", + }, + "display.style.triangularmesh.mesh.disconnected.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected line width.", + }, + "display.style.triangularmesh.mesh.disconnected.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh disconnected line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.disconnected.line.color": { + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected marker size.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh disconnected marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh disconnected marker color.", + }, + "display.style.triangularmesh.mesh.disconnected.colorsequence": { + "$type": "List", + "item_type": "Color", + "default": ["FF0000", "#0000FF", "008000", "00FFFF", "FF00FF", "FFFF00"], + "doc": """ + An iterable of color values used to cycle trough for every disconnected part to be + displayed. A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + }, + "display.style.triangularmesh.mesh.selfintersecting.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh selfintersecting", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting line width.", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh selfintersecting line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.color": { + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting marker size.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh selfintersecting marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh selfintersecting marker color.", + }, + "display.style.markers.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Markers marker size.", + }, + "display.style.markers.marker.color": { + "$type": "Color", + "default": "#808080", + "doc": "Markers marker color.", + }, + "display.style.markers.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Markers marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, } diff --git a/magpylib/_src/defaults/defaults_values2.py b/magpylib/_src/defaults/defaults_values2.py deleted file mode 100644 index a18c31911..000000000 --- a/magpylib/_src/defaults/defaults_values2.py +++ /dev/null @@ -1,882 +0,0 @@ -"""Package level config defaults""" -import param - -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS - -ALLOWED_SIZEMODES = ("scaled", "absolute") -ALLOWED_ORIENTATION_SYMBOLS = ("cone", "arrow3d") -ALLOWED_PIVOTS = ("tail", "middle", "tip") - -DEFAULTS = { - "display.autosizefactor": { - "$type": "Number", - "default": 10, - "bounds": (0, None), - "inclusive_bounds": (False, True), - "softbounds": (5, 15), - "doc": """ - Defines at which scale objects like sensors and dipoles are displayed. - -> object_size = canvas_size / autosizefactor""", - }, - "display.animation.fps": { - "$type": "Integer", - "default": 20, - "bounds": (0, None), - "inclusive_bounds": (False, None), - "doc": "Target number of frames to be displayed per second.", - }, - "display.animation.maxfps": { - "$type": "Integer", - "default": 30, - "bounds": (0, None), - "inclusive_bounds": (False, None), - "doc": "Maximum number of frames to be displayed per second before downsampling kicks in.", - }, - "display.animation.maxframes": { - "$type": "Integer", - "default": 200, - "bounds": (0, None), - "inclusive_bounds": (False, None), - "doc": "Maximum total number of frames to be displayed before downsampling kicks in.", - }, - "display.animation.time": { - "$type": "Number", - "default": 5, - "bounds": (0, None), - "inclusive_bounds": (False, None), - "doc": "Default animation time.", - }, - "display.animation.slider": { - "$type": "Boolean", - "default": True, - "doc": """If True, an interactive slider will be displayed and stay in sync with the - animation, will be hidden otherwise.""", - }, - "display.animation.output": { - "$type": "String", - "default": "", - "doc": "Animation output type", - "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", # either `mp4` or `gif` or ending with `.mp4` or `.gif`" - }, - "display.backend": { - "$type": "Selector", - "default": "auto", - "objects": ["auto", *SUPPORTED_PLOTTING_BACKENDS], - "doc": """ - Plotting backend to be used by default, if not explicitly set in the `display` - function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", - }, - "display.colorsequence": { - "$type": "List", - "item_type": "Color", - "default": [ - "#2E91E5", - "#E15F99", - "#1CA71C", - "#FB0D0D", - "#DA16FF", - "#B68100", - "#750D86", - "#EB663B", - "#511CFB", - "#00A08B", - "#FB00D1", - "#FC0080", - "#B2828D", - "#6C7C32", - "#778AAE", - "#862A16", - "#A777F1", - "#620042", - "#1616A7", - "#DA60CA", - "#6C4516", - "#0D2A63", - "#AF0038", - "#222A2A", - ], - "doc": """ - An iterable of color values used to cycle trough for every object displayed. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""", - }, - "display.style.base.path.line.width": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Path line width.", - }, - "display.style.base.path.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Path line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.base.path.line.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Explicit Path line color. Takes object color by default.", - }, - "display.style.base.path.marker.size": { - "$type": "Number", - "default": 3, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Path marker size.", - }, - "display.style.base.path.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Path marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.base.path.marker.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Path marker color.", - }, - "display.style.base.path.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide path.", - }, - "display.style.base.path.frames.indices": { - "$type": "List", - "default": [], - "item_type": int, - "doc": "Array_like shape (n,) of integers: describes certain path indices.", - }, - "display.style.base.path.frames.step ": { - "$type": "Integer", - "default": 1, - "bounds": (1, None), - "softbounds": (0, 10), - "doc": "Displays the object(s) at every i'th path position.", - }, - "display.style.base.path.frames.mode ": { - "$type": "Selector", - "default": "indices", - "objects": ["indices", "step"], - "doc": """ - The object path frames mode. - - step: integer i: displays the object(s) at every i'th path position. - - indices: array_like shape (n,) of integers: describes certain path indices.""", - }, - "display.style.base.path.numbering": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide numbering on path positions. Only applies if show=True.", - }, - "display.style.base.description.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide object description in legend (shown in parentheses).", - }, - "display.style.base.description.text": { - "$type": "String", - "default": "", - "doc": "Object description text.", - }, - "display.style.base.legend.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide legend.", - }, - "display.style.base.legend.text": { - "$type": "String", - "default": "", - "doc": "Custom legend text. Overrides complete legend.", - }, - "display.style.base.opacity": { - "$type": "Number", - "default": 1, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": "Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", - }, - "display.style.base.model3d.showdefault": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide default 3D-model.", - }, - "display.style.base.model3d.data": { - "$type": "List", - "default": [], - "doc": """ - A trace or list of traces where each is an instance of `Trace3d` or dictionary of - equivalent key/value pairs. Defines properties for an additional user-defined model3d - object which is positioned relatively to the main object to be displayed and moved - automatically with it. This feature also allows the user to replace the original 3d - representation of the object.""", - }, - "display.style.base.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Object explicit color", - }, - "display.style.magnet.magnetization.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide magnetization indication (arrow and/or color).", - }, - "display.style.magnet.magnetization.arrow.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide magnetization arrow.", - }, - "display.style.magnet.magnetization.arrow.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Magnetization arrow size.", - }, - "display.style.magnet.magnetization.arrow.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.magnet.magnetization.arrow.offset": { - "$type": "Number", - "default": 1, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": """ - Magnetization arrow offset. `offset=0` results in the arrow head to be - coincident with the start of the line, and `offset=1` with the end.""", - }, - "display.style.magnet.magnetization.arrow.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Magnetization arrow line width", - }, - "display.style.magnet.magnetization.arrow.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.magnet.magnetization.arrow.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Explicit magnetization arrow color. Takes the object color by default.", - }, - "display.style.magnet.magnetization.color.north": { - "$type": "Color", - "default": "#E71111", - "doc": "The color of the magnetic north pole.", - }, - "display.style.magnet.magnetization.color.middle": { - "$type": "Color", - "default": "#DDDDDD", - "doc": "The color between the magnetic poles.", - }, - "display.style.magnet.magnetization.color.south": { - "$type": "Color", - "default": "#00B050", - "doc": "The color of the magnetic south pole.", - }, - "display.style.magnet.magnetization.color.transition": { - "$type": "Number", - "default": 0.2, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. - - `transition=0`: discrete transition - - `transition=1`: smoothest transition - """, - }, - "display.style.magnet.magnetization.color.mode": { - "$type": "Selector", - "default": "tricolor", - "objects": ("tricolor", "bicolor", "tricycle"), - "doc": """ - Sets the coloring mode for the magnetization. - - `'bicolor'`: only north and south poles are shown, middle color is hidden. - - `'tricolor'`: both pole colors and middle color are shown. - - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling - through the color sequence.""", - }, - "display.style.magnet.magnetization.mode": { - "$type": "Selector", - "default": "auto", - "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), - "doc": """ - One of {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`.""", - }, - "display.style.current.arrow.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide current arrow.", - }, - "display.style.current.arrow.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Current arrow size.", - }, - "display.style.current.arrow.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the current arrow size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.current.arrow.offset": { - "$type": "Number", - "default": 0.5, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": """ - Current arrow offset. `offset=0` results in the arrow head to be coincident - with the start of the line, and `offset=1` with the end.""", - }, - "display.style.current.arrow.width": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Current arrow line width", - }, - "display.style.current.arrow.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Current arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.current.arrow.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Explicit current arrow color. Takes the object color by default.", - }, - "display.style.current.line.show": { - "$type": "Boolean", - "default": True, - "doc": "", - }, - "display.style.current.line.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Current line width.", - }, - "display.style.current.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Current line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.current.line.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Explicit current line color. Takes object color by default.", - }, - "display.style.sensor.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Sensor size.", - }, - "display.style.sensor.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the sensor size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.sensor.pixel.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Sensor pixel size.", - }, - "display.style.sensor.pixel.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the sensor pixel size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.sensor.pixel.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Sensor pixel color.", - }, - "display.style.sensor.pixel.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Pixel symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.sensor.arrows.x.color": { - "$type": "Color", - "default": "#ff0000", - "allow_None": True, - "doc": "Sensor x-arrow color.", - }, - "display.style.sensor.arrows.y.color": { - "$type": "Color", - "default": "#008000", - "doc": "Sensor y-arrow color.", - }, - "display.style.sensor.arrows.z.color": { - "$type": "Color", - "default": "0000FF", - "doc": "Sensor z-arrow color.", - }, - "display.style.dipole.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Dipole size.", - }, - "display.style.dipole.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the dipole size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.dipole.pivot": { - "$type": "Selector", - "default": "middle", - "objects": ALLOWED_PIVOTS, - "doc": f""" - The part of the arrow that is anchored to the X, Y grid. The arrow rotates about - this point. Can be one of `{ALLOWED_PIVOTS}`""", - }, - "display.style.triangle.magnetization.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide magnetization indication (arrow and/or color).", - }, - "display.style.triangle.magnetization.arrow.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide magnetization arrow.", - }, - "display.style.triangle.magnetization.arrow.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Magnetization arrow size.", - }, - "display.style.triangle.magnetization.arrow.sizemode": { - "$type": "Selector", - "default": "scaled", - "objects": ALLOWED_SIZEMODES, - "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", - }, - "display.style.triangle.magnetization.arrow.offset": { - "$type": "Number", - "default": 1, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": """ - Magnetization arrow offset. `offset=0` results in the arrow head to be - coincident with the start of the line, and `offset=1` with the end.""", - }, - "display.style.triangle.magnetization.arrow.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Magnetization arrow line width", - }, - "display.style.triangle.magnetization.arrow.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Triangle magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.triangle.magnetization.arrow.color": { - "$type": "Color", - "default": None, - "allow_None": True, - "doc": "Explicit triangle magnetization arrow color. Takes the object color by default.", - }, - "display.style.triangle.magnetization.color.north": { - "$type": "Color", - "default": "#E71111", - "doc": "The color of the magnetic north pole.", - }, - "display.style.triangle.magnetization.color.middle": { - "$type": "Color", - "default": "#DDDDDD", - "doc": "The color between the magnetic poles.", - }, - "display.style.triangle.magnetization.color.south": { - "$type": "Color", - "default": "#00B050", - "doc": "The color of the magnetic south pole.", - }, - "display.style.triangle.magnetization.color.transition": { - "$type": "Number", - "default": 0.2, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0, 1), - "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. - - `transition=0`: discrete transition - - `transition=1`: smoothest transition - """, - }, - "display.style.triangle.magnetization.color.mode": { - "$type": "Selector", - "default": "tricolor", - "objects": ("tricolor", "bicolor", "tricycle"), - "doc": """ - Sets the coloring mode for the magnetization. - - `'bicolor'`: only north and south poles are shown, middle color is hidden. - - `'tricolor'`: both pole colors and middle color are shown. - - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling - through the color sequence.""", - }, - "display.style.triangle.magnetization.mode": { - "$type": "Selector", - "default": "auto", - "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), - "doc": """ - One of {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`.""", - }, - "display.style.triangle.orientation.show": { - "$type": "Boolean", - "default": True, - "doc": "Show/hide orientation symbol.", - }, - "display.style.triangle.orientation.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Size of the orientation symbol", - }, - "display.style.triangle.orientation.color": { - "$type": "Color", - "default": "grey", - "doc": "Explicit orientation symbol color. Takes the objet color by default.", - }, - "display.style.triangle.orientation.offset": { - "$type": "Number", - "default": 0.9, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0.1, 0.9), - "doc": """ - Orientation symbol offset, normal to the triangle surface. `offset=0` results - in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the - base""", - }, - "display.style.triangle.orientation.symbol": { - "$type": "Selector", - "default": "arrow3d", - "objects": ALLOWED_ORIENTATION_SYMBOLS, - "doc": f""" - Orientation symbol for the triangular faces. Can be one of: - {ALLOWED_ORIENTATION_SYMBOLS}""", - }, - "display.style.triangularmesh.orientation.show": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide orientation symbol.", - }, - "display.style.triangularmesh.orientation.size": { - "$type": "Number", - "default": 1, - "bounds": (0, None), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Size of the orientation symbol", - }, - "display.style.triangularmesh.orientation.color": { - "$type": "Color", - "default": "grey", - "doc": "Explicit orientation symbol color. Takes the objet color by default.", - }, - "display.style.triangularmesh.orientation.offset": { - "$type": "Number", - "default": 0.9, - "bounds": (0, 1), - "inclusive_bounds": (True, True), - "softbounds": (0.1, 0.9), - "doc": """ - Orientation symbol offset, normal to the triangle surface. `offset=0` results - in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the - base""", - }, - "display.style.triangularmesh.orientation.symbol": { - "$type": "Selector", - "default": "arrow3d", - "objects": ALLOWED_ORIENTATION_SYMBOLS, - "doc": f""" - Orientation symbol for the triangular faces. Can be one of: - {ALLOWED_ORIENTATION_SYMBOLS}""", - }, - "display.style.triangularmesh.mesh.grid.show": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide mesh grid", - }, - "display.style.triangularmesh.mesh.grid.line.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh grid line width.", - }, - "display.style.triangularmesh.mesh.grid.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Mesh grid line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.triangularmesh.mesh.grid.line.color": { - "$type": "Color", - "default": "#000000", - "doc": "Explicit current line color. Takes object color by default.", - }, - "display.style.triangularmesh.mesh.grid.marker.size": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh grid marker size.", - }, - "display.style.triangularmesh.mesh.grid.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Mesh grid marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.triangularmesh.mesh.grid.marker.color": { - "$type": "Color", - "default": "#000000", - "doc": "Mesh grid marker color.", - }, - "display.style.triangularmesh.mesh.open.show": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide mesh open", - }, - "display.style.triangularmesh.mesh.open.line.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh open line width.", - }, - "display.style.triangularmesh.mesh.open.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Mesh open line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.triangularmesh.mesh.open.line.color": { - "$type": "Color", - "default": "#00FFFF", - "doc": "Explicit current line color. Takes object color by default.", - }, - "display.style.triangularmesh.mesh.open.marker.size": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh open marker size.", - }, - "display.style.triangularmesh.mesh.open.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Mesh open marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.triangularmesh.mesh.open.marker.color": { - "$type": "Color", - "default": "#000000", - "doc": "Mesh open marker color.", - }, - "display.style.triangularmesh.mesh.disconnected.show": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide mesh disconnected", - }, - "display.style.triangularmesh.mesh.disconnected.line.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh disconnected line width.", - }, - "display.style.triangularmesh.mesh.disconnected.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Mesh disconnected line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.triangularmesh.mesh.disconnected.line.color": { - "$type": "Color", - "default": "#000000", - "doc": "Explicit current line color. Takes object color by default.", - }, - "display.style.triangularmesh.mesh.disconnected.marker.size": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh disconnected marker size.", - }, - "display.style.triangularmesh.mesh.disconnected.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Mesh disconnected marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.triangularmesh.mesh.disconnected.marker.color": { - "$type": "Color", - "default": "#000000", - "doc": "Mesh disconnected marker color.", - }, - "display.style.triangularmesh.mesh.disconnected.colorsequence": { - "$type": "List", - "item_type": "Color", - "default": ["FF0000", "0000FF", "008000", "00FFFF", "FF00FF", "FFFF00"], - "doc": """ - An iterable of color values used to cycle trough for every disconnected part to be - displayed. A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""", - }, - "display.style.triangularmesh.mesh.selfintersecting.show": { - "$type": "Boolean", - "default": False, - "doc": "Show/hide mesh selfintersecting", - }, - "display.style.triangularmesh.mesh.selfintersecting.line.width": { - "$type": "Number", - "default": 2, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh selfintersecting line width.", - }, - "display.style.triangularmesh.mesh.selfintersecting.line.style": { - "$type": "Selector", - "default": "solid", - "objects": ALLOWED_LINESTYLES, - "doc": f"Mesh selfintersecting line style. Can be one of: {ALLOWED_LINESTYLES}.", - }, - "display.style.triangularmesh.mesh.selfintersecting.line.color": { - "$type": "Color", - "default": "#000000", - "doc": "Explicit current line color. Takes object color by default.", - }, - "display.style.triangularmesh.mesh.selfintersecting.marker.size": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Mesh selfintersecting marker size.", - }, - "display.style.triangularmesh.mesh.selfintersecting.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Mesh selfintersecting marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, - "display.style.triangularmesh.mesh.selfintersecting.marker.color": { - "$type": "Color", - "default": "#000000", - "doc": "Mesh selfintersecting marker color.", - }, - "display.style.markers.marker.size": { - "$type": "Number", - "default": 1, - "bounds": (0, 20), - "inclusive_bounds": (True, True), - "softbounds": (1, 5), - "doc": "Markers marker size.", - }, - "display.style.markers.marker.color": { - "$type": "Color", - "default": "#808080", - "doc": "Markers marker color.", - }, - "display.style.markers.marker.symbol": { - "$type": "Selector", - "default": "o", - "objects": ALLOWED_SYMBOLS, - "doc": f"Markers marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", - }, -} - - -def convert_to_param(dict_, parent=None): - parent = "" if not parent else parent[0].upper() + parent[1:] - params = {} - for key, val in dict_.items(): - if not isinstance(val, dict): - raise TypeError(f"{val} must be dict.") - typ = val.get("$type", None) - if typ: - params[key] = getattr(param, typ)( - **{k: v for k, v in val.items() if k != "$type"} - ) - else: - name = parent + key[0].upper() + key[1:] - val = convert_to_param(val, parent=name) - params[key] = param.ClassSelector(class_=val, default=val()) - class_ = type(parent, (param.Parameterized,), params) - return class_ diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 9e973360e..0ee3e7e3c 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -15,9 +15,10 @@ import magpylib as magpy from magpylib._src.defaults.defaults_classes import default_settings -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS +from magpylib._src.defaults.defaults_classes import MarkersStyle from magpylib._src.defaults.defaults_utility import linearize_dict +from magpylib._src.defaults.defaults_values import ALLOWED_LINESTYLES +from magpylib._src.defaults.defaults_values import ALLOWED_SYMBOLS from magpylib._src.display.traces_utility import draw_arrowed_line from magpylib._src.display.traces_utility import get_flatten_objects_properties from magpylib._src.display.traces_utility import get_legend_label @@ -28,7 +29,6 @@ from magpylib._src.display.traces_utility import group_traces from magpylib._src.display.traces_utility import place_and_orient_model3d from magpylib._src.display.traces_utility import slice_mesh_from_colorscale -from magpylib._src.style import MarkersStyleSpecific from magpylib._src.utility import format_obj_input @@ -36,7 +36,7 @@ class MagpyMarkers: """A class that stores markers 3D-coordinates.""" def __init__(self, *markers): - self._style = MarkersStyleSpecific() + self._style = MarkersStyle() self.markers = np.array(markers) @property diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index e981f664b..78e65abff 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -9,8 +9,8 @@ from scipy.spatial.transform import Rotation as RotScipy from magpylib._src.defaults.defaults_classes import default_settings +from magpylib._src.defaults.defaults_utility import get_style from magpylib._src.defaults.defaults_utility import linearize_dict -from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index 06ab08183..98baa2ff5 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -9,7 +9,7 @@ from magpylib import _src from magpylib._src.defaults.defaults_classes import default_settings -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.utility import format_obj_input diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index 20ce7f91e..500928421 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -4,6 +4,8 @@ import numpy as np +from magpylib._src.defaults.defaults_classes import CurrentStyle +from magpylib._src.defaults.defaults_classes import MagnetStyle from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_scalar @@ -11,8 +13,6 @@ from magpylib._src.input_checks import validate_field_func from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr from magpylib._src.obj_classes.class_BaseGeo import BaseGeo -from magpylib._src.style import CurrentStyle -from magpylib._src.style import MagnetStyle from magpylib._src.utility import format_star_input diff --git a/magpylib/_src/obj_classes/class_BaseGeo.py b/magpylib/_src/obj_classes/class_BaseGeo.py index 59e7d1690..a8fd827ad 100644 --- a/magpylib/_src/obj_classes/class_BaseGeo.py +++ b/magpylib/_src/obj_classes/class_BaseGeo.py @@ -5,11 +5,11 @@ import numpy as np from scipy.spatial.transform import Rotation as R +from magpylib._src.defaults.defaults_classes import BaseStyle from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.input_checks import check_format_input_orientation from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseTransform import BaseTransform -from magpylib._src.style import BaseStyle from magpylib._src.utility import add_iteration_suffix diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index 2a4a70c3a..93edfa791 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -1,13 +1,13 @@ """Sensor class code""" import numpy as np +from magpylib._src.defaults.defaults_classes import SensorStyle from magpylib._src.display.traces_core import make_Sensor from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr from magpylib._src.obj_classes.class_BaseGeo import BaseGeo -from magpylib._src.style import SensorStyle from magpylib._src.utility import format_star_input diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 5e987b5f5..7c6553968 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -4,6 +4,7 @@ import numpy as np from scipy.spatial import ConvexHull # pylint: disable=no-name-in-module +from magpylib._src.defaults.defaults_classes import TriangularmeshStyle from magpylib._src.display.traces_core import make_TriangularMesh from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.fields.field_BH_triangularmesh import calculate_centroid @@ -19,7 +20,6 @@ from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet from magpylib._src.obj_classes.class_Collection import Collection from magpylib._src.obj_classes.class_misc_Triangle import Triangle -from magpylib._src.style import TriangularMeshStyle # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods @@ -117,7 +117,7 @@ class TriangularMesh(BaseMagnet): _field_func = staticmethod(magnet_trimesh_field) _field_func_kwargs_ndim = {"polarization": 2, "mesh": 3} get_trace = make_TriangularMesh - _style_class = TriangularMeshStyle + _style_class = TriangularmeshStyle def __init__( self, diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index ae18537df..c53778618 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -1,11 +1,11 @@ """Dipole class code""" import numpy as np +from magpylib._src.defaults.defaults_classes import DipoleStyle from magpylib._src.display.traces_core import make_Dipole from magpylib._src.fields.field_BH_dipole import dipole_field from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseSource -from magpylib._src.style import DipoleStyle from magpylib._src.utility import unit_prefix diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index 8f14f974c..a8ca480f4 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -2,11 +2,11 @@ """ import numpy as np +from magpylib._src.defaults.defaults_classes import TriangleStyle from magpylib._src.display.traces_core import make_Triangle from magpylib._src.fields.field_BH_triangle import triangle_field from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet -from magpylib._src.style import TriangleStyle class Triangle(BaseMagnet): diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py deleted file mode 100644 index 8f1e0af7c..000000000 --- a/magpylib/_src/style.py +++ /dev/null @@ -1,1000 +0,0 @@ -"""Collection of classes for display styling.""" -# pylint: disable=C0302 -# pylint: disable=too-many-instance-attributes -# pylint: disable=cyclic-import -import numpy as np -import param - -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS -from magpylib._src.defaults.defaults_utility import color_validator -from magpylib._src.defaults.defaults_utility import get_defaults_dict -from magpylib._src.defaults.defaults_utility import MagicParameterized -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.defaults.defaults_utility import validate_style_keys - -ALLOWED_SIZEMODES = ("scaled", "absolute") - -# pylint: disable=missing-class-docstring - - -def get_families(obj): - """get obj families""" - # pylint: disable=import-outside-toplevel - # pylint: disable=possibly-unused-variable - # pylint: disable=redefined-outer-name - from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet as Magnet - from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid - from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder - from magpylib._src.obj_classes.class_magnet_Sphere import Sphere - from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment - from magpylib._src.obj_classes.class_magnet_Tetrahedron import Tetrahedron - from magpylib._src.obj_classes.class_magnet_TriangularMesh import TriangularMesh - from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent as Current - from magpylib._src.obj_classes.class_current_Circle import Circle - from magpylib._src.obj_classes.class_current_Polyline import Polyline - from magpylib._src.obj_classes.class_misc_Dipole import Dipole - from magpylib._src.obj_classes.class_misc_CustomSource import CustomSource - from magpylib._src.obj_classes.class_misc_Triangle import Triangle - from magpylib._src.obj_classes.class_Sensor import Sensor - from magpylib._src.display.traces_generic import MagpyMarkers as Markers - - loc = locals() - obj_families = [] - for item, val in loc.items(): - if not item.startswith("_"): - try: - if isinstance(obj, val): - obj_families.append(item.lower()) - except TypeError: - pass - return obj_families - - -def get_style(obj, default_settings, **kwargs): - """Returns default style based on increasing priority: - - style from defaults - - style from object - - style from kwargs arguments - """ - obj_families = get_families(obj) - # parse kwargs into style an non-style arguments - style_kwargs = kwargs.get("style", {}) - style_kwargs.update( - {k[6:]: v for k, v in kwargs.items() if k.startswith("style") and k != "style"} - ) - - # retrieve default style dictionary, local import to avoid circular import - # pylint: disable=import-outside-toplevel - - default_style = default_settings.display.style - base_style_flat = default_style.base.as_dict(flatten=True, separator="_") - - # construct object specific dictionary base on style family and default style - for obj_family in obj_families: - family_style = getattr(default_style, obj_family, {}) - if family_style: - family_dict = family_style.as_dict(flatten=True, separator="_") - base_style_flat.update( - {k: v for k, v in family_dict.items() if v is not None} - ) - style_kwargs = validate_style_keys(style_kwargs) - - # create style class instance and update based on precedence - style = obj.style.copy() - style_kwargs_specific = { - k: v for k, v in style_kwargs.items() if k.split("_")[0] in style.as_dict() - } - style.update(**style_kwargs_specific, _match_properties=True) - style.update(**base_style_flat, _match_properties=False, _replace_None_only=True) - - return style - - -class Description(MagicParameterized): - show = param.Boolean( - default=True, - doc="if `True`, adds legend entry suffix based on value", - ) - text = param.String(doc="Object description text") - - -class Legend(MagicParameterized): - show = param.Boolean( - default=True, - doc="if `True`, overrides complete legend text", - ) - text = param.String(doc="Object legend text") - - -class Marker(MagicParameterized): - """Defines the styling properties of plot markers""" - - color = param.Color( - default=None, - allow_None=True, - doc=""" - The marker color. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, True), - softbounds=(1, 5), - doc="""Marker size""", - ) - - symbol = param.Selector( - objects=list(ALLOWED_SYMBOLS), - doc=f"""Marker symbol. Can be one of: {ALLOWED_SYMBOLS}""", - ) - - -class Line(MagicParameterized): - color = param.Color( - default=None, - allow_None=True, - doc="""A valid css color""", - ) - - width = param.Number( - default=1, - bounds=(0, 20), - inclusive_bounds=(True, True), - softbounds=(1, 5), - doc="""Line width""", - ) - - style = param.Selector( - default=ALLOWED_LINESTYLES[0], - objects=ALLOWED_LINESTYLES, - doc=f"""Line style. Can be one of: {ALLOWED_LINESTYLES}""", - ) - - -class Arrow(Line): - show = param.Boolean( - default=True, - doc="Show/hide Arrow", - ) - - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(False, True), - softbounds=(1, 5), - doc="""Arrow size""", - ) - - sizemode = param.Selector( - default=ALLOWED_SIZEMODES[0], - objects=ALLOWED_SIZEMODES, - doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", - ) - - offset = param.Magnitude( - bounds=(0, 1), - inclusive_bounds=(True, True), - doc="""Defines the arrow offset. `offset=0` results in the arrow head to be coincident with - the start of the line, and `offset=1` with the end.""", - ) - - -class Frames(MagicParameterized): - indices = param.List( - default=[], - item_type=int, - doc="""Array_like shape (n,) of integers: describes certain path indices.""", - ) - - step = param.Integer( - default=1, - bounds=(1, None), - softbounds=(0, 10), - doc="""Displays the object(s) at every i'th path position""", - ) - - mode = param.Selector( - default="indices", - objects=["indices", "step"], - doc=""" - The object path frames mode. - - step: integer i: displays the object(s) at every i'th path position. - - indices: array_like shape (n,) of integers: describes certain path indices.""", - ) - - @param.depends("indices", watch=True) - def _update_indices(self): - self.mode = "indices" - - @param.depends("step", watch=True) - def _update_step(self): - self.mode = "step" - - -class Path(MagicParameterized): - def __setattr__(self, name, value): - if name == "frames": - if isinstance(value, (tuple, list, np.ndarray)): - self.frames.indices = [int(v) for v in value] - elif ( - isinstance(value, (int, np.integer)) - and value is not False - and value is not True - ): - self.frames.step = value - else: - super().__setattr__(name, value) - return - super().__setattr__(name, value) - - show = param.Boolean( - default=True, - doc=""" - Show/hide path - - False: shows object(s) at final path position and hides paths lines and markers. - - True: shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters.""", - ) - - marker = param.ClassSelector( - class_=Marker, - default=Marker(), - doc=""" - Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent - key/value pairs""", - ) - - line = param.ClassSelector( - class_=Line, - default=Line(), - doc=""" - Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent - key/value pairs""", - ) - - numbering = param.Boolean( - doc="""Show/hide numbering on path positions. Only applies if show=True.""", - ) - - frames = param.ClassSelector( - class_=Frames, - default=Frames(), - doc=""" - Show copies of the 3D-model along the given path indices. - - mode: either `step` or `indices`. - - step: integer i: displays the object(s) at every i'th path position. - - indices: array_like shape (n,) of integers: describes certain path indices.""", - ) - - -class Trace3d(MagicParameterized): - def __setattr__(self, name, value): - validation_func = getattr(self, f"_validate_{name}", None) - if validation_func is not None: - value = validation_func(value) - return super().__setattr__(name, value) - - backend = param.Selector( - default="generic", - objects=list(SUPPORTED_PLOTTING_BACKENDS) + ["generic"], - doc=f""" - Plotting backend corresponding to the trace. Can be one of - {list(SUPPORTED_PLOTTING_BACKENDS) + ['generic']}""", - ) - - constructor = param.String( - doc=""" - Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend.""" - ) - - args = param.Parameter( - default=(), - doc=""" - Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object.""", - ) - - kwargs = param.Parameter( - default={}, - doc=""" - Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object.""", - ) - - coordsargs = param.Dict( - default=None, - doc=""" - Tells Magpylib the name of the coordinate arrays to be moved or rotated. - by default: - - plotly backend: `{"x": "x", "y": "y", "z": "z"}` - - matplotlib backend: `{"x": "args[0]", "y": "args[1]", "z": "args[2]}"`""", - ) - - show = param.Boolean( - default=True, - doc="""Show/hide model3d object based on provided trace.""", - ) - - scale = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, False), - softbounds=(0.1, 5), - doc=""" - Scaling factor by which the trace vertices coordinates should be multiplied by. - Be aware that if the object is not centered at the global CS origin, its position will also - be affected by scaling.""", - ) - - updatefunc = param.Callable( - doc=""" - Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed.""" - ) - - def _validate_coordsargs(self, value): - assert isinstance(value, dict) and all(key in value for key in "xyz"), ( - f"the `coordsargs` property of {type(self).__name__} must be " - f"a dictionary with `'x', 'y', 'z'` keys" - f" but received {repr(value)} instead" - ) - return value - - def _validate_updatefunc(self, val): - """Validate updatefunc.""" - if val is None: - - def val(): - return {} - - msg = "" - valid_keys = self.param.values().keys() - if not callable(val): - msg = f"Instead received {type(val)}" - else: - test_val = val() - if not isinstance(test_val, dict): - msg = f"but callable returned type {type(test_val)}." - else: - bad_keys = [k for k in test_val.keys() if k not in valid_keys] - if bad_keys: - msg = f"but invalid output dictionary keys received: {bad_keys}." - - assert msg == "", ( - f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " - f"dictionary with a subset of following keys: {list(valid_keys)}.\n" - f"{msg}" - ) - return val - - -class Model3d(MagicParameterized): - def __setattr__(self, name, value): - if name == "data": - value = self._validate_data(value) - return super().__setattr__(name, value) - - showdefault = param.Boolean( - default=True, - doc="""Show/hide default 3D-model.""", - ) - - data = param.List( - item_type=Trace3d, - doc=""" - A trace or list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """, - ) - - @staticmethod - def _validate_trace(trace, **kwargs): - updatefunc = None - if trace is None: - trace = Trace3d() - if not isinstance(trace, Trace3d) and callable(trace): - updatefunc = trace - trace = Trace3d() - if isinstance(trace, dict): - trace = Trace3d(**trace) - if isinstance(trace, Trace3d): - trace.updatefunc = updatefunc - if kwargs: - trace.update(**kwargs) - trace.update(trace.updatefunc()) - return trace - - def _validate_data(self, traces): - if traces is None: - traces = [] - elif not isinstance(traces, (list, tuple)): - traces = [traces] - new_traces = [] - for trace in traces: - new_traces.append(self._validate_trace(trace)) - return new_traces - - def add_trace(self, trace=None, **kwargs): - """Adds user-defined 3d model object which is positioned relatively to the main object to be - displayed and moved automatically with it. This feature also allows the user to replace the - original 3d representation of the object. - - trace: Trace3d instance, dict or callable - Trace object. Can be a `Trace3d` instance or an dictionary with equivalent key/values - pairs, or a callable returning the equivalent dictionary. - - backend: str - Plotting backend corresponding to the trace. Can be one of `['matplotlib', 'plotly']`. - - constructor: str - Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - - args: tuple, default=None - Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object. - - kwargs: dict or callable, default=None - Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object. - - coordsargs: dict, default=None - Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - - show: bool, default=None - Show/hide model3d object based on provided trace. - - scale: float, default=1 - Scaling factor by which the trace vertices coordinates are multiplied. - - updatefunc: callable, default=None - Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed. - """ - self.data = list(self.data) + [self._validate_trace(trace, **kwargs)] - return self - - -class BaseStyle(MagicParameterized): - label = param.String(doc="Label of the class instance, can be any string.") - - description = param.ClassSelector( - class_=Description, - default=Description(), - doc="Object description properties such as `text` and `show`.", - ) - - legend = param.ClassSelector( - class_=Legend, - default=Legend(), - doc="Object description properties such as `text` and `show`.", - ) - - color = param.Color( - default=None, - allow_None=True, - doc="A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.", - ) - - opacity = param.Number( - default=1, - bounds=(0, 1), - doc="Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", - ) - - path = param.ClassSelector( - class_=Path, - default=Path(), - doc=""" - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object path - marker and path line properties.""", - ) - - model3d = param.ClassSelector( - class_=Model3d, - default=Model3d(), - doc=""" - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """, - ) - - -class MagnetizationColor(MagicParameterized): - _allowed_modes = ("tricolor", "bicolor", "tricycle") - - north = param.Color( - default="red", - doc=""" - The color of the magnetic north pole. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - south = param.Color( - default="green", - doc=""" - The color of the magnetic south pole. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - middle = param.Color( - default="grey", - doc=""" - The color between the magnetic poles. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - transition = param.Number( - default=0.2, - bounds=(0, 1), - inclusive_bounds=(True, True), - doc=""" - Sets the transition smoothness between poles colors. Must be between 0 and 1. - - `transition=0`: discrete transition - - `transition=1`: smoothest transition - """, - ) - - mode = param.Selector( - default=_allowed_modes[0], - objects=_allowed_modes, - doc=""" - Sets the coloring mode for the magnetization. - - `'bicolor'`: only north and south poles are shown, middle color is hidden. - - `'tricolor'`: both pole colors and middle color are shown. - - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling - through the color sequence.""", - ) - - -class Magnetization(MagicParameterized): - _allowed_modes = ("auto", "arrow", "color", "arrow+color", "color+arrow") - - show = param.Boolean( - default=True, - doc="""Show/hide magnetization based on active plotting backend""", - ) - - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, True), - softbounds=(1, 5), - doc=""" - Arrow size of the magnetization direction (for the matplotlib backend only), only applies if - `show=True`""", - ) - - color = param.ClassSelector( - class_=MagnetizationColor, - default=MagnetizationColor(), - doc=""" - Color properties showing the magnetization direction, only applies if `show=True` - and `mode` contains 'color'""", - ) - - arrow = param.ClassSelector( - class_=Arrow, - default=Arrow(), - doc=""" - Arrow properties showing the magnetization direction, only applies if `show=True` - and `mode` contains 'arrow'""", - ) - - mode = param.Selector( - default=_allowed_modes[0], - objects=_allowed_modes, - doc=""" - One of {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`.""", - ) - - -class MagnetSpecific(MagicParameterized): - magnetization = param.ClassSelector( - class_=Magnetization, - default=Magnetization(), - doc=""" - Magnetization styling with `'show'`, `'size'`, `'color'` properties or a dictionary with - equivalent key/value pairs""", - ) - - -class ArrowCoordSysSingle(MagicParameterized): - show = param.Boolean( - default=True, - doc="""Show/hide single CS arrow""", - ) - - color = param.Color( - default=None, - allow_None=True, - doc=""" - The color of a single CS arrow. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - -class ArrowCoordSys(MagicParameterized): - x = param.ClassSelector( - class_=ArrowCoordSysSingle, - default=ArrowCoordSysSingle(), - doc=""" - `Arrowsingle` class or dict with equivalent key/value pairs for x-direction - (e.g. `color`, `show`)""", - ) - - y = param.ClassSelector( - class_=ArrowCoordSysSingle, - default=ArrowCoordSysSingle(), - doc=""" - `Arrowsingle` class or dict with equivalent key/value pairs for y-direction - (e.g. `color`, `show`)""", - ) - - z = param.ClassSelector( - class_=ArrowCoordSysSingle, - default=ArrowCoordSysSingle(), - doc=""" - `Arrowsingle` class or dict with equivalent key/value pairs for z-direction - (e.g. `color`, `show`)""", - ) - - -class Pixel(MagicParameterized): - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, None), - softbounds=(0.5, 2), - doc=""" - The relative pixel size. - - matplotlib backend: pixel size is the marker size - - plotly backend: relative size to the distance of nearest neighboring pixel""", - ) - - sizemode = param.Selector( - default=ALLOWED_SIZEMODES[0], - objects=ALLOWED_SIZEMODES, - doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", - ) - - color = param.Color( - default=None, - allow_None=True, - doc=""" - The color of sensor pixel. Must be a valid css color or one of - `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""", - ) - - symbol = param.Selector( - default="o", - objects=list(ALLOWED_SYMBOLS), - doc=f""" - Marker symbol. Can be one of: - {ALLOWED_SYMBOLS}""", - ) - - -class SensorSpecific(MagicParameterized): - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, True), - softbounds=(1, 5), - doc="""Sensor size relative to the canvas size.""", - ) - sizemode = param.Selector( - default=ALLOWED_SIZEMODES[0], - objects=ALLOWED_SIZEMODES, - doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", - ) - arrows = param.ClassSelector( - class_=ArrowCoordSys, - default=ArrowCoordSys(), - doc="""`ArrowCS` class or dict with equivalent key/value pairs (e.g. `color`, `size`)""", - ) - - pixel = param.ClassSelector( - class_=Pixel, - default=Pixel(), - doc="""`Pixel` class or dict with equivalent key/value pairs (e.g. `color`, `size`)""", - ) - - -class CurrentLine(Line): - show = param.Boolean( - default=True, - doc="""Show/hide current direction arrow""", - ) - - -class CurrentSpecific(MagicParameterized): - arrow = param.ClassSelector( - class_=Arrow, - default=Arrow(), - doc=""" - `Arrow` class or dict with equivalent key/value pairs""", - ) - - line = param.ClassSelector( - class_=CurrentLine, - default=CurrentLine(), - doc=""" - Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent - key/value pairs""", - ) - - -class DipoleSpecific(MagicParameterized): - _allowed_pivots = ("tail", "middle", "tip") - - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, True), - softbounds=(0.5, 5), - doc="""The dipole arrow size relative to the canvas size""", - ) - - sizemode = param.Selector( - default=ALLOWED_SIZEMODES[0], - objects=ALLOWED_SIZEMODES, - doc=f"""The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`""", - ) - - pivot = param.Selector( - default="middle", - objects=_allowed_pivots, - doc="""The part of the arrow that is anchored to the X, Y grid. The arrow rotates about - this point. Can be one of `['tail', 'middle', 'tip']`""", - ) - - -class MarkerLineSpecific(MagicParameterized): - show = param.Boolean( - default=True, - doc=""" - Show/hide path - - False: shows object(s) at final path position and hides paths lines and markers. - - True: shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters.""", - ) - - marker = param.ClassSelector( - class_=Marker, - default=Marker(), - doc=""" - Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent - key/value pairs""", - ) - - line = param.ClassSelector( - class_=Line, - default=Line(), - doc=""" - Line class with `'color'``, 'width'`, `'style'` properties, or dictionary with equivalent - key/value pairs""", - ) - - -class GridMesh(MarkerLineSpecific): - ... - - -class OpenMesh(MarkerLineSpecific): - ... - - -class SelfIntersectingMesh(MarkerLineSpecific): - ... - - -class DisconnectedMesh(MarkerLineSpecific): - def __setattr__(self, name, value): - if name == "colorsequence": - value = [ - color_validator(v, allow_None=False, parent_name="Colorsequence") - for v in value - ] - return super().__setattr__(name, value) - - colorsequence = param.List( - doc=""" - An iterable of color values used to cycle trough for every disconnected part of - disconnected triangular mesh object. A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""", - ) - - -class Mesh(MagicParameterized): - grid = param.ClassSelector( - class_=GridMesh, - default=GridMesh(), - doc="""`GridMesh` properties.""", - ) - - open = param.ClassSelector( - class_=OpenMesh, - default=OpenMesh(), - doc="""`OpenMesh` properties.""", - ) - - disconnected = param.ClassSelector( - class_=DisconnectedMesh, - default=DisconnectedMesh(), - doc="""`DisconnectedMesh` properties.""", - ) - - selfintersecting = param.ClassSelector( - class_=SelfIntersectingMesh, - default=SelfIntersectingMesh(), - doc="""`SelfIntersectingMesh` properties.""", - ) - - -class Orientation(MagicParameterized): - _allowed_symbols = ("cone", "arrow3d") - - show = param.Boolean( - default=True, - doc="Show/hide orientation symbol.", - ) - - size = param.Number( - default=1, - bounds=(0, None), - inclusive_bounds=(True, True), - softbounds=(1, 5), - doc="""Size of the orientation symbol""", - ) - - color = param.Color( - default=None, - allow_None=True, - doc="""A valid css color""", - ) - - offset = param.Magnitude( - bounds=(0, 1), - inclusive_bounds=(True, True), - doc=""" - Defines the orientation symbol offset, normal to the triangle surface. `offset=0` results - in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the - base""", - ) - symbol = param.Selector( - objects=_allowed_symbols, - doc=f"""Orientation symbol for the triangular faces. Can be one of: {_allowed_symbols}""", - ) - - -class TriangleSpecific(MagnetSpecific): - orientation = param.ClassSelector( - class_=Orientation, - default=Orientation(), - doc="""`Orientation` properties.""", - ) - - -class TriangularMeshSpecific(TriangleSpecific): - mesh = param.ClassSelector( - class_=Mesh, - default=Mesh(), - doc="""`Mesh` properties.""", - ) - - -class MagnetStyle(BaseStyle, MagnetSpecific): - ... - - -class CurrentStyle(BaseStyle, CurrentSpecific): - ... - - -class DipoleStyle(BaseStyle, DipoleSpecific): - ... - - -class SensorStyle(BaseStyle, SensorSpecific): - ... - - -class TriangleStyle(BaseStyle, TriangleSpecific): - ... - - -class TriangularMeshStyle(BaseStyle, TriangularMeshSpecific): - ... - - -class MarkersStyleSpecific(MagicParameterized): - marker = param.ClassSelector( - class_=Marker, - default=Marker(), - doc=""" - Marker class with `'color'``, 'symbol'`, `'size'` properties, or dictionary with equivalent - key/value pairs""", - ) - - -class DisplayStyle(MagicParameterized): - """ - Base class containing styling properties for all object families. The properties of the - sub-classes get set to hard coded defaults at class instantiation. - """ - - def reset(self): - """Resets all nested properties to their hard coded default values""" - self.update(get_defaults_dict("display.style"), _match_properties=False) - return self - - base = param.ClassSelector( - class_=BaseStyle, - default=BaseStyle(), - doc="""Base properties common to all families""", - ) - - magnet = param.ClassSelector( - class_=MagnetSpecific, - default=MagnetSpecific(), - doc="""Magnet properties""", - ) - - current = param.ClassSelector( - class_=CurrentSpecific, - default=CurrentSpecific(), - doc="""Current properties""", - ) - - triangularmesh = param.ClassSelector( - class_=TriangularMeshSpecific, - default=TriangularMeshSpecific(), - doc="""Triangularmesh properties""", - ) - - triangle = param.ClassSelector( - class_=TriangleSpecific, - default=TriangleSpecific(), - doc="""Triangularmesh properties""", - ) - - dipole = param.ClassSelector( - class_=DipoleSpecific, - default=DipoleSpecific(), - doc="""Dipole properties""", - ) - - sensor = param.ClassSelector( - class_=SensorSpecific, - default=SensorSpecific(), - doc="""Sensor properties""", - ) - - markers = param.ClassSelector( - class_=MarkersStyleSpecific, - default=MarkersStyleSpecific(), - doc="""Markers properties""", - ) diff --git a/magpylib/graphics/__init__.py b/magpylib/graphics/__init__.py index b16b204b0..4055a84d2 100644 --- a/magpylib/graphics/__init__.py +++ b/magpylib/graphics/__init__.py @@ -6,4 +6,4 @@ __all__ = ["model3d", "style", "Trace3d"] from magpylib.graphics import model3d, style -from magpylib._src.style import Trace3d +from magpylib._src.defaults.defaults_classes import Trace3d diff --git a/magpylib/graphics/style/__init__.py b/magpylib/graphics/style/__init__.py index 5040c6507..17ff20d8c 100644 --- a/magpylib/graphics/style/__init__.py +++ b/magpylib/graphics/style/__init__.py @@ -9,7 +9,7 @@ "SensorStyle", ] -from magpylib._src.style import ( +from magpylib._src.defaults.defaults_classes import ( MagnetStyle, CurrentStyle, DipoleStyle, diff --git a/tests/test_defaults.py b/tests/test_defaults.py index ae5529a70..3b49ac1b5 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -2,10 +2,10 @@ import magpylib as magpy from magpylib._src.defaults.defaults_classes import DefaultSettings -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.style import DisplayStyle +from magpylib._src.defaults.defaults_classes import DisplayStyle +from magpylib._src.defaults.defaults_values import ALLOWED_LINESTYLES +from magpylib._src.defaults.defaults_values import ALLOWED_SYMBOLS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS bad_inputs = { From aba7cda22efcddc4f07631dda010f91040be68c1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 5 Feb 2024 22:05:04 +0100 Subject: [PATCH 224/240] fix defaults --- magpylib/_src/defaults/defaults_classes.py | 55 ++++++++++++++++++++-- magpylib/_src/defaults/defaults_values.py | 2 +- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 00a5fc6d8..2d4db8479 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -1,6 +1,7 @@ import param from magpylib._src.defaults.defaults_utility import color_validator +from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.defaults.defaults_utility import magic_to_dict from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_values import DEFAULTS @@ -216,6 +217,12 @@ def __set__(self, obj, val): super().__set__(obj, val) +class Color(param.Color): + def __set__(self, obj, val): + val = color_validator(val) + super().__set__(obj, val) + + class Model3dData(param.List): def __set__(self, obj, val): self._validate_value(val, self.allow_None) @@ -252,8 +259,9 @@ def convert_to_param(dict_, parent=None): typ_str = str(val["$type"]).capitalize() args = {k: v for k, v in val.items() if k != "$type"} if typ_str == "Color": - typ = param.Color + typ = Color elif typ_str == "List": + typ = param.List it_typ = str(args.get("item_type", None)).capitalize() if it_typ == "Color": args.pop("item_type", None) @@ -273,10 +281,51 @@ def convert_to_param(dict_, parent=None): return class_ -default_settings = convert_to_param( - magic_to_dict(DEFAULTS, separator="."), parent="Settings" +Display = convert_to_param( + magic_to_dict(DEFAULTS, separator=".")["display"], parent="display" ) + +class DefaultSettings(MagicParameterized): + """Library default settings. All default values get reset at class instantiation.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._declare_watchers() + with param.parameterized.batch_call_watchers(self): + self.reset() + + def reset(self): + """Resets all nested properties to their hard coded default values""" + self.update(get_defaults_dict(), _match_properties=False) + return self + + def _declare_watchers(self): + props = get_defaults_dict(flatten=True, separator=".").keys() + for prop in props: + attrib_chain = prop.split(".") + child = attrib_chain[-1] + parent = self # start with self to go through dot chain + for attrib in attrib_chain[:-1]: + parent = getattr(parent, attrib) + parent.param.watch(self._set_to_defaults, parameter_names=[child]) + + @staticmethod + def _set_to_defaults(event): + """Sets class defaults whenever magpylib defaults parameters instance are modifed.""" + event.obj.__class__.param[event.name].default = event.new + + display = param.ClassSelector( + class_=Display, + default=Display(), + doc=""" + `Display` defaults-class containing display settings. + `(e.g. 'backend', 'animation', 'colorsequence', ...)`""", + ) + + +default_settings = DefaultSettings() + default_style_classes = { k: v.__class__ for k, v in default_settings.display.style.param.values().items() diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 1461ac23b..f1ef3f0fd 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -805,7 +805,7 @@ "display.style.triangularmesh.mesh.disconnected.colorsequence": { "$type": "List", "item_type": "Color", - "default": ["FF0000", "#0000FF", "008000", "00FFFF", "FF00FF", "FFFF00"], + "default": ["#FF0000", "#0000FF", "#008000", "#00FFFF", "#FF00FF", "#FFFF00"], "doc": """ An iterable of color values used to cycle trough for every disconnected part to be displayed. A color may be specified by From 933e873faf63df90a3aeb8be2d9880bc5f67c191 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 6 Feb 2024 18:27:23 +0100 Subject: [PATCH 225/240] add special cases handling --- magpylib/_src/defaults/defaults_classes.py | 53 +++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 2d4db8479..b391d05b2 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -1,3 +1,4 @@ +import numpy as np import param from magpylib._src.defaults.defaults_utility import color_validator @@ -247,6 +248,52 @@ def _validate_trace(trace, **kwargs): return trace +class PathLogic: + def __setattr__(self, name, value): + if name == "frames": + if isinstance(value, (tuple, list, np.ndarray)): + self.frames.indices = [int(v) for v in value] + elif ( + isinstance(value, (int, np.integer)) + and value is not False + and value is not True + ): + self.frames.step = value + else: + super().__setattr__(name, value) + return + super().__setattr__(name, value) + + +class TextLogic: + def __setattr__(self, name, value): + try: + super().__setattr__(name, value) + except ValueError: + if isinstance(value, str) and not name.startswith("_"): + for key, typ in self.param.objects(instance=False).items(): + if isinstance(typ, param.ClassSelector): + child_objs = self.param[key].class_.param.objects( + instance=False + ) + if "text" in child_objs: + getattr(self, key).text = value + else: + raise + + +def get_frames_logic(): + @param.depends("indices", watch=True) + def _update_indices(self): + self.mode = "indices" + + @param.depends("step", watch=True) + def _update_step(self): + self.mode = "step" + + return locals() + + def convert_to_param(dict_, parent=None): """Convert nested defaults dict to nested MagicParameterized instances""" parent = "" if not parent else parent[0].upper() + parent[1:] @@ -277,7 +324,11 @@ def convert_to_param(dict_, parent=None): name = parent + key[0].upper() + key[1:] val = convert_to_param(val, parent=name) params[key] = param.ClassSelector(class_=val, default=val()) - class_ = type(parent, (MagicParameterized,), params) + if parent.endswith("PathFrames"): + params.update(get_frames_logic()) + class_ = type(parent, (MagicParameterized, TextLogic), params) + if parent.endswith("Path"): + class_ = type(parent, (class_, PathLogic), {}) return class_ From f6352231b0a49e866f796b1bafd3314ec2e12fe5 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 6 Feb 2024 18:32:36 +0100 Subject: [PATCH 226/240] add arrows show for sensor --- magpylib/_src/defaults/defaults_values.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index f1ef3f0fd..3e9cffb77 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -463,19 +463,33 @@ "display.style.sensor.arrows.x.color": { "$type": "Color", "default": "#ff0000", - "allow_None": True, "doc": "Sensor x-arrow color.", }, + "display.style.sensor.arrows.x.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor x-arrow.", + }, "display.style.sensor.arrows.y.color": { "$type": "Color", "default": "#008000", "doc": "Sensor y-arrow color.", }, + "display.style.sensor.arrows.y.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor y-arrow.", + }, "display.style.sensor.arrows.z.color": { "$type": "Color", "default": "#0000FF", "doc": "Sensor z-arrow color.", }, + "display.style.sensor.arrows.z.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor z-arrow.", + }, "display.style.dipole.size": { "$type": "Number", "default": 1, From 717ad41f6155741ae86b8bc556141dfee962f2dd Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 12:45:25 +0100 Subject: [PATCH 227/240] fix Model3d add_trace --- magpylib/_src/defaults/defaults_classes.py | 98 +++++++--------------- 1 file changed, 29 insertions(+), 69 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index b391d05b2..dd645f127 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -118,54 +118,24 @@ def val(): return val -class Model3d(MagicParameterized): - def __setattr__(self, name, value): - if name == "data": - value = self._validate_data(value) - return super().__setattr__(name, value) - - showdefault = param.Boolean( - default=True, - doc="""Show/hide default 3D-model.""", - ) - - data = param.List( - item_type=Trace3d, - doc=""" - A trace or list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """, - ) - - @staticmethod - def _validate_trace(trace, **kwargs): - updatefunc = None - if trace is None: - trace = Trace3d() - if not isinstance(trace, Trace3d) and callable(trace): - updatefunc = trace - trace = Trace3d() - if isinstance(trace, dict): - trace = Trace3d(**trace) - if isinstance(trace, Trace3d): - trace.updatefunc = updatefunc - if kwargs: - trace.update(**kwargs) - trace.update(trace.updatefunc()) - return trace - - def _validate_data(self, traces): - if traces is None: - traces = [] - elif not isinstance(traces, (list, tuple)): - traces = [traces] - new_traces = [] - for trace in traces: - new_traces.append(self._validate_trace(trace)) - return new_traces - +def validate_trace3d(trace, **kwargs): + updatefunc = None + if trace is None: + trace = Trace3d() + if not isinstance(trace, Trace3d) and callable(trace): + updatefunc = trace + trace = Trace3d() + if isinstance(trace, dict): + trace = Trace3d(**trace) + if isinstance(trace, Trace3d): + trace.updatefunc = updatefunc + if kwargs: + trace.update(**kwargs) + trace.update(trace.updatefunc()) + return trace + + +class Model3dLogic: def add_trace(self, trace=None, **kwargs): """Adds user-defined 3d model object which is positioned relatively to the main object to be displayed and moved automatically with it. This feature also allows the user to replace the @@ -207,7 +177,7 @@ def add_trace(self, trace=None, **kwargs): depending on class attributes, and postpone the trace construction to when the object is displayed. """ - self.data = list(self.data) + [self._validate_trace(trace, **kwargs)] + self.data = [*self.data, validate_trace3d(trace, **kwargs)] return self @@ -227,26 +197,9 @@ def __set__(self, obj, val): class Model3dData(param.List): def __set__(self, obj, val): self._validate_value(val, self.allow_None) - val = [self._validate_trace(v) for v in val] + val = [validate_trace3d(v) for v in val] super().__set__(obj, val) - @staticmethod - def _validate_trace(trace, **kwargs): - updatefunc = None - if trace is None: - trace = Trace3d() - if not isinstance(trace, Trace3d) and callable(trace): - updatefunc = trace - trace = Trace3d() - if isinstance(trace, dict): - trace = Trace3d(**trace) - if isinstance(trace, Trace3d): - trace.updatefunc = updatefunc - if kwargs: - trace.update(**kwargs) - trace.update(trace.updatefunc()) - return trace - class PathLogic: def __setattr__(self, name, value): @@ -325,10 +278,13 @@ def convert_to_param(dict_, parent=None): val = convert_to_param(val, parent=name) params[key] = param.ClassSelector(class_=val, default=val()) if parent.endswith("PathFrames"): + # param.depends does not work with adding a class to bases params.update(get_frames_logic()) class_ = type(parent, (MagicParameterized, TextLogic), params) if parent.endswith("Path"): class_ = type(parent, (class_, PathLogic), {}) + if parent.endswith("Model3d"): + class_ = type(parent, (class_, Model3dLogic), {}) return class_ @@ -384,5 +340,9 @@ def _set_to_defaults(event): } locals()["BaseStyle"] = base = default_style_classes.pop("base") for fam, klass in default_style_classes.items(): - klass_name = f"{fam.capitalize()}Style" - locals()[klass_name] = type(klass_name, (base, klass), {}) + fam = fam.capitalize() + bases = (base, klass) + if fam == "Triangularmesh": + bases += (default_style_classes["magnet"],) + klass_name = f"{fam}Style" + locals()[klass_name] = type(klass_name, bases, {}) From f774f3e80c1a5f01302b1a2846c265cd4dab570c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 12:45:55 +0100 Subject: [PATCH 228/240] fix tests --- magpylib/_src/defaults/defaults_utility.py | 10 +++++----- magpylib/_src/obj_classes/class_BaseGeo.py | 2 +- magpylib/_src/utility.py | 2 +- tests/test_obj_BaseGeo.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 84706c5f6..c91efbff0 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -506,14 +506,14 @@ def as_dict(self, flatten=False, separator="_"): the separator to be used when flattening the dictionary. Only applies if `flatten=True` """ - params = (v[0] for v in self.param.get_param_values() if v[0] != "name") dict_ = {} - for k in params: - val = getattr(self, k) + for key, val in self.param.values().items(): + if key == "name": + continue if hasattr(val, "as_dict"): - dict_[k] = val.as_dict() + dict_[key] = val.as_dict() else: - dict_[k] = val + dict_[key] = val if flatten: dict_ = linearize_dict(dict_, separator=separator) return dict_ diff --git a/magpylib/_src/obj_classes/class_BaseGeo.py b/magpylib/_src/obj_classes/class_BaseGeo.py index a8fd827ad..487d249d5 100644 --- a/magpylib/_src/obj_classes/class_BaseGeo.py +++ b/magpylib/_src/obj_classes/class_BaseGeo.py @@ -357,7 +357,7 @@ def copy(self, **kwargs): ): # pylint: disable=no-member label = self.style.label - if label is None: + if not label: label = f"{type(self).__name__}_01" else: label = add_iteration_suffix(label) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index f4eef2514..c4e072318 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -280,7 +280,7 @@ def add_iteration_suffix(name): m = re.search(r"\d+$", name) n = "00" endstr = None - midchar = "_" if name[-1] != "_" else "" + midchar = "" if name.endswith("_") else "_" if m is not None: midchar = "" n = m.group() diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index c48038cc6..fb574c96e 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -372,7 +372,7 @@ def test_copy(): # check if label suffix iterated correctly assert bg1c.style.label == "label2" - assert bg2c.style.label is None + assert bg2c.style.label == "" assert bg3c.style.label == "BaseGeo_01" # check if style is passed correctly From 8241aad9646efbe1fc9d186fd5bba2a984d15470 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 13:42:06 +0100 Subject: [PATCH 229/240] minifix: propagate separator kw in magic_to_dict --- magpylib/_src/defaults/defaults_utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 77e33dabf..f9017dd7c 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -154,7 +154,7 @@ def magic_to_dict(kwargs, separator="_") -> dict: new_kwargs[keys[0]] = val for k, v in new_kwargs.items(): if isinstance(v, dict): - new_kwargs[k] = magic_to_dict(v) + new_kwargs[k] = magic_to_dict(v, separator=separator) return new_kwargs From 22e700b5a73366a9fdb0d24a9ce9bd13cc62c81b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 13:47:43 +0100 Subject: [PATCH 230/240] update defaults --- magpylib/_src/defaults/defaults_values.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 3e9cffb77..ecbe03ffa 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -68,8 +68,8 @@ "display.animation.output": { "$type": "String", "default": "", - "doc": "Animation output type", - "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", # either `mp4` or `gif` or ending with `.mp4` or `.gif`" + "doc": "Animation output type (either `mp4` or `gif` or ending with `.mp4` or `.gif`)", + "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", }, "display.backend": { "$type": "Selector", @@ -155,7 +155,8 @@ "bounds": (0, 1), "inclusive_bounds": (True, True), "softbounds": (0, 1), - "doc": "Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.", + "doc": """ + Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.""", }, "display.style.base.path.line.width": { "$type": "Number", @@ -788,13 +789,13 @@ }, "display.style.triangularmesh.mesh.disconnected.line.style": { "$type": "Selector", - "default": "solid", + "default": "dashed", "objects": ALLOWED_LINESTYLES, "doc": f"Mesh disconnected line style. Can be one of: {ALLOWED_LINESTYLES}.", }, "display.style.triangularmesh.mesh.disconnected.line.color": { "$type": "Color", - "default": "#000000", + "default": "#FF00FF", "doc": "Explicit current line color. Takes object color by default.", }, "display.style.triangularmesh.mesh.disconnected.marker.size": { From 6ba9fee08c636a5e7bc2c43ea412843f96e2a308 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 17:10:18 +0100 Subject: [PATCH 231/240] fix Textlogic --- magpylib/_src/defaults/defaults_classes.py | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index dd645f127..5ca853ed6 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -223,16 +223,18 @@ def __setattr__(self, name, value): try: super().__setattr__(name, value) except ValueError: - if isinstance(value, str) and not name.startswith("_"): - for key, typ in self.param.objects(instance=False).items(): - if isinstance(typ, param.ClassSelector): - child_objs = self.param[key].class_.param.objects( - instance=False - ) - if "text" in child_objs: - getattr(self, key).text = value - else: - raise + # this allows to set a string value from parent class if there is a `text` child + # e.g. .description = "desc" -> .description.text = "desc" + if ( + isinstance(value, str) + and not name.startswith("_") + and isinstance(self.param[name], param.ClassSelector) + ): + child = getattr(self, name) + if "text" in child.param.values(): + child.text = value + return + raise def get_frames_logic(): @@ -338,10 +340,10 @@ def _set_to_defaults(event): for k, v in default_settings.display.style.param.values().items() if isinstance(v, param.Parameterized) } -locals()["BaseStyle"] = base = default_style_classes.pop("base") +BaseStyle = default_style_classes.pop("base") for fam, klass in default_style_classes.items(): fam = fam.capitalize() - bases = (base, klass) + bases = (BaseStyle, klass) if fam == "Triangularmesh": bases += (default_style_classes["magnet"],) klass_name = f"{fam}Style" From 385becfc39afa65a896e94891961c6b6f22bded4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 17:10:45 +0100 Subject: [PATCH 232/240] refactoring --- magpylib/_src/defaults/defaults_utility.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index c91efbff0..9c60bd0bf 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -375,8 +375,9 @@ def update_with_nested_dict(parameterized, nested_dict): with param.parameterized.batch_call_watchers(parameterized): for pname, value in nested_dict.items(): if isinstance(value, dict): - if isinstance(getattr(parameterized, pname), param.Parameterized): - update_with_nested_dict(getattr(parameterized, pname), value) + child = getattr(parameterized, pname) + if isinstance(child, param.Parameterized): + update_with_nested_dict(child, value) continue setattr(parameterized, pname, value) @@ -426,15 +427,14 @@ def __init__(self, arg=None, **kwargs): def __setattr__(self, name, value): if self.__isfrozen and not hasattr(self, name) and not name.startswith("_"): raise AttributeError( - f"{type(self).__name__} has no property '{name}'" + f"{type(self).__name__} has no parameter '{name}'" f"\n Available properties are: {list(self.as_dict().keys())}" ) p = getattr(self.param, name, None) - if p is not None: + if name in self.param: + p = self.param[name] # pylint: disable=unidiomatic-typecheck - if isinstance(p, param.Color): - value = color_validator(value) - elif isinstance(p, param.List) and isinstance(value, tuple): + if isinstance(p, param.List) and isinstance(value, tuple): value = list(value) elif isinstance(p, param.Tuple) and isinstance(value, list): value = tuple(value) From 58b2ed938bc7055b333817191a09c743c9c49618 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 17:10:59 +0100 Subject: [PATCH 233/240] fix default values --- magpylib/_src/defaults/defaults_values.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index ecbe03ffa..63c1d9a44 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -205,7 +205,7 @@ }, "display.style.base.path.frames.indices": { "$type": "List", - "default": [], + "default": [-1], "item_type": int, "doc": "Array_like shape (n,) of integers: describes certain path indices.", }, @@ -218,7 +218,7 @@ }, "display.style.base.path.frames.mode": { "$type": "Selector", - "default": "step", + "default": "indices", "objects": ("indices", "step"), "doc": """ The object path frames mode. @@ -635,9 +635,9 @@ "display.style.triangle.orientation.offset": { "$type": "Number", "default": 0.9, - "bounds": (0, 1), + "bounds": (-2, 2), "inclusive_bounds": (True, True), - "softbounds": (0.1, 0.9), + "softbounds": (-0.9, 0.9), "doc": """ Orientation symbol offset, normal to the triangle surface. `offset=0` results in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the @@ -672,9 +672,9 @@ "display.style.triangularmesh.orientation.offset": { "$type": "Number", "default": 0.9, - "bounds": (0, 1), + "bounds": (-2, 2), "inclusive_bounds": (True, True), - "softbounds": (0.1, 0.9), + "softbounds": (-0.9, 0.9), "doc": """ Orientation symbol offset, normal to the triangle surface. `offset=0` results in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the From 212ca04f926c9a4b824fe7b85874e06b049b98f6 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 7 Feb 2024 18:32:16 +0100 Subject: [PATCH 234/240] fix some tests --- magpylib/_src/defaults/defaults_classes.py | 21 ++++++----- magpylib/_src/defaults/defaults_values.py | 1 + magpylib/_src/display/display.py | 4 +-- magpylib/_src/display/traces_generic.py | 2 +- magpylib/_src/obj_classes/class_Collection.py | 2 +- tests/test_default_utils.py | 36 ++++++------------- tests/test_defaults.py | 28 ++++++--------- tests/test_display_matplotlib.py | 12 +++---- 8 files changed, 39 insertions(+), 67 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 5ca853ed6..ff80cf255 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -122,7 +122,7 @@ def validate_trace3d(trace, **kwargs): updatefunc = None if trace is None: trace = Trace3d() - if not isinstance(trace, Trace3d) and callable(trace): + if not isinstance(trace, Trace3d) and trace is not Trace3d and callable(trace): updatefunc = trace trace = Trace3d() if isinstance(trace, dict): @@ -136,6 +136,13 @@ def validate_trace3d(trace, **kwargs): class Model3dLogic: + def __setattr__(self, name, value): + if name == "data": + if not isinstance(value, (tuple, list)): + value = [value] + value = [validate_trace3d(v) for v in value] + super().__setattr__(name, value) + def add_trace(self, trace=None, **kwargs): """Adds user-defined 3d model object which is positioned relatively to the main object to be displayed and moved automatically with it. This feature also allows the user to replace the @@ -177,7 +184,7 @@ def add_trace(self, trace=None, **kwargs): depending on class attributes, and postpone the trace construction to when the object is displayed. """ - self.data = [*self.data, validate_trace3d(trace, **kwargs)] + self.data = [*self.data, trace] return self @@ -194,13 +201,6 @@ def __set__(self, obj, val): super().__set__(obj, val) -class Model3dData(param.List): - def __set__(self, obj, val): - self._validate_value(val, self.allow_None) - val = [validate_trace3d(v) for v in val] - super().__set__(obj, val) - - class PathLogic: def __setattr__(self, name, value): if name == "frames": @@ -269,8 +269,7 @@ def convert_to_param(dict_, parent=None): args.pop("item_type", None) typ = ColorSequence elif it_typ == "Trace3d": - args.pop("item_type", None) - typ = Model3dData + args.pop("item_type", Trace3d) else: typ = getattr(param, typ_str) if typ is not None: diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 63c1d9a44..188fc9bdb 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -68,6 +68,7 @@ "display.animation.output": { "$type": "String", "default": "", + "allow_None": True, "doc": "Animation output type (either `mp4` or `gif` or ending with `.mp4` or `.gif`)", "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", }, diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 1a6b37f72..d84043f64 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -366,8 +366,8 @@ def show( ... polarization=(1,1,1), ... style_path_show=False ... ) - >>> magpy.defaults.display.style.magnet.magnetization.size = 2 - >>> src1.style.magnetization.size = 1 + >>> magpy.defaults.display.style.magnet.magnetization.arrow.size = 2 + >>> src1.style.magnetization.arrow.size = 1 >>> magpy.show(src1, src2, style_color='r') # doctest: +SKIP >>> # graphic output diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 0ee3e7e3c..4b09fdb6d 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -508,7 +508,7 @@ def get_generic_traces( tr_generic["color"] = tr_generic.get("color", style.color) else: # pragma: no cover raise ValueError( - f"{ttype} is not supported, only 'scatter3d' and 'mesh3d' are" + f"{ttype!r} is not supported, only 'scatter3d' and 'mesh3d' are" ) tr_generic.update(linearize_dict(obj_extr_trace, separator="_")) traces_generic.append(tr_generic) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index f0a484f23..545ca0684 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -464,7 +464,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs >>> # We apply styles using underscore magic for magnetization vector size and a style >>> # dictionary for the color. >>> - >>> col.set_children_styles(magnetization_size=0.5) + >>> col.set_children_styles(magnetization_arrow_size=0.5) Collection(id=...) >>> col.set_children_styles({"color": "g"}) Collection(id=...) diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py index 4c801de85..9ac089e5c 100644 --- a/tests/test_default_utils.py +++ b/tests/test_default_utils.py @@ -1,5 +1,6 @@ from copy import deepcopy +import param import pytest from magpylib._src.defaults.defaults_utility import color_validator @@ -7,7 +8,7 @@ from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.defaults.defaults_utility import linearize_dict from magpylib._src.defaults.defaults_utility import magic_to_dict -from magpylib._src.defaults.defaults_utility import MagicProperties +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import update_nested_dict @@ -138,32 +139,18 @@ def test_bad_colors(color, allow_None, expected_exception): color_validator(color, allow_None=allow_None) -def test_MagicProperties(): - """test MagicProperties class""" +def test_MagicParameterized(): + """test MagicParameterized class""" - class BPsub1(MagicProperties): - "MagicProperties class" + class BPsub1(MagicParameterized): + "MagicParameterized class" - @property - def prop1(self): - """prop1""" - return self._prop1 + prop1 = param.Parameter() - @prop1.setter - def prop1(self, val): - self._prop1 = val + class BPsub2(MagicParameterized): + "MagicParameterized class" - class BPsub2(MagicProperties): - "MagicProperties class" - - @property - def prop2(self): - """prop2""" - return self._prop2 - - @prop2.setter - def prop2(self, val): - self._prop2 = val + prop2 = param.Parameter() bp1 = BPsub1(prop1=1) @@ -212,9 +199,6 @@ def prop2(self, val): with pytest.raises(AttributeError): BPsub1(a=0) # `a` is not a property in the class - # check repr - assert repr(MagicProperties()) == "MagicProperties()", "repr failed" - def test_get_defaults_dict(): """test get_defaults_dict""" diff --git a/tests/test_defaults.py b/tests/test_defaults.py index 3b49ac1b5..6aedfe667 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -2,7 +2,6 @@ import magpylib as magpy from magpylib._src.defaults.defaults_classes import DefaultSettings -from magpylib._src.defaults.defaults_classes import DisplayStyle from magpylib._src.defaults.defaults_values import ALLOWED_LINESTYLES from magpylib._src.defaults.defaults_values import ALLOWED_SYMBOLS from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS @@ -14,10 +13,10 @@ "display_animation_fps": (0,), # int>0 "display_animation_time": (0,), # int>0 "display_animation_maxframes": (0,), # int>0 - "display_animation_slider": ("notbool"), # bool + "display_animation_slider": ("notbool",), # bool "display_animation_output": ("filename.badext", "badext"), # bool "display_backend": ("plotty",), # str typo - "display_colorsequence": (["#2E91E5", "wrongcolor"], 123), # iterable of colors + "display_colorsequence": (("#2E91E5", "wrongcolor"), 123), # iterable of colors "display_style_base_path_line_width": (-1,), # float>=0 "display_style_base_path_line_style": ("wrongstyle",), "display_style_base_path_line_color": ("wrongcolor",), # color @@ -25,7 +24,7 @@ "display_style_base_path_marker_symbol": ("wrongsymbol",), "display_style_base_path_marker_color": ("wrongcolor",), # color "display_style_base_path_show": ("notbool", 1), # bool - "display_style_base_path_frames": (True, False, ["1"], "1"), # int or iterable + "display_style_base_path_frames": (True, False, ["a"], "b"), # int or iterable "display_style_base_path_numbering": ("notbool",), # bool "display_style_base_description_show": ("notbool",), # bool "display_style_base_description_text": ( @@ -78,14 +77,7 @@ def get_bad_test_data(): bad_test_data = [] for k, tup in bad_inputs.items(): for v in tup: - if "description_text" not in k: - if "color" in k and "transition" not in k and "mode" not in k: - # color attributes use a the color validator, which raises a ValueError - errortype = ValueError - else: - # all other parameters raise AssertionError - errortype = AssertionError - bad_test_data.append((k, v, pytest.raises(errortype))) + bad_test_data.append((k, v, pytest.raises(ValueError))) return bad_test_data @@ -113,8 +105,8 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_animation_output": ("filename.mp4", "gif"), # bool "display_backend": ["auto", *SUPPORTED_PLOTTING_BACKENDS], # str typo "display_colorsequence": ( - ("#2e91e5", "#0d2a63"), - ("blue", "red"), + ["#2e91e5", "#0d2a63"], + ["blue", "red"], ), # ]), # iterable of colors "display_style_base_path_line_width": (0, 1), # float>=0 "display_style_base_path_line_style": ALLOWED_LINESTYLES, @@ -123,7 +115,7 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_style_base_path_marker_symbol": ALLOWED_SYMBOLS, "display_style_base_path_marker_color": ("blue", "#2E91E5"), # color "display_style_base_path_show": (True, False), # bool - "display_style_base_path_frames": (-1, (1, 3)), # int or iterable + # "display_style_base_path_frames": (1, (2, 3)), # int or iterable "display_style_base_path_numbering": (True, False), # bool "display_style_base_description_show": (True, False), # bool "display_style_base_description_text": ("a string",), # string @@ -131,7 +123,7 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_style_base_model3d_showdefault": (True, False), "display_style_base_color": ("blue", "#2E91E5"), # color "display_style_magnet_magnetization_show": (True, False), - "display_style_magnet_magnetization_size": (0, 1), # float>=0 + "display_style_magnet_magnetization_arrow_size": (0, 1), # float>=0 "display_style_magnet_magnetization_color_north": ("blue", "#2E91E5"), "display_style_magnet_magnetization_color_middle": ("blue", "#2E91E5"), "display_style_magnet_magnetization_color_south": ("blue", "#2E91E5"), @@ -201,7 +193,7 @@ def test_defaults_good_inputs(key, value, expected): v0 = c for v in key.split("_"): v0 = getattr(v0, v) - assert v0 == expected, f"{key} should be {expected}, but received {v0} instead" + assert v0 == expected @pytest.mark.parametrize( @@ -226,7 +218,7 @@ def test_defaults_good_inputs(key, value, expected): ) def test_bad_style_classes(style_class): """testing properties which take classes as properties""" - c = DisplayStyle().reset() + c = DefaultSettings().display.style with pytest.raises(ValueError): c.update(**{style_class: "bad class"}) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 472eb3a28..251d95527 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -405,20 +405,16 @@ def test_matplotlib_model3d_extra_updatefunc(): obj.show(canvas=ax, return_fig=True) with pytest.raises(ValueError): - updatefunc = "not callable" - obj.style.model3d.add_trace(updatefunc) + obj.style.model3d.add_trace("not callable") with pytest.raises(AssertionError): - updatefunc = "not callable" - obj.style.model3d.add_trace(updatefunc=updatefunc) + obj.style.model3d.add_trace(updatefunc="not callable") with pytest.raises(AssertionError): - updatefunc = lambda: "bad output type" - obj.style.model3d.add_trace(updatefunc=updatefunc) + obj.style.model3d.add_trace(updatefunc=lambda: "bad output type") with pytest.raises(AssertionError): - updatefunc = lambda: {"bad_key": "some_value"} - obj.style.model3d.add_trace(updatefunc=updatefunc) + obj.style.model3d.add_trace(updatefunc=lambda: {"bad_key": "some_value"}) def test_empty_display(): From 9666e1c3dd8eca2c383ce37da8c7e9fa6d8f7fd3 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 8 Feb 2024 10:31:12 +0100 Subject: [PATCH 235/240] rework get_style --- magpylib/_src/defaults/defaults_classes.py | 2 +- magpylib/_src/defaults/defaults_utility.py | 27 ++++------------------ magpylib/_src/display/traces_utility.py | 3 +-- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index ff80cf255..a4af3ff8c 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -184,7 +184,7 @@ def add_trace(self, trace=None, **kwargs): depending on class attributes, and postpone the trace construction to when the object is displayed. """ - self.data = [*self.data, trace] + self.data = [*self.data, validate_trace3d(trace, **kwargs)] return self diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 9c60bd0bf..ceef9fa63 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -326,42 +326,23 @@ def get_families(obj): return obj_families -def get_style(obj, default_settings, **kwargs): +def get_style(obj, **kwargs): """Returns default style based on increasing priority: - style from defaults - style from object - style from kwargs arguments """ - obj_families = get_families(obj) # parse kwargs into style an non-style arguments style_kwargs = kwargs.get("style", {}) style_kwargs.update( {k[6:]: v for k, v in kwargs.items() if k.startswith("style") and k != "style"} ) - - # retrieve default style dictionary, local import to avoid circular import - # pylint: disable=import-outside-toplevel - - default_style = default_settings.display.style - base_style_flat = default_style.base.as_dict(flatten=True, separator="_") - - # construct object specific dictionary base on style family and default style - for obj_family in obj_families: - family_style = getattr(default_style, obj_family, {}) - if family_style: - family_dict = family_style.as_dict(flatten=True, separator="_") - base_style_flat.update( - {k: v for k, v in family_dict.items() if v is not None} - ) style_kwargs = validate_style_keys(style_kwargs) - # create style class instance and update based on precedence style = obj.style.copy() - style_kwargs_specific = { - k: v for k, v in style_kwargs.items() if k.split("_")[0] in style.as_dict() - } - style.update(**style_kwargs_specific, _match_properties=True) - style.update(**base_style_flat, _match_properties=False, _replace_None_only=True) + keys = style.as_dict().keys() + style_kwargs = {k: v for k, v in style_kwargs.items() if k.split("_")[0] in keys} + style.update(**style_kwargs, _match_properties=True) return style diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 78e65abff..7e1d7e590 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -8,7 +8,6 @@ import numpy as np from scipy.spatial.transform import Rotation as RotScipy -from magpylib._src.defaults.defaults_classes import default_settings from magpylib._src.defaults.defaults_utility import get_style from magpylib._src.defaults.defaults_utility import linearize_dict from magpylib._src.utility import format_obj_input @@ -273,7 +272,7 @@ def get_flatten_objects_properties_recursive( flat_objs = {} for subobj in obj_list_semi_flat: isCollection = getattr(subobj, "children", None) is not None - style = get_style(subobj, default_settings, **kwargs) + style = get_style(subobj, **kwargs) if style.label is None: style.label = str(type(subobj).__name__) if parent_legendgroup is not None: From c02198bfad99e247ee0df9345d5b7ee6a6bf0efd Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 8 Feb 2024 14:33:34 +0100 Subject: [PATCH 236/240] refactor + precedence on defaults settings setting --- magpylib/_src/defaults/defaults_classes.py | 2 +- magpylib/_src/defaults/defaults_utility.py | 38 +++++++++---------- magpylib/_src/display/traces_generic.py | 2 +- magpylib/_src/obj_classes/class_Collection.py | 2 +- .../class_magnet_TriangularMesh.py | 2 +- tests/test_default_utils.py | 34 ++++++++++++++--- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index a4af3ff8c..dc8a121ea 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -305,7 +305,7 @@ def __init__(self, *args, **kwargs): def reset(self): """Resets all nested properties to their hard coded default values""" - self.update(get_defaults_dict(), _match_properties=False) + self.update(get_defaults_dict(), match_properties=False) return self def _declare_watchers(self): diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index ceef9fa63..1fb9c7b1e 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -342,7 +342,7 @@ def get_style(obj, **kwargs): style = obj.style.copy() keys = style.as_dict().keys() style_kwargs = {k: v for k, v in style_kwargs.items() if k.split("_")[0] in keys} - style.update(**style_kwargs, _match_properties=True) + style.update(**style_kwargs, match_properties=True) return style @@ -420,55 +420,51 @@ def __setattr__(self, name, value): elif isinstance(p, param.Tuple) and isinstance(value, list): value = tuple(value) if type(p) == param.ClassSelector: - if isinstance(value, dict): - self.update({name: value}) - return - if value is None: - value = type(getattr(self, name))() + if value is None or isinstance(value, dict): + value = type(getattr(self, name))().update(value) super().__setattr__(name, value) def _freeze(self): self.__isfrozen = True - def update( - self, arg=None, _match_properties=True, _replace_None_only=False, **kwargs - ): + def update(self, arg=None, match_properties=True, **kwargs): """ Updates the class properties with provided arguments, supports magic underscore notation Parameters ---------- - - _match_properties: bool + arg : dict + Dictionary of properties to be updated + match_properties: bool If `True`, checks if provided properties over keyword arguments are matching the current object properties. An error is raised if a non-matching property is found. If `False`, the `update` method does not raise any error when an argument is not matching a property. - - _replace_None_only: - updates matching properties that are equal to `None` (not already been set) - + kwargs : + Keyword/value pair of properties to be updated Returns ------- - self + ParameterizedType + Updated parameterized object """ if arg is None: arg = {} elif isinstance(arg, MagicParameterized): arg = arg.as_dict() - if kwargs: - arg.update(kwargs) + else: + arg = arg.copy() + arg.update(kwargs) if arg: arg = magic_to_dict(arg) current_dict = get_current_values_from_dict( - self, arg, match_properties=_match_properties + self, arg, match_properties=match_properties ) new_dict = update_nested_dict( current_dict, arg, - same_keys_only=not _match_properties, - replace_None_only=_replace_None_only, + same_keys_only=not match_properties, + replace_None_only=False, ) update_with_nested_dict(self, new_dict) return self diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 4b09fdb6d..a1274c9cd 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -640,7 +640,7 @@ def process_animation_kwargs(obj_list, animation=False, **kwargs): # pylint: disable=no-member anim_def = default_settings.display.animation.copy() - anim_def.update({k[10:]: v for k, v in kwargs.items()}, _match_properties=False) + anim_def.update({k[10:]: v for k, v in kwargs.items()}, match_properties=False) animation_kwargs = {f"animation_{k}": v for k, v in anim_def.as_dict().items()} kwargs = {k: v for k, v in kwargs.items() if not k.startswith("animation")} return kwargs, animation, animation_kwargs diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 545ca0684..23d735489 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -496,7 +496,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs for k, v in style_kwargs.items() if k.split("_")[0] in child.style.as_dict() } - child.style.update(**style_kwargs_specific, _match_properties=True) + child.style.update(**style_kwargs_specific, match_properties=True) return self def _validate_getBH_inputs(self, *inputs): diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 7c6553968..ad0d0b09a 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -522,7 +522,7 @@ def to_TriangleCollection(self): coll.position = self.position coll.orientation = self.orientation # pylint: disable=no-member - coll.style.update(self.style.as_dict(), _match_properties=False) + coll.style.update(self.style.as_dict(), match_properties=False) return coll @classmethod diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py index 9ac089e5c..ea9d4c20d 100644 --- a/tests/test_default_utils.py +++ b/tests/test_default_utils.py @@ -3,6 +3,7 @@ import param import pytest +import magpylib as magpy from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import COLORS_SHORT_TO_LONG from magpylib._src.defaults.defaults_utility import get_defaults_dict @@ -174,16 +175,11 @@ class BPsub2(MagicParameterized): with pytest.raises(AttributeError): bp1.update(prop1_prop2=10, prop3=4) - assert bp1.update(prop1_prop2=10, prop3=4, _match_properties=False).as_dict() == { + assert bp1.update(prop1_prop2=10, prop3=4, match_properties=False).as_dict() == { "prop1": {"prop2": 10} }, "magic property setting failed, should ignore `'prop3'`" - assert bp1.update(prop1_prop2=20, _replace_None_only=True).as_dict() == { - "prop1": {"prop2": 10} - }, "magic property setting failed, `prop2` should be remained unchanged `10`" - # check copy method - bp3 = bp2.copy() assert bp3 is not bp2, "failed copying, should return a different id" assert ( @@ -205,3 +201,29 @@ def test_get_defaults_dict(): s0 = get_defaults_dict("display.style") s1 = get_defaults_dict()["display"]["style"] assert s0 == s1, "dicts don't match" + + +def test_settings_precedence(): + magpy.defaults.reset() + mag_col_default = magpy.defaults.display.style.magnet.magnetization.color + c1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + + # assigning a dict + c1.style.magnetization = {"color_north": "magenta", "color_south": "turquoise"} + assert c1.style.magnetization.color.north == "magenta" + assert c1.style.magnetization.color.south == "turquoise" + + # assigning a dict, should fall back to defaults for unspecified values + c1.style.magnetization = {"color_south": "turquoise"} + assert c1.style.magnetization.color.north == mag_col_default.north + assert c1.style.magnetization.color.south == "turquoise" + + # assigning None, all should fall back to defaults + c1.style.magnetization = None + assert c1.style.magnetization.color.north == mag_col_default.north + assert c1.style.magnetization.color.south == mag_col_default.south + + # updating, updates specified only, other parameters remain + c1.style.magnetization.update(color_north="magenta") + assert c1.style.magnetization.color.north == "magenta" + assert c1.style.magnetization.color.south == mag_col_default.south From bc8fa2e2e7476eba8e61d2a0138153018571acf1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 8 Feb 2024 17:09:33 +0100 Subject: [PATCH 237/240] fix trace3d check --- magpylib/_src/defaults/defaults_classes.py | 21 +++++++++++++++------ tests/test_display_matplotlib.py | 6 +++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index dc8a121ea..2b7a85b25 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -110,19 +110,22 @@ def val(): if bad_keys: msg = f"but invalid output dictionary keys received: {bad_keys}." - assert msg == "", ( - f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " - f"dictionary with a subset of following keys: {list(valid_keys)}.\n" - f"{msg}" - ) + if msg: + raise ValueError( + f"The `updatefunc` property of {type(self).__name__} must be a callable returning " + f"a dictionary with a subset of following keys: {list(valid_keys)}.\n" + f"{msg}" + ) return val def validate_trace3d(trace, **kwargs): + """Validate and if necessary transform into Trace3d""" updatefunc = None + trace_orig = trace if trace is None: trace = Trace3d() - if not isinstance(trace, Trace3d) and trace is not Trace3d and callable(trace): + if not isinstance(trace, Trace3d) and callable(trace): updatefunc = trace trace = Trace3d() if isinstance(trace, dict): @@ -132,6 +135,12 @@ def validate_trace3d(trace, **kwargs): if kwargs: trace.update(**kwargs) trace.update(trace.updatefunc()) + else: + raise ValueError( + "A `Model3d` data element must be a `Trace3d` object or a dict with equivalent" + " parameters, or a callable returning a dict with equivalent parameters." + f" Instead received: {trace_orig!r}" + ) return trace diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 251d95527..89c9281cb 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -407,13 +407,13 @@ def test_matplotlib_model3d_extra_updatefunc(): with pytest.raises(ValueError): obj.style.model3d.add_trace("not callable") - with pytest.raises(AssertionError): + with pytest.raises(ValueError): obj.style.model3d.add_trace(updatefunc="not callable") - with pytest.raises(AssertionError): + with pytest.raises(ValueError): obj.style.model3d.add_trace(updatefunc=lambda: "bad output type") - with pytest.raises(AssertionError): + with pytest.raises(ValueError): obj.style.model3d.add_trace(updatefunc=lambda: {"bad_key": "some_value"}) From 39c1dad9f049c423085c34548634c8b5fca1b882 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Thu, 8 Feb 2024 18:14:43 +0100 Subject: [PATCH 238/240] allow tests to run --- tests/test_field_cylinder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index 7eb1d379e..9c8ca493b 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -7,7 +7,8 @@ import magpylib as magpy from magpylib._src.fields.field_BH_cylinder import BHJM_magnet_cylinder from magpylib._src.fields.field_BH_cylinder_segment import BHJM_cylinder_segment -from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_core + +# from magpylib._src.fields.field_BH_cylinder_segment import magnet_cylinder_segment_core # pylint: disable="pointless-string-statement" # creating test data From e070fa4ef3becfaa19d74cce3e86d2913247662c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 9 Feb 2024 14:49:17 +0100 Subject: [PATCH 239/240] improve magicparameterized speed --- magpylib/_src/defaults/defaults_utility.py | 33 +++++++++++++--------- tests/test_default_utils.py | 14 ++++----- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index 1fb9c7b1e..8ca64b341 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -5,6 +5,7 @@ from copy import deepcopy from functools import lru_cache +import numpy as np import param from matplotlib.colors import CSS4_COLORS as mcolors @@ -409,20 +410,26 @@ def __setattr__(self, name, value): if self.__isfrozen and not hasattr(self, name) and not name.startswith("_"): raise AttributeError( f"{type(self).__name__} has no parameter '{name}'" - f"\n Available properties are: {list(self.as_dict().keys())}" + f"\n Available parameters are: {list(self.as_dict().keys())}" ) - p = getattr(self.param, name, None) - if name in self.param: - p = self.param[name] - # pylint: disable=unidiomatic-typecheck - if isinstance(p, param.List) and isinstance(value, tuple): - value = list(value) - elif isinstance(p, param.Tuple) and isinstance(value, list): - value = tuple(value) - if type(p) == param.ClassSelector: - if value is None or isinstance(value, dict): - value = type(getattr(self, name))().update(value) - super().__setattr__(name, value) + try: + super().__setattr__(name, value) + except ValueError: + if name in self.param: + p = self.param[name] + # pylint: disable=unidiomatic-typecheck + if type(p) == param.ClassSelector: + value = {} if value is None else value + if isinstance(value, dict): + value = type(getattr(self, name))(**value) + return + if isinstance(value, (list, tuple, np.ndarray)): + if isinstance(p, param.List): + super().__setattr__(name, list(value)) + elif isinstance(p, param.Tuple): + super().__setattr__(name, tuple(value)) + return + raise def _freeze(self): self.__isfrozen = True diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py index ea9d4c20d..4b12a1ab2 100644 --- a/tests/test_default_utils.py +++ b/tests/test_default_utils.py @@ -209,14 +209,14 @@ def test_settings_precedence(): c1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) # assigning a dict - c1.style.magnetization = {"color_north": "magenta", "color_south": "turquoise"} - assert c1.style.magnetization.color.north == "magenta" - assert c1.style.magnetization.color.south == "turquoise" + c1.style.magnetization = {"color_north": "#e71111", "color_south": "#00b050"} + assert c1.style.magnetization.color.north == "#e71111" + assert c1.style.magnetization.color.south == "#00b050" # assigning a dict, should fall back to defaults for unspecified values - c1.style.magnetization = {"color_south": "turquoise"} + c1.style.magnetization = {"color_south": "#00b050"} assert c1.style.magnetization.color.north == mag_col_default.north - assert c1.style.magnetization.color.south == "turquoise" + assert c1.style.magnetization.color.south == "#00b050" # assigning None, all should fall back to defaults c1.style.magnetization = None @@ -224,6 +224,6 @@ def test_settings_precedence(): assert c1.style.magnetization.color.south == mag_col_default.south # updating, updates specified only, other parameters remain - c1.style.magnetization.update(color_north="magenta") - assert c1.style.magnetization.color.north == "magenta" + c1.style.magnetization.update(color_north="#e71111") + assert c1.style.magnetization.color.north == "#e71111" assert c1.style.magnetization.color.south == mag_col_default.south From e33403f3ff5666a48bde8b31d585780294776a3b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 15 Apr 2024 12:20:27 +0200 Subject: [PATCH 240/240] fix basic tests --- magpylib/__init__.py | 2 +- .../_src/fields/field_BH_cylinder_segment.py | 8 -- magpylib/_src/fields/field_BH_tetrahedron.py | 1 - magpylib/_src/obj_classes/class_Collection.py | 92 +++++++++---------- magpylib/graphics/__init__.py | 2 +- magpylib/graphics/style/__init__.py | 8 +- tests/test_default_utils.py | 2 +- 7 files changed, 51 insertions(+), 64 deletions(-) diff --git a/magpylib/__init__.py b/magpylib/__init__.py index f67df2f4d..9eb5bd2a3 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -61,7 +61,7 @@ from magpylib import magnet from magpylib import misc from magpylib._src.defaults.defaults_classes import default_settings as defaults -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS from magpylib._src.display.display import show from magpylib._src.display.display import show_context from magpylib._src.fields import getB diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 783e88ac6..ea4a73983 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2445,11 +2445,3 @@ def BHJM_cylinder_segment( raise ValueError( # pragma: no cover "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" ) - - # return convert_HBMJ( - # output_field_type=field, - # polarization=polarization, - # input_field_type="H", - # field_values=H_all, - # mask_inside=mask_inside & mask_not_on_surf, - # ) diff --git a/magpylib/_src/fields/field_BH_tetrahedron.py b/magpylib/_src/fields/field_BH_tetrahedron.py index 56f2e5f3b..5b392ec43 100644 --- a/magpylib/_src/fields/field_BH_tetrahedron.py +++ b/magpylib/_src/fields/field_BH_tetrahedron.py @@ -8,7 +8,6 @@ from magpylib._src.fields.field_BH_triangle import BHJM_triangle from magpylib._src.input_checks import check_field_input -from magpylib._src.utility import convert_HBMJ def check_chirality(points: np.ndarray) -> np.ndarray: diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 60c8464bf..5c5fd6f74 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -713,54 +713,50 @@ def getM( ): """Compute M-field for given sources and observers. - SI units are used for all inputs and outputs. - - Parameters - ---------- - inputs: source or observer objects - Input can only be observers if the collection contains only sources. In this case the - collection behaves like a single source. - Input can only be sources if the collection contains sensors. In this case the - collection behaves like a list of all its sensors. - - squeeze: bool, default=`True` - If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. - only a single source) are eliminated. - - pixel_agg: str, default=`None` - Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, - which is applied to observer output values, e.g. mean of all sensor pixel outputs. - With this option, observers input with different (pixel) shapes is allowed. - - output: str, default='ndarray' - Output type, which must be one of `('ndarray', 'dataframe')`. By default a - `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` - object is returned (the Pandas library must be installed). - - Returns - ------- - M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame - M-field at each path position (index m) for each sensor (index k) and each sensor - <<<<<<< HEAD - pixel position (indeices n1,n2,...) in units of A/m. Sensor pixel positions are - ======= - pixel position (indices n1,n2,...) in units of A/m. Sensor pixel positions are - >>>>>>> ccf11c357ac4225696b398801c6818b740b62985 - equivalent to simple observer positions. Paths of objects that are shorter than - index m are considered as static beyond their end. - - in_out: {'auto', 'inside', 'outside'} - This parameter only applies for magnet bodies. It specifies the location of the - observers relative to the magnet body, affecting the calculation of the magnetic field. - The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for - each observer. - - 'inside': All observers are considered to be inside the cuboid; use this for - performance optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for - performance optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. + SI units are used for all inputs and outputs. + + Parameters + ---------- + inputs: source or observer objects + Input can only be observers if the collection contains only sources. In this case the + collection behaves like a single source. + Input can only be sources if the collection contains sensors. In this case the + collection behaves like a list of all its sensors. + + squeeze: bool, default=`True` + If `True`, the output is squeezed, i.e. all axes of length 1 in the output (e.g. + only a single source) are eliminated. + + pixel_agg: str, default=`None` + Reference to a compatible numpy aggregator function like `'min'` or `'mean'`, + which is applied to observer output values, e.g. mean of all sensor pixel outputs. + With this option, observers input with different (pixel) shapes is allowed. + + output: str, default='ndarray' + Output type, which must be one of `('ndarray', 'dataframe')`. By default a + `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` + object is returned (the Pandas library must be installed). + + Returns + ------- + M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + M-field at each path position (index m) for each sensor (index k) and each sensor + pixel position (indices n1,n2,...) in units of A/m. Sensor pixel positions are + equivalent to simple observer positions. Paths of objects that are shorter than + index m are considered as static beyond their end. + + in_out: {'auto', 'inside', 'outside'} + This parameter only applies for magnet bodies. It specifies the location of the + observers relative to the magnet body, affecting the calculation of the magnetic field. + The options are: + - 'auto': The location (inside or outside the cuboid) is determined automatically for + each observer. + - 'inside': All observers are considered to be inside the cuboid; use this for + performance optimization if applicable. + - 'outside': All observers are considered to be outside the cuboid; use this for + performance optimization if applicable. + Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer + locations is unknown. """ sources, sensors = self._validate_getBH_inputs(*inputs) diff --git a/magpylib/graphics/__init__.py b/magpylib/graphics/__init__.py index 9963f8f39..7c1f10e5d 100644 --- a/magpylib/graphics/__init__.py +++ b/magpylib/graphics/__init__.py @@ -5,6 +5,6 @@ __all__ = ["model3d", "style", "Trace3d"] -from magpylib._src.style import Trace3d +from magpylib._src.defaults.defaults_classes import Trace3d from magpylib.graphics import model3d from magpylib.graphics import style diff --git a/magpylib/graphics/style/__init__.py b/magpylib/graphics/style/__init__.py index 5febd7c8d..d6bf6efc0 100644 --- a/magpylib/graphics/style/__init__.py +++ b/magpylib/graphics/style/__init__.py @@ -9,7 +9,7 @@ "SensorStyle", ] -from magpylib._src.style import CurrentStyle -from magpylib._src.style import DipoleStyle -from magpylib._src.style import MagnetStyle -from magpylib._src.style import SensorStyle +from magpylib._src.defaults.defaults_classes import CurrentStyle +from magpylib._src.defaults.defaults_classes import DipoleStyle +from magpylib._src.defaults.defaults_classes import MagnetStyle +from magpylib._src.defaults.defaults_classes import SensorStyle diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py index ecdcf7c1d..5624e4e71 100644 --- a/tests/test_default_utils.py +++ b/tests/test_default_utils.py @@ -4,7 +4,7 @@ import pytest from magpylib._src.defaults.defaults_utility import COLORS_SHORT_TO_LONG -from magpylib._src.defaults.defaults_utility import MagicProperties +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.defaults.defaults_utility import linearize_dict