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

Lifecycle and Ownership

Runtime creation, ownership, state transitions, and shutdown contracts.

Use this section when you need to know who owns a Runtime handle, which API ends that ownership, and which shutdown call fits your host code. This section is about handle lifetime only; the per-step loop itself belongs in Step Execution.

Related Chapters For Builder configuration and pack generation, see Builder and Data Contracts. For the step-loop execution model, see Step Execution. For concurrency rules and the customer-facing thread-safety matrix, see Concurrency and Shutdown.

Use this section when your host integration question is primarily:

  • who owns the tdse_model_t* handle right now
  • which shutdown path fits the host's policy and wait budget
  • when local ownership is gone and all further calls must stop

If your question is instead "what exact calls happen every accepted step," jump to Step Execution first.

Thread-Safety Matrix

Lifecycle ownership and thread-safety are the same contract viewed from two angles. The user-facing rule is one owner per live handle; the complete customer-facing matrix is in Concurrency and Shutdown.

Use that matrix when you need to classify:

  • snapshot-style queries that can read a live handle during worker traffic
  • guarded step, reset, close, destroy, and release calls
  • immutable device and resource options supplied to tdse_model_create(...)
  • callback/logging and telemetry worker interactions

Why Lifecycle Comes First

The most important Runtime rule is not the math. It is lifecycle. If you understand when a handle is live, closing, destroying, released, or no longer locally owned, the rest of the API becomes much easier to use correctly.

Where This Fits In A Host Integration

For a minimal host integration, lifecycle is usually one of three patterns:

Host patternRuntime lifecycle concern
single model owned by one solver threadordinary create -> destroy path
worker pool with one model per threadownership must stay per-handle, not per process
supervisory or fault-handling shutdownclose or timeout-bearing destroy policy matters

This chapter only answers the ownership side of those patterns. It does not define the numerical step loop.

The Runtime Lifecycle In One Table

APIPrimary UseWait BehaviorReturns StatusRecommended For Host Code
tdse_model_create(...)create a model from pack bytesn/ayesyes
tdse_model_execution_prepare(...)prepare the selected CPU, GPU, or FPGA execution before the step loopsetup-only; may allocate representations and provider plansyeshosts that use a prepared execution interval
tdse_model_execution_end(...)release the prepared execution intervalbounded at a completed-step boundaryyeshosts that prepared execution
tdse_model_close(...)explicit non-blocking close attemptdoes not wait for an in-flight same-handle APIyessituational
tdse_model_destroy(...)explicit bounded destroycaller-controlled wait policyyesyes
tdse_model_release(...)terminal cleanup / finalizer pathmay wait for an in-flight same-handle API; returns INVALID_STATE if another terminal path owns ityesno

Short version:

  • use create to create
  • finish host resource configuration before calling tdse_model_execution_prepare(...); preparation performs the selected device/provider setup but does not advance simulation state
  • after preparation, optionally run non-committing trial queries followed by tdse_step_discard(...) when the host needs to remove first-use compute effects before a deadline
  • build physically meaningful history only by running the normal host-driven step loop and committing accepted primary values; TDSE never invents application warmup inputs
  • keep only step calls in the realtime interval, then call tdse_model_execution_end(...) on the owner thread
  • use destroy for ordinary host-managed shutdown
  • use close when you need an immediate status-bearing close attempt
  • use release only for terminal cleanup paths that are not driving lifecycle policy

After tdse_model_execution_end(...), a later prepare begins a new control-plane interval. CPU execution rechecks the current host-owned provider/thread/CPU-set contract before reusing retained plans; for host-inherited threading, the reference is the provider state observed at model creation, so a changed contract fails closed. Accelerator execution revalidates its selected provider. These checks are not performed by the step APIs.

Benchmark warmup and application initialization are different. The benchmark runs untimed prepared steps solely to make its latency samples representative of steady execution. A customer simulation does not need to reproduce that measurement policy. It either starts with the model's defined zero prehistory or runs its own physically valid pre-event/initialization interval through the ordinary step API.

When first-step latency matters, the host may additionally run a bounded, non-committing computational prewarm after preparation. See Warmup, Computational Prewarm, and Physical Initialization for the common CPU/GPU call count and exact call order.

High-level lifecycle state model

Create Semantics

The standard create path is:

tdse_model_create_diagnostics_t diag = tdse_model_create_diagnostics_init();
tdse_model_t* model = NULL;
tdse_status_t st = tdse_model_create(
    pack_data, pack_size, NULL, &diag, &model);

