Python binding

The Python binding exposes the SNT modules through the scinumtools3 package. It is built on the same C++ implementation used by the command-line and CMake interfaces, so PUQ and DIP expressions have the same semantics in Python and C++.

After installation, import the module or one of its submodules:

Quantities and units

PUQ represents numerical values together with their physical units and, when needed, uncertainties. Quantities can be added, multiplied, compared, formatted, and converted between compatible unit systems while retaining dimensional information. Use it when calculations should prevent accidental mixing of 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 definition from a file, evaluate its parameters, and return a value in Python. For example, given a file named parameters.dip:

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

Parse it into an environment with DIP.add_file:

from pathlib import Path
from scinumtools3.dip import DIP

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

For a reusable DIPfile manifest, call add_project instead of adding each file, source, and unit individually:

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

See DIPfile projects for the manifest format.

Application adapters

Subclass Adapter to turn an evaluated environment into one or more application-specific files. The Python callback receives the same registered output types as C++: text, binary bytes, and a streamed write(bytes) function. run_adapter_project parses a DIPfile; run_adapter_snapshot loads a DIPH5 snapshot; run_adapter accepts 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

There are two main ways to access nodes:

  • Cursor: use env["path"] to inspect a known path or traverse groups and collections in the environment.

  • Select: use env.select(...) to discover value nodes by path and tags and inspect independent snapshots of the results.

Cursor: inspect a known path

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

A cursor accesses a path in the environment. Use cursor.elements() for list elements and cursor.items() for named items in a map. It also exposes shape, metadata, provenance, and to_numpy().

The cursor also provides values and units for unitless scalars. For example, a file containing count int = 42 can be queried with env["count"].value. The cursor returned by env["area"] exposes both value and units; pass the value to Quantity when an explicit unit conversion is needed:

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 whose values, units, tags, and metadata can be inspected directly:

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; omitted or empty filters impose no restriction. Tags match only explicit assignments, without parent-tag inheritance.

? selects all value nodes, ?physics. selects a subtree including its value-bearing root and collection members, and ?physics.speed selects an exact node. Results retain fully qualified paths in environment order, with each matching node returned once. No matches returns an empty list.

Selected nodes are independent snapshots; selection does not modify the environment. Their 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()

Bodies start at indentation zero and omit the $schema wrapper. Registration preserves source information and leaves value evaluation to schema application. Put schema-level ? metadata before the first body node, then inspect it with env.schemas["settings"].metadata. Schema metadata describe the definition and are not copied onto the applying group, collection, item, or value nodes. DIPH5 stores evaluated nodes and descriptive schema provenance rather than reusable schema definitions. After loading a snapshot, inspect env.schema_manifest for schema descriptions and citations, or use env.applied_schemas("physics.speed") to find schemas applied along a value’s path. env.contributing_schema("physics.speed") identifies the schema that supplied that value node, if any; selected value nodes also expose its schema_id. Each record includes metadata, source_name, source_line, and an optional source identity with its path and hash. env.schemas remains empty after loading; the snapshot cannot instantiate schemas. The command API also supports argument_add("schema_string", [name, body]) and argument_add("schema_file", [name, path]); file paths there are 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

Override bodies also accept DIPL references, expressions, and calls to registered value functions, for example dip.add_override_string("radius = ({?base} * 2) cm"). The replacement is evaluated at the target declaration, so its dependencies must already be available then. See Overriding initial values for examples, evaluation order, and unit conversion rules.

Override bodies contain path = value modifications and optional nested path prefixes, such as "simulation\n  steps = 1024". Indentation starts at zero in string and file bodies. Each expanded path may appear once and must have a normal declaration. Overrides also replace !constant values during initial evaluation. Later ordinary modifications are ignored; dependent expressions and conditions see the replacement value. Existing conditions may activate or deactivate nodes. A target that is not instantiated is an unresolved override error. Registration can accompany add_project("DIPfile"); a rejected override body registers no entries. Inspect node.override on a selected node and cursor.provenance.override_source for its origin. DIPH5 preserves both the effective value and override provenance.

Environment persistence

Save evaluated parameters to a DIPH5 file and restore them through Environment:

from scinumtools3.dip import DIP, Environment

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

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

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

The command-oriented API also supports persistence. Configure the command with argument_save() or argument_load(); the file is written or read when execute() is called:

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 and always includes the full environment, regardless of request or tag filters. Output validation must succeed before saving. These command methods take file paths as strings; use str(path) for a pathlib.Path.

Generating static parameters

Export an evaluated environment with Environment.generate() when a validated DIPL configuration should become a source or data file consumed by another application. Import ExportFormat from scinumtools3.dip and choose one of 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-oriented Python API can generate during execute() as well:

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 only restrict textual output. See Static parameter generation for native representations and format-specific behavior.

Generating reports

Call Environment.generate_report() after parsing a project or loading a DIPH5 snapshot. ReportFormat selects TeX, PDF, Markdown, reStructuredText, HTML, Typst, plain text, or 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 values are RST, TYPST, TEXT, and JSON. Markdown uses MyST-style table directives; JSON contains a briefpp/1 document tree. These text formats need no external tools.

intro_file is an optional trusted LaTeX fragment without a preamble, accepted only for TeX and PDF. PDF output needs a local TeX compiler; pass tex_compiler="lualatex" or another compatible executable when required. The same method works on an Environment restored with load() and reports only provenance retained in DIPH5. The default title is DIP parameter report; an empty author appears as Not specified, while date and version default to the local generation date and SNT build version. The PDF has a cover and linked contents page; TeX output needs no external tool. See the CreateReport example for a PDF and Environment persistence for DIPH5 limits.

Source provenance

Each value cursor exposes a read-only provenance object. It contains the source name, source line, captured source line, and DIPL citation metadata. DIPH5 version 2 also preserves a source manifest, available as environment.source_manifest, with recorded 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)

When a value was overridden, provenance.override_source, provenance.override_line, and provenance.override_code identify the effective value’s origin; the original declaration fields remain available.

Inspecting values and tables

The read-only DIP inspection API gives a renderer stable paths, effective values, units, metadata, source locations, applied changes, and schema facts. inspect_values(env) retains environment order. A value’s changes list is also in evaluation order. inspect_capabilities(env, path) tells a client whether a path contains a value, children, an array, or a table. Flags for reference graphs and direct editing 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 every table. Column values live at their paths; the table inspection object contains column metadata and row count. Slice ranges are zero-based and inclusive, and read from an evaluated in-memory value. DIPH5 loading is still eager. open_artifact(path) loads a DIPfile, DIPL file, or DIPH5 snapshot, while reload_artifact(env, path) replaces an environment after a successful load.

DIP exceptions provide a diagnostic attribute containing 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 ordinary Python values: int, float, str, bool, lists, and numpy.ndarray. There is no separate VAL value class to learn, so results work directly with normal Python and NumPy code.

Why EXS is not exposed

EXS is used internally by PUQ and DIP, but is not exposed as a standalone Python module. Python already has mature tools for custom expression evaluation and grammars; a direct EXS binding would be useful mainly when the same custom language must run across Python, C++, the CLI, and REST services.

For application-oriented operations, use the Python API helpers exposed by the installed package. They evaluate PUQ and DIPL definitions through the same implementation as the snt command-line tool and the C++ API. The Python binding is therefore the preferred interface 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.