Time-Domain System Equivalent logoTime-Domain System EquivalentLinear dynamics, solved faster.Discuss an evaluation
SDK Documentation

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:

  1. import external sources into ModelSpace, then compile the ModelSpace case
  2. compute a matrix, series, or probe output
  3. inspect diagnostics on failure or during tuning
  4. 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 functionReturns
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 five tdse_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:

FamilyRepresentative entry pointsUse
component identity and lifecycletdse_circuit_version_string(), tdse_circuit_destroy()loaded-version checks and handle release
compile and introspectiontdse_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 computetdse_circuit_compute_port_frequency_sweep(), tdse_circuit_compute_port_series(), tdse_circuit_compute_probes(), tdse_circuit_prepare_region()caller-owned compute outputs
solver diagnosticstdse_circuit_get_solver_policy_diagnostics()read-only effective-selection evidence
planningtdse_circuit_plan_adaptive_sweep(), tdse_circuit_analyze_tail_convergence()choose production grid and tail budgets
ModelSpacetdse_modelspace_circuit_execute_workflow()promoted case/study/result/evidence workflows
high-level workflowstdse_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):

FieldDefaultDescription
newton_abs_tol1e-8Absolute convergence tolerance for Newton iterations
newton_rel_tol1e-6Relative convergence tolerance
newton_residual_tol1e-6Residual norm tolerance
newton_max_iterations32Maximum Newton iterations per timestep (0 = use default)
gpu_nonlinear_convergence_sync_interval0GPU 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:

FieldDefaultMeaning
policyTDSE_CIRCUIT_DEFAULT -> TDSE_CIRCUIT_W0_REGULARIZED_EXACT_DCHow DC (zero frequency) is handled
inductor_gbig0 -> 1e12Conductance used to regularize inductors at DC
dc_extrapolate_points0 -> 4Number 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 = &region;

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:

CheckWhy it mattersBest place to read it
Port order and matrix family are intentionalwrong ordering can produce a numerically valid but physically wrong packCircuit request + Builder handoff record
Passivity result is acceptable for the exported spectrumcatches unstable or non-physical source data before Runtimeworkflow result or tdse_circuit_nport_check_passivity(...)
Round-trip frequency-response error is acceptableconfirms the written pack still reproduces the source spectrum closely enoughworkflow result or tdse_model_verify_frequency_response(...)
Adaptive planning or tail scan produced an acceptable nfreq / tail budgetavoids shipping a pack that is too short, too coarse, or too aggressively truncatedtail_scan, workflow result, planning reports
Runtime can create the pack cleanly on the target hostcatches compatibility surprises before the pack leaves engineeringtdse_pack_inspect(...), tdse_model_create(...), create diagnostics

In practice there are two good qualification styles:

  1. 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.

  2. 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 (Y or Z) 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.