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.
Pass layer_stack=qpdk.tech.LAYER_STACK explicitly. The PDK default is a
mask preview with an implicit ground plane, not a simulation stack. Also 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 LAYER_STACK, 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, layer_stack=LAYER_STACK, 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
FEM#
Volumetric-FEM helpers for qpdk layouts.
Converts the PDK’s subtractive etch masks into the explicit conductor and
dielectric regions that full-wave 3-D solvers (Palace through gsim, Meep, …)
expect, and provides the matching layer stacks. Install gsim with the palace
dependency group on Python 3.12. Functions that need it import it lazily.
- qpdk.simulation.fem.flip_chip_stack(substrate_thickness=500.0, bump_thickness=10.0)[source]#
Return a gsim layer stack for a face-to-face qpdk flip-chip pair.
The bottom chip carries M1 with the substrate below it; the top chip is flipped so its M2 faces down across the indium-bump gap, with its substrate above. Both metals become perfect electric conductors and the bumps conductive vias; the substrate uses the qpdk microwave silicon, so FEM and analytical models describe the same chips.
- qpdk.simulation.fem.single_chip_stack(substrate_thickness=500.0, vacuum_thickness=500.0)[source]#
Return a gsim layer stack for a single qpdk silicon chip.
The substrate uses the microwave silicon from the qpdk material properties (\(\varepsilon_r = 11.45\), \(\tan\delta = 2.7 \times 10^{-6}\)), so FEM and analytical CPW models describe the same chip. The zero-thickness niobium conductor becomes a perfect electric conductor in the solver; its material entry only names the layer.
- qpdk.simulation.fem.to_fem_regions(component)[source]#
Convert an etch-based qpdk layout into explicit FEM regions.
The PDK draws metal subtractively:
M1_ETCHmarks where metal is removed and additive shapes onM1_DRAWpunch through the etch mask (the conductor isSIM_AREA - (M1_ETCH - M1_DRAW)), matchingget_layer_stack()’s M1 definition. The conductor, substrate and vacuum regions are copied onto the layer numbering ofFEM_LAYERS, with the zero-thickness conductor later becoming a perfect-electric-conductor boundary in the solver. Junction masks are omitted because lumped ports represent them. Other fabrication layers outside M1 are rejected.- Parameters:
component (Component) – Layout with a
SIM_AREAlayer and simulation ports.- Returns:
Component carrying the
SUBSTRATE,SUPERCONDUCTORandVACUUMregions plus the input ports.- Raises:
ValueError – If unsupported fabrication layers are present, or the component carries no
SIM_AREAshapes or the simulation area is fully etched away, either of which would silently produce an empty FEM model.- Return type:
- qpdk.simulation.fem.to_flip_chip_regions(component)[source]#
Convert a two-metal-layer qpdk layout into explicit flip-chip regions.
Both metal levels follow the same subtractive convention as
to_fem_regions()(conductor isSIM_AREA - (ETCH - DRAW), perLAYER_STACK_FLIP_CHIP), and the indium bump layer is copied to its own region. The regions land on the layer numbering ofFLIP_CHIP_FEM_LAYERS: two substrate slabs, two zero-thickness metal sheets, the vacuum gap, and the bumps. Junction masks are omitted because lumped ports represent them. Other fabrication layers are rejected.- Parameters:
component (Component) – Flip-chip layout with a
SIM_AREAlayer, M1/M2 metal andINDbump shapes, plus simulation ports.- Returns:
Component carrying the flip-chip regions plus the input ports.
- Raises:
ValueError – If unsupported fabrication layers are present, the component carries no
SIM_AREAshapes, a metal level has neither etch nor draw shapes (which the subtractive convention would turn into a solid metal plane shorting the model), is fully etched away, or there are no indium bumps to connect the two chips.- Return type:
- qpdk.simulation.fem.to_tsv_regions(component, chip='bottom')[source]#
Convert a double-sided qpdk layout into explicit TSV FEM regions.
The front (M1) and back (MB) metal levels both follow the subtractive convention of
to_fem_regions()(conductor isSIM_AREA - (ETCH - DRAW), perLAYER_STACK_BACKSIDE), and theTSVlayer is copied to its own region. Backside shapes are drawn in the front-side frame, as intsv_transition_double_sided(), so no mirroring is applied. The regions land on the layer numbering ofTSV_FEM_LAYERS. Withchip="top"the flip-chip top chip is converted instead, reading M2, MB2 and TSV2 into the same regions.- Parameters:
- Returns:
Component carrying the TSV regions plus the input ports.
- Raises:
ValueError – If the component carries no
SIM_AREAshapes, a metal level has neither etch nor draw shapes, or there are no TSVs inside the simulation area to connect the two faces.- Return type:
- qpdk.simulation.fem.tsv_stack(substrate_thickness=200.0, vacuum_thickness=500.0, tsv_conductivity=1000000.0)[source]#
Return a gsim layer stack for a chip with metal on both faces.
The front metal M1 sits at
z = 0on top of the silicon and the backside metal MB atz = -substrate_thicknessbelow it; both become perfect electric conductors. The TSVs are conductive vias spanning the full substrate. Air fillsvacuum_thicknessabove and below the chip.The default substrate thickness follows the 200 µm TSV depth of [MYR+21].
- Parameters:
substrate_thickness (float) – Silicon thickness (TSV depth) in μm.
vacuum_thickness (float) – Air height above and below the chip in μm.
tsv_conductivity (float) – Normal-state conductivity of the via lining in S/m. The lining is superconducting at operating temperature; any finite value large enough to make the via a good conductor works here. Without a conductivity gsim degrades the via to a 2-D PEC sheet at its base, which no longer bridges the faces.
- Returns:
A
gsim.common.stack.LayerStack(requires gsim from themodelsextra).- Return type:
From etch layers to simulation regions#
The PDK and a volumetric solver describe metal in opposite ways, and every FEM workflow in qpdk starts by converting between them.
qpdk draws metal subtractively: M1_ETCH marks where metal is removed from a full
sheet, and additive shapes on M1_DRAW punch back through it, so the conductor is
SIM_AREA - (M1_ETCH - M1_DRAW). This matches get_layer_stack()’s M1 definition
and is how the transmon pads are drawn.
A volumetric solver needs the positive form instead: explicit conductor and
dielectric bodies. to_fem_regions() performs that conversion and
copies conductor, substrate and vacuum onto dedicated GDS layers that the FEM layer
stack maps to materials. The zero-thickness conductor sheet becomes a
perfect-electric-conductor boundary.
Two consequences are worth seeing rather than reading: everything the etch mask covers turns into vacuum, and an additive shape sitting inside an etched gap survives as an isolated metal island.
The same rule applies per metal level for flip-chip stacks, where
to_flip_chip_regions() converts both levels and copies the indium
bump layer to its own region.
Batched layout optimisation and cluster execution#
The Palace optimisation notebook defines its transmon layout and loss objective. The generic runner keeps trials in flight and sends that callable to Slurm or Ray workers. gsim provides single-node local execution and text-result loading; these helpers add multi-node MPI launching, domain-energy requests and port-connectivity checks.
Add Palace domain-energy output and run meshed directories with MPI.
gsim already provides single-node run_local() and Palace text-result
loading. This module covers Slurm multi-node MPI, domain-energy output, and
port-connectivity checks. The latter two could move to gsim later.
- qpdk.simulation.palace_run.add_domain_energy_postprocessing(sim_dir)[source]#
Ask Palace to report the field energy in every dielectric domain.
gsim writes the domain materials but leaves the postprocessing list empty, so the domain attributes are read back out of the written config and requested one by one. Palace then reports the electric and magnetic energy in each, which is what the participation ratios come from.
- qpdk.simulation.palace_run.domain_loss_tangents(sim_dir)[source]#
Map each domain’s postprocessing index to its loss tangent.
- qpdk.simulation.palace_run.palace_command(*, ranks, launcher_args=None)[source]#
Return the command that solves a simulation directory in place.
The native Palace launcher starts MPI with the requested ranks. Set
QPDK_PALACE_SIFto an Apptainer image to run the solver from a container, which is how a compute node usually has it; otherwisepalaceis taken fromPATH.
- qpdk.simulation.palace_run.solve(sim_dir, *, ranks, timeout=1800.0)[source]#
Run the solver over an already-meshed simulation directory.
- Parameters:
- Raises:
RuntimeError – If Palace exits non-zero.
- Return type:
- qpdk.simulation.palace_run.verify_port_connectivity(sim_dir)[source]#
Check that every lumped port is attached to metal in the mesh.
A port rectangle that overhangs thin conductor features makes gsim’s boolean pipeline delete them, which can leave the port with nothing to drive. Nothing downstream complains on its own: the mesh validates, Palace runs, and the modes it returns are port-local artifacts whose frequency tracks the port inductance rather than the geometry. A sweep would inherit that for every trial, so this runs once per built directory.
The check is deliberately narrow: it verifies each port shares mesh nodes with some conductor. It cannot tell which conductor, so a port that reaches the ground plane but has lost the island it was meant to drive still passes. Treat it as a floor, not a proof.
- Parameters:
sim_dir (Path) – Directory holding the written
config.jsonandpalace.msh.- Returns:
Shared node counts keyed by port group name.
- Raises:
ValueError – If the config has no lumped port or no conductor surfaces at all, if a port’s mesh group is missing, or if a port shares no nodes with any conductor.
- Return type:
Drive an Optuna study whose trials are solved somewhere else.
A geometry sweep is a queue of independent solver runs, so the only real
questions are where a trial runs and how many run at once. Those are separate
concerns here: a runner knows how to start one trial and how to collect it,
and run_study() keeps a fixed number of them in flight while feeding
results back to the sampler.
Keeping trials in flight rather than evaluating fixed batches matters twice over. A batch can only advance when its slowest trial finishes, so a wave of twelve solves idles most of its allocation waiting for one big mesh; and the sampler learns nothing until the whole wave lands, when it could have been told about each result as it arrived.
Two runners come with the module and the difference between them is only where the solve happens:
SlurmRunnersubmits one job per trial. A small job starts on any node with room, so the sweep fills whatever the cluster has free.RayRunnerdispatches a Ray task per trial, for when the study already owns an allocation.
- class qpdk.simulation.study.RayRunner(run_root, ranks, evaluator)[source]#
Bases:
objectRun each trial as a Ray task inside an allocation the study owns.
- evaluator#
Callable serialized by Ray or an importable
module:functionname.
- class qpdk.simulation.study.SlurmRunner(cluster, run_root, ranks, evaluator, timeout=1800.0)[source]#
Bases:
objectRun each trial as its own Slurm job.
One script serves the whole study: Slurm forwards the arguments after the script name to the script, so each submission differs only in the parameter file it is given.
- Parameters:
- cluster#
Site settings the job is sized and submitted with.
- Type:
- evaluator#
Callable to serialize for worker jobs, or an importable
module:functionname.
- timeout#
Seconds before a solve is abandoned. Keep it under the job’s own wall clock, or Slurm kills the job first and the trial reports nothing.
- Type:
- class qpdk.simulation.study.TrialRunner(*args, **kwargs)[source]#
Bases:
ProtocolStarts one trial somewhere and collects it when it is done.
- qpdk.simulation.study.run_study(study, runner, *, suggest, objectives, n_trials, max_in_flight, poll_seconds=20.0)[source]#
Run
studyton_trials, keeping trials in flight as they finish.The sampler is asked for a new trial as soon as one comes back rather than at a batch boundary, so no part of the study waits on its slowest solve and every result informs the next suggestion.
- Parameters:
study (Study) – The study to advance. Its sampler decides what to try next.
runner (TrialRunner) – Starts and collects a trial.
suggest (Callable[[Trial], None]) – Fills a trial’s parameters, usually with
trial.suggest_*.objectives (Callable[[dict[str, Any]], Sequence[float]]) – Maps a result row to the values
studywas created for.n_trials (int) – Total trials to evaluate.
max_in_flight (int) – How many trials may be running at once. On Slurm this is the real concurrency; under Ray the cluster’s cores also bound it.
poll_seconds (float) – Seconds between checks for finished trials.
- Returns:
One row per successful trial, carrying its number and parameters.
- Raises:
ModuleNotFoundError – If Optuna is not installed. Use
qpdk[models].ValueError – If
max_in_flightis not positive.RuntimeError – If every trial failed, since there would be nothing to analyse.
- Return type:
Evaluate one trial of a parameter sweep, as its own process.
A sweep is a queue of independent solver runs, and the cheapest way to put that queue on a cluster is one job per trial: a small allocation starts on any node with room, so the sweep fills whatever is free. This module is what such a job runs. It reads the trial’s parameters from a JSON file, hands them to an evaluator, and writes the result beside them, so the driver only has to read files back.
Nothing here knows what kind of simulation the evaluator runs. The job loads
either an importable module:function or a callable serialized by the
notebook driver. Both take (params, run_root) plus keyword arguments and
return a row of results.
Running it by hand is the same call the scheduler makes:
python -m qpdk.simulation.trial params.json --run-root /scratch/run
--ranks 8 --evaluator mysweep:evaluate_layout
- qpdk.simulation.trial.main(argv=None)[source]#
Evaluate one trial and write its result next to its parameters.
Launch simulation work on a Slurm cluster.
A geometry sweep is a queue of independent solver runs, and this module renders
the sbatch scripts for the two ways of handing that queue to Slurm:
One job per trial, submitted as the study goes. Each asks for a few ranks and a memory limit on each requested node. The driver keeps a fixed number in flight.
A Ray cluster inside one allocation, one worker per node. A good fit when the job owns its nodes, but it can only start once a whole node’s worth of cores is free.
This module only turns settings into scripts and submits them; the loop that
decides what to submit lives in qpdk.simulation.study.
Every site-specific value is a plain string, so adapting the scripts to a given cluster means filling in the fields rather than editing the templates.
- class qpdk.simulation.cluster.SlurmCluster(*, partition='batch', scratch='.', setup='', account=None, nodes=1, cores_per_node=16, mem_per_node='32G', time_limit='04:00:00', job_name='qpdk-ray', log_dir='slurm-logs', solver_cores=8, trial_nodes=1, mem_per_task='24G', driver_time_limit='24:00:00')[source]#
Bases:
objectSite settings for running batched simulation work on Slurm.
The same settings describe both arrangements: one job per trial, and a Ray cluster inside a single multi-node allocation.
- Parameters:
- partition#
Partition to request, or a comma-separated list of them. Naming several lets the scheduler start the job wherever it can begin earliest, which usually beats pinning one fast partition.
- Type:
- scratch#
Shared filesystem the job runs from, visible on every node. The driver writes results there and Ray stages its session under it.
- Type:
- setup#
Shell snippet run on every node before Ray starts, for loading environment modules, activating a virtual environment and redirecting package caches onto
scratch.- Type:
- account#
Slurm billing account. Optional because on many sites the default account is the only one the general partitions accept.
- cores_per_node#
Cores per node, which also bounds how many solver ranks a node can run at once.
- Type:
- mem_per_node#
Memory per node. Sites commonly default to a few hundred megabytes per core, so this is worth setting explicitly.
- Type:
- log_dir#
Directory for Slurm’s job output. Relative paths are resolved when the script is rendered.
- Type:
- solver_cores#
Cores one solver invocation may use, and so the size of one trial’s job. Under Ray the driver asks for this many CPUs per trial.
- Type:
- mem_per_task#
Memory per trial node. Separate from
mem_per_nodebecause Ray uses its own allocation.- Type:
- driver_time_limit#
Wall-clock limit for the driver job. The driver only submits and waits, so it needs one core but usually outlives each trial by a long way.
- Type:
- driver_sbatch_script(driver)[source]#
Render the script for the driver job.
The driver asks the optimizer for trials and submits a job per trial; the solving happens in those jobs, so this one needs a single core and a wall clock longer than the whole study.
- sbatch_script(driver)[source]#
Render the script that runs
driveron a Ray cluster.- Parameters:
driver (str) – Command to run on the head node once the cluster is up.
- Returns:
The script as a string.
- Raises:
ValueError – The Ray worker needs more cores than one node provides.
- Return type:
- task_sbatch_script(command)[source]#
Render the script that solves one trial.
Submitted once per trial. Slurm forwards any arguments after the script name to the script itself, so
sbatch trial.sbatch params.jsongives the command its parameter file as$1and one script serves the whole study.
- exception qpdk.simulation.cluster.SlurmJobError[source]#
Bases:
RuntimeErrorRaised when a Slurm command fails.
- qpdk.simulation.cluster.job_state(job_id)[source]#
Return a job’s Slurm state, or
Noneonce it has left the queue.- Parameters:
job_id (str) – A Slurm job id.
- Returns:
The state Slurm reports, or
Noneif the job is no longer queued.- Raises:
SlurmJobError – If
squeueitself fails, which is not the same as the job having left the queue.- Return type:
- qpdk.simulation.cluster.submit(script, *args)[source]#
Submit
scriptto Slurm and return the job id.- Parameters:
- Returns:
The job id Slurm assigned.
- Raises:
SlurmJobError – If
sbatchfails.- Return type:
- qpdk.simulation.cluster.wait(job_id, *, poll_seconds=20.0, max_query_failures=10)[source]#
Block until
job_idleaves the queue.A job can be briefly absent from
squeueright after submission, so a single miss is not treated as completion.- Parameters:
- Raises:
SlurmJobError – If
squeuekeeps failing, rather than silently reporting the job as finished.- Return type:
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. Pass layer_stack=qpdk.tech.LAYER_STACK explicitly to its HFSS,
Q3D and Q2D classes. The PDK default is LAYER_STACK_3D, which
adds an implicit ground plane for mask previews and must not be used for simulations.
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. The VACASK maintainer reads out-of-process use as
staying outside the AGPL’s combined work rule
(issue #161).
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:
- 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.
- 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:
- 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#
Build COMSOL models 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#
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. Lookups read
the table with Polars and return jittable SAX models. Requires the models extra. See
FEM Dataset Lookup in SAX Models for a walkthrough.
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.
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
)
Datasets and models#
Every model below takes geometry in µm and frequency f in Hz. Values outside the
stored grid return NaN, an omitted geometry argument falls back to the model’s stored
default, usually the middle grid coordinate, and discrete variants such as the turn
count are fixed when the model is created. The generation recipe for each dataset lives
in qpdk/models/datasets/recipes/.
Model |
Dataset |
Gives you |
Geometry inputs |
Notes |
|---|---|---|---|---|
|
|
Maxwell matrix, ports |
|
Separate electrodes between two 300 µm chips, no junction or bonding bump |
|
|
Ports |
same as above |
Ideal wire from the inner pad to the top pad; no bump field or inductance |
|
|
Differential pad capacitance \(C_\text{diff}\) in F |
|
Junction excluded; add \(C_\text{J}\) in parallel before computing \(E_\text{C} = e^2/[2(C_\text{diff}+C_\text{J})]\) |
|
|
Ports |
same as above |
Both ports use the coupon’s finite 300 µm ground frame; no readout, package or backside ground |
|
|
\(C_\Sigma\) in F, which sets \(E_\text{C}\); \(C_\text{g}\), which sets \(n_\text{g} = C_\text{g}V_\text{g}/2e\); \(C_\text{c}\) |
|
Only M1 sheets, so \(C_\text{g}\) is the M1 feed coupling, not the nanotube gate lever arm. \(C_\Sigma\) already includes both branches |
|
|
Ports |
same as above, plus |
Electrostatic solve, since \(E_\text{J}\) is gate-tuned rather than set by
the geometry. |
|
|
Ports |
|
Feedline cut open and the whole conductor at one potential, so this is not a resonator mode. Needs RF validation |
|
|
Ports |
same as above |
Same caveat. Feedline gaps of 14 to 20 µm retain the ground strip |
|
|
Ports |
|
Matches |
|
|
Winding-to-ground capacitance in F, from 54 extractions |
|
Both winding ends at one potential, so this is common mode only: no differential parasitics, inductance or self-resonance |
|
|
Complex S-parameters on ports |
|
Fixed 0.4 pH per square on both winding and frame, no material loss, radiation
allowed. Use |
|
|
Four-port quasi-TEM coupled section of a uniform slice |
|
Interpolates logarithmically once a ground strip is present.
|
|
|
Coupling \(C_\text{c}\), parasitic \(C_\text{p}\) and drum \(C_\text{d}\) in F, from 54 electrostatic solves |
|
The other cell arguments are fixed at their defaults, including 75 meander runs. Values outside the grid return NaN |
|
|
Two-port lumped SAX model with the extracted capacitances in place of the analytical ones |
same as above, plus |
The extracted \(C_\text{d}\) replaces the analytical value at the stack gap,
so |
flipmon_ideal_bond_model and double_pad_transmon_fem_model build on
junction_model(), which documents the circuit convention.
The winding and ground frame of the full-wave coupon share its sheet inductance, so the
terminal response includes both. For a straight strip with nearly uniform current, sheet
inductance times length over width estimates only the strip’s contribution; compare
against a control solve with a PEC ground return, and keep the frame’s sheet inductance
on when generating a device dataset.
For the hybrid process, use qpdk.tech.LAYER_STACK_GATEMON and measure the flake
thickness before meshing a local electrostatics solve.
Gatemon validation#
The bundled gatemon_palace dataset holds all 144 geometries and 3024 records,
including the complete three-terminal Maxwell matrices and solver diagnostics. A
representative convergence study re-solved four M1 island geometries on a finer 0.6 µm /
40 µm mesh with fourth-order elements and a larger domain: \(C_\Sigma\),
\(C_\text{g}\) and \(C_\text{c}\) moved by at most 0.74%, inside their 1%, 5%
and 1% tolerances. Three independent holdout solves interpolate to within 2.61%, inside
the 3%, 10% and 5% limits. Every matrix gives a finite, reciprocal, lossless response
over 1 to 10 GHz. At the default geometry the M1 values are 51.93 fF, 1.582 fF and 5.731
fF, for the finite extraction coupon without the hybrid process’s local capacitances.
Full-grid coverage and interpolation are checked separately.
Both reports sit in each Parquet part’s qpdk.dataset metadata under
provenance.validation, so read them through the dataset API:
from qpdk.models.datasets import Dataset
validation = Dataset("gatemon_palace").metadata.provenance["validation"]
convergence = validation["convergence"]
verification = validation["verification"]
The verification report hashes the result rows independently of the metadata. Its source provenance retains the original solver-file hashes, while Git LFS identifies the published Parquet file including the embedded reports.
The gatemon test chip#
The gatemon_test_chip sample follows Fig. 1a of
[RAP+25]: two independent transmission resonators with
series coupling capacitors at both ends, both targeting device A’s 5.056 GHz bare-cavity
frequency. A capacitor-to-capacitor CPW path is a 600 µm coupling straight, a 9946.135
µm meander and a 1400 µm tail, giving about 5.14 fF of end capacitance each. The
capacitor geometry and the 10 µm / 6 µm CPW width and gap are QPDK design choices, not
published dimensions. The targets are \(E_\text{C}/h = 260\,\text{MHz}\) and
\(g \approx 50\,\text{MHz}\) for device A, and 330 MHz and about 120 MHz for device
B, which stay targets for electromagnetic calibration of the island and readout gap.
gatemon_coupler models the coupling CPW with a lumped 5.4 fF branch to the island,
its third port referencing the island edge across the etched gap without adding metal
there. gatemon_island adds the grounded Josephson inductance, a 49.1 fF island
shunt, 20 fF island-to-gate and 35 fF gate-to-ground. Each contact overlaps the top gate
by 18 µm², and 30 nm hBN of relative permittivity 3.4 puts a parallel-plate contact near
18 fF. These are nominal hybrid estimates that still need a measured flake thickness and
a full-stack extraction.
The junction is linearized, so the simulation gives the qubit’s loading and the gate-tuned readout response but not the nonlinear spectrum or decoherence. The gate ports use a 50 ohm reference, so a matched gate loads the qubit; the paper’s bias tee grounds its RF branch and acts as a reflective filter, so use an ideal RF-grounded gate for frequency and coupling checks. The nonlinear ABS Hamiltonian instead gives an isolated \(f_{01}\) near 4.447 GHz for the nominal charging energy, and reproducing the paper’s quantum-dot parity transitions needs its extended Hamiltonian [RAP+25]. Broad sweeps can miss the maxima of narrow resonances, so use a local frequency sweep before interpreting their heights.
Drum LC resonator#
drum_capacitances returns, in F, the coupling \(C_\text{c}\), the parasitic
\(C_\text{p}\) and the drum capacitance \(C_\text{d}\) of a
drum_lc_resonator layout, from coupling_gap, coupling_pad_size, radius
and turn_length in um. drum_lc_resonator_fem_model() returns the lumped SAX
model with these values in place of its coupling_capacitance and
parasitic_capacitance inputs. The extracted \(C_\text{d}\) replaces the
analytical one at the stack gap, and the gap argument still tunes the analytical
parallel-plate term. The bundled drum_lc_resonator_palace dataset holds 54 Palace
solves over coupling_gap, coupling_pad_size, radius and turn_length, and
both functions interpolate it.
The recipe qpdk/models/datasets/recipes/drum.py is a 3D electrostatic solve of the
whole cell with its feedline in a finite coplanar ground. Its terminals are the node
(pad, wiring and bottom electrode), the VG_TOP membrane one vacuum gap above it, the
feedline cut open beyond the ports, and seventeen blocks of meander runs separated by 1
um slits. The recipe weights the Maxwell matrix with the node at 1, the membrane and
feed at 0 and each meander block at its mean position along the wire: the feed and
membrane charges give \(C_\text{c}\) and \(C_\text{d}\), and the remaining
energy gives \(C_\text{p}\). The full matrix is stored so other weightings can be
applied. A 2D sheet solve cannot hold the membrane, so the recipe is 3D. Conductors are
zero-thickness sheets by default; conductors="solid" extrudes M1 and the membrane. A
gmsh box field sets 0.14 um elements in the plane of the 70 nm gap. The 1 um near-metal
and 60 um far-field sizes, 600 um padding and third-order basis functions are the
published settings. At the default geometry the solve gives 5.05 fF, 82.46 fF and 26.82
fF. With the analytical meander those values put the lumped transmission zero near 4.0
GHz, below the 6.7 GHz that the analytical defaults predict with the paper’s 16 fF
parasitic.
The embedded convergence study compares that setting with a 0.07 um gap mesh, a 2 um near-metal mesh, fourth-order elements and a 1200 um domain, all at the default geometry and the published segmentation. The coupling moves by 0.05 %, 1.33 %, 0.55 % and less than 0.01 % respectively, inside the 2 % tolerance, and the parasitics by at most 0.73 %, inside their 1 % tolerance. The segmentation is the largest approximation: block midpoint weights form a staircase of the linear voltage profile, \(C_\text{c}\) rises 1.92 % from five blocks to nine and 0.33 % from thirteen to seventeen, so the published seventeen still sit about 0.7 % under the continuous profile while the other quantities stay within 0.1 %. Solid conductors are a different model rather than a refinement, and they move \(C_\text{d}\) by -6.4 %, a difference a 0.14 um mesh cannot resolve through a 120 nm membrane. Three holdout solves at off-grid points interpolate high by 4.6 to 5.1 % in \(C_\text{c}\), the chord error of a doubling gap axis on an exponentially decaying coupling, and by at most 3.3 % in \(C_\text{d}\).
Run uv run --script qpdk/models/datasets/recipes/drum.py convergence to reproduce
the study at the default geometry. The linear voltage profile is a lumped approximation:
the default meander is about 23 mm of wire, a substantial fraction of a guided
wavelength near 7 GHz, so the driven meander recipe is the reference for its distributed
response.
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/recipes/ 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/recipes/plate_capacitor.py --dry-run
uv run --script qpdk/models/datasets/recipes/plate_capacitor.py --processes 4
uv run --script qpdk/models/datasets/recipes/plate_capacitor.py --sif /path/to/palace.sif
Copy the script beside the original in qpdk/models/datasets/recipes/ 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 refinement measures mesh sensitivity
only; check larger domains and held-out-geometry interpolation 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 topology 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/recipes/cpw_coupling.py
sbatch --array=0-1 --cpus-per-task=4 qpdk/models/datasets/recipes/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.
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.
For a full-wave dataset, copy meander_inductor.py and use the shared
_driven.Recipe and _driven.run_cli helpers. Supply your geometry grid,
frequencies in Hz and port names in matrix order. The prepare function builds and
meshes a driven simulation, sets an independent excitation at each port, writes the
config, and returns the simulation. Record the reference impedances, reference planes
and materials in the recipe provenance.
The helper runs the complete frequency sweep once per geometry, retains all complex
matrix entries and resumes completed solves. Geometry shards retain the full frequency
axis. Missing ports, frequencies or failed solves cannot be merged into a complete
dataset. Passivity and optional reciprocity checks apply to any number of ports. These
checks do not replace mesh refinement or held-out interpolation checks. For
nonreciprocal media, set reciprocal=False in the recipe.
Load the results with s_parameters_model(path, **variants). The returned SAX model
interpolates frequency and continuous geometry axes together and returns NaN outside the
stored domain. Select discrete variants when loading. Complex linear interpolation can
introduce apparent loss between sample points, so check withheld frequencies near
resonances before choosing the frequency spacing.
Dataset API#
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:
- 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:
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:
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:
- 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)
- 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.
- 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:
- 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)
- 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:
- Return type:
- 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}\).
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.- Returns:
SAX model with the dataset terminal names.
- Raises:
ValueError – If the quantity is not a physical real Maxwell matrix.
- Parameters:
- Return type:
- 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:
- 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.- Returns:
SAX model with the dataset terminal names.
- Raises:
ValueError – If the quantity or frequency axis has the wrong convention.
- Parameters:
- Return type: