Skip to content

CellDesigner layout

pd2af.building.celldesigner.layout

Build the AF layout, reusing the input map's glyphs where possible.

make_and_add_layout is the layout pass: it walks the model built by :mod:pd2af.building.celldesigner.model and populates context.layout and context.layout_model_mapping, branching on layout_mode. The leaf primitives below -- synthetic nodes, modulation arcs, mapping helpers, and background cloning for overlay -- do the per-element construction.

:mod:pd2af.core invokes this pass through a :class:pd2af.building.context.BuilderContext after the model pass.

Functions:

Name Description
add_mappings_for_layout_and_descendants

Map an input glyph and its descendants to their model elements.

add_modulation_mapping

Map the arc, together with its two endpoint clusters, to modulation.

clone_layout_pruning_foreground

Clone an input layout element as a builder, to serve as dimmed background.

make_and_add_layout

Build context.layout and context.layout_model_mapping (pass 2).

make_decoration_stripped_layout

Return a frozen copy of input_layout stripped of its decoration glyphs.

make_logic_arc

Build a LogicArcLayout from a gate to one of its input species.

make_modulation_arc

Build the arc layout for modulation between two node layouts.

make_synthetic_gate_layout

Build a placeholder gate node for gate.

make_synthetic_layout

Build a placeholder Node for species.

new_layout_and_mapping_builders

A fresh, empty (layout builder, layout-model mapping builder) pair.

add_mappings_for_layout_and_descendants

add_mappings_for_layout_and_descendants(context: BuilderContext, input_layout: Any, original_to_stripped: dict[int, Any] | None = None)

Map an input glyph and its descendants to their model elements.

With original_to_stripped, the glyphs actually placed are the stripped clones of :func:make_decoration_stripped_layout: a clone may be a fresh dataclasses.replace copy that is not a key of the input layout-model mapping (keyed by the input frozen objects), so we walk the input originals and follow each to its stripped counterpart. Dropped decoration glyphs are absent from that map, so they never get mapped -- and being absent from the layout, never render.

Source code in src/pd2af/building/celldesigner/layout.py
def add_mappings_for_layout_and_descendants(
    context: pd2af.building.context.BuilderContext,
    input_layout: typing.Any,
    original_to_stripped: dict[int, typing.Any] | None = None,
):
    """Map an input glyph and its descendants to their model elements.

    With ``original_to_stripped``, the glyphs actually placed are the stripped
    clones of :func:`make_decoration_stripped_layout`: a clone may be a fresh
    ``dataclasses.replace`` copy that is not a key of the input layout-model
    mapping (keyed by the input frozen objects), so we walk the input originals
    and follow each to its stripped counterpart. Dropped decoration glyphs are
    absent from that map, so they never get mapped -- and being absent from the
    layout, never render.
    """
    input_layout_model_mapping = context.input_map.layout_model_mapping
    for original in [input_layout] + list(input_layout.descendants()):
        placed = original
        if original_to_stripped is not None:
            placed = original_to_stripped.get(id(original))
            if placed is None:
                continue
        if original not in input_layout_model_mapping:
            continue
        model_value = input_layout_model_mapping[original]
        model_value = context.input_model_element_to_canonical_model_element.get(
            id(model_value), model_value
        )
        context.layout_model_mapping.add_mapping(placed, model_value)

add_modulation_mapping

add_modulation_mapping(mapping_builder: Any, arc: Any, source_layout: Any, target_layout: Any, modulation: Any)

Map the arc, together with its two endpoint clusters, to modulation.

An endpoint that is already part of a cluster (a species drawn as several glyphs) contributes that whole cluster, so the mapping stays consistent with the one the endpoint's own layout was registered under.

