Beispiel #1
0
    def __init__(
        self,
        name,
        colour=None,
        placement=Placement(),
        fem: FEM = None,
        settings: Settings = Settings(),
        metadata=None,
        parent=None,
        units="m",
        ifc_elem=None,
        guid=None,
        ifc_ref: IfcRef = None,
    ):
        super().__init__(name,
                         guid=guid,
                         metadata=metadata,
                         units=units,
                         parent=parent,
                         ifc_elem=ifc_elem,
                         ifc_ref=ifc_ref)
        self._nodes = Nodes(parent=self)
        self._beams = Beams(parent=self)
        self._plates = Plates(parent=self)
        self._pipes = list()
        self._walls = list()
        self._connections = Connections(parent=self)
        self._materials = Materials(parent=self)
        self._sections = Sections(parent=self)
        self._colour = colour
        self._placement = placement
        self._instances: Dict[Any, Instance] = dict()
        self._shapes = []
        self._parts = dict()
        self._groups: Dict[str, Group] = dict()

        if ifc_elem is not None:
            self.metadata["ifctype"] = self._import_part_from_ifc(ifc_elem)
        else:
            if self.metadata.get("ifctype") is None:
                self.metadata["ifctype"] = "site" if type(
                    self) is Assembly else "storey"

        self._props = settings
        if fem is not None:
            fem.parent = self

        self.fem = FEM(name + "-1", parent=self) if fem is None else fem
Beispiel #2
0
def get_sections_from_cache(part_cache, parent):
    sections_str = part_cache.get("SECTIONS_STR")
    sections_int = part_cache.get("SECTIONS_INT")
    if sections_str is None:
        return None

    def sec_from_list(sec_str, sec_int):
        guid, name, units, sec_type = str_fix(sec_str)
        r, wt, h, w_top, w_btn, t_w, t_ftop, t_fbtn, sec_id = [x if x != 0 else None for x in sec_int]
        return Section(
            name=name,
            guid=guid,
            sec_id=sec_id,
            units=units,
            sec_type=sec_type,
            r=r,
            wt=wt,
            h=h,
            w_top=w_top,
            w_btn=w_btn,
            t_w=t_w,
            t_ftop=t_ftop,
            t_fbtn=t_fbtn,
        )

    return Sections(
        [sec_from_list(sec_str, sec_int) for sec_str, sec_int in zip(sections_str, sections_int)], parent=parent
    )
Beispiel #3
0
    def move_all_mats_and_sec_here_from_subparts(self):
        for p in self.get_all_subparts():
            self._materials += p.materials
            self._sections += p.sections
            p._materials = Materials(parent=p)
            p._sections = Sections(parent=p)

        self.sections.merge_sections_by_properties()
        self.materials.merge_materials_by_properties()
Beispiel #4
0
def get_sections(bulk_str, fem: FEM, mass_elem, spring_elem) -> FemSections:
    # Section Names
    sect_names = {
        sec_id: name
        for sec_id, name in map(get_section_names,
                                cards.re_sectnames.finditer(bulk_str))
    }
    # Local Coordinate Systems
    lcsysd = {
        transno: vec
        for transno, vec in map(get_lcsys, cards.re_lcsys.finditer(bulk_str))
    }
    # Hinges
    hinges = {
        fixno: values
        for fixno, values in map(get_hinges, cards.re_belfix.finditer(
            bulk_str))
    }
    # Thickness'
    thick = {
        geono: t
        for geono, t in map(get_thicknesses, cards.re_thick.finditer(bulk_str))
    }
    # Eccentricities
    ecc = {
        eccno: values
        for eccno, values in map(get_eccentricities,
                                 cards.re_geccen.finditer(bulk_str))
    }

    list_of_sections = chain(
        (get_isection(m, sect_names, fem)
         for m in cards.re_giorh.finditer(bulk_str)),
        (get_box_section(m, sect_names, fem)
         for m in cards.re_gbox.finditer(bulk_str)),
        (get_tubular_section(m, sect_names, fem)
         for m in cards.re_gpipe.finditer(bulk_str)),
        (get_flatbar(m, sect_names, fem)
         for m in cards.re_gbarm.finditer(bulk_str)),
    )

    fem.parent._sections = Sections(list_of_sections, parent=fem.parent)
    [add_general_sections(m, fem) for m in cards.re_gbeamg.finditer(bulk_str)]

    geom = count(1)
    total_geo = count(1)
    res = (get_femsecs(m, total_geo, geom, lcsysd, hinges, ecc, thick, fem,
                       mass_elem, spring_elem)
           for m in cards.re_gelref1.finditer(bulk_str))
    sections = filter(lambda x: type(x) is FemSection, res)

    fem_sections = FemSections(sections, fem_obj=fem)
    logging.info(
        f"Successfully imported {next(geom) - 1} FEM sections out of {next(total_geo) - 1}"
    )
    return fem_sections
