magnelio.io#

An open Project is both the store reader and a scattering result: it satisfies ScatteringResult, so S, db, phase, plot_s and the Touchstone / scikit-rf exports work on it exactly as on the in-RAM result. Its runs mapping hands out one Run per run — state, step count, clock, energy trace, result and monitors — and checkpoint_state reads a resume checkpoint back as a CheckpointState mapping; see Projects and runs in the technical description for the vocabulary.

Project store and CAD file import.

open_project lives in the core magnelio namespace; this component holds the reader/writer classes and geometry file I/O. The one-shot save_project/load_project (io/hdf5.py) was removed; the store supersedes it.

import_step / import_brep read geometry drawn in a CAD system into the geometry API; import_pcb reads a printed circuit board from the fabrication data its layout tool writes.

class magnelio.io.CheckpointState(data, *, run=None, path=None)#

A run’s resume checkpoint, as the solver’s state_dict reads back.

A read-only mapping with the nested shape FITTimeDomainSolver wrote — n_completed, the peak energy and port signal, the flat e and h field vectors, and a group per boundary, port and monitor — so state["e"] and "dispersion" in state work as on the plain dict, while printing the object shows its size and step rather than its field vectors.

Parameters:
  • data (dict)

  • run (str | None)

get(k[, d]) → D[k] if k in D, else d.  d defaults to None.#
items() → a set-like object providing a view on D's items#
keys() → a set-like object providing a view on D's keys#
values() → an object providing a view on D's values#
class magnelio.io.LoadedGeometry(shapes, background)#

Read-only geometry recovered from a project’s BREP + material list.

Mirrors the iteration surface of GeometryModel (shapes, background, iteration, len) so it drops into the same consumers.

Parameters:

shapes (list)

plot(**kwargs)#

Interactive 3D view — same wrapper as GeometryModel.plot.

plot_cross_section(normal, position, **kwargs)#

2D cross-section — same wrapper as GeometryModel.plot_cross_section.

Parameters:
  • normal (str)

  • position (float)

class magnelio.io.Project(path)#

Read-only view over a project directory.

Lazily loads and caches the mesh, geometry and setup metadata on first access. Implements the same scattering-result contract as the in-RAM ScatteringTDResult (see magnelio.analysis.result_interface), so a separate post-processing script and the return value of run() are one and the same reader.

Parameters:

path (str | Path)

S(out_port, in_port, *, mode_out=0, mode_in=0, f_axis=None)#

S-parameter column, derived on read (optionally on a custom f_axis).

a(port, mode=0, *, excited=None, f_ref=None, destagger=True)#

Incident power-wave time series a(t) (see ScatteringTDResult.a).

b(port, mode=0, *, excited=None, f_ref=None, destagger=True)#

Outgoing power-wave time series b(t) (see ScatteringTDResult.b).

checkpoint_state(excited=None)#

Load a run’s resume checkpoint (state_dict), or None.

Returns the solver state persisted at the last periodic / final / graceful-abort checkpoint as a CheckpointState — a read-only mapping with the nested-dict shape of the solver’s state_dict — or None if the run wrote no checkpoint (streaming without resume, or aborted before the first interval). resume() feeds this straight into FITTimeDomainSolver.load_state_dict; the n_completed entry is the step the checkpoint corresponds to.

Parameters:

excited (str | tuple[str, int] | None)

Return type:

CheckpointState | None

db(out_port, in_port, *, mode_out=0, mode_in=0, floor_db=-200.0, f_axis=None)#

S-parameter magnitude in dB, derived on read (see S()).

floor_db clamps the result from below so an exact zero stays plottable instead of becoming -inf.

deembed(distances)#

Shift port reference planes; return the de-embedded S-matrix.

Removes the feed-line propagation between a port plane and its new reference plane: result.deembed({"port1": d}) moves port1’s reference plane the distance d [m] from the port plane into the domain, and every S-parameter touching that port is multiplied by the inverse line propagation factor over d — reflections twice, transmissions once per shifted end. A negative distance moves the plane outward (adds line length); ports not named keep their plane.

The shift uses the discrete dispersion of the port’s uniform feed chain — the same grid propagation the solver applied — so de-embedding a uniform feed line removes its phase down to the accuracy floor of the run itself, including the grid-dispersion part that an analytic exp(-jβd) would leave behind on coarse meshes. It assumes the cross-section stays that of the port over the shifted length. A quasi-TEM channel (microstrip, CPW — an inhomogeneous cross-section on modal Mur) carries no certified line parameters; the run keeps the port’s dispersion record instead, and the shift uses the true discrete modes of the feed solved at every frequency of the axis, so the line’s physical dispersion is removed as well (the first call on such a result spends a few seconds on that solve). Only a channel with neither — a feed section that is not a uniform chain behind the port — falls back to the mode’s continuum γ(f), for a quasi-TEM mode the frequency-flat quasi-static one.

Below its cut-off a channel’s factor grows exponentially with distance, so de-embedded values there keep the diagnostic character the raw ones have. Lumped ports carry no feed-line dispersion; naming one raises.