Source code in src/pd2af/building/celldesigner/layout.py
def add_modulation_mapping(
    mapping_builder: typing.Any,
    arc: typing.Any,
    source_layout: typing.Any,
    target_layout: typing.Any,
    modulation: typing.Any,
):
    """Map the arc, together with its two endpoint clusters, to ``modulation``.

    An endpoint that is already part of a cluster (a species drawn as several
    glyphs) contributes that whole cluster, so the mapping stays consistent with
    the one the endpoint's own layout was registered under.
    """
    source_key = mapping_builder.representative_to_key.get(source_layout)
    source_cluster = (
        source_key if source_key is not None else frozenset([source_layout])
    )
    target_key = mapping_builder.representative_to_key.get(target_layout)
    target_cluster = (
        target_key if target_key is not None else frozenset([target_layout])
    )
    mapping_builder.add_mapping(
        frozenset([arc]) | source_cluster | target_cluster,
        modulation,
        representative=arc,
    )

clone_layout_pruning_foreground

clone_layout_pruning_foreground(input_layout_element: Any, foreground_ids: set[int], object_to_builder: dict) -> Any

Clone an input layout element as a builder, to serve as dimmed background.

Any descendant subtree whose input original is in foreground_ids (already drawn in the foreground) is omitted, and None is returned when the element itself belongs to the foreground.

Clones share object_to_builder so structure shared across the input layout stays shared once cloned, and they are builders -- not the input frozen objects -- so dimming the background never mutates the input map's layout.

Source code in src/pd2af/building/celldesigner/layout.py
def clone_layout_pruning_foreground(
    input_layout_element: typing.Any, foreground_ids: set[int], object_to_builder: dict
) -> typing.Any:
    """Clone an input layout element as a builder, to serve as dimmed background.

    Any descendant subtree whose input original is in ``foreground_ids``
    (already drawn in the foreground) is omitted, and ``None`` is returned when
    the element itself belongs to the foreground.

    Clones share ``object_to_builder`` so structure shared across the input
    layout stays shared once cloned, and they are builders -- not the input
    frozen objects -- so dimming the background never mutates the input
    map's layout.
    """
    if id(input_layout_element) in foreground_ids:
        return None
    clone = momapy.builder.builder_from_object(
        input_layout_element, object_to_builder=object_to_builder
    )
    _prune_foreground_from_clone(
        input_layout_element, clone, foreground_ids, object_to_builder
    )
    return clone

make_and_add_layout

make_and_add_layout(context: BuilderContext)

Build context.layout and context.layout_model_mapping (pass 2).

Source code in src/pd2af/building/celldesigner/layout.py
def make_and_add_layout(context: pd2af.building.context.BuilderContext):
    """Build ``context.layout`` and ``context.layout_model_mapping`` (pass 2)."""
    context.layout, context.layout_model_mapping = new_layout_and_mapping_builders()

    for compartment in pd2af.building.celldesigner.model.compartments_outermost_first(
        context.model.compartments
    ):
        _make_and_add_compartment_layout(context, compartment)
    # Only compartments have been added so far, so this counts exactly the
    # foreground compartment layouts -- the insertion point for the overlay
    # background below.
    compartment_count = len(context.layout.layout_elements)
    for _key_class, species, input_species in context.activity_emissions:
        _make_and_add_species_layout(context, species, input_species)
    # Gates after species (their logic arcs target species layouts) and before
    # modulations (a gate-sourced modulation resolves its source through the
    # gate layout registered here).
    for gate, input_gate in context.operator_emissions:
        _make_and_add_gate_layout(context, gate, input_gate)
    for modulation in context.model.modulations:
        _make_and_add_modulation_layout(context, modulation)

    # Overlay = the plain foreground built above + the input map's remaining
    # glyphs cloned in as dimmed, unmapped background. The background carries
    # PD context for rendering only; being unmapped, the model-driven writer
    # drops it, so overlay round-trips identically to plain.
    if context.layout_mode == "overlay":
        foreground = list(context.layout.layout_elements)
        _add_dimmed_background(context, foreground, compartment_count)
        context.layout = pd2af.building.layout.highlight_layout_elements(
            foreground, context.layout
        )
    pd2af.building.layout.harmonize_root_layout(context.layout)

make_decoration_stripped_layout

make_decoration_stripped_layout(input_layout: Any, original_to_stripped: dict[int, Any] | None) -> Any

Return a frozen copy of input_layout stripped of its decoration glyphs.

Every PTM-decoration and active-border sub-glyph is dropped, at any depth, and id(original) -> stripped recorded for every kept element.

