Skip to content

niess.brep

Solid geometry from the niess object tree, as a build123d assembly or a STEP file. See Export an instrument to CAD for the task-oriented guide and the CAD gallery for what each component is drawn as.

brep

Solid geometry -- STEP/CAD -- from a niess instrument.

from niess.instrument import Instrument, Mount
from niess.brep import save_step

save_step(instrument, 'instrument.step')

Reads the tree: a component's position and orientation are on the component, as scipp Variables, and composing them with the frame it hangs from is arithmetic. There is no other route -- niess draws niess instruments.

Importing this module registers the shape builders, which is what to_assembly resolves against.

Modules:

  • assembly –

    Solid geometry, built from the tree.

  • builders –

    The shape each component is drawn as.

Classes:

Functions:

  • save_step –

    Write an instrument to a STEP file.

  • to_assembly –

    Build a labelled build123d assembly of everything with a shape.

NiessBRepRegistry

NiessBRepRegistry(parent=None)

Bases: NiessRegistry

Shape-builder lookup for the CAD target.

Resolves either way: :meth:resolve_for_object for a niess object the walk handed over, :meth:resolve_builder for an emitted instance. The builders take a :class:Subject and do not know which happened.

Source code in src/niess/brep/assembly.py
def __init__(self, parent=None):
    super().__init__(parent=parent, hooks='brep')

Subject dataclass

Subject(name: str, params: dict, obj: Any = None, visit: Any = None)

What a shape builder is given.

The builders were written against the McCode parameters a component reports, so that is what they still take, alongside the object itself -- reading a dimension off it beats reading the four edges it emits as.

save_step

save_step(instrument, path, registry=None, parameters: dict | None = None) -> None

Write an instrument to a STEP file.

Source code in src/niess/brep/assembly.py
def save_step(instrument, path, registry=None, parameters: dict | None = None) -> None:
    """Write an instrument to a STEP file."""
    from mccode_antlr.display.render.brep import save_step as write
    write(to_assembly(instrument, registry=registry, parameters=parameters), path)

to_assembly

to_assembly(instrument, registry=None, parameters: dict | None = None)

Build a labelled build123d assembly of everything with a shape.

The assembly follows the tree: each component is a child labelled with its emitted name, inside a group for each node that holds more than one shaped thing -- tank > channel_3 > channel_3_1_analyzer -- so a viewer can show, hide and pick them one at a time. A node holding just one is not a group of its own.

Every node is placed, whether or not it has a shape: a frame draws nothing and is still what the things hanging from it are measured against.

parameters sets the run-time knobs frames turn by, by name -- BIFROST's {'detector_tank_angle': 60, 'sample_rotation': 30}, in degrees. A knob not given is drawn at its declared default; a name no frame turns by is an error.

Source code in src/niess/brep/assembly.py
def to_assembly(instrument, registry=None, parameters: dict | None = None):
    """Build a labelled ``build123d`` assembly of everything with a shape.

    The assembly follows the tree: each component is a child labelled with its emitted
    name, inside a group for each node that holds more than one shaped thing --
    ``tank`` > ``channel_3`` > ``channel_3_1_analyzer`` -- so a viewer can show, hide
    and pick them one at a time. A node holding just one is not a group of its own.

    Every node is placed, whether or not it has a shape: a frame draws nothing and is
    still what the things hanging from it are measured against.

    ``parameters`` sets the run-time knobs frames turn by, by name -- BIFROST's
    ``{'detector_tank_angle': 60, 'sample_rotation': 30}``, in degrees. A knob not
    given is drawn at its declared default; a name no frame turns by is an error.
    """
    import build123d as bd
    from ..walk import Visit

    resolver = BREP_REGISTRY if registry is None else registry
    context = BRepContext(instrument=instrument)
    _check_knobs(instrument, parameters)

    def descend(visit):
        # placed before its children, in declaration order, as a frame they name must be
        position, orientation = context.place(visit, *_local_placement(visit, parameters))
        own = None
        builder = resolver.resolve_for_object(visit.obj)
        shape = None if builder is None else builder(Subject(
            name=visit.name, obj=visit.obj, params=mccode_parameters(visit.obj), visit=visit))
        if shape is not None:
            shape.label = _label(visit)
            own = _located(shape, position, orientation)
            context.shapes.append(own)
        parts = [] if own is None else [own]
        parts.extend(p for p in map(descend, visit.children()) if p is not None)
        if len(parts) < 2:
            return parts[0] if parts else None
        return bd.Compound(children=parts, label=_label(visit))

    built = descend(Visit(context=context, obj=instrument))
    name = getattr(instrument, 'name', None) or 'instrument'
    if built is None:
        return bd.Compound(children=[bd.Box(1e-3, 1e-3, 1e-3)], label=name)
    if built in context.shapes:
        # a lone shape is still one child of the instrument, not the instrument itself
        return bd.Compound(children=[built], label=name)
    built.label = name
    return built