Circuit API and Host Integration
Integrate the Circuit API and its diagnostics into a host application.
Use this chapter after the first command-line workflow is correct. It covers handle ownership, compile/compute calls, Builder handoff, and host integration without duplicating the Runtime lifecycle.
Core C API Tasks
The C API is what you use when integrating TDSE Circuit into a host application. It is organized around a compile-then-compute pattern: compile a ModelSpace case 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:
- import external sources into ModelSpace, then compile the ModelSpace case
- compute a matrix, series, or probe output
- inspect diagnostics on failure or during tuning
- destroy the handle cleanly when the work is done
Most operation APIs return a tdse_status_t error code. The public _init()
helpers return initialized structs, and status-message helpers return text.
For a non-OK operation status, call tdse_circuit_status_message(code) for a
human-readable description and tdse_circuit_get_last_error_text() for
thread-local diagnostic detail.
If you prefer C++, include the feature-specific wrapper header (for example,
tdse/circuit/cxx_core.hpp or tdse/circuit/workflow.hpp). These headers
provide RAII handles, exception-based error handling, and convenience APIs over
the same underlying solver.
Compile ModelSpace directly
tdse_modelspace_circuit_compile accepts the UTF-8 JSON text of a
tdse.modelspace.circuit_case.v1 technical case. It validates the case and
constructs native Circuit elements directly. The returned handle uses the same
Circuit compute APIs and destruction function. Import external sources into ModelSpace before this step.
tdse_modelspace_circuit_compile_request_t request =
tdse_modelspace_circuit_compile_request_init();
request.case_json = case_json_utf8;
request.region_id = NULL; /* whole circuit, including scheduled events */
tdse_circuit_compile_result_t result = tdse_circuit_compile_result_init();
tdse_status_t status = tdse_modelspace_circuit_compile(&request, &result);
/* On success, use result.handle with the Circuit compute APIs. */
tdse_circuit_destroy(&result.handle);
A nonempty region_id compiles the qualified LTI region. Whole-case compilation
copies the simulation step and stop time into the native analysis plan. Package
validation and asset resolution precede this technical-case API.
Whole-circuit execution, Runtime coupling, and region Pack construction share
this typed lowering. External SPICE clients import their source into ModelSpace
before calling tdse_modelspace_circuit_compile. There is no generated-netlist
export or direct SPICE execution API.
RAW import uses one ModelSpace electrical mapper. Generator equivalents and load/transformer policies must be recorded explicitly: changing Norton to Thevenin can change broadband behavior. The legacy RAW C API selects Norton; the ModelSpace RAW import defaults to Thevenin. Both choices are represented in the resulting model; neither is inferred from an exported filename.
base.coordinate_system defaults to si. per_unit denotes normalized
electrical coordinates and requires a positive base_mva; waveform results
then use pu. Seconds and Hz are unchanged. Electrical parameter unit labels
describe the corresponding dimensional quantity; the coordinate system
determines whether its numeric value is normalized.
Calling Conventions
Before writing any C API code, internalize these four rules. They are enforced by the SDK but the error you get may not obviously point to the root cause.
1. Use the public _init() helper for versioned structs.
Request, result, option, report, and diagnostic structs that carry
struct_size have public initializers. Use the initializer declared for that
type rather than hand-zeroing it; a zero struct_size is rejected with
TDSE_STATUS_INVALID_ARG. See the Init Functions table for
the core workflow helpers.
2. The first call with NULL buffers is a sizing pass, not an error.
For port frequency sweeps, port series, probes, and region preparation, pass
NULL/zero output buffers first. A valid sizing call returns TDSE_STATUS_OK
and populates the relevant required_*_count. Allocate that many elements and
call again. A non-NULL but undersized output buffer is a caller error and
returns the status documented by that API (for the port compute APIs,
TDSE_STATUS_INVALID_ARG); no partial output is promised.
Import APIs have their own owned-output and buffer contracts. Follow the request/result type documentation rather than applying the compute-buffer pattern by analogy.
See The two-output pattern for a summary table.
3. Ports are specified by index XOR by name, never both.
When port_count > 0, exactly one of ports (numeric port_def_t*) or named_ports
(string named_port_def_t*) must be non-NULL. Providing both returns
TDSE_STATUS_INVALID_ARG.
Named-port resolution happens at compute time, not compile time - a successful compile
does not guarantee the named ports will resolve.
4. Compute/query ownership follows the compile thread.
The thread that compiles a handle owns its query and compute APIs. Passing the
handle to a different execution thread is invalid; for parallel work, compile
a separate handle per worker. After all operations have stopped,
tdse_circuit_destroy() may release the handle on any thread. See
Handle Reuse and Thread Safety.
Init Functions
Versioned request/result/option/report structs have _init() functions. 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 TDSE release versions.
| Init function | Returns |
|---|---|
tdse_circuit_options_init() | tdse_circuit_options_t |
tdse_circuit_region_spec_init() | tdse_circuit_region_spec_t |
tdse_circuit_time_options_init() | tdse_circuit_time_options_t |
tdse_circuit_probe_options_init() | tdse_circuit_probe_options_t |
tdse_circuit_ac_probe_options_init() | tdse_circuit_ac_probe_options_t |
tdse_circuit_ac_sweep_init() | tdse_circuit_ac_sweep_t |
tdse_circuit_compiled_info_init() | tdse_circuit_compiled_info_t |
tdse_circuit_diagnostics_init() | tdse_circuit_diagnostics_t |
tdse_circuit_node_voltage_probe_init(p, n) | tdse_circuit_probe_def_t |
tdse_circuit_branch_current_probe_init(name) | tdse_circuit_probe_def_t |
tdse_circuit_named_port_def_init(p, n) | tdse_circuit_named_port_def_t |
tdse_circuit_compile_request_init() | tdse_circuit_compile_request_t |
tdse_circuit_compile_result_init() | tdse_circuit_compile_result_t |
tdse_circuit_port_fsweep_request_init() | tdse_circuit_port_fsweep_request_t |
tdse_circuit_port_fsweep_result_init() | tdse_circuit_port_fsweep_result_t |
tdse_circuit_port_series_request_init() | tdse_circuit_port_series_request_t |
tdse_circuit_port_series_result_init() | tdse_circuit_port_series_result_t |
tdse_circuit_probe_compute_request_init() | tdse_circuit_probe_compute_request_t |
tdse_circuit_probe_compute_result_init() | tdse_circuit_probe_compute_result_t |
tdse_circuit_prepare_region_request_init() | tdse_circuit_prepare_region_request_t |
tdse_circuit_prepare_region_result_init() | tdse_circuit_prepare_region_result_t |
tdse_circuit_seq_options_init() | tdse_circuit_seq_options_t |
tdse_circuit_seq_network_compile_request_init() | tdse_circuit_seq_network_compile_request_t |
tdse_circuit_seq_network_compile_result_init() | tdse_circuit_seq_network_compile_result_t |
tdse_circuit_seq_fault_request_init() | tdse_circuit_seq_fault_request_t |
tdse_circuit_seq_fault_result_init() | tdse_circuit_seq_fault_result_t |
Related helper families are grouped by header:
-
planning.h:tdse_circuit_adaptive_sweep_config_init(),tdse_circuit_adaptive_sweep_request_init(),tdse_circuit_adaptive_sweep_result_init(),tdse_circuit_tail_convergence_request_init(),tdse_circuit_tail_convergence_result_init() -
raw.h:tdse_circuit_raw_request_init(),tdse_circuit_raw_result_init(),tdse_circuit_raw_options_init() -
seq.h: the fivetdse_circuit_seq_*_init()functions 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.
Current Public API Family Map
Use this map to avoid treating the compile/compute examples as the entire Circuit surface:
| Family | Representative entry points | Use |
|---|---|---|
| component identity and lifecycle | tdse_circuit_version_string(), tdse_circuit_destroy() | loaded-version checks and handle release |
| compile and introspection | tdse_modelspace_circuit_compile(), tdse_circuit_get_compiled_info(), tdse_circuit_get_parse_warning_count() | compile once, inspect topology and non-fatal parser warnings |
| port/probe/region compute | tdse_circuit_compute_port_frequency_sweep(), tdse_circuit_compute_port_series(), tdse_circuit_compute_probes(), tdse_circuit_prepare_region() | caller-owned compute outputs |
| solver diagnostics | tdse_circuit_get_solver_policy_diagnostics() | read-only effective-selection evidence |
| planning | tdse_circuit_plan_adaptive_sweep(), tdse_circuit_analyze_tail_convergence() | choose production grid and tail budgets |
| ModelSpace | tdse_modelspace_circuit_execute_workflow() | promoted case/study/result/evidence workflows |
| high-level workflows | tdse_circuit_workflow_handle_to_pack() | qualified Circuit-to-Builder-to-pack handoff |
Use the generated API reference for every field and ownership rule.
SEQ network construction is fail-closed by default for missing generator
sequence records. With the initialized defaults,
tdse_modelspace_import_seq_networks() returns
TDSE_STATUS_UNSUPPORTED without publishing sequence cases when a RAW generator
needs negative- or zero-sequence data that is absent from the SEQ source. Enable
the generator-sequence substitution option only when the host explicitly accepts
positive-sequence substitution and archives the generated report.
Configuring Newton Solver Tolerances
tdse_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) |
gpu_nonlinear_convergence_sync_interval | 0 | GPU Newton scalar convergence sync policy: 0 uses the default checkpoint interval, positive values force that interval, and negative values disable host convergence sync |
Adjust these when circuits exhibit slow convergence (try relaxing tolerances) or require higher precision (tighten tolerances).
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_circuit_compiled_info_t info =
tdse_circuit_compiled_info_init();
tdse_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 = 0;
tdse_circuit_get_node_name(result.handle, 0, NULL, 0, &required_len);
char* name = malloc(required_len);
tdse_circuit_get_node_name(result.handle, 0, name, required_len, &required_len);
Options
tdse_circuit_options_init() returns a zero/sentinel policy that is normalized
at call time. The fields you are most likely to adjust are:
| Field | Default | Meaning |
|---|---|---|
policy | TDSE_CIRCUIT_DEFAULT -> TDSE_CIRCUIT_W0_REGULARIZED_EXACT_DC | How DC (zero frequency) is handled |
inductor_gbig | 0 -> 1e12 | Conductance used to regularize inductors at DC |
dc_extrapolate_points | 0 -> 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_circuit_port_def_t ports[] = {{1, 0}};
// The public grid stores angular frequency. This helper accepts Hz.
tdse_circuit_grid_t grid = tdse_circuit_grid_from_hz(0.0, 100.0, 5);
tdse_circuit_port_fsweep_request_t freq_req =
tdse_circuit_port_fsweep_request_init();
freq_req.handle = result.handle;
freq_req.matrix_kind = TDSE_CIRCUIT_MATRIX_Y;
freq_req.ports = ports;
freq_req.port_count = 1;
freq_req.grid = grid;
tdse_circuit_port_fsweep_result_t freq_result =
tdse_circuit_port_fsweep_result_init();
/* Sizing pass: NULL/zero output is valid and returns TDSE_STATUS_OK. */
int rc = tdse_circuit_compute_port_frequency_sweep(&freq_req, &freq_result);
size_t n_vals = freq_result.required_matrix_ri_count;
double* matrix_ri = malloc(n_vals * sizeof(double));
freq_req.out_matrix_ri = matrix_ri;
freq_req.out_matrix_ri_len = n_vals;
rc = tdse_circuit_compute_port_frequency_sweep(&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_circuit_port_series_request_t series_req =
tdse_circuit_port_series_request_init();
series_req.handle = result.handle;
series_req.response_kind = TDSE_CIRCUIT_PORT_RESPONSE_VOC;
series_req.ports = ports;
series_req.port_count = 1;
series_req.dt = 1e-4;
series_req.steps = 64;
tdse_circuit_port_series_result_t series_result =
tdse_circuit_port_series_result_init();
int rc = tdse_circuit_compute_port_series(&series_req, &series_result);
double* series_out =
malloc(series_result.required_values_count * sizeof(double));
series_req.out_values = series_out;
series_req.out_values_len = series_result.required_values_count;
rc = tdse_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_circuit_probe_def_t probe =
tdse_circuit_node_voltage_probe_init(1, 0);
// or: tdse_circuit_branch_current_probe_init("r1");
tdse_circuit_ac_sweep_t ac_sweep = tdse_circuit_ac_sweep_init();
ac_sweep.kind = TDSE_CIRCUIT_AC_SWEEP_LIN;
ac_sweep.points = 5;
ac_sweep.fstart_hz = 10.0;
ac_sweep.fstop_hz = 1000.0;
tdse_circuit_ac_probe_options_t ac_options =
tdse_circuit_ac_probe_options_init();
ac_options.excitation_mode = TDSE_CIRCUIT_AC_EXCITATION_SMALL_SIGNAL;
tdse_circuit_probe_compute_request_t probe_req =
tdse_circuit_probe_compute_request_init();
probe_req.handle = result.handle;
probe_req.domain = TDSE_CIRCUIT_PROBE_DOMAIN_AC;
probe_req.probes = &probe;
probe_req.probe_count = 1;
probe_req.ac_sweep = &ac_sweep;
probe_req.ac_probe_options = &ac_options;
tdse_circuit_probe_compute_result_t probe_result =
tdse_circuit_probe_compute_result_init();
int rc = tdse_circuit_compute_probes(&probe_req, &probe_result);
double* probe_freq_hz =
malloc(probe_result.required_freq_hz_count * sizeof(double));
double* probe_values =
malloc(probe_result.required_values_count * sizeof(double));
probe_req.out_freq_hz = probe_freq_hz;
probe_req.out_freq_hz_len = probe_result.required_freq_hz_count;
probe_req.out_values = probe_values;
probe_req.out_values_len = probe_result.required_values_count;
rc = tdse_circuit_compute_probes(&probe_req, &probe_result);
Each probe domain has different required parameters. The original convenience wrappers construct an AC or transient request with only the domain-relevant inputs and remain useful for a NULL-buffer sizing pass:
// AC probes -only frequency-sweep parameters are accepted
tdse_circuit_compute_ac_probes(
handle, probes, probe_count, ac_sweep, ac_probe_options, &result);
// Transient probes -only time-domain parameters are accepted
tdse_circuit_compute_transient_probes(
handle, probes, probe_count, dt, steps, time_options, &result);
Use the matching _ex wrapper for the fill call. It supplies the domain-relevant
request fields plus the output buffers while retaining the same two-call sizing
contract:
int rc = tdse_circuit_compute_ac_probes_ex(
handle, probes, probe_count, ac_sweep, ac_probe_options,
NULL, 0U, NULL, 0U, &result);
double* freq_hz = malloc(result.required_freq_hz_count * sizeof(*freq_hz));
double* values = malloc(result.required_values_count * sizeof(*values));
rc = tdse_circuit_compute_ac_probes_ex(
handle, probes, probe_count, ac_sweep, ac_probe_options,
freq_hz, result.required_freq_hz_count,
values, result.required_values_count, &result);
// Transient probes have values but no frequency output buffer.
rc = tdse_circuit_compute_transient_probes_ex(
handle, probes, probe_count, dt, steps, time_options,
values, result.required_values_count, &result);
Use tdse_circuit_compute_probes() directly when you also need fields that the
domain wrappers intentionally do not expose, such as JSON report output.
Prepare a Circuit Region
Region preparation takes an explicit set of public node names, resolves it into an explicit port list, and can create a prepared handle for that subnetwork:
tdse_circuit_region_spec_t region =
tdse_circuit_region_spec_init();
const char* region_nodes[] = {"OUT"};
region.nodes = region_nodes;
region.node_count = 1;
tdse_circuit_prepare_region_request_t prep_req =
tdse_circuit_prepare_region_request_init();
prep_req.source_handle = result.handle;
prep_req.region = ®ion;
tdse_circuit_prepare_region_result_t prep_result =
tdse_circuit_prepare_region_result_init();
int rc = tdse_circuit_prepare_region(&prep_req, &prep_result);
tdse_circuit_port_def_t* resolved_ports =
malloc(prep_result.required_ports_count * sizeof(*resolved_ports));
tdse_circuit_handle_t* prepared_handle = NULL;
prep_req.out_ports = resolved_ports;
prep_req.out_ports_len = prep_result.required_ports_count;
prep_req.out_prepared_handle = &prepared_handle;
rc = tdse_circuit_prepare_region(&prep_req, &prep_result);
// Destroy prepared_handle separately after its compute calls.
This is most useful when the host has already selected a public-node boundary and wants Circuit to derive the corresponding port basis and prepared subnetwork.
Named Ports
When you know node names but not their numeric indices, use named ports. The circuit resolves string names to indices during the compute call:
tdse_circuit_named_port_def_t ports[] = {
tdse_circuit_named_port_def_init("1", "0"),
tdse_circuit_named_port_def_init("OUT", "GND"),
};
freq_req.named_ports = ports;
freq_req.port_count = 2;
// Leave freq_req.ports = NULL -the circuit resolves names to indices.
Named ports work with port_fsweep_request_t, port_series_request_t, and the
high-level modelspace-to-pack workflow request. The two
specification modes are mutually exclusive: set ports XOR named_ports,
never both. Providing both returns TDSE_STATUS_INVALID_ARG.
Named-port resolution happens at compute time, not compile time. A ModelSpace case that
compiles successfully may still fail at the compute stage if a named port
references a node that does not exist in the circuit. When debugging a named-port
failure, verify the node names against tdse_circuit_get_node_name()
output before suspecting a solver issue.
Touchstone Export
Export computed Y or Z matrix data to a Touchstone v1 file for interchange with other
tools. The function writes # HZ Y RI R <z0> or # HZ Z RI R <z0> and emits
standard column-major port ordering in the file
(Y11, Y21, ..., Y12, Y22, ... for Y).
tdse_status_t tdse_circuit_export_touchstone(
const char* path, // output file path
size_t nports, // number of ports
size_t nfreq, // number of frequency points
const double* freq_hz, // frequency array [nfreq]
const double* data_ri, // RI data, row-major per frequency
size_t data_ri_len, // = 2 * nfreq * nports * nports
tdse_circuit_matrix_kind_t matrix_kind,// MATRIX_Y or MATRIX_Z
double z0, // reference impedance (typically 50)
const char* comment); // optional comment line (can be NULL)
A complete export after a frequency sweep:
// After compute_port_fsweep, export the result:
const double* freqs = my_freq_array;
const double* data = matrix_ri; /* caller-owned fsweep output */
tdse_status_t rc = tdse_circuit_export_touchstone(
"output.y2p", 2, nfreq,
freqs, data, n_vals,
TDSE_CIRCUIT_MATRIX_Y,
50.0, "2-port RL export");
The API input uses the same row-major-per-frequency layout as the frequency
sweep output. The exporter reorders each Touchstone v1 data line to the
standard column-major sequence; for a 2-port system that is
Y11 Y21 Y12 Y22 (real-imaginary pairs).
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_modelspace_circuit_compile(&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_circuit_compute_port_frequency_sweep(&freq_req, &freq_result);
}
tdse_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.
The compile thread owns query and compute calls on that handle. Do not move those calls to a different worker or enter the handle concurrently.
// WRONG -sharing a handle across threads:
tdse_circuit_handle_t* shared = result.handle;
#pragma omp parallel for
for (int i = 0; i < N; i++) {
freq_req.handle = shared; // invalid foreign-thread use
tdse_circuit_compute_port_frequency_sweep(&freq_req, &freq_result);
}
// CORRECT -one handle per thread:
#pragma omp parallel for
for (int i = 0; i < N; i++) {
tdse_circuit_compile_result_t local_result = /* compile per thread */;
freq_req.handle = local_result.handle;
tdse_circuit_compute_port_frequency_sweep(&freq_req, &freq_result);
tdse_circuit_destroy(&local_result.handle);
}
Handle lifecycle: pass the address of the caller-owned handle variable to
tdse_circuit_destroy(). A null address or null handle value is a no-op. On
success the function releases the object and writes null to that variable, so
repeating the call with the same variable is safe. Any separately copied alias
becomes dangling and must never be passed to a Circuit API. Circuit does not
attempt to validate arbitrary dangling addresses. Once all query/compute work
has stopped, destruction is thread-independent; racing destroy against another
API call is never safe.
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_circuit_time_options_t time_opt =
tdse_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_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.
Progress callbacks are available for time-domain operations only. Matrix sweeps do
not expose either progress or cancellation callbacks. AC probes do not expose
progress callbacks, but their tdse_circuit_ac_probe_options_t accepts a
cancel_callback and cancel_user_ptr.
Error Handling
Check every operation's tdse_status_t result against TDSE_STATUS_OK.
For error messages, use tdse_circuit_status_message() - it handles circuit,
RAW import, and sequence-network error codes:
if (tdse_circuit_is_valid(handle)) {
int rc = tdse_circuit_compute_port_frequency_sweep(&req, &result);
if (rc != TDSE_STATUS_OK) {
fprintf(stderr, "fsweep failed: %s\n",
tdse_circuit_status_message(rc));
const char* detail = tdse_circuit_get_last_error_text();
if (detail && detail[0]) {
fprintf(stderr, "detail: %s\n", detail);
}
}
}
Always check the returned status. For the compute APIs, a non-OK status does not commit a partial caller output buffer; the result's sizing and diagnostics fields follow that API's individual contract and may still be useful for triage.
Diagnostics
Every result struct includes a diagnostics field. Access its sub-fields directly:
const tdse_circuit_diagnostics_t* d = &result.diagnostics;
// Human-readable summary of what happened
printf("call_kind=%d, matrix_size=%zu, total_ns=%lu\n",
d->summary.call_kind, d->summary.matrix_size,
d->timing.total_ns);
// Solver statistics -backend used, matrix properties, conditioning
printf("backend=%d, density=%.4f, factor_cache_hit=%d\n",
d->solver.backend, d->solver.matrix_density,
d->solver.factor_cache_hit);
// Policy trace -how the solver backend was selected for this call
printf("policy_source=%d, override_used=%d\n",
d->policy.source, d->policy.request_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?"
Integration Patterns
Builder Handoff
For a matrix that has already been computed, use the public workflow entry point to perform the Builder handoff, pack write, passivity check, and round-trip check:
#include <tdse/circuit/workflow.h>
// The caller owns both the matrix output and its frequency samples in Hz.
const double freqs_hz[] = {0.0, 100.0, 200.0, 300.0, 400.0};
tdse_circuit_workflow_yz_matrix_to_pack_request_t pack_req =
tdse_circuit_workflow_yz_matrix_to_pack_request_init();
pack_req.matrix_kind = TDSE_CIRCUIT_MATRIX_Y;
pack_req.nports = freq_result.shape.port_count;
pack_req.freqs_hz = freqs_hz;
pack_req.nfreq = sizeof(freqs_hz) / sizeof(freqs_hz[0]);
pack_req.matrix_ri = matrix_ri;
pack_req.matrix_ri_len = freq_result.required_matrix_ri_count;
pack_req.out_pack_path = "model.pack";
tdse_workflow_result_t pack_result =
tdse_circuit_workflow_result_init();
tdse_status_t rc =
tdse_circuit_workflow_yz_matrix_to_pack(&pack_req, &pack_result);
A frequency sweep grid uses rad/s internally; retain the grid you supplied and derive
freqs_hz[k] = (grid.w0 + k * grid.dw) / (2 * pi) for the workflow. A few
convenience helpers make the Circuit side shorter:
// Create a frequency grid in Hz instead of rad/s
tdse_circuit_grid_t g = tdse_circuit_grid_from_hz(0, 50, 101);
// 1-port shorthand
tdse_circuit_port_def_t port = TDSE_CIRCUIT_PORT_1P(1, 0);
// Convert circuit output shape to Builder input view in one call
tdse_builder_cplx_mat_view_t spec =
tdse_circuit_to_builder_view(&result.shape, matrix_ri);
Critical invariant: The port order in the circuit's ports array must match the
port order in Builder's nq and np dimensions. If circuit 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 | Circuit 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_circuit_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_inspect(...), 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)
TDSE Circuit's co-simulation workflow already performs the native global solve; applications using that workflow do not implement another solver. The following sketch is for integrating Runtime into a separately owned simulator. It explicitly uses Y/Norton, with port current positive into the TDSE region. Buffers are preallocated and every API status must be checked.
tdse_step_begin(model, t, dt);
tdse_step_op(model, &op); // dense instantaneous admittance G_tdse
uint64_t hr_required = 0, ir_required = 0;
tdse_step_hr(model, hr, nq, &hr_required); // history current source
tdse_step_ir(model, ir, nq, &ir_required); // packaged independent response
// Host assembles the full system:
// (G_host + G_tdse) * v = i_src - hr - ir
// ...solve for v...
tdse_step_commit(model, v, np); // 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 same host-owned global solve also supports the dual
Thevenin representation: tdse_step_op() returns the impedance operator, the
host adds one MNA branch per port, and the branch equation is
B^T*v - Z0*i = hr + ir. In that mode the solved branch currents, rather than node
voltages, are passed to tdse_step_commit(). The host still owns the global
solve and TDSE provides the Runtime operator, history, and independent response.
Generate IR for the full simulation horizon; an out-of-range IR query is an
error to handle, not permission to drop the source contribution.
For a runnable example, see mna_block_embed_2p in the
Examples Guide.
