There are several convenience methods that can be used to analyse the field. Let us first define the mesh we are going to work with.
import numpy as np
import discretisedfield as df
p1 = (-50, -50, -50)
p2 = (50, 50, 50)
n = (2, 2, 2)
mesh = df.Mesh(p1=p1, p2=p2, n=n)
We are going to initialise the vector field (dim=3
), with
For that, we are going to use the following Python function.
def value_function(pos):
x, y, z = pos
return x * y, 2 * x * y, x * y * z
Finally, our field is
field = df.Field(mesh, dim=3, value=value_function)
As we have shown previously, a field can be sampled by calling it. The argument must be a 3-length iterable and it contains the coordinates of the point.
point = (0, 0, 0)
field(point)
(625.0, 1250.0, -15625.0)
However if the point is outside the mesh, an exception is raised.
point = (100, 100, 100)
try:
field(point)
except ValueError:
print("Exception raised.")
Exception raised.
A three-dimensional vector field can be understood as three separate scalar fields, where each scalar field is a component of a vector field value. A scalar field of a component can be extracted by accessing x
, y
, or z
attribute of the field.
x_component = field.x
x_component((0, 0, 0))
625.0
field.y
Default names x
, y
, and (for dim 3) z
are only available for fields with dimensionality 2 or 3.
field.components
['x', 'y', 'z']
It is possible to change the component names:
field.components = ["mx", "my", "mz"]
field.mx((0, 0, 0))
625.0
This overrides the component labels and the old x
, y
and z
cannot be used anymore:
try:
field.x
except AttributeError as e:
print(e)
Object has no attribute x.
We change the component labels back to x
, y
, and z
for the rest of this notebook.
field.components = ["x", "y", "z"]
Custom component names can optionally also be specified during field creation. If not specified, the default values are used for fields with dimensions 2 or 3. Higher-dimensional fields have no defaults and custom labes have to be specified in order to access individual field components:
field_4d = df.Field(
mesh, dim=4, value=[1, 1, 1, 1], components=["c1", "c2", "c3", "c4"]
)
field_4d
field_4d.c1((0, 0, 0))
1.0
Let us say we are not interested in the entire field but only in its smaller portion - only some discretisation cells. In that case, we have two options. Before we discuss them, let us first define what we mean by "aligned" meshes:
There is |
operator which checks that. Let us have a look at a few meshes:
mesh1 = df.Mesh(region=df.Region(p1=(0, 0, 0), p2=(10, 10, 10)), cell=(1, 1, 1))
mesh2 = df.Mesh(region=df.Region(p1=(3, 3, 3), p2=(6, 6, 6)), cell=(1, 1, 1))
mesh3 = df.Mesh(region=df.Region(p1=(0, 0, 0), p2=(10, 10, 10)), cell=(2, 2, 2))
mesh4 = df.Mesh(
region=df.Region(p1=(3.5, 3.5, 3.5), p2=(6.5, 6.5, 6.5)), cell=(1, 1, 1)
)
Let us now have a look if those meshes are aligned:
mesh1 | mesh2
True
mesh1 | mesh3 # discretisation cell is different
False
mesh1 | mesh4 # although discretisation cell is the same, mesh4 is shifted in space by 0.5
False
If we want to get a subfield whose mesh is aligned to the field we want to take part of, we use []
operator. The resulting field is going to have a minimum-sized mesh which contains the region we pass as an argument.
subregion = df.Region(p1=(1.5, 2.2, 3.9), p2=(6.1, 5.9, 9.9))
field[subregion]
We can see that the resulting field's mesh has the minimum dimesions aligned mesh should have in order to contain the subregion
. The resulting field has the same discretisation cell as the original one.
If we want to extact part of the field on any mesh which is contained inside the field, we do that by "resampling". We create a new field on a submesh and pass the field we want take subfield from as value
.
subregion = df.Region(p1=(1.5, 2.5, 3.5), p2=(5.5, 5.5, 6.5))
submesh = df.Mesh(region=subregion, cell=(0.5, 0.5, 0.5))
df.Field(submesh, dim=3, value=field)
One could ask why don't we always use resampling because it is a generalised case. The reason is because computing a subfield using []
operator is much faster.
The average of the field can be obtained by calling discretisedfield.Field.average
property.
field.average
(0.0, 0.0, 0.0)
Average always return a tuple, independent of the dimension of the field's value.
field.x.average
0.0
The field object itself is an iterable. That means that it can be iterated through. As a result it returns a tuple, where the first element is the coordinate of the mesh point, whereas the second one is its value.
for coordinate, value in field:
print(coordinate, value)
(-25.0, -25.0, -25.0) (625.0, 1250.0, -15625.0) (25.0, -25.0, -25.0) (-625.0, -1250.0, 15625.0) (-25.0, 25.0, -25.0) (-625.0, -1250.0, 15625.0) (25.0, 25.0, -25.0) (625.0, 1250.0, -15625.0) (-25.0, -25.0, 25.0) (625.0, 1250.0, 15625.0) (25.0, -25.0, 25.0) (-625.0, -1250.0, -15625.0) (-25.0, 25.0, 25.0) (-625.0, -1250.0, -15625.0) (25.0, 25.0, 25.0) (625.0, 1250.0, 15625.0)
To sample the points of the field which are on a certain line, discretisedfield.Field.line
method is used. It takes two points p1
and p2
that define the line and an integer n
which defines how many mesh coordinates on that line are required. The default value of n
is 100.
line = field.line(p1=(-10, 0, 0), p2=(10, 0, 0), n=5)
If we intersect the field with a plane, discretisedfield.Field.plane
will return a new field object which contains only discretisation cells that belong to that plane. The planes allowed are the planes perpendicular to the axes of the Cartesian coordinate system. For instance, a plane parallel to the $yz$-plane (perpendicular to the $x$-axis) which intesects the $x$-axis at 1, can be written as
field.plane(x=1)
If we want to cut through the middle of the mesh, we do not need to provide a particular value for a coordinate.
field.plane("x")
Let us say we want to compute the average of an $x$ component of the field on the plane $y=10$. In order to do that, we can cascade several operation in a single line.
field.plane(y=10).x.average
0.0
This gives the same result as for instance
field.x.plane(y=10).average
0.0
discretisedfield
supports complex-valued fields.
cfield = df.Field(mesh, dim=3, value=(1 + 1.5j, 2, 3j))
We can extract real
and imaginary
part.
cfield.real((0, 0, 0))
(1.0, 2.0, 0.0)
cfield.imag((0, 0, 0))
(1.5, 0.0, 3.0)
Similarly we get real
and imaginary
parts of individual components.
cfield.x.real((0, 0, 0))
1.0
cfield.x.imag((0, 0, 0))
1.5
Complex conjugate.
cfield.conjugate((0, 0, 0))
((1-1.5j), (2-0j), -3j)
Phase in the complex plane.
cfield.phase((0, 0, 0))
(0.982793723247329, 0.0, 1.5707963267948966)
Let us define two fields:
region = df.Region(p1=(0, 0, 0), p2=(10e-9, 10e-9, 10e-9))
mesh = df.Mesh(region=region, n=(10, 10, 10))
f1 = df.Field(mesh, dim=3, value=(1, 1, 0))
f2 = df.Field(mesh, dim=3, value=(2, 1, 3))
f1.average
(1.0, 1.0, 0.0)
f2.average
(2.0, 1.0, 3.0)
+
operation¶f1 + f2
(f1 + f2).average
(3.0, 2.0, 3.0)
-
operation¶f1 - f2
(f1 - f2).average
(-1.0, 0.0, -3.0)
*
operation¶Basic multiplication is not defined between vector fields. In that case, we perform either dot or cross product, which we are going to discuss later.
f1 * f2 # both are vector fields
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Input In [42], in <module> ----> 1 f1 * f2 File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1331, in Field.__mul__(self, other) 1328 if self.dim == 3 and other.dim == 3: 1329 msg = (f'Cannot apply operator * on {self.dim=} ' 1330 f'and {other.dim=} fields.') -> 1331 raise ValueError(msg) 1332 if self.mesh != other.mesh: 1333 msg = ('Cannot apply operator * on fields ' 1334 'defined on different meshes.') ValueError: Cannot apply operator * on self.dim=3 and other.dim=3 fields.
Scalar with vector field:
f1.x * f2
Scalar with vector field:
f1.x * f2.y
/
operation¶f1 / f2 # both are vector fields
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Input In [45], in <module> ----> 1 f1 / f2 File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1424, in Field.__truediv__(self, other) 1359 def __truediv__(self, other): 1360 """Binary ``/`` operator. 1361 1362 It can be applied between: (...) 1422 1423 """ -> 1424 return self * other**(-1) File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1102, in Field.__pow__(self, other) 1100 if self.dim != 1: 1101 msg = f'Cannot apply ** operator on {self.dim=} field.' -> 1102 raise ValueError(msg) 1103 if not isinstance(other, numbers.Real): 1104 msg = (f'Unsupported operand type(s) for **: ' 1105 f'{type(self)=} and {type(other)=}.') ValueError: Cannot apply ** operator on self.dim=3 field.
Dividing vector field by a scalar field:
f1 / f2.x
Scalar field divided by vector field is not allowed:
f2.x / f1
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Input In [47], in <module> ----> 1 f2.x / f1 File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1424, in Field.__truediv__(self, other) 1359 def __truediv__(self, other): 1360 """Binary ``/`` operator. 1361 1362 It can be applied between: (...) 1422 1423 """ -> 1424 return self * other**(-1) File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1102, in Field.__pow__(self, other) 1100 if self.dim != 1: 1101 msg = f'Cannot apply ** operator on {self.dim=} field.' -> 1102 raise ValueError(msg) 1103 if not isinstance(other, numbers.Real): 1104 msg = (f'Unsupported operand type(s) for **: ' 1105 f'{type(self)=} and {type(other)=}.') ValueError: Cannot apply ** operator on self.dim=3 field.
**
operator¶This operator is allowed only on scalar fields:
f1**2
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Input In [48], in <module> ----> 1 f1 ** 2 File ~/PhD/repos/ubermag-devtools/repos/discretisedfield/discretisedfield/field.py:1102, in Field.__pow__(self, other) 1100 if self.dim != 1: 1101 msg = f'Cannot apply ** operator on {self.dim=} field.' -> 1102 raise ValueError(msg) 1103 if not isinstance(other, numbers.Real): 1104 msg = (f'Unsupported operand type(s) for **: ' 1105 f'{type(self)=} and {type(other)=}.') ValueError: Cannot apply ** operator on self.dim=3 field.
f1.x**2
f1 += f2
f1 -= f2
f1 *= f2.x
f2 /= f2.y
f1 @ f2
Cross product between vector fields is performed using &
operator:
f1 & f2
f1.derivative("x")
Defined on scalar fields:
f1.x.grad
Defined on vector fields:
f1.div
Defined on vector fields:
f1.curl
Defined on both vector and scalar fields:
f1.laplace
f1.x.laplace
In the most recent version of discretisedfield
, computing integrals is generalised to accomodate the calculation of different types of integrals. Instead of giving the "theory" behind how it was implemented, we are going to show several examples which hopefully are going to give you an idea how you can compute different integrals.
Let us first create a field:
import discretisedfield as df
p1 = (0, 0, 0)
p2 = (100e-9, 100e-9, 100e-9)
cell = (2e-9, 2e-9, 2e-9)
region = df.Region(p1=p1, p2=p2)
mesh = df.Mesh(region=region, cell=cell)
f = df.Field(mesh, dim=3, value=(-3, 0, 4), norm=1e6)
df.integral(f * df.dV)
(-6.000000000017728e-16, 0.0, 8.00000000001152e-16)
Since $\text{d}V = \text{d}x\text{d}y\text{d}z$, we can compute the integral as:
df.integral(f * df.dx * df.dy * df.dz)
(-6.000000000017728e-16, 0.0, 8.00000000001152e-16)
df.integral(f.x * df.dV)
-6.000000000000003e-16
There is disretisedfield.dS
value which is a vector field perpendicular to the surface with magnitude equal to the area of $\text{d}S$.
Like all plane-related operations, the field must be sliced.
df.integral(f.plane("z") @ df.dS)
7.999999999999996e-09
Similarly, we can write $\text{d}S = \text{d}x\text{d}y$ when we cut $z$-plane or we can use $|\text{d}\mathbf{S}|$.
$$\int_{S}f_{x}(\mathbf{r}) \text{d}x\text{d}y$$df.integral(f.x.plane("z") * df.dx * df.dy)
-5.999999999999999e-09
df.integral(f.x.plane("z") * abs(df.dS))
-5.9999999999999975e-09
df.integral(f * df.dx, direction="x")
df.integral(f.x * df.dy, direction="y")
df.integral(f.x * df.dy, direction="y", improper=True)
We have showed how to compute an integral when integrand is just a field. It is important to have in mind that this can be any field after some operations have been applied on it. For instance:
$$\int_{V}\nabla\cdot\mathbf{f}(\mathbf{r})\text{d}V$$df.integral(f.div * df.dV)
0.0
df.integral(f1.x.grad.div.grad.curl.y.grad * df.dV)
(0.0, 0.0, 0.0)
Here we implement skyrmion number calculations using operations on fields:
$$S = \frac{1}{4\pi} \int \mathbf{m} \cdot \left(\frac{\partial \mathbf{m}}{\partial x} \times \frac{\partial \mathbf{m}}{\partial y}\right) dxdy$$import math
m = field.orientation.plane("z")
S = df.integral(m @ (m.derivative("x") & m.derivative("y")) * df.dx * df.dy) / (
4 * math.pi
)
S
0.0
Or using Ubermag function:
import discretisedfield.tools as dft
dft.topological_charge(m)
0.0
Using Berg-Luescher method
dft.topological_charge(m, method="berg-luescher")
0.0
numpys
universal functions¶All numpy universal functions can be applied to discretisedfield.Field
objects. Below we show a different examples. For available functions please refer to the numpy
documentation.
import numpy as np
f1 = df.Field(mesh, dim=1, value=1)
f2 = df.Field(mesh, dim=1, value=np.pi)
f3 = df.Field(mesh, dim=1, value=2)
np.sin(f1)
np.sin(f2)((0, 0, 0))
1.2246467991473532e-16
np.sum((f1, f2, f3))((0, 0, 0))
6.141592653589793
np.exp(f1)((0, 0, 0))
2.718281828459045
np.power(f3, 2)((0, 0, 0))
4.0
Full description of all existing functionality can be found in the API Reference.