Parameters:

distances (dict[str, float]) – Per-port shift distance [m], positive into the domain.

Returns:

A new result referenced at the shifted planes — the original is untouched. It answers S / db / phase / plot_s and the Touchstone / scikit-rf exports.

Return type:

SParameterResult

energy_trace(excited=None)#

Stored (step, time, energy) trace of a run [structured array].

The same as project.runs[name].energy_trace; this form takes a scattering run’s excited pair and may omit the selector on a one-run project.

Parameters:

excited (str | tuple[str, int] | None)

export_paraview(excited=None, *, glyph_percentile=98.0, bake_state=True, pvpython=None)#

Write the ready-to-open ParaView session for one run.

The only place the ParaView artefacts are written: a run’s close, a resume and the eigenmode solver write none of them. Writes the tessellated geometry.vtm in the project directory if it is missing, one .vtr per frame of every field monitor under paraview/ (collected by a .pvd per monitor), paraview_open.py (open with paraview --script=…), and — when pvpython is available and bake_state — the double-clickable paraview.pvsm, all in the run directory. Call it again after a resume; the set is regenerated.

Parameters:
  • excited (str or tuple, optional) – Selects the run: a port name, or a (name, mode) pair. May be omitted when the project holds one run.

  • glyph_percentile (float, default 98.0) – Percentile of the field-vector magnitude used as the glyph clip cap (edge singularities would otherwise dictate the arrow scaling).

  • bake_state (bool, default True) – Bake paraview.pvsm via pvpython when available.

  • pvpython (str or Path or sequence of str, optional) – Which pvpython bakes the state, when the machine carries more than one ParaView (default: MAGNELIO_PVPYTHON, else the first on PATH). A path, or the command that launches one — a sandboxed ParaView is reached through its runner, e.g. "flatpak run --command=pvpython org.paraview.ParaView". A state file names its proxies the way the release that wrote it spells them, so bake it with the ParaView that will open it; paraview_open.py builds the session live and needs no such care.

Returns:

Written artefact paths (script, state or None, monitors); empty when there is nothing to visualise.

Return type:

dict

export_paraview_eigenmodes(*, glyph_percentile=98.0, bake_state=True, pvpython=None)#

Write the ParaView session for the stored eigenmodes.

The eigenmode counterpart of export_paraview(), and like it the only place these files are written. Eigenmodes have no excitation and no time axis, so they belong to the project rather than to a run: the artefacts land in the project directory itself (paraview_open.py, paraview.pvsm, paraview/eigenmodes.pvd and one .vtr per mode).

Stepping the ParaView time axis steps through the modes — degenerate pairs share an eigenfrequency exactly, so the frequency cannot serve as that axis; it travels as field data instead. Fields are peak-normalised per mode, since an eigenvector carries no absolute amplitude, and the divisors are written alongside so the scaling stays reversible.

Parameters:
  • glyph_percentile (float, default 98.0) – Percentile of |E| used as the glyph magnitude clip cap (a field peak on a conductor edge would otherwise dictate the arrow scaling).

  • bake_state (bool, default True) – Bake paraview.pvsm via pvpython when available.

  • pvpython (str or Path or sequence of str, optional) – Which pvpython bakes the state, when the machine carries more than one ParaView (default: MAGNELIO_PVPYTHON, else the first on PATH). A path, or the command that launches one — a sandboxed ParaView is reached through its runner, e.g. "flatpak run --command=pvpython org.paraview.ParaView". A state file names its proxies the way the release that wrote it spells them, so bake it with the ParaView that will open it; paraview_open.py builds the session live and needs no such care.

Returns:

Written artefact paths (script, state or None, monitors); empty when the project stores no eigenmodes.

Return type:

dict

Examples

>>> project = mio.AnalysisEigenmode(mesh=mesh, n_modes=4,
...                                 project="cavity").run()
>>> project.export_paraview_eigenmodes()["script"]
follow(interval=2.0, *, plot=False, timeout=None, stream=None)#

Watch this project and show its state in place until it is finished.

The zero-code form of watch(): at every change the project’s summary and run table are shown again, replacing the previous one rather than scrolling below it. In a notebook the cell’s output is cleared and the table redrawn; on a terminal the table is redrawn over its own lines; in a log file each table is appended. Blocks until the project is finished (done, aborted or stale) or timeout seconds have passed, and returns the project. Interrupting the kernel (Ctrl-C) ends it early.

plot=True adds the energy plot (plot_energy()) below the table, redrawn with it. A figure drawn inside a loop of your own would not show until the cell ends — the notebook’s inline backend flushes figures per cell — so the figure is rendered here at every change and replaced with the table. To draw your own picture, pass a callable plot(project, ax) that draws into the fresh axes it is given:

def draw(proj, ax):
    proj.plot_energy(ax=ax)
    ax.set_ylim(-80, 0)

proj.follow(interval=5, plot=draw)

On a terminal with a window-capable matplotlib backend the picture is one window redrawn in place; without one (a log, a headless run) only the table is shown.

For a panel that keeps moving while you work in other cells, see monitor().

Parameters:
  • interval (float, default 2.0) – Seconds between two looks at the store.

  • plot (bool or callable, default False) – True for the energy plot; a callable plot(project, ax) for a picture of your own.

  • timeout (float, optional) – Give up after this many seconds.

  • stream (file-like, optional) – Where the text form goes; defaults to sys.stdout. A notebook is recognised on its own.

monitor(interval=2.0, *, x='time')#

A live panel for a notebook: the run table above the energy plot.

Returns an ipywidgets box that a background thread refreshes every interval seconds — the project summary with its run table as HTML, and every run’s stored energy in dB below the peak as a picture — until the project is finished. Leave it as the last expression of a cell; the cell returns at once and the panel keeps moving. panel.stop() ends the refresh early. Needs the jupyter extra (pip install 'magnelio[jupyter]').

Parameters:
  • interval (float, default 2.0) – Seconds between refreshes.

  • x ({"time", "step"}, default "time") – Horizontal axis of the energy plot.

monitors_for(excited=None)#

Lazy monitor readers for one run, keyed by monitor name.

Resolves each monitor kind by name into its matching lazy reader — _LoadedFieldMonitor (MonitorFieldTime snapshots) and _LoadedFluxMonitor (MonitorFluxTime scalar series) from the run’s results.h5, plus _LoadedFreqMonitor (MonitorFieldFrequency DFT) from fields_freq.h5 and _LoadedMonitorWallLoss (per-tag dissipated fractions) from wall_loss.h5. The user only knows the name, not the kind or the file. excited selects the run; it may be omitted when the project holds one run.

Parameters:

excited (str | tuple[str, int] | None)

Return type:

dict

phase(out_port, in_port, *, mode_out=0, mode_in=0, deg=True, unwrap=True, f_axis=None)#

Phase of one S-parameter over the frequency axis.

Parameters:
  • out_port (str) – Observed / excited port names (modes via mode_out / mode_in), as in S().

  • in_port (str) – Observed / excited port names (modes via mode_out / mode_in), as in S().

  • deg (bool, default True) – Return degrees; False returns radians.

  • unwrap (bool, default True) – Unwrap 2π discontinuities along the frequency axis.

  • f_axis (array-like, optional) – Custom frequency axis, on hosts whose S() can recompute the spectrum (run results); a plain SParameterResult holds one fixed axis and rejects it.

  • mode_out (int)

  • mode_in (int)

Returns:

Phase per frequency point.

Return type:

np.ndarray

plot_balance(*excitations, deficit=False, ax=None)#

Plot the power balance of every excitation.

For each excited channel j the sum of the squared magnitudes over the observed channels, Σ_i |S_ij|² — the share of the incident power that comes back out of the ports. On a lossless, fully exported network it is one; what is missing is what left the ports: ohmic and dielectric loss, and radiation.

The reading depends on two things the plot cannot check. Only channels present in the result are summed, so a network whose higher modes or whose ports were not all exported reads short and the deficit looks like loss (is_complete says whether every observed channel was also excited). And a channel that carries no propagating mode at a frequency is NaN there; an evanescent channel transports no active power, so it counts as zero rather than poisoning the sum.

Parameters:
  • *excitations (str or tuple) – Excited channels to plot, each port or (port, mode). Without arguments every excitation is plotted.

  • deficit (bool, default False) – Plot 10·log10(1 − Σ) — the power that left the ports, in dB — instead of the sum itself. The useful form when the balance is close to one, where the interesting number is how far from it.

  • ax (matplotlib.axes.Axes, optional)

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (matplotlib.axes.Axes)

plot_energy(*, x='time', floor_db=None, ax=None)#

Plot every run’s stored energy in dB below its peak, one curve per run.

The figure the progress line reports, for the whole project: the legend names the runs, and the energy criterion is a dashed line when every run shares one. x is "time" (nanoseconds) or "step"; the axis runs from ten dB below the criterion (floor_db pins it) to +5 dB; ax draws into existing axes. Returns (fig, ax).

Parameters:
  • x (str)

  • floor_db (float | None)

plot_polar(*pairs, mark=None, r_max=None, ax=None)#

Plot channels in the complex plane, magnitude over phase.

The polar counterpart of plot_smith() for channels the Smith chart does not describe — transmission above all, whose magnitude is not bounded by one in a network with gain and which has no impedance reading.

Parameters:
  • *pairs (tuple) – As plot_smith(); without arguments every recorded channel of every excitation.

  • mark (array_like, optional) – Frequencies [Hz] to mark on every trace.

  • r_max (float, optional) – Radial limit; default the peak magnitude, at least one.

  • ax (matplotlib.axes.Axes, optional) – Must be a polar axes; one is created otherwise.

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (matplotlib.axes.Axes)

plot_s(*pairs, db=True, floor_db=-200.0, ax=None)#

Plot S-parameter magnitudes over frequency.

