Skip to content

compas_timber.model ¤

Classes¤

TimberModel ¤

TimberModel(tolerance=None, **kwargs)

Represents a timber model containing different elements such as panels, beams and joints.

The timber model allows expressing the hierarchy and interactions between the different elements it contains.

Attributes:

  • beams (list[:class:`~compas_timber.elements.Beam`]) –

    A list of all beams assigned to this model.

  • plates (list[:class:`~compas_timber.elements.Plate`]) –

    A list of all plates assigned to this model.

  • joints (Iterable[:class:`~compas_timber.connections.Joint`]) –

    A view of all actual joints assigned to this model.

  • joint_candidates (set[:class:`~compas_timber.connections.JointCandidate`]) –

    A set of all joint candidates in the model.

  • unpromoted_joint_candidates (set[:class:`~compas_timber.connections.JointCandidate`]) –

    A set of all joint candidates in the model which have not been promoted to joints.

  • panels (list[:class:`~compas_timber.elements.Panel`]) –

    A list of all panels assigned to this model.

  • layers (list[:class:`~compas_timber.elements.Layer`]) –

    A list of all layers assigned to this model.

  • fasteners (list[:class:`~compas_timber.elements.Fastener`]) –

    A list of all fasteners assigned to this model.

  • center_of_mass (:class:`~compas.geometry.Point`) –

    The calculated center of mass of the model.

  • topologies (list(dict)) –

    A list of JointTopology for model. dict is: {"detected_topo": detected_topo, "beam_a_key": beam_a_key, "beam_b_key":beam_b_key} See :class:~compas_timber.connections.JointTopology.

  • tolerance (:class:`~compas.tolerance.Tolerance`) –

    The tolerance configuration used for this model. TOL if none provided.

  • volume (float) –

    The calculated total volume of the model.

Source code in src/compas_timber/model.py
def __init__(self, tolerance=None, **kwargs):
    super(TimberModel, self).__init__()
    self._joints = {}
    self._topologies = []  # added to avoid calculating multiple times
    self._tolerance = tolerance or TOL
    self._graph.update_default_edge_attributes(**self._TIMBER_GRAPH_EDGE_ATTRIBUTES)
    self._graph.update_default_node_attributes(**self._TIMBER_GRAPH_NODE_ATTRIBUTES)

Methods:¤

__getitem__ ¤
__getitem__(guid)

Get an element by its unique identifier.

Parameters:

  • guid (str) –

    The GUID of the element to retrieve.

Returns:

  • class:`~compas_model.elements.Element` –

    The element with the specified GUID.

Source code in src/compas_timber/model.py
def __getitem__(self, guid):
    # type: (str) -> Element
    """Get an element by its unique identifier.

    Parameters
    ----------
    guid : str
        The GUID of the element to retrieve.

    Returns
    -------
    :class:`~compas_model.elements.Element`
        The element with the specified GUID.

    """
    return self._elements[guid]
add_beam_structural_segments ¤
add_beam_structural_segments(beam: Beam, segments: list[StructuralSegment]) -> None

Adds a structural segment to the model node corresponding to the given beam.

Notes

Normally, this method shouldn't be called directy. Use :meth:create_beam_structural_segments when possible to add structural segments.

Parameters:

  • beam (:class:`~compas_timber.elements.Beam`) –

    The beam to which the structural segments belong.

  • segments (list[:class:`~compas_timber.structural.StructuralSegment`]) –

    The structural segments to add.

Source code in src/compas_timber/model.py
def add_beam_structural_segments(self, beam: Beam, segments: List[StructuralSegment]) -> None:
    """Adds a structural segment to the model node corresponding to the given beam.

    Notes
    -----
    Normally, this method shouldn't be called directy. Use :meth:`create_beam_structural_segments` when possible to add structural segments.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam to which the structural segments belong.
    segments : list[:class:`~compas_timber.structural.StructuralSegment`]
        The structural segments to add.
    """
    node = beam.graphnode
    existing_segments = cast(List[StructuralSegment], self._graph.node_attribute(node, "structural_segments")) or []
    existing_segments.extend(segments)
    self._graph.node_attribute(node, "structural_segments", existing_segments)
add_element ¤
add_element(element, parent=None, material=None, keep_world_placement=False)

Add an element to the model.

Parameters:

  • element (:class:`~compas_model.elements.Element`) –

    The element to add.

  • parent (:class:`~compas_model.elements.Element`, default: None ) –

    The parent element. The element's transformation is relative to it.

  • material (:class:`~compas_model.materials.Material`, default: None ) –

    A material already added to the model.

  • keep_world_placement (bool, default: False ) –

    If True, element is taken to be placed in world coordinates and its transformation is converted to be relative to parent (or to the model), so it stays where it is. If False (default), its transformation is used as is, i.e. already relative to parent.

Returns:

  • class:`~compas_model.elements.Element` –
Source code in src/compas_timber/model.py
def add_element(self, element, parent=None, material=None, keep_world_placement=False):
    """Add an element to the model.

    Parameters
    ----------
    element : :class:`~compas_model.elements.Element`
        The element to add.
    parent : :class:`~compas_model.elements.Element`, optional
        The parent element. The element's `transformation` is relative to it.
    material : :class:`~compas_model.materials.Material`, optional
        A material already added to the model.
    keep_world_placement : bool, optional
        If True, `element` is taken to be placed in world coordinates and its `transformation` is converted
        to be relative to `parent` (or to the model), so it stays where it is.
        If False (default), its `transformation` is used as is, i.e. already relative to `parent`.

    Returns
    -------
    :class:`~compas_model.elements.Element`

    """
    if keep_world_placement:
        reference = parent.modeltransformation if parent else self.transformation
        if reference:
            element.transformation = reference.inverted() * (element.transformation or Transformation())
    super().add_element(element, parent=parent, material=material)
    # the element's model transformation now composes with its new ancestors'
    clear_model_cache(element)
    return element
add_joint ¤
add_joint(joint)

Add a joint object to the model.

Parameters:

  • joint (:class:`~compas_timber.connections.joint`) –

    An instance of a Joint class.

Source code in src/compas_timber/model.py
def add_joint(self, joint):
    # type: (Joint) -> None
    """Add a joint object to the model.

    Parameters
    ----------
    joint : :class:`~compas_timber.connections.joint`
        An instance of a Joint class.
    """
    # explicitly remove old joint instances between the same elements.
    # This ensures that the joint being removed clears its features from the elements
    # before the new is added, which will add its own features to the elements.
    # NOTE: in the case where a 2 element joint is added between elements previously in a 3+ element joint, one or more elements will be left unconnected.
    for pair in combinations(joint.elements, 2):
        old = self.get_joint(pair[0], pair[1])
        if old:
            self.remove_joint(old)

    joint_guid = str(joint.guid)
    self._joints[joint_guid] = joint
    self.add_elements(joint.generated_elements)
    for interaction in joint.interactions:
        element_a, element_b = interaction
        edge = self.add_interaction(element_a, element_b)
        self._graph.edge_attribute(edge, "joints", value=joint_guid)
add_joint_candidate ¤
add_joint_candidate(candidate)

Add a joint candidate to the model.

Unlike actual joints (tracked in self._joints), candidates are stored only on the graph edge, under the "candidates" attribute, separate from actual joints which are stored under the "joints" attribute. Since a candidate always connects exactly two elements, this is enough to look it up again without a separate registry.

Parameters:

  • candidate (:class:`~compas_timber.connections.JointCandidate`) –

    An instance of a JointCandidate class.

Source code in src/compas_timber/model.py
def add_joint_candidate(self, candidate):
    # type: (JointCandidate) -> None
    """Add a joint candidate to the model.

    Unlike actual joints (tracked in `self._joints`), candidates are stored only on the graph edge, under
    the "candidates" attribute, separate from actual joints which are stored under the "joints" attribute.
    Since a candidate always connects exactly two elements, this is enough to look it up again without a
    separate registry.

    Parameters
    ----------
    candidate : :class:`~compas_timber.connections.JointCandidate`
        An instance of a JointCandidate class.
    """
    for interaction in candidate.interactions:
        element_a, element_b = interaction
        edge = (element_a.graphnode, element_b.graphnode)
        if edge not in self._graph.edges():
            self._graph.add_edge(*edge)

        # this is how joints and candidates co-exist on the same edge, they are stored under different attributes
        # (``joints`` vs. ``candidates``)
        self._graph.edge_attribute(edge, "candidates", candidate)
add_structural_connector_segments ¤
add_structural_connector_segments(
    element_a: Element, element_b: Element, segments: list[StructuralSegment]
) -> None

Adds structural segments to the interaction (edge) between two elements.

Notes

Normally, this method shouldn't be called directy. Use :meth:create_beam_structural_segments when possible to add structural segments.

Parameters:

  • element_a (:class:`~compas_timber.elements.TimberElement`) –

    The first element.

  • element_b (:class:`~compas_timber.elements.TimberElement`) –

    The second element.

  • segments (list[:class:`~compas_timber.structural.StructuralSegment`]) –

    The structural segments to add.

Source code in src/compas_timber/model.py
def add_structural_connector_segments(self, element_a: Element, element_b: Element, segments: List[StructuralSegment]) -> None:
    """Adds structural segments to the interaction (edge) between two elements.

    Notes
    -----
    Normally, this method shouldn't be called directy. Use :meth:`create_beam_structural_segments` when possible to add structural segments.

    Parameters
    ----------
    element_a : :class:`~compas_timber.elements.TimberElement`
        The first element.
    element_b : :class:`~compas_timber.elements.TimberElement`
        The second element.
    segments : list[:class:`~compas_timber.structural.StructuralSegment`]
        The structural segments to add.
    """
    edge = (element_a.graphnode, element_b.graphnode)
    if edge not in self._graph.edges():
        edge = (element_b.graphnode, element_a.graphnode)
        if edge not in self._graph.edges():
            raise ValueError("Interaction between elements {} and {} does not exist in the model.".format(element_a.name, element_b.name))

    existing_segments = self._graph.edge_attribute(edge, "structural_segments") or []
    existing_segments.extend(segments)
    self._graph.edge_attribute(edge, "structural_segments", existing_segments)
compute_topologies ¤
compute_topologies(elements=None, max_distance=None)

Detects adjacent elements and creates joint candidates for them.

Dispatches each adjacent pair to a handler based on the pair's element types (beam-beam, plate-plate, and panel-panel are supported today). Additional type combinations can be supported by registering a handler in _CONNECTION_HANDLERS; unregistered combinations are silently skipped.

Clears all existing joint candidates before recomputing, regardless of the element types involved. Concrete (already promoted) joints are left untouched.

Parameters:

  • elements (list[:class:`~compas_model.elements.Element`], default: None ) –

    The elements to connect. If not provided, defaults to all beams, plates, and panels in the model.

  • max_distance (float, default: None ) –

    The maximum distance between elements to consider them adjacent. Defaults to TOL.absolute.

Source code in src/compas_timber/model.py
def compute_topologies(self, elements=None, max_distance=None):
    """Detects adjacent elements and creates joint candidates for them.

    Dispatches each adjacent pair to a handler based on the pair's element types (beam-beam,
    plate-plate, and panel-panel are supported today). Additional type combinations can be
    supported by registering a handler in `_CONNECTION_HANDLERS`; unregistered combinations are
    silently skipped.

    Clears all existing joint candidates before recomputing, regardless of the element types
    involved. Concrete (already promoted) joints are left untouched.

    Parameters
    ----------
    elements : list[:class:`~compas_model.elements.Element`], optional
        The elements to connect. If not provided, defaults to all beams, plates, and panels in the model.
    max_distance : float, optional
        The maximum distance between elements to consider them adjacent. Defaults to `TOL.absolute`.

    """

    if elements is None:
        to_remove = list(self.joint_candidates)
    else:
        # use `all` here so that e.g. a beam-plate candidate is not removed when only beams are passed in.
        to_remove = [candidate for candidate in self.joint_candidates if all(e in elements for e in candidate.elements)]

    for candidate in to_remove:
        self.remove_joint_candidate(candidate)

    elements = list(elements) if elements is not None else list(self.beams) + list(self.plates) + list(self.panels)

    max_distance = max_distance or TOL.absolute
    pairs = ConnectionSolver.find_intersecting_pairs(elements, rtree=True, max_distance=max_distance)
    for pair in pairs:
        element_a, element_b = tuple(pair)
        candidate = get_connection_candidate(element_a, element_b, max_distance)
        if candidate is not None:
            self.add_joint_candidate(candidate)
connect_adjacent_beams ¤
connect_adjacent_beams(max_distance=None)

Connects adjacent beams in the model.

Source code in src/compas_timber/model.py
def connect_adjacent_beams(self, max_distance=None):
    """Connects adjacent beams in the model."""
    self.compute_topologies(self.beams, max_distance)
connect_adjacent_panels ¤
connect_adjacent_panels(max_distance=None)

Connects adjacent panels in the model.

Parameters:

  • max_distance (float, default: None ) –

    The maximum distance between panels to consider them adjacent. Default is 0.0.

Source code in src/compas_timber/model.py
def connect_adjacent_panels(self, max_distance=None):
    """Connects adjacent panels in the model.

    Parameters
    ----------
    max_distance : float, optional
        The maximum distance between panels to consider them adjacent. Default is 0.0.
    """
    self.compute_topologies(self.panels, max_distance)
connect_adjacent_plates ¤
connect_adjacent_plates(max_distance=None)

Connects adjacent plates in the model.

Parameters:

  • max_distance (float, default: None ) –

    The maximum distance between plates to consider them adjacent. Default is 0.0.

Source code in src/compas_timber/model.py
def connect_adjacent_plates(self, max_distance=None):
    """Connects adjacent plates in the model.

    Parameters
    ----------
    max_distance : float, optional
        The maximum distance between plates to consider them adjacent. Default is 0.0.
    """
    self.compute_topologies(self.plates, max_distance)
create_beam_structural_segments ¤
create_beam_structural_segments(solver=None) -> None

Creates structural segments for all beams in the model based on their joints.

Parameters:

  • solver (:class:`~compas_timber.structural.BeamStructuralElementSolver`, default: None ) –

    The solver to use for creating structural segments. If not provided, a default solver is created.

Source code in src/compas_timber/model.py
def create_beam_structural_segments(self, solver=None) -> None:
    """Creates structural segments for all beams in the model based on their joints.

    Parameters
    ----------
    solver : :class:`~compas_timber.structural.BeamStructuralElementSolver`, optional
        The solver to use for creating structural segments.
        If not provided, a default solver is created.

    """
    if not self.beams:
        raise ValueError("No beams in the model to create structural segments for.")

    for beam in self.beams:
        self.remove_beam_structural_segments(beam)

    for edge in self._graph.edges():
        self._graph.unset_edge_attribute(edge, "structural_segments")

    solver = solver or BeamStructuralElementSolver()
    _, joints_traversed = solver.add_structural_segments(model=self)
    solver.add_joint_structural_segments(model=self, joints=joints_traversed)
element_by_guid ¤
element_by_guid(guid)

DEPRECATED Get an element by its unique identifier.

Parameters:

  • guid (str) –

    The GUID of the element to retrieve.

Returns:

  • class:`~compas_model.elements.Element` –

    The element with the specified GUID.

Source code in src/compas_timber/model.py
def element_by_guid(self, guid):
    # type: (str) -> Element
    """DEPRECATED
    Get an element by its unique identifier.

    Parameters
    ----------
    guid : str
        The GUID of the element to retrieve.

    Returns
    -------
    :class:`~compas_model.elements.Element`
        The element with the specified GUID.

    """
    warnings.warn("element_by_guid() is deprecated;use get_element() for optional access or TimberModel[guid] for strict access.", DeprecationWarning, stacklevel=2)
    return self[guid]
extract_model_from_parent ¤
extract_model_from_parent(parent)

Detach parent's child subtree into a new, standalone :class:TimberModel.

The parent element itself stays in its current model; its descendants are removed from that model and re-rooted (hierarchy preserved) in a fresh :class:TimberModel, which is returned.

A child element detached from its parent reports its geometry in the parent's local frame — convenient for operating on, e.g., a panel's layers in isolation, then merging them back with :func:merge_model_into_model.

Parameters:

  • parent (:class:`~compas_model.elements.Element`) –

    The element whose children are extracted. Must be in a model.

Returns:

  • class:`TimberModel` –

    A new model containing parent's former subtree.

Source code in src/compas_timber/model.py
def extract_model_from_parent(self, parent):
    # type: (Element) -> TimberModel
    """Detach *parent*'s child subtree into a new, standalone :class:`TimberModel`.

    The *parent* element itself stays in its current model; its descendants are
    removed from that model and re-rooted (hierarchy preserved) in a fresh
    :class:`TimberModel`, which is returned.

    A child element detached from its parent reports its geometry in the
    parent's local frame — convenient for operating on, e.g., a panel's layers
    in isolation, then merging them back with :func:`merge_model_into_model`.

    Parameters
    ----------
    parent : :class:`~compas_model.elements.Element`
        The element whose children are extracted.  Must be in a model.

    Returns
    -------
    :class:`TimberModel`
        A new model containing *parent*'s former subtree.
    """
    tuples, joints = self._detach_subtree(parent)
    new_model = TimberModel()
    new_model._attach_subtree(tuples, joints=joints)
    return new_model
get_beam_structural_segments ¤
get_beam_structural_segments(beam: Beam) -> list[StructuralSegment]

Gets the structural segments assigned to the given beam.

Parameters:

  • beam (:class:`~compas_timber.elements.Beam`) –

    The beam whose structural segments to retrieve.

Returns:

  • list[:class:`~compas_timber.structural.StructuralSegment`] –

    The structural segments assigned to the beam.

Source code in src/compas_timber/model.py
def get_beam_structural_segments(self, beam: Beam) -> List[StructuralSegment]:
    """Gets the structural segments assigned to the given beam.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam whose structural segments to retrieve.

    Returns
    -------
    list[:class:`~compas_timber.structural.StructuralSegment`]
        The structural segments assigned to the beam.
    """
    segments = cast(List[StructuralSegment], self._graph.node_attribute(beam.graphnode, "structural_segments"))
    return segments or []
get_candidate ¤
get_candidate(element_a, element_b)

Get the joint candidate that joins two given elements, if any.

Parameters:

  • element_a (:class:`~compas_model.elements.Element`) –

    The first element.

  • element_b (:class:`~compas_model.elements.Element`) –

    The second element.

Returns:

  • class:`~compas_timber.connections.JointCandidate` or None –

    The joint candidate connecting the two elements, or None if they are not connected by one.

Source code in src/compas_timber/model.py
def get_candidate(self, element_a, element_b):
    # type: (Element, Element) -> JointCandidate | None
    """Get the joint candidate that joins two given elements, if any.

    Parameters
    ----------
    element_a : :class:`~compas_model.elements.Element`
        The first element.
    element_b : :class:`~compas_model.elements.Element`
        The second element.

    Returns
    -------
    :class:`~compas_timber.connections.JointCandidate` or None
        The joint candidate connecting the two elements, or None if they are not connected by one.
    """
    candidates = self._safely_get_edge_attribute((element_a.graphnode, element_b.graphnode), "candidates")
    return candidates[0] if candidates else None
get_candidates_for_element ¤
get_candidates_for_element(element) -> list[JointCandidate]

Get all joint candidates for a given element.

Parameters:

  • element (:class:`~compas_model.elements.Element`) –

    The element to query.

Returns:

  • list[:class:`~compas_timber.connections.JointCandidate`] –

    A list of joint candidates for the given element.

Source code in src/compas_timber/model.py
def get_candidates_for_element(self, element) -> List[JointCandidate]:
    """Get all joint candidates for a given element.

    Parameters
    ----------
    element : :class:`~compas_model.elements.Element`
        The element to query.

    Returns
    -------
    list[:class:`~compas_timber.connections.JointCandidate`]
        A list of joint candidates for the given element.
    """
    neighbors = self._graph.neighbors(element.graphnode)
    result = []
    for nbr in neighbors:
        candidates = self._safely_get_edge_attribute((element.graphnode, nbr), "candidates")
        result.extend(candidates)
    return result
get_element ¤
get_element(guid)

Get an element by its unique identifier.

Parameters:

  • guid (str) –

    The GUID of the element to retrieve.

Returns:

  • class:`~compas_model.elements.Element` or None –

    The element with the specified GUID. None if an element with this GUID is not in the Model.

Source code in src/compas_timber/model.py
def get_element(self, guid):
    # type: (str) -> Element | None
    """Get an element by its unique identifier.

    Parameters
    ----------
    guid : str
        The GUID of the element to retrieve.

    Returns
    -------
    :class:`~compas_model.elements.Element` or None
        The element with the specified GUID.
        None if an element with this GUID is not in the Model.

    """
    return self._elements.get(guid)
get_elements_in_group ¤
get_elements_in_group(group_element, filter_=None)

Get all elements in a group element.

Todo

upstream this to compas_model

Parameters:

  • group_element (:class:`~compas_timber.elements.TimberElement`) –

    The group element to query.

  • filter_ (callable, default: None ) –

    A filter function to apply to the elements.

Returns:

  • Generator[:class:`~compas_timber.elements.TimberElement`] –

    A generator of elements in the group.

Source code in src/compas_timber/model.py
def get_elements_in_group(self, group_element, filter_=None):
    """Get all elements in a group element.

    TODO: upstream this to compas_model

    Parameters
    ----------
    group_element : :class:`~compas_timber.elements.TimberElement`
        The group element to query.
    filter_ : callable, optional
        A filter function to apply to the elements.

    Returns
    -------
    Generator[:class:`~compas_timber.elements.TimberElement`]
        A generator of elements in the group.

    """
    # type: (TimberElement, callable | None) -> Generator[TimberElement, None, None]
    if not self.has_group(group_element):
        raise ValueError("Group {} not found in model.".format(group_element.name))

    filter_ = filter_ or (lambda _: True)
    elements = group_element.children
    return filter(filter_, elements)
get_interactions_for_element ¤
get_interactions_for_element(element)

Get all interactions (joints and candidates) for a given element.

Parameters:

  • element (:class:`~compas_model.elements.Element`) –

    The element to query.

Returns:

  • list[:class:`~compas_model.interactions.Interaction`] –

    A list of interactions for the given element.

Source code in src/compas_timber/model.py
def get_interactions_for_element(self, element):
    # type: (Element) -> List[Interaction]
    """Get all interactions (joints and candidates) for a given element.

    Parameters
    ----------
    element : :class:`~compas_model.elements.Element`
        The element to query.

    Returns
    -------
    list[:class:`~compas_model.interactions.Interaction`]
        A list of interactions for the given element.
    """
    result = self.get_joints_for_element(element)
    result.extend(self.get_candidates_for_element(element))
    return result
get_joint ¤
get_joint(element_a, element_b)

Get the joint instance that joins two given elements, if any.

Parameters:

  • element_a (:class:`~compas_model.elements.Element`) –

    The first element.

  • element_b (:class:`~compas_model.elements.Element`) –

    The second element.

Returns:

  • class:`~compas_timber.connections.Joint` or None –

    The joint connecting the two elements, or None if they are not joined.

Source code in src/compas_timber/model.py
def get_joint(self, element_a, element_b):
    # type: (Element, Element) -> Joint | None
    """Get the joint instance that joins two given elements, if any.

    Parameters
    ----------
    element_a : :class:`~compas_model.elements.Element`
        The first element.
    element_b : :class:`~compas_model.elements.Element`
        The second element.

    Returns
    -------
    :class:`~compas_timber.connections.Joint` or None
        The joint connecting the two elements, or None if they are not joined.
    """
    joint_guids = self._safely_get_edge_attribute((element_a.graphnode, element_b.graphnode), "joints")
    for guid in joint_guids:
        if guid in self._joints:
            return self._joints[guid]
    return None
get_joints_for_element ¤
get_joints_for_element(element) -> list[Joint]

Get all joints for a given element.

Parameters:

  • element (:class:`~compas_model.elements.Element`) –

    The element to query.

Returns:

  • list[:class:`~compas_timber.connections.Joint`] –

    A list of joints for the given element.

Source code in src/compas_timber/model.py
def get_joints_for_element(self, element) -> List[Joint]:
    """Get all joints for a given element.

    Parameters
    ----------
    element : :class:`~compas_model.elements.Element`
        The element to query.

    Returns
    -------
    list[:class:`~compas_timber.connections.Joint`]
        A list of joints for the given element.
    """
    neighbors = self._graph.neighbors(element.graphnode)
    result = []
    for nbr in neighbors:
        joint_guids = self._safely_get_edge_attribute((element.graphnode, nbr), "joints")
        result.extend([self._joints[guid] for guid in joint_guids if guid in self._joints])
    return result
get_structural_connector_segments ¤
get_structural_connector_segments(
    element_a: Element, element_b: Element
) -> list[StructuralSegment]

Gets the structural segments assigned to the interaction between two elements.

Parameters:

  • element_a (:class:`~compas_timber.elements.TimberElement`) –

    The first element.

  • element_b (:class:`~compas_timber.elements.TimberElement`) –

    The second element.

Returns:

  • list[:class:`~compas_timber.structural.StructuralSegment`] –

    The structural segments assigned to the interaction.

Source code in src/compas_timber/model.py
def get_structural_connector_segments(self, element_a: Element, element_b: Element) -> List[StructuralSegment]:
    """Gets the structural segments assigned to the interaction between two elements.

    Parameters
    ----------
    element_a : :class:`~compas_timber.elements.TimberElement`
        The first element.
    element_b : :class:`~compas_timber.elements.TimberElement`
        The second element.

    Returns
    -------
    list[:class:`~compas_timber.structural.StructuralSegment`]
        The structural segments assigned to the interaction.
    """
    edge = (element_a.graphnode, element_b.graphnode)
    if edge not in self._graph.edges():
        edge = (element_b.graphnode, element_a.graphnode)
        if edge not in self._graph.edges():
            return []

    return self._graph.edge_attribute(edge, "structural_segments") or []
has_group ¤
has_group(group_element)

Check if a group with group_element exists in the model.

Parameters:

  • group_element (class:`~compas_timber.elements.TimberElement`) –

    The group element to check for existence.

Returns:

  • bool –

    True if the group element exists in the model.

Source code in src/compas_timber/model.py
def has_group(self, group_element):
    # type: (TimberElement) -> bool
    """Check if a group with `group_element` exists in the model.

    Parameters
    ----------
    group_element : class:`~compas_timber.elements.TimberElement`
        The group element to check for existence.

    Returns
    -------
    bool
        True if the group element exists in the model.
    """
    return self.has_element(group_element)
merge_model ¤
merge_model(model, parent=None)

Move every element (and joints) of model into target_model under parent.

All of model's top-level elements and their subtrees are detached and re-added beneath parent in target_model (or under the root when parent is None), preserving the hierarchy and resetting each moved element's computed properties. Joints defined on model are copied across.

Parameters:

  • model (:class:`TimberModel`) –

    The source model whose contents are moved out.

  • target_model (:class:`TimberModel`) –

    The model to merge model's elements into.

  • parent (:class:`~compas_model.elements.Element`, default: None ) –

    The element under which the moved elements are re-rooted. None attaches them under the target model's root.

Source code in src/compas_timber/model.py
def merge_model(self, model, parent=None):
    # type: (TimberModel, TimberModel, Element | None) -> None
    """Move every element (and joints) of *model* into *target_model* under *parent*.

    All of *model*'s top-level elements and their subtrees are detached and
    re-added beneath *parent* in *target_model* (or under the root when *parent*
    is ``None``), preserving the hierarchy and resetting each moved element's
    computed properties.  Joints defined on *model* are copied across.

    Parameters
    ----------
    model : :class:`TimberModel`
        The source model whose contents are moved out.
    target_model : :class:`TimberModel`
        The model to merge *model*'s elements into.
    parent : :class:`~compas_model.elements.Element`, optional
        The element under which the moved elements are re-rooted.  ``None``
        attaches them under the target model's root.
    """
    tuples, joints = model._detach_subtree()
    self._attach_subtree(tuples, joints=joints, parent=parent)
process_joinery ¤
process_joinery(joints_to_process=None, stop_on_first_error=False)

Process the joinery of the model. This methods checks the feasibility of the joints and instructs all joints to add their extensions and features.

The sequence is important here since the feature parameters must be calculated based on the extended blanks. For this reason, the first iteration will only extend the beams, and the second iteration will add the features.

Parameters:

  • stop_on_first_error (bool, default: False ) –

    If True, the method will raise an exception on the first error it encounters. Default is False.

Returns:

  • list[:class:`~compas_timber.errors.BeamJoiningError`] –

    A list of errors that occurred during the joinery process.

Source code in src/compas_timber/model.py
def process_joinery(self, joints_to_process=None, stop_on_first_error=False):
    """Process the joinery of the model. This methods checks the feasibility of the joints and instructs all joints to add their extensions and features.

    The sequence is important here since the feature parameters must be calculated based on the extended blanks.
    For this reason, the first iteration will only extend the beams, and the second iteration will add the features.

    Parameters
    ----------
    stop_on_first_error : bool, optional
        If True, the method will raise an exception on the first error it encounters. Default is False.

    Returns
    -------
    list[:class:`~compas_timber.errors.BeamJoiningError`]
        A list of errors that occurred during the joinery process.

    """
    errors = []
    joints = joints_to_process if joints_to_process is not None else self.joints

    for joint in joints:
        joint.clear_extensions()
        joint.clear_features()

    for joint in joints:
        try:
            joint.check_elements_compatibility(joint.elements)  # TODO: is this necessary here? This should be done at joint creation.
            joint.add_extensions()
        except BeamJoiningError as bje:
            errors.append(bje)
            if stop_on_first_error:
                raise bje

    for joint in joints:
        try:
            joint.add_features()
        except BeamJoiningError as bje:
            errors.append(bje)
            if stop_on_first_error:
                raise bje
        # TODO: should we be handling the BTLxProcessing application errors differently?
        # TODO: Maybe a ProcessingApplicationError raised for the processing(s) that failed when adding the features to the elements?
        # TODO: This would allow us to catch the processing that failed with the necessary info, while applying the rest of the required by the joint processings that were sucessfull.  # noqa: E501
        except ValueError as ve:
            bje = BeamJoiningError(joint.elements, joint, debug_info=str(ve))
            errors.append(bje)
            if stop_on_first_error:
                raise bje
    return errors
remove_beam_structural_segments ¤
remove_beam_structural_segments(beam: Beam) -> None

Removes all structural segments assigned to the given beam.

Parameters:

  • beam (:class:`~compas_timber.elements.Beam`) –

    The beam whose structural segments to remove.

Source code in src/compas_timber/model.py
def remove_beam_structural_segments(self, beam: Beam) -> None:
    """Removes all structural segments assigned to the given beam.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam whose structural segments to remove.
    """
    self._graph.unset_node_attribute(beam.graphnode, "structural_segments")
remove_element_subtree ¤
remove_element_subtree(element)

Remove all children (and their descendants) of element from the model.

element itself is kept in the model as a childless leaf. Any joints whose both elements are among the removed descendants are also removed; joints that span a removed element and an element outside the subtree are removed as well.

Parameters:

  • element (:class:`~compas_model.elements.Element`) –

    The element whose descendants are removed. Must already be in the model.

Source code in src/compas_timber/model.py
def remove_element_subtree(self, element):
    """Remove all children (and their descendants) of *element* from the model.

    *element* itself is kept in the model as a childless leaf.  Any joints
    whose both elements are among the removed descendants are also removed;
    joints that span a removed element and an element outside the subtree are
    removed as well.

    Parameters
    ----------
    element : :class:`~compas_model.elements.Element`
        The element whose descendants are removed.  Must already be in the model.
    """
    _, _ = self._detach_subtree(element)
remove_interaction ¤
remove_interaction(a, b, _=None)

Remove the interaction between two elements.

Extends :meth:Model.remove_interaction to not remove the edge if there are still other timber related attribute on the same edge.

Parameters:

  • a (:class:`TimberElement`) –
  • b (:class:`TimberElement`) –

Returns:

  • None –
Source code in src/compas_timber/model.py
def remove_interaction(self, a, b, _=None):
    """Remove the interaction between two elements.

    Extends :meth:`Model.remove_interaction` to not remove the edge if there are still other timber related attribute on the same edge.

    Parameters
    ----------
    a : :class:`TimberElement`
    b : :class:`TimberElement`

    Returns
    -------
    None

    """
    edge = (a.graphnode, b.graphnode)
    if edge not in self._graph.edges():
        return

    self._graph.unset_edge_attribute(edge, "joints")

    if not self._is_remaining_attrs_on_edge(edge):
        # if there's no other timber related attributes on that edge, then remove the edge as well
        super().remove_interaction(a, b)
remove_joint ¤
remove_joint(joint)

Removes this joint object from the model.

Parameters:

  • joint (:class:`~compas_timber.connections.Joint`) –

    The joint to remove.

Source code in src/compas_timber/model.py
def remove_joint(self, joint):
    # type: (Joint) -> None
    """Removes this joint object from the model.

    Parameters
    ----------
    joint : :class:`~compas_timber.connections.Joint`
        The joint to remove.

    """
    # clear the features of the joint before removing it from the model, to avoid leaving any features on the elements that are no longer part of a joint
    joint.clear_features()
    joint.clear_extensions()

    self._joints.pop(str(joint.guid), None)
    for interaction in joint.interactions:
        element_a, element_b = interaction
        self.remove_interaction(element_a, element_b)
    for element in joint.generated_elements:
        self.remove_element(element)
    joint.reset_location()
remove_joint_candidate ¤
remove_joint_candidate(candidate)

Removes this joint candidate from the model.

Parameters:

  • candidate (:class:`~compas_timber.connections.JointCandidate`) –

    The joint candidate to remove.

Source code in src/compas_timber/model.py
def remove_joint_candidate(self, candidate):
    # type: (JointCandidate) -> None
    """Removes this joint candidate from the model.

    Parameters
    ----------
    candidate : :class:`~compas_timber.connections.JointCandidate`
        The joint candidate to remove.
    """
    for interaction in candidate.interactions:
        element_a, element_b = interaction
        edge = (element_a.graphnode, element_b.graphnode)

        if edge in self._graph.edges():
            stored_candidate = self._graph.edge_attribute(edge, "candidates")
            if stored_candidate is candidate:
                self._graph.unset_edge_attribute(edge, "candidates")

        if not self._is_remaining_attrs_on_edge(edge):
            # if there's no other timber related attributes on that edge, then remove the edge as well
            super().remove_interaction(element_a, element_b)
remove_structural_connector_segments ¤
remove_structural_connector_segments(element_a: Element, element_b: Element) -> None

Removes all structural segments assigned to the interaction between two elements.

Parameters:

  • element_a (:class:`~compas_timber.elements.TimberElement`) –

    The first element.

  • element_b (:class:`~compas_timber.elements.TimberElement`) –

    The second element.

Source code in src/compas_timber/model.py
def remove_structural_connector_segments(self, element_a: Element, element_b: Element) -> None:
    """Removes all structural segments assigned to the interaction between two elements.

    Parameters
    ----------
    element_a : :class:`~compas_timber.elements.TimberElement`
        The first element.
    element_b : :class:`~compas_timber.elements.TimberElement`
        The second element.
    """
    edge = (element_a.graphnode, element_b.graphnode)
    if edge in self._graph.edges():
        self._graph.unset_edge_attribute(edge, "structural_segments")
set_topologies ¤
set_topologies(topologies)

Todo

calculate the topologies inside the model using the ConnectionSolver.

Source code in src/compas_timber/model.py
def set_topologies(self, topologies):
    """TODO: calculate the topologies inside the model using the ConnectionSolver."""
    self._topologies = topologies
transform ¤
transform(transformation)

Transform the model and clear the model-space caches of all elements.

Source code in src/compas_timber/model.py
def transform(self, transformation):
    """Transform the model and clear the model-space caches of all elements."""
    super().transform(transformation)
    for node in self.tree.root.children:
        clear_model_cache(node.element)