diff --git a/frads/eplus.py b/frads/eplus.py index 1264185..569d59d 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -70,17 +70,13 @@ def add_glazing_system(self, glzsys: GlazingSystem): >>> model = load_energyplus_model(Path("model.idf")) >>> model.add_glazing_system(glazing_system1) """ - if glzsys.solar_results is None or glzsys.photopic_results is None: - glzsys.compute_solar_photopic_results() - if glzsys.solar_results is None or glzsys.photopic_results is None: - raise ValueError("Solar and photopic results not computed.") name = glzsys.name gap_inputs = [] for i, gap in enumerate(glzsys.gaps): gap_inputs.append( epmodel.ConstructionComplexFenestrationStateGapInput( - gas=getattr(epm.GasType, gap[0][0].name.lower()), thickness=gap[-1] + gas=gap.gas[0].gas.capitalize(), thickness=gap.thickness ) ) layer_inputs: List[epmodel.ConstructionComplexFenestrationStateLayerInput] = [] @@ -94,21 +90,17 @@ def add_glazing_system(self, glzsys: GlazingSystem): emissivity_front=layer.emissivity_front, emissivity_back=layer.emissivity_back, infrared_transmittance=layer.ir_transmittance, - directional_absorptance_front=glzsys.solar_results.layer_results[ - i - ].front.absorptance.angular_total, - directional_absorptance_back=glzsys.solar_results.layer_results[ - i - ].back.absorptance.angular_total, + directional_absorptance_front=glzsys.solar_front_absorptance[i], + directional_absorptance_back=glzsys.solar_back_absorptance[i], ) ) input = epmodel.ConstructionComplexFenestrationStateInput( gaps=gap_inputs, layers=layer_inputs, - solar_reflectance_back=glzsys.solar_results.system_results.back.reflectance.matrix, - solar_transmittance_back=glzsys.solar_results.system_results.front.transmittance.matrix, - visible_transmittance_back=glzsys.photopic_results.system_results.back.transmittance.matrix, - visible_transmittance_front=glzsys.photopic_results.system_results.front.transmittance.matrix, + solar_reflectance_back=glzsys.solar_back_reflectance, + solar_transmittance_back=glzsys.solar_back_transmittance, + visible_transmittance_back=glzsys.visible_back_reflectance, + visible_transmittance_front=glzsys.visible_front_transmittance, ) self.add_construction_complex_fenestration_state(name, input) diff --git a/frads/window.py b/frads/window.py index 7302571..70a78b0 100644 --- a/frads/window.py +++ b/frads/window.py @@ -1,18 +1,21 @@ -import tempfile -from pathlib import Path +from dataclasses import asdict, is_dataclass, dataclass, field import json -from typing import List, NamedTuple, Optional, Tuple, Union +from pathlib import Path +import tempfile +from typing import List, Optional, Tuple, Union import pyradiance as pr import pywincalc as pwc + AIR = pwc.PredefinedGasType.AIR KRYPTON = pwc.PredefinedGasType.KRYPTON XENON = pwc.PredefinedGasType.XENON ARGON = pwc.PredefinedGasType.ARGON -class PaneRGB(NamedTuple): +@dataclass +class PaneRGB: """Pane color data object. Attributes: @@ -22,313 +25,91 @@ class PaneRGB(NamedTuple): trans_rgb: Transmittance RGB. """ - measured_data: pwc.ProductData coated_rgb: Tuple[float, float, float] glass_rgb: Tuple[float, float, float] trans_rgb: Tuple[float, float, float] + coated_side: Optional[str] = None -def create_gap(*gases_ratios: Tuple[pwc.PredefinedGasType, float], thickness): - """Create a gap with the gas and thickness.""" - if sum([ratio for _, ratio in gases_ratios]) != 1: - raise ValueError("The sum of the gas ratios must be 1.") - - components = pwc.create_gas([[ratio, gas] for gas, ratio in gases_ratios]) - - return pwc.Layers.gap(thickness=thickness, gas=components) - - -# class Layer: -# def __init__(self, inp): -# self.inp = inp -# if isinstance(inp, (str, Path)): -# self.inp = Path(inp) -# if not self.inp.exists(): -# raise FileNotFoundError(inp) -# self.data = None -# self.thickness = 0 -# -# -# class Glazing(Layer): -# def __init__(self, inp, name=None): -# super().__init__(inp) -# if isinstance(self.inp, Path): -# product_name = self.inp.stem -# if self.inp.suffix == ".json": -# self.data = pwc.parse_json_file(str(self.inp)) -# else: -# self.data = pwc.parse_optics_file(str(self.inp)) -# else: -# self.data = pwc.parse_json(self.inp) -# product_name = self.inp["name"] or self.inp["product_name"] -# self.data.product_name = ( -# self.data.product_name or product_name or name or str(inp)[:6] -# ) -# self.thickness = self.data.thickness -# -# -# class Shading(Layer): -# def __init__(self, inp, name=None): -# super().__init__(inp) -# if isinstance(self.inp, Path): -# self.data = pwc.parse_bsdf_xml_file(str(self.inp)) -# else: -# self.data = pwc.parse_bsdf_xml_string(self.inp) -# self.thickness = self.data.thickness -# self.data.product_name = self.data.product_name or name or str(inp)[:6] -# -# -# class AppliedFilm(Glazing): -# def __init__(self, inp, name=None): -# super().__init__(inp, name=name) - - -# class Gap(Layer): -# def __init__(self, *gases_ratios, thickness): -# if len(gases_ratios) > 1: -# if sum([ratio for _, ratio in gases_ratios]) != 1: -# raise ValueError("The sum of the gas ratios must be 1.") -# components = [ -# pwc.PredefinedGasMixtureComponent(gas, ratio) -# for gas, ratio in gases_ratios -# ] -# self.data = pwc.Gap(components, thickness) -# self.data = pwc.Gap(gases_ratios[0][0], thickness) +@dataclass +class Layer: + product_name: str + thickness: float + product_type: str + conductivity: float + emissivity_front: float + emissivity_back: float + ir_transmittance: float + rgb: PaneRGB -class GlazingSystem: - default_air_gap = (AIR, 1), 0.0127 - - def __init__(self): - self._name = "" - self._gaps = [] - self.layers = [] - self._thickness = 0 - self.glzsys = None - self.photopic_results = None - self.solar_results = None - self.updated = True +@dataclass +class Gas: + gas: str + ratio: float - @classmethod - def from_gls(cls, gls_path): - """Create a GlazingSystem from a glazing system file.""" - # unzip the gls file - pass - - @property - def name(self): - """Return the name of the glazing system.""" - if self._name: - return self._name - return "_".join([l.product_name for l in self.layers]) - - @name.setter - def name(self, value): - """Set the name of the glazing system.""" - self._name = value - - @property - def gaps(self): - """Return the gaps.""" - return self._gaps - - @gaps.setter - def gaps(self, value: List[Tuple[Tuple[pwc.PredefinedGasType, float], float]]): - """Set the gaps.""" - self._gaps = value - self._thickness -= len(value) * self.default_air_gap[-1] - self._thickness += sum([g[-1] for g in value]) - self.updated = True - - def add_glazing_layer(self, inp: Union[str, Path, bytes]) -> None: - """Add a glazing layer. - Add a glazing layer, which can be a json file, optics file, or json bytes. - - Args: - inp: The input file or bytes. - - Returns: - None - - Raises: - ValueError: If the input type is not valid. - FileNotFoundError: If the input file does not exist. - """ - - input_data: Optional[bytes] = None - input_path: Optional[Path] = None - product_name: Optional[str] = None - product_data: Optional[pwc.ProductData] = None - - if isinstance(inp, str): - input_path = Path(inp) - elif isinstance(inp, Path): - input_path = inp - elif isinstance(inp, bytes): - input_data = inp - else: - raise ValueError("Invalid input type") - - if input_path is not None: - if not input_path.exists(): - raise FileNotFoundError(inp) - product_name = input_path.stem - if input_path.suffix == ".json": - product_data = pwc.parse_json_file(str(input_path)) - else: - product_data = pwc.parse_optics_file(str(input_path)) - elif input_data is not None: - if len(input_data) == 0: - raise ValueError("Empty json data") - product_data = pwc.parse_json(input_data) - else: - raise ValueError("Invalid input type") + def __post_init__(self): + if self.ratio < 0 or self.ratio > 1: + raise ValueError("Gas ratio must be between 0 and 1.") + if self.gas.lower() not in ("air", "argon", "krypton", "xenon"): + raise ValueError("Invalid gas type.") - if product_data is None: - raise ValueError("Invalid input type") - - if product_data.product_name is None: - product_data.product_name = product_name or str(inp)[:6] - self.layers.append(product_data) - - self._thickness += product_data.thickness / 1000.0 or 0 # mm to m - if len(self.layers) > 1: - self._gaps.append(self.default_air_gap) - self._thickness += self.default_air_gap[-1] - self.updated = True - - def add_shading_layer(self, inp): - """Add a shading layer.""" - if isinstance(inp, (str, Path)): - _path = Path(inp) - if not _path.exists(): - raise FileNotFoundError(inp) - data = pwc.parse_bsdf_xml_file(str(_path)) - else: - data = pwc.parse_bsdf_xml_string(inp) - self.layers.append(data) - self._thickness += data.thickness / 1e3 or 0 - if len(self.layers) > 1: - self._gaps.append(self.default_air_gap) - self._thickness += self.default_air_gap[-1] - self.updated = True - - # def add_film_layer(self, inp, glazing, inside=False): - # """Add a film layer.""" - # film = AppliedFilm(inp) - - # if isinstance(inp, (str, Path)): - # _path = Path(inp) - # if not _path.exists(): - # raise FileNotFoundError(inp) - # data = pwc.parse_optics_file(str(_path)) - # else: - # data = pwc.parse_json(inp) - # if inside: - # self.layers.append(data) - # else: - # self.layers.insert(0, data) - # self._thickness += data.thickness / 1e3 or 0 - # self.updated = True - - def build(self): - """Build the glazing system.""" - if (len(self.layers) - 1) != len(self.gaps): - raise ValueError("Number of gaps must be one less than number of layers.") - - self.glzsys = pwc.GlazingSystem( - solid_layers=self.layers, - gap_layers=[create_gap(*g[:-1], thickness=g[-1]) for g in self._gaps], - width_meters=1, - height_meters=1, - environment=pwc.nfrc_shgc_environments(), - bsdf_hemisphere=pwc.BSDFHemisphere.create(pwc.BSDFBasisType.FULL), - ) - def compute_solar_photopic_results(self, force=False): - """Compute the solar photopic results.""" - self.updated = True if force else self.updated - if self.updated: - self.build() - if self.glzsys is None: - raise ValueError("Glazing system not built") - self.solar_results = self.glzsys.optical_method_results("SOLAR") - self.photopic_results = self.glzsys.optical_method_results("PHOTOPIC") - self.updated = False +@dataclass +class Gap: + gas: List[Gas] + thickness: float + + def __post_init__(self): + if self.thickness <= 0: + raise ValueError("Gap thickness must be greater than 0.") + if sum(g.ratio for g in self.gas) != 1: + raise ValueError("The sum of the gas ratios must be 1.") + + +@dataclass +class GlazingSystem: + name: str + thickness: float = 0 + layers: List[Layer] = field(default_factory=list) + gaps: List[Gap] = field(default_factory=list) + visible_front_transmittance: List[List[float]] = field(default_factory=list) + visible_back_transmittance: List[List[float]] = field(default_factory=list) + visible_front_reflectance: List[List[float]] = field(default_factory=list) + visible_back_reflectance: List[List[float]] = field(default_factory=list) + solar_front_transmittance: List[List[float]] = field(default_factory=list) + solar_back_transmittance: List[List[float]] = field(default_factory=list) + solar_front_reflectance: List[List[float]] = field(default_factory=list) + solar_back_reflectance: List[List[float]] = field(default_factory=list) + solar_front_absorptance: List[List[float]] = field(default_factory=list) + solar_back_absorptance: List[List[float]] = field(default_factory=list) + + def _matrix_to_str(self, matrix: List[List[float]]) -> str: + """Convert a matrix to a string.""" + return "\n".join([" ".join([str(n) for n in row]) for row in matrix]) def to_xml(self, out): """Save the glazing system to a file.""" - if self.solar_results is None or self.photopic_results is None: - self.compute_solar_photopic_results() - if self.solar_results is None or self.photopic_results is None: - raise ValueError("Glazing system not computed") with tempfile.TemporaryDirectory() as tmpdir: - _tbv = Path(tmpdir) / "tbv" - _tfv = Path(tmpdir) / "tfv" - _rfv = Path(tmpdir) / "rfv" - _rbv = Path(tmpdir) / "rbv" - _tbs = Path(tmpdir) / "tbs" - _tfs = Path(tmpdir) / "tfs" - _rfs = Path(tmpdir) / "rfs" - _rbs = Path(tmpdir) / "rbs" - with open(_tbv, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.photopic_results.system_results.front.transmittance.matrix - ) - ) - with open(_tfv, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.photopic_results.system_results.back.transmittance.matrix - ) - ) - with open(_rfv, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.photopic_results.system_results.front.reflectance.matrix - ) - ) - with open(_rbv, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.photopic_results.system_results.back.reflectance.matrix - ) - ) - with open(_tbs, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.solar_results.system_results.front.transmittance.matrix - ) - ) - with open(_tfs, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.solar_results.system_results.back.transmittance.matrix - ) - ) - with open(_rfs, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.solar_results.system_results.front.reflectance.matrix - ) - ) - with open(_rbs, "w") as f: - f.write( - "\n".join( - " ".join(str(n) for n in row) - for row in self.solar_results.system_results.back.reflectance.matrix - ) - ) - _vi = pr.WrapBSDFInput("Visible", _tbv, _tfv, _rfv, _rbv) - _si = pr.WrapBSDFInput("Solar", _tbs, _tfs, _rfs, _rbs) + _tmpdir = Path(tmpdir) + with open(_tvf := _tmpdir / "tbv", "w") as f: + f.write(self._matrix_to_str(self.visible_front_transmittance)) + with open(_tvb := _tmpdir / "tfv", "w") as f: + f.write(self._matrix_to_str(self.visible_back_transmittance)) + with open(_rvf := _tmpdir / "rfv", "w") as f: + f.write(self._matrix_to_str(self.visible_front_reflectance)) + with open(_rvb := _tmpdir / "rbv", "w") as f: + f.write(self._matrix_to_str(self.visible_back_reflectance)) + with open(_tsf := _tmpdir / "tbs", "w") as f: + f.write(self._matrix_to_str(self.solar_front_transmittance)) + with open(_tsb := _tmpdir / "tfs", "w") as f: + f.write(self._matrix_to_str(self.solar_back_transmittance)) + with open(_rsf := _tmpdir / "rfs", "w") as f: + f.write(self._matrix_to_str(self.solar_front_reflectance)) + with open(_rsb := _tmpdir / "rbs", "w") as f: + f.write(self._matrix_to_str(self.solar_back_reflectance)) + _vi = pr.WrapBSDFInput("Visible", _tvf, _tvb, _rvf, _rvb) + _si = pr.WrapBSDFInput("Solar", _tsf, _tsb, _rsf, _rsb) with open(out, "wb") as f: f.write( pr.wrapbsdf( @@ -337,41 +118,167 @@ def to_xml(self, out): inp=[_vi, _si], n=self.name, m="", - t=str(self._thickness), + t=str(self.thickness), ) ) - def save(self): - """ - Compress the glazing system into a .gls file. - A .gls file contain individual layer data and gap data. - System matrix results are also included. - """ - pass - - def gen_glazing(self): - """ - Generate a brtdfunc for a single or double pane glazing system. - """ - # Check if is more than two layers + def save(self, out: Union[str, Path]): + out = Path(out) + if out.suffix == ".xml": + self.to_xml(out) + elif out.suffix == ".json": + with open(out, "w") as f: + json.dump(asdict(self), f) + + @classmethod + def from_json(cls, path: Union[str, Path]): + with open(path, "r") as f: + data = json.load(f) + return cls(**data) + + def get_brtdfunc(self) -> pr.Primitive: + if any(layer.product_type != "glazing" for layer in self.layers): + raise ValueError("Only glass layers supported.") if len(self.layers) > 2: - raise ValueError("Only single and double pane supported") - # Check if all layers are glazing - for layer in self.layers: - if not layer.type == "glazing": - raise ValueError("Only glazing layers supported") - # Call gen_glaze to generate brtdfunc - return + raise ValueError("Only double pane supported.") + rgb = [layer.rgb for layer in self.layers] + return get_glazing_primitive(self.name, rgb) + + +def get_layers(input: List[pwc.ProductData]) -> List[Layer]: + layers = [] + for inp in input: + layers.append( + Layer( + product_name=inp.product_name, + thickness=inp.thickness, + product_type=inp.product_type, + conductivity=inp.conductivity, + emissivity_front=inp.emissivity_front, + emissivity_back=inp.emissivity_back, + ir_transmittance=inp.ir_transmittance, + rgb=get_layer_rgb(inp), + ) + ) + return layers + + +def create_pwc_gaps(gaps: List[Gap]): + """Create a list of pwc gaps from a list of gaps.""" + pwc_gaps = [] + for gap in gaps: + _gas = pwc.create_gas( + [[g.ratio, getattr(pwc.PredefinedGasType, g.gas.upper())] for g in gap.gas] + ) + _gap = pwc.Layers.gap(gas=_gas, thickness=gap.thickness) + pwc_gaps.append(_gap) + return pwc_gaps + + +def create_glazing_system( + name: str, layers: List[Union[Path, bytes]], gaps: Optional[List[Gap]] = None +) -> GlazingSystem: + """Create a glazing system from a list of layers and gaps.""" + if gaps is None: + gaps = [Gap([Gas("air", 1)], 0.0127) for _ in range(len(layers) - 1)] + layer_data = [] + thickness = 0 + for layer in layers: + product_data = None + if isinstance(layer, Path): + if layer.suffix == ".json": + product_data = pwc.parse_json_file(str(layer)) + elif layer.suffix == ".xml": + product_data = pwc.parse_bsdf_xml_file(str(layer)) + else: + product_data = pwc.parse_optics_file(str(layer)) + elif isinstance(layer, bytes): + try: + product_data = pwc.parse_json(layer) + except json.JSONDecodeError: + product_data = pwc.parse_bsdf_xml_string(layer) + if product_data is None: + raise ValueError("Invalid layer type") + layer_data.append(product_data) + thickness += product_data.thickness / 1000.0 or 0 # mm to m + + glzsys = pwc.GlazingSystem( + solid_layers=layer_data, + gap_layers=create_pwc_gaps(gaps), + width_meters=1, + height_meters=1, + environment=pwc.nfrc_shgc_environments(), + bsdf_hemisphere=pwc.BSDFHemisphere.create(pwc.BSDFBasisType.FULL), + ) + + solres = glzsys.optical_method_results("SOLAR") + solsys = solres.system_results + visres = glzsys.optical_method_results("PHOTOPIC") + vissys = visres.system_results + + return GlazingSystem( + name=name, + thickness=thickness, + layers=get_layers(layer_data), + gaps=gaps, + solar_front_absorptance=[ + alpha.front.absorptance.angular_total for alpha in solres.layer_results + ], + solar_back_absorptance=[ + alpha.back.absorptance.angular_total for alpha in solres.layer_results + ], + visible_back_reflectance=vissys.back.reflectance.matrix, + visible_front_reflectance=vissys.front.reflectance.matrix, + visible_back_transmittance=vissys.back.transmittance.matrix, + visible_front_transmittance=vissys.front.transmittance.matrix, + solar_back_reflectance=solsys.back.reflectance.matrix, + solar_front_reflectance=solsys.front.reflectance.matrix, + solar_back_transmittance=solsys.back.transmittance.matrix, + solar_front_transmittance=solsys.front.transmittance.matrix, + ) + + +def get_layer_rgb(layer: pwc.ProductData) -> PaneRGB: + photopic_wvl = range(380, 781, 10) + if isinstance(layer.measurements, pwc.DualBandBSDF): + return PaneRGB( + coated_rgb=(0, 0, 0), + glass_rgb=(0, 0, 0), + trans_rgb=(0, 0, 0), + coated_side=None, + ) + hemi = { + d.wavelength + * 1e3: ( + d.direct_component.transmittance_front, + d.direct_component.transmittance_back, + d.direct_component.reflectance_front, + d.direct_component.reflectance_back, + ) + for d in layer.measurements + } + tvf = [hemi[w][0] for w in photopic_wvl] + rvf = [hemi[w][2] for w in photopic_wvl] + rvb = [hemi[w][3] for w in photopic_wvl] + tf_x, tf_y, tf_z = pr.spec_xyz(tvf, 380, 780) + rf_x, rf_y, rf_z = pr.spec_xyz(rvf, 380, 780) + rb_x, rb_y, rb_z = pr.spec_xyz(rvb, 380, 780) + tf_rgb = pr.xyz_rgb(tf_x, tf_y, tf_z) + rf_rgb = pr.xyz_rgb(rf_x, rf_y, rf_z) + rb_rgb = pr.xyz_rgb(rb_x, rb_y, rb_z) + if layer.coated_side == "front": + coated_rgb = rf_rgb + glass_rgb = rb_rgb + else: + coated_rgb = rb_rgb + glass_rgb = rf_rgb + return PaneRGB(coated_rgb, glass_rgb, tf_rgb, layer.coated_side) -def get_glazing_primitive(panes: List[PaneRGB]) -> pr.Primitive: +def get_glazing_primitive(name: str, panes: List[PaneRGB]) -> pr.Primitive: """Generate a BRTDfunc to represent a glazing system.""" if len(panes) > 2: raise ValueError("Only double pane supported") - names = [] - for pane in panes: - names.append(pane.measured_data.product_name or "Unnamed") - name = "+".join(names) if len(panes) == 1: str_arg = [ "sr_clear_r", @@ -385,7 +292,7 @@ def get_glazing_primitive(panes: List[PaneRGB]) -> pr.Primitive: "0", "glaze1.cal", ] - coated_real = 1 if panes[0].measured_data.coated_side == "front" else -1 + coated_real = 1 if panes[0].coated_side == "front" else -1 real_arg = [ 0, 0, @@ -404,13 +311,13 @@ def get_glazing_primitive(panes: List[PaneRGB]) -> pr.Primitive: else: s12t_r, s12t_g, s12t_b = panes[0].trans_rgb s34t_r, s34t_g, s34t_b = panes[1].trans_rgb - if panes[0].measured_data.coated_side == "back": + if panes[0].coated_side == "back": s2r_r, s2r_g, s2r_b = panes[0].coated_rgb s1r_r, s1r_g, s1r_b = panes[0].glass_rgb else: # front or neither side coated s2r_r, s2r_g, s2r_b = panes[0].glass_rgb s1r_r, s1r_g, s1r_b = panes[0].coated_rgb - if panes[1].measured_data.coated_side == "back": + if panes[1].coated_side == "back": s4r_r, s4r_g, s4r_b = panes[1].coated_rgb s3r_r, s3r_g, s3r_b = panes[1].glass_rgb else: # front or neither side coated diff --git a/pyproject.toml b/pyproject.toml index 271756f..a423ae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ description = "Framework for lighting and energy simulations" dependencies = [ "epmodel>=0.2.5", "numpy>=1.24.4", - "pyradiance >= 0.1.2", + "pyradiance >= 0.3.0", "pywincalc>=3.1.0", "pyenergyplus_lbnl", "scipy~=1.10.1", diff --git a/test/test_eplus.py b/test/test_eplus.py index 12376df..10f84f3 100644 --- a/test/test_eplus.py +++ b/test/test_eplus.py @@ -1,7 +1,9 @@ from pathlib import Path -from frads.eplus import EnergyPlusModel, EnergyPlusSetup, load_energyplus_model -from frads.window import GlazingSystem +from frads.eplus import EnergyPlusSetup, load_energyplus_model +from frads.window import create_glazing_system +from pyenergyplus.dataset import ref_models +import pytest test_dir = Path(__file__).resolve().parent resource_dir = test_dir / "Resources" @@ -9,78 +11,76 @@ idf_path = resource_dir / "RefBldgMediumOfficeNew2004_southzone.idf" glazing_path = resource_dir / "igsdb_product_7406.json" +@pytest.fixture +def medium_office(): + return load_energyplus_model(ref_models["medium_office"]) -def test_add_glazingsystem(): - epmodel = load_energyplus_model(idf_path) - gs = GlazingSystem() - gs.add_glazing_layer(glazing_path) - epmodel.add_glazing_system(gs) - assert epmodel.construction_complex_fenestration_state != {} - assert isinstance(epmodel.construction_complex_fenestration_state, dict) - assert isinstance(epmodel.matrix_two_dimension, dict) - assert isinstance(epmodel.window_material_glazing, dict) - assert isinstance(epmodel.window_thermal_model_params, dict) +def test_add_glazingsystem(medium_office): + gs = create_glazing_system( + name="test", + layers=[glazing_path], + ) + medium_office.add_glazing_system(gs) + assert medium_office.construction_complex_fenestration_state != {} + assert isinstance(medium_office.construction_complex_fenestration_state, dict) + assert isinstance(medium_office.matrix_two_dimension, dict) + assert isinstance(medium_office.window_material_glazing, dict) + assert isinstance(medium_office.window_thermal_model_params, dict) -def test_add_lighting(): - epmodel = load_energyplus_model(idf_path) +def test_add_lighting(medium_office): try: - epmodel.add_lighting("z1") # zone does not exist + medium_office.add_lighting("z1") # zone does not exist assert False except ValueError: pass -def test_add_lighting1(): - epmodel = load_energyplus_model(idf_path) +def test_add_lighting1(medium_office): try: - epmodel.add_lighting("Perimeter_bot_ZN_1") # zone already has lighting + medium_office.add_lighting("Perimeter_bot_ZN_1") # zone already has lighting assert False except ValueError: pass -def test_add_lighting2(): - epmodel = load_energyplus_model(idf_path) - epmodel.add_lighting("Perimeter_bot_ZN_1", replace=True) +def test_add_lighting2(medium_office): + medium_office.add_lighting("Perimeter_bot_ZN_1", replace=True) - assert isinstance(epmodel.lights, dict) - assert isinstance(epmodel.schedule_constant, dict) - assert isinstance(epmodel.schedule_type_limits, dict) + assert isinstance(medium_office.lights, dict) + assert isinstance(medium_office.schedule_constant, dict) + assert isinstance(medium_office.schedule_type_limits, dict) -def test_output_variable(): +def test_output_variable(medium_office): """Test adding output variable to an EnergyPlusModel.""" - epmodel = load_energyplus_model(idf_path) - epmodel.add_output(output_name="Zone Mean Air Temperature", output_type="variable") + medium_office.add_output(output_name="Zone Mean Air Temperature", output_type="variable") assert "Zone Mean Air Temperature" in [ - i.variable_name for i in epmodel.output_variable.values() + i.variable_name for i in medium_office.output_variable.values() ] -def test_output_meter(): +def test_output_meter(medium_office): """Test adding output meter to an EnergyPlusModel.""" - epmodel = load_energyplus_model(idf_path) - epmodel.add_output( + medium_office.add_output( output_name="CO2:Facility", output_type="meter", reporting_frequency="Hourly", ) assert "CO2:Facility" in [ - i.key_name for i in epmodel.output_meter.values() + i.key_name for i in medium_office.output_meter.values() ] assert "Hourly" in [ - i.reporting_frequency.value for i in epmodel.output_meter.values() + i.reporting_frequency.value for i in medium_office.output_meter.values() ] -def test_energyplussetup(): +def test_energyplussetup(medium_office): """Test running EnergyPlusSetup.""" - epmodel = load_energyplus_model(idf_path) # file with Design Day - ep = EnergyPlusSetup(epmodel) + ep = EnergyPlusSetup(medium_office) ep.run(design_day=True) assert Path("eplusout.csv").exists() diff --git a/test/test_methods.py b/test/test_methods.py index c5dcb8d..21ce257 100644 --- a/test/test_methods.py +++ b/test/test_methods.py @@ -1,11 +1,14 @@ from datetime import datetime +import os + from frads.methods import TwoPhaseMethod, ThreePhaseMethod, WorkflowConfig -from frads.window import GlazingSystem +from frads.window import GlazingSystem, create_glazing_system, Gap, Gas from frads.ep2rad import epmodel_to_radmodel from frads.eplus import load_energyplus_model import frads as fr import numpy as np import pytest +from pyenergyplus.dataset import ref_models, weather_files @pytest.fixture @@ -100,22 +103,20 @@ def test_eprad_threephase(resources_dir): """ Integration test for ThreePhaseMethod using EnergyPlusModel and GlazingSystem """ - idf_path = resources_dir / "RefBldgMediumOfficeNew2004_Chicago.idf" view_path = resources_dir / "view1.vf" - epw_path = resources_dir / "USA_CA_Oakland.Intl.AP.724930_TMY3.epw" clear_glass_path = resources_dir / "CLEAR_3.DAT" product_7406_path = resources_dir / "igsdb_product_7406.json" shade_path = resources_dir / "ec60.rad" shade_bsdf_path = resources_dir / "ec60.xml" - epmodel = load_energyplus_model(idf_path) - gs_ec60 = GlazingSystem() - gs_ec60.add_glazing_layer(product_7406_path) - gs_ec60.add_glazing_layer(clear_glass_path) - gs_ec60.gaps = [((fr.AIR, 0.1), (fr.ARGON, 0.9), 0.0127)] - gs_ec60.name = "ec60" + epmodel = load_energyplus_model(ref_models["medium_office"]) + gs_ec60 = create_glazing_system( + name="ec60", + layers=[product_7406_path, clear_glass_path], + gaps=[Gap([Gas("air", 0.1), Gas("argon", 0.9)], 0.0127)], + ) epmodel.add_glazing_system(gs_ec60) - rad_models = epmodel_to_radmodel(epmodel, epw_file=epw_path) + rad_models = epmodel_to_radmodel(epmodel, epw_file=weather_files["usa_ca_san_francisco"]) zone = "Perimeter_bot_ZN_1" zone_dict = rad_models[zone] zone_dict["model"]["views"]["view1"] = {"file": view_path, "xres": 16, "yres": 16} diff --git a/test/test_window.py b/test/test_window.py index d1851f1..40e9362 100644 --- a/test/test_window.py +++ b/test/test_window.py @@ -1,5 +1,6 @@ +import os from pathlib import Path -from frads.window import GlazingSystem, AIR, ARGON +from frads.window import create_glazing_system, Gas, Gap, GlazingSystem, AIR, ARGON import pytest @@ -11,8 +12,27 @@ def glass_path(resources_dir): def shade_path(resources_dir): return resources_dir / "2011-SA1.xml" +@pytest.fixture +def glazing_system(glass_path): + gs = create_glazing_system( + name="gs1", + layers=[glass_path, glass_path], + ) + return gs + +def test_save_and_load(glazing_system): + """ + Test the save method of the GlazingSystem class. + """ + glazing_system.save("test.json") + assert Path("test.json").exists() + gs2 = GlazingSystem.from_json("test.json") + os.remove("test.json") + assert gs2.name == glazing_system.name + assert gs2.visible_back_reflectance == glazing_system.visible_back_reflectance -def test_simple_glazingsystem(glass_path): + +def test_simple_glazingsystem(glazing_system): """ Test the GlazingSystem class. Build a GlazingSystem object consisting of two layer of clear glass. @@ -21,26 +41,13 @@ def test_simple_glazingsystem(glass_path): Check the order and name of the layers. Check the composition of the default gap. """ - gs = GlazingSystem() - gs.add_glazing_layer(glass_path) - gs.add_glazing_layer(glass_path) - assert gs.layers[0].product_name == "Generic Clear Glass" - assert gs.layers[1].product_name == "Generic Clear Glass" - assert gs.name == f"{gs.layers[0].product_name}_{gs.layers[1].product_name}" - assert gs.gaps[0][0][0] == AIR - assert gs.gaps[0][0][1] == 1 - assert gs.gaps[0][1] == 0.0127 - assert round(gs._thickness, 6) == round( - sum( - [ - gs.layers[0].thickness / 1e3, - gs.gaps[0][1], - gs.layers[1].thickness / 1e3, - ] - ), - 6, - ) + assert glazing_system.layers[0].product_name == "Generic Clear Glass" + assert glazing_system.layers[1].product_name == "Generic Clear Glass" + assert glazing_system.name == "gs1" + assert glazing_system.gaps[0].gas[0].gas == "air" + assert glazing_system.gaps[0].gas[0].ratio == 1 + assert glazing_system.gaps[0].thickness == 0.0127 def test_customized_gap(glass_path): @@ -51,27 +58,18 @@ def test_customized_gap(glass_path): Check the thickness of the glazing system. Check the order and composition of the gap. """ - gs = GlazingSystem() - gs.add_glazing_layer(glass_path) - gs.add_glazing_layer(glass_path) - gs.gaps = [((AIR, 0.1), (ARGON, 0.9), 0.03)] - - assert gs.gaps[0][0][0] == AIR - assert gs.gaps[0][0][1] == 0.1 - assert gs.gaps[0][1][0] == ARGON - assert gs.gaps[0][1][1] == 0.9 - assert gs.gaps[0][2] == 0.03 - assert round(gs._thickness, 6) == round( - sum( - [ - gs.layers[0].thickness / 1e3, - gs.gaps[0][2], - gs.layers[1].thickness / 1e3, - ] - ), - 6, + gs = create_glazing_system( + name="gs2", + layers=[glass_path, glass_path], + gaps=[Gap([Gas("air", 0.1), Gas("argon", 0.9)], 0.03)], ) + assert gs.gaps[0].gas[0].gas == "air" + assert gs.gaps[0].gas[0].ratio == 0.1 + assert gs.gaps[0].gas[1].gas == "argon" + assert gs.gaps[0].gas[1].ratio == 0.9 + assert gs.gaps[0].thickness == 0.03 + def test_multilayer_glazing_shading(glass_path, shade_path): """ @@ -81,54 +79,28 @@ def test_multilayer_glazing_shading(glass_path, shade_path): Check the order of the layers. Check the order and composition of the gaps. """ - gs = GlazingSystem() - gs.add_glazing_layer(glass_path) - gs.add_glazing_layer(glass_path) - gs.add_shading_layer(shade_path) - gs.gaps = [((AIR, 0.1), (ARGON, 0.9), 0.03), ((AIR, 1), 0.01)] + gs = create_glazing_system( + name="gs3", + layers=[glass_path, glass_path, shade_path], + gaps=[ + Gap([Gas("air", 0.1), Gas("argon", 0.9)], 0.03), + Gap([Gas("air", 1)], 0.01), + ], + ) assert gs.layers[0].product_name == "Generic Clear Glass" assert gs.layers[1].product_name == "Generic Clear Glass" assert gs.layers[2].product_name == "Satine 5500 5%, White Pearl" - assert ( - gs.name - == f"{gs.layers[0].product_name}_{gs.layers[1].product_name}_{gs.layers[2].product_name}" - ) - assert gs.gaps[0][0][0] == AIR - assert gs.gaps[0][0][1] == 0.1 - assert gs.gaps[0][1][0] == ARGON - assert gs.gaps[0][1][1] == 0.9 - assert gs.gaps[0][2] == 0.03 - assert gs.gaps[1][0][0] == AIR - assert gs.gaps[1][0][1] == 1 - assert gs.gaps[1][1] == 0.01 - assert round(gs._thickness, 6) == round( - sum( - [ - gs.layers[0].thickness / 1e3, - gs.gaps[0][2], - gs.layers[1].thickness / 1e3, - gs.gaps[1][1], - gs.layers[2].thickness / 1e3, - ] - ), - 6, - ) - - assert gs.photopic_results is None - assert gs.solar_results is None - - -def test_compute_results(glass_path): - """ - Test the computation of the solar and photopic results. - - Check the results are not None. - """ - gs = GlazingSystem() - gs.add_glazing_layer(glass_path) - gs.compute_solar_photopic_results() - - assert gs.photopic_results is not None - assert gs.solar_results is not None + assert gs.name == "gs3" + assert gs.gaps[0].gas[0].gas == "air" + assert gs.gaps[0].gas[0].ratio == 0.1 + assert gs.gaps[0].gas[1].gas == "argon" + assert gs.gaps[0].gas[1].ratio == 0.9 + assert gs.gaps[0].thickness == 0.03 + assert gs.gaps[1].gas[0].gas == "air" + assert gs.gaps[1].gas[0].ratio == 1 + assert gs.gaps[1].thickness == 0.01 + + assert gs.visible_back_reflectance is not None + assert gs.solar_back_absorptance is not None