Parameters:
  • *pairs (tuple) – Channels to plot, each (out_port, in_port) or (out_port, in_port, mode_out, mode_in). Without arguments every recorded channel of every excitation is plotted.

  • db (bool, default True) – Magnitude in dB (with floor_db) instead of linear.

  • floor_db (float, default -200.0) – Clip floor for the dB display.

  • ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created otherwise.

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (matplotlib.axes.Axes)

plot_smith(*pairs, mark=None, labels=True, ax=None)#

Plot reflection channels on a Smith chart.

The complex reflection coefficient traced over frequency on the unit disc, with the circles of constant normalised resistance and reactance behind it. The chart is only readable against one normalisation: a channel whose reference impedance varies with frequency (a dispersive waveguide mode, DD-244) has no single set of circles, and its trace here is the power-wave coefficient against a moving reference. Call renormalize() first — renormalize(50) — when that matters; a warning names the case.

Parameters:
  • *pairs (tuple) – Channels to draw, each (out_port, in_port) or (out_port, in_port, mode_out, mode_in). Without arguments the reflection channel of every excitation.

  • mark (array_like, optional) – Frequencies [Hz] to mark and label on every trace.

  • labels (bool, default True) – Draw the resistance labels of the chart’s grid.

  • ax (matplotlib.axes.Axes, optional) – Axes to draw into; a square figure is created otherwise.

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (matplotlib.axes.Axes)

reference_impedance(port, mode=0)#

Reference impedance [Ω] of one channel along the frequency axis.

The real impedance the channel’s power waves — and so its row and column of the S-matrix — are defined against: the line impedance of a TEM or quasi-TEM port mode as the grid carries it, the wave impedance of a hollow-pipe mode (which varies with frequency), the Thévenin impedance of a lumped port. Full-model values on ports cut by a symmetry plane. See renormalize() for moving to a common reference.

Parameters:
  • port (str)

  • mode (int)

Return type:

ndarray

refresh()#

Re-read the metadata and drop cached run / S-parameter data.

A project that is not finished re-reads its metadata on its own whenever the file changes; call this to force a re-read on a finished project (a run resumed elsewhere), or to drop the cached run and S-parameter data. The immutable model (mesh, geometry) is kept. Returns self for chaining (project.refresh().s_params).

Return type:

Project

renormalize(z_ref)#

Re-reference the S-matrix to new port impedances.

The raw S-matrix is measured against each port mode’s own impedance on the grid (reference_impedance()) — a uniform line is matched there whatever its impedance came out at. This returns the same network against z_ref instead, typically renormalize(50): what a network analyser with 50 Ω reference planes would read, and what a circuit simulator expects before it cascades this block with others. It acts on the square matrix over the excited channels: a channel that was observed but never excited stays matched to its own impedance and is left out, as the exports leave it out.

Whether the re-referenced mismatch is real is a modelling question: a 49 Ω grid line feeding a 50 Ω system does reflect, while a line meant to be the 50 Ω one shows a discretisation artefact — converge its impedance on the port plane first (refine_port_modes).

Parameters:

z_ref (float or dict) – New real reference impedance [Ω] for every channel, or a mapping {port_name: Z} / {(port, mode): Z}, each value a scalar or an array on the frequency axis.

Returns:

A new result on the same channels; the original is untouched. It answers S / db / phase / plot_s and the Touchstone / scikit-rf exports.

Return type:

SParameterResult

result(name=None)#

One run as a TDResult.

Rebuilds the in-RAM result object of the run from the store: the recorded port signals, the sampled excitations, the energy trace and lazy readers for the run’s monitors. name is the run name (run_1, or the name= given to run()); a scattering channel run may be named by its excited pair. May be omitted when the project holds one run.

Parameters:

name (str | tuple[str, int] | None)

to_skrf(name='magnelio', *, channels=None, z_ref=None)#

Return the S-matrix as a skrf.Network.

Requires scikit-rf (extra magnelio[interop]). Same sub-matrix, channel selection and warning as to_touchstone(); the network’s z0 carries each channel’s reference impedance per frequency.

Parameters:
  • name (str, optional) – Network name.

  • channels (sequence of str or (str, int), optional) – Explicit channel selection, as in to_touchstone().

  • z_ref (float or dict, optional) – Renormalise first, as in renormalize().

Return type:

skrf.Network

to_touchstone(path, *, channels=None, z_ref=None)#

Write the S-matrix as a Touchstone .sNp file.

Exports the square sub-matrix over the excited channels — one Touchstone port per channel, so a multi-mode port occupies one port per mode. Channels that were observed but never excited are dropped from rows and columns alike; they carry a reflection-free boundary throughout the run, so the export is the network seen with them matched, the same quantity a network analyser measures with its unused ports terminated.

The option line’s R states the reference impedance the data refer to. Touchstone 1.x holds one constant value for all ports, so pass z_ref (typically z_ref=50) to renormalise first when the ports’ own references differ or vary with frequency; without it such a file is written with a nominal R 50, a warning, and each port’s actual reference in the header.