The merged reading (keep_species off) strips the model's species of their decorations; this drops the matching layout glyphs (ModificationLayout / StructuralStateLayout and the *ActiveLayout active-border sibling -- see _STRIPPABLE_SPECIES_DECORATION_CLASSES) so a stripped species renders plain. Subunit glyphs of kept complexes are preserved (and recursively stripped), so the real glyph size and subunit structure -- which the dot layout deliberately keeps -- survive.

Frozen objects throughout (via :func:dataclasses.replace), never builders, so the result pickles and round-trips like the reused input glyphs of the non-stripping path; a subtree that needs no change is returned unchanged (shared with the input). original_to_stripped lets the caller map the surviving glyphs back to their model elements (:func:add_mappings_for_layout_and_descendants).

Source code in src/pd2af/building/celldesigner/layout.py
def make_decoration_stripped_layout(
    input_layout: typing.Any, original_to_stripped: dict[int, typing.Any] | None
) -> typing.Any:
    """Return a frozen copy of ``input_layout`` stripped of its decoration glyphs.

    Every PTM-decoration and active-border sub-glyph is dropped, at any depth,
    and ``id(original) -> stripped`` recorded for every kept element.

    The merged reading (`keep_species` off) strips the model's species of their
    decorations; this drops the matching layout glyphs
    (``ModificationLayout`` / ``StructuralStateLayout`` and the ``*ActiveLayout``
    active-border sibling -- see ``_STRIPPABLE_SPECIES_DECORATION_CLASSES``) so a
    stripped species renders plain. Subunit glyphs of kept complexes are
    preserved (and recursively stripped), so the real glyph size and subunit
    structure -- which the dot layout deliberately keeps -- survive.

    Frozen objects throughout (via :func:`dataclasses.replace`), never builders,
    so the result pickles and round-trips like the reused input glyphs of the
    non-stripping path; a subtree that needs no change is returned unchanged
    (shared with the input). ``original_to_stripped`` lets the caller map the
    surviving glyphs back to their model elements
    (:func:`add_mappings_for_layout_and_descendants`).
    """
    children = getattr(input_layout, "layout_elements", ()) or ()
    stripped_children = []
    changed = False
    for child in children:
        if isinstance(child, _STRIPPABLE_SPECIES_DECORATION_CLASSES):
            changed = True
            continue
        stripped_child = make_decoration_stripped_layout(child, original_to_stripped)
        if stripped_child is not child:
            changed = True
        stripped_children.append(stripped_child)
    if changed:
        stripped = dataclasses.replace(
            input_layout, layout_elements=tuple(stripped_children)
        )
    else:
        stripped = input_layout
    original_to_stripped[id(input_layout)] = stripped
    return stripped

make_logic_arc

make_logic_arc(context: BuilderContext, gate_layout: Any, input_layout: Any) -> Any

Build a LogicArcLayout from a gate to one of its input species.

The CellDesigner writer locates a gate's inputs by scanning for logic arcs whose source is the gate layout, so the arc runs gate -> input species (the CellDesigner convention), not input -> gate.

Source code in src/pd2af/building/celldesigner/layout.py
def make_logic_arc(
    context: pd2af.building.context.BuilderContext,
    gate_layout: typing.Any,
    input_layout: typing.Any,
) -> typing.Any:
    """Build a ``LogicArcLayout`` from a gate to one of its input species.

    The CellDesigner writer locates a gate's inputs by scanning for logic arcs
    whose ``source`` is the gate layout, so the arc runs gate -> input species
    (the CellDesigner convention), not input -> gate.
    """
    if context.layout_mode == "dot":
        segments = pd2af.building.layout.PLACEHOLDER_ARC_SEGMENTS
    else:
        segments = pd2af.building.layout.make_arc_segments_from_source_and_target(
            gate_layout, input_layout
        )
    return momapy.celldesigner.LogicArcLayout(
        source=gate_layout,
        target=input_layout,
        segments=tuple(segments),
    )

make_modulation_arc

make_modulation_arc(context: BuilderContext, modulation: Any, source_layout: Any, target_layout: Any) -> Any