Key rules:

  • pass tdse_model_create_diagnostics_t so create failures remain request-scoped
  • treat diag.pack_error_code as part of the normal error path, not as optional trivia
  • after successful create, the caller owns the handle locally

Device, precision, threading, residency, and memory policy are explicit and per handle:

tdse_model_create_options_t options = tdse_model_create_options_init();
options.device = TDSE_DEVICE_CPU;
options.precision = TDSE_PRECISION_FP64;
tdse_status_t st = tdse_model_create(
    pack_data, pack_size, &options, &diag, &model);

Passing NULL options selects CPU, FP64, and host-resident data. A model's execution options are immutable; release and recreate the model to change them. Use tdse_model_get_execution_info(...) to inspect the selected and observed resources.

When the host needs metadata before deciding whether to create, prefer:

  • tdse_pack_inspect(...) for a tdse_model_info_t snapshot decoded from pack bytes
  • tdse_runtime_pack_inspect_summary(...) for version and payload summary fields in tdse_pack_summary_t

These APIs validate pack header and required metadata chunks without creating a tdse_model_t.

Owning the handle locally means:

  • your thread or wrapper may enter the handle according to the Runtime rules
  • your code is responsible for selecting the shutdown path
  • another thread has not yet taken over close or destroy

Live-Handle Query APIs

These queries are safe snapshot-style reads on a live handle:

  • tdse_model_info(...)
  • tdse_model_state_info(...)
  • tdse_model_last_error_info(...)
  • tdse_model_diagnostics_summary(...)
  • tdse_model_diagnostics_payload(...)
  • tdse_model_diagnostics_query_many(...)

They are intended for metadata, state, and failure diagnosis. Use the compact summary for the default support record, add only the versioned payloads needed by the incident, and use tdse_model_diagnostics_query_many(...) when the summary and several payloads must come from one internally consistent snapshot. The earlier aggregate draft was never released and was removed before ABI v1. If close or destroy has already started on the handle, these queries return TDSE_STATUS_INVALID_STATE.

Memory Preflight Before Preparation

tdse_model_get_memory_preflight(...) is the read-only capacity-planning query for a created model. Call it before tdse_model_execution_prepare(...) or the first step. It is repeatable and does not allocate, materialize representations, scan topology, call provider setters, change thread/affinity state, or change model state.

The result separates three byte views so callers do not double-count:

  • currently_allocated_*_bytes and currently_allocated_bytes are TDSE-owned allocations that already exist;
  • h_matrix_bytes through runtime_scratch_bytes, together with total_resident_bytes, are the known TDSE-owned resident bytes expected for the model's preparation; and
  • preparation_required_*_bytes and preparation_required_bytes are additional known TDSE-owned bytes still required beyond the current allocations.

These known totals cover the reported matrix, history/ring, linear-mirror, and runtime-scratch payload categories. They exclude allocator metadata, the model control object, and implementation-private allocations whose exact size is not portable to query. For CPU, the total is exact when resident_bytes_are_upper_bound == 0. When CPU preparation may benchmark a zero-padded execution shape, the flag is nonzero and the result is a safe bound for the largest candidate that may remain resident; the logical and bounded output-row counts are both reported. For GPU_CUBLAS, the result reports the largest known per-device payload across the explicitly selected GPU set. estimate_kind is TDSE_MODEL_MEMORY_ESTIMATE_KNOWN_PAYLOAD_LOWER_BOUND because CUDA, cuBLAS, graph, allocator, and driver-private allocations cannot be queried portably before preparation. Ordinary GPU preflight includes only the column-major delayed-H matrix and the ring, mirror, packed-input, and output workspace used by the normal history path; optional Circuit persistent-capability storage is not created or charged unless Circuit requests that capability. FPGA preflight remains unsupported because its provider-owned device allocation contract is opaque.

The total excludes allocator metadata, the model control object, transient preparation-only buffers, thread stacks, and implementation-private memory. The result identifies the selected device, storage precision/layout and rectangular dimensions, plus the expected thread and host CPU execution contract. Implementation-private memory remains explicitly opaque and is not added to the TDSE-owned total. The query never substitutes a CPU estimate for an accelerator.

Lifecycle States In Practice

The runtime does not publish a first-class enum for every human-readable lifecycle state, but product integrations should reason in these terms:

Practical StateMeaningSafe Actions
locally owned and idlehandle exists, no active trial stepquery info/state, start a step, destroy, close
trial step activetdse_step_begin(...) succeeded and commit has not yet ended the trialquery op, hr, ir, commit
destroy pending locallycaller has entered destroy and is waiting or finishingobserve destroy result only
ownership already handed offanother thread already started close or destroystop using the handle locally
terminally cleaned upstorage has been releasedno further API use is valid

