# Simulation

## Circuit-level model hierarchy

SAX simulations expand unmodeled assemblies until they reach cells with declared models.
Model declarations therefore define hierarchy boundaries instead of relying on a fixed
depth.

Coupled resonators declare their own models so the coupling is preserved. Chip
assemblies remain unmodeled and expand to these boundaries and other modeled primitives.

For new components, attach the compact model through `schematic_function` and keep its
`port_order` consistent with the layout ports.

<a id="module-qpdk.simulation"></a>

Simulation utilities: shared layout preparation and solver inputs.

[`prepare_metal_layout()`](#qpdk.simulation.layout.prepare_metal_layout) folds a QPDK mask into
positive metal geometry for a field solver. This package also holds
QPDK-specific inputs for `gplugins.comsol`.

**Ansys AEDT (HFSS, Q3D Extractor, Q2D):** use `gplugins.ansys` directly.
Activate QPDK first (`qpdk.PDK.activate()`); importing `qpdk` does not.
The gplugins classes then pick up `LAYER_STACK` from the active
PDK; pass
`material_properties=qpdk.tech.material_properties` to use QPDK’s material
table instead of the gplugins default:

1. Prepare a component with [`prepare_metal_layout()`](#qpdk.simulation.layout.prepare_metal_layout)
2. Wrap a PyAEDT `Hfss`, `Q3d` or `Q2d` app in `gplugins.ansys.HFSS`,
   `Q3D` or `Q2D` and import the component with `import_component`
3. Configure the setup through PyAEDT, solve, and read the results through the
   gplugins helpers. For a CPW cross-section in Q2D, pass the width and gap from
   `get_cpw_dimensions()` to `Q2D.create_2d_cpw` with
   `substrate_level="Substrate"` and `conductor_level="M1"`.

**Palace workflow:**

Run `uv run --script datasets/plate_capacitor.py` from a checkout. The script
uses gsim’s electrostatic APIs and writes a self-describing dataset for lookup.

**COMSOL workflow:**

The COMSOL models come from `gplugins.comsol`; import them from there.
[`qpdk.simulation.comsol`](#module-qpdk.simulation.comsol) adds only the QPDK-specific inputs:

1. Extract metal polygons and optional feed ports with
   [`qpdk.simulation.comsol.prepare_comsol_layout()`](#qpdk.simulation.comsol.prepare_comsol_layout)
2. Build a model with `gplugins.comsol.COMSOL`, passing
   [`SILICON_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.SILICON_RELATIVE_PERMITTIVITY) and
   [`AIR_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.AIR_RELATIVE_PERMITTIVITY) for the sheet
   dielectrics, then add a study and mesh it with its methods

#### NOTE
`gplugins.ansys` requires `uv sync --extra hfss`. The COMSOL module
requires `uv sync --extra comsol` and, to build and solve models, a local
COMSOL installation. It is not imported by this package, so
[`qpdk.simulation`](#module-qpdk.simulation) stays importable without the extra.

The VACASK netlist classes, runner and raw-file reader need only the base
install and the external `vacask` binary. The model-conversion helpers,
[`vector_fit()`](#qpdk.simulation.vacask.vector_fit) and
[`cpw_tline_params()`](#qpdk.simulation.vacask.cpw_tline_params), also need
`uv sync --extra models`.

**VACASK workflow:**

1. Write the circuit and its analyses in VACASK’s own netlist language and
   pass the blocks to a [`Netlist`](#qpdk.simulation.vacask.Netlist), which fills
   them into a Jinja2 template. Loading `josephson_junction.va` or
   `squid.va` uses the models shipped with qpdk.
2. Run it with [`run()`](#qpdk.simulation.vacask.Vacask.run), which calls the
   external `vacask` binary and returns one
   [`RawPlot`](#qpdk.simulation.vacask.RawPlot) per analysis.
3. Use [`run_record()`](#qpdk.simulation.vacask.Vacask.run_record) with a work directory
   to retain decks, logs, raw results and input/binary/compiler hashes.
4. Optionally embed any linear SAX model as a subcircuit: fit it with
   [`vector_fit()`](#qpdk.simulation.vacask.vector_fit) and emit the fit with
   [`vector_fit_subckt()`](#qpdk.simulation.vacask.vector_fit_subckt).

### Example

```pycon
>>> from ansys.aedt.core import Hfss
>>> from gplugins.ansys import HFSS
>>> from qpdk import PDK
>>> from qpdk.cells import resonator
>>> from qpdk.simulation.layout import prepare_metal_layout
>>> from qpdk.tech import material_properties
>>> PDK.activate()
>>> comp = prepare_metal_layout(resonator(length=4000, meanders=4))
>>> hfss_app = Hfss(project="resonator_sim", solution_type="Eigenmode")
>>> hfss_sim = HFSS(hfss_app, material_properties=material_properties)
>>> hfss_sim.import_component(comp)
```

### References

- PyAEDT documentation: [https://aedt.docs.pyansys.com/](https://aedt.docs.pyansys.com/)
- HFSS import_gds_3d: [https://aedt.docs.pyansys.com/version/stable/API/_autosummary/ansys.aedt.core.hfss.Hfss.import_gds_3d.html](https://aedt.docs.pyansys.com/version/stable/API/_autosummary/ansys.aedt.core.hfss.Hfss.import_gds_3d.html)
- Q3D Extractor: [https://aedt.docs.pyansys.com/version/stable/API/_autosummary/ansys.aedt.core.q3d.Q3d.html](https://aedt.docs.pyansys.com/version/stable/API/_autosummary/ansys.aedt.core.q3d.Q3d.html)
- MPh: [https://github.com/MPh-py/MPh](https://github.com/MPh-py/MPh)

## Common

Neutral geometry preparation shared by the AEDT and COMSOL exporters.

This module is pure gdsfactory geometry: it turns a QPDK mask into the physical
metal a field solver actually sees. It has no PyAEDT, MPh, or COMSOL imports so
both exporters can depend on it.

### qpdk.simulation.layout.prepare_metal_layout(component, margin_draw=0.0, margin_etch=0.0, , name=None)

Fold a QPDK mask into positive metal geometry for a field solver.

Applies the additive metals to the etch layer, inverts the mask polarity
around the component bounding box, grows the ground by `margin_draw`,
drops the etch layers, and re-adds the original ports.

* **Parameters:**
  * **component** (*Component*) – The component to prepare.
  * **margin_draw** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Margin in µm added to the M1/M2 draw layers.
  * **margin_etch** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Margin in µm added to the M1/M2 etch layers before
    inversion.
  * **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *None*) – Name for the initial preparation cell. Defaults to
    `f"{component.name}_metal"`.
* **Returns:**
  A copy of the component prepared for simulation.
* **Return type:**
  Component

## Ansys HFSS, Q3D and Q2D

QPDK does not wrap Ansys AEDT. Use `gplugins.ansys` (installed by the `hfss`
extra) directly: its `HFSS`, `Q3D` and `Q2D` classes default to the active PDK’s
layer stack, which is QPDK’s `LAYER_STACK` once QPDK is active. Pass
`material_properties=qpdk.tech.material_properties` to use QPDK’s material table, and
prepare components with [`prepare_metal_layout()`](#qpdk.simulation.layout.prepare_metal_layout). The HFSS
notebooks show the full workflow.

## VACASK

The external simulator runs [Lumped Josephson parametric amplifiers with VACASK](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/vacask_lumped_parametric_amplifier.html.md) and
[CPW parametric amplifiers: layout and circuit models](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/vacask_cpw_parametric_amplifier.html.md) with released VACASK. Native
cold-network and subprocess checks also use the release. The TWPA notebook,
[Josephson traveling-wave parametric amplifiers with JosephsonCircuits.jl](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/josephsoncircuits_twpa.html.md), uses JosephsonCircuits.jl instead.

<a id="module-qpdk.simulation.vacask"></a>

Run the VACASK circuit simulator as a subprocess and read its results.

[VACASK](https://codeberg.org/arpadbuermen/VACASK) is an analog circuit
simulator with multi-tone harmonic balance (`hb`) and small-signal analysis
around a pumped operating point (`hbac`). Devices are written in Verilog-A
and compiled on the fly by OpenVAF-Reloaded (`openvaf-r`).

VACASK is AGPL-3.0 licensed. This package only calls the `vacask` binary as a
separate process and parses the SPICE raw files it writes; it does not import,
link or copy any VACASK code.

Circuits and analyses are written directly in VACASK’s netlist syntax. The
package only adds what is awkward to do by hand:

- [`Netlist`](#qpdk.simulation.vacask.Netlist) fills raw VACASK text into a Jinja2 template and copies the
  Verilog-A models shipped with qpdk next to it.
- [`Vacask`](#qpdk.simulation.vacask.Vacask) runs a netlist and returns one [`RawPlot`](#qpdk.simulation.vacask.RawPlot) per analysis.
- [`RawFile`](#qpdk.simulation.vacask.RawFile) reads the SPICE raw files that VACASK writes.
- [`vector_fit()`](#qpdk.simulation.vacask.vector_fit) and [`vector_fit_subckt()`](#qpdk.simulation.vacask.vector_fit_subckt) turn S-parameters into a
  VACASK subcircuit. They need the `models` extra.

### Example

```pycon
>>> from qpdk.simulation.vacask import Netlist, Vacask
>>> netlist = Netlist(
...     "JJ resonance",
...     loads=["josephson_junction.va", "capacitor.osdi"],
...     circuit='''
...         model isource isource
...         model capacitor capacitor
...         model jj jj
...         iin (0 1) isource dc=0 mag=1n
...         j1 (1 0) jj ic=1u
...         c1 (1 0) capacitor c=2p
...     ''',
...     control='analysis ac1 ac from=1G to=10G mode="lin" points=1000',
... )
>>> results = Vacask.find().run(netlist)
>>> results["ac1"]["1"]  # complex node voltage
```

### *class* qpdk.simulation.vacask.Netlist(title, circuit='', control='', loads=(), subckts=(), files=<factory>)

A VACASK netlist, rendered from `vacask_templates/netlist.sim.j2`.

`circuit`, `control` and `subckts` are VACASK netlist text and are
inserted as-is, after removing their common indentation. The control block always
starts with `abort always`, so that a failed analysis gives a nonzero
exit code.

* **Parameters:**
  * **title** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **circuit** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **control** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **loads** ([*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
  * **subckts** ([*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
  * **files** ([*Mapping*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *]*)

#### title

First line of the netlist.

* **Type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

#### circuit

Models and instances, in VACASK syntax.

* **Type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

#### control

Contents of the `control` block: options, sweeps and
analyses. Each `analysis <name> ...` line produces a result.

* **Type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

#### loads

Files to load, e.g. `"capacitor.osdi"` from the VACASK
installation or `"josephson_junction.va"`. Verilog-A models
shipped with qpdk are copied next to the netlist automatically.

* **Type:**
  [*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### subckts

Subcircuit definitions, e.g. from [`vector_fit_subckt()`](#qpdk.simulation.vacask.vector_fit_subckt).
Each one is dedented on its own, so they can come from different
sources.

* **Type:**
  [*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### files

Extra files to place next to the netlist, as `{name: content}`.
A [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) value is copied; a string is written as text.

* **Type:**
  [*Mapping*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) | [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

#### all_files()

Files to write next to the netlist, including loaded qpdk models.

* **Return type:**
  [*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) | [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

#### *property* analyses *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Names of the analyses in [`control`](#qpdk.simulation.vacask.Netlist.control), in order.

#### render()

Return the netlist text.

* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* qpdk.simulation.vacask.RawFile(path)

A SPICE raw file (binary or ASCII) as written by VACASK.

The file may hold several plots back to back. Binary data is little-endian
`float64` (`complex128` for complex plots), stored point-major.

#### path

Path of the file.

#### plots

The plots in file order.

* **Raises:**
  [**VacaskError**](#qpdk.simulation.vacask.VacaskError) – If the file is truncated or malformed.
* **Parameters:**
  **path** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *Path*)

### *class* qpdk.simulation.vacask.RawPlot(title, plotname, names, data)

One plot of a SPICE raw file.

* **Parameters:**
  * **title** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **plotname** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **names** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,*  *...* *]*)
  * **data** ([*ndarray*](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html#numpy.ndarray))

#### title

Circuit title.

* **Type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

#### plotname

Analysis description written by the simulator.

* **Type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

#### names

Variable names, in column order.

* **Type:**
  [*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), …]

#### data

Array of shape `(points, variables)`; complex for complex plots.

* **Type:**
  [*ndarray*](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html#numpy.ndarray)

#### split(name)

Split a swept plot into one plot per value of the sweep variable `name`.

A new group starts wherever the sweep column changes value.

* **Returns:**
  One plot per sweep point, in order.
* **Parameters:**
  **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)[[*RawPlot*](#qpdk.simulation.vacask.RawPlot)]

### *class* qpdk.simulation.vacask.SimulationResult(, plots, output, elapsed, binary_sha256)

Plots and diagnostics from one simulator invocation.

With an explicit workdir, the deck, model files, raw results, `vacask.log`
and `run.json` remain available after success or failure. A successful
process exit does not establish harmonic convergence or pump stability.

* **Parameters:**
  * **plots** ([*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*RawPlot*](#qpdk.simulation.vacask.RawPlot) *]*)
  * **output** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **elapsed** ([*float*](https://docs.python.org/3/builtins/functions.html#float))
  * **binary_sha256** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))

### *class* qpdk.simulation.vacask.Vacask(binary, openvaf=None)

The VACASK simulator, run as a separate process.

* **Parameters:**
  * **binary** ([*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path))
  * **openvaf** ([*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* *None*)

#### binary

Path to the `vacask` binary.

* **Type:**
  [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

#### openvaf

Path to the Verilog-A compiler, passed to VACASK as
`SIM_OPENVAF` unless that is already set.

* **Type:**
  [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | *None*

#### *classmethod* find()

Locate VACASK and OpenVAF.

Checks the `VACASK` and `QPDK_VACASK` environment variables, then
`PATH`, for `vacask`, and `OPENVAF` then `PATH` for `openvaf-r`.

* **Returns:**
  A runner for the binaries found.
* **Raises:**
  [**VacaskError**](#qpdk.simulation.vacask.VacaskError) – If no `vacask` binary is found.
* **Return type:**
  [*Self*](https://docs.python.org/3/library/typing.html#typing.Self)

#### run(netlist, , workdir=None, timeout=None)

Run a netlist and return the results of every analysis.

The netlist and its files are written to a scratch directory, which is
also where VACASK compiles Verilog-A sources.

* **Parameters:**
  * **netlist** ([*Netlist*](#qpdk.simulation.vacask.Netlist)) – The circuit and analyses.
  * **workdir** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* *None*) – Directory to run in. Defaults to a temporary directory that
    is removed afterwards.
  * **timeout** ([*float*](https://docs.python.org/3/builtins/functions.html#float) *|* *None*) – Timeout in seconds.
* **Returns:**
  `{analysis name: plot}`, read from `<analysis name>.raw`. Swept
  analyses hold all sweep points; see [`RawPlot.split()`](#qpdk.simulation.vacask.RawPlot.split).
* **Return type:**
  [*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), [*RawPlot*](#qpdk.simulation.vacask.RawPlot)]

See [`run_record()`](#qpdk.simulation.vacask.Vacask.run_record) for diagnostics and failure conditions.

#### run_record(netlist, , workdir=None, timeout=None)

Run a netlist with captured diagnostics and optional retained artifacts.

Expected raw outputs from a previous invocation are removed before the
process starts, so a reused directory cannot supply stale results.
`run.json` records input and binary hashes, elapsed seconds and exit
status. Compiler overrides and module paths are recorded from the
effective environment; they can refer to external, unbundled files.

* **Returns:**
  Plots, captured output, elapsed seconds and the simulator hash.
* **Raises:**
  [**VacaskError**](#qpdk.simulation.vacask.VacaskError) – If analyses have duplicate or invalid names, the
      simulator cannot start, fails or times out, or an output is missing.
* **Parameters:**
  * **netlist** ([*Netlist*](#qpdk.simulation.vacask.Netlist))
  * **workdir** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* *None*)
  * **timeout** ([*float*](https://docs.python.org/3/builtins/functions.html#float) *|* *None*)
* **Return type:**
  [*SimulationResult*](#qpdk.simulation.vacask.SimulationResult)

### *exception* qpdk.simulation.vacask.VacaskError

Raised when VACASK is missing, a simulation fails, or a raw file is malformed.

### qpdk.simulation.vacask.cpw_tline_params(length, cross_section='cpw')

Characteristic impedance and delay of a lossless CPW for `tline_ideal`.

Uses `cpw_parameters()` with zero loss tangent, so the
result matches VACASK’s lossless `tline_ideal` device. Needs the
`models` extra.

* **Parameters:**
  * **length** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Line length in µm.
  * **cross_section** (*CrossSectionSpec*) – CPW cross-section.
* **Returns:**
  characteristic impedance in Ω and propagation delay in s.
* **Return type:**
  `(z0, td)`
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the `models` extra is not installed.

### qpdk.simulation.vacask.format_value(value)

Format a Python value as a VACASK parameter value.

Useful for writing long vectors into netlist text, e.g.
`f"values={format_value(frequencies)}"`. Strings are quoted, numbers use
the shortest exact representation, sequences become vectors `[a, b]` and
sequences of sequences become lists of vectors `{[a],[b]}`, as used by
`spur` and `outspur`.

* **Returns:**
  The value as VACASK netlist text.
* **Raises:**
  [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – If the value has no VACASK representation.
* **Parameters:**
  **value** ([*Any*](https://docs.python.org/3/library/typing.html#typing.Any))
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

### qpdk.simulation.vacask.vector_fit(sdict, f, , ports=None, z0=50.0, n_poles_real=1, n_poles_cmplx=10, enforce_passivity=True, n_samples=1000)

Fit S-parameters with `skrf.vectorFitting.VectorFitting`.

A thin wrapper that builds the `skrf.Network` from an SDict and
checks that the result can be used as a circuit: every pole stable and the
model passive. Evaluate the fit with
`get_model_response()` and turn it
into a VACASK subcircuit with [`vector_fit_subckt()`](#qpdk.simulation.vacask.vector_fit_subckt).

* **Parameters:**
  * **sdict** (*Mapping* *[*[*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]* *,* *ArrayLike* *]*) – S-parameters on `f`, referenced to `z0`. Missing entries
    of a sparse SDict are zero.
  * **f** (*ArrayLike*) – Frequencies in Hz. Cover every frequency the circuit will see,
    e.g. up to `nharm` times the pump frequency for harmonic balance.
  * **ports** (*Sequence* *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*  *|* *None*) – Port order. Defaults to the order in which `sdict` names them.
    Stored as the network’s `port_names`.
  * **z0** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Reference impedance in Ω.
  * **n_poles_real** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – Number of initial real poles.
  * **n_poles_cmplx** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – Number of initial complex-conjugate pole pairs.
  * **enforce_passivity** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – Make the model passive if the fit is not.
  * **n_samples** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – Frequency samples for the passivity enforcement.
* **Returns:**
  The fitted `VectorFitting` object.
* **Raises:**
  * [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the `models` extra is not installed.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If a port is missing from `sdict`, the fit has an
        unstable pole, or it is still not passive after enforcement.
* **Return type:**
  VectorFitting

### qpdk.simulation.vacask.vector_fit_subckt(vf, name)

Emit a vector fit as a VACASK subcircuit.

The fitted S-matrix is

$$
S(s) = D + \sum_k \frac{R_k}{s - p_k},
$$

where each complex pole stands for itself and its conjugate.

Each port is a resistor $Z_0$ to ground in parallel with a current
source $2 b / \sqrt{Z_0}$, which gives the port voltage
$\sqrt{Z_0}(a + b)$. The incident waves $a$ drive one
first-order state per real pole and two per complex pair, and the
reflected waves $b$ are weighted sums of the states. Waves and
states are node voltages scaled by $\sqrt{Z_0}$; each state
capacitor is $1/|p_k|$, so all conductances and gains are of order
one.

The subcircuit uses the builtin `vccs` and the `resistor` and
`capacitor` models, so the netlist must load `resistor.osdi` and
`capacitor.osdi` and define models of those names.

* **Parameters:**
  * **vf** (*VectorFitting*) – A fit from [`vector_fit()`](#qpdk.simulation.vacask.vector_fit), with a constant term and no
    proportional term, referenced to one real impedance.
  * **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Subcircuit name.
* **Returns:**
  The subcircuit text, with terminals `p1`, `p2`, … in port order.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the fit has a proportional term, an unstable pole, or
      port-dependent or complex reference impedances.
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

## COMSOL

COMSOL models are built with `gplugins.comsol` directly: its `COMSOL` class
subclasses `mph.Model`, holds the layout, and chains the sheet or metal builder,
the RF or electrostatic study, and the mesh helpers. Plotting and result helpers live in
`gplugins.comsol.plotting` and `gplugins.comsol.results`.

[`qpdk.simulation.comsol`](#module-qpdk.simulation.comsol) adds only what is specific to QPDK:
[`prepare_comsol_layout()`](#qpdk.simulation.comsol.prepare_comsol_layout), which inverts the QPDK
`M1_ETCH` mask into metal with a ground margin before the gplugins extraction, and the
technology permittivities [`SILICON_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.SILICON_RELATIVE_PERMITTIVITY)
and [`AIR_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.AIR_RELATIVE_PERMITTIVITY), which are passed
explicitly to `COMSOL.create_sheet`.

The module needs the `comsol` extra, which installs `gplugins[comsol]`; building and
solving a model also needs COMSOL. [`qpdk.simulation`](#module-qpdk.simulation) does not import it, so the
rest of the package works without the extra.

<a id="module-qpdk.simulation.comsol"></a>

QPDK-specific inputs for the COMSOL models of `gplugins.comsol`.

The COMSOL geometry builders, studies, mesh, plotting, and result helpers, and
the chainable `gplugins.comsol.COMSOL` model live in
`gplugins.comsol`; use them from there. This module holds only what depends
on the QPDK technology:

- [`prepare_comsol_layout()`](#qpdk.simulation.comsol.prepare_comsol_layout) validates the M1_ETCH mask, inverts it into
  physical M1_DRAW metal with a ground margin, rejects layers the extraction
  would silently drop, and defaults to the `coupling_o1`/`coupling_o2`
  feeds, before handing the metal to
  `gplugins.comsol.layout.prepare_comsol_layout()`.
- [`SILICON_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.SILICON_RELATIVE_PERMITTIVITY) and [`AIR_RELATIVE_PERMITTIVITY`](#qpdk.simulation.comsol.AIR_RELATIVE_PERMITTIVITY)
  are the QPDK technology permittivities. The gplugins builders default to
  their own values, so pass these explicitly.

#### NOTE
Needs the optional `comsol` extra (`uv sync --extra comsol`), which
installs `gplugins[comsol]`. Building and solving models also needs a
local COMSOL installation.

### Example

```pycon
>>> from gplugins.comsol import COMSOL
>>> from qpdk.simulation.comsol import (
...     AIR_RELATIVE_PERMITTIVITY,
...     SILICON_RELATIVE_PERMITTIVITY,
...     prepare_comsol_layout,
... )
>>> layout = prepare_comsol_layout(component)
>>> model = COMSOL.create_sheet(
...     client,
...     layout,
...     "resonator",
...     silicon_relative_permittivity=SILICON_RELATIVE_PERMITTIVITY,
...     air_relative_permittivity=AIR_RELATIVE_PERMITTIVITY,
... )
```

### qpdk.simulation.comsol.AIR_RELATIVE_PERMITTIVITY *= 1*

Relative permittivity of the air above the chip, from the material of the
QPDK layer stack’s vacuum level.

### qpdk.simulation.comsol.SILICON_RELATIVE_PERMITTIVITY *= 11.45*

Relative permittivity of the silicon substrate, from the material of the
QPDK layer stack’s substrate level.

### qpdk.simulation.comsol.prepare_comsol_layout(component, feed_ports=('coupling_o1', 'coupling_o2'), ground_margin=100.0, , crop_to_feed_ports=False)

Extract M1_DRAW metal polygons and optional feed ports from a component.

The M1_ETCH mask is inverted into positive M1_DRAW metal around the
component bounding box grown by `ground_margin`, with
[`prepare_metal_layout()`](#qpdk.simulation.layout.prepare_metal_layout), and the result is
extracted with `gplugins.comsol.layout.prepare_comsol_layout()`.

* **Parameters:**
  * **component** (*Component*) – The gdsfactory component to extract. It must contain an
    M1_ETCH mask; positive M1_DRAW shapes alone do not define the gaps.
    The original ports are preserved.
  * **feed_ports** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*  *|* *None*) – Names of exactly two distinct ports to expose as feeds, or
    `None` for an unfed layout, in which case the result carries no
    feed ports. `None` fits layouts with no CPW feedline, e.g. an
    eigenmode qubit cell.
  * **ground_margin** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Positive margin in µm added around the component
    bounding box to form the ground plane.
  * **crop_to_feed_ports** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – When `True`, cut the prepared metal back to the
    two feed-port planes so each external face shows an open CPW cross
    section instead of the extended ground. Requires two feed ports
    that are axis-aligned, opposite, outward-facing, and share their
    transverse coordinate (left/right along x, or bottom/top along y).
    Cropping is refused unless every M1_DRAW and M1_ETCH polygon of the
    input component lies between the planes and the etch gaps reach
    both planes, so no resonator metal is cut and no face is shorted.
    The planes snap outwards to the database grid, so the returned
    `bbox` can exceed the port span by less than one database unit.
    Defaults to `False`, which leaves the prepared ground untouched.
* **Returns:**
  A `gplugins.comsol.layout.ComsolLayout` with metal polygons
  (holes preserved), the selected feed ports (empty when `feed_ports`
  is `None`), and the prepared bounding box, all in µm.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `ground_margin` is not positive and finite, if
      `feed_ports` is supplied but does not name two distinct available
      ports, if a feed port is not cardinal or has non-finite coordinates
      or width, if the component has no M1_ETCH mask or carries geometry
      on unsupported fabrication layers, if no M1_DRAW metal remains, or
      if `crop_to_feed_ports` is set but the feeds are not a valid
      opposite pair or the component does not fit between the planes.
* **Return type:**
  ComsolLayout

## FEM datasets

Reusable FEM extraction results, read with Polars and looked up in jittable SAX models.
A dataset is nothing but its files: Parquet parts, or a Delta Lake table, each carrying
the dataset metadata (units, terminals, quantities, provenance) inside it. Requires the
`models` extra. See [FEM Dataset Lookup in SAX Models](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/fem_dataset_lookup.html.md).

Curated datasets ship in `qpdk/models/datasets/data/` as Parquet in Git LFS. From a
checkout, fetch only these with `git lfs pull
--include="qpdk/models/datasets/data/**"`. Wheels on PyPI ship the resolved Parquet
files, so installed users need no Git LFS.

`flipmon_unbonded_model()` returns a three-port SAX model for separate inner, ring and
top electrodes between two silicon chips. The returned model accepts `chip_gap`,
`alignment`, `inner_radius` and `top_radius` in um, and frequency `f` in Hz.
`flipmon_ideal_bond_model(inductance=10e-9)` joins the inner and top electrodes with
an ideal wire and adds a linearized Josephson junction. This reduction omits finite bump
fields and inductance. The generation recipe is
`qpdk/models/datasets/data/flipmon.py`.

`double_pad_transmon_capacitance` looks up the differential pad capacitance in F,
including the two pad-to-ground branches. With junction capacitance $C_\text{J}$,
the charging frequency is $E_\text{C}/h = e^2/[2h(C_\text{diff}+C_\text{J})]$ in
Hz. For a linearized circuit, create `double_pad_transmon_fem_model(inductance=10e-9,
junction_capacitance=2e-15)` and pass geometry dimensions in um to the returned SAX
model. Both ports reference the coupon’s finite ground frame. The dataset omits the
readout, package and Josephson junction; its recipe is
`qpdk/models/datasets/data/double_pad_transmon.py`.

`flipmon_readout_coupon_model()` creates a 5-port static SAX coupon model. Pass
`pad_length`, `pad_offset`, `feedline_gap` and `coupling_length` in um, and
frequency `f` in Hz. The whole readout conductor shares one potential, and feedline
reference cuts are open. This dataset requires RF validation before replacing a
distributed resonator circuit. Its recipe is
`qpdk/models/datasets/data/flipmon_readout_assembly.py`.

Results can also live outside Git, in a [Delta Lake](https://delta.io) table on a
cloud bucket (`gs://`, `s3://`, `az://`) or a local path, optionally pinned to a
table version. Credentials are passed when opening the dataset and never stored in it.
Requires the `delta` extra.

```python
from qpdk.models.datasets import Dataset

options = {"google_service_account": "key.json"}
dataset = Dataset("gs://my-bucket/plate_capacitor", delta=True, storage_options=options)
dataset.append(new_rows)  # one atomic Delta commit

pinned = Dataset(
    "gs://my-bucket/plate_capacitor", delta=True, version=3, storage_options=options
)
```

### Generating Palace datasets

The bundled `plate_capacitor_palace` and `cpw_coupling_palace` datasets contain real
Palace electrostatic extractions. Their standalone scripts in
`qpdk/models/datasets/data/` define the grid, settings, metadata and solver function.
Inline dependencies select the Python 3.12 environment required by gsim; generation
scripts are excluded from the installed package.

Preview or run the bundled 27-point sweep from a checkout:

```default
uv run --script qpdk/models/datasets/data/plate_capacitor.py --dry-run
uv run --script qpdk/models/datasets/data/plate_capacitor.py --processes 4
uv run --script qpdk/models/datasets/data/plate_capacitor.py --sif /path/to/palace.sif
```

Copy the script beside the original in `qpdk/models/datasets/data/` and edit `GRID`
and `SETTINGS` for another experiment. Meshwell meshes labelled conductor sheets and
the surrounding air and silicon volumes. The shared experiment helper uses gsim to write
the Palace configuration, run `ElectrostaticSim` and load capacitance matrices. Lookup
models need none of these simulation dependencies.

Each geometry retains its inputs, mesh, config, solver log and results under
`--workdir`. Rerunning reuses matching completed solves. Extend the grid while
retaining the work directory to solve only new points. `--output` is replaced only
after the sweep succeeds. The output Parquet files contain all dataset metadata, so
lookups need no generator or solver. Container runs fingerprint the complete SIF; native
runs fingerprint only the supplied executable.

Set `save_fields=True` for ParaView outputs. Compare representative geometries at
finer `near_mesh` sizes and larger `domain_pad` before relying on small differences.
Every solve stores `solver_relative_residual`, `solver_iterations` and
`fem_error_indicator_norm` alongside its capacitance. Missing diagnostics and
unconverged terminal solves prevent publication. The FEM indicator is Palace’s
energy-normalized recovered-flux estimate, not a capacitance error bound. Scan and rank
it to select refinement candidates. The CPW script also refines every geometry until all
raw matrix entries change by at most `--mesh-tolerance` (default 1%) and the identical
traces agree within 1%. It stores `mesh_relative_change`, `mesh_refinement_level`
and the accepted `mesh_near_size` / `mesh_far_size` in SI units. Exhausting
`--max-refinements` blocks publication. This measures mesh sensitivity; compare larger
domains separately. Check interpolation with held-out geometries separately. The
notebook demonstrates a lazy diagnostic scan and outlier flag. The plate example has a
separate ground frame. The CPW example is a uniform slice of two identical traces with
outer ground rails, sweeping trace width, outer slot width and inter-trace gap. Use
Palace 0.17 or newer for its two-dimensional transverse mesh. `Model.Lc` sets the
implicit depth to `slice_length_um`, so the stored capacitances remain in F. Divide by
the depth in meters for capacitance per length. Select `--topology as-drawn` to retain
the ground strip of width `max(gap - 2 * cpw_gap, 0)` left between the component’s CPW
etch masks. This produces `cpw_coupling_ground_strip_palace`, used by
`coupler_straight` and the default distributed model. The default `fully-etched`
experiment produces `cpw_coupling_palace` for comparison with the unshielded
analytical ECCPW formula. The two geometries agree when the CPW slots touch or overlap.
Above that gap, their difference measures the shielding from the intervening ground
strip. The grounded sweep uses `ground_strip_width = gap - 2 * cpw_gap` as its third
axis, with logarithmic sampling from 0.01 to 248 µm. The CPW models interpolate
coordinates and capacitance magnitudes logarithmically in this regime and use the fully
etched grid where the slots touch or overlap. Positive strips narrower than the sampled
minimum return NaN. `cpw_coupling_model()` converts its capacitance lookup into a
jittable four-port quasi-TEM SAX model; see the notebook for geometry heatmaps and
S-parameters.

Both scripts support disjoint rectangular shards with `--shard INDEX --shards COUNT`.
Use separate output and work directories for each worker, then merge after all succeed.
For the smaller CPW development grid, use two workers with the generic Slurm helper:

```default
generator=qpdk/models/datasets/data/cpw_coupling.py
sbatch --array=0-1 --cpus-per-task=4 qpdk/models/datasets/data/slurm_array.sh "$generator" build/cpw-shards build/cpw-runs --sif /path/to/palace.sif
shards=()
for i in {0..1}; do shards+=(--merge-shards "build/cpw-shards/shard-$i"); done
uv run --script "$generator" "${shards[@]}" --output build/datasets/cpw_coupling_palace
```

Supply your scheduler’s partition and resource options to `sbatch`. Merging checks
matching provenance, successful results and exact coverage before publishing. Keep the
experiment, runtime and array size fixed when resuming.

<a id="module-qpdk.models.datasets"></a>

Reusable FEM extraction datasets and their JAX lookup.

A dataset is a directory of Parquet parts (Git LFS for curated datasets) or a
Delta Lake table, with its `DatasetMetadata` stored inside the files. See
[`qpdk.models.datasets.table`](#module-qpdk.models.datasets.table) for the table layout, [`qpdk.models.datasets.generate`](#module-qpdk.models.datasets.generate)
for producing one, and [`qpdk.models.datasets.store`](#module-qpdk.models.datasets.store) for where it lives.

Requires the `models` extra; Delta Lake also needs the `delta` extra.

<a id="module-qpdk.models.datasets.metadata"></a>

Metadata describing the physical meaning of a FEM extraction dataset.

The metadata travels inside the results files, so a dataset is nothing but its
Parquet files (or Delta table): each Parquet part stores it as JSON under the
key-value metadata key [`METADATA_KEY`](#qpdk.models.datasets.metadata.METADATA_KEY), and a Delta table stores it as
metadata of the `value` column. Parameter grids are not repeated here; they
are read from the data.

The metadata holds what a row of the table cannot carry on its own: the units
of the axes, the meaning of each quantity, the terminal order and reference
ground of matrix quantities, and free-form provenance (recipe, geometry, layer
stack, solver, accuracy).

### *class* qpdk.models.datasets.metadata.Axis(, name, unit, validated=None)

A continuous parameter axis; its grid values come from the data.

* **Parameters:**
  * **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **unit** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **validated** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*float*](https://docs.python.org/3/builtins/functions.html#float) *,* [*float*](https://docs.python.org/3/builtins/functions.html#float) *]*  *|* *None*)

#### model_config *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

### *class* qpdk.models.datasets.metadata.DatasetMetadata(\*, name, description='', synthetic=False, axes, variants=(), quantities, terminals=(), reference_ground='ground', provenance=<factory>, schema_version=1)

Metadata stored inside the results files of a dataset.

* **Parameters:**
  * **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Dataset name.
  * **description** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – One-line description.
  * **synthetic** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – True when the values are not real solver output; such data
    must not be used for design.
  * **axes** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*Axis*](#qpdk.models.datasets.metadata.Axis) *,*  *...* *]*) – Continuous parameters, in tensor order.
  * **variants** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,*  *...* *]*) – Discrete parameters (string columns), selected and never
    interpolated.
  * **quantities** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*Quantity*](#qpdk.models.datasets.metadata.Quantity) *,*  *...* *]*) – Stored quantities.
  * **terminals** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,*  *...* *]*) – Ordered terminal labels; defines matrix row and column order.
  * **reference_ground** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Conductor all potentials are referenced to; not a
    matrix terminal.
  * **provenance** ([*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*Any*](https://docs.python.org/3/library/typing.html#typing.Any) *]*) – Free-form recipe, geometry, layer stack, solver, and accuracy
    information.
  * **schema_version** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – Schema version; must equal [`SCHEMA_VERSION`](#qpdk.models.datasets.metadata.SCHEMA_VERSION).

#### *classmethod* from_json(text)

Parse and validate metadata read from a file.

* **Returns:**
  The validated metadata.
* **Parameters:**
  **text** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  [*Self*](https://docs.python.org/3/library/typing.html#typing.Self)

#### model_config *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

#### quantity(name)

Return the quantity called `name`.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – if there is no such quantity.
* **Parameters:**
  **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  [*Quantity*](#qpdk.models.datasets.metadata.Quantity)

#### to_json()

Serialize for the file metadata.

* **Returns:**
  Compact JSON.
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

### qpdk.models.datasets.metadata.METADATA_KEY *= 'qpdk.dataset'*

Parquet key-value (and Delta column) metadata key holding the JSON metadata.

### *class* qpdk.models.datasets.metadata.Quantity(, name, kind, unit, complex=False)

A quantity stored in the results table.

Every kind except [`QuantityKind.CIRCUIT_PARAMETER`](#qpdk.models.datasets.metadata.QuantityKind.CIRCUIT_PARAMETER) is a matrix over the
dataset terminals.

* **Parameters:**
  * **name** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **kind** ([*QuantityKind*](#qpdk.models.datasets.metadata.QuantityKind))
  * **unit** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **complex** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool))

#### *property* matrix *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether rows carry `row`/`col` terminals.

#### model_config *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

### *class* qpdk.models.datasets.metadata.QuantityKind(\*values)

Physical meaning of an extracted quantity.

The Maxwell capacitance matrix, the mutual (SPICE-style) capacitance matrix,
and reduced circuit parameters are different quantities and are never mixed.

#### CIRCUIT_PARAMETER *= 'circuit_parameter'*

Scalar circuit parameter or numerical diagnostic, e.g. coupling capacitance or a solver residual.

#### INDUCTANCE *= 'inductance'*

Inductance matrix between terminal loops.

#### MAXWELL_CAPACITANCE *= 'maxwell_capacitance'*

positive diagonal, non-positive off-diagonal, w.r.t. the reference ground.

* **Type:**
  Maxwell matrix

#### MUTUAL_CAPACITANCE *= 'mutual_capacitance'*

non-negative branch capacitances, diagonal entries to the reference ground.

* **Type:**
  Mutual matrix

#### S_PARAMETERS *= 's_parameters'*

Complex scattering matrix at the reference planes recorded in the provenance.

### qpdk.models.datasets.metadata.RESERVED_COLUMNS *= ('run_id', 'status', 'quantity', 'row', 'col', 'value', 'value_imag', 'unit')*

Results-table columns that are not sweep parameters.

### qpdk.models.datasets.metadata.SCHEMA_VERSION *= 1*

Version of the metadata and results-table schema implemented here.

<a id="module-qpdk.models.datasets.table"></a>

Polars results table of a FEM extraction dataset.

The table is long-format: one row per parameter point, quantity, and matrix entry.

| Column       | Type    | Meaning                                                                                              |
|--------------|---------|------------------------------------------------------------------------------------------------------|
| `run_id`     | String  | Identity of the solver run that produced the row.                                                    |
| `status`     | String  | `ok`, `failed`, or `not_converged` (see [`RunStatus`](#qpdk.models.datasets.table.RunStatus)). |
| <axis>       | Float64 | One column per metadata axis, in the axis’ unit.                                                     |
| <variant>    | String  | One column per metadata variant.                                                                     |
| `quantity`   | String  | Name of a metadata quantity.                                                                         |
| `row`        | String  | Row terminal label; null for scalar quantities.                                                      |
| `col`        | String  | Column terminal label; null for scalar quantities.                                                   |
| `value`      | Float64 | Value (real part for complex quantities); null unless `ok`.                                          |
| `value_imag` | Float64 | Imaginary part; null for real quantities.                                                            |
| `unit`       | String  | SI unit; must equal the metadata unit of the quantity.                                               |

The [`DatasetMetadata`](#qpdk.models.datasets.metadata.DatasetMetadata) is stored inside the files
(see [`qpdk.models.datasets.store`](#module-qpdk.models.datasets.store)), so a dataset is self-describing and nothing
but its Parquet parts or Delta table.

### qpdk.models.datasets.table.DATASETS_PATH *= PosixPath('/home/runner/work/quantum-rf-pdk/quantum-rf-pdk/qpdk/models/datasets/data')*

Curated datasets shipped with qpdk.

### *class* qpdk.models.datasets.table.Dataset(location, metadata=None, , delta=False, version=None, storage_options=None)

A FEM extraction dataset: Parquet parts or a Delta table with its metadata.

* **Parameters:**
  * **location** (*Path* *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Directory of Parquet parts, the name of a dataset shipped in
    [`DATASETS_PATH`](#qpdk.models.datasets.table.DATASETS_PATH), or with `delta=True` a Delta table path or
    object-store URI such as `gs://bucket/dataset`.
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata)) – Metadata for a new dataset. For an existing one it is read
    from the files and, if given, must match.
  * **delta** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – Store results as a Delta Lake table (`delta` extra).
  * **version** ([*int*](https://docs.python.org/3/builtins/functions.html#int) *|* *None*) – Delta table version to read; a pinned dataset is read-only.
  * **storage_options** ([*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*  *|* *None*) – Object-store options (credentials, endpoints) for a
    Delta URI. Never written to the dataset.
* **Raises:**
  * [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – if nothing is stored yet and no `metadata` is given.
  * [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – if `metadata` differs from the stored metadata.

#### append(frame, , part=None)

Validate `frame` together with the stored results, then store it.

Stored rows are never rewritten. Duplicates against earlier results are
rejected, so re-running a sweep cannot silently double a point. Parquet
parts take one writer at a time; a Delta append is one atomic commit and
the first append creates the table.

* **Returns:**
  a part path, or a Delta URI and version.
* **Return type:**
  What was written
* **Raises:**
  [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – If rows duplicate existing entries or mix run identities.
* **Parameters:**
  * **frame** (*pl.DataFrame*)
  * **part** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *None*)

#### grid(quantity, , allow_missing=False, \*\*variant)

Arrange `quantity` on its grid; see [`to_grid()`](#qpdk.models.datasets.table.to_grid).

* **Returns:**
  The dense grid of `quantity`.
* **Raises:**
  [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – If the selected rows are invalid or variants are ambiguous.
* **Parameters:**
  * **quantity** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **allow_missing** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool))
  * **variant** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  [*Grid*](#qpdk.models.datasets.table.Grid)

#### scan()

Lazily scan the results with the metadata schema.

* **Returns:**
  Lazy frame over all results; not validated.
* **Return type:**
  pl.LazyFrame

#### *property* table *: pl.DataFrame*

The full results table, validated against the metadata.

* **Raises:**
  [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – if the stored results do not match the schema.

### *exception* qpdk.models.datasets.table.DatasetError(dataset, problems)

The dataset is inconsistent with its metadata or cannot be gridded.

* **Parameters:**
  * **dataset** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **problems** ([*list*](https://docs.python.org/3/builtins/stdtypes.html#list) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
* **Return type:**
  *None*

### *class* qpdk.models.datasets.table.Grid(dataset, quantity, axes, coords, terminals, variant, values)

A quantity on a complete rectilinear grid, ready for interpolation.

`values` has shape `(*axis sizes, *component shape)` where the component
shape is `(n_terminals, n_terminals)` for matrix quantities and `()` for
scalars. Axes follow metadata order with ascending `coords`, terminals
follow `terminals`. Missing or
failed points are NaN, never zero.

* **Parameters:**
  * **dataset** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **quantity** ([*Quantity*](#qpdk.models.datasets.metadata.Quantity))
  * **axes** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*Axis*](#qpdk.models.datasets.metadata.Axis) *,*  *...* *]*)
  * **coords** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*ndarray*](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html#numpy.ndarray) *|* [*Array*](https://docs.jax.dev/en/latest/_autosummary/jax.Array.html#jax.Array) *,*  *...* *]*)
  * **terminals** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,*  *...* *]*)
  * **variant** ([*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
  * **values** (*ArrayLike*)

#### *property* axis_names *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Axis names in tensor order.

#### *property* domain *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)]]*

Validated domain of each axis; defaults to the span of its grid values.

### *class* qpdk.models.datasets.table.RunStatus(\*values)

Outcome of a solver run.

### qpdk.models.datasets.table.schema(metadata)

Column schema of the results table for `metadata`.

* **Returns:**
  The Polars schema.
* **Parameters:**
  **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata))
* **Return type:**
  pl.Schema

### qpdk.models.datasets.table.to_grid(frame, metadata, quantity, , allow_missing=False, \*\*variant)

Arrange one quantity of one variant on its complete rectilinear grid.

The grid of each axis is the sorted set of values in the selected rows, and
every combination of them must be present. The layout is deterministic:
axes in metadata order with ascending values, then row and column terminals
in metadata order.

* **Parameters:**
  * **frame** (*pl.DataFrame*) – Validated results table.
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata)) – Dataset metadata.
  * **quantity** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Quantity name.
  * **allow_missing** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool)) – Return NaN for missing or failed points instead of raising.
  * **\*\*variant** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Value for every metadata variant; discrete variants are
    selected explicitly and never interpolated.
* **Returns:**
  The dense grid of `quantity`.
* **Raises:**
  [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – if the selection is ambiguous or empty, a validated domain
      leaves the grid, or the grid is incomplete.
* **Return type:**
  [*Grid*](#qpdk.models.datasets.table.Grid)

### qpdk.models.datasets.table.validate(frame, metadata)

Check a results table against its metadata.

Detects wrong columns, unknown quantities or statuses, unit mismatches,
terminals outside the declared ones, non-finite values, inconsistent runs,
duplicate points, and missing or spurious values. Grid completeness is
checked per selection by [`to_grid()`](#qpdk.models.datasets.table.to_grid).

* **Raises:**
  [**DatasetError**](#qpdk.models.datasets.table.DatasetError) – listing every problem found.
* **Parameters:**
  * **frame** (*pl.DataFrame*)
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata))
* **Return type:**
  *None*

<a id="module-qpdk.models.datasets.generate"></a>

Generate a dataset by sweeping a solver over a rectilinear parameter grid.

Every dataset, synthetic or produced by a FEM solver, is built the same way: a
`solve` function maps one parameter point to its quantities, and
[`sweep()`](#qpdk.models.datasets.generate.sweep) runs it over the grid and lays the results out as the long-format
table of [`qpdk.models.datasets.table`](#module-qpdk.models.datasets.table). [`write()`](#qpdk.models.datasets.generate.write) then stores the table with
its metadata.

### *type* qpdk.models.datasets.generate.Solve *= [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[...], [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Array](https://docs.jax.dev/en/latest/_autosummary/jax.Array.html#jax.Array) | [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html#numpy.ndarray) | [bool](https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.bool) | [number](https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.number) | [bool](https://docs.python.org/3/builtins/functions.html#bool) | [int](https://docs.python.org/3/builtins/functions.html#int) | [float](https://docs.python.org/3/builtins/functions.html#float) | [complex](https://docs.python.org/3/builtins/functions.html#complex) | TypedNdArray] | [None](https://docs.python.org/3/builtins/constants.html#None)]*

Solver for one point: keyword axis and variant values in, quantities out.

Matrix quantities are `(n, n)` arrays in terminal order, scalars are numbers,
and `None` records the run as failed.

### qpdk.models.datasets.generate.merge(parts, output, , grid, variants=None)

Publish independently generated shards after checking the entire sweep.

Workers write separate dataset directories. Only this single coordinator
writes the final directory. Metadata must match, points must occur exactly
once, and every requested point and quantity must have successful results.

* **Returns:**
  The complete published dataset.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If shards are absent, disagree, overlap, fail, or do not
      cover the requested grid and variants.
* **Parameters:**
  * **parts** ([*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
  * **output** ([*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **grid** ([*Mapping*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*float*](https://docs.python.org/3/builtins/functions.html#float) *]* *]*)
  * **variants** ([*Mapping*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]* *]*  *|* *None*)
* **Return type:**
  [*Dataset*](#qpdk.models.datasets.table.Dataset)

### qpdk.models.datasets.generate.partition_grid(grid, , shard, shards)

Select one disjoint rectangular shard of a parameter grid.

Split the longest axis in round-robin order. Each shard remains a complete
Cartesian grid, so it can be generated and checked independently. Use the
same grid and shard count for every worker; shard indices start at zero.
Scheduling does not change geometry, solver inputs or run identities.

* **Returns:**
  A grid for one worker, without modifying the input.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the grid is empty, an axis is empty, or shard settings
      would create an empty worker or an invalid index.
* **Parameters:**
  * **grid** ([*Mapping*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*Sequence*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) *[*[*float*](https://docs.python.org/3/builtins/functions.html#float) *]* *]*)
  * **shard** ([*int*](https://docs.python.org/3/builtins/functions.html#int))
  * **shards** ([*int*](https://docs.python.org/3/builtins/functions.html#int))
* **Return type:**
  [*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict)[[*str*](https://docs.python.org/3/builtins/stdtypes.html#str), [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)[[*float*](https://docs.python.org/3/builtins/functions.html#float)]]

### qpdk.models.datasets.generate.sweep(metadata, solve, grid, variants=None)

Run `solve` at every point of `grid` × `variants`.

Each point gets a `run_id` derived from the dataset name and the point, so
re-running a sweep reproduces the same identities.

* **Parameters:**
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata)) – Dataset metadata; defines the axes, variants, and quantities.
  * **solve** ([*Solve*](#qpdk.models.datasets.generate.Solve)) – Solver for one point.
  * **grid** (*Mapping* *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* *Sequence* *[*[*float*](https://docs.python.org/3/builtins/functions.html#float) *]* *]*) – Values of every axis.
  * **variants** (*Mapping* *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* *Sequence* *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]* *]*  *|* *None*) – Values of every variant.
* **Returns:**
  Long-format results table, not yet validated or stored.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the grid or variants differ from metadata, a quantity has
      the wrong shape, or a real quantity contains imaginary values.
* **Return type:**
  pl.DataFrame

### qpdk.models.datasets.generate.write(location, metadata, frame)

Replace the dataset at `location` with `frame` as one part.

Meant for regenerating a curated dataset from scratch; use
`append()` to grow an existing one. The
new part is written to a sibling staging directory and swapped in only once
it is complete, so a failure leaves the existing dataset untouched.

* **Returns:**
  The written dataset.
* **Raises:**
  * [**FileExistsError**](https://docs.python.org/3/builtins/exceptions.html#FileExistsError) – if `location` holds anything other than dataset parts.
  * [**OSError**](https://docs.python.org/3/builtins/exceptions.html#OSError) – if publishing fails; the existing dataset is then kept.
* **Parameters:**
  * **location** (*Path* *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata))
  * **frame** (*pl.DataFrame*)
* **Return type:**
  [*Dataset*](#qpdk.models.datasets.table.Dataset)

<a id="module-qpdk.models.datasets.store"></a>

Where the results table of a dataset is stored.

The heavy lifting is left to the dependencies: Polars reads and writes Parquet,
including its key-value metadata, and `deltalake` provides atomic, versioned,
concurrent appends on local disks and object stores. The two stores here only
add what those do not: carrying the [`DatasetMetadata`](#qpdk.models.datasets.metadata.DatasetMetadata)
in the files, and a readable error for an unresolved Git LFS pointer.

[`ParquetParts`](#qpdk.models.datasets.store.ParquetParts)
: A local directory of append-only `*.parquet` parts, each carrying the
  metadata in its footer. The default, used for curated datasets in Git LFS.
  One writer at a time; use a Delta table for concurrent writers.

[`DeltaStore`](#qpdk.models.datasets.store.DeltaStore)
: A [Delta Lake](https://delta.io) table at a local path or an object-store
  URI such as `gs://`, `s3://`, or `az://`. Each append is one atomic
  commit and a new table version, so a team can write sweep results straight to
  a cloud bucket and pin a model to an exact version. The metadata is kept on
  the `value` column of the table schema, since Delta tables accept no
  free-form table properties. Requires the `delta` extra.

Credentials are passed as `storage_options`, in the keys Polars and
`deltalake` accept (for example `aws_endpoint_url`,
`google_service_account`, `azure_storage_account_name`), or come from the
environment and application-default credentials of the cloud SDKs.

### *class* qpdk.models.datasets.store.DeltaStore(uri, , storage_options=None, version=None)

Results stored as a Delta Lake table.

Every append is one atomic commit and creates a new table version. Reading a
pinned `version` reproduces exactly the rows a model was built from.

Concurrent appends are committed atomically on local disks, Google Cloud
Storage, and Azure. On S3, pass `{"conditional_put": "etag"}` (S3 and most
S3-compatible stores support it) or configure a `deltalake` locking
provider. Validation runs before each commit and again on every read, so a
duplicate committed by a racing writer is reported rather than silently used.

* **Parameters:**
  * **uri** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *Path*) – Local path or object-store URI of the table, e.g. `gs://bucket/dataset`.
  * **storage_options** ([*dict*](https://docs.python.org/3/builtins/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*  *|* *None*) – Object-store options, passed to Polars and `deltalake`.
  * **version** ([*int*](https://docs.python.org/3/builtins/functions.html#int) *|* *None*) – Table version to read; `None` reads the latest. A pinned store
    is read-only.

#### metadata()

Metadata stored in the table schema, or `None` if there is no table.

* **Returns:**
  The table’s dataset metadata.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if the table has no dataset metadata.
* **Return type:**
  [*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata) | *None*

#### scan(schema)

Scan the table, cast to `schema`.

* **Returns:**
  Lazy frame over the table.
* **Raises:**
  [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – if there is no table.
* **Parameters:**
  **schema** (*pl.Schema*)
* **Return type:**
  pl.LazyFrame

#### *property* table_version *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Version of the table that [`scan()`](#qpdk.models.datasets.store.DeltaStore.scan) reads.

#### write(frame, metadata, part)

Commit `frame` as one append, creating the table if needed.

New tables are partitioned by quantity and variants, which are selected
exactly and never interpolated, and carry `metadata` on the `value`
column. Initial creation is exclusive; retry an append if another
writer creates the table first.

* **Returns:**
  The URI and the committed table version.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if the store is pinned to a version.
* **Parameters:**
  * **frame** (*pl.DataFrame*)
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata))
  * **part** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *None*)
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

### *exception* qpdk.models.datasets.store.LFSPointerError

A results file is an unresolved Git LFS pointer instead of Parquet data.

### *class* qpdk.models.datasets.store.ParquetParts(location)

Local, append-only directory of Parquet parts.

* **Parameters:**
  **location** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *Path*) – Directory of the parts.

#### *property* files *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]*

Part files in a deterministic order.

#### metadata()

Metadata stored in the parts, or `None` if there are none yet.

* **Returns:**
  The metadata shared by every part.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if the parts disagree or a part has no metadata.
* **Return type:**
  [*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata) | *None*

#### scan(schema)

Scan all parts with `schema`.

* **Returns:**
  Lazy frame over every part.
* **Raises:**
  [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – if there are no parts.
* **Parameters:**
  **schema** (*pl.Schema*)
* **Return type:**
  pl.LazyFrame

#### write(frame, metadata, part)

Publish `frame` as a new part without ever replacing an existing one.

* **Returns:**
  Path of the written part.
* **Raises:**
  * [**FileExistsError**](https://docs.python.org/3/builtins/exceptions.html#FileExistsError) – if the part already exists.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if the part is not a filename stem.
* **Parameters:**
  * **frame** (*pl.DataFrame*)
  * **metadata** ([*DatasetMetadata*](#qpdk.models.datasets.metadata.DatasetMetadata))
  * **part** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *|* *None*)
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

<a id="module-qpdk.models.datasets.interpolation"></a>

Jittable N-dimensional lookup of gridded FEM datasets.

Data is loaded, validated, and arranged once, outside tracing (see
`qpdk.models.datasets.Dataset.grid()`). The interpolation itself is
[`jax.scipy.interpolate.RegularGridInterpolator`](https://docs.jax.dev/en/latest/_autosummary/jax.scipy.interpolate.RegularGridInterpolator.html#jax.scipy.interpolate.RegularGridInterpolator), so calling the lookup
under [`jax.jit()`](https://docs.jax.dev/en/latest/_autosummary/jax.jit.html#jax.jit), [`jax.vmap()`](https://docs.jax.dev/en/latest/_autosummary/jax.vmap.html#jax.vmap), or [`jax.grad()`](https://docs.jax.dev/en/latest/_autosummary/jax.grad.html#jax.grad) involves no Polars,
file I/O, or data-dependent Python control flow.

[`GridInterpolator`](#qpdk.models.datasets.interpolation.GridInterpolator) only adds what a dataset lookup needs on top: keyword
arguments named after the axes, matrix-valued results, axes with a single grid
value, and NaN outside the *validated* domain rather than just outside the grid.

### *class* qpdk.models.datasets.interpolation.GridInterpolator(grid, , out_of_range='nan')

Multilinear interpolation of a [`Grid`](#qpdk.models.datasets.table.Grid).

Call with one keyword argument per axis. Arguments broadcast against each
other; the result has shape `(*broadcast shape, *component shape)`.

* **Parameters:**
  * **grid** ([*Grid*](#qpdk.models.datasets.table.Grid)) – Complete grid from `qpdk.models.datasets.Dataset.grid()`.
  * **out_of_range** (*OutOfRange*) – `"nan"` (default) returns NaN for any query outside the
    validated domain of an axis. `"clip"` evaluates at the nearest point
    of the domain; use it only where a deliberate clamp is acceptable.

### Example

```pycon
>>> grid = Dataset("plate_capacitor_palace").grid(
...     "maxwell_capacitance", cross_section="cpw"
... )
>>> c = GridInterpolator(grid)(length=100.0, width=10.0, gap=5.0)  # (2, 2) in F
```

#### in_domain(\*\*params)

Check which query points lie inside the validated domain.

* **Returns:**
  Boolean mask of the broadcast shape.
* **Parameters:**
  **params** (*ArrayLike*)
* **Return type:**
  [*Array*](https://docs.jax.dev/en/latest/_autosummary/jax.Array.html#jax.Array)

<a id="module-qpdk.models.datasets.capacitance"></a>

Conversions and physical checks for capacitance matrices.

Matrices have terminals on the last two axes, so leading batch axes from
interpolated lookups pass straight through.

### *exception* qpdk.models.datasets.capacitance.NonPhysicalMatrixError(problems)

A Maxwell capacitance matrix violates a physical property.

* **Parameters:**
  **problems** ([*list*](https://docs.python.org/3/builtins/stdtypes.html#list) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*)
* **Return type:**
  *None*

### qpdk.models.datasets.capacitance.check_maxwell(maxwell, , rtol=1e-09)

Check that a Maxwell capacitance matrix is physical.

Checks symmetry, positive diagonal, non-positive off-diagonal entries, and
diagonal dominance (non-negative capacitance to ground), each up to `rtol`
relative to the largest diagonal entry of each matrix. Non-finite entries are reported
before any other check, since they make the tolerance meaningless. Runs
eagerly on concrete arrays; it is a check, not a model, and is not jittable.

* **Raises:**
  [**NonPhysicalMatrixError**](#qpdk.models.datasets.capacitance.NonPhysicalMatrixError) – Listing every violated property.
* **Parameters:**
  * **maxwell** (*ArrayLike*)
  * **rtol** ([*float*](https://docs.python.org/3/builtins/functions.html#float))
* **Return type:**
  *None*

### qpdk.models.datasets.capacitance.differential_capacitance(maxwell, positive=0, negative=1)

Return the capacitance seen when moving charge between two floating islands.

Put charge $+Q$ on `positive` and $-Q$ on `negative`. This
returns $Q/(V_\text{positive}-V_\text{negative})$, including the effect
of capacitances to ground. Neither island is held at ground voltage.
For two islands, their mutual capacitance is in parallel with the series
combination of their two capacitances to ground.

Other named conductors float with no added charge; the reference ground
stays at zero voltage. To ground a named conductor, first remove its row
and column. Add any junction capacitance separately in parallel.
The calculation is $(d^{\text{T}} C^{-1} d)^{-1}$, where
$d = e_\text{positive} - e_\text{negative}$; see the node-charge energy
in [[VD17](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/fem_dataset_lookup.html.md#id71)], section 2.1.10.

* **Parameters:**
  * **maxwell** (*ArrayLike*) – Maxwell capacitance matrix in F, with optional leading batch axes.
  * **positive** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – First terminal index; a static Python integer when using JIT.
  * **negative** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – Second terminal index; a static Python integer when using JIT.
* **Returns:**
  Differential capacitance in F, preserving leading batch dimensions.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the terminal indices are equal or outside the matrix.
* **Return type:**
  [*Array*](https://docs.jax.dev/en/latest/_autosummary/jax.Array.html#jax.Array)

### qpdk.models.datasets.capacitance.maxwell_to_mutual(maxwell)

Convert a Maxwell capacitance matrix to the mutual (SPICE-style) matrix.

Off-diagonal entries become the branch capacitances $C_{ij}^m = -C_{ij}$,
and diagonal entries become the capacitance of each terminal to the reference
ground, $C_{ii}^m = \sum_j C_{ij}$.

* **Returns:**
  Mutual capacitance matrix, same shape as `maxwell`.
* **Parameters:**
  **maxwell** (*ArrayLike*)
* **Return type:**
  [*Array*](https://docs.jax.dev/en/latest/_autosummary/jax.Array.html#jax.Array)

<a id="module-qpdk.models.datasets.models"></a>

Generic SAX models from stored N-port scattering or Maxwell matrices.

### qpdk.models.datasets.models.capacitance_model(dataset, quantity='maxwell_capacitance', \*\*variants)

Return a lumped N-port SAX model from a Maxwell capacitance dataset.

Continuous geometry axes are passed by name; discrete variants are fixed
here. `f` is in Hz and `z_ref` is a common real port impedance in ohms.
The admittance is $Y = j 2 \pi f C$; all terminals reference the
dataset ground. This model includes terminal-to-ground capacitances and
mutual branches, with no distributed propagation or inductance.
Unspecified geometry axes use their middle stored coordinates.

* **Returns:**
  SAX model with the dataset terminal names.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the quantity is not a physical real Maxwell matrix.
* **Parameters:**
  * **dataset** ([*Dataset*](#qpdk.models.datasets.table.Dataset) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **quantity** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **variants** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  *SDictModel*

### qpdk.models.datasets.models.junction_model(dataset, , junction, inductance, junction_capacitance=0.0, terminal_groups=None, \*\*variants)

Build a small-signal circuit from electrode capacitances and one junction.

The dataset supplies capacitances between electrodes and to ground. Between
the two `junction` ports, this adds an inductor in parallel with an optional
junction capacitor. The inductor approximates the Josephson junction for
small oscillations around its operating point; it does not model nonlinear
dynamics or qubit anharmonicity. See [[VD17](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/fem_dataset_lookup.html.md#id71)],
sections 2.1.6 and 4.1, for this linearization and the Josephson element.

The branch admittance is $Y_\text{J}=1/(j\omega L_\text{J})+j\omega C_\text{J}$.
Its inductance and capacitance are explicit inputs, since an electrostatic
dataset cannot determine the Josephson inductance. Only add junction
capacitance that is not already included in the dataset.

`terminal_groups` optionally connects electrodes with ideal wires. For
example, `(("island", "top"), ("ring",))` joins `island` and `top` and
leaves `ring` separate. Each group’s first name becomes its SAX port.
Every dataset terminal must appear exactly once. This applies
$B^{\text{T}} C B$, with $B$ mapping group voltages to electrodes,
and retains capacitance to ground. The wires add no field or inductance.
All SAX ports reference dataset ground, and frequency is in Hz.

* **Parameters:**
  * **dataset** ([*Dataset*](#qpdk.models.datasets.table.Dataset) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Dataset or path containing real Maxwell capacitance matrices in F.
  * **junction** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *]*) – Names of the two ports connected by the junction, after grouping.
  * **inductance** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Positive small-signal Josephson inductance in H.
  * **junction_capacitance** ([*float*](https://docs.python.org/3/builtins/functions.html#float)) – Additional parallel junction capacitance in F.
  * **terminal_groups** ([*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple) *[*[*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,*  *...* *]* *,*  *...* *]*  *|* *None*) – Electrodes to connect together; defaults to separate ports.
  * **\*\*variants** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Discrete dataset selections.
* **Returns:**
  Jittable small-signal SAX model, with geometry in dataset axis units.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If terminals, grouping, matrix type or explicit inputs are invalid.
* **Return type:**
  *SDictModel*

### qpdk.models.datasets.models.s_parameters_model(dataset, quantity='s_parameters', , frequency_axis='frequency', \*\*variants)

Load stored N-port S-parameters and return a jittable SAX lookup.

`f` is in Hz and supplies `frequency_axis`. Other continuous grid axes
are passed by name; discrete variants are fixed when creating the model.
Terminals become port names. Reference impedances and planes are those of
the stored simulation, without renormalization. Out-of-domain values are
NaN. Multilinear interpolation does not enforce losslessness between points.
Unspecified geometry axes use their middle stored coordinates.

* **Returns:**
  SAX model with the dataset terminal names.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If the quantity or frequency axis has the wrong convention.
* **Parameters:**
  * **dataset** ([*Dataset*](#qpdk.models.datasets.table.Dataset) *|* [*Path*](https://docs.python.org/3/library/pathlib.html#pathlib.Path) *|* [*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **quantity** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **frequency_axis** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
  * **variants** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str))
* **Return type:**
  *SDictModel*

Without Slurm, run the generator directly to solve the full grid sequentially. The
notebook also shows a local shard loop with the same merge step. Each Palace solve
defaults to four MPI ranks; the Slurm helper reserves four CPUs and sets one thread per
rank. Supply the array range explicitly at submission, starting at zero.

`qpdk.models.datasets.s_parameters_model` loads a stored complex scattering matrix as
a jittable SAX model with any number of labelled ports. `capacitance_model` converts a
real Maxwell matrix into a lumped N-port model, including capacitances to ground.
Distributed CPW physics lives in `qpdk.models.couplers.cpw_coupling_model`;
`coupler_straight` uses the same Palace mutual-capacitance lookup through
`cpw_cpw_coupling_capacitance`.