Build the arc layout for modulation between two node layouts.

In the dot mode the segments are placeholders that graphviz overwrites; otherwise they are computed from the two layouts' real geometry.

Source code in src/pd2af/building/celldesigner/layout.py
def make_modulation_arc(
    context: pd2af.building.context.BuilderContext,
    modulation: typing.Any,
    source_layout: typing.Any,
    target_layout: typing.Any,
) -> typing.Any:
    """Build the arc layout for ``modulation`` between two node layouts.

    In the ``dot`` mode the segments are placeholders that graphviz overwrites;
    otherwise they are computed from the two layouts' real geometry.
    """
    arc_class = _MODULATION_CLASS_TO_LAYOUT_CLASS[type(modulation)]
    if context.layout_mode == "dot":
        segments = pd2af.building.layout.PLACEHOLDER_ARC_SEGMENTS
    else:
        segments = pd2af.building.layout.make_arc_segments_from_source_and_target(
            source_layout, target_layout
        )
    return arc_class(
        source=source_layout,
        target=target_layout,
        segments=tuple(segments),
    )

make_synthetic_gate_layout

make_synthetic_gate_layout(gate: Any, index: int) -> Any

Build a placeholder gate node for gate.

Used by the dot mode when the input gate has no curated layout. index seeds the position so two content-equal synthetic gates stay content-distinct; make_auto_layout repositions every node before render.

Source code in src/pd2af/building/celldesigner/layout.py
def make_synthetic_gate_layout(gate: typing.Any, index: int) -> typing.Any:
    """Build a placeholder gate node for ``gate``.

    Used by the ``dot`` mode when the input gate has no curated layout.
    ``index`` seeds the position so two content-equal synthetic gates stay
    content-distinct; ``make_auto_layout`` repositions every node before render.
    """
    layout_class = _GATE_CLASS_TO_LAYOUT_CLASS.get(type(gate))
    if layout_class is None:
        raise ValueError(
            f"no default layout class registered for gate type {type(gate).__name__}"
        )
    position = momapy.geometry.Point(float(index), 0.0)
    return layout_class(position=position)

make_synthetic_layout

make_synthetic_layout(species: Any, index: int) -> Any

Build a placeholder Node for species.

index seeds the position so two synthetic layouts for content-equal species (same class, same name) are themselves content-distinct. Layout dataclasses have compare=False on id_, so identical content collapses to one mapping key — and the layout-model mapping then drops every synthetic but the first. The position itself is throwaway: make_auto_layout repositions every node before render.

Source code in src/pd2af/building/celldesigner/layout.py
def make_synthetic_layout(species: typing.Any, index: int) -> typing.Any:
    """Build a placeholder Node for ``species``.

    ``index`` seeds the position so two synthetic layouts for
    content-equal species (same class, same name) are themselves
    content-distinct. Layout dataclasses have ``compare=False`` on
    ``id_``, so identical content collapses to one mapping key — and
    the layout-model mapping then drops every synthetic but the first.
    The position itself is throwaway: ``make_auto_layout`` repositions
    every node before render.
    """
    layout_class = _SPECIES_CLASS_TO_LAYOUT_CLASS.get(type(species))
    if layout_class is None:
        raise ValueError(
            f"no default layout class registered for species type "
            f"{type(species).__name__}"
        )
    position = momapy.geometry.Point(float(index), 0.0)
    label = momapy.core.layout.TextLayout(
        text=getattr(species, "name", "") or "",
        position=position,
    )
    return layout_class(position=position, label=label)

new_layout_and_mapping_builders

new_layout_and_mapping_builders() -> Any

A fresh, empty (layout builder, layout-model mapping builder) pair.

Source code in src/pd2af/building/celldesigner/layout.py
def new_layout_and_mapping_builders() -> typing.Any:
    """A fresh, empty (layout builder, layout-model mapping builder) pair."""
    layout_builder_class = momapy.builder.get_or_make_builder_cls(
        momapy.core.layout.Layout
    )
    return (
        layout_builder_class(),
        momapy.core.mapping.LayoutModelMappingBuilder(),
    )