Warns when a port that is exported carries propagating modes that the export leaves out: the file then looks like a complete N-port while the mode conversion at that port is missing from it.

The .sNp extension must agree with the number of exported channels — Touchstone records the port count nowhere else — so a mismatch raises instead of writing an unreadable file. A path without an extension gets the matching one.

Parameters:
  • path (str or pathlib.Path) – Output file. <name>.s{N}p, or <name> to have the extension filled in.

  • channels (sequence of str or (str, int), optional) – Select the exported sub-network explicitly, e.g. ["port1", "port3"] to cut a two-port out of a fully excited three-port. A bare port name means mode 0. Every entry must have been excited.

  • z_ref (float or dict, optional) – Renormalise to this reference before writing, as in renormalize().

Return type:

None

watch(interval=2.0, *, on_change=None, timeout=None)#

Follow this project while another process writes it.

Polls the store every interval seconds and reports each change — a run starting or ending, a new energy sample, a status change — until the project is finished (done, aborted or stale) or timeout seconds have passed. The first report comes at once, so the current state is always delivered, and so is the final one.

Without on_change this is a generator yielding the project itself at every change:

for proj in mio.open_project("magic_tee").watch():
    print(proj)                     # the run table, as it moves

Print (or plot) what you want to see: inside a loop a bare expression such as proj.runs["port1_mode0"].energy_db displays nothing, in a notebook as anywhere else — only the last expression of a cell is shown. For the ready-made display that replaces itself at every change, see follow(); for a panel that keeps moving while other cells run, monitor().

With on_change the loop runs here: the callable is called with the project at every change, and the project is returned when the loop ends:

mio.open_project("magic_tee").watch(on_change=lambda p: p.plot_energy())
Parameters:
  • interval (float, default 2.0) – Seconds between two looks at the store.

  • on_change (callable, optional) – on_change(project) at every change, instead of yielding.

  • timeout (float, optional) – Give up after this many seconds; the generator (or the call) then ends with the project in whatever state it is — check status to tell a finished project from one still marching.

Returns:

The generator form without on_change; the project itself with it.

Return type:

generator, or Project

property channels: tuple#

Observed (port_name, mode_idx) pairs, in S-matrix order.

property eigenmodes#

The stored EigenmodeResult, or None if absent (lazy).

property elapsed: float | None#

Wall time of the marching [s], summed over the runs and their resumes.

property excitations: tuple#

Excited (port_name, mode_idx) pairs — the S-matrix columns stored.

property f_axis: ndarray#

Frequency axis of the stored S-matrix [Hz], ascending.

property finished#

Wall-clock end (UTC datetime) of the last march; None while one marches.

property geometry#

The reconstructed geometry (LoadedGeometry) or None.

Requires OCC. Returns None if the project carries no geometry (geometry.brep absent).

property grid#

The mesh grid (shortcut for project.mesh.grid).

property mesh#

The reconstructed Mesh (lazy).

property meta: dict#

The parsed project.json contents.

Re-read from disk whenever the file changed, for as long as the project is not finished — so a project opened while a solver writes it shows the current state without refresh(). The file is small and replaced atomically by the writer, so the check is one stat per access and a parse only on a change. Once the stored status is terminal the parsed copy is kept.

property monitors: dict#

Monitor readers of the project’s sole run.

Convenience for the common single-excitation case (project.monitors["Ez_plane"].plot(t=…)) — resolves time, flux, frequency and wall-loss monitors by name (see monitors_for()). With more than one run the monitor name alone is ambiguous — use monitors_for() with the excited pair.

property params: dict#

Free-form user parameters stored with the project.

Whatever dict the analysis was given as params= — design variables, sweep coordinates, notes. Empty when none were stored.

property reference_signal#

Excitation waveform of the longest run (see ScatteringTDResult).

property runs: _RunIndex#

The project’s runs by name, each a Run.

A read-only mapping: project.runs["port1_mode0"] is the run object, list(project.runs) the names, and the mapping itself prints as a table of every run’s state. Run states: pending (planned by the analysis, not started — no run directory on disk yet) → running → done / aborted; a running run whose writer process no longer exists reads as stale.

property s_params#

The full S-matrix, derived on read from the stored signals.

property settings: RunSettings#

Settings the stored run was produced with (result contract).

Filled from the stored recipe and run data; entries the store does not (yet) record are None.

property setup: dict#

Analysis setup metadata stored at creation time.

property signals: dict#

{excited_key: {(port, mode): (V, I)}} across all runs.

property started#

Wall-clock start (UTC datetime) of the first march, or None.

property status: str#

created → running → done / aborted / stale.

done means every planned run is done: the analysis pre-registers its runs as pending, so the status holds at running in the gaps between sequential runs. aborted means a run ended on a graceful stop or an error and the analysis went with it — resume it, or run the analysis again. stale is not stored: it is running on disk with nobody writing — the solver process recorded on the project is gone from this host. A project written on another host cannot be told stale and reads running until its writer finishes.

Type:

Project status

class magnelio.io.ProjectStore(path)#

