"""Collection class code"""
import copy
from magpylib._lib.utility import (format_obj_input, check_duplicates,
only_allowed_src_types)
from magpylib._lib.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
from magpylib._lib.obj_classes.class_BaseGetBH import BaseGetBH
# ON INTERFACE
[docs]class Collection(BaseDisplayRepr, BaseGetBH):
"""
Group multiple sources in one Collection for common manipulation.
Operations applied to a Collection are sequentially applied to all sources in the Collection.
Collections do not allow duplicate sources (will be eliminated automatically).
Collections have the following dunders defined: __add__, __sub__, __iter__, __getitem__,
__repr__
Parameters
----------
sources: source objects, Collections or arbitrary lists thereof
Ordered list of sources in the Collection.
Returns
-------
Collection object: Collection
Examples
--------
Create Collections for common manipulation. All sources added to a Collection
are stored in the ``sources`` attribute, which is an ordered set (list with
unique elements only)
>>> import magpylib as mag3
>>> sphere = mag3.magnet.Sphere((1,2,3),1)
>>> loop = mag3.current.Circular(1,1)
>>> dipole = mag3.misc.Dipole((1,2,3))
>>> col = mag3.Collection(sphere, loop, dipole)
>>> print(col.sources)
[Sphere(id=1879891544384), Circular(id=1879891543040), Dipole(id=1879892157152)]
Cycle directly through the Collection ``sources`` attribute
>>> for src in col:
>>> print(src)
Sphere(id=1879891544384)
Circular(id=1879891543040)
Dipole(id=1879892157152)
and directly access objects from the Collection
>>> print(col[1])
Circular(id=1879891543040)
Add and subtract sources to form a Collection and to remove sources from a Collection.
>>> col = sphere + loop
>>> print(col.sources)
[Sphere(id=1879891544384), Circular(id=1879891543040)]
>>> col - sphere
>>> print(col.sources)
[Circular(id=1879891543040)]
Manipulate all objects in a Collection directly using ``move`` and ``rotate`` methods
>>> import magpylib as mag3
>>> sphere = mag3.magnet.Sphere((1,2,3),1)
>>> loop = mag3.current.Circular(1,1)
>>> col = sphere + loop
>>> col.move((1,1,1))
>>> print(sphere.position)
[1. 1. 1.]
and compute the total magnetic field generated by the Collection.
>>> B = col.getB((1,2,3))
>>> print(B)
[-0.00372678 0.01820438 0.03423079]
"""
def __init__(self, *sources):
# inherit
BaseDisplayRepr.__init__(self)
self.sources = sources
self.object_type = 'Collection'
# sources properties --------------------------------------------
@property
def sources(self):
""" Collection sources attribute getter and setter.
"""
return self._sources
@sources.setter
def sources(self, sources):
""" Set Collection sources.
"""
# format input
src_list = format_obj_input(sources)
# check and eliminate duplicates
src_list = check_duplicates(src_list)
# allow only designated source types in Collection
src_list = only_allowed_src_types(src_list)
# set attributes
self._sources = src_list
# dunders -------------------------------------------------------
def __add__(self, source):
return Collection(*self.sources, source)
def __sub__(self, source):
self.remove(source)
return self
def __iter__(self):
yield from self._sources
def __getitem__(self,i):
return self._sources[i]
# methods -------------------------------------------------------
[docs] def add(self,*sources):
"""
Add arbitrary sources or Collections.
Parameters
----------
sources: src objects, Collections or arbitrary lists thereof
Add arbitrary sequences of sources and Collections to the Collection.
The new sources will be added at the end of self.sources. Duplicates
will be eliminated.
Returns
-------
self: Collection
Examples
--------
Add sources to a Collection:
>>> import magpylib as mag3
>>> src = mag3.current.Circular(1,1)
>>> col = mag3.Collection()
>>> col.add(src)
>>> print(col.sources)
[Circular(id=2519738714432)]
"""
# format input
src_list = format_obj_input(sources)
# combine with original src_list
src_list = self._sources + src_list
# check and eliminate duplicates
src_list = check_duplicates(src_list)
# set attributes
self._sources = src_list
return self
[docs] def remove(self,source):
"""
Remove a specific source from the Collection.
Parameters
----------
source: source object
Remove the given source from the Collection.
Returns
-------
self: Collection
Examples
--------
Remove a specific source from a Collection:
>>> import magpylib as mag3
>>> src1 = mag3.current.Circular(1,1)
>>> src2 = mag3.current.Circular(1,1)
>>> col = src1 + src2
>>> print(col.sources)
[Circular(id=2405009623360), Circular(id=2405010235504)]
>>> col.remove(src1)
>>> print(col.sources)
[Circular(id=2405010235504)]
"""
self._sources.remove(source)
return self
[docs] def move(self, displacement, start=-1, increment=False):
"""
Translates each object in the Collection individually by the input displacement
(can be a path).
This method uses vector addition to merge the input path given by displacement and the
existing old path of an object. It keeps the old orientation. If the input path extends
beyond the old path, the old path will be padded by its last entry before paths are
added up.
Parameters
----------
displacement: array_like, shape (3,) or (N,3)
Displacement vector shape=(3,) or path shape=(N,3) in units of [mm].
start: int or str, default=-1
Choose at which index of the original object path, the input path will begin.
If `start=-1`, inp_path will start at the last old_path position.
If `start=0`, inp_path will start with the beginning of the old_path.
If `start=len(old_path)` or `start='append'`, inp_path will be attached to
the old_path.
increment: bool, default=False
If `increment=False`, input displacements are absolute.
If `increment=True`, input displacements are interpreted as increments of each other.
For example, an incremental input displacement of `[(2,0,0), (2,0,0), (2,0,0)]`
corresponds to an absolute input displacement of `[(2,0,0), (4,0,0), (6,0,0)]`.
Returns
-------
self: Collection
Examples
--------
This method will apply the ``move`` operation to each Collection object individually.
>>> import magpylib as mag3
>>> dipole = mag3.misc.Dipole((1,2,3))
>>> loop = mag3.current.Circular(1,1)
>>> col = loop + dipole
>>> col.move([(1,1,1), (2,2,2)])
>>> for src in col:
>>> print(src.position)
[[1. 1. 1.] [2. 2. 2.]]
[[1. 1. 1.] [2. 2. 2.]]
But manipulating individual objects keeps them in the Collection
>>> dipole.move((1,1,1))
>>> for src in col:
>>> print(src.position)
[[1. 1. 1.] [2. 2. 2.]]
[[1. 1. 1.] [3. 3. 3.]]
"""
for s in self:
s.move(displacement, start, increment)
return self
[docs] def rotate(self, rot, anchor=None, start=-1, increment=False):
"""
Rotates each object in the Collection individually.
This method applies given rotations to the original orientation. If the input path
extends beyond the existing path, the old path will be padded by its last entry before paths
are added up.
Parameters
----------
rotation: scipy Rotation object
Rotation to be applied. The rotation object can feature a single rotation
of shape (3,) or a set of rotations of shape (N,3) that correspond to a path.
anchor: None, 0 or array_like, shape (3,), default=None
The axis of rotation passes through the anchor point given in units of [mm].
By default (`anchor=None`) the object will rotate about its own center.
`anchor=0` rotates the object about the origin (0,0,0).
start: int or str, default=-1
Choose at which index of the original object path, the input path will begin.
If `start=-1`, inp_path will start at the last old_path position.
If `start=0`, inp_path will start with the beginning of the old_path.
If `start=len(old_path)` or `start='append'`, inp_path will be attached to
the old_path.
increment: bool, default=False
If `increment=False`, input rotations are absolute.
If `increment=True`, input rotations are interpreted as increments of each other.
Returns
-------
self: Collection
Examples
--------
This method will apply the ``rotate`` operation to each Collection object individually.
>>> from scipy.spatial.transform import Rotation as R
>>> import magpylib as mag3
>>> dipole = mag3.misc.Dipole((1,2,3))
>>> loop = mag3.current.Circular(1,1)
>>> col = loop + dipole
>>> col.rotate(R.from_euler('x', [45,90], degrees=True))
>>> for src in col:
>>> print(src.orientation.as_euler('xyz', degrees=True))
[[45. 0. 0.] [90. 0. 0.]]
[[45. 0. 0.] [90. 0. 0.]]
Manipulating individual objects keeps them in the Collection
>>> dipole.rotate(R.from_euler('x', [45], degrees=True))
>>> for src in col:
>>> print(src.orientation.as_euler('xyz', degrees=True))
[[45. 0. 0.] [ 90. 0. 0.]]
[[45. 0. 0.] [135. 0. 0.]]
"""
for s in self:
s.rotate(rot, anchor, start, increment)
return self
[docs] def rotate_from_angax(self, angle, axis, anchor=None, start=-1, increment=False, degrees=True):
"""
Rotates each object in the Collection individually from angle-axis input.
This method applies given rotations to the original orientation. If the input path
extends beyond the existingp path, the oldpath will be padded by its last entry before paths
are added up.
Parameters
----------
angle: int/float or array_like with shape (n,) unit [deg] (by default)
Angle of rotation, or a vector of n angles defining a rotation path in units
of [deg] (by default).
axis: str or array_like, shape (3,)
The direction of the axis of rotation. Input can be a vector of shape (3,)
or a string 'x', 'y' or 'z' to denote respective directions.
anchor: None or array_like, shape (3,), default=None, unit [mm]
The axis of rotation passes through the anchor point given in units of [mm].
By default (`anchor=None`) the object will rotate about its own center.
`anchor=0` rotates the object about the origin (0,0,0).
start: int or str, default=-1
Choose at which index of the original object path, the input path will begin.
If `start=-1`, inp_path will start at the last old_path position.
If `start=0`, inp_path will start with the beginning of the old_path.
If `start=len(old_path)` or `start='append'`, inp_path will be attached to
the old_path.
increment: bool, default=False
If `increment=False`, input rotations are absolute.
If `increment=True`, input rotations are interpreted as increments of each other.
For example, the incremental angles [1,1,1,2,2] correspond to the absolute angles
[1,2,3,5,7].
degrees: bool, default=True
By default angle is given in units of [deg]. If degrees=False, angle is given
in units of [rad].
Returns
-------
self: Collection
Examples
--------
This method will apply the ``rotate_from_angax`` operation to each Collection object
individually.
>>> import magpylib as mag3
>>> dipole = mag3.misc.Dipole((1,2,3))
>>> loop = mag3.current.Circular(1,1)
>>> col = loop + dipole
>>> col.rotate_from_angax([45,90], 'x')
>>> for src in col:
>>> print(src.orientation.as_euler('xyz', degrees=True))
[[45. 0. 0.] [90. 0. 0.]]
[[45. 0. 0.] [90. 0. 0.]]
Manipulating individual objects keeps them in the Collection
>>> dipole.rotate_from_angax(45, 'x')
>>> for src in col:
>>> print(src.orientation.as_euler('xyz', degrees=True))
[[45. 0. 0.] [ 90. 0. 0.]]
[[45. 0. 0.] [135. 0. 0.]]
"""
for s in self:
s.rotate_from_angax(angle, axis, anchor, start, increment, degrees)
return self
[docs] def copy(self):
"""
Returns a copy of the Collection.
Returns
-------
self: Collection
Examples
--------
Create a copy of a Collection object:
>>> import magpylib as mag3
>>> col = mag3.Collection()
>>> print(id(col))
2221754911040
>>> col2 = col.copy()
>>> print(id(col2))
2221760504160
"""
return copy.copy(self)
[docs] def reset_path(self):
"""
Reset all object paths to position = (0,0,0) and orientation = unit rotation.
Returns
-------
self: Collection
Examples
--------
Create a collection with non-zero paths
>>> import magpylib as mag3
>>> dipole = mag3.misc.Dipole((1,2,3), position=(1,2,3))
>>> loop = mag3.current.Circular(1,1, position=[(1,1,1)]*2)
>>> col = loop + dipole
>>> for src in col:
>>> print(src.position)
[[1. 1. 1.] [1. 1. 1.]]
[1. 2. 3.]
>>> col.reset_path()
>>> for src in col:
>>> print(src.position)
[0. 0. 0.]
[0. 0. 0.]
"""
for obj in self:
obj.reset_path()
return self