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.
Simulation utilities: shared layout preparation and solver inputs.
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:
Prepare a component with
prepare_metal_layout()Wrap a PyAEDT
Hfss,Q3dorQ2dapp ingplugins.ansys.HFSS,Q3DorQ2Dand import the component withimport_componentConfigure 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()toQ2D.create_2d_cpwwithsubstrate_level="Substrate"andconductor_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 adds only the QPDK-specific inputs:
Extract metal polygons and optional feed ports with
qpdk.simulation.comsol.prepare_comsol_layout()Build a model with
gplugins.comsol.COMSOL, passingSILICON_RELATIVE_PERMITTIVITYandAIR_RELATIVE_PERMITTIVITYfor 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 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() and
cpw_tline_params(), also need
uv sync --extra models.
VACASK workflow:
Write the circuit and its analyses in VACASK’s own netlist language and pass the blocks to a
Netlist, which fills them into a Jinja2 template. Loadingjosephson_junction.vaorsquid.vauses the models shipped with qpdk.Run it with
run(), which calls the externalvacaskbinary and returns oneRawPlotper analysis.Use
run_record()with a work directory to retain decks, logs, raw results and input/binary/compiler hashes.Optionally embed any linear SAX model as a subcircuit: fit it with
vector_fit()and emit the fit withvector_fit_subckt().
Example
>>> 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/
HFSS import_gds_3d: 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
MPh: 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)[source]#
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:
- 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(). The HFSS
notebooks show the full workflow.
VACASK#
The external simulator runs Lumped Josephson parametric amplifiers with VACASK and CPW parametric amplifiers: layout and circuit models with released VACASK. Native cold-network and subprocess checks also use the release. The TWPA notebook, Josephson traveling-wave parametric amplifiers with JosephsonCircuits.jl, uses JosephsonCircuits.jl instead.
Run the VACASK circuit simulator as a subprocess and read its results.
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:
Netlistfills raw VACASK text into a Jinja2 template and copies the Verilog-A models shipped with qpdk next to it.RawFilereads the SPICE raw files that VACASK writes.vector_fit()andvector_fit_subckt()turn S-parameters into a VACASK subcircuit. They need themodelsextra.
Example
>>> 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>)[source]#
A VACASK netlist, rendered from
vacask_templates/netlist.sim.j2.circuit,controlandsubcktsare VACASK netlist text and are inserted as-is, after removing their common indentation. The control block always starts withabort always, so that a failed analysis gives a nonzero exit code.- Parameters:
- control#
Contents of the
controlblock: options, sweeps and analyses. Eachanalysis <name> ...line produces a result.- Type:
- 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.
- subckts#
Subcircuit definitions, e.g. from
vector_fit_subckt(). Each one is dedented on its own, so they can come from different sources.
- files#
Extra files to place next to the netlist, as
{name: content}. APathvalue is copied; a string is written as text.
- class qpdk.simulation.vacask.RawFile(path)[source]#
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(complex128for complex plots), stored point-major.- path#
Path of the file.
- plots#
The plots in file order.
- Raises:
VacaskError – If the file is truncated or malformed.
- Parameters:
path (str | Path)
- class qpdk.simulation.vacask.RawPlot(title, plotname, names, data)[source]#
One plot of a SPICE raw file.
- class qpdk.simulation.vacask.SimulationResult(*, plots, output, elapsed, binary_sha256)[source]#
Plots and diagnostics from one simulator invocation.
With an explicit workdir, the deck, model files, raw results,
vacask.logandrun.jsonremain available after success or failure. A successful process exit does not establish harmonic convergence or pump stability.
- class qpdk.simulation.vacask.Vacask(binary, openvaf=None)[source]#
The VACASK simulator, run as a separate process.
- openvaf#
Path to the Verilog-A compiler, passed to VACASK as
SIM_OPENVAFunless that is already set.- Type:
Path | None
- classmethod find()[source]#
Locate VACASK and OpenVAF.
Checks the
VACASKandQPDK_VACASKenvironment variables, thenPATH, forvacask, andOPENVAFthenPATHforopenvaf-r.- Returns:
A runner for the binaries found.
- Raises:
VacaskError – If no
vacaskbinary is found.- Return type:
- run(netlist, *, workdir=None, timeout=None)[source]#
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:
- Returns:
{analysis name: plot}, read from<analysis name>.raw. Swept analyses hold all sweep points; seeRawPlot.split().- Return type:
See
run_record()for diagnostics and failure conditions.
- run_record(netlist, *, workdir=None, timeout=None)[source]#
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.jsonrecords 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 – If analyses have duplicate or invalid names, the simulator cannot start, fails or times out, or an output is missing.
- Parameters:
- Return type:
- exception qpdk.simulation.vacask.VacaskError[source]#
Raised when VACASK is missing, a simulation fails, or a raw file is malformed.
- qpdk.simulation.vacask.cpw_tline_params(length, cross_section='cpw')[source]#
Characteristic impedance and delay of a lossless CPW for
tline_ideal.Uses
cpw_parameters()with zero loss tangent, so the result matches VACASK’s losslesstline_idealdevice. Needs themodelsextra.- Parameters:
length (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 – If the
modelsextra is not installed.
- qpdk.simulation.vacask.format_value(value)[source]#
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 byspurandoutspur.
- 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)[source]#
Fit S-parameters with
skrf.vectorFitting.VectorFitting.A thin wrapper that builds the
skrf.Networkfrom an SDict and checks that the result can be used as a circuit: every pole stable and the model passive. Evaluate the fit withget_model_response()and turn it into a VACASK subcircuit withvector_fit_subckt().- Parameters:
sdict (Mapping[tuple[str, str], ArrayLike]) – S-parameters on
f, referenced toz0. Missing entries of a sparse SDict are zero.f (ArrayLike) – Frequencies in Hz. Cover every frequency the circuit will see, e.g. up to
nharmtimes the pump frequency for harmonic balance.ports (Sequence[str] | None) – Port order. Defaults to the order in which
sdictnames them. Stored as the network’sport_names.z0 (float) – Reference impedance in Ω.
n_poles_real (int) – Number of initial real poles.
n_poles_cmplx (int) – Number of initial complex-conjugate pole pairs.
enforce_passivity (bool) – Make the model passive if the fit is not.
n_samples (int) – Frequency samples for the passivity enforcement.
- Returns:
The fitted
VectorFittingobject.- Raises:
ImportError – If the
modelsextra is not installed.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)[source]#
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
vccsand theresistorandcapacitormodels, so the netlist must loadresistor.osdiandcapacitor.osdiand define models of those names.- Parameters:
vf (VectorFitting) – A fit from
vector_fit(), with a constant term and no proportional term, referenced to one real impedance.name (str) – Subcircuit name.
- Returns:
The subcircuit text, with terminals
p1,p2, … in port order.- Raises:
ValueError – If the fit has a proportional term, an unstable pole, or port-dependent or complex reference impedances.
- Return type:
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 adds only what is specific to QPDK:
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
and 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 does not import it, so the
rest of the package works without the extra.
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()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 thecoupling_o1/coupling_o2feeds, before handing the metal togplugins.comsol.layout.prepare_comsol_layout().SILICON_RELATIVE_PERMITTIVITYandAIR_RELATIVE_PERMITTIVITYare 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
>>> 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)[source]#
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, withprepare_metal_layout(), and the result is extracted withgplugins.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[str, str] | None) – Names of exactly two distinct ports to expose as feeds, or
Nonefor an unfed layout, in which case the result carries no feed ports.Nonefits layouts with no CPW feedline, e.g. an eigenmode qubit cell.ground_margin (float) – Positive margin in µm added around the component bounding box to form the ground plane.
crop_to_feed_ports (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 returnedbboxcan exceed the port span by less than one database unit. Defaults toFalse, which leaves the prepared ground untouched.
- Returns:
A
gplugins.comsol.layout.ComsolLayoutwith metal polygons (holes preserved), the selected feed ports (empty whenfeed_portsisNone), and the prepared bounding box, all in µm.- Raises:
ValueError – If
ground_marginis not positive and finite, iffeed_portsis 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 ifcrop_to_feed_portsis 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.
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 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.
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:
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:
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.
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 for the table layout, qpdk.models.datasets.generate
for producing one, and qpdk.models.datasets.store for where it lives.
Requires the models extra; Delta Lake also needs the delta extra.
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, 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)[source]#
A continuous parameter axis; its grid values come from the data.
- 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)[source]#
Metadata stored inside the results files of a dataset.
- Parameters:
name (str) – Dataset name.
description (str) – One-line description.
synthetic (bool) – True when the values are not real solver output; such data must not be used for design.
axes (tuple[Axis, ...]) – Continuous parameters, in tensor order.
variants (tuple[str, ...]) – Discrete parameters (string columns), selected and never interpolated.
terminals (tuple[str, ...]) – Ordered terminal labels; defines matrix row and column order.
reference_ground (str) – Conductor all potentials are referenced to; not a matrix terminal.
provenance (dict[str, Any]) – Free-form recipe, geometry, layer stack, solver, and accuracy information.
schema_version (int) – Schema version; must equal
SCHEMA_VERSION.
- model_config = {'extra': 'forbid', 'frozen': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- 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)[source]#
A quantity stored in the results table.
Every kind except
QuantityKind.CIRCUIT_PARAMETERis a matrix over the dataset terminals.- Parameters:
name (str)
kind (QuantityKind)
unit (str)
complex (bool)
- 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)[source]#
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.
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 |
|---|---|---|
|
String |
Identity of the solver run that produced the row. |
|
String |
|
<axis> |
Float64 |
One column per metadata axis, in the axis’ unit. |
<variant> |
String |
One column per metadata variant. |
|
String |
Name of a metadata quantity. |
|
String |
Row terminal label; null for scalar quantities. |
|
String |
Column terminal label; null for scalar quantities. |
|
Float64 |
Value (real part for complex quantities); null unless |
|
Float64 |
Imaginary part; null for real quantities. |
|
String |
SI unit; must equal the metadata unit of the quantity. |
The DatasetMetadata is stored inside the files
(see qpdk.models.datasets.store), so a dataset is self-describing and nothing
but its Parquet parts or Delta table.
flowchart LR
A["Python generator: metadata + grid"] --> B["Palace solves"]
B --> C["Validated Parquet parts or Delta table"]
C --> D["Lazy scan: filter + select"]
D --> E["Dense grid for one quantity and variant"]
E --> F["JAX interpolation"]
F --> G["SAX circuit model"]
- 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)[source]#
A FEM extraction dataset: Parquet parts or a Delta table with its metadata.
- Parameters:
location (Path | str) – Directory of Parquet parts, the name of a dataset shipped in
DATASETS_PATH, or withdelta=Truea Delta table path or object-store URI such asgs://bucket/dataset.metadata (DatasetMetadata) – Metadata for a new dataset. For an existing one it is read from the files and, if given, must match.
delta (bool) – Store results as a Delta Lake table (
deltaextra).version (int | None) – Delta table version to read; a pinned dataset is read-only.
storage_options (dict[str, str] | None) – Object-store options (credentials, endpoints) for a Delta URI. Never written to the dataset.
- Raises:
FileNotFoundError – if nothing is stored yet and no
metadatais given.DatasetError – if
metadatadiffers from the stored metadata.
- append(frame, *, part=None)[source]#
Validate
frametogether 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 – If rows duplicate existing entries or mix run identities.
- Parameters:
frame (pl.DataFrame)
part (str | None)
- grid(quantity, *, allow_missing=False, **variant)[source]#
Arrange
quantityon its grid; seeto_grid().- Returns:
The dense grid of
quantity.- Raises:
DatasetError – If the selected rows are invalid or variants are ambiguous.
- Parameters:
- Return type:
- scan()[source]#
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 – if the stored results do not match the schema.
- exception qpdk.models.datasets.table.DatasetError(dataset, problems)[source]#
The dataset is inconsistent with its metadata or cannot be gridded.
- class qpdk.models.datasets.table.Grid(dataset, quantity, axes, coords, terminals, variant, values)[source]#
A quantity on a complete rectilinear grid, ready for interpolation.
valueshas 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 ascendingcoords, terminals followterminals. Missing or failed points are NaN, never zero.- Parameters:
- qpdk.models.datasets.table.schema(metadata)[source]#
Column schema of the results table for
metadata.- Returns:
The Polars schema.
- Parameters:
metadata (DatasetMetadata)
- Return type:
pl.Schema
- qpdk.models.datasets.table.to_grid(frame, metadata, quantity, *, allow_missing=False, **variant)[source]#
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) – Dataset metadata.
quantity (str) – Quantity name.
allow_missing (bool) – Return NaN for missing or failed points instead of raising.
**variant (str) – Value for every metadata variant; discrete variants are selected explicitly and never interpolated.
- Returns:
The dense grid of
quantity.- Raises:
DatasetError – if the selection is ambiguous or empty, a validated domain leaves the grid, or the grid is incomplete.
- Return type:
- qpdk.models.datasets.table.validate(frame, metadata)[source]#
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().- Raises:
DatasetError – listing every problem found.
- Parameters:
frame (pl.DataFrame)
metadata (DatasetMetadata)
- Return type:
None
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() runs it over the grid and lays the results out as the long-format
table of qpdk.models.datasets.table. write() then stores the table with
its metadata.
- type qpdk.models.datasets.generate.Solve = Callable[[...], Mapping[str, Array | ndarray | bool | number | bool | int | float | complex | TypedNdArray] | 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, andNonerecords the run as failed.
- qpdk.models.datasets.generate.merge(parts, output, *, grid, variants=None)[source]#
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 – If shards are absent, disagree, overlap, fail, or do not cover the requested grid and variants.
- Parameters:
- Return type:
- qpdk.models.datasets.generate.partition_grid(grid, *, shard, shards)[source]#
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.
- qpdk.models.datasets.generate.sweep(metadata, solve, grid, variants=None)[source]#
Run
solveat every point ofgrid×variants.Each point gets a
run_idderived from the dataset name and the point, so re-running a sweep reproduces the same identities.- Parameters:
- Returns:
Long-format results table, not yet validated or stored.
- Raises:
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)[source]#
Replace the dataset at
locationwithframeas 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 – if
locationholds anything other than dataset parts.OSError – if publishing fails; the existing dataset is then kept.
- Parameters:
location (Path | str)
metadata (DatasetMetadata)
frame (pl.DataFrame)
- Return type:
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
in the files, and a readable error for an unresolved Git LFS pointer.
ParquetPartsA local directory of append-only
*.parquetparts, 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.DeltaStoreA Delta Lake table at a local path or an object-store URI such as
gs://,s3://, oraz://. 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 thevaluecolumn of the table schema, since Delta tables accept no free-form table properties. Requires thedeltaextra.
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)[source]#
Results stored as a Delta Lake table.
Every append is one atomic commit and creates a new table version. Reading a pinned
versionreproduces 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 adeltalakelocking 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:
- metadata()[source]#
Metadata stored in the table schema, or
Noneif there is no table.- Returns:
The table’s dataset metadata.
- Raises:
ValueError – if the table has no dataset metadata.
- Return type:
DatasetMetadata | None
- scan(schema)[source]#
Scan the table, cast to
schema.- Returns:
Lazy frame over the table.
- Raises:
FileNotFoundError – if there is no table.
- Parameters:
schema (pl.Schema)
- Return type:
pl.LazyFrame
- write(frame, metadata, part)[source]#
Commit
frameas one append, creating the table if needed.New tables are partitioned by quantity and variants, which are selected exactly and never interpolated, and carry
metadataon thevaluecolumn. Initial creation is exclusive; retry an append if another writer creates the table first.- Returns:
The URI and the committed table version.
- Raises:
ValueError – if the store is pinned to a version.
- Parameters:
frame (pl.DataFrame)
metadata (DatasetMetadata)
part (str | None)
- Return type:
- exception qpdk.models.datasets.store.LFSPointerError[source]#
A results file is an unresolved Git LFS pointer instead of Parquet data.
- class qpdk.models.datasets.store.ParquetParts(location)[source]#
Local, append-only directory of Parquet parts.
- Parameters:
location (str | Path) – Directory of the parts.
- metadata()[source]#
Metadata stored in the parts, or
Noneif there are none yet.- Returns:
The metadata shared by every part.
- Raises:
ValueError – if the parts disagree or a part has no metadata.
- Return type:
DatasetMetadata | None
- scan(schema)[source]#
Scan all parts with
schema.- Returns:
Lazy frame over every part.
- Raises:
FileNotFoundError – if there are no parts.
- Parameters:
schema (pl.Schema)
- Return type:
pl.LazyFrame
- write(frame, metadata, part)[source]#
Publish
frameas a new part without ever replacing an existing one.- Returns:
Path of the written part.
- Raises:
FileExistsError – if the part already exists.
ValueError – if the part is not a filename stem.
- Parameters:
frame (pl.DataFrame)
metadata (DatasetMetadata)
part (str | None)
- Return type:
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, so calling the lookup
under jax.jit(), jax.vmap(), or jax.grad() involves no Polars,
file I/O, or data-dependent Python control flow.
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')[source]#
Multilinear interpolation of a
Grid.Call with one keyword argument per axis. Arguments broadcast against each other; the result has shape
(*broadcast shape, *component shape).- Parameters:
grid (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
>>> 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
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)[source]#
A Maxwell capacitance matrix violates a physical property.
- qpdk.models.datasets.capacitance.check_maxwell(maxwell, *, rtol=1e-09)[source]#
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
rtolrelative 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 – Listing every violated property.
- Parameters:
maxwell (ArrayLike)
rtol (float)
- Return type:
None
- qpdk.models.datasets.capacitance.differential_capacitance(maxwell, positive=0, negative=1)[source]#
Return the capacitance seen when moving charge between two floating islands.
Put charge \(+Q\) on
positiveand \(-Q\) onnegative. 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], section 2.1.10.
- Parameters:
- Returns:
Differential capacitance in F, preserving leading batch dimensions.
- Raises:
ValueError – If the terminal indices are equal or outside the matrix.
- Return type:
- qpdk.models.datasets.capacitance.maxwell_to_mutual(maxwell)[source]#
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:
Generic SAX models from stored N-port scattering or Maxwell matrices.
- qpdk.models.datasets.models.capacitance_model(dataset, quantity='maxwell_capacitance', **variants)[source]#
Return a lumped N-port SAX model from a Maxwell capacitance dataset.
Continuous geometry axes are passed by name; discrete variants are fixed here.
fis in Hz andz_refis 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.
- qpdk.models.datasets.models.junction_model(dataset, *, junction, inductance, junction_capacitance=0.0, terminal_groups=None, **variants)[source]#
Build a small-signal circuit from electrode capacitances and one junction.
The dataset supplies capacitances between electrodes and to ground. Between the two
junctionports, 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], 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_groupsoptionally connects electrodes with ideal wires. For example,(("island", "top"), ("ring",))joinsislandandtopand leavesringseparate. 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 | Path | str) – Dataset or path containing real Maxwell capacitance matrices in F.
junction (tuple[str, str]) – Names of the two ports connected by the junction, after grouping.
inductance (float) – Positive small-signal Josephson inductance in H.
junction_capacitance (float) – Additional parallel junction capacitance in F.
terminal_groups (tuple[tuple[str, ...], ...] | None) – Electrodes to connect together; defaults to separate ports.
**variants (str) – Discrete dataset selections.
- Returns:
Jittable small-signal SAX model, with geometry in dataset axis units.
- Raises:
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)[source]#
Load stored N-port S-parameters and return a jittable SAX lookup.
fis in Hz and suppliesfrequency_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.
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.