Write-once model store for a project directory.

Persists the static model — geometry (BREP + VTM), mesh, and setup metadata. Streamed time-domain results and per-run resume checkpoints are added by later work packages; ProjectStore holds the directory handle they will attach to.

Create a store with create(); read one back with open_project().

Parameters:

path (str | Path)

classmethod create(path, mesh, *, geometry=None, setup=None, paraview=None, exist_ok=True)#

Create a project directory and write the static model.

Parameters:
  • path (str or Path) – Project directory (created if absent).

  • mesh (Mesh) – The simulation mesh (grid, materials, conformal sub-cell data) — written to mesh.h5.

  • geometry (GeometryModel or list, optional) – Source geometry. When given, an exact geometry.brep is written, plus a geometry.json carrying per-shape names and materials in compound order. The tessellated geometry.vtm for ParaView is written by Project.export_paraview().

  • setup (dict, optional) – JSON-serialisable analysis metadata (dt, frequency plan, port/BC/excitation descriptions). Stored under setup in project.json; consumed by later work packages.

  • paraview (bool, optional) – Deprecated and without effect: ParaView files are written on request by Project.export_paraview().

  • exist_ok (bool, default True) – Reuse an existing directory (raise if False and it exists).

Return type:

ProjectStore

mark_analysis_finished(elapsed)#

Close the analysis stamp opened by mark_analysis_started().

Parameters:

elapsed (float)

Return type:

None

mark_analysis_started()#

Stamp the start of an analysis call (run() or resume()).

The per-run stamps cover the marches; this one covers the whole call, setup included, which is what the finished in line reports and what a reader wants to see at the top of a project.

Return type:

None

open_run(run_name, *, excitations, excited, dt, f_axis, channels, port_modes, port_normal_dx, port_line_params, excitation_fns, recorder, port_band=None, port_model='modal', port_reference_scale=None, energy_stop_db=None, port_signal_stop_db=None, total_time_steps=None, taper_signals=False, monitors=None, grid=None)#

Open a live streaming sink for one time-domain run.

Declares the run’s resizable results.h5 streams (HDF5-SWMR), registers the run in project.json as running (replacing a pending pre-registration, see register_planned_runs()), and returns a solver-attachable _RunSink. The solver appends the V/I and energy tails during run(); call _RunSink.close() when the run finishes to flip its state to done. run_name is sanitised for the filesystem. excitations are the run’s Excitation dicts (DD-224), excited the S-matrix column of a scattering channel run (None on a general time-domain run), excitation_fns the [(key, drive), ...] sampled alongside the signals.

energy_stop_db / total_time_steps are recorded in the run index so resume() can default to the run’s original stop criterion — e.g. finish an aborted run to the target it was launched with, without the caller repeating it. monitors + grid declare the field-monitor write-through streams; the sink drains each MonitorFieldTime to disk as the run proceeds.

Parameters:
  • run_name (str)

  • excitations (list)

  • excited (tuple[str, int] | None)

  • dt (float)

  • port_modes (dict)

  • port_normal_dx (dict)

  • port_line_params (dict)

  • port_band (dict | None)

  • port_model (str)

  • port_reference_scale (dict | None)

  • energy_stop_db (float | None)

  • port_signal_stop_db (float | None)

  • total_time_steps (int | None)

  • taper_signals (bool)

Return type:

_RunSink

register_planned_runs(planned)#

Pre-register planned runs as pending in the run index.

planned is an iterable of (run_name, entry) pairs, one per run the caller is about to stream, entry the index payload ({"excited": [port, mode]} for a scattering channel). Registering them up front closes the status gap between sequential runs: without it, finishing run k while run k+1 is not yet in the index made _finalize_run() report status = "done" for a project that was still mid-analysis. A pending entry counts as not-done, so the project status stays "running" until the last planned run finishes.

Existing entries are left untouched (fill-in: a second analysis adding excitations must not clobber done/aborted runs). pending entries carry no run directory on disk; they are replaced wholesale when open_run() starts the run.

Return type:

None

reopen_run(run_name, *, recorder, excitation_fns, dt, n_keep, step_offset, monitors=None, grid=None, monitor_keep=None, flux_keep=None)#

Reopen a run’s streams to append a resumed tail.

Truncates results.h5 back to n_keep (the checkpoint’s committed step count), marks the run running again, and returns a sink whose reference sampling is offset to global step step_offset so the appended tail is phase-aligned with the pre-resume stream. The caller attaches it to the resuming solver exactly like a fresh sink. monitor_keep truncates each field monitor’s stream to its checkpointed snapshot count so the resumed run appends onward consistently.

Parameters:
  • run_name (str)

  • dt (float)

  • n_keep (int)

  • step_offset (int)

  • monitor_keep (dict | None)

  • flux_keep (dict | None)

Return type:

_RunSink

write_eigenmodes(result)#

Persist an EigenmodeResult to eigenmodes.h5.

Eigenmode analysis has no time-marching state, so it produces a one-shot result rather than a streamable/resumable run. The ParaView session is written on request by Project.export_paraview_eigenmodes() (DD-262).

