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 plaindict[str, JSONValue]for the API. Container fields (Mapping,Sequence) are copied to concretedict/listtypes becauseJSONValuerequires concrete types.
Field types from schema
The code generator maps each schema field type to a Python type annotation:
Schema type | Python annotation |
|---|---|
string | str |
integer | int |
number | float |
boolean | bool |
json / object | Mapping[str, JSONValue] |
array<T> | Sequence[T] |
enum | Literal[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: falsewith adefault→name: T = <default>required: falsewithout adefault→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:
{
"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 | LayoutNodeThis means isinstance(node, SimulationNode) behaves correctly regardless of which project you ingested into.