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: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.13", "3.14"]

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
3.13

86 changes: 63 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,28 @@

![Lint and Test](https://github.com/upb-lea/mag-net-hub/actions/workflows/python-package.yml/badge.svg)

This repository acts as a hub for selected power loss models that were elaborated by different competitors during the [MagNet Challenge 2023](https://github.com/minjiechen/magnetchallenge).
Feel free to use these loss models for your power converter design as a complement to your datasheet.
This repository provides a unified interface for certified magnetic component models from both the [MagNet Challenge 2023](https://github.com/minjiechen/magnetchallenge) and the [MagNet Challenge 2025](https://github.com/minjiechen/magnetchallenge-2).
It hosts two families of models under a common API:

The loss models are designed such that you can request a certain frequency, temperature, material and $B$ wave (sequence), in order to be provided with a scalar power loss estimate and a corresponding $H$ wave estimate.
- **Loss models** accept a $B$ waveform, frequency, temperature, and material to produce a scalar power loss estimate together with a corresponding $H$ waveform.
- **Sequence-to-sequence models** predict a future $H$ sequence directly from a short warmup window of past $B$/$H$ samples, a future $B$ sequence, and the temperature — operating on variable-length waveforms without resampling.

Feel free to use these models for your power converter design as a complement to your datasheet.

__Disclaimer__: Only steady-state and no varying DC-Bias is supported yet.
Moreover, training data stemmed from measurements on toroid-shaped ferrites that had a fix size.

Supported materials:
- ML95S
- T37
- 3C90
- 3C92
- 3C94
- 3C95
- 3E6
- 3F4
- 77
- 78
- 79
- N27
- N30
- N49
- N87
Supported materials for the loss models:
- 3C90, 3C92, 3C94, 3C95, 3E6, 3F4, 77, 78, 79, ML95S, N27, N30, N49, N87, T37

Supported materials for the sequence-to-sequence models:
- 3C90, 3C92, 3C94, 3C95, 3E6, 3F4, 77, 78, FEC007, FEC014, N27, N30, N49, N87, T37


## Installation

### Python
We strongly recommend Python __3.10__.
We strongly recommend Python __3.13__.
Higher versions may also work.

Then install through pip:
Expand All @@ -51,11 +43,19 @@ cd mag-net-hub
pip install .
```

If you use `uv`, then just add to dependencies:
```
uv add mag-net-hub
```

## Usage
Models are provided as executable code with readily trained coefficients.
Hence, no training is conducted in this project.

### Python

#### Power loss models

```py
import numpy as np
import magnethub as mh
Expand All @@ -79,6 +79,40 @@ p, h = mdl(b_waves, freqs, temps)

```

#### Sequence-to-sequence models

Sequence models predict a future $H$ waveform from a short warmup window of past $B$/$H$
samples plus a future $B$ waveform and the temperature. Any sequence length is accepted.

```py
import numpy as np
import magnethub as mh

# instantiate material-specific sequence model
mdl = mh.sequence.SequenceModel(material="3C90", team="paderborn")

future = 1024
b_future = np.random.randn(future) * 200e-3 # future B waveform in T
temp = 25 # °C

# simplest call — no warmup, past defaults to zero
h = mdl(b_future, temp)

# with an explicit warmup window for better accuracy
warmup = 128
b_past = np.random.randn(warmup) * 200e-3 # past B warmup window in T
h_past = np.random.randn(warmup) * 5 # past H warmup window in A/m
h = mdl(b_future, temp, b_past, h_past)

# batch execution for 100 trajectories
b_past = np.random.randn(100, warmup) * 200e-3
h_past = np.random.randn(100, warmup) * 5
b_future = np.random.randn(100, future) * 200e-3
temps = np.random.randint(20, 80, size=100)
h = mdl(b_future, temps, b_past, h_past)

```


## Contributing
Whether you want to contribute your submission to the MagNet Challenge, or you are a single contributor who wants to add an awesome model to this hub -- any contribution is welcome.
Expand All @@ -94,25 +128,31 @@ See the below folder structure overview with annotations on how to contribute a
│   └── magnethub
│   ├── __init__.py
│   ├── loss.py
│ ├── sequence.py
│   ├── models
│   │   ├── paderborn
│   │   │   ├── changelog.md
│   │   │   ├── cnn_3C90_experiment_1b4d8_model_f3915868_seed_0_fold_0.pt
│   │   │   ├── cnn_3C92_experiment_ea1fe_model_72510647_seed_0_fold_0.pt
| | | └── ...
│   │   ├── sydney
│   │   ├── paderborn_sequence_modeling
│ │ │ ├── 3C90_GRU8_MagNetHub-reduced-features-f32_5c3f8051_seed49.eqx
│ │ │ ├── example_usage.py
│ │ │ └── ...
│ │ ├── sydney
│   │   │   └── ...
│   │   └── <add your contributor folder here>
│   │   │   └── <add your model coefficients here>
│   ├── paderborn.py
│ ├── paderborn_loss.py
│ ├── paderborn_sequence.py
| ├── sydney.py
| ├── <add your model code here>

```

Any number of models can be incorporated easily according to this code structure policy.
If you have added model coefficients and execution logic via code, it only requires to be hooked in
`loss.py` and you are ready to fire this pull request (PR).
`loss.py` (or `sequence.py` for sequence-to-sequence models) and you are ready to fire this pull request (PR).

If it is possible, please also consider adding tests for your model logic under `tests/`, writing comprehensive docstrings in your code with some comments, and discuss the performance of your model in your PR.

Expand Down
25 changes: 14 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,42 @@ authors = [
]
description = "MagNet Toolkit - Certified Models of the MagNet Challenge"
readme = "README.md"
requires-python = "~=3.10"
requires-python = ">=3.13"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dynamic = ["dependencies"] # commented due to packaging issues: "optional-dependencies"

[tool.setuptools.dynamic]
dependencies = { file = ["requirements.txt"] }
optional-dependencies = { dev = { file = ["requirements-dev.txt"] } }
dependencies = [
"tqdm",
"pandas>=2",
"torch>=2.0.1",
"torchinfo>=1.8.0",
"joblib>=1.3.2",
"scipy",
"pytest",
"jax>=0.6.2",
"equinox>=0.13.8",
]

[project.urls]
Homepage = "https://github.com/upb-lea/mag-net-hub"
Issues = "https://github.com/upb-lea/mag-net-hub/issues"

[build-system]
requires = ["hatchling", "hatch-requirements-txt"]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata.hooks.requirements_txt]
files = ["requirements.txt"]

[tool.hatch.build.targets.wheel]
packages = ["src_py/magnethub"]

[tool.hatch.build.targets.sdist]
include = [
"src_py/magnethub/*.py",
"src_py/magnethub/models/paderborn/*.pt",
"src_py/magnethub/models/paderborn_sequence_modeling/*.eqx",
"src_py/magnethub/models/sydney/*.pt",
"tests",
"requirements.txt"
]


Expand Down
7 changes: 0 additions & 7 deletions requirements.txt

This file was deleted.

4 changes: 3 additions & 1 deletion src_py/magnethub/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Init file for python package."""
import magnethub.loss
import magnethub.paderborn
import magnethub.sequence
import magnethub.paderborn_loss
import magnethub.paderborn_sequence
import magnethub.sydney
6 changes: 3 additions & 3 deletions src_py/magnethub/loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""

from pathlib import Path
import magnethub.paderborn as pb
import magnethub.paderborn_loss as pb
import magnethub.sydney as sy
import numpy as np

Expand Down Expand Up @@ -91,7 +91,7 @@ def __call__(self, b_field, frequency, temperature):
query_points = np.arange(L)
support_points = np.arange(actual_len) * L / actual_len
# TODO Does a vectorized form of 1d interpolation exist?
b_field = np.row_stack(
b_field = np.vstack(
[np.interp(query_points, support_points, b_field[i]) for i in range(b_field.shape[0])]
)

Expand All @@ -106,5 +106,5 @@ def __call__(self, b_field, frequency, temperature):
actual_len = h_seq.shape[-1]
query_points = np.arange(original_seq_len)
support_points = np.arange(actual_len) * original_seq_len / actual_len
h_seq = np.row_stack([np.interp(query_points, support_points, h_seq[i]) for i in range(h_seq.shape[0])])
h_seq = np.vstack([np.interp(query_points, support_points, h_seq[i]) for i in range(h_seq.shape[0])])
return p, h_seq
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Code is adapted from https://github.com/upb-lea/RHINO-MAG."""

import pathlib
import json
from functools import partial
from copy import deepcopy

import jax
import jax.numpy as jnp
import equinox as eqx

from magnethub.paderborn_sequence import filter_spec, Normalizer, setup_featurize, setup_model


def reconstruct_model_from_file(filename: pathlib.Path):
"""Reconstruct a model from its file stored on disk.

Args:
filename (pathlib.Path): The path of file to load. If the suffix is missing, `.eqx` is
added by default. One can give the full path of the model file or, if the
file cannot be found at the specified path, the function looks for the model in
the `mc2.data_management.MODEL_DUMP_ROOT`, which is the default folder models are
stored in.

The commands:
```
from mc2.data_management import MODEL_DUMP_ROOT
reconstruct_model_from_file('A_GRU8_reduced-features-f32_2a1473b6_seed12')
reconstruct_model_from_file('A_GRU8_reduced-features-f32_2a1473b6_seed12.eqx')
reconstruct_model_from_file(MODEL_DUMP_ROOT / 'A_GRU8_reduced-features-f32_2a1473b6_seed12.eqx')
```
All load the same model at `data/models` unless a file with the same name is present in the cwd
in which case the first two calls load that model from the cwd and the last one loads the model
from `data/models`. Generally, the intended way is to call using the first version.

Returns:
The ModelInterface object (i.e., the model wrapped into the corresponing interface)
"""
filename = pathlib.Path(filename)

# append the '.eqx' suffix if it is missing
if filename.suffix == "":
filename = filename.with_name(f"{filename.name}.eqx")

with open(filename, "rb") as f:
params = json.loads(f.readline().decode())

normalizer = Normalizer.from_dict(params["normalizer_dict"])
featurize = setup_featurize(
disable_features=params["training_params"]["disable_features"],
dyn_avg_kernel_size=params["training_params"]["dyn_avg_kernel_size"],
time_shift=params["training_params"]["time_shift"],
)
fresh_wrapped_model, _ = setup_model(
model_label=params["model_type"],
model_key=jax.random.PRNGKey(0),
normalizer=normalizer,
featurize=featurize,
time_shift=params["training_params"]["time_shift"],
)

loading_params = deepcopy(params["model_params"])
loading_params["key"] = jnp.array(loading_params["key"], dtype=jnp.uint32)
try:
model = type(fresh_wrapped_model.model)(**loading_params)
except TypeError:
model = type(fresh_wrapped_model.model)(normalizer=normalizer, **loading_params)
model = eqx.tree_deserialise_leaves(f, model, partial(filter_spec, f64_enabled=jax.config.x64_enabled))
wrapped_model = eqx.tree_at(lambda t: t.model, fresh_wrapped_model, model)

return wrapped_model


if __name__ == "__main__":

# load model
model = reconstruct_model_from_file("3C90_GRU8_MagNetHub-reduced-features-f32_5c3f8051_seed49")

# generate dummy data
batch_size = 20
sequence_length = 1000
warmup_len = 100

key = jax.random.PRNGKey(seed=0)
b_key, h_key, t_key = jax.random.split(key, 3)

B_sequence = jax.random.normal(b_key, (batch_size, sequence_length)) # (20, 1000)
H_past = jax.random.normal(h_key, (batch_size, warmup_len)) # (20, 100)
T = jax.random.randint(t_key, (batch_size,), minval=25, maxval=70)

# inference
H_pred = model(
B_past=B_sequence[:, :warmup_len], # (20, 100)
H_past=H_past, # (20, 100)
B_future=B_sequence[:, warmup_len:], # (20, 900)
T=T, # (20,)
)
print(H_pred.shape) # (20, 900)
Loading
Loading