Adapter Circuit
Circuit-domain preparation workflows from netlist or spectrum inputs into Builder-ready artifacts.
Use this chapter when your starting point is a circuit description rather than Builder-ready H or FRF data. It shows how Adapter Circuit turns netlists and RAW cases into matrices, source-side waveforms, probes, and Builder-ready handoff data.
Use this chapter for workflow meaning and integration choices. If you only need exact command flags, jump to CLI Reference. If you only need to check whether your input deck is inside the supported subset, jump to Element Reference.
Read this chapter in three passes if you need to:
- get one common task working from the CLI
- embed the adapter through the C API
- tune advanced behavior such as solver policy, planning, or performance
Start with the shortest supported path that matches your question. If your goal
is a qualified .pack with minimal glue, jump to the workflow path first. If
your goal is circuit visibility, start with the CLI matrix / probe flows. Drop
to the lower-level C API only when you need explicit phase control or
intermediate artifacts.
For most integrations, Adapter Circuit falls into one of these roles:
| Integration goal | Recommended starting surface |
|---|---|
netlist or RAW -> .pack as fast as possible | workflow API |
| inspect matrices or probes before building a pack | CLI matrix / probe path |
| embed circuit compilation and sweeps inside host code | compile-then-compute C API |
What Adapter Circuit Owns
Imagine you have a circuit — maybe a SPICE netlist from your PDK, maybe a PSS/E RAW case from a grid study, maybe a custom deck with frequency-dependent NPORT elements. You need to get that circuit into TDSE so Builder can produce a runtime pack. But Builder doesn't understand circuits. It understands frequency-domain matrices and time-domain source waveforms.
Adapter Circuit bridges that gap. It takes circuit descriptions, compiles them into a solvable form, and produces the outputs Builder needs: Y and Z matrices over frequency, VOC and ISC time sequences, and probe data for validation. It also handles RAW import — converting PSS/E power-system cases into circuit netlists.
Circuit Input Adapter Circuit Builder Runtime
(netlist, RAW) → Y/Z, VOC/ISC, → .pack file → step loop
probe outputs
The separation between layers is deliberate:
- Adapter Circuit owns circuit parsing, solve orchestration, and circuit-domain validation. It knows about nodes, branches, and MNA stamping. It does not know about Builder packs or Runtime step loops.
- Builder converts validated frequency-domain data into a runtime pack. It knows about rational fitting and passivity enforcement. It does not know about circuits.
- Runtime executes the pack against a host solver. It knows about time stepping and conductance matrices. It does not know about circuits or fitting.
This layering means you can swap any piece independently. Use a different circuit tool to produce Y matrices? Feed them directly to Builder. Want to use the circuit solver without Builder? Adapter Circuit's probe and series commands give you standalone access.
When to Use It
You should reach for Adapter Circuit when your source data begins as:
- A SPICE-like netlist (
.cir,.sp, or text) - A PSS/E RAW case file that needs conversion
- A circuit deck containing
NPORTelements that reference Touchstone files (.ynp,.znp,.y2p,.s2p, etc.)
The typical workflow is: compile a netlist, run one or more compute operations, then hand the results to Builder. But you can also use Adapter Circuit standalone — for circuit validation, for exploring frequency response, or for generating probe data.
What It Produces
Every Adapter Circuit operation falls into one of these categories:
- Frequency-domain matrices:
Y(jω)orZ(jω)over a user-specified frequency grid. These are the primary input to Builder'sh_from_spectrum. - Source-side time sequences: Open-circuit voltage (
VOC) or short-circuit current (ISC) waveforms. These define the Norton/Thevenin equivalents that drive the TDSE model in a host solver. - Probe outputs: Internal node voltages or branch currents, in either AC (frequency sweep) or transient (time-domain) form. Probes let you inspect what's happening inside the circuit without extracting full port matrices.
- Diagnostics: Structured data describing parser behavior, solver backend selection, matrix conditioning, and policy decisions. Every result struct carries a diagnostics block.
Header Map
Most users only need tdse_adapter_circuit.h. Pull in narrower headers only when you are using those features directly:
| Header | Purpose |
|---|---|
tdse_adapter_circuit.h | Umbrella include — pulls in everything |
tdse_adapter_circuit/common.h | Handle lifecycle, error codes, core data types |
tdse_adapter_circuit/compile.h | Netlist compilation, compiled info queries |
tdse_adapter_circuit/compute.h | Matrix sweeps, port series, probes, region preparation |
tdse_adapter_circuit/raw.h | RAW import and conversion (PSS/E → netlist) |
tdse_adapter_circuit/policy.h | Solver policy and backend selection |
tdse_adapter_circuit/diagnostics.h | Diagnostics, policy tracing, solver statistics |
tdse_adapter_circuit/planning.h | Adaptive sweep planning and tail-vs-nfreq analysis |
tdse_adapter_circuit/seq.h | Sequence network import (.seq files) and fault analysis |
tdse_adapter_circuit/init.h | Struct initializers, convenience helpers, macros |
tdse_adapter_circuit.hpp | C++ wrapper with RAII and exception-based errors |
tdse_workflow.h | High-level workflows (e.g. tdse_workflow_netlist_to_pack) |
For most integration work, treat this table as a lookup aid rather than required reading.
Quick Start
Before diving into the full API, here is the shortest path from a netlist to a
frequency-domain matrix. If you have a SPICE file called my_circuit.cir and you want
its Y matrix at DC plus four positive frequencies:
tdse adapter circuit matrix \
--netlist-kind file --netlist my_circuit.cir \
--matrix y --ports "1,0" \
--w0-radps 0 --dw-radps 100 --nfreq 5 \
--out-data matrix.csv --json-out -
This single command compiles the netlist, solves the circuit at each frequency, extracts
the port admittance, and writes the results to matrix.csv. The --json-out - flag
prints a machine-readable envelope to stdout — you will want this in any automated
workflow.
The output CSV contains one row per frequency, with the real and imaginary parts of each
Y matrix element in interleaved order: re(Y11), im(Y11), re(Y12), im(Y12), ....
For runnable examples of every CLI command and C API pattern, see the Examples Guide.
Choose Your Path
Use this table before reading deeper:
| If you need to... | Read first | Then use |
|---|---|---|
turn a netlist or RAW case into a .pack with minimum glue | Recommended Workflow API Path | Examples Guide |
| get a Y or Z matrix for Builder | Common CLI Tasks | Builder Handoff |
| convert a PSS/E RAW case | RAW Import | Builder Handoff |
| inspect internal circuit behavior | probe — Observe Internal Voltages or Currents | Diagnostics |
| embed the adapter in host code | Core C API Tasks | Embedding in a Host Solver (MNA) |
| tune backend choice or sweep strategy | Solver Policy | Advanced Tuning And Performance |
For exhaustive CLI flags and machine-readable CLI outputs, use CLI Reference. This chapter keeps the command examples and the adapter-side meaning, not every CLI contract detail.
Common End-To-End Paths
Most users do not need this whole chapter at once. They need one of these short paths:
| Starting point | Short path |
|---|---|
| SPICE-like netlist -> Builder pack | matrix -> Builder handoff -> Runtime create |
| PSS/E RAW case -> Builder pack | raw-to-netlist -> matrix -> Builder handoff |
| netlist -> validation data only | probe or series -> diagnostics review |
If your goal is a .pack file, stay focused on matrix generation and Builder handoff first. Probe and series workflows are more useful when you are validating the circuit model or debugging a mismatch.
Minimal Integration Decision
Before writing code, decide which side of the boundary needs to own intermediate artifacts:
- if the host only needs a qualified
.pack, use the workflow path - if the host must archive or inspect Y/Z, VOC/ISC, or probe outputs, use Adapter directly
- if the host must control every phase boundary, use compile-then-compute and then hand off to Builder explicitly
Recommended Workflow API Path
If your real goal is "turn this netlist or RAW case into a qualified .pack
with as little hand-stitched glue as possible," prefer the workflow API before
you build your own Adapter -> Builder orchestration.
The public workflow surface covers three common starting points:
tdse_workflow_netlist_to_pack(...)tdse_workflow_raw_to_pack(...)tdse_workflow_yz_matrix_to_pack(...)
Why this path is valuable:
- it keeps Builder planning inputs in one request object
- it records whether the grid or sweep plan was auto-derived
- it returns passivity and round-trip verification in the same result
- it gives you one status surface that still preserves underlying Adapter, Builder, Runtime, and N-port outcomes
Use the workflow API when:
- you want the shortest supported path from circuit input to
.pack - your host does not need to intercept every intermediate matrix artifact
- you want one qualification result that already includes pack-quality signals
Drop down to manual Adapter + Builder handoff only when you need to:
- archive intermediate Y or Z matrices explicitly
- inject a custom Builder correction or tail-processing sequence outside the workflow defaults
- debug a mismatch by isolating compile, sweep, Builder conversion, and Runtime verification as separate phases
For a runnable public example, start with workflow_cpp_quickstart in the
Examples Guide.
Workflow ownership split:
- workflow / Adapter / Builder own circuit compilation, planning, and pack construction
- the host owns source-file selection, workflow parameters, artifact archival, and the later Runtime embedding choice
Mental Model
Before you start writing code, it helps to understand the two fundamental patterns in Adapter Circuit.
The compile-then-compute pattern. You compile a netlist once, which gives you a handle — an opaque object that holds the parsed and analyzed circuit. That handle is then reused across as many compute calls as you need. Compilation is the expensive step (parsing, topology analysis, MNA construction). Compute calls are relatively cheap, and you can run them with different parameters — different frequency grids, different ports, different time steps — against the same handle.
compile_from_netlist(&req, &result) → handle
│
├── compute_port_fsweep(handle, ...) → Y or Z matrix
├── compute_port_series(handle, ...) → VOC or ISC time series
├── compute_probes(handle, ...) → internal voltage/current
└── compute_port_fsweep(handle, ...) → another sweep, same handle
The two-output pattern. Every Adapter Circuit function that produces data follows a
two-call pattern. First call: pass NULL for the output buffer and zero for its length
— the result tells you how much space you need. Second call: allocate and pass the real
buffer. This avoids guessing buffer sizes. The _init() functions and the sizing pass
handle the details for you.
Common CLI Tasks
The CLI is the fastest way to validate a netlist, explore a circuit, or generate Builder-facing artifacts. The sections below explain the intent of each command family. Use CLI Reference when you need exhaustive flag or output-field detail.
All adapter circuit commands share two conventions: --json-out <path|->
for machine-readable output, and --netlist-kind <file|text|stdin> for specifying how
the netlist is provided.
If you are writing a script, always use --json-out - (stdout) and parse the JSON
envelope. The human-readable output is for interactive use only — its format may change
between versions.
caps — Check Available Backends
Before you run any computation, you need to know what solver backends are available on
your machine. The caps command answers this: it prints a table of every backend the
SDK was built with, and whether each one is usable right now.
tdse adapter circuit caps --out-caps - --json-out -
This is the first thing to run on a new machine or after upgrading the SDK. A backend might show as unavailable because a required library is missing (CUDA, MKL), because the GPU driver is outdated, or because the backend was not compiled into this build.
| Flag | Meaning |
|---|---|
--out-caps <path|-> | Write backend capability table (default: stdout) |
--out-build-features <path|-> | Write build-time feature flags as JSON |
--json-out <path|-> | Machine-readable output envelope |
matrix — Compute Y or Z over Frequency
This is the workhorse command. You have a netlist and a set of ports, and you want the
admittance (Y) or impedance (Z) matrix at each frequency in a grid. The output goes
directly to Builder — the port order you specify here must match the port order you
use in h_from_spectrum.
tdse adapter circuit matrix \
--netlist-kind file --netlist my_circuit.cir \
--matrix y --ports "1,0;2,0" \
--w0-radps 0 --dw-radps 100 --nfreq 5 \
--out-data matrix.csv --json-out -
The frequency grid is defined by three numbers: a start frequency w0 in rad/s, a step
dw in rad/s, and a count nfreq. The grid covers w0, w0 + dw, w0 + 2*dw, ...,
w0 + (nfreq-1)*dw. For a DC start use --w0-radps 0.
| Flag | Meaning | Default |
|---|---|---|
--netlist-kind file|text|stdin | How the netlist is provided | (required) |
--netlist <path_or_text> | Netlist source (omit for stdin) | — |
--matrix y|z | Admittance (Y) or impedance (Z) | (required) |
--ports "p,n;p,n;..." | Port definitions by numeric node index. String names in C API | (required) |
--w0-radps | Start frequency in rad/s. Use 0 for DC | (required) |
--dw-radps | Frequency step in rad/s | (required) |
--nfreq | Number of frequency points | (required) |
--dc-policy | DC frequency resolution policy | exact_dc_then_fallback |
--dc-extrapolate-points <N> | Points used for DC extrapolation | 4 |
--out-data <path|-> | Output CSV file path | — |
DC policy values:
Solving at exactly zero frequency (DC) is harder than it sounds. Inductors become shorts, capacitors become opens, and the MNA matrix can become singular. These policies control how the solver handles ω = 0:
| CLI value | Enum constant | Description |
|---|---|---|
regularized_exact_dc | W0_REGULARIZED_EXACT_DC | Adds small conductance to regularize inductors at DC |
extrapolate_from_positive | W0_EXTRAPOLATE_FROM_POSITIVE | Skips DC, extrapolates from positive-frequency data |
exact_dc_then_fallback | W0_EXACT_DC_THEN_FALLBACK | Exact DC first; regularization fallback if singular |
The default (exact_dc_then_fallback) is the right choice for most circuits. Switch to
extrapolate_from_positive if your circuit has no DC path to ground (floating nets,
ideal transformers) and regularization isn't helping.
series — Compute VOC or ISC Time Sequences
The matrix command gives you frequency-domain behavior. But to drive a time-domain
simulation, you often need source waveforms — the open-circuit voltage or short-circuit
current seen at each port as a function of time. That's what series computes.
tdse adapter circuit series \
--netlist-kind file --netlist my_circuit.cir \
--series voc --ports "1,0" \
--method transient --dt 1e-4 --steps 64 \
--out-data series.csv --json-out -
The output is a time series: one row per time step, one column per port. The time at
step k is t0 + k * dt.
| Flag | Meaning | Default |
|---|---|---|
--series voc|isc | Open-circuit voltage or short-circuit current | (required) |
--ports "p,n;p,n;..." | Port definitions | (required) |
--method transient|ifft|tone | Synthesis method | (required) |
--dt | Time step in seconds | (required) |
--steps | Number of time steps | (required) |
--nfft <N> | FFT size for ifft method | — |
--t0 <t0> | Start time offset | 0 |
--out-data <path|-> | Output CSV file path | — |
Choosing a synthesis method:
The method you pick depends on your circuit and what accuracy you need:
| Method | When to use |
|---|---|
transient | General-purpose time-domain solver. Handles nonlinear devices and switching |
ifft | Frequency-domain sources, linear circuit. Faster than transient. Requires nfft ≥ 2*(nfreq-1) |
tone | Single-frequency steady-state analysis. Use for AC steady-state verification, not for transient simulation |
If you are unsure, start with transient. It is slower but always correct. Once you
have validated results, you can experiment with ifft for speed.
probe — Observe Internal Voltages or Currents
Port matrices tell you what happens at the terminals. But often you need to see inside the circuit — the voltage at an internal node, the current through a particular branch. Probes let you instrument the circuit and extract those values without modifying the netlist.
# AC probe — frequency sweep of internal quantities
tdse adapter circuit probe \
--domain ac --netlist-kind file --netlist my_circuit.cir \
--observe "v(1);v(2,0)" --observe-source argument \
--ac-excitation small_signal \
--sweep-kind lin --fstart-hz 50 --fstop-hz 5000 --sweep-points 100 \
--out-data probe.csv --json-out -
# Transient probe — time-domain waveform of internal quantities
tdse adapter circuit probe \
--domain transient --netlist-kind file --netlist my_circuit.cir \
--observe "v(1);i(r1)" --observe-source argument \
--method transient --dt 1e-4 --steps 64 \
--out-data probe.csv --json-out -
Probes work in both AC and transient domains. In the AC domain, you get frequency sweeps of complex voltages and currents. In the transient domain, you get time-domain waveforms. The probe expressions use a simple syntax:
| Expression | Meaning |
|---|---|
v(n) | Voltage at node n relative to ground |
v(p,n) | Voltage between nodes p and n |
i(r1) | Current through element named r1 |
i(vsrc) | Current through independent source vsrc |
| Flag | Meaning | Default |
|---|---|---|
--domain ac|transient | AC small-signal or transient time-domain | (required) |
--observe "v(n);v(p,n);i(element);..." | Probe expressions, semicolon-separated | — |
--observe-source argument|netlist|argument_or_netlist | Probe definition source | argument_or_netlist |
--ac-excitation small_signal|legacy_tones | AC excitation mode | small_signal |
--sweep-kind from_netlist|lin|dec|oct|list | Frequency sweep type for AC | from_netlist |
--sweep-points <N> | Number of points for lin/dec/oct sweeps | — |
--fstart-hz <f0> | Start frequency in Hz | — |
--fstop-hz <f1> | Stop frequency in Hz | — |
--freq-list-hz <f0,f1,...> | Explicit frequency list for list sweep | — |
--method transient|ifft|tone | Synthesis method (transient probes) | (required) |
--dt, --steps | Time step and count (transient probes) | (required) |
--nfft <N> | FFT size (transient) | — |
--t0 <t0> | Start time offset (transient) | 0 |
The --observe-source flag is worth understanding. When set to argument, the CLI
uses the probes you specify with --observe. When set to netlist, it reads .probe
directives from the netlist itself. The default argument_or_netlist checks both: if
you pass --observe it uses those; otherwise it falls back to netlist probes.
When to Use Which Command
If you are new to Adapter Circuit, this table is your cheat sheet:
| You want to... | Use |
|---|---|
| Check what solvers are available on this machine | caps |
Get Y or Z over frequency for Builder handoff | matrix |
Get VOC or ISC time-domain waveforms for a host solver | series |
| Observe internal voltages or branch currents for validation | probe |
RAW Import
Not every circuit starts as a SPICE netlist. In power systems, the starting point is often a PSS/E RAW case file — a grid description with buses, generators, branches, loads, and transformers. RAW Import converts these files into circuit netlists that Adapter Circuit can compile.
The conversion process reads the RAW file, builds an equivalent circuit model for each power-system element, stitches them together, and writes out a SPICE netlist. Base- frequency validation runs automatically — the generated netlist is solved at the nominal system frequency and the resulting bus voltages and branch flows are compared against the original RAW data.
CLI
tdse adapter circuit raw-to-netlist \
--raw-kind file --raw case.raw \
--out-netlist converted.cir \
--unit pu --transformer-model ideal_tap_series --load-model shunt \
--include-generator-sources 1 --json-out -
The three model choices — transformer, load, and generator — control how faithfully the netlist reproduces the RAW case behavior:
| Flag | Meaning | Default |
|---|---|---|
--raw-kind file|text|stdin | How the RAW input is provided | (required) |
--raw <path_or_text> | RAW source | (required) |
--out-netlist <path|-> | Output netlist path | — |
--out-report-json <path|-> | Output validation report | — |
--unit pu|si | Per-unit or SI output | pu |
--transformer-model series_only|ideal_tap_series|tap_split_shunt | Equivalent circuit model | ideal_tap_series |
--load-model shunt|series|zip_split | Load equivalent model | shunt |
--include-generator-sources 0|1 | Include generator Norton equivalents | 1 |
--include-fallback-source 0|1 | Add fallback source for unexcited buses | 1 |
--include-tran 0|1 | Emit .tran directive | 1 |
--include-options 0|1 | Emit .options directive | 1 |
--dt <sec> | Time step for .tran directive | 5e-5 |
--tstop <sec> | Stop time for .tran directive | 0.4 |
--freq-hz|--nominal-frequency-hz <hz> | Base frequency in Hz | 60 |
--global-bus-shunt-c-f <farad> | Global bus shunt capacitance | 0 |
--include-inactive-bus 0|1 | Include inactive buses in validation | 0 |
--ac-adjust-from-sine 0|1 | Adjust AC phasors from sine reference | 1 |
--mag-tol-pu <x> | Magnitude tolerance for validation | 0.03 |
--ang-tol-deg <x> | Angle tolerance in degrees | 3.0 |
--complex-tol-pu <x> | Complex tolerance for validation | 0.08 |
--report-top-k <N> | Top-K worst-case buses in the report | 20 |
C API
The C API for RAW import uses the same pattern as the rest of Adapter Circuit:
configure a request struct, call the function, and check the result. It uses the
same tdse_adapter_circuit_err_t error type, so tdse_adapter_circuit_status_message()
works for both adapter and RAW errors.
#include <tdse_adapter_circuit/raw.h>
tdse_adapter_circuit_raw_import_options_t import_opt;
memset(&import_opt, 0, sizeof(import_opt));
import_opt.struct_size = sizeof(import_opt);
import_opt.unit_mode = TDSE_ADAPTER_CIRCUIT_RAW_UNIT_PU;
import_opt.transformer_model =
TDSE_ADAPTER_CIRCUIT_RAW_TRANSFORMER_IDEAL_TAP_SERIES;
import_opt.load_model = TDSE_ADAPTER_CIRCUIT_RAW_LOAD_SHUNT;
import_opt.include_generator_sources = 1;
tdse_adapter_circuit_raw_options_t opt;
memset(&opt, 0, sizeof(opt));
opt.struct_size = sizeof(opt);
opt.import_options = import_opt;
opt.validation_options.struct_size =
sizeof(tdse_adapter_circuit_raw_validation_options_t);
tdse_adapter_circuit_raw_request_t req;
memset(&req, 0, sizeof(req));
req.struct_size = sizeof(req);
req.source_kind = TDSE_ADAPTER_CIRCUIT_RAW_SOURCE_FILE;
req.output_kind = TDSE_ADAPTER_CIRCUIT_RAW_OUTPUT_FILE;
req.raw_source = "case.raw";
req.out_netlist_path = "converted.cir";
req.options = &opt;
tdse_adapter_circuit_raw_result_t result;
memset(&result, 0, sizeof(result));
result.struct_size = sizeof(result);
int rc = tdse_adapter_circuit_raw_to_netlist(&req, &result);
There are two output modes. When output_kind is RAW_OUTPUT_FILE, the netlist is
written to out_netlist_path. When output_kind is RAW_OUTPUT_BUFFER, the adapter
allocates the output and you release it with tdse_adapter_circuit_raw_free_owned_output().
Use the buffer mode when you want to pipe the netlist directly into compilation without
touching disk.
The legacy function tdse_adapter_circuit_raw_status_message() still works and
delegates to the unified status message, but new code should use
tdse_adapter_circuit_status_message().
For runnable examples, see raw_import_api (C API) and raw_import_cpp (C++ wrapper)
in the Examples Guide.
If you want the full RAW-to-pack pipeline in a single call, use the workflow
tdse_workflow_raw_to_pack() which chains RAW import, compilation, frequency
sweep (or adaptive planning), and Builder handoff into one operation.
Transformer Models
Transformers in PSS/E RAW files have tap ratios and phase shifts. The model you choose determines how these are represented in the circuit netlist:
| Model | Enum constant | Description |
|---|---|---|
series_only | RAW_TRANSFORMER_SERIES_ONLY | Only series impedance, ignores tap ratio. Simplest |
ideal_tap_series | RAW_TRANSFORMER_IDEAL_TAP_SERIES | Ideal tap in series with leakage impedance |
tap_split_shunt | RAW_TRANSFORMER_TAP_SPLIT_SHUNT | Tap split into series + shunt. Most physical, more nodes |
Load Models
Loads (constant power, constant current, constant impedance) are converted to circuit elements. The model controls the equivalent:
| Model | Enum constant | Description |
|---|---|---|
shunt | RAW_LOAD_SHUNT | Constant admittance to ground. Works well for most studies |
series | RAW_LOAD_SERIES | Series impedance. Use when you need to preserve the branch topology |
zip_split | RAW_LOAD_ZIP_SPLIT | Separates constant-Z, I, P components. Most accurate for load-flow |
Validation Output
After conversion, the import result includes a validation summary. This tells you whether the generated netlist reproduces the original RAW case behavior:
| Field | Meaning |
|---|---|
bus_count | Total buses in the RAW case |
active_bus_count | Buses with defined voltage (excludes isolated/de-energized buses) |
generator_count | Number of generators converted to Norton sources |
branch_count | Number of branches (lines + transformers) |
warning_count | Number of non-fatal warnings during conversion |
netlist_required_len | Size of the generated netlist in bytes |
Core C API Tasks
The C API is what you use when integrating Adapter Circuit into a host application. It is organized around a compile-then-compute pattern: compile a netlist once to get a handle, then run as many compute operations as you want against that handle.
Most integrations only need four C API tasks:
- compile a netlist
- compute a matrix, series, or probe output
- inspect diagnostics on failure or during tuning
- destroy the handle cleanly when the work is done
All functions return a tdse_adapter_circuit_err_t error code. Call
tdse_adapter_circuit_status_message(code) for a human-readable description and
tdse_adapter_circuit_get_last_error_text() for thread-local diagnostic detail.
If you prefer C++, the header tdse_adapter_circuit.hpp provides a thin wrapper with
RAII handle management, exception-based error handling, and convenience constructors.
The underlying solver is the same — the C++ wrapper is pure syntactic convenience.
Init Functions
Most structs in Adapter Circuit have a corresponding _init() function. These functions
zero-initialize the struct and set struct_size correctly. Always use them — they
protect you from ABI issues when structs grow new fields in future SDK versions.
| Init function | Returns |
|---|---|
tdse_adapter_circuit_options_init() | tdse_adapter_circuit_options_t |
tdse_adapter_circuit_region_spec_init() | tdse_adapter_circuit_region_spec_t |
tdse_adapter_circuit_time_options_init() | tdse_adapter_circuit_time_options_t |
tdse_adapter_circuit_probe_options_init() | tdse_adapter_circuit_probe_options_t |
tdse_adapter_circuit_ac_probe_options_init() | tdse_adapter_circuit_ac_probe_options_t |
tdse_adapter_circuit_ac_sweep_init() | tdse_adapter_circuit_ac_sweep_t |
tdse_adapter_circuit_node_voltage_probe_init(p, n) | tdse_adapter_circuit_probe_def_t |
tdse_adapter_circuit_branch_current_probe_init(name) | tdse_adapter_circuit_probe_def_t |
tdse_adapter_circuit_named_port_def_init(p, n) | tdse_adapter_circuit_named_port_def_t |
tdse_adapter_circuit_compile_request_init() | tdse_adapter_circuit_compile_request_t |
tdse_adapter_circuit_compile_result_init() | tdse_adapter_circuit_compile_result_t |
tdse_adapter_circuit_port_fsweep_request_init() | tdse_adapter_circuit_port_fsweep_request_t |
tdse_adapter_circuit_port_fsweep_result_init() | tdse_adapter_circuit_port_fsweep_result_t |
tdse_adapter_circuit_port_series_request_init() | tdse_adapter_circuit_port_series_request_t |
tdse_adapter_circuit_port_series_result_init() | tdse_adapter_circuit_port_series_result_t |
tdse_adapter_circuit_probe_compute_request_init() | tdse_adapter_circuit_probe_compute_request_t |
tdse_adapter_circuit_probe_compute_result_init() | tdse_adapter_circuit_probe_compute_result_t |
tdse_adapter_circuit_diagnostics_summary_init() | tdse_adapter_circuit_diagnostics_summary_t |
tdse_adapter_circuit_policy_trace_init() | tdse_adapter_circuit_policy_trace_t |
tdse_adapter_circuit_prepare_region_request_init() | tdse_adapter_circuit_prepare_region_request_t |
tdse_adapter_circuit_prepare_region_result_init() | tdse_adapter_circuit_prepare_region_result_t |
tdse_adapter_circuit_seq_options_init() | tdse_adapter_circuit_seq_options_t |
tdse_adapter_circuit_seq_network_compile_request_init() | tdse_adapter_circuit_seq_network_compile_request_t |
tdse_adapter_circuit_seq_network_compile_result_init() | tdse_adapter_circuit_seq_network_compile_result_t |
tdse_adapter_circuit_seq_fault_request_init() | tdse_adapter_circuit_seq_fault_request_t |
tdse_adapter_circuit_seq_fault_result_init() | tdse_adapter_circuit_seq_fault_result_t |
Related helper families are grouped by header:
planning.h:_adaptive_sweep_config_init,_adaptive_sweep_request_init,_adaptive_sweep_result_init,_tail_vs_nfreq_request_init,_tail_vs_nfreq_result_initraw.h:_raw_request_init,_raw_result_init,_raw_options_initseq.h: the five_seq_*_initfunctions listed above
Use this table as a lookup aid. You do not need to memorize the full list before calling the core compile and compute APIs.
Configuring Newton Solver Tolerances
tdse_adapter_circuit_time_options_init() also sets these appended fields
(gated on struct_size; safe defaults for standard circuits):
| Field | Default | Description |
|---|---|---|
newton_abs_tol | 1e-8 | Absolute convergence tolerance for Newton iterations |
newton_rel_tol | 1e-6 | Relative convergence tolerance |
newton_residual_tol | 1e-6 | Residual norm tolerance |
newton_max_iterations | 32 | Maximum Newton iterations per timestep (0 = use default) |
Adjust these when circuits exhibit slow convergence (try relaxing tolerances) or require higher precision (tighten tolerances).
Compile a Netlist
Compilation is always the first step. You provide a netlist (as a file path or as an in-memory string) and receive a handle:
#include <tdse_adapter_circuit.h>
tdse_adapter_circuit_options_t opt = tdse_adapter_circuit_options_init();
opt.policy = TDSE_ADAPTER_CIRCUIT_W0_EXACT_DC_THEN_FALLBACK;
opt.inductor_gbig = 1e12;
tdse_adapter_circuit_compile_request_t req =
tdse_adapter_circuit_compile_request_init();
req.source_kind = TDSE_ADAPTER_CIRCUIT_NETLIST_SOURCE_FILE;
req.netlist_source = "my_circuit.cir";
req.options = opt;
tdse_adapter_circuit_compile_result_t result =
tdse_adapter_circuit_compile_result_init();
int rc = tdse_adapter_circuit_compile_from_netlist(&req, &result);
// result.handle is now ready for compute calls
The options struct controls DC policy and inductor modeling. If you are using the
default EXACT_DC_THEN_FALLBACK policy, inductor_gbig sets the conductance used
to regularize inductors when the exact DC solve fails. The default of 1e12 works
for most circuits.
Query a Compiled Handle
Once you have a compiled handle, you can ask it about the circuit it contains. The most common queries are node count, MNA matrix size, and node names:
tdse_adapter_circuit_compiled_info_t info;
memset(&info, 0, sizeof(info));
info.struct_size = sizeof(info);
tdse_adapter_circuit_get_compiled_info(result.handle, &info);
// info.node_count — total public nodes
// info.internal_node_count — nodes created by MNA stamping
// info.mna_matrix_size — dimension of the MNA system
// Query a public node name by index (0 = ground when present):
size_t required_len;
tdse_adapter_circuit_get_node_name(result.handle, 0, NULL, &required_len);
char* name = malloc(required_len);
tdse_adapter_circuit_get_node_name(result.handle, 0, name, &required_len);
Options
tdse_adapter_circuit_options_init() provides safe defaults. The fields you are most
likely to adjust:
| Field | Default | Meaning |
|---|---|---|
policy | W0_EXACT_DC_THEN_FALLBACK | How DC (zero frequency) is handled |
inductor_gbig | 1e12 | Conductance used to regularize inductors at DC |
dc_extrapolate_points | 4 | Number of positive-frequency points used when extrapolating DC |
Compute a Y Matrix
This is the most common operation. Given a compiled handle and a list of ports, compute the admittance matrix at every frequency in a grid:
tdse_adapter_circuit_port_def_t ports[] = {{1, 0}};
tdse_adapter_circuit_grid_t grid = {.w0 = 0.0, .dw = 100.0, .nfreq = 5};
tdse_adapter_circuit_port_fsweep_request_t freq_req =
tdse_adapter_circuit_port_fsweep_request_init();
freq_req.handle = result.handle;
freq_req.matrix_kind = TDSE_ADAPTER_CIRCUIT_MATRIX_Y;
freq_req.ports = ports;
freq_req.port_count = 1;
freq_req.grid = grid;
// Output buffer: 2 doubles per matrix element (real + imag),
// nfreq frequency points,
// port_count rows × port_count columns
size_t n_vals = 2 * grid.nfreq * port_count * port_count;
double* matrix_ri = malloc(n_vals * sizeof(double));
freq_req.out_matrix_ri = matrix_ri;
freq_req.out_matrix_ri_len = n_vals;
tdse_adapter_circuit_port_fsweep_result_t freq_result =
tdse_adapter_circuit_port_fsweep_result_init();
int rc = tdse_adapter_circuit_compute_port_fsweep(&freq_req, &freq_result);
The output is stored in interleaved real-imaginary format, row-major by port index.
For a 2-port Y matrix, element Y[p][q] at frequency k lives at:
matrix_ri[2 * (k * nports * nports + p * nports + q)] = real(Y_pq[k])
matrix_ri[2 * (k * nports * nports + p * nports + q) + 1] = imag(Y_pq[k])
Compute Port Series (VOC / ISC)
For time-domain excitation, you need waveforms not matrices. compute_port_series
produces open-circuit voltage or short-circuit current versus time:
tdse_adapter_circuit_port_series_request_t series_req =
tdse_adapter_circuit_port_series_request_init();
series_req.handle = result.handle;
series_req.response_kind = TDSE_ADAPTER_CIRCUIT_PORT_RESPONSE_VOC;
series_req.ports = ports;
series_req.port_count = 1;
series_req.dt = 1e-4;
series_req.steps = 64;
double* series_out = malloc(series_req.steps * port_count * sizeof(double));
series_req.out_values = series_out;
series_req.out_values_len = series_req.steps * port_count;
tdse_adapter_circuit_port_series_result_t series_result =
tdse_adapter_circuit_port_series_result_init();
int rc = tdse_adapter_circuit_compute_port_series(&series_req, &series_result);
The output is step-major: series_out[step * nports + port] gives the value at that
time step for that port.
Compute Probes
Probes observe internal circuit quantities without extracting full port matrices.
Define what you want to measure, then call compute_probes:
tdse_adapter_circuit_probe_def_t probe =
tdse_adapter_circuit_node_voltage_probe_init(1, 0);
// or: tdse_adapter_circuit_branch_current_probe_init("r1");
tdse_adapter_circuit_probe_compute_request_t probe_req =
tdse_adapter_circuit_probe_compute_request_init();
probe_req.handle = result.handle;
probe_req.domain = TDSE_ADAPTER_CIRCUIT_PROBE_DOMAIN_AC;
probe_req.probes = &probe;
probe_req.probe_count = 1;
// For AC: set ac_sweep and ac_probe_options
// For transient: set dt, steps, and time_options
tdse_adapter_circuit_probe_compute_result_t probe_result =
tdse_adapter_circuit_probe_compute_result_init();
int rc = tdse_adapter_circuit_compute_probes(&probe_req, &probe_result);
Each probe domain has different required parameters. To prevent mistakes — like passing transient time-step fields to an AC probe — two convenience wrappers expose only the relevant fields:
// AC probes — only frequency-sweep parameters are accepted
tdse_adapter_circuit_compute_ac_probes(
handle, probes, probe_count, ac_sweep, ac_probe_options, &result);
// Transient probes — only time-domain parameters are accepted
tdse_adapter_circuit_compute_transient_probes(
handle, probes, probe_count, dt, steps, time_options, &result);
Use these wrappers in new code. They make domain-mismatch mistakes a compile-time error instead of a runtime surprise.
Prepare a Circuit Region
Some netlists define named regions — groups of ports treated as a unit. Region preparation resolves a region name into an explicit port list and creates a prepared handle for it:
tdse_adapter_circuit_region_spec_t region =
tdse_adapter_circuit_region_spec_init();
region.name = "main";
tdse_adapter_circuit_prepare_region_request_t prep_req =
tdse_adapter_circuit_prepare_region_request_init();
prep_req.source_handle = result.handle;
prep_req.region = ®ion;
tdse_adapter_circuit_prepare_region_result_t prep_result =
tdse_adapter_circuit_prepare_region_result_init();
int rc = tdse_adapter_circuit_prepare_region(&prep_req, &prep_result);
// prep_result.resolved_port_count tells how many ports were resolved
This is most useful when your netlist defines regions with TDSE directives and you
want the adapter to enumerate the ports automatically instead of listing them by hand.
Named Ports
When you know node names but not their numeric indices, use named ports. The adapter resolves string names to indices during the compute call:
tdse_adapter_circuit_named_port_def_t ports[] = {
tdse_adapter_circuit_named_port_def_init("1", "0"),
tdse_adapter_circuit_named_port_def_init("OUT", "GND"),
};
freq_req.named_ports = ports;
freq_req.port_count = 2;
// Leave freq_req.ports = NULL — the adapter resolves names to indices.
Named ports work everywhere numeric ports work: port_fsweep_request_t,
port_series_request_t, and adaptive sweep / tail analysis requests. You can even mix
them — pass named_ports for resolution and leave ports NULL.
Handle Reuse and Thread Safety
A compiled handle is designed to be reused. Compile once, then run as many compute calls as you need:
tdse_adapter_circuit_compile_from_netlist(&req, &result);
for (int k = 0; k < num_sweeps; ++k) {
freq_req.handle = result.handle;
freq_req.grid = grids[k]; // different grid each iteration
tdse_adapter_circuit_compute_port_fsweep(&freq_req, &freq_result);
}
tdse_adapter_circuit_destroy(result.handle);
Thread safety: A handle is not safe for concurrent use from multiple threads. Each thread that needs to compute against a circuit should compile its own handle. The rule is one handle per execution thread — do not enter any compute or lifecycle API on the same handle from two threads simultaneously. This mirrors the Runtime contract and keeps the internal solver state simple and fast.
Progress and Cancellation
Long-running time-domain operations — transient probes, port series with many steps — can take seconds or minutes. You can receive progress updates and cancel in-flight computations:
tdse_adapter_circuit_time_options_t time_opt =
tdse_adapter_circuit_time_options_init();
// Progress callback — called periodically during the solve
time_opt.progress.enable = 1;
time_opt.progress.interval_sec = 0.5; // callback at most every 0.5 seconds
time_opt.progress.callback = my_progress_callback;
time_opt.progress.user_ptr = &my_state;
// Cancel callback — polled by the solver;
// return non-zero to request cancellation
time_opt.cancel.callback = my_cancel_callback;
time_opt.cancel.user_ptr = &my_state;
probe_req.time_options = &time_opt;
The progress event (tdse_adapter_circuit_progress_event_t) carries the current
simulation time, accepted and rejected step counts, nonlinear iteration counts, and
the transient integration scheme in use. The progress field is a number between 0
and 1 indicating overall completion.
Note: progress and cancellation are currently supported for time-domain operations
only. Frequency-domain operations (matrix, AC probes) complete quickly enough that
they do not expose progress callbacks.
Error Handling
All functions return a tdse_adapter_circuit_err_t. Check it against TDSE_ADAPTER_CIRCUIT_OK.
For error messages, use tdse_adapter_circuit_status_message() — it handles
adapter, RAW import, and sequence-network error codes:
if (tdse_adapter_circuit_handle_is_valid(handle)) {
int rc = tdse_adapter_circuit_compute_port_fsweep(&req, &result);
if (rc != TDSE_ADAPTER_CIRCUIT_OK) {
fprintf(stderr, "fsweep failed: %s\n",
tdse_adapter_circuit_status_message(rc));
const char* detail = tdse_adapter_circuit_get_last_error_text();
if (detail && detail[0]) {
fprintf(stderr, "detail: %s\n", detail);
}
}
}
Always check the return code. A non-zero code means the output buffers were not filled and the result struct's diagnostics may contain partial or stale data.
Diagnostics
Every result struct includes a diagnostics field. Three query functions extract
structured information from it:
// Human-readable summary of what happened
tdse_adapter_circuit_diagnostics_summary_t summary =
tdse_adapter_circuit_diagnostics_summary_init();
tdse_adapter_circuit_diagnostics_get_summary(&result.diagnostics, &summary);
// summary.call_kind, summary.matrix_size, summary.timing_total_ns, ...
// Solver statistics — backend used, matrix properties, conditioning
tdse_adapter_circuit_solver_diagnostics_t solver =
tdse_adapter_circuit_solver_diagnostics_init();
tdse_adapter_circuit_diagnostics_get_solver_stats(&result.diagnostics, &solver);
// solver.solver_backend, solver.matrix_density, solver.singular, ...
// Policy trace — how the solver backend was selected for this call
tdse_adapter_circuit_policy_trace_t trace =
tdse_adapter_circuit_policy_trace_init();
tdse_adapter_circuit_diagnostics_get_policy_trace(&result.diagnostics, &trace);
// trace.solver_policy_source, trace.solver_backend,
// trace.solver_policy_override_used, ...
Use diagnostics during development and for production monitoring. The summary tells you whether things worked. The solver stats tell you why a particular backend was chosen and how the matrix behaved. The policy trace is essential when debugging "why did it use this solver instead of that one?"
Solver Policy
When you run a frequency sweep, the SDK selects a solver backend for you — CPU dense, CPU sparse (KLU), or CUDA. The default policy picks the best available backend for your circuit size and structure, and decides whether to parallelize the sweep across frequency points.
For most users, the defaults are correct and you can skip this section. You only need to touch solver policy when:
- You want to force a specific backend (e.g., always use CUDA)
- You want to disable parallel sweeps for deterministic ordering
- You are benchmarking or debugging solver selection
// Query the current default policy
tdse_adapter_circuit_solver_policy_t policy =
tdse_adapter_circuit_get_solver_policy();
// Set process-wide defaults
policy.sweep_parallel_mode = TDSE_ADAPTER_CIRCUIT_SWEEP_PARALLEL_FORCE;
policy.sweep_parallel_max_workers = 4;
tdse_adapter_circuit_set_solver_policy(&policy);
// Or override per-request (takes precedence over process-wide defaults)
req.solver_policy_override = &custom_policy;
Key policy fields:
| Field | Default | Meaning |
|---|---|---|
sweep_parallel_mode | AUTO | AUTO (SDK decides), FORCE (always), DISABLE (never) |
sweep_parallel_max_workers | — | Maximum number of worker threads for parallel sweeps |
sweep_parallel_min_points | — | Minimum frequency points to trigger parallel sweep |
sweep_parallel_chunk_size | — | Points per chunk in dynamic scheduling |
sweep_parallel_allow_cuda | 0 | Whether the CUDA backend may be selected |
For a multi-threaded example using solver policy, see adapter_cpp_threaded_lanes in
the Examples Guide.
Adaptive Sweep Planning
Choosing a frequency grid is one of the hardest parts of frequency-domain modeling. Too few points and you miss resonant features. Too many and you waste computation, or worse, produce an over-resolved spectrum that creates fitting artifacts in Builder.
The adaptive sweep planner automates this. You give it a circuit and a target time
step, and it runs planning sweeps to determine the required frequency range and
resolution. It evaluates impulse response convergence and recommends production
parameters: nfreq, nh, and dt.
#include <tdse_adapter_circuit/planning.h>
tdse_adapter_circuit_adaptive_sweep_config_t plan_cfg =
tdse_adapter_circuit_adaptive_sweep_config_init();
plan_cfg.tolerance_mode = TDSE_ADAPTER_CIRCUIT_ADAPTIVE_SWEEP_TOLERANCE_MEDIUM;
plan_cfg.dt_target = 1e-4;
plan_cfg.limit_f_max_hz = 1e4;
// Create a report handle to capture detailed planning output
tdse_adapter_circuit_adaptive_sweep_report_t* report = NULL;
tdse_adapter_circuit_adaptive_sweep_report_create(&report);
tdse_adapter_circuit_adaptive_sweep_request_t plan_req =
tdse_adapter_circuit_adaptive_sweep_request_init();
plan_req.handle = result.handle;
plan_req.ports = ports;
plan_req.port_count = port_count;
plan_req.matrix_kind = TDSE_ADAPTER_CIRCUIT_MATRIX_Y;
plan_req.correction_method = TDSE_BUILDER_CORRECTION_RECONSTRUCT_FROM_REAL;
plan_req.adapter_options = opt;
plan_req.planning_config = &plan_cfg;
plan_req.out_report = report;
tdse_adapter_circuit_adaptive_sweep_result_t plan_result =
tdse_adapter_circuit_adaptive_sweep_result_init();
int rc = tdse_adapter_circuit_plan_adaptive_sweep(&plan_req, &plan_result);
// plan_result.summary.production_dt_s — recommended time step
// plan_result.summary.production_nh — recommended history length
// plan_result.summary.production_nfreq — recommended frequency count
The report handle gives you detailed justification for each recommendation:
tdse_adapter_circuit_adaptive_sweep_report_summary_t report_summary;
memset(&report_summary, 0, sizeof(report_summary));
report_summary.struct_size = sizeof(report_summary);
tdse_adapter_circuit_adaptive_sweep_report_get_summary(report, &report_summary);
// Export the full report as JSON or text for archival
size_t required;
tdse_adapter_circuit_adaptive_sweep_report_json(
report, buf, buf_size, &required);
tdse_adapter_circuit_adaptive_sweep_report_text(
report, buf, buf_size, &required);
tdse_adapter_circuit_adaptive_sweep_report_destroy(report);
The tolerance mode (LOW, MEDIUM, HIGH) controls how aggressively the planner
refines the grid. MEDIUM is the right starting point. Use HIGH for production
runs where accuracy matters more than speed.
Tail vs. Nfreq Analysis
When preparing for Builder handoff, you need to choose nh — the number of history
samples in the discrete-time impulse response. Too small and the tail is truncated,
introducing error. Too large and you waste memory and computation in Runtime.
The tail-vs-nfreq analysis helps. It scans the impulse response as nfreq increases,
tracking how the tail magnitude decays. When the tail settles below a threshold, you
have found your nh.
tdse_adapter_circuit_tail_vs_nfreq_request_t tail_req =
tdse_adapter_circuit_tail_vs_nfreq_request_init();
tail_req.handle = result.handle;
tail_req.ports = ports;
tail_req.port_count = port_count;
tail_req.adapter_options = opt;
// The tail-vs-nfreq scan evaluates both Y and Z internally and uses
// RECONSTRUCT_FROM_REAL, so no matrix_kind/correction_method fields exist
// on this request struct.
tdse_adapter_circuit_tail_vs_nfreq_result_t tail_result =
tdse_adapter_circuit_tail_vs_nfreq_result_init();
int rc = tdse_adapter_circuit_scan_tail_vs_nfreq(&tail_req, &tail_result);
// tail_result.summary.recommended_nh — suggested history length
// tail_result.summary.tail_settled — whether the tail converged
As with adaptive sweep planning, you can create a report handle
(tdse_adapter_circuit_tail_vs_nfreq_report_t) for per-point detail and JSON/text
export.
NPORT
NPORT is a SPICE extension that imports frequency-dependent multiport data from Touchstone files directly into a circuit solve. If you have measured or simulated S-parameters, Y-parameters, or Z-parameters in a Touchstone file, you can reference it from a netlist without manually converting formats:
NPORT NP1 1 0 2 0 FILE=example.y2p TYPE=Y
When the adapter compiles a netlist containing NPORT elements, it reads the referenced Touchstone file, interpolates the data onto the solver's frequency grid, and stamps a frequency-dependent admittance block into the MNA matrix. All of this happens at compile time — no extra steps for you.
Supported Touchstone formats include .ynp, .znp, .y2p, .z2p, .s2p, and their
multi-port variants. The file must be readable relative to the working directory at
compile time.
NPORT elements work transparently with all Adapter Circuit commands: matrix, series,
and probe. For a runnable example, see nport_y2p_ac in the
Examples Guide.
Integration Patterns
Builder Handoff
This is the most common end-to-end pattern. Adapter Circuit produces a Y matrix, and Builder consumes it to produce a runtime pack:
// 1. Compute Y from adapter
tdse_adapter_circuit_compute_port_fsweep(&freq_req, &freq_result);
// 2. Feed into Builder
tdse_builder_cplx_mat_view_t spec;
spec.ri = y_matrix_data;
spec.nfreq = nfreq;
spec.nq = np; spec.np = np;
spec.ld = np;
spec.stride_freq = np * np;
tdse_builder_h_from_spectrum(omega, spec, dt, nh,
TDSE_BUILDER_CORRECTION_RECONSTRUCT_FROM_REAL,
0.0, NULL, h_data, h_data_len);
// 3. Build pack
tdse_builder_configure_ex(builder, &bopt);
tdse_builder_apply_h(builder, &h_desc);
tdse_builder_write_pack(builder, "model.pack");
A few convenience helpers make this pattern even shorter:
// Create a frequency grid in Hz instead of rad/s
tdse_adapter_circuit_grid_t g = tdse_adapter_circuit_grid_from_hz(0, 50, 101);
// 1-port shorthand
tdse_adapter_circuit_port_def_t port = TDSE_ADAPTER_CIRCUIT_PORT_1P(1, 0);
// Convert adapter output shape to Builder input view in one call
tdse_builder_cplx_mat_view_t spec =
tdse_adapter_circuit_to_builder_view(&result.shape, matrix_ri);
Critical invariant: The port order in the adapter's ports array must match the
port order in Builder's nq and np dimensions. If adapter computes Y with ports in
order [A, B, C] and Builder expects [C, A, B], the resulting pack will be wrong
and the error may not be obvious until runtime. Archive the frequency grid parameters
and port ordering alongside every output.
Qualification Checks Before You Ship The Pack
Do not stop at "Builder wrote a file." The pack is ready for handoff only when the conversion path has also cleared the quality checks that matter for your integration.
Use this checklist:
| Check | Why it matters | Best place to read it |
|---|---|---|
| Port order and matrix family are intentional | wrong ordering can produce a numerically valid but physically wrong pack | Adapter request + Builder handoff record |
| Passivity result is acceptable for the exported spectrum | catches unstable or non-physical source data before Runtime | workflow result or tdse_nport_check_passivity(...) |
| Round-trip frequency-response error is acceptable | confirms the written pack still reproduces the source spectrum closely enough | workflow result or tdse_model_verify_frequency_response(...) |
Adaptive planning or tail scan produced an acceptable nfreq / tail budget | avoids shipping a pack that is too short, too coarse, or too aggressively truncated | tail_scan, workflow result, planning reports |
| Runtime can create the pack cleanly on the target host | catches compatibility surprises before the pack leaves engineering | tdse_pack_validate, tdse_model_create(...), create diagnostics |
In practice there are two good qualification styles:
- Workflow-first qualification Use the workflow result as the primary artifact. It already carries passivity and round-trip fields and records whether the SDK auto-derived the grid or sweep plan.
- Manual handoff qualification Archive the matrix family, port list, frequency grid, Builder settings, pack validation output, and one Runtime create proof on the same host profile where the pack will be consumed.
For most teams, the quality bar is:
- the source circuit is understood
- the chosen representation (
YorZ) is intentional - passivity and round-trip metrics are within project limits
- Runtime creates the pack without compatibility or diagnostics surprises
That is a much stronger release signal than "the CSV looked reasonable."
Embedding in a Host Solver (MNA)
When embedding TDSE inside a larger circuit simulator, the Runtime step loop integrates with the host's Modified Nodal Analysis formulation:
tdse_step_begin(model, t, dt);
tdse_step_op(model, &op); // op.G = conductance matrix G_tdse
tdse_step_hr(model, hr); // hr = history current source
// Host assembles the full system:
// (G_host + G_tdse) * v = i_src - hr
// ...solve for v...
tdse_step_commit(model, v); // advance internal state
This pattern lets you treat the TDSE model as a black-box Norton equivalent that stamps into the host's MNA matrix. The host owns the global solve; TDSE provides the conductance stamp and the history current source each time step.
For a runnable example, see mna_block_embed_2p in the
Examples Guide.
Deep Reference: Adapter Lookup
Use this section as a lookup area after the main task sections above. It is not the best place to start if you are still deciding which adapter operation you need.
Parameter Cookbook
This table is a quick lookup for every CLI flag and its purpose. Bookmark it.
| Parameter | Where | Notes |
|---|---|---|
--netlist-kind file|text|stdin | all commands | file for scripts, stdin for pipes, text for inline |
--matrix y|z | matrix | Y = admittance (Norton form), Z = impedance (Thevenin form) |
--ports "p,n;p,n" | matrix, series | Order matters — it must match Builder input order |
--w0-radps, --dw-radps, --nfreq | matrix | Archive these with your output; you will need them later |
--dc-policy | matrix | Affects DC stability: see DC policy values |
--series voc|isc | series | voc = open-circuit voltage, isc = short-circuit current |
--method transient|ifft|tone | series, transient probe | transient = general; ifft = freq; tone = steady-state |
--dt, --steps | series, transient probe | Must match Builder's dt for consistent handoff |
--nfft | series, probe | FFT size; must be ≥ 2 * (nfreq - 1) for ifft method |
--observe "v(1);i(r1)" | probe | Semicolon-separated probe expressions |
--observe-source | probe | argument = CLI; netlist = .probe directives; argument_or_netlist = auto |
--sweep-kind | AC probe | from_netlist, lin, dec, oct, list |
--ac-excitation | AC probe | small_signal for new designs; legacy_tones for backward compatibility |
--json-out - | all commands | Always use - (stdout) in automation |
Enum Reference
| Enum type | Values |
|---|---|
tdse_adapter_circuit_matrix_kind_t | MATRIX_Y, MATRIX_Z |
tdse_adapter_circuit_port_response_kind_t | PORT_RESPONSE_VOC, PORT_RESPONSE_ISC |
tdse_adapter_circuit_probe_domain_t | PROBE_DOMAIN_AC, PROBE_DOMAIN_TRANSIENT |
tdse_adapter_circuit_netlist_source_t | NETLIST_SOURCE_TEXT, NETLIST_SOURCE_FILE |
tdse_adapter_circuit_raw_source_t | RAW_SOURCE_TEXT, RAW_SOURCE_FILE |
tdse_adapter_circuit_raw_output_kind_t | RAW_OUTPUT_BUFFER, RAW_OUTPUT_FILE |
tdse_adapter_circuit_raw_unit_mode_t | RAW_UNIT_PU, RAW_UNIT_SI |
tdse_adapter_circuit_sweep_parallel_mode_t | SWEEP_PARALLEL_AUTO, _FORCE, _DISABLE |
tdse_builder_correction_method_t | CORRECTION_NONE, _RECONSTRUCT_FROM_REAL|_IMAG|_MAG|_PHASE |
Failure Modes
When something goes wrong, start here:
| Symptom | Likely cause | Fix |
|---|---|---|
| Parse failure | Malformed netlist, missing include | Run with --json-out - on smallest deck |
| Ports resolve incorrectly | Wrong node names or polarity | Reduce to one port and verify the node pair |
| Singular or ill-conditioned solve | Floating nodes, invalid topology | Try extrapolate_from_positive DC policy or single-freq Y |
| VOC/ISC differs from expectation | Method mismatch, insufficient nfft | Compare transient vs ifft |
| NPORT import fails | Type mismatch, bad path, non-monotonic data | Check TYPE, dimensions, CWD for relative paths |
| RAW conversion fails "invalid argument" | Missing struct_size fields | Set all struct_size; check output_kind matches method |
| All backends show "no" in caps | Build configuration issue | Rebuild with required dependencies (KLU, CUDA, etc.) |
Troubleshooting
When a command fails, collect these four things before asking for help:
- The exact command line you ran
- The full
--json-out -output - The smallest netlist that reproduces the problem
tdse adapter circuit capsoutput from the same machine
To narrow down the problem yourself: verify the netlist parses → verify one port → verify one frequency → add complexity. Almost all circuit bugs become obvious when you reduce to the simplest failing case. For Runtime-side issues after pack creation, continue in Troubleshooting.
Validation Checklist
Use this before handing off results to Builder or committing output to a project:
-
tdse adapter circuit capsshows expected backends - Minimal
matrixreplay passes on a known-good deck - Output dimensions match expectations (ports × frequency points)
- Builder handoff produces a valid
.packfile - Port order is consistent throughout the pipeline (adapter → Builder → Runtime)
Advanced Tuning And Performance
This section is for tuning and scale work after the basic adapter flow is already correct. It covers the key knobs that affect Adapter Circuit throughput and when it is worth touching them.
Choosing a Solver Backend
For most circuits the auto-selector picks the right backend. Here is when to override it:
| Backend | Best for |
|---|---|
| DENSE_LU | Small circuits (n < 200). Lowest overhead per solve point |
| SPARSE_KLU | Medium-to-large circuits (n > 200, density < 0.18). Much faster factorisation for sparse matrices |
| DENSE_CUDA | Large circuits (n > 256) when a CUDA GPU is available. Best for batched frequency sweeps |
| SPARSE_CUDA | Very large sparse circuits (n > 20000). Only available with CUDA GPU |
| SPARSE_MKL_PARDISO | Large sparse circuits when Intel MKL is available. Alternative to KLU |
Parallel Frequency Sweeps
Frequency sweeps are embarrassingly parallel — each frequency point is an independent linear solve. Enable parallel sweeps to use all CPU cores:
tdse adapter circuit matrix ... --sweep-parallel auto
Or via C API:
policy.sweep_parallel_mode = TDSE_ADAPTER_CIRCUIT_SWEEP_PARALLEL_AUTO;
policy.sweep_parallel_max_workers = 0; // use all hardware threads
tdse_adapter_circuit_set_solver_policy(&policy);
Parallel sweeps are most effective when:
- nfreq ≥ 8: Overhead of thread creation amortized over many points
- n > 100: Each solve point is expensive enough to justify threading
- CUDA backend: The CUDA solver can batch multiple frequency points into a single GPU kernel launch (see Dense CUDA Batched Fast Path below)
When NOT to use parallel sweeps:
- Debugging: serial execution gives deterministic ordering
- Very small circuits (n < 50): thread overhead exceeds benefit
- Already running many independent adapter calls in parallel (nesting overhead)
Dense CUDA Batched Fast Path
When the CUDA dense backend is selected and the following conditions are met, frequency sweeps take a highly optimised batched path:
- Affine AC build template is valid (enabled by default for nfreq ≥ 4)
- Frequency grid starts at w0 > 0 (non-DC)
- nfreq ≥ 2
- The selected backend is DENSE_CUDA
dense_cuda_batched_fsweep_fast_path_enabled()returns true (default)
On this path, the solver batches all frequency points into one GPU kernel, dramatically reducing kernel launch overhead. For large circuits (n > 1000) with many frequency points (nfreq > 64), the speedup can be 5-20× versus per-point GPU solves.
The fast path is controlled by the environment variable:
TDSE_ADAPTER_DENSE_CUDA_BATCHED_FSWEEP_FAST_PATH=1 # enable (default)
TDSE_ADAPTER_DENSE_CUDA_BATCHED_FSWEEP_FAST_PATH=0 # disable
DC Policy Performance
The DC policy choice affects the number of factorizations at ω=0:
| Policy | Factorizations | Notes |
|---|---|---|
exact_dc_then_fallback | 1-2 | Tries exact first. Two factorizations only when singular |
regularized_exact_dc | 1 | Always regularises with inductor_gbig. Fastest correct path |
extrapolate_from_positive | 0 | Skips DC entirely. Fastest but least accurate at low frequencies |
Sparse Solver Factor Caching
KLU and SPARSE_CUDA solvers cache the symbolic factorization pattern. When
computing multiple sweeps against the same circuit with different frequency
grids, the symbolic analysis is reused. This is automatic — no configuration
needed. The diagnostics field factor_cache_enabled and factor_cache_hit
report whether caching was used.
Affine AC Build
When enabled, the adapter builds an affine template of the MNA system at the first frequency point. Subsequent frequency points materialise the template with O(nnz) work instead of rebuilding the full system from scratch. This is enabled by default for nfreq ≥ 4 and provides approximately 2-3x speedup for large circuits.
Compile Once, Sweep Many
A compiled handle is reusable across multiple compute calls. Always compile once and reuse the handle for multiple sweeps, rather than recompiling:
// Good — compile once
tdse_adapter_circuit_compile_from_netlist(&req, &result);
for (int k = 0; k < num_configs; ++k) {
freq_req.handle = result.handle;
freq_req.grid = grids[k];
tdse_adapter_circuit_compute_port_fsweep(&freq_req, &freq_result);
}
// Bad — recompiles every iteration
for (int k = 0; k < num_configs; ++k) {
tdse_adapter_circuit_compile_from_netlist(&req, &result);
freq_req.handle = result.handle;
tdse_adapter_circuit_compute_port_fsweep(&freq_req, &freq_result);
tdse_adapter_circuit_destroy(result.handle);
}
Compilation involves parsing, MNA construction, and topology analysis — it is typically 10-100× more expensive than a single frequency-point solve.