Return type:

None

class magnelio.io.Run(project, name)#

One run of a project: its index entry, its files, its result.

Handed out by project.runs[name]. A live view — every attribute reads the project’s current metadata, so a run that is marching in another process shows its growing step count and energy without any call on your side. pending runs have no directory yet and read as empty.

Parameters:
checkpoint_state()#

The run’s resume checkpoint (see Project.checkpoint_state()).

plot_energy(*, x='time', floor_db=None, ax=None)#

Plot the run’s stored energy in dB below its peak.

The figure the progress line reports, over the whole run, with the run’s energy criterion as a dashed line. x is "time" (nanoseconds) or "step"; the axis runs from ten dB below the criterion (floor_db pins it) to +5 dB; ax draws into existing axes. Returns (fig, ax).

Parameters:
  • x (str)

  • floor_db (float | None)

result()#

The run as a TDResult (see Project.result()).

property dt: float | None#

Solver time step [s].

property elapsed: float | None#

Wall time of the marching [s], resumes summed.

On a run that is still marching, the time since its current march started is added to what earlier marches booked.

property energy_db: float | None#

The latest stored energy in dB below the run’s peak — the progress figure.

property energy_stop_db: float | None#

The energy criterion [dB below peak] the run stops at, if any.

property energy_trace: ndarray#

Stored energy at the solver’s check cadence — a structured array.

Fields step, time [s] and energy [J]; read afresh on every access while the run marches, so it grows as the solver flushes. Empty on a pending run.

property excitations: tuple#

Every (source, mode) that drove the run.

property excited: tuple | None#

The excited (port, mode) of a scattering run; None on a general run.

property finished#

UTC datetime the latest march ended; None while marching.

property has_checkpoint: bool#

Whether a resume checkpoint exists on disk.

property host: str | None#

Host name of the solver that wrote (or writes) the run.

property monitors: dict#

The run’s monitor readers by name (see Project.monitors_for()).

property n_energy_samples: int#

How many energy samples the run has flushed — the cheapest sign of life.

property n_steps: int#

Leapfrog steps committed to disk so far.

The index books the count when a march ends; while a run marches, the step of its latest flushed energy sample stands in, so the count moves with the solver.

property path: Path#

The run’s directory (runs/<name>), whether it exists yet or not.

property pid: int | None#

Process id of the solver that wrote (or writes) the run.

property port_model: str | None#

"modal" or "band" — the port pipeline that wrote the run.

property port_signal_stop_db: float | None#

The port-signal criterion [dB below peak] the run stops at, if any.

property resumed#

UTC datetime the latest resumed march started, or None.

property started#

UTC datetime the first march started, or None.

property state: str#

pending, running, done, aborted — or stale.

stale is running on disk with the recorded solver process gone from this host: the kernel died, or the machine rebooted. Resume the run, or run the analysis again.

property stop_reason: str | None#

energy, port_signal, steps, …

Type:

Why the marching ended

property total_time_steps: int | None#

The fixed step count the run was given, or None for an open-ended run.

magnelio.io.import_brep(path, *, unit, material=None, heal=False, name=None)#

Import the solids of a BREP file.

BREP is the geometry kernel’s native dump: exact, but it records nothing but the geometry — no length unit, no names, no colours. The unit therefore has to be stated, and it has to be right: a file drawn in millimetres and read as meters is a thousand times too big, and nothing in the file says so. Prefer import_step() whenever the CAD system can write STEP.

Parameters:
  • path (str or Path) – The .brep file to read.

  • unit (str or float) – Length unit the file is written in: "m", "cm", "mm", "um", "nm", "in", "mil" — or a number giving the length of one unit in meters.

  • material (Material, optional) – Material for every solid in the file. Omitted, the solids come back as construction bodies (see import_step()).

  • heal (bool) – Repair each solid on import. Off by default: a BREP file comes from the same kernel and is normally already clean.

  • name (str, optional) – Name for the returned Group and the base name of its solids. Defaults to the file name.

Returns:

The imported solids as ImportedSolid members, named <name> for a single solid and <name>_1, <name>_2, … for several.

Return type:

Group

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – If the file cannot be read, or contains no solid.

Examples

from magnelio import Material
from magnelio.io import import_brep

horn = import_brep("horn.brep", unit="mm", material=Material.pec())
magnelio.io.import_pcb(path, materials=None, *, copper=None, name=None)#

Import a printed circuit board from its fabrication data.

The set is the one a layout tool writes for a board house: Gerber files for the copper layers and the board outline, Excellon drill files, and the .gbrjob job file that records the stackup. The job file is required — the Gerber files are flat drawings and say nothing about layer thicknesses or the dielectric, so without it there is no third dimension to build.

Each layer of the stackup arrives as one solid: a copper layer with its real thickness, a dielectric filling the board outline. Plated holes arrive as solid copper barrels joining the layers they run between; unplated holes and slots are cut out of everything they pass through. Solder mask and silkscreen are ignored.

Parameters:
  • path (str or Path) – The .gbrjob job file, or the folder holding the fabrication set (which must contain exactly one job file).

  • materials (Material or dict, optional) – A single Material is given to every solid. A dict maps solid names to materials; keys may use shell wildcards ("via_*"), a literal name beats a wildcard, and every key must match at least one solid, so a typo is reported instead of silently doing nothing. The names are the layer names of the stackup ("F.Cu", "dielectric_1") and "via_1", "via_2", … for the barrels.

  • copper (Material, optional) – Material for every copper layer and every plated barrel. Defaults to a perfect electric conductor. A copper layer is thin against any usable cell size, which the mesher resolves below the cell only for a perfect conductor — give a finite conductivity here only with a mesh fine enough to hold the metal thickness.

  • name (str, optional) – Name for the returned Group. Defaults to the project name in the job file, or the job file’s name.

Returns:

The layers and barrels, each an ImportedSolid carrying its name and material. Add it to a model like any other shape.

Return type:

Group

Raises:
  • FileNotFoundError – If the job file, or a file it names, is missing.

  • ValueError – If the fabrication data is malformed, states no layer thicknesses, or a materials key matches no solid.

Warns:

UserWarning – If the stackup states a loss tangent (which is reported, never modelled), or omits a dielectric constant.

Notes

Copper is placed where the stackup puts it, so the top of the topmost dielectric is at z = 0, the top copper occupies 0 to its thickness, and the stack grows downwards.

A plated hole is modelled as a solid cylinder rather than as a plated wall around a void. The wall is a closed conductor, so the space it encloses carries no field either way.

A copper layer is thin against any usable cell size. The mesher resolves it below the cell — one grid plane, thickness carried in the sub-cell material fractions — for a perfect conductor and only when MeshControl carries a min_cell_size larger than the metal thickness.

Examples

Import a board and give the substrate a material of its own:

from magnelio import GeometryModel, Material
from magnelio.io import import_pcb

board = import_pcb(
    "fabrication/",
    {"dielectric_1": Material(name="RO4350B", epsilon=(3.66,) * 3)},
)
model = GeometryModel().add(board)

Inspect the names before assigning anything:

board = import_pcb("fabrication/")
print([solid.name for solid in board.members()])
magnelio.io.import_step(path, materials=None, *, heal=True, unify=False, name=None)#

Import the solids of a STEP file.

The file’s length unit is read from the file itself, so a model drawn in millimetres arrives at its true size in meters and no conversion is needed on the caller’s side. Assemblies are flattened: every solid comes back where it sits in the assembly, as a member of one Group.

STEP carries no material physics — only names — so materials are assigned here, against the name each solid carries in the file. Names survive a re-export of the same CAD model, so the same call keeps working after the drawing changes.

Parameters:
  • path (str or Path) – The .step / .stp file to read.

  • materials (Material or dict, optional) – A single Material is given to every solid. A dict maps solid names to materials; keys may use shell wildcards ("shield_*"), a literal name beats a wildcard, and "*" acts as a catch-all. Every key must match at least one solid, so a typo is reported instead of silently doing nothing. Solids left unmatched (and every solid if materials is omitted) come back as construction bodies — usable as Boolean operands, rejected by add() until they are given a material.

  • heal (bool) – Repair each solid on import (tolerances, orientation, small topological defects). Cheap and harmless on a clean file; turn it off only to inspect a file exactly as written.

  • unify (bool) – Additionally merge adjacent faces that lie on the same surface. CAD kernels often split one planar face into many; merging them simplifies the solid, at the price of editing its topology. Off by default.

  • name (str, optional) – Name for the returned Group. Defaults to the file name.

Returns:

The imported solids, each an ImportedSolid carrying its name, colour and assigned material. Add it to a model like any other shape — a Group is flattened into its members on insertion.

Return type:

Group

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – If the file cannot be read, contains no solid, or a materials key matches no solid.

Examples

Assign materials by the names the parts carry in the CAD model:

from magnelio import GeometryModel, Material
from magnelio.io import import_step

parts = import_step(
    "connector.step",
    {"pin": Material.pec(), "shell": Material.pec(),
     "insulator": Material(name="PTFE", epsilon=(2.1,) * 3)},
)
model = GeometryModel().add(parts)

Import first, inspect the names, assign afterwards:

parts = import_step("connector.step")
print([s.name for s in parts.members()])
magnelio.io.read_brep(path)#

Read a BREP compound file back into an ordered list of shapes.

Parameters:

path (str or Path) – A .brep file written by write_brep().

Returns:

The compound’s direct children, in write order.

Return type:

list of TopoDS_Shape

magnelio.io.write_brep(shapes, path)#

Write an ordered list of geometry shapes to a BREP compound file.

Parameters:
  • shapes (list) – Geometry shape objects (each exposing _occ_shape()) or raw TopoDS_Shape instances. The write order is preserved and recovered verbatim by read_brep().

  • path (str or Path) – Output .brep file.

Return type:

None