Python binding

The scinumtools3 package exposes SNT modules to Python. It uses the same C++ implementation as the command-line and CMake interfaces, so PUQ and DIP expressions behave the same way in Python and C++.

Import the classes you need from their respective submodules, as shown below.

Quantities and units

PUQ combines numerical values with physical units and optional uncertainties. You can add, multiply, compare, format, and convert quantities while retaining their dimensions. This helps catch calculations that mix incompatible units.

from scinumtools3.puq import Quantity
from scinumtools3.dip import DIP

length = Quantity(2.5, "m")
print(length.convert("cm"))

DIPL parameters

The Python binding can load a DIPL file, evaluate its parameters, and make their values available as Python objects. For example, given parameters.dip:

length float = 2.5 dm
width float = 40 mm
area float = ( {?length} * {?width} ) m2

Add the file to a parser, then evaluate it into an environment:

from pathlib import Path
from scinumtools3.dip import DIP

dip = DIP()
dip.add_file(Path("parameters.dip"))
env = dip.parse()

For a DIPfile project, add_project registers its files, sources, and units from the manifest:

dip = DIP()
dip.add_project("DIPfile")
env = dip.parse()

See DIPfile projects for the manifest format.

Compare saved snapshots with compare_diph5 or loaded environments with compare. The default scope compares effective values; Full also compares persisted metadata and provenance.

from scinumtools3.dip import (
    ComparisonOptions, ComparisonScope, compare_diph5, render_comparison
)

options = ComparisonOptions()
options.scope = ComparisonScope.Full
result = compare_diph5("before.diph5", "after.diph5", options)
print(render_comparison(result, max_details=20))
print(result.equal, result.added, result.removed, result.changed)

The command layer also provides scinumtools3.api.dip.DIPCompare. See DIPH5 comparison for the compared fields.

Application adapters

Subclass Adapter to turn an evaluated environment into files for an application. As in C++, an adapter can register text, binary data, and a streaming callback that writes bytes. run_adapter_project parses a DIPfile, run_adapter_snapshot loads a DIPH5 snapshot, and run_adapter uses an existing environment.

from scinumtools3.dip import Adapter, run_adapter_project

class AnalysisAdapter(Adapter):
    def plan(self, env, context):
        steps = env["run.steps"].value
        context.add_text("job.json", '{"steps": %d}\n' % steps)
        context.add_binary("marker.bin", b"SNT3")

        def write_steps(write):
            for i in range(steps):
                write(f"{i}\n".encode())

        context.add_stream("steps.csv", write_steps)

run_adapter_project("DIPfile", AnalysisAdapter(), "analysis-inputs")

See Application adapters for path rules and the complete C++ and Python examples.

Accessing nodes

Use either of these approaches to access nodes:

  • Cursor: use env["path"] when you know the path, or to traverse its groups and collections.

  • Select: use env.select(...) to find value nodes by path and tags. Each result is an independent snapshot.

Cursor: inspect a known path

area = env["area"]
print(area.value, area.units)  # 0.01 m2
print(area.metadata.description)

A cursor points to a path in the environment. Use cursor.elements() to traverse a list and cursor.items() to traverse named map items. Cursors also expose shape, metadata, provenance, and to_numpy().

For a unitless scalar such as count int = 42, read env["count"].value. For a value with units, a cursor exposes both value and units. Pass them to Quantity when you need an explicit conversion:

from scinumtools3.puq import Quantity

area_in_cm2 = Quantity(area.value, area.units.to_string()).convert("cm2")
print(area_in_cm2)  # 100 cm2

Select: discover and inspect nodes

Select returns value nodes with their values, units, tags, and metadata:

dip = DIP()
dip.add_string('''physics
  speed float = 2 m/s
    !tags ["export", "runtime", "hydro"]
    ?descr "Flow speed"
''')
env = dip.parse()
for node in env.select(
    "?physics.",
    tags_all=["export", "runtime"],
    tags_any=["hydro", "gravity"],
    tags_none=["internal", "deprecated"],
):
    print(node.name, node.value, node.units, node.tags, node.metadata.description)

