Linked models, jobs and results

Save and pin linked subsystems

Dependencies reference saved revisions. A root’s source declares usages of the types defined in those dependency documents:

library_source = """package Cells {
    part def Cell { attribute charge = 10; }
}"""
root_source = """package Assembly {
    part cell : Cells::Cell;
}"""

# Inside an authenticated Client context:
library = studio.submit_model("Cells", library_source)
assembly = studio.submit_model(
    "Assembly", root_source,
    dependencies=[{"project_id": library.id, "version": library.version}],
)
bundle = studio.resolve_model(assembly.id, version=assembly.version)
local_model = studio.get_connected_model(assembly.id, version=assembly.version)

resolve_model returns the authorized source bundle without needing the local engine. get_connected_model resolves that bundle into a local executable object and needs sysml2py. Model.to_connected_model() handles standalone downloaded models; it rejects external dependency pins because it cannot fetch them. Mixed SysML/Python sources are resolved together.

Update a model using the snapshot’s revision:

current = studio.get_model(library.id)
saved = studio.save_model(current, source=current.source.replace("10", "20"))

The returned object is a new revision. Existing assemblies still refer to their previous dependency revision until their dependency pins are updated. A stale save raises APIError with status 409; reload and reconcile the source before trying again.

Simulation inputs and clocks

Inputs use fully qualified paths to declared, undriven attributes. Stored initial values use initial_values. Connected or bound targets cannot be overridden. The model’s declared unit is used when an explicit input unit is omitted.

job = studio.simulate(
    model.id, version=model.version,
    end_time=2, step_size=0.25,
    inputs={
        "Motion.cart.speed": {
            "unit": "m/s",
            "samples": [{"time": 0, "value": 2}, {"time": 1, "value": 4}],
        },
    },
    initial_values={"Motion.cart.distance": {"value": 50, "unit": "cm"}},
    time_attribute="Motion.cart.time",
    step_attribute="Motion.cart.dt",
    events=[{"time": 1.5, "target": "Motion.cart.mode", "event": "Stop"}],
)

Paths depend on the actual model; the snippet assumes the corresponding input and state-machine declarations exist. The checked local example on Executable local examples shows a complete model. Sampled signals are piecewise constant. Signal and event times use seconds. Clock bindings populate duration inputs automatically, including the final partial step.

Keep long-running jobs resumable

Convenience methods such as simulate submit and wait. To retain the job ID before waiting, submit explicitly:

from uuid import uuid4
from sysml_studio import APIError, JobFailed

request_key = str(uuid4())  # Persist and reuse only for this exact request.
job = studio.start_job(model.id, "simulate", version=model.version,
                       end_time=20, step_size=1,
                       idempotency_key=request_key)
print("Retain this job ID:", job.id)
try:
    finished = studio.wait(job.id, timeout=60, poll_interval=1)
except TimeoutError:
    # Waiting stopped; the worker continues. Poll later or explicitly cancel.
    snapshot = studio.get_job(job.id)
except JobFailed as error:
    print("Worker failure or cancellation:", error)
except APIError as error:
    print(error.status_code, error.detail, error.request_id)

Use studio.cancel_job(job.id) to request cancellation. Transport errors remain httpx exceptions; they are separate from worker failures. No automatic retry occurs. After an ambiguous submission failure, reuse the same idempotency key and payload to recover an accepted response. The current server retains these keys for seven days. For HTTP 429, inspect the raw retry_after header before deciding when to retry.

Read and export results

Job.result contains job-kind-specific metadata. Fetch numerical samples with results:

payload = studio.results(job.id)
assert payload["schema"] == "sysml2py.results.v1"
columns, units, rows = payload["columns"], payload["units"], payload["rows"]
index = columns.index("Motion.cart.distance")
series = [(row[0], row[index]) for row in rows]
print("Distance unit:", units[index])
files = studio.download_results(job.id, "results/run-001")

The first column is time. Use column names instead of assuming an attribute order. The remote JSON envelope and local ConnectedModel.simulate dictionary have different metadata layouts, described in their references.

Downloads include advertised result artifacts and a client job.json manifest. Archives are saved without extraction. Existing destinations are never replaced. Completed files remain after a failed download; retry into a new directory.

Catalogs and sharing

list_components(search=..., offset=...) returns items and next_offset; continue until the latter is None. instantiate_component saves a model from a template. list_spaces, create_space and space_items manage project containers. share("projects", model.id, visibility="project", project_space_id=space_id) assigns a permitted sharing scope. The server enforces visibility, ownership and organization membership for all of these operations. See Client API reference for each method’s parameters and results.