Beispiel #5
0
def test_positive_contained(sec, sec2):
    sec_collection = Sections([sec, sec2])
    assert sec2 in sec_collection
Beispiel #6
0
def test_negative_contained(sec, sec2):
    sec_collection = Sections([sec])
    assert sec2 not in sec_collection
Beispiel #7
0
class Part(BackendGeom):
    """A Part superclass design to host all relevant information for cad and FEM modelling."""
    def __init__(
        self,
        name,
        colour=None,
        placement=Placement(),
        fem: FEM = None,
        settings: Settings = Settings(),
        metadata=None,
        parent=None,
        units="m",
        ifc_elem=None,
        guid=None,
        ifc_ref: IfcRef = None,
    ):
        super().__init__(name,
                         guid=guid,
                         metadata=metadata,
                         units=units,
                         parent=parent,
                         ifc_elem=ifc_elem,
                         ifc_ref=ifc_ref)
        self._nodes = Nodes(parent=self)
        self._beams = Beams(parent=self)
        self._plates = Plates(parent=self)
        self._pipes = list()
        self._walls = list()
        self._connections = Connections(parent=self)
        self._materials = Materials(parent=self)
        self._sections = Sections(parent=self)
        self._colour = colour
        self._placement = placement
        self._instances: Dict[Any, Instance] = dict()
        self._shapes = []
        self._parts = dict()
        self._groups: Dict[str, Group] = dict()

        if ifc_elem is not None:
            self.metadata["ifctype"] = self._import_part_from_ifc(ifc_elem)
        else:
            if self.metadata.get("ifctype") is None:
                self.metadata["ifctype"] = "site" if type(
                    self) is Assembly else "storey"

        self._props = settings
        if fem is not None:
            fem.parent = self

        self.fem = FEM(name + "-1", parent=self) if fem is None else fem

    def add_beam(self, beam: Beam) -> Beam:
        if beam.units != self.units:
            beam.units = self.units
        beam.parent = self
        mat = self.add_material(beam.material)
        if mat != beam.material:
            beam.material = mat

        sec = self.add_section(beam.section)
        if sec != beam.section:
            beam.section = sec

        tap = self.add_section(beam.taper)
        if tap != beam.taper:
            beam.taper = tap

        old_node = self.nodes.add(beam.n1)
        if old_node != beam.n1:
            beam.n1 = old_node

        old_node = self.nodes.add(beam.n2)
        if old_node != beam.n2:
            beam.n2 = old_node

        self.beams.add(beam)
        return beam

    def add_plate(self, plate: Plate) -> Plate:
        if plate.units != self.units:
            plate.units = self.units

        plate.parent = self

        mat = self.add_material(plate.material)
        if mat is not None:
            plate.material = mat

        for n in plate.nodes:
            self.nodes.add(n)

        self._plates.add(plate)
        return plate

    def add_pipe(self, pipe: Pipe) -> Pipe:
        if pipe.units != self.units:
            pipe.units = self.units
        pipe.parent = self

        mat = self.add_material(pipe.material)
        if mat is not None:
            pipe.material = mat

        self._pipes.append(pipe)
        return pipe

    def add_wall(self, wall: Wall) -> Wall:
        if wall.units != self.units:
            wall.units = self.units
        wall.parent = self
        self._walls.append(wall)
        return wall

    def add_shape(self, shape: Shape) -> Shape:
        if shape.units != self.units:
            logger.info(
                f'shape "{shape}" has different units. changing from "{shape.units}" to "{self.units}"'
            )
            shape.units = self.units
        shape.parent = self

        mat = self.add_material(shape.material)
        if mat != shape.material:
            shape.material = mat

        self._shapes.append(shape)
        return shape

    def add_part(self, part: Part) -> Part:
        if issubclass(type(part), Part) is False:
            raise ValueError(
                "Added Part must be a subclass or instance of Part")
        if part.units != self.units:
            part.units = self.units
        part.parent = self
        if part.name in self._parts.keys():
            raise ValueError(
                f'Part name "{part.name}" already exists and cannot be overwritten'
            )
        self._parts[part.name] = part
        try:
            part._on_import()
        except NotImplementedError:
            logger.info(
                f'Part "{part}" has not defined its "on_import()" method')
        return part

    def add_joint(self, joint: JointBase) -> JointBase:
        """
        This method takes a Joint element containing two intersecting beams. It will check with the existing
        list of joints to see whether or not it is part of a larger more complex joint. It usese primarily
        two criteria.

        Criteria 1: If both elements are in an existing joint already, it will u

        Criteria 2: If the intersecting point coincides within a specified tolerance (currently 10mm)
        with an exisiting joint intersecting point. If so it will add the elements to this joint.
        If not it will create a new joint based on these two members.
        """
        if joint.units != self.units:
            joint.units = self.units
        self._connections.add(joint)
        return joint

    def add_material(self, material: Material) -> Material:
        if material.units != self.units:
            material.units = self.units
        material.parent = self
        return self._materials.add(material)

    def add_section(self, section: Section) -> Section:
        if section.units != self.units:
            section.units = self.units
        return self._sections.add(section)

    def add_object(self, obj: Union[Part, Beam, Plate, Wall, Pipe, Shape]):
        from ada import Beam

        if isinstance(obj, Part):
            self.add_part(obj)
        elif isinstance(obj, Beam):
            self.add_beam(obj)
        else:
            raise NotImplementedError()

    def add_penetration(self,
                        pen: Union[Penetration, PrimExtrude, PrimRevolve,
                                   PrimCyl, PrimBox],
                        add_pen_to_subparts=True) -> Penetration:
        if type(pen) in (PrimExtrude, PrimRevolve, PrimCyl, PrimBox):
            pen = Penetration(pen, parent=self)

        for bm in self.beams:
            bm.add_penetration(pen)

        for pl in self.plates:
            pl.add_penetration(pen)

        for shp in self.shapes:
            shp.add_penetration(pen)

        for pipe in self.pipes:
            for seg in pipe.segments:
                seg.add_penetration(pen)

        for wall in self.walls:
            wall.add_penetration(pen)

        if add_pen_to_subparts:
            for p in self.get_all_subparts():
                p.add_penetration(pen, False)
        return pen

    def add_instance(self, element, placement: Placement):
        if element not in self._instances.keys():
            self._instances[element] = Instance(element)
        self._instances[element].placements.append(placement)

    def add_set(
        self, name, set_members: List[Union[Part, Beam, Plate, Wall, Pipe,
                                            Shape]]) -> Group:
        if name not in self.groups.keys():
            self.groups[name] = Group(name, set_members, parent=self)
        else:
            logger.info(f'Appending set "{name}"')
            for mem in set_members:
                if mem not in self.groups[name].members:
                    self.groups[name].members.append(mem)

        return self.groups[name]

    def add_elements_from_ifc(self,
                              ifc_file_path: os.PathLike,
                              data_only=False):
        a = Assembly("temp")
        a.read_ifc(ifc_file_path, data_only=data_only)
        all_shapes = [shp for p in a.get_all_subparts()
                      for shp in p.shapes] + a.shapes
        for shp in all_shapes:
            self.add_shape(shp)

        all_beams = [bm for p in a.get_all_subparts()
                     for bm in p.beams] + [bm for bm in a.beams]
        for bm in all_beams:
            ids = self.beams.dmap.keys()
            names = [b.name for b in self.beams.dmap.values()]
            if bm.guid in ids:
                raise NotImplementedError(
                    "Have not considered merging ifc elements with identical IDs yet."
                )
            if bm.name in names:
                start = max(ids) + 1
                bm_name = f"bm{start}"
                if bm_name not in names:
                    bm.name = bm_name
                else:
                    while bm_name in names:
                        bm_name = f"bm{start}"
                        bm.name = bm_name
                        start += 1
            self.add_beam(bm)

        all_plates = [pl for p in a.get_all_subparts()
                      for pl in p.plates] + [pl for pl in a.plates]
        for pl in all_plates:
            self.add_plate(pl)

        all_pipes = [pipe for p in a.get_all_subparts()
                     for pipe in p.pipes] + a.pipes
        for pipe in all_pipes:
            self.add_pipe(pipe)

        all_walls = [wall for p in a.get_all_subparts()
                     for wall in p.walls] + a.walls
        for wall in all_walls:
            self.add_wall(wall)

    def read_step_file(self,
                       step_path,
                       name=None,
                       scale=None,
                       transform=None,
                       rotate=None,
                       colour=None,
                       opacity=1.0,
                       source_units="m"):
        """

        :param step_path: Can be path to stp file or path to directory of step files.
        :param name: Desired name of destination Shape object
        :param scale: Scale the step content upon import
        :param transform: Transform the step content upon import
        :param rotate: Rotate step content upon import
        :param colour: Assign a specific colour upon import
        :param opacity: Assign Opacity upon import
        :param source_units: Unit of the imported STEP file. Default is 'm'
        """
        from ada.occ.utils import extract_shapes

        shapes = extract_shapes(step_path, scale, transform, rotate)

        if len(shapes) > 0:
            ada_name = name if name is not None else "CAD" + str(
                len(self.shapes) + 1)
            for i, shp in enumerate(shapes):
                ada_shape = Shape(ada_name + "_" + str(i),
                                  shp,
                                  colour,
                                  opacity,
                                  units=source_units)
                self.add_shape(ada_shape)

    def create_objects_from_fem(self,
                                skip_plates=False,
                                skip_beams=False) -> None:
        """Build Beams and Plates from the contents of the local FEM object"""
        from ada.fem.formats.utils import convert_part_objects

        if type(self) is Assembly:
            for p_ in self.get_all_parts_in_assembly():
                logger.info(
                    f'Beginning conversion from fem to structural objects for "{p_.name}"'
                )
                convert_part_objects(p_, skip_plates, skip_beams)
        else:
            logger.info(
                f'Beginning conversion from fem to structural objects for "{self.name}"'
            )
            convert_part_objects(self, skip_plates, skip_beams)
        logger.info("Conversion complete")

    def get_part(self, name: str) -> Part:
        key_map = {key.lower(): key for key in self.parts.keys()}
        return self.parts[key_map[name.lower()]]

    def get_by_name(
            self,
            name) -> Union[Part, Plate, Beam, Shape, Material, Pipe, None]:
        """Get element of any type by its name."""
        for p in self.get_all_subparts() + [self]:
            if p.name == name:
                return p

            for bm in p.beams:
                if bm.name == name:
                    return bm

            for pl in p.plates:
                if pl.name == name:
                    return pl

            for shp in p.shapes:
                if shp.name == name:
                    return shp

            for pi in p.pipes:
                if pi.name == name:
                    return pi

            for mat in p.materials:
                if mat.name == name:
                    return mat

        logger.debug(
            f'Unable to find"{name}". Check if the element type is evaluated in the algorithm'
        )
        return None

    def get_all_parts_in_assembly(self, include_self=False) -> List[Part]:
        parent = self.get_assembly()
        list_of_ps = []
        self._flatten_list_of_subparts(parent, list_of_ps)
        if include_self:
            list_of_ps += [self]
        return list_of_ps

    def get_all_subparts(self, include_self=False) -> List[Part]:
        list_of_parts = [] if include_self is False else [self]
        self._flatten_list_of_subparts(self, list_of_parts)
        return list_of_parts

    def get_all_physical_objects(
        self,
        sub_elements_only=False,
        by_type=None,
        filter_by_guids: Union[List[str]] = None
    ) -> Iterable[Union[Beam, Plate, Wall, Pipe, Shape]]:
        physical_objects = []
        if sub_elements_only:
            iter_parts = iter([self])
        else:
            iter_parts = iter(self.get_all_subparts(include_self=True))

        for p in iter_parts:
            all_as_iterable = chain(p.plates, p.beams, p.shapes, p.pipes,
                                    p.walls)
            physical_objects.append(all_as_iterable)

        if by_type is not None:
            res = filter(lambda x: type(x) is by_type,
                         chain.from_iterable(physical_objects))
        else:
            res = chain.from_iterable(physical_objects)

        if filter_by_guids is not None:
            res = filter(lambda x: x.guid in filter_by_guids, res)

        return res

    def beam_clash_check(self, margins=5e-5):
        """
        For all beams in a Assembly get all beams touching or within the beam. Essentially a clash check is performed
        and it returns a dictionary of all beam ids and the touching beams. A margin to the beam volume can be included.

        :param margins: Add margins to the volume box (equal in all directions). Input is in meters. Can be negative.
        :return: A map generator for the list of beams and resulting intersecting beams
        """
        from ada.core.clash_check import basic_intersect

        all_parts = self.get_all_subparts() + [self]
        all_beams = [bm for p in all_parts for bm in p.beams]

        return filter(
            None,
            [basic_intersect(bm, margins, all_parts) for bm in all_beams])

    def move_all_mats_and_sec_here_from_subparts(self):
        for p in self.get_all_subparts():
            self._materials += p.materials
            self._sections += p.sections
            p._materials = Materials(parent=p)
            p._sections = Sections(parent=p)

        self.sections.merge_sections_by_properties()
        self.materials.merge_materials_by_properties()

    def _flatten_list_of_subparts(self, p, list_of_parts=None):
        for value in p.parts.values():
            list_of_parts.append(value)
            self._flatten_list_of_subparts(value, list_of_parts)

    def get_ifc_elem(self):
        if self._ifc_elem is None:
            self._ifc_elem = self._generate_ifc_elem()
        return self._ifc_elem

    def _generate_ifc_elem(self):
        from ada.ifc.write.write_levels import write_ifc_part

        return write_ifc_part(self)

    def _import_part_from_ifc(self, ifc_elem):
        convert = dict(
            site="IfcSite",
            space="IfcSpace",
            building="IfcBuilding",
            storey="IfcBuildingStorey",
            spatial="IfcSpatialZone",
        )
        opposite = {val: key for key, val in convert.items()}
        pr_type = ifc_elem.is_a()
        return opposite[pr_type]

    def _on_import(self):
        """A method call that will be triggered when a Part is imported into an existing Assembly/Part"""
        raise NotImplementedError()

    def to_fem_obj(
        self,
        mesh_size: float,
        bm_repr=ElemType.LINE,
        pl_repr=ElemType.SHELL,
        shp_repr=ElemType.SOLID,
        options: GmshOptions = None,
        silent=True,
        interactive=False,
        use_quads=False,
        use_hex=False,
    ) -> FEM:
        from ada import Beam, Plate, Shape
        from ada.fem.meshing import GmshOptions, GmshSession

        options = GmshOptions(Mesh_Algorithm=8) if options is None else options
        masses: List[Shape] = []
        with GmshSession(silent=silent, options=options) as gs:
            for obj in self.get_all_physical_objects(sub_elements_only=False):
                if type(obj) is Beam:
                    gs.add_obj(obj,
                               geom_repr=bm_repr.upper(),
                               build_native_lines=False)
                elif type(obj) is Plate:
                    gs.add_obj(obj, geom_repr=pl_repr.upper())
                elif issubclass(type(obj), Shape) and obj.mass is not None:
                    masses.append(obj)
                elif issubclass(type(obj), Shape):
                    gs.add_obj(obj, geom_repr=shp_repr.upper())
                else:
                    logger.error(
                        f'Unsupported object type "{obj}". Should be either plate or beam objects'
                    )

            # if interactive is True:
            #     gs.open_gui()

            gs.split_plates_by_beams()
            gs.mesh(mesh_size, use_quads=use_quads, use_hex=use_hex)

            if interactive is True:
                gs.open_gui()

            fem = gs.get_fem()

        for mass_shape in masses:
            cog_absolute = mass_shape.placement.absolute_placement(
            ) + mass_shape.cog
            n = fem.nodes.add(Node(cog_absolute))
            fem.add_mass(Mass(f"{mass_shape.name}_mass", [n], mass_shape.mass))

        return fem

    def to_vis_mesh(self,
                    export_config=None,
                    auto_merge_by_color=True,
                    opt_func: Callable = None) -> VisMesh:
        from ada.visualize.concept import PartMesh, VisMesh
        from ada.visualize.config import ExportConfig
        from ada.visualize.formats.assembly_mesh.write_objects_to_mesh import (
            filter_mesh_objects,
            obj_to_mesh,
        )
        from ada.visualize.formats.assembly_mesh.write_part_to_mesh import generate_meta

        if export_config is None:
            export_config = ExportConfig()

        all_obj_num = len(
            list(self.get_all_physical_objects(sub_elements_only=False)))
        print(
            f"Exporting {all_obj_num} physical objects to custom json format.")

        obj_num = 1
        part_array = []
        for p in self.get_all_subparts(include_self=True):
            if export_config.max_convert_objects is not None and obj_num > export_config.max_convert_objects:
                break
            obj_list = filter_mesh_objects(
                p.get_all_physical_objects(sub_elements_only=True),
                export_config)
            if obj_list is None:
                continue
            id_map = dict()
            for obj in obj_list:

                print(
                    f'Exporting "{obj.name}" [{obj.get_assembly().name}] ({obj_num} of {all_obj_num})'
                )
                res = obj_to_mesh(obj, export_config, opt_func=opt_func)
                if res is None:
                    continue
                id_map[obj.guid] = res
                obj_num += 1
                if export_config.max_convert_objects is not None and obj_num >= export_config.max_convert_objects:
                    print(
                        f'Maximum number of converted objects of "{export_config.max_convert_objects}" reached'
                    )
                    break

            if id_map is None:
                print(f'Part "{p.name}" has no physical members. Skipping.')
                continue

            for inst in p.instances.values():
                id_map[
                    inst.instance_ref.
                    guid].instances = inst.to_list_of_custom_json_matrices()

            part_array.append(PartMesh(name=p.name, id_map=id_map))

        amesh = VisMesh(
            name=self.name,
            project=self.metadata.get("project", "DummyProject"),
            world=part_array,
            meta=generate_meta(self, export_config),
        )

        if auto_merge_by_color:
            return amesh.merge_objects_in_parts_by_color()

        return amesh

    @property
    def parts(self) -> dict[str, Part]:
        return self._parts

    @property
    def shapes(self) -> List[Shape]:
        return self._shapes

    @shapes.setter
    def shapes(self, value: List[Shape]):
        self._shapes = value

    @property
    def beams(self) -> Beams:
        return self._beams

    @beams.setter
    def beams(self, value: Beams):
        self._beams = value

    @property
    def plates(self) -> Plates:
        return self._plates

    @plates.setter
    def plates(self, value: Plates):
        self._plates = value

    @property
    def pipes(self) -> List[Pipe]:
        return self._pipes

    @pipes.setter
    def pipes(self, value: List[Pipe]):
        self._pipes = value

    @property
    def walls(self) -> List[Wall]:
        return self._walls

    @walls.setter
    def walls(self, value: List[Wall]):
        self._walls = value

    @property
    def nodes(self) -> Nodes:
        return self._nodes

    @property
    def fem(self) -> FEM:
        return self._fem

    @fem.setter
    def fem(self, value: FEM):
        value.parent = self
        self._fem = value

    @property
    def connections(self) -> Connections:
        return self._connections

    @property
    def sections(self) -> Sections:
        return self._sections

    @sections.setter
    def sections(self, value: Sections):
        self._sections = value

    @property
    def materials(self) -> Materials:
        return self._materials

    @materials.setter
    def materials(self, value: Materials):
        self._materials = value

    @property
    def colour(self):
        if self._colour is None:
            from random import randint

            self._colour = randint(0, 255) / 255, randint(
                0, 255) / 255, randint(0, 255) / 255

        return self._colour

    @colour.setter
    def colour(self, value):
        self._colour = value

    @property
    def properties(self):
        return self._props

    @property
    def placement(self) -> Placement:
        return self._placement

    @placement.setter
    def placement(self, value: Placement):
        self._placement = value

    @property
    def instances(self) -> Dict[Any, Instance]:
        return self._instances

    @property
    def units(self):
        return self._units

    @units.setter
    def units(self, value):
        if value != self._units:
            for bm in self.beams:
                bm.units = value

            for pl in self.plates:
                pl.units = value

            for pipe in self._pipes:
                pipe.units = value

            for shp in self._shapes:
                shp.units = value

            for wall in self.walls:
                wall.units = value

            for pen in self.penetrations:
                pen.units = value

            for p in self.get_all_subparts():
                p.units = value

            self.sections.units = value
            self.materials.units = value
            self._units = value

            if type(self) is Assembly:
                assert isinstance(self, Assembly)
                from ada.ifc.utils import assembly_to_ifc_file

                self._ifc_file = assembly_to_ifc_file(self)

    @property
    def groups(self) -> Dict[str, Group]:
        return self._groups

    def __truediv__(self, other_object):
        from ada import Beam, Part, Pipe, Plate, Shape, Wall

        if type(other_object) in [list, tuple]:
            for obj in other_object:
                if type(obj) is Beam:
                    self.add_beam(obj)
                elif type(obj) is Plate:
                    self.add_plate(obj)
                elif type(obj) is Pipe:
                    self.add_pipe(obj)
                elif issubclass(type(obj), Part):
                    self.add_part(obj)
                elif issubclass(type(obj), Shape):
                    self.add_shape(obj)
                elif type(obj) is Wall:
                    self.add_wall(obj)
                else:
                    raise NotImplementedError(
                        f'"{type(obj)}" is not yet supported for smart append')
        elif issubclass(type(other_object), Part):
            self.add_part(other_object)
        elif type(other_object) is Beam:
            self.add_beam(other_object)
        elif type(other_object) is Plate:
            self.add_plate(other_object)
        elif type(other_object) is Pipe:
            self.add_pipe(other_object)
        elif issubclass(type(other_object), Shape):
            self.add_shape(other_object)
        else:
            raise NotImplementedError(
                f'"{type(other_object)}" is not yet supported for smart append'
            )
        return self

    def __repr__(self):
        nbms = len(self.beams) + len(
            [bm for p in self.get_all_subparts() for bm in p.beams])
        npls = len(self.plates) + len(
            [pl for p in self.get_all_subparts() for pl in p.plates])
        npipes = len(self.pipes) + len(
            [pl for p in self.get_all_subparts() for pl in p.pipes])
        nshps = len(self.shapes) + len(
            [shp for p in self.get_all_subparts() for shp in p.shapes])
        nels = len(self.fem.elements) + len(
            [el for p in self.get_all_subparts() for el in p.fem.elements])
        nnodes = len(self.fem.nodes) + len(
            [no for p in self.get_all_subparts() for no in p.fem.nodes])
        return (
            f'Part("{self.name}": Beams: {nbms}, Plates: {npls}, '
            f"Pipes: {npipes}, Shapes: {nshps}, Elements: {nels}, Nodes: {nnodes})"
        )