Step-Lifecycle Ownership

Inside a live handle, the runtime lifecycle is:

  1. tdse_step_begin(...)
  2. one or more trial-safe queries:
    • tdse_step_op(...)
    • tdse_step_hr(...)
    • tdse_step_ir(...)
  3. tdse_step_commit(...)
  4. optional tdse_step_dr(...)

tdse_step_discard(...) may be used instead of commit while a trial step is active. It clears only the trial/interpolation/IR state (and invalidates trial prefetch work), leaving the last committed state and output unchanged. Ordinary mode may then retry with a different t or dt. A prepared real-time session remains armed and owned by the same thread, so its fixed-step contract still applies on retry. Calling discard without an active trial returns TDSE_STATUS_INVALID_STATE; a foreign owner receives the normal concurrent-use status.

Two rules matter:

  • tdse_step_commit(...) is the preferred state-advancing call
  • if a trial solve fails, do not call commit; committed state remains unchanged

Lifecycle bugs and step-order bugs usually appear together. If ownership or sequencing is wrong, the first visible symptom is often a bad step call rather than a clearly named lifecycle error.

Close Semantics

tdse_model_close(...) is the explicit non-blocking lifecycle endpoint.

Use it when:

  • the caller needs a synchronous lifecycle result immediately
  • the caller does not want to wait for an in-flight same-handle API

Interpretation:

  • TDSE_STATUS_OK: close completed and local ownership ended
  • TDSE_STATUS_CONCURRENT_API_USE: another same-handle API is still in flight
  • TDSE_STATUS_INVALID_STATE: another thread already started close or destroy

close is not the recommended production shutdown path. It is the immediate-answer path.

Typical uses:

  • supervisory code wants a fast lifecycle answer
  • a wrapper wants overlap detection without entering a wait path
  • tests intentionally exercise ownership conflicts

Close is best understood as a guardrail API, not as a general shutdown API.

Destroy Semantics

tdse_model_destroy(...) is the recommended host-managed shutdown path.

Use it when:

  • the caller owns lifecycle policy
  • timeout matters
  • the caller wants structured result details

Typical usage:

tdse_model_destroy_options_t opt = tdse_model_destroy_options_init();
tdse_model_destroy_result_t result = tdse_model_destroy_result_init();
opt.wait_timeout_ms = 250.0;

tdse_status_t st = tdse_model_destroy(model, &opt, &result);

Interpretation:

  • TDSE_STATUS_OK: destroy completed
  • TDSE_STATUS_TIMEOUT: destroy did not acquire ownership before the budget expired; the handle is still valid
  • TDSE_STATUS_INVALID_STATE: another thread already started close or destroy; local ownership should be treated as gone

Destroy decision model

Destroy Result Matrix

StatusOwnership After ReturnHandle Still Live?Typical Host Action
TDSE_STATUS_OKno longer localnoclear references and continue
TDSE_STATUS_TIMEOUTstill localyesretry later, escalate, or change policy
TDSE_STATUS_INVALID_STATEno longer localunknown to this thread; assume not locally usablestop using the handle locally

Release Semantics

tdse_model_release(...) exists so C and C++ have an explicit finalizer target.

Use it when:

  • a destructor, guard, or finally block must make a best-effort terminal cleanup
  • the caller is not trying to implement a bounded lifecycle policy

Do not use it as the primary shutdown path in ordinary host code.

Interpretation:

  • TDSE_STATUS_OK: release completed
  • TDSE_STATUS_INVALID_STATE: another thread already started close, destroy, or release

This API is intentionally secondary to tdse_model_destroy(...).

Its purpose is very narrow and very important:

  • it gives destructors and unwind paths a clean terminal target
  • it avoids turning finalizer code into a policy engine

Release Result Matrix

StatusInterpretation
TDSE_STATUS_OKfinalizer cleanup completed
TDSE_STATUS_INVALID_STATEanother thread already owns terminal cleanup; local ownership is gone

C++ Wrapper Interpretation

The C++ wrapper follows the same lifecycle rules:

  • tdse::Model::fromMemory(...) wraps tdse_model_create(...)
  • tdse::Model::close() wraps tdse_model_close(...)
  • tdse::Model::destroy(options) wraps tdse_model_destroy(...)
  • tdse::Model::release() wraps tdse_model_release(...) but is noexcept and ignores the returned status
  • runtime status-bearing failures surface as tdse::Error, which preserves both the API name and tdse_status_t
  • tdse::Model::empty() / operator bool() are the supported way to check moved-from or already-cleaned-up wrappers
  • plain C integrations already receive the unified tdse_status_t; use tdse_status_error_category(...), tdse_status_stable_reason(...), and tdse_status_message(...) for machine classification, stable reason tokens, and human-readable logging