tags_all requires every listed tag; tags_any requires at least one; and tags_none excludes nodes with any listed tag. The filters combine with AND. An omitted or empty filter imposes no restriction. Only tags assigned directly to a node match; parent tags are not inherited.

? selects every value node. ?physics. selects a subtree, including a value-bearing root and collection members. ?physics.speed selects one exact node. Results retain fully qualified paths and environment order, and each matching node appears once. If nothing matches, the result is an empty list.

Selection does not modify the environment. A selected node is an independent snapshot, and its tags and metadata properties are read-only. Changing a returned tags list affects only that list. Metadata remains valid while its Python wrapper is alive.

Additional request helpers remain available in the Python DIP API reference.

Registering schemas

Register a schema body directly, then apply it in ordinary DIPL code:

dip = DIP()
dip.add_schema_string("settings", "speed float = 2 m/s\n")
# Alternatively: dip.add_schema_file("settings", Path("settings.dipl"))
dip.add_string("physics : settings")
env = dip.parse()

Schema bodies start at indentation zero and omit the $schema wrapper. Registration retains source information; values are evaluated when the schema is applied. Put schema-level ? metadata before the first body node and inspect it through env.schemas["settings"].metadata. This metadata describes the schema definition. It is not copied onto the group, collection, item, or value node where the schema is applied.

DIPH5 stores evaluated nodes and descriptive schema provenance, but not reusable schema definitions. After loading a snapshot, use env.schema_manifest for schema descriptions and citations. env.applied_schemas("physics.speed") lists schemas applied along a value’s path, while env.contributing_schema("physics.speed") identifies the schema that supplied the value node, if any. Selected value nodes also expose its schema_id. Each schema record contains metadata, source_name, source_line, and an optional source identity with a path and hash. env.schemas remains empty after loading, so a snapshot cannot instantiate schemas.

The command API also accepts argument_add("schema_string", [name, body]) and argument_add("schema_file", [name, path]). Pass file paths as strings.

Overriding values

Use a $override region in DIPL, or register an unwrapped override body before parse():

dip = DIP()
dip.add_override_string("simulation.steps = 1024")
# Alternatively, read an unwrapped body from a file:
# dip.add_override_file("overrides.dip")
dip.add_string("simulation\n  steps int = 100")
env = dip.parse()
print(env["simulation.steps"].value)  # 1024

An override body contains path = value modifications and may use nested path prefixes such as "simulation\n  steps = 1024". Both string and file bodies start at indentation zero. Each expanded path may appear only once and must have a normal declaration. Override values can also use DIPL references, expressions, and registered value functions. For example, call dip.add_override_string("radius = ({?base} * 2) cm"). The replacement is evaluated at the target declaration, so its dependencies must already be available. See Overriding initial values for more examples, evaluation order, and unit conversion rules.

Overrides can replace !constant values during initial evaluation. Later ordinary modifications are ignored, while dependent expressions and conditions see the replacement. An existing condition may then activate or deactivate nodes. An override whose target is never instantiated produces an unresolved override error. You can also register overrides after add_project("DIPfile"); if an override body is rejected, it registers no entries.

Inspect node.override on a selected node and cursor.provenance.override_source to find the replacement’s origin. DIPH5 preserves both the effective value and its override provenance.

Environment persistence

Save evaluated parameters to DIPH5 and restore them with Environment:

from scinumtools3.dip import DIP, Environment

parser = DIP()
parser.add_string("simulation.steps int = 100")
env = parser.parse(record_dependency_graph=True)
env.save("parameters.diph5")

restored = Environment()
restored.load("parameters.diph5")
assert restored["simulation.steps"].value == 100

See Environment persistence for the file format, save and load behavior, and current limitations.

The command API supports the same workflow. Configure argument_save() or argument_load(); the file is written or read when you call execute():

from scinumtools3.api.dip import DIPParse

save = DIPParse()
save.argument_add("string", ["simulation.steps int = 100"])
save.argument_save("parameters.diph5")
save.execute()

load = DIPParse()
load.argument_load("parameters.diph5")
load.argument_request("simulation.steps")
load.argument_value("integer")
assert load.execute() == "100\n"

