"""Public field API, config constants and low-level helpers.
This module is a leaf of the ``fields`` package: it depends only on the
standard library and JAX so that every other ``fields`` submodule can import
its constants and helpers without creating an import cycle. The public
``getB``/``getH``/``getJ``/``getM`` entry points and the ``_compute_field``
router lazy-import the heavier engine/eager submodules inside their bodies.
"""
from __future__ import annotations
from math import prod
import jax
import jax.numpy as jnp
from magpylib_jax._types import ArrayLike
_ALIASES = {
"dipole": "dipole",
"circle": "circle",
"cuboid": "cuboid",
"box": "cuboid",
"cylinder": "cylinder",
"cylindersegment": "cylindersegment",
"cylinder_segment": "cylindersegment",
"sphere": "sphere",
"polyline": "polyline",
"triangle": "triangle",
"trianglesheet": "trianglesheet",
"triangle_sheet": "trianglesheet",
"trianglestrip": "trianglestrip",
"triangle_strip": "trianglestrip",
"triangularmesh": "triangularmesh",
"triangular_mesh": "triangularmesh",
"tetrahedron": "tetrahedron",
}
_SOURCE_TYPE_ORDER = (
"dipole",
"circle",
"cuboid",
"cylinder",
"cylindersegment",
"sphere",
"triangle",
"polyline",
"trianglesheet",
"trianglestrip",
"triangularmesh",
"tetrahedron",
)
_SOURCE_TYPE_IDS = {name: idx for idx, name in enumerate(_SOURCE_TYPE_ORDER)}
_SUPPORTED_PIXEL_AGGS = {"mean", "sum", "min", "max"}
def _check_getbh_output_type(output: str) -> str:
acceptable = ("ndarray", "dataframe")
if output not in acceptable:
msg = f"Input output must be one of {acceptable}; instead received {output!r}."
raise ValueError(msg)
if output == "dataframe":
try: # pragma: no cover - import check
import pandas as _ # noqa: F401
except Exception as err: # pragma: no cover
msg = (
"Input output='dataframe' requires Pandas installation, "
"see https://pandas.pydata.org/docs/getting_started/install.html"
)
raise ModuleNotFoundError(msg) from err
return output
def _check_pixel_agg(pixel_agg: str | None):
if pixel_agg is None:
return None
if callable(pixel_agg):
return pixel_agg
if not isinstance(pixel_agg, str):
raise AttributeError(
"Input pixel_agg must be a reference to a NumPy callable that reduces "
"an array shape like 'mean', 'std', 'median', 'min', ...; "
f"instead received {pixel_agg!r}."
)
if not hasattr(jnp, pixel_agg):
raise AttributeError(
"Input pixel_agg must be a reference to a NumPy callable that reduces "
"an array shape like 'mean', 'std', 'median', 'min', ...; "
f"instead received {pixel_agg!r}."
)
return getattr(jnp, pixel_agg)
def _is_array_like(obj: object) -> bool:
if isinstance(obj, (jnp.ndarray, jax.Array)):
return True
if isinstance(obj, jax.core.Tracer):
return True
return hasattr(obj, "shape") and hasattr(obj, "dtype")
def _has_tracer(obj: object) -> bool:
if isinstance(obj, jax.core.Tracer):
return True
if isinstance(obj, (list, tuple)):
return any(_has_tracer(item) for item in obj)
if isinstance(obj, dict):
return any(_has_tracer(val) for val in obj.values())
return False
def _pad_path(arr: ArrayLike, target_len: int) -> jnp.ndarray:
arr_jnp = jnp.asarray(arr, dtype=float)
if arr_jnp.shape[0] == target_len:
return arr_jnp
if arr_jnp.shape[0] == 1:
return jnp.broadcast_to(arr_jnp, (target_len,) + arr_jnp.shape[1:])
pad_len = target_len - arr_jnp.shape[0]
pad = jnp.broadcast_to(arr_jnp[-1:], (pad_len,) + arr_jnp.shape[1:])
return jnp.concatenate((arr_jnp, pad), axis=0)
def _pad_axis0(arr: ArrayLike, target_len: int, pad_value: float = 0.0) -> jnp.ndarray:
arr_jnp = jnp.asarray(arr, dtype=float)
if arr_jnp.shape[0] == target_len:
return arr_jnp
pad_shape = (target_len - arr_jnp.shape[0],) + arr_jnp.shape[1:]
pad = jnp.full(pad_shape, pad_value, dtype=arr_jnp.dtype)
return jnp.concatenate((arr_jnp, pad), axis=0)
def _normalize_source_type(source_type: str) -> str:
key = source_type.lower()
if key not in _ALIASES:
valid = sorted(_ALIASES)
raise ValueError(f"Unsupported source_type {source_type!r}. Expected one of {valid}.")
return _ALIASES[key]
def _apply_squeeze(
field: jnp.ndarray,
observer_input: jnp.ndarray,
*,
squeeze: bool,
sumup: bool,
n_sources: int,
) -> jnp.ndarray:
obs_prefix = observer_input.shape[:-1] if observer_input.ndim > 1 else ()
n_obs = prod(obs_prefix) if obs_prefix else 1
def _unflatten_obs(arr: jnp.ndarray) -> jnp.ndarray:
if arr.ndim >= 2 and arr.shape[-1] == 3 and arr.shape[-2] == n_obs:
return arr.reshape((*arr.shape[:-2], *obs_prefix, 3))
return arr
field = _unflatten_obs(field)
if squeeze:
axes = tuple(i for i, size in enumerate(field.shape[:-1]) if size == 1)
return jnp.squeeze(field, axis=axes) if axes else field
obs_has_path = observer_input.ndim >= 3
if sumup:
if field.ndim == len(obs_prefix) + 2:
path_len = field.shape[0]
pix = field.shape[1:-1]
elif obs_has_path:
path_len = field.shape[0]
pix = field.shape[1:-1]
else:
path_len = 1
pix = field.shape[:-1]
arr = field.reshape((1, path_len, 1, *pix, 3))
else:
if n_sources == 1 and (field.ndim == observer_input.ndim or field.shape[0] != 1):
field = field[None, ...]
rest = field.shape[1:-1]
if len(rest) > len(obs_prefix):
path_len = rest[0]
pix = rest[1:]
elif obs_has_path and len(rest) >= 1:
path_len = rest[0]
pix = rest[1:]
else:
path_len = 1
pix = rest
arr = field.reshape((n_sources, path_len, 1, *pix, 3))
return arr
def _compute_field(
source: str | object,
observers: object,
field: str,
*,
position: ArrayLike = (0.0, 0.0, 0.0),
orientation: ArrayLike | None = None,
squeeze: bool = True,
sumup: bool = False,
pixel_agg: str | None = None,
output: str = "ndarray",
in_out: str = "auto",
**kwargs: ArrayLike,
) -> jnp.ndarray:
from magpylib_jax.fields.eager import _compute_field_legacy
from magpylib_jax.fields.engine import _compute_field_jit
if isinstance(source, str):
src_type = _normalize_source_type(source)
if src_type == "triangularmesh":
mesh = kwargs.get("mesh")
if mesh is not None:
mesh_arr = jnp.asarray(mesh)
if mesh_arr.ndim == 4:
return _compute_field_legacy(
source,
observers,
field,
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
if pixel_agg is not None and not isinstance(pixel_agg, str):
return _compute_field_legacy(
source,
observers,
field,
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
if isinstance(pixel_agg, str) and pixel_agg not in _SUPPORTED_PIXEL_AGGS:
return _compute_field_legacy(
source,
observers,
field,
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
return _compute_field_jit(
source,
observers,
field,
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
[docs]
def getB(
source: str | object,
observers: ArrayLike | None = None,
*,
position: ArrayLike = (0.0, 0.0, 0.0),
orientation: ArrayLike | None = None,
squeeze: bool = True,
sumup: bool = False,
pixel_agg: str | None = None,
output: str = "ndarray",
in_out: str = "auto",
**kwargs: ArrayLike,
) -> jnp.ndarray:
"""Return B-field in Tesla from source type strings or source objects."""
return _compute_field(
source,
observers,
"B",
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
[docs]
def getH(
source: str | object,
observers: ArrayLike | None = None,
*,
position: ArrayLike = (0.0, 0.0, 0.0),
orientation: ArrayLike | None = None,
squeeze: bool = True,
sumup: bool = False,
pixel_agg: str | None = None,
output: str = "ndarray",
in_out: str = "auto",
**kwargs: ArrayLike,
) -> jnp.ndarray:
"""Return H-field in A/m from source type strings or source objects."""
return _compute_field(
source,
observers,
"H",
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
[docs]
def getJ(
source: str | object,
observers: ArrayLike | None = None,
*,
position: ArrayLike = (0.0, 0.0, 0.0),
orientation: ArrayLike | None = None,
squeeze: bool = True,
sumup: bool = False,
pixel_agg: str | None = None,
output: str = "ndarray",
in_out: str = "auto",
**kwargs: ArrayLike,
) -> jnp.ndarray:
"""Return J-field from source type strings or source objects."""
return _compute_field(
source,
observers,
"J",
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)
[docs]
def getM(
source: str | object,
observers: ArrayLike | None = None,
*,
position: ArrayLike = (0.0, 0.0, 0.0),
orientation: ArrayLike | None = None,
squeeze: bool = True,
sumup: bool = False,
pixel_agg: str | None = None,
output: str = "ndarray",
in_out: str = "auto",
**kwargs: ArrayLike,
) -> jnp.ndarray:
"""Return M-field from source type strings or source objects."""
return _compute_field(
source,
observers,
"M",
position=position,
orientation=orientation,
squeeze=squeeze,
sumup=sumup,
pixel_agg=pixel_agg,
output=output,
in_out=in_out,
**kwargs,
)