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. .. code-block:: python 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``: .. code-block:: dipl length float = 2.5 dm width float = 40 mm area float = ( {?length} * {?width} ) m2 Parse it into an environment with ``DIP.add_file``: .. code-block:: python 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: .. code-block:: python dip = DIP() dip.add_project("DIPfile") env = dip.parse() See :doc:`DIPfile projects <../modules/dip/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. .. code-block:: python 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 :doc:`Application adapters <../modules/dip/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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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 :doc:`Python DIP API reference <../api/python_dip>`. Registering schemas ------------------- Register a schema body directly, then apply it in ordinary DIPL code: .. code-block:: python 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()``: .. code-block:: python 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 :ref:`dip-overrides` 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``: .. code-block:: python 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 :doc:`Environment persistence <../modules/dip/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: .. code-block:: python 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``: .. code-block:: python 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: .. code-block:: python 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 :doc:`Static parameter generation <../modules/dip/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: .. code-block:: python 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 :ref:`CreateReport example ` for a PDF and :doc:`Environment persistence <../modules/dip/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: .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python 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 `_.