argument_load() cannot be combined with argument_add(). Saving overwrites the destination with the full environment, regardless of request or tag filters, and requires successful output validation. These command methods take file paths as strings; use str(path) for a pathlib.Path.

Generating static parameters

Use Environment.generate() to export validated DIPL parameters as source or data files for another application. Import ExportFormat from scinumtools3.dip and choose CPP, C, FORTRAN, RUST, JULIA, JSON, or YAML:

from scinumtools3.dip import DIP, ExportFormat

parser = DIP()
parser.add_string("simulation.steps int = 100")
env = parser.parse()
env.generate(ExportFormat.CPP, "parameters.hpp")
env.generate(ExportFormat.JSON, "parameters.json")

The command API can also generate a file during execute():

from scinumtools3.api.dip import DIPParse

generate = DIPParse()
generate.argument_add("file", ["parameters.dip"])
generate.argument_generate("rust", "parameters.rs")
generate.execute()

Generation always exports the complete evaluated environment. Requests and tags restrict only textual output. See Static parameter generation for native representations and format-specific behavior.

Generating reports

After parsing a project or loading a DIPH5 snapshot, call Environment.generate_report() to create a report. ReportFormat supports TeX, PDF, Markdown, reStructuredText, HTML, Typst, plain text, and Brief++ document JSON:

from scinumtools3.dip import DIP, ReportFormat

parser = DIP()
parser.add_project("DIPfile")
env = parser.parse()
env.generate_report(ReportFormat.TEX, "report.tex",
                  input_label="DIPfile", intro_file="introduction.tex")
env.generate_report(ReportFormat.PDF, "report.pdf",
    input_label="DIPfile", intro_file="introduction.tex",
    title="Mock Heat Flow Study", author="Example Research Team",
    date="2026-09-28", version="1.0 demo")
env.generate_report(ReportFormat.HTML, "report.html", input_label="DIPfile")
env.generate_report(ReportFormat.MARKDOWN, "report.md")

The remaining format values are RST, TYPST, TEXT, and JSON. Markdown uses MyST-style table directives, and JSON contains a briefpp/1 document tree. These text formats need no external tools. The report keeps a full entry for every parameter. Its Parameter guide links to entries in HTML and PDF; the Schemas section lists parameters supplied by each schema. Parsing with record_dependency_graph=True adds evaluated expressions, direct reads, branch selections, and readers to the relevant entries. A loaded DIPH5 environment uses its saved graph if present. Without a graph, the report states that calculation relationships are unavailable.

Use intro_file to supply a trusted LaTeX fragment without a preamble for TeX or PDF output. PDF generation requires a local TeX compiler; set tex_compiler="lualatex" or another compatible executable if needed. TeX output itself needs no external tool. Generated PDFs include a cover and a linked contents page.

You can also generate a report from an Environment restored with load(). Such a report includes only provenance retained in DIPH5. The default title is DIP parameter report; an empty author appears as Not specified. The date and version default to the local generation date and SNT build version. See the CreateReport example for a PDF and Environment persistence for DIPH5 limits.

Source provenance

Each value cursor has a read-only provenance object with the source name, line number, captured source text, and DIPL citation metadata. DIPH5 version 2 also preserves a source manifest. Access it through environment.source_manifest to inspect source paths and SHA-256 content fingerprints:

provenance = env["simulation.steps"].provenance
print(provenance.source_name, provenance.source_line)
print(provenance.metadata.doi)
if provenance.source is not None:
    print(provenance.source.path, provenance.source.hash)

If a value was overridden, provenance.override_source, provenance.override_line, and provenance.override_code identify its effective origin. The original declaration fields remain available.

Inspecting environments

Values and tables

The read-only DIP inspection API exposes stable paths, effective values, units, metadata, source locations, applied changes, and schema information. inspect_values(env) returns values in environment order; each value’s changes list follows evaluation order. Use inspect_capabilities(env, path) to check whether a path contains a value, children, an array, or a table. The direct editing flags remain false.

from scinumtools3.dip import inspect_capabilities, inspect_table, inspect_value, read_value_slice