Important rule:

  • tdse::Model::destroy(...) requires explicit destroy options so the wait policy is always intentional
  • moved-from wrappers may be destroyed, checked with empty() / if (model), or assigned again; business APIs still throw on use

That requirement is deliberate. Product code should not drift into implicit infinite-wait shutdown through a convenient wrapper default.

Ownership Handoff Rules

When local ownership is gone, stop using the handle immediately. This is especially important when:

  • tdse_model_close(...) returns TDSE_STATUS_OK
  • tdse_model_destroy(...) returns TDSE_STATUS_OK
  • tdse_model_destroy(...) returns TDSE_STATUS_INVALID_STATE
  • tdse_model_release(...) returns TDSE_STATUS_OK
  • tdse_model_release(...) returns TDSE_STATUS_INVALID_STATE

The Runtime contract is:

  • if another thread has already started close or destroy, ownership is no longer local
  • the old pointer must not be treated as a reusable live handle

Ownership handoff is the place where many host bugs become latent use-after-destroy bugs. The safest policy is simple: once handoff happens, clear local references immediately.

C And C++ Mapping Table

IntentC APIC++ Wrapper
create with request-scoped diagnosticstdse_model_create(...)tdse::Model::fromMemory(..., &diag)
collect the compact runtime summarytdse_model_diagnostics_summary(...)tdse::Model::diagnosticsSummary() / tryDiagnosticsSummary()
collect one versioned detail payloadtdse_model_diagnostics_payload(...)tdse::Model::diagnosticsPayload() / tryDiagnosticsPayload()
collect one consistent summary plus payload settdse_model_diagnostics_query_many(...)tdse::Model::diagnosticsQueryMany() / tryDiagnosticsQueryMany()
fast close attempttdse_model_close(...)tdse::Model::close()
canonical bounded shutdowntdse_model_destroy(...)tdse::Model::destroy(options)
terminal finalizer cleanuptdse_model_release(...)tdse::Model::release() and destructor

Anti-Patterns

Avoid these patterns even if they appear to work in a small local test:

  1. using tdse_model_release(...) as the primary host shutdown API
  2. retrying destroy locally after TDSE_STATUS_INVALID_STATE
  3. treating TDSE_STATUS_TIMEOUT as if the handle were already gone
  4. continuing to use a handle after ownership handoff
  5. assuming close is just a faster destroy

A Good Mental Checklist

Before every lifecycle decision, ask:

  1. Do I still own this handle locally?
  2. Do I need a status-bearing shutdown answer?
  3. Do I need a bounded wait budget?
  4. Am I in business logic or finalizer cleanup?

If the answer is "business logic and I need a bounded answer," the API is almost always tdse_model_destroy(...).

Worked Lifecycle Scenarios

Scenario A. Clean Business Shutdown

  1. caller owns a live handle
  2. no same-handle API is in flight
  3. caller invokes tdse_model_destroy(...)
  4. runtime returns TDSE_STATUS_OK
  5. caller drops all references

This is the ideal host-managed path.

Scenario B. Destroy Races With A Worker Step

  1. worker thread is still inside a same-handle step call
  2. supervisory code calls tdse_model_destroy(...)
  3. destroy waits within its configured budget
  4. runtime returns either TDSE_STATUS_OK or TDSE_STATUS_TIMEOUT

This is why destroy carries explicit wait policy and result details.

Scenario C. Another Thread Already Took Ownership

  1. thread A starts close or destroy
  2. thread B also attempts close, destroy, or release
  3. thread B receives TDSE_STATUS_INVALID_STATE
  4. thread B must stop using the handle locally

This is not a local retry condition. It is ownership handoff.

Recommended:

  • ordinary host shutdown: tdse_model_destroy(...)

  • timeout-aware supervision: tdse_model_destroy(...) plus an explicit policy for TDSE_STATUS_TIMEOUT

  • destructor or guard cleanup: tdse_model_release(...)

  • immediate non-waiting close attempt: tdse_model_close(...)

Avoid:

  • using tdse_model_release(...) as the first-choice business-logic shutdown API
  • relying on infinite-wait destroy as the default production pattern without documenting why
  • continuing to use a handle after destroy, release, or ownership handoff