Identity and versions

The stable IDs of structures, materials, surfaces and antennas, how to allocate them, and the four scene versions that drive cache invalidation.

Solvers compile a scene once and reuse the result as long as nothing relevant changed. Core supports this with two things: stable IDs that name each part of the world, and four version numbers that change when a part changes.

IDs#

ID typeNamesSet on
StructureIdone structureStructure.structure_id
MaterialIdone material objectPhysicalMaterial.material_id, Structure.material_id
AssignmentIdone material-to-surface bindingStructure.assignment_id
SurfaceIdone surfaceStructure.surface_id
AntennaIdone endpointAntennaState.antenna_id
PrimitiveIdone primitive (for example a triangle) of a structureStructure.primitive_ids, Structure.primitive_id(i)

All of them are plain non-negative int values wrapped in typing.NewType, so StructureId(4) == 4.

Two ways to get an ID#

Automatic. If you omit an ID, Core calls new_*_id(), which draws the next value from a counter that is global to the Python process. The values are unique within the process, but they depend on how many objects you built before. The same scene built in another order, or in another process, gets other IDs.

Reserved. If you pass an ID (Structure(..., structure_id=10)), Core calls reserve_*_id(10). It records your value as is and moves the counter past it, so later automatic IDs never collide with it.

Use reserved IDs whenever the IDs must mean the same thing later: a scene loaded from a file, a scene compared with a stored result, or a cache that outlives the process. Do not mix the two modes for one world: solvers key their caches on these IDs, and mixing can defeat cache reuse.

AntennaState has no automatic mode. Its first argument, antenna_id, is required.

Core never derives an ID from content. Choosing a reproducible scheme is up to you.

Rules a scene enforces#

Scene(...) raises ValueError when:

  • two structures share a StructureId, AssignmentId or SurfaceId;
  • two different material objects share a MaterialId (the same object used twice is fine, and is how structures share a material);
  • a PrimitiveId appears twice in one structure or in two structures;
  • two endpoints share an AntennaId.

A structure inherits material_id from its material when the material has one, which PhysicalMaterial always does. Without explicit primitive_ids, Structure.primitive_id(i) returns (structure_id << 32) | i, so primitive IDs are unique across structures as long as each structure has fewer than 2³² primitives.

Versions#

A Scene and a SceneSnapshot each report four versions, in scene.versions (a SceneVersions) or one by one:

VersionChanges when
topology_versionThe set of structures, their structure, surface or primitive IDs, a mesh's face tensor, or a face_uv tensor changes.
geometry_versionA geometry tensor, a uv tensor, an endpoint, the snapshot time or any motion changes.
material_versionA material's ID or version field changes, or one of its tensors changes.
assignment_versionThe structure-to-material binding or a phase screen changes.

A solver compares versions to decide what to rebuild. For example, when only material_version changed, it can keep everything it built from geometry.

How versions are computed#

Each version combines two things:

  1. an authored counter that the with_* methods advance (for example with_structure_geometry adds 1 to the geometry counter);
  2. a fingerprint of the tensors in that domain: their identity, device, dtype, shape, stride, requires_grad and PyTorch's in-place modification counter.

So an in-place update such as position.add_(1.0) or an optimizer step changes the version of the domain that owns the tensor, and only that one.

What versions do not see

  • Python scalars are not fingerprinted. Two PhysicalMaterial objects with the same material_id and different float eps_r give the same material_version. When you replace a material with Python values, raise its version (PhysicalMaterial(..., material_id=m, version=1)) or use Scene.with_material, which advances the counter for you.
  • Versions are not content hashes. They include tensor identity, so rebuilding an equal scene from new tensors gives new versions. Compare versions only within one process, and only for equality.

Version values are large integers. Treat them as opaque tokens. SceneVersions(...) and SceneVersions.advanced(...) work with the small authored counters; the values a scene reports are not small.

Reading the versions walks the whole scene, so the cost grows linearly with the number of structures. The 0.4.0 release measured about 17 ms to read all four on a 1024-structure scene.