diff --git a/archetypal/template/building_template.py b/archetypal/template/building_template.py index b79dc1e5..4f020da1 100644 --- a/archetypal/template/building_template.py +++ b/archetypal/template/building_template.py @@ -31,7 +31,6 @@ class BuildingTemplate(UmiBase): .. image:: ../images/template/buildingtemplate.png """ - _CREATED_OBJECTS = [] __slots__ = ( "_partition_ratio", @@ -116,7 +115,7 @@ def __init__( self.Version = Version # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Perimeter(self): @@ -658,6 +657,13 @@ def __eq__(self, other): ] ) + @property + def ParentTemplates(self): + """Bails out of Parent Templates recursive call from UmiBase + And returns array of self for concatenation + """ + return {self} + @property def children(self): return self.Core, self.Perimeter, self.Structure, self.Windows diff --git a/archetypal/template/conditioning.py b/archetypal/template/conditioning.py index c2dc553e..df32a5c9 100644 --- a/archetypal/template/conditioning.py +++ b/archetypal/template/conditioning.py @@ -88,7 +88,8 @@ class ZoneConditioning(UmiBase): .. image:: ../images/template/zoninfo-conditioning.png """ - _CREATED_OBJECTS = [] + + _POSSIBLE_PARENTS = [("ZoneDefinition", ["Conditioning"])] __slots__ = ( "_cooling_setpoint", @@ -287,7 +288,7 @@ def __init__( self.area = area # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def area(self): diff --git a/archetypal/template/constructions/opaque_construction.py b/archetypal/template/constructions/opaque_construction.py index cbf4691f..a076eda4 100644 --- a/archetypal/template/constructions/opaque_construction.py +++ b/archetypal/template/constructions/opaque_construction.py @@ -31,7 +31,10 @@ class OpaqueConstruction(LayeredConstruction): * solar_reflectance_index """ - _CREATED_OBJECTS = [] + _POSSIBLE_PARENTS = [ + ("ZoneDefinition", ["InternalMassConstruction"]), + ("ZoneConstructionSet", ["Facade", "Slab", "Ground", "Partition", "Roof"]), + ] __slots__ = ("area",) @@ -48,7 +51,7 @@ def __init__(self, Name, Layers, **kwargs): self.area = 1 # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def r_value(self): diff --git a/archetypal/template/constructions/window_construction.py b/archetypal/template/constructions/window_construction.py index 220d85ed..e1e2ad9f 100644 --- a/archetypal/template/constructions/window_construction.py +++ b/archetypal/template/constructions/window_construction.py @@ -63,8 +63,7 @@ class WindowConstruction(LayeredConstruction): .. image:: ../images/template/constructions-window.png """ - _CREATED_OBJECTS = [] - + _POSSIBLE_PARENTS = [("WindowSetting", ["Construction"])] _CATEGORIES = ("single", "double", "triple", "quadruple") __slots__ = ("_category",) @@ -88,7 +87,7 @@ def __init__(self, Name, Layers, Category="Double", **kwargs): self.Category = Category # set here for validators # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Category(self): diff --git a/archetypal/template/dhw.py b/archetypal/template/dhw.py index d6966a48..d212eb87 100644 --- a/archetypal/template/dhw.py +++ b/archetypal/template/dhw.py @@ -19,7 +19,8 @@ class DomesticHotWaterSetting(UmiBase): .. image:: ../images/template/zoneinfo-dhw.png """ - _CREATED_OBJECTS = [] + + _POSSIBLE_PARENTS = [("ZoneDefinition", ["DomesticHotWater"])] __slots__ = ( "_flow_rate_per_floor_area", @@ -63,7 +64,7 @@ def __init__( self.area = area # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def FlowRatePerFloorArea(self): diff --git a/archetypal/template/load.py b/archetypal/template/load.py index fad07a3e..974677b5 100644 --- a/archetypal/template/load.py +++ b/archetypal/template/load.py @@ -42,7 +42,8 @@ class ZoneLoad(UmiBase): .. image:: ../images/template/zoneinfo-loads.png """ - _CREATED_OBJECTS = [] + + _POSSIBLE_PARENTS = [("ZoneDefinition", ["Loads"])] __slots__ = ( "_dimming_type", @@ -133,7 +134,7 @@ def __init__( self.volume = volume # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def DimmingType(self): diff --git a/archetypal/template/materials/gas_material.py b/archetypal/template/materials/gas_material.py index 045b18e9..4d0601c9 100644 --- a/archetypal/template/materials/gas_material.py +++ b/archetypal/template/materials/gas_material.py @@ -14,7 +14,6 @@ class GasMaterial(MaterialBase): .. image:: ../images/template/materials-gas.png """ - _CREATED_OBJECTS = [] __slots__ = ("_type", "_conductivity", "_density") @@ -41,7 +40,7 @@ def __init__( self.Density = Density # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Name(self): diff --git a/archetypal/template/materials/glazing_material.py b/archetypal/template/materials/glazing_material.py index 7bb3f5dc..63ba8535 100644 --- a/archetypal/template/materials/glazing_material.py +++ b/archetypal/template/materials/glazing_material.py @@ -17,7 +17,6 @@ class GlazingMaterial(MaterialBase): .. image:: ../images/template/materials-glazing.png """ - _CREATED_OBJECTS = [] __slots__ = ( "_ir_emissivity_back", @@ -106,7 +105,7 @@ def __init__( self.SolarTransmittance = SolarTransmittance # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Conductivity(self): diff --git a/archetypal/template/materials/material_base.py b/archetypal/template/materials/material_base.py index 7574566c..36388535 100644 --- a/archetypal/template/materials/material_base.py +++ b/archetypal/template/materials/material_base.py @@ -14,6 +14,27 @@ class MaterialBase(UmiBase): -cycle-impact """ + _POSSIBLE_PARENTS = [ + ( + "WindowConstruction", + lambda parent: [ + (i, layer.Material) for i, layer in enumerate(parent.Layers) + ], + ), + ( + "OpaqueConstruction", + lambda parent: [ + (i, layer.Material) for i, layer in enumerate(parent.Layers) + ], + ), + ( + "StructureInformation", + lambda parent: [ + (i, ratio.Material) for i, ratio in enumerate(parent.MassRatios) + ], + ), + ] + __slots__ = ( "_cost", "_embodied_carbon", diff --git a/archetypal/template/materials/nomass_material.py b/archetypal/template/materials/nomass_material.py index ea4b9564..5db94d2d 100644 --- a/archetypal/template/materials/nomass_material.py +++ b/archetypal/template/materials/nomass_material.py @@ -14,8 +14,6 @@ class NoMassMaterial(MaterialBase): """Use this component to create a custom no mass material.""" - _CREATED_OBJECTS = [] - _ROUGHNESS_TYPES = ( "VeryRough", "Rough", @@ -80,7 +78,7 @@ def __init__( self.MoistureDiffusionResistance = MoistureDiffusionResistance # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def r_value(self): diff --git a/archetypal/template/materials/opaque_material.py b/archetypal/template/materials/opaque_material.py index abb243f2..76fda6d6 100644 --- a/archetypal/template/materials/opaque_material.py +++ b/archetypal/template/materials/opaque_material.py @@ -16,8 +16,6 @@ class OpaqueMaterial(MaterialBase): .. image:: ../images/template/materials-opaque.png """ - _CREATED_OBJECTS = [] - _ROUGHNESS_TYPES = ( "VeryRough", "Rough", @@ -124,7 +122,7 @@ def __init__( # TODO: replace when NoMass and AirGap when properly is supported # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Conductivity(self): diff --git a/archetypal/template/schedule.py b/archetypal/template/schedule.py index 93da4291..457220cb 100644 --- a/archetypal/template/schedule.py +++ b/archetypal/template/schedule.py @@ -17,7 +17,6 @@ class UmiSchedule(Schedule, UmiBase): """Class that handles Schedules.""" - _CREATED_OBJECTS = [] __slots__ = ("_quantity",) @@ -33,7 +32,7 @@ def __init__(self, Name, quantity=None, **kwargs): self.quantity = quantity # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def quantity(self): @@ -503,6 +502,10 @@ def __hash__(self): class DaySchedule(UmiSchedule): """Superclass of UmiSchedule that handles daily schedules.""" + _POSSIBLE_PARENTS = [ + ("WeekSchedule", lambda parent: [(i, day) for i, day in enumerate(parent.Days)]) + ] + __slots__ = ("_values",) def __init__(self, Name, Values, Category="Day", **kwargs): @@ -517,6 +520,7 @@ def __init__(self, Name, Values, Category="Day", **kwargs): super(DaySchedule, self).__init__( Category=Category, Name=Name, Values=Values, **kwargs ) + self.CREATED_OBJECTS.append(self) @property def all_values(self) -> np.ndarray: @@ -719,6 +723,12 @@ def to_epbunch(self, idf): class WeekSchedule(UmiSchedule): """Superclass of UmiSchedule that handles weekly schedules.""" + _POSSIBLE_PARENTS = [ + ( + "YearSchedule", + lambda parent: [(i, p.Schedule) for i, p in enumerate(parent.Parts)], + ) + ] __slots__ = ("_days", "_values") def __init__(self, Name, Days=None, Category="Week", **kwargs): @@ -730,6 +740,7 @@ def __init__(self, Name, Days=None, Category="Week", **kwargs): """ super(WeekSchedule, self).__init__(Name, Category=Category, **kwargs) self.Days = Days + self.CREATED_OBJECTS.append(self) @property def Days(self): @@ -912,6 +923,35 @@ def children(self): class YearSchedule(UmiSchedule): """Superclass of UmiSchedule that handles yearly schedules.""" + _POSSIBLE_PARENTS = [ + ( + "WindowSetting", + [ + "AfnWindowAvailability", + "ShadingSystemAvailabilitySchedule", + "ZoneMixingAvailabilitySchedule", + ], + ), + ("VentilationSetting", ["NatVentSchedule", "ScheduledVentilationSchedule"]), + ( + "ZoneLoad", + [ + "EquipmentAvailabilitySchedule", + "LightsAvailabilitySchedule", + "OccupancySchedule", + ], + ), + ("DomesticHotWaterSetting", ["WaterSchedule"]), + ( + "ZoneConditioning", + [ + "CoolingSchedule", + "HeatingSchedule", + "MechVentSchedule", + ], + ), + ] + def __init__(self, Name, Type="Fraction", Parts=None, Category="Year", **kwargs): """Initialize a YearSchedule object with parameters. @@ -930,6 +970,7 @@ def __init__(self, Name, Type="Fraction", Parts=None, Category="Year", **kwargs) super(YearSchedule, self).__init__( Name=Name, Type=Type, schType="Schedule:Year", Category=Category, **kwargs ) + self.CREATED_OBJECTS.append(self) def __eq__(self, other): """Assert self is equivalent to other.""" @@ -1068,7 +1109,7 @@ def _get_parts(self, epbunch): next( ( x - for x in self._CREATED_OBJECTS + for x in self.CREATED_OBJECTS if x.Name == week_day_schedule_name and type(x).__name__ == "WeekSchedule" ) diff --git a/archetypal/template/structure.py b/archetypal/template/structure.py index 7818e703..104275d7 100644 --- a/archetypal/template/structure.py +++ b/archetypal/template/structure.py @@ -139,7 +139,7 @@ class StructureInformation(ConstructionBase): .. image:: ../images/template/constructions-structure.png """ - _CREATED_OBJECTS = [] + _POSSIBLE_PARENTS = [("BuildingTemplate", ["Structure"])] __slots__ = ("_mass_ratios",) @@ -154,7 +154,7 @@ def __init__(self, Name, MassRatios, **kwargs): self.MassRatios = MassRatios # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def MassRatios(self): diff --git a/archetypal/template/umi_base.py b/archetypal/template/umi_base.py index 782d9d09..64efa13d 100644 --- a/archetypal/template/umi_base.py +++ b/archetypal/template/umi_base.py @@ -45,6 +45,30 @@ def _shorten_name(long_name): class UmiBase(object): """Base class for template objects.""" + _POSSIBLE_PARENTS = [] + + _CREATED_OBJECTS = { + "GasMaterials": [], + "GlazingMaterials": [], + "OpaqueMaterials": [], + "NoMassMaterials": [], + "OpaqueConstructions": [], + "WindowConstructions": [], + "StructureInformations": [], + "DaySchedules": [], + "WeekSchedules": [], + "YearSchedules": [], + "DomesticHotWaterSettings": [], + "VentilationSettings": [], + "ZoneConditionings": [], + "ZoneConstructionSets": [], + "ZoneLoads": [], + "ZoneDefinitions": [], + "WindowSettings": [], + "BuildingTemplates": [], + "UmiSchedules": [], + } + __slots__ = ( "_id", "_datasource", @@ -91,6 +115,7 @@ def __init__( self.unit_number = next(self._ids) self.predecessors = None + @property def Name(self): """Get or set the name of the object.""" @@ -370,7 +395,7 @@ def extend(self, other, allow_duplicates): return other if other is None: return self - self._CREATED_OBJECTS.remove(self) + self.CREATED_OBJECTS.remove(self) id = self.id new_obj = self.combine(other, allow_duplicates=allow_duplicates) new_obj.id = id @@ -409,9 +434,8 @@ def get_unique(self): sorted( ( x - for x in self._CREATED_OBJECTS - if x == self - and x.Name == self.Name + for x in self.CREATED_OBJECTS + if x == self and x.Name == self.Name ), key=lambda x: x.unit_number, ) @@ -424,11 +448,7 @@ def get_unique(self): obj = next( iter( sorted( - ( - x - for x in self._CREATED_OBJECTS - if x == self - ), + (x for x in self.CREATED_OBJECTS if x == self), key=lambda x: x.unit_number, ) ), @@ -437,6 +457,57 @@ def get_unique(self): return obj + @property + def Parents(self): + """ Get the parents of an UmiBase Object""" + parents = {} + for (umi_class, lookup_children_of) in self._POSSIBLE_PARENTS: + possible_parents = UmiBase._CREATED_OBJECTS[umi_class + "s"] + for parent in possible_parents: + # For each parent, find the possible children which + # may equal self + keyed_children = [] # stores list of (key, child) + if callable(lookup_children_of): + keyed_children = lookup_children_of(parent) + elif isinstance(lookup_children_of, list): + keyed_children = [ + (attr, getattr(parent, attr)) + for attr in lookup_children_of + if getattr(parent, attr, None) + ] + else: + raise ValueError( + "The provided parent lookup is not valid. It must be a list of str or return a list of enumerated children" + ) + for key, child in keyed_children: + if child.id == self.id: + if child == self: + if parent not in parents: + parents[parent] = set() + parents[parent].add(key) + return parents + + @property + def ParentTemplates(self): + """ Get the parent templates of an UmiBase object""" + templates = set() + for parent in self.Parents.keys(): + # Recursive call terminates at Parent Template level, or if self.Parents is empty + templates = templates.union(parent.ParentTemplates) + return templates + + @classmethod + def CREATED_OBJECTS(cls, object_class): + """ Return all objects of a given class that UmiBase is aware of + Args: + object_class (class): Inherits UmiBase + """ + return cls._CREATED_OBJECTS[object_class.__name__ + "s"] + + @property + def CREATED_OBJECTS(self): + return UmiBase._CREATED_OBJECTS[self.__class__.__name__ + "s"] + class UserSet(Hashable, MutableSet): """UserSet class.""" diff --git a/archetypal/template/ventilation.py b/archetypal/template/ventilation.py index 4c8ee94f..8477ff2b 100644 --- a/archetypal/template/ventilation.py +++ b/archetypal/template/ventilation.py @@ -62,8 +62,8 @@ class VentilationSetting(UmiBase): .. image:: ../images/template/zoneinfo-ventilation.png """ - _CREATED_OBJECTS = [] + _POSSIBLE_PARENTS = [("ZoneDefinition", ["Ventilation"])] __slots__ = ( "_infiltration", "_is_infiltration_on", @@ -200,7 +200,7 @@ def __init__( self.volume = volume # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def NatVentSchedule(self): diff --git a/archetypal/template/window_setting.py b/archetypal/template/window_setting.py index 4e06d52b..634bbd72 100644 --- a/archetypal/template/window_setting.py +++ b/archetypal/template/window_setting.py @@ -36,8 +36,11 @@ class WindowSetting(UmiBase): .. _eppy : https://eppy.readthedocs.io/en/latest/ """ - _CREATED_OBJECTS = [] + _POSSIBLE_PARENTS = [ + ("BuildingTemplate", ["Windows"]), + ("ZoneDefinition", ["Windows"]), + ] __slots__ = ( "_operable_area", "_afn_discharge_c", @@ -133,7 +136,7 @@ def __init__( self.area = area # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def area(self): diff --git a/archetypal/template/zone_construction_set.py b/archetypal/template/zone_construction_set.py index 4e87a0d4..5f6b2a2a 100644 --- a/archetypal/template/zone_construction_set.py +++ b/archetypal/template/zone_construction_set.py @@ -12,8 +12,8 @@ class ZoneConstructionSet(UmiBase): """ZoneConstructionSet class.""" - _CREATED_OBJECTS = [] + _POSSIBLE_PARENTS = [("ZoneDefinition", ["Constructions"])] __slots__ = ( "_facade", "_ground", @@ -82,7 +82,7 @@ def __init__( self.volume = volume # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Facade(self): diff --git a/archetypal/template/zonedefinition.py b/archetypal/template/zonedefinition.py index dd2c9f6a..0f06569d 100644 --- a/archetypal/template/zonedefinition.py +++ b/archetypal/template/zonedefinition.py @@ -25,7 +25,8 @@ class ZoneDefinition(UmiBase): .. image:: ../images/template/zoneinfo-zone.png """ - _CREATED_OBJECTS = [] + + _POSSIBLE_PARENTS = [("BuildingTemplate", ["Perimeter", "Core"])] __slots__ = ( "_internal_mass_exposed_per_floor_area", @@ -122,7 +123,7 @@ def __init__( self.is_core = is_core # Only at the end append self to _CREATED_OBJECTS - self._CREATED_OBJECTS.append(self) + self.CREATED_OBJECTS.append(self) @property def Constructions(self): @@ -134,7 +135,7 @@ def Constructions(self, value): if value is not None: assert isinstance(value, ZoneConstructionSet), ( f"Input value error. Constructions must be of " - f"type {ZoneConstructionSet}, not {type(value)}." + f"type { ZoneConstructionSet}, not {type(value)}." ) self._constructions = value @@ -148,7 +149,7 @@ def Loads(self, value): if value is not None: assert isinstance(value, ZoneLoad), ( f"Input value error. Loads must be of " - f"type {ZoneLoad}, not {type(value)}." + f"type { ZoneLoad}, not {type(value)}." ) self._loads = value @@ -162,7 +163,7 @@ def Conditioning(self, value): if value is not None: assert isinstance(value, ZoneConditioning), ( f"Input value error. Conditioning must be of " - f"type {ZoneConditioning}, not {type(value)}." + f"type { ZoneConditioning}, not {type(value)}." ) self._conditioning = value @@ -176,7 +177,7 @@ def Ventilation(self, value): if value is not None: assert isinstance(value, VentilationSetting), ( f"Input value error. Ventilation must be of " - f"type {VentilationSetting}, not {type(value)}." + f"type { VentilationSetting}, not {type(value)}." ) self._ventilation = value @@ -190,7 +191,7 @@ def DomesticHotWater(self, value): if value is not None: assert isinstance(value, DomesticHotWaterSetting), ( f"Input value error. DomesticHotWater must be of " - f"type {DomesticHotWaterSetting}, not {type(value)}." + f"type { DomesticHotWaterSetting}, not {type(value)}." ) self._domestic_hot_water = value @@ -222,7 +223,7 @@ def InternalMassConstruction(self, value): if value is not None: assert isinstance(value, OpaqueConstruction), ( f"Input value error. InternalMassConstruction must be of " - f"type {OpaqueConstruction}, not {type(value)}." + f"type { OpaqueConstruction}, not {type(value)}." ) self._internal_mass_construction = value @@ -245,7 +246,7 @@ def Windows(self, value): if value is not None: assert isinstance(value, WindowSetting), ( f"Input value error. Windows must be of " - f"type {WindowSetting}, not {type(value)}." + f"type { WindowSetting}, not {type(value)}." ) self._windows = value @@ -674,7 +675,7 @@ def combine(self, other, weights=None, allow_duplicates=False): def validate(self): """Validate object and fill in missing values.""" - if self.InternalMassConstruction is None: + if not self.InternalMassConstruction: internal_mass = InternalMass.generic_internalmass_from_zone(self) self.InternalMassConstruction = internal_mass.construction self.InternalMassExposedPerFloorArea = ( diff --git a/archetypal/umi_template.py b/archetypal/umi_template.py index 0c6bc95b..b4550515 100644 --- a/archetypal/umi_template.py +++ b/archetypal/umi_template.py @@ -271,7 +271,7 @@ def from_idf_files( ] if keep_all_zones: - _zones = set(obj.get_unique() for obj in ZoneDefinition._CREATED_OBJECTS) + _zones = set(obj.get_unique() for obj in UmiBase.CREATED_OBJECTS(ZoneDefinition)) for zone in _zones: umi_template.ZoneDefinitions.append(zone) exceptions = [ZoneDefinition.__name__] diff --git a/tests/test_umi.py b/tests/test_umi.py index 97447246..368bb93c 100644 --- a/tests/test_umi.py +++ b/tests/test_umi.py @@ -24,7 +24,10 @@ from archetypal.template.window_setting import WindowSetting from archetypal.template.zone_construction_set import ZoneConstructionSet from archetypal.template.zonedefinition import ZoneDefinition -from archetypal.umi_template import UmiTemplateLibrary, no_duplicates +from archetypal.umi_template import ( + UmiTemplateLibrary, + no_duplicates, +) class TestUmiTemplate: @@ -84,6 +87,33 @@ def test_graph(self): G = a.to_graph(include_orphans=True) assert len(G) > n_nodes + def test_parent_templates(self): + """ Test that changing an object accurately updates the ParentTemplates list""" + file = "tests/input_data/umi_samples/BostonTemplateLibrary_2.json" + + lib = UmiTemplateLibrary.open(file) + for bt in lib.BuildingTemplates: + assert bt in bt.Perimeter.ParentTemplates + assert bt in bt.Perimeter.Loads.ParentTemplates + assert bt in bt.Core.Loads.ParentTemplates + assert bt in bt.Core.ParentTemplates + bt.Perimeter.Loads = lib.ZoneLoads[0] + bt.Core.Loads = lib.ZoneLoads[0] + + for bt in lib.BuildingTemplates: + # Can't use == here since the other Building Templates from opening the library are still in mem + assert bt in lib.ZoneLoads[0].ParentTemplates + + def test_all_children_for_parent_templates(self): + file = "tests/input_data/umi_samples/BostonTemplateLibrary_2.json" + + lib = UmiTemplateLibrary.open(file) + lib.unique_components(keep_orphaned=False) + for group, components in lib: + for component in components: + parent_bts_from_current_lib = [bt for bt in component.ParentTemplates if bt in lib.BuildingTemplates] + assert len(parent_bts_from_current_lib) > 0 + def test_template_to_template(self): """load the json into UmiTemplateLibrary object, then convert back to json and compare"""