Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* text=auto eol=lf
*.h5 binary
62 changes: 62 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Documentation

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip

- name: Install the package and documentation tools
run: python -m pip install -e '.[docs,test]'

- name: Test documentation examples
run: python -m pytest tests/test_docs_examples.py --no-cov

- name: Build documentation
run: mkdocs build --strict

- name: Configure GitHub Pages
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
uses: actions/configure-pages@v5

- name: Upload GitHub Pages artifact
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
uses: actions/upload-pages-artifact@v4
with:
path: site

deploy:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
18 changes: 11 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
*.swp
.DS_Store
venv
.venv/
__pycache__
*.egg-info/
htmlcov/
.coverage
.pytest_cache/

# Dune/OCaml Project Ignore File
_build/
.dune/
.merlin
.ocaml-history
.cmi
target/
build/
dist/
site/
.eggs/
.mypy_cache/
.pytype/
.tox/
.nox/
.ruff_cache/
9 changes: 5 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ocaml.sandbox": {
"kind": "opam",
"switch": "4.14.2"
}
"python.analysis.diagnosticMode": "workspace",
"python.analysis.extraPaths": [
"${workspaceFolder}/src"
],
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}
53 changes: 0 additions & 53 deletions PARITY_COMPLETION.md

This file was deleted.

139 changes: 119 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,119 @@
# NMAG Python 3

