Metadata-Version: 2.4
Name: microcubed
Version: 1.0.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Dist: matplotlib>=3.11.1
Requires-Dist: numpy>=2.5.3
Requires-Dist: scipy>=1.18.1
Requires-Dist: discretisedfield>=0.92.0 ; extra == 'comparison'
Requires-Dist: micromagneticmodel>=0.65.0 ; extra == 'comparison'
Requires-Dist: oommfc>=0.66.0 ; extra == 'comparison'
Requires-Dist: pandas>=3.0.5 ; extra == 'comparison'
Requires-Dist: mypy>=2.3.1 ; extra == 'dev'
Requires-Dist: pre-commit>=4.6.2 ; extra == 'dev'
Requires-Dist: pytest>=9.1.1 ; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
Requires-Dist: ruff>=0.16.6 ; extra == 'dev'
Requires-Dist: furo>=2025.12.19 ; extra == 'docs'
Requires-Dist: myst-nb>=1.3.0 ; extra == 'docs'
Requires-Dist: ipykernel>=7.3.0 ; extra == 'docs'
Requires-Dist: myst-parser[linkify]>=5.1.0 ; extra == 'docs'
Requires-Dist: sphinx>=9.1.0 ; extra == 'docs'
Requires-Dist: ipykernel>=7.3.0 ; extra == 'examples'
Requires-Dist: nbconvert>=7.17.0 ; extra == 'examples'
Requires-Dist: nbformat>=5.10.4 ; extra == 'examples'
Provides-Extra: comparison
Provides-Extra: dev
Provides-Extra: docs
Provides-Extra: examples
License-File: LICENSE.txt
License-File: AUTHORS.md
Summary: Calculate 3D stray fields of cuboid micro magnet arrangements.
Keywords: magnetism,micromagnetics,magnetic-field,rust,numpy
Home-Page: https://github.com/newton-per-sqm/microcubed
Author-email: Pascal Muster <Pascal.Muster@infineon.com>
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://newton-per-sqm.github.io/microcubed/
Project-URL: Homepage, https://github.com/newton-per-sqm/microcubed
Project-URL: Issues, https://github.com/newton-per-sqm/microcubed/issues
Project-URL: Repository, https://github.com/newton-per-sqm/microcubed

[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-4b8bbe)](https://newton-per-sqm.github.io/microcubed/)
[![CI](https://github.com/newton-per-sqm/microcubed/actions/workflows/ci.yml/badge.svg)](https://github.com/newton-per-sqm/microcubed/actions/workflows/ci.yml)
[![Tested Python versions](https://img.shields.io/badge/tested%20Python-3.12%20%7C%203.13%20%7C%203.14-3776ab)](https://github.com/newton-per-sqm/microcubed/actions/workflows/ci.yml)
[![Tested platforms](https://img.shields.io/badge/tested%20platforms-Linux%20%7C%20macOS%20%7C%20Windows-3776ab)](https://github.com/newton-per-sqm/microcubed/actions/workflows/ci.yml)

# Microcubed

Calculate 3D magnetic stray fields and analytical gradients for uniformly
magnetized, axis-aligned cuboids and arrangements. Microcubed provides NumPy
and compiled, parallel Rust backends behind the same Python API, plus polygon
decomposition and Matplotlib plots.

The field equations follow Ravaud and Lemarquand,
[Magnetic Field Produced by a Parallelepipedic Magnet of Various and Uniform Polarization](https://doi.org/10.2528/PIER09091704).
Magnetization is an input; Microcubed does not solve magnetic equilibrium or dynamics.

## Installation

Python **3.12 or newer** is required. Install the published package with:

```bash
python -m pip install microcubed
```

PyPI wheels contain the compiled Rust backend for supported Linux, macOS, and
Windows platforms, so a Rust toolchain is not needed for normal installation.
When no matching wheel exists, pip falls back to the source distribution;
building that package requires Cargo, rustc, and a platform linker/C compiler.

To install from a checkout:

```bash
git clone https://github.com/newton-per-sqm/microcubed.git
cd microcubed
python -m pip install .
```

The Maturin build includes the Rust extension. At runtime `auto` selects Rust
when available and otherwise falls back to NumPy. Select a backend explicitly
with `microcubed.get_backend("numpy")` or `microcubed.get_backend("rust")`.

Use one consistent length unit for size, position, and observation points.
Magnetization is in A/m, `Bfield` returns T, and `dBfield` returns T per length
unit. Points are columns in a `(3, N)` array. Evaluate outside the magnets;
the analytical exterior-field model does not describe their internal field.

## Examples

These snippets are generated from the tagged cells in the Jupyter notebooks.
GitHub Actions builds the extension, executes every core notebook, checks numerical
assertions, and uploads executed notebooks and HTML under the `examples` artifact.

```bash
uv sync --locked --extra examples
uv run --locked --extra examples python tools/notebooks.py --check --execute
```

Open the `.ipynb` files in a Jupyter-compatible editor using `.venv` as the
Python environment. Generated HTML is written to `build/examples/`.

<!-- examples:start -->

### Superposition and field maps

Build a small array by translating one cuboid. The arrangement field is the sum of its members. Sample a plane below the magnets, safely outside all material.

[Open notebook](examples/arrangement.ipynb)

```python
import numpy as np

from microcubed import Arrangement, Magnet

cube = Magnet([80, 80, 40], [0, 0, 0], [0, 0, 8e5])
magnets = [cube.moved_to([x, 0, 0]) for x in (-150, 0, 150)]
array = Arrangement(magnets)
points = np.array([[0, 0, -100], [100, 25, -100]]).T
field = array.Bfield(points)
```

```python
import matplotlib.pyplot as plt

fig, ax = array.plot_2d(x=(-300, 300, 61), y=(-200, 200, 41), z=-100, component="z")
for index, boundary in enumerate(array.union_boundary("xy")):
    ax.plot(*boundary, color="#00ffff", linewidth=2, label="Material boundary" if index == 0 else None)
ax.set(xlabel="x (nm)", ylabel="y (nm)", title="Bz (T) at z = -100 nm", aspect="equal")
ax.legend(loc="upper right", fontsize=8, facecolor="#555555", labelcolor="white", framealpha=0.95)
fig.tight_layout()
plt.show()
```

### Compare NumPy and Rust

Choose backend namespaces explicitly without changing global state. This notebook requires the compiled Rust extension and checks both fields and gradients at exterior points.

[Open notebook](examples/backends.ipynb)

```python
import numpy as np

from microcubed import get_backend

points = np.array([[0, 0, -150], [80, 30, -120], [-90, 50, 160]]).T
results = {}
for name in ("numpy", "rust"):
    backend = get_backend(name)
    magnet = backend.Magnet([100, 80, 40], [0, 0, 0], [2e5, 1e5, 8e5])
    results[name] = (magnet.Bfield(points), magnet.dBfield(points))

for reference, compiled in zip(results["numpy"], results["rust"]):
    np.testing.assert_allclose(compiled, reference, rtol=1e-9, atol=1e-13)
print("Fields and gradients agree.")
```

### Resolving a polygon boundary

A concave polygon with slanted edges makes rasterization error visible. All lengths
are in nm. `delta` limits the raster-cell size, not the size of the final cuboids:
merging adjacent occupied cells into larger cuboids preserves the rasterized geometry
exactly. Refining `delta` improves the staircase approximation along oblique edges.
The previous axis-aligned L-shape could be represented exactly by two large cuboids;
a small cuboid count alone does not imply a coarse approximation.

[Open notebook](examples/shape.ipynb)

```python
import numpy as np

from microcubed import cuboidize

polygon = np.array([(0, 0), (120, 15), (95, 65), (55, 45), (35, 115), (-15, 80)])
thickness = 20
magnetization = [0, 0, 8e5]
delta = 0.5
shape = cuboidize(polygon, t=thickness, delta=delta, mag=magnetization)
field = shape.Bfield([50, 50, -60])
print(f"Raster spacing: {delta} nm; merged cuboids: {len(shape)}")
print("Field at (50, 50, -60) nm (T):", field.ravel())
```

```python
import matplotlib.pyplot as plt

fig, ax = shape.plot_2d(x=(-40, 145, 201), y=(-25, 140, 201), z=-60, component="z")
for index, cuboid in enumerate(shape):
    ax.plot(
        *cuboid.union_boundary("xy")[0],
        color="black",
        linewidth=0.3,
        alpha=0.35,
        label="Projected cuboids" if index == 0 else None,
    )
ax.plot(*np.vstack([polygon, polygon[0]]).T, color="#ff9500", linewidth=1.5, label="Input polygon")
for index, boundary in enumerate(shape.union_boundary("xy")):
    ax.plot(*boundary, color="#00ffff", linewidth=1.5, label="Union boundary" if index == 0 else None)
ax.set(xlabel="x (nm)", ylabel="y (nm)", title="Bz (T) at z = -60 nm; delta = 0.5 nm", aspect="equal")
ax.legend(loc="upper right", fontsize=7, facecolor="#555555", labelcolor="white", framealpha=0.95)
fig.tight_layout()
plt.show()
```

### Single cuboid

Calculate the field and its analytical gradient outside a uniformly magnetized cube. All lengths here are in nm, magnetization is in A/m, fields are in T, and gradients are in T/nm.

[Open notebook](examples/single_cuboid.ipynb)

```python
import numpy as np

from microcubed import Magnet

cube = Magnet(size=[100, 100, 100], center=[0, 0, 0], magnetization=[0, 0, 8e5])
points = np.array([[0, 0, -150], [80, 0, -150]]).T
field = cube.Bfield(points)  # (3, N): Bx, By, Bz
gradient = cube.dBfield(points)  # (3, 3, N): derivative axis, field axis, point
print(field)
```

```python
import matplotlib.pyplot as plt

fig, ax = cube.plot_2d(x=(-250, 250, 61), y=(-250, 250, 61), z=-150, component="z")
for boundary in cube.union_boundary("xy"):
    ax.plot(*boundary, "w-", linewidth=2, label="Material boundary")
ax.set(xlabel="x (nm)", ylabel="y (nm)", title="Bz (T) at z = -150 nm", aspect="equal")
ax.legend(loc="upper right", fontsize=8, facecolor="#555555", labelcolor="white", framealpha=0.95)
fig.tight_layout()
plt.show()
```

<!-- examples:end -->

## Optional solver comparison

The [Ubermag/OOMMF comparison](examples/optional/compare_ubermag.ipynb) compares
fields and gradients at identical exterior points. Install the `comparison`
extra and an OOMMF runner to execute it; neither is needed for Microcubed itself.
See the [example guide](docs/examples.md) for setup and documentation builds
with comparison results.

## Documentation and development

Read the [published documentation](https://newton-per-sqm.github.io/microcubed/).
See the [user guide sources](docs/index.md), [backend guide](docs/backends.md), and
[contributing guide](CONTRIBUTING.md) for development and validation commands.
Python code lives in `src/microcubed/`; Rust kernels live in `src/rust/`.

Microcubed is distributed under the [MIT license](LICENSE.txt).

