OptixLog Docs
Python SDKGenerated Bindings

Node Classes

How optixlog generate turns schema field definitions into frozen dataclasses with typed fields and a to_payload() method.

Every node type in your schema becomes a @dataclass(frozen=True, kw_only=True) class in the generated module. These classes are the values you pass to ingest; they carry your typed data and serialize it for the API.

Anatomy of a node class

@dataclass(frozen=True, kw_only=True)
class SimulationNode:
    # Required fields come first.
    config: Mapping[str, JSONValue]
    solver: Literal["fdtd", "eme", "varfdtd"]
    # Optional fields follow, with defaults.
    wavelength_nm: float = 1550.0
    other_param: str | None = None

    def to_payload(self) -> dict[str, JSONValue]:
        return {
            "config": dict(self.config),
            "solver": self.solver,
            "wavelength_nm": self.wavelength_nm,
            "other_param": self.other_param,
        }

Key properties:

  • frozen=True — instances are immutable. You cannot reassign fields after construction.
  • kw_only=True — all fields must be passed as keyword arguments, avoiding positional-argument confusion.
  • to_payload() — converts the dataclass to a plain dict[str, JSONValue] for the API. Container fields (Mapping, Sequence) are copied to concrete dict / list types because JSONValue requires concrete types.

Field types from schema

The code generator maps each schema field type to a Python type annotation:

Schema typePython annotation
stringstr
integerint
numberfloat
booleanbool
json / objectMapping[str, JSONValue]
array<T>Sequence[T]
enumLiteral[members...]

array<T> is resolved recursively, so array<number> becomes Sequence[float].

Required / optional rules

For a field with base annotation T:

  • required: true → name: T (no default; must be supplied at construction time)
  • required: false with a default → name: T = <default>
  • required: false without a default → name: T | None = None

Required fields are ordered before optional fields in both the dataclass body and to_payload.

Full example: SimulationNode

The schema for SimulationNode is:

schema.sample.json (excerpt)
{
  "name": "SimulationNode",
  "fields": [
    { "name": "config",        "type": "json",   "required": true },
    { "name": "solver",        "type": "enum",   "enum": ["fdtd", "eme", "varfdtd"], "required": true },
    { "name": "wavelength_nm", "type": "number", "required": false, "default": 1550.0 },
    { "name": "other_param",   "type": "string", "required": false }
  ]
}

This produces:

@dataclass(frozen=True, kw_only=True)
class SimulationNode:
    config: Mapping[str, JSONValue]
    solver: Literal["fdtd", "eme", "varfdtd"]
    wavelength_nm: float = 1550.0
    other_param: str | None = None

    def to_payload(self) -> dict[str, JSONValue]:
        return {
            "config": dict(self.config),
            "solver": self.solver,
            "wavelength_nm": self.wavelength_nm,
            "other_param": self.other_param,
        }

Constructing and using it:

from optixlog_gen import SimulationNode

node = SimulationNode(
    config={"mesh": "fine", "boundary": "pml"},
    solver="fdtd",
    wavelength_nm=1310.0,
    other_param="sweep-01",
)

# Frozen — this raises FrozenInstanceError:
# node.solver = "eme"

# Serialize for the API:
payload = node.to_payload()
# {"config": {"mesh": "fine", "boundary": "pml"}, "solver": "fdtd",
#  "wavelength_nm": 1310.0, "other_param": "sweep-01"}

Another example: MeasurementNode

@dataclass(frozen=True, kw_only=True)
class MeasurementNode:
    instrument: str
    samples: Sequence[float]
    notes: str | None = None

    def to_payload(self) -> dict[str, JSONValue]:
        return {
            "instrument": self.instrument,
            "samples": list(self.samples),
            "notes": self.notes,
        }

Note that samples: Sequence[float] becomes list(self.samples) in to_payload so the API receives a concrete list.

Universal fields

Every node class also gets two universal fields that every node type shares, regardless of schema:

  • name: str — a human-readable display name for the node in the workflow graph.
  • attached_files: list[str] — a list of file identifiers to attach to the node.

Node classes are emitted once per node type, even if that type is shared across multiple projects. A node class that appears in two projects' schemas is a single Python class in the generated module — not copied per project.

Shared node types

A node type defined in more than one project's schema appears as a single class in the generated module. Its class is the member of each relevant project's union alias:

# SimulationNode is in both projects.
_GratingCouplerLabNodes = SimulationNode | MeasurementNode
_ModulatorProgramNodes  = SimulationNode | LayoutNode

This means isinstance(node, SimulationNode) behaves correctly regardless of which project you ingested into.

On this page