This is an in-progress port of [nmag](https://github.com/nmag-project/nmag-src) that updates old dependencies and packages nmag as a standalone python 3 module.

The Python 3 port is intended to be standalone and must not require OCaml at runtime. See [PARITY_COMPLETION.md](PARITY_COMPLETION.md) for the Section 4 parity record and validation gate.

### Installation
* Create a virtual environment: `python -m venv venv`
* Activate the virtual environment:
* On Windows: `venv\Scripts\activate`
* On macOS/Linux: `source venv/bin/activate`
* To initialize the project and install dependencies: `pip install -e .`

### Testing
* To install the optional test dependencies run `pip install -e ".[test]"`
* Once the project has been initialized simply run `pytest` to run your tests
* To generate modern nmesh parity scenario outputs run `python tools/nmesh_parity_compare.py --output-dir parity/new`
* To compare against legacy `.nmesh` artifacts run `python tools/nmesh_parity_compare.py --legacy-dir parity/legacy`
* To view coverage open `htmlcov/index.html` in your browser
* [Specifying what tests to run](https://docs.pytest.org/en/latest/how-to/usage.html#specifying-which-tests-to-run)
# Nmag for Python 3

`nmag-python-3` is a standalone Python implementation of the Nmag finite-element
micromagnetic simulation interface. It supports tetrahedral meshes, magnetic
materials, static effective fields, adaptive LLG dynamics and relaxation,
NDT/HDF5 output, field probing, and native restart checkpoints.

The human documentation is available at
**<https://trimagnetix.github.io/nmag-python-3/>**. Start with the
[installation guide](https://trimagnetix.github.io/nmag-python-3/getting-started/installation/)
and [quickstart](https://trimagnetix.github.io/nmag-python-3/getting-started/quickstart/).

## Requirements

- Linux
- Python 3.10 or newer
- A C compiler for Python dependencies without a platform wheel
- Rust and the matching Python development library only for optional native
acceleration

## Installation

From a clone of this repository:

```bash
./scripts/setup.sh
```

The setup script creates or reuses `.venv`, offers to build the optional Rust
accelerator, and runs the standard checks. Run a simulation with:

```bash
.venv/bin/python {{simulation_file_name.py}}
```

## Minimal simulation

```python
import nmag
import nmesh

mesh = nmesh.mesh_from_points_and_simplices(
points=[[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]],
simplices_indices=[[0, 1, 2, 3]],
simplices_regions=[1],
)
mesh.save("sample.nmesh.h5")

material = nmag.MagMaterial(
name="Py",
Ms=nmag.SI(1e6, "A/m"),
exchange_coupling=nmag.SI(13e-12, "J/m"),
)

simulation = nmag.Simulation(name="sample")
simulation.load_mesh(
"sample.nmesh.h5",
[("magnetic", material)],
unit_length=nmag.SI(1e-9, "m"),
)
simulation.set_m([1, 0, 0])
simulation.set_H_ext([0, 0, 0], nmag.SI("A/m"))
simulation.save_data(fields="all")
```

For a tested, rerunnable version and a canonical sphere example, see the
[getting-started guide](https://trimagnetix.github.io/nmag-python-3/getting-started/quickstart/).

## Status

The supported solver covers 3D tetrahedral demagnetization, exchange, uniform
applied fields, uniaxial and cubic anisotropy, custom polynomial anisotropy,
pinning, Zhang-Li current torque, adaptive dynamics and relaxation, checkpoints,
and resource-aware demagnetization storage.

Thermal dynamics, Slonczewski torque, periodic micromagnetic boundaries,
material-specific magnetization at incompatible shared nodes, local
inter-material coupling, and full legacy hysteresis compatibility are not yet
supported. Validate new geometries and material models before relying on
production results. The complete and current list is maintained in the
[supported-scope guide](https://trimagnetix.github.io/nmag-python-3/guides/supported-scope/).

## Configuration and acceleration

Each simulation accepts an immutable `NmagConfig`. `accelerator="auto"` uses
installed Rust kernels where helpful, `"off"` keeps Python/Numba paths, and
`"rust"` requires the extension. Advanced callers can use
`accelerator_overrides={nmag.RustKernel.LLG: "rust"}`.

For a simulation created without an explicit configuration,
`NMAG_ACCELERATOR=auto|off|rust` selects the process default. Prefer
`NmagConfig` in reusable programs. See the
[configuration guide](https://trimagnetix.github.io/nmag-python-3/guides/configuration/)
for storage, memory, and integrator choices.

## Development checks

```bash
./scripts/verify.sh
./scripts/verify.sh --rust
```

Build the documentation locally with:

```bash
.venv/bin/python -m pip install -e '.[docs]'
.venv/bin/mkdocs build --strict
```

## Project lineage

This project modernizes the original
[nmag-project/nmag-src](https://github.com/nmag-project/nmag-src). The
[historical Nmag 0.2 manual](https://nmag.readthedocs.io/en/latest/) remains a
useful background reference, but its Python 2 runtime and some of its features
do not describe this rewrite.

Nmag for Python 3 is distributed under the GNU General Public License version 2
or, at your option, any later version. See [LICENSE](LICENSE).
Binary file added docs/assets/sphere1.nmesh.h5
Binary file not shown.
46 changes: 46 additions & 0 deletions docs/examples/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Small, self-contained Nmag simulation used by the quickstart guide."""

from pathlib import Path

import nmag
import nmesh


output_directory = Path("results")
output_directory.mkdir(exist_ok=True)

mesh = nmesh.mesh_from_points_and_simplices(
points=[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
],
simplices_indices=[[0, 1, 2, 3]],
simplices_regions=[1],
)
mesh_path = output_directory / "quickstart.nmesh.h5"
mesh.save(mesh_path)

permalloy = nmag.MagMaterial(
name="Py",
Ms=nmag.SI(1e6, "A/m"),
exchange_coupling=nmag.SI(13e-12, "J/m"),
)

config = nmag.NmagConfig(
output_directory=output_directory,
output_policy="replace",
)
simulation = nmag.Simulation(name="quickstart", config=config)
simulation.load_mesh(
str(mesh_path),
[("magnetic", permalloy)],
unit_length=nmag.SI(1e-9, "m"),
)
simulation.set_m([1.0, 0.0, 0.0])
simulation.set_H_ext([0.0, 0.0, 0.0], nmag.SI("A/m"))
simulation.save_data(fields="all")

print("Average demagnetization field:", simulation.get_subfield_average("H_demag"))
print("Results written to:", output_directory.resolve())
41 changes: 41 additions & 0 deletions docs/examples/sphere_demag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Compute and sample the demagnetization field of a uniformly magnetized sphere."""

import sys
from pathlib import Path

import nmag


mesh_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("sphere1.nmesh.h5")
if not mesh_path.is_file():
raise SystemExit(f"Mesh not found: {mesh_path}")

output_directory = Path("results")
output_directory.mkdir(exist_ok=True)

permalloy = nmag.MagMaterial(
name="Py",
Ms=nmag.SI(1e6, "A/m"),
exchange_coupling=nmag.SI(13e-12, "J/m"),
)

config = nmag.NmagConfig(
output_directory=output_directory,
output_policy="replace",
)
simulation = nmag.Simulation(name="sphere1", config=config)
simulation.load_mesh(
str(mesh_path),
[("sphere", permalloy)],
unit_length=nmag.SI(1e-9, "m"),
)
simulation.set_m([1.0, 0.0, 0.0])
simulation.set_H_ext([0.0, 0.0, 0.0], nmag.SI("A/m"))
simulation.save_data(fields="all")

field_at_origin = simulation.probe_subfield_siv("H_demag", [0.0, 0.0, 0.0])
expected_x = -permalloy.Ms.value / 3.0

print("H_demag at the origin:", field_at_origin, "A/m")
print("Analytic x component for a sphere:", expected_x, "A/m")
print("Average magnetization:", simulation.get_subfield_average("m"))
Loading