if inspect_capabilities(env, "measurements").has_tabular_data:
    table = inspect_table(env, "measurements")
    for column in table.columns:  # original DIPL header order
        if table.rows:
            values = read_value_slice(env, column.path, [(0, table.rows - 1)])
            print(column.name, column.units, values)

speed = inspect_value(env, "physics.speed")
print(speed.value, speed.declaration_location, speed.override_location)

inspect_tables(env) lists all tables. A table inspection object gives its column metadata and row count; column values remain available at their own paths. Slice ranges are zero-based and inclusive, and read from an evaluated in-memory value. DIPH5 loading is still eager.

describe(env, path) assembles a bounded semantic description with a stable path, type, tags, metadata, available value text, provenance, and dependency availability. list_descriptions(env, query, limit=...) returns a bounded selection with the total match count. It accepts tags_all, tags_any, and tags_none like env.select. Its default omits value content. preview(path, overrides) evaluates separate baseline and candidate environments and returns diagnostics and an effective comparison. override_contract(env, path) classifies a possible override target and reports its current type and enforced rules. Preview the actual candidate to validate it:

from scinumtools3.dip import (
    PreviewOverride, PreviewOverrideKind, describe, list_descriptions,
    open_artifact, override_contract, preview,
)

env = open_artifact("DIPfile")
print(describe(env, "simulation.steps").value_text)
print(override_contract(env, "simulation.steps").kind)
print(list_descriptions(env, "?simulation.", limit=20).total)
result = preview("DIPfile", [
    PreviewOverride(PreviewOverrideKind.Text, "simulation.steps = 200")
])
if result.candidate_valid:
    print(result.comparison.differences)
else:
    print(result.candidate_diagnostics)

Dependency graphs

Dependency recording is off by default. Enable it with parser.parse(record_dependency_graph=True) or open_artifact(path, record_dependency_graph=True). Pass the option again to reload_artifact when refreshing parsed source. inspect_dependency_graph(env) returns node reads and numerical or logical operation trees. Its recorded flag distinguishes a captured graph from an ordinary parse.

from scinumtools3.dip import DIP, DependencyEventKind, inspect_dependency_graph

parser = DIP()
parser.add_string(
    "distance float = 12 m\n"
    "time float = 3 s\n"
    "speed float = ({?distance} / {?time}) m/s\n"
)
env = parser.parse(record_dependency_graph=True)
graph = inspect_dependency_graph(env)
print([edge.target for edge in graph.dependencies("?speed")])
event = graph.latest("?speed", DependencyEventKind.Value)
if event is not None and event.composition is not None:
    print(event.composition.root, len(event.composition.nodes))

Node IDs use ?path for the current environment and source?path for imports. dependencies reports reads from the latest value evaluation; events also retains earlier evaluations and branch decisions. A saved DIPH5 snapshot retains its recorded graph, and loading uses that saved state regardless of the record_dependency_graph option. See the DIP inspection guide for graph semantics and limitations.

Reload and diagnostics

open_artifact(path) loads a DIPfile, DIPL file, or DIPH5 snapshot. reload_artifact(env, path) replaces an environment only after the new artifact loads successfully. DIP exceptions have a diagnostic attribute with a structured category, message, details, suggestion, and available source locations:

from scinumtools3.dip import diagnostic_from_exception, reload_artifact

try:
    reload_artifact(env, "DIPfile")
except RuntimeError as error:
    diagnostic = diagnostic_from_exception(error)
    if diagnostic is not None:
        print(diagnostic.code, diagnostic.message, diagnostic.location)

Python values and NumPy

The C++ VAL layer maps to familiar Python types: int, float, str, bool, lists, and numpy.ndarray. Results therefore work directly with Python and NumPy; there is no separate VAL value class to learn.

Why EXS is not exposed

PUQ and DIP use EXS internally, but there is no standalone EXS Python module. Python has tools for custom expression evaluation and grammars. A direct EXS binding would mainly help when the same custom language must run in Python, C++, the CLI, and REST services.

For application operations, use the package’s Python API helpers. They evaluate PUQ and DIPL definitions through the same implementation as the snt command-line tool and the C++ API. Use the binding when a Python program needs typed results or direct access to module objects without starting a subprocess.

For installation options and the complete Python API, see the Python binding README.