From 09276fa65c9ebd27acfdb2f2cbcb9e30a1338e1d Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Tue, 17 Oct 2023 17:41:49 -0700 Subject: [PATCH 01/16] docs(guide7): add guide7 --- docs/how-to/guide7.md | 247 ++++++++++++++++++++++++++++++++++++++++++ docs/how-to/guide8.md | 25 +++++ docs/how-to/index.md | 3 + 3 files changed, 275 insertions(+) create mode 100644 docs/how-to/guide7.md create mode 100644 docs/how-to/guide8.md diff --git a/docs/how-to/guide7.md b/docs/how-to/guide7.md new file mode 100644 index 0000000..d5fb486 --- /dev/null +++ b/docs/how-to/guide7.md @@ -0,0 +1,247 @@ +# How to set up a workflow configuration? + +This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. The workflow configuration can be used to run a Three-Phase method workflow. + +The workflow configuration has two parts: settings and model. +1. `settings` is used to define the simulation settings, such as the number of parallel processes, the sky basis, the window basis, the epw/wea file, the latitude, the longitude, the timezone, the site elevation, the matrices sampling parameters, etc. +2. `model` is used to define the model, which includes the scene, the windows, the materials, the sensors, and the views. + +## Ways to set up a configuration file + +**Method 1** Use the `settings` and `model` attributes in the `WorkflowConfig` class to set up the settings and model parameters in the workflow configuration. The `settings` and `model` attributes are instances of the `Settings` and `Model` classes, respectively. + +**Method 2** Use the `from_dict()` method of the `WorkflowConfig` class to generate a workflow configuration by passing a dictionary to the method. The dictionary should contain the settings and model parameters. + +## Method 1 + +### 1.1 Initialize a `WorkflowConfig` instance + +```python +cfg = fr.WorkflowConfig() +``` + +### 1.2 Set up the settings + +Use the `settings` attribute to set up the settings. The `settings` attribute is an instance of the `Settings` class. + +The `Settings` class has the following default settings. You can change the default settings by assigning new values to the attributes of the `Settings` class. + +??? note "Default settings" + **name** The name of the simulation. (default="") + + **num_processors**: The number of processors to use for the simulation. (default=1) + + **method**: The Radiance method to use for the simulation. + **(default="3phase") + + **overwrite**: Whether to overwrite existing files. (default=False) + + **save_matrices**: Whether to save the matrices generated by the simulation. (default=False) + + **sky_basis**: The sky basis to use for the simulation. (default="r1") + + **window_basis**: The window basis to use for the simulation. (default="kf") + + **non_coplanar_basis**: The non-coplanar basis to use for the simulation. (default="kf") + + **sun_basis**: The sun basis to use for the simulation. (default="r6") + + **sun_culling**: Whether to cull suns. (default=True) + + **separate_direct**: Whether to separate direct and indirect contributions. (default=False) + + **epw_file**: The path to the EPW file to use for the simulation. (default="") + + **wea_file**: The path to the WEA file to use for the simulation. (default="") + + **start_hour**: The start hour for the simulation. (default=8) + + **end_hour**: The end hour for the simulation. (default=18) + + **daylight_hours_only**: Whether to simulate only daylight hours. (default=True) + + **latitude**: The latitude for the simulation. (default=37) + + **longitude**: The longitude for the simulation. (default=122) + + **timezone**: The timezone for the simulation. (default=120) + + **orientation**: sky rotation. (default=0) + + **site_elevation**: The elevation for the simulation. (default=100) + + **sensor_sky_matrix**: The sky matrix sampling parameters. (default_factory=lambda: ["-ab", "6", "-ad", "8192", "-lw", "5e-5"]) + + **view_sky_matrix**: View sky matrix sampling parameters. (default_factory=lambda: ["-ab", "6", "-ad", "8192", "-lw", "5e-5"]) + + **sensor_sun_matrix**: Sensor sun matrix sampling parameters. (Default_factory=lambda: [ "-ab", "1", "-ad", "256", "-lw", "1e-3", "-dj", "0", "-st", "0"]) + + **view_sun_matrix**: View sun matrix sampling parameters.(default_factory=lambda: ["-ab", "1", "-ad", "256", "-lw", "1e-3", "-dj", "0", "-st", "0"]) + + **sensor_window_matrix**: Sensor window matrix sampling parameters. (default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"]) + + **view_window_matrix**: View window matrix sampling parameters. (default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"]) + + **daylight_matrix**: Daylight matrix sampling parameters. (default_factory=lambda: ["-ab", "2", "-c", "5000"]) + + +```python +# Set the number of parallel processes +cfg.settings.num_processors = 4 +``` + +### 1.2 Set up the model + +Use the `model` attribute to set up the model. The `model` attribute is an instance of the `Model` class. + +The `Model` class has the following attributes. + +1. `scene`: The scene to use for the simulation. An instance of the `SceneConfig` class. + +2. `windows`: The windows to use for the simulation. A dictionary of instances of the `WindowConfig` class. + +3. `materials`: The materials to use for the simulation. An instances of the `MaterialConfig` class. + +4. `sensors`: The sensors to use for the simulation. A dictionary of instances of the `SensorConfig` class. + +5. `views`: The views to use for the simulation. A dictionary of instances of the `ViewConfig` class. + +```python title="scene" +cfg.model.scene = fr.SceneConfig( + files=[ + "walls.rad", + "ceiling.rad", + "floor.rad", + "ground.rad", + ] +) +``` +??? example "wall.rad" + geometry primitive + ``` + wall_mat polygon wall_1 + 0 + 0 + 12 + 0 0 0 + 0 0 3 + 0 3 3 + 0 3 0 + ``` + + +```python title="windows" +cfg.model.windows = {"window_1": fr.WindowConfig( + files=["window.rad"], + matrix_file="blinds.xml", + ) +} +``` + +??? example "window.rad" + geometry primitive + ``` + window_mat polygon window_1 + 0 + 0 + 12 + 0 1 1 + 0 1 2 + 0 2 2 + 0 2 1 + ``` + +```python title="materials" +cfg.model.materials = fr.MaterialConfig(files=["materials.mat"]) +``` + +??? example "materials.mat" + material primitive + ``` + void plastic wall_mat + 0 + 0 + 5 0.5 0.5 0.5 0 0 + ``` + +```python title="sensors" +cfg.model.sensors = {"sensor_1": fr.SensorConfig(file="sensor.txt")} +``` + +??? example "sensor.txt" + x_viewpoint y_viewpoint z_viewpoint x_direction y_direction z_direction + ``` + 1 1 1 0 -1 0 + ``` + +```python title="views" +cfg.model.views = { + "view_1": fr.ViewConfig( + file="view_1.vf", + xres=16, + yres=16, + ) +} +``` + +??? example "view.vf" + view_type view_point view_direction view_up_direction view_horizontal_field_of_view view_vertical_field_of_view view_rotation_angle + ``` + -vta -vp 1 1 1 -vd 0 -1 0 -vu 0 0 1 -vh 180 -vv 180 + ``` + +## Method 2 + +### 2.1 Initialize a `WorkflowConfig` instance with a dictionary + +```python +cfg = fr.WorkflowConfig.from_dict(dict) +``` + +??? example "dict" + ```json + cfg = { + "settings": { + "method": "3phase", + "sky_basis": "r1", + "epw_file": "", + "wea_file": "oak.wea", + "sensor_sky_matrix": ["-ab", "0"], + "view_sky_matrix": ["-ab", "0"], + "sensor_window_matrix": ["-ab", "0"], + "view_window_matrix": ["-ab", "0"], + "daylight_matrix": ["-ab", "0"] + }, + "model": { + "scene": { + "files": [ + "walls.rad", + "ceiling.rad", + "floor.rad", + "ground.rad" + ] + }, + "windows": { + "glass_1": { + "file": "glass.rad", + "matrix_file": "blinds.xml" + } + }, + "materials": { + "files": ["materials.mat"] + }, + "sensors": { + "sensor_1": {"file": "grid.txt"} + }, + "views": { + "view_1": { + "file": "view_1.vf", + "xres": 16, + "yres": 16 + } + } + } + } + ``` + + diff --git a/docs/how-to/guide8.md b/docs/how-to/guide8.md new file mode 100644 index 0000000..14cc414 --- /dev/null +++ b/docs/how-to/guide8.md @@ -0,0 +1,25 @@ +# How to set up a Three-Phase method workflow with a configuration file? + +This guide will show you how to set up a Three-Phase method workflow with a configuration file. The configuration file is a ???? file that contains all the information needed to run a Three-Phase method workflow. + +**Workflow** + +1. + + +## + + + +cfg = fr.WorkflowConfig() +cfg.Model.scene = +cfg.Model.materials = +cfg.Model.sensors = +cfg.Model.views = + +rad_workflow = fr.ThreePhaseMethod(rad_cfg) +print(rad_workflow.mfile) +# Separate run to generate matrices and save to file +# A *.npz file will be generated in the current working directory +rad_workflow.config.settings.save_matrices = True +rad_workflow.generate_matrices(view_matrices=False) \ No newline at end of file diff --git a/docs/how-to/index.md b/docs/how-to/index.md index b9890a5..dc56b4b 100644 --- a/docs/how-to/index.md +++ b/docs/how-to/index.md @@ -15,5 +15,8 @@ provided in this project. 6. [How to setup a simple rtrace workflow?](guide6.md) +7. [How to setup a workflow configuration?](guide7.md) + +8. [How to setup a Three-Phase method workflow with a workflow configuration?](guide8.md) From 3ac54c6ae227d8752e77149872423fc98060d225 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 18 Oct 2023 10:39:50 -0700 Subject: [PATCH 02/16] docs(guide7): Update guide7 --- docs/how-to/guide7.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/how-to/guide7.md b/docs/how-to/guide7.md index d5fb486..d1dc9ad 100644 --- a/docs/how-to/guide7.md +++ b/docs/how-to/guide7.md @@ -1,10 +1,11 @@ # How to set up a workflow configuration? -This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. The workflow configuration can be used to run a Three-Phase method workflow. +This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. The workflow configuration can be used to run a two- or three- or five- Phase method workflow. The workflow configuration has two parts: settings and model. -1. `settings` is used to define the simulation settings, such as the number of parallel processes, the sky basis, the window basis, the epw/wea file, the latitude, the longitude, the timezone, the site elevation, the matrices sampling parameters, etc. -2. `model` is used to define the model, which includes the scene, the windows, the materials, the sensors, and the views. + +1. `settings` defines the simulation settings, such as number of parallel processes, sky basis, window basis, epw/wea file, latitude, longitude, timezone, site elevation, matrices sampling parameters, etc. +2. `model` defines the model, which includes scene, windows, materials, sensors, and views. ## Ways to set up a configuration file @@ -132,13 +133,13 @@ cfg.model.scene = fr.SceneConfig( ```python title="windows" cfg.model.windows = {"window_1": fr.WindowConfig( - files=["window.rad"], + files=["window_!.rad"], matrix_file="blinds.xml", ) } ``` -??? example "window.rad" +??? example "window_1.rad" geometry primitive ``` window_mat polygon window_1 @@ -168,7 +169,7 @@ cfg.model.materials = fr.MaterialConfig(files=["materials.mat"]) cfg.model.sensors = {"sensor_1": fr.SensorConfig(file="sensor.txt")} ``` -??? example "sensor.txt" +??? example "sensor_1.txt" x_viewpoint y_viewpoint z_viewpoint x_direction y_direction z_direction ``` 1 1 1 0 -1 0 @@ -184,7 +185,7 @@ cfg.model.views = { } ``` -??? example "view.vf" +??? example "view_1.vf" view_type view_point view_direction view_up_direction view_horizontal_field_of_view view_vertical_field_of_view view_rotation_angle ``` -vta -vp 1 1 1 -vd 0 -1 0 -vu 0 0 1 -vh 180 -vv 180 @@ -222,8 +223,8 @@ cfg = fr.WorkflowConfig.from_dict(dict) ] }, "windows": { - "glass_1": { - "file": "glass.rad", + "window_1": { + "file": "window_1.rad", "matrix_file": "blinds.xml" } }, @@ -231,7 +232,7 @@ cfg = fr.WorkflowConfig.from_dict(dict) "files": ["materials.mat"] }, "sensors": { - "sensor_1": {"file": "grid.txt"} + "sensor_1": {"file": "sensor_1.txt"} }, "views": { "view_1": { From 793938f65564559c3322ccea9d17e684c42a0afa Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Thu, 19 Oct 2023 13:39:22 -0700 Subject: [PATCH 03/16] docs(guide7): Update guide7 --- docs/how-to/guide7.md | 80 ++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/docs/how-to/guide7.md b/docs/how-to/guide7.md index d1dc9ad..751b989 100644 --- a/docs/how-to/guide7.md +++ b/docs/how-to/guide7.md @@ -1,11 +1,11 @@ -# How to set up a workflow configuration? +# How to set up a workflow configuration for Radiance simulation? -This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. The workflow configuration can be used to run a two- or three- or five- Phase method workflow. +The workflow configuration can be used to run a two- or three- or five-Phase method simulation in Radiance. This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. The workflow configuration has two parts: settings and model. 1. `settings` defines the simulation settings, such as number of parallel processes, sky basis, window basis, epw/wea file, latitude, longitude, timezone, site elevation, matrices sampling parameters, etc. -2. `model` defines the model, which includes scene, windows, materials, sensors, and views. +2. `model` defines the model, which includes the scene, windows, materials, sensors, and views. ## Ways to set up a configuration file @@ -13,7 +13,13 @@ The workflow configuration has two parts: settings and model. **Method 2** Use the `from_dict()` method of the `WorkflowConfig` class to generate a workflow configuration by passing a dictionary to the method. The dictionary should contain the settings and model parameters. -## Method 1 +## 0. Import the required classes and functions + +```python +import frads as fr +``` + +## 1. Use the `settings` and `model` attributes ### 1.1 Initialize a `WorkflowConfig` instance @@ -81,7 +87,7 @@ The `Settings` class has the following default settings. You can change the defa **sensor_window_matrix**: Sensor window matrix sampling parameters. (default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"]) - **view_window_matrix**: View window matrix sampling parameters. (default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"]) + **view_window_matrix**: View window matrix sampling parameters. (default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"]) **daylight_matrix**: Daylight matrix sampling parameters. (default_factory=lambda: ["-ab", "2", "-c", "5000"]) @@ -108,15 +114,11 @@ The `Model` class has the following attributes. 5. `views`: The views to use for the simulation. A dictionary of instances of the `ViewConfig` class. ```python title="scene" -cfg.model.scene = fr.SceneConfig( - files=[ - "walls.rad", - "ceiling.rad", - "floor.rad", - "ground.rad", - ] -) +scene = fr.SceneConfig() +scene.files = ["walls.rad", "ceiling.rad", "floor.rad", "ground.rad"] +cfg.model.scene = scene ``` + ??? example "wall.rad" geometry primitive ``` @@ -132,11 +134,10 @@ cfg.model.scene = fr.SceneConfig( ```python title="windows" -cfg.model.windows = {"window_1": fr.WindowConfig( - files=["window_!.rad"], - matrix_file="blinds.xml", - ) -} +window_1 = fr.WindowConfig() +window_1.file = "window_1.rad" +window_1.matrix_file = "window_1.xml" +cfg.model.windows = {"window_1": window_1} ``` ??? example "window_1.rad" @@ -153,7 +154,9 @@ cfg.model.windows = {"window_1": fr.WindowConfig( ``` ```python title="materials" -cfg.model.materials = fr.MaterialConfig(files=["materials.mat"]) +materials = fr.MaterialConfig() +materials.files = ["materials.mat"] +cfg.model.materials = materials ``` ??? example "materials.mat" @@ -166,7 +169,9 @@ cfg.model.materials = fr.MaterialConfig(files=["materials.mat"]) ``` ```python title="sensors" -cfg.model.sensors = {"sensor_1": fr.SensorConfig(file="sensor.txt")} +sensor_1 = fr.SensorConfig() +sensor_1.files = ["sensor_1.txt"] +cfg.model.sensors = {"sensor_1": sensor_1} ``` ??? example "sensor_1.txt" @@ -176,13 +181,11 @@ cfg.model.sensors = {"sensor_1": fr.SensorConfig(file="sensor.txt")} ``` ```python title="views" -cfg.model.views = { - "view_1": fr.ViewConfig( - file="view_1.vf", - xres=16, - yres=16, - ) -} +view_1 = fr.ViewConfig() +view_1.file = "view_1.vf" +view_1.xres = 16 +view_1.yres = 16 +cfg.model.views = {"view_1": view_1} ``` ??? example "view_1.vf" @@ -191,17 +194,17 @@ cfg.model.views = { -vta -vp 1 1 1 -vd 0 -1 0 -vu 0 0 1 -vh 180 -vv 180 ``` -## Method 2 +## 2. Use the `from_dict()` method ### 2.1 Initialize a `WorkflowConfig` instance with a dictionary ```python -cfg = fr.WorkflowConfig.from_dict(dict) +cfg = fr.WorkflowConfig.from_dict(dict_1) ``` -??? example "dict" +??? example "dict_1" ```json - cfg = { + dict = { "settings": { "method": "3phase", "sky_basis": "r1", @@ -225,7 +228,7 @@ cfg = fr.WorkflowConfig.from_dict(dict) "windows": { "window_1": { "file": "window_1.rad", - "matrix_file": "blinds.xml" + "matrix_file": "window_1.xml" } }, "materials": { @@ -245,4 +248,17 @@ cfg = fr.WorkflowConfig.from_dict(dict) } ``` +!!! tips "Use an EnergyPlus model to set up a workflow configuration" + You can use the `epjson_to_rad()` function to convert an EnergyPlus model to a Radiance model. The function returns a dictionary of the Radiance model for each exterior zone in the EnergyPlus model. You can use the dictionary to set up the workflow configuration. + + ```python + epmodel = fr.EnergyPlusModel("file.idf") # EnergyPlus model + radmodel = fr.epjson_to_rad(epmodel) # Radiance model + dict_zone1 = radmodel["zone_1"] # Get the dictionary of the Radiance model for zone_1. + ``` + + ```python + cfg = fr.WorkflowConfig.from_dict(dict_zone1) + ``` + From a6f3bc95575cc646b03f37d359f3a5b0ee6c78bc Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Thu, 19 Oct 2023 13:39:33 -0700 Subject: [PATCH 04/16] docs(guide8): Update guide8 --- docs/how-to/guide8.md | 132 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 16 deletions(-) diff --git a/docs/how-to/guide8.md b/docs/how-to/guide8.md index 14cc414..d80dee6 100644 --- a/docs/how-to/guide8.md +++ b/docs/how-to/guide8.md @@ -1,25 +1,125 @@ -# How to set up a Three-Phase method workflow with a configuration file? +# How to use the Three-Phase method to calculate eDGPs and workplane illuminance? -This guide will show you how to set up a Three-Phase method workflow with a configuration file. The configuration file is a ???? file that contains all the information needed to run a Three-Phase method workflow. +This guide will show you how to set up a Three-Phase method workflow in Radiance to calculate eDGPs and workplane illuminance. -**Workflow** +To set up a Three-Phase method workflow, call the `ThreePhaseMethod` class by passing in a workflow configuration that contains information about the settings and model. See [How to set up a workflow configuration?](../guide7/) for more information. -1. +**Workflow for setting up a three-phase method** -## +1. Initialize a ThreePhaseMethod instance with a workflow configuration. +2. (Optional) Save the matrices to file. +3. Generate matrices. -cfg = fr.WorkflowConfig() -cfg.Model.scene = -cfg.Model.materials = -cfg.Model.sensors = -cfg.Model.views = +4. Calculate eDGPs or workplane illuminance. -rad_workflow = fr.ThreePhaseMethod(rad_cfg) -print(rad_workflow.mfile) -# Separate run to generate matrices and save to file -# A *.npz file will be generated in the current working directory -rad_workflow.config.settings.save_matrices = True -rad_workflow.generate_matrices(view_matrices=False) \ No newline at end of file +## 0. Import the required classes and functions + +```python +from datetime import datetime +import frads as fr +``` + +## 1. Initialize a ThreePhaseMethod instance with a workflow configuration + + +??? example "cfg" + ```json + dict_1 = { + "settings": { + "method": "3phase", + "sky_basis": "r1", + "epw_file": "", + "wea_file": "oak.wea", + "sensor_sky_matrix": ["-ab", "0"], + "view_sky_matrix": ["-ab", "0"], + "sensor_window_matrix": ["-ab", "0"], + "view_window_matrix": ["-ab", "0"], + "daylight_matrix": ["-ab", "0"] + }, + "model": { + "scene": { + "files": [ + "walls.rad", + "ceiling.rad", + "floor.rad", + "ground.rad" + ] + }, + "windows": { + "window_1": { + "file": "window_1.rad", + "matrix_file": "window_1.xml" + } + }, + "materials": { + "files": ["materials.mat"] + }, + "sensors": { + "sensor_1": {"file": "sensor_1.txt"}, + }, + "views": { + "view_1": { + "file": "view_1.vf", + "xres": 16, + "yres": 16 + } + } + } + } + ``` + + ``` python + cfg = fr.WorkflowConfig.from_dict(dict_1) + ``` + +```python +workflow = fr.ThreePhaseMethod(cfg) +``` + +## 2. (Optional) Save the matrices to file + +A *.npz file will be generated in the current working directory. The file name is a hash string of the configuration content. + +```python +workflow.config.settings.save_matrices = True #default=False +``` + +## 3. Generate matrices +Use the `generate_matrices()` method to generate the following matrices: + +- View --> window +- Sensor --> window +- Daylight + +```python +workflow.generate_matrices() +``` + +## 4.1 Calculate eDGPs + +```python +workflow.calculate_edgps( + view="view_1", + shades=["window_1.rad"], #shade geometry files + bsdf=workflow.window_bsdfs["window_1"], #shade BSDF + date_time=datetime(2023, 1, 1, 12), + dni=800, + dhi=100, + ambient_bounce=1, +) +``` + +## 4.2 Calculate workplane illuminance + +```python +workflow.calculate_sensor( + sensor="sensor_1", + bsdf=rad_workflow.window_bsdfs["window_1"], # shade BSDF + date_time=datetime(2023, 1, 1, 12), + dni=800, + dhi=100, +) +``` \ No newline at end of file From e756e6440494573b3347aa546bdc3d52002a3b47 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Mon, 8 Jan 2024 17:40:12 -0800 Subject: [PATCH 05/16] refactor(methods): fix windowconfig --- frads/methods.py | 174 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 40 deletions(-) diff --git a/frads/methods.py b/frads/methods.py index 28aa561..c67ac08 100755 --- a/frads/methods.py +++ b/frads/methods.py @@ -85,7 +85,9 @@ def __post_init__(self): @dataclass class MatrixConfig: matrix_file: Union[str, Path] = "" - matrix_data: np.ndarray = field(default_factory=lambda: np.ones((145, 145, 3))) + matrix_data: np.ndarray = field( + default_factory=lambda: np.ones((145, 145, 3)) + ) def __post_init__(self): if self.matrix_data is None: @@ -162,8 +164,11 @@ def __post_init__(self): if not isinstance(self.file, Path): self.file = Path(self.file) if self.bytes == b"": - with open(self.file, "rb") as f: - self.bytes = f.read() + if self.file != "": + with open(self.file) as f: + self.bytes = f.read() + else: + raise ValueError("WindowConfig must have either file or bytes") @dataclass @@ -198,7 +203,8 @@ def __post_init__(self): if self.file != "": with open(self.file) as f: self.data = [ - [float(i) for i in line.split()] for line in f.readlines() + [float(i) for i in line.split()] + for line in f.readlines() ] else: raise ValueError("SensorConfig must have either file or data") @@ -339,7 +345,16 @@ class Settings: default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"] ) surface_window_matrix: List[str] = field( - default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5", "-c", "10000"] + default_factory=lambda: [ + "-ab", + "5", + "-ad", + "8192", + "-lw", + "5e-5", + "-c", + "10000", + ] ) view_window_matrix: List[str] = field( default_factory=lambda: ["-ab", "5", "-ad", "8192", "-lw", "5e-5"] @@ -415,7 +430,9 @@ def __post_init__(self): self.settings = Settings(**self.settings) if isinstance(self.model, dict): self.model = Model(**self.model) - self.hash_str = hashlib.md5(str(self.__dict__).encode()).hexdigest()[:16] + self.hash_str = hashlib.md5(str(self.__dict__).encode()).hexdigest()[ + :16 + ] @staticmethod def from_dict(obj: Dict[str, Any]) -> "WorkflowConfig": @@ -485,12 +502,16 @@ def __init__(self, config: WorkflowConfig): with open(self.config.settings.epw_file) as f: self.wea_metadata, self.wea_data = parse_epw(f.read()) self.wea_header = self.wea_metadata.wea_header() - self.wea_str = self.wea_header + "\n".join(str(d) for d in self.wea_data) + self.wea_str = self.wea_header + "\n".join( + str(d) for d in self.wea_data + ) elif self.config.settings.wea_file != "": with open(self.config.settings.wea_file) as f: self.wea_metadata, self.wea_data = parse_wea(f.read()) self.wea_header = self.wea_metadata.wea_header() - self.wea_str = self.wea_header + "\n".join(str(d) for d in self.wea_data) + self.wea_str = self.wea_header + "\n".join( + str(d) for d in self.wea_data + ) else: if ( self.config.settings.latitude is None @@ -555,7 +576,13 @@ def calculate_view(self, view, time, dni, dhi): def calculate_sensor(self, sensor, time, dni, dhi): raise NotImplementedError - def get_sky_matrix(self, time: Union[datetime, List[datetime]], dni: Union[float, List[float]], dhi: Union[float, List[float]], solar_spectrum: bool = False) -> np.ndarray: + def get_sky_matrix( + self, + time: Union[datetime, List[datetime]], + dni: Union[float, List[float]], + dhi: Union[float, List[float]], + solar_spectrum: bool = False, + ) -> np.ndarray: """Generates a sky matrix based on the time, Direct Normal Irradiance (DNI), and Diffuse Horizontal Irradiance (DHI). @@ -570,14 +597,24 @@ def get_sky_matrix(self, time: Union[datetime, List[datetime]], dni: Union[float """ _wea = self.wea_header _ncols = 1 - if isinstance(time, datetime) and isinstance(dni, (float, int)) and isinstance(dhi, (float, int)): + if ( + isinstance(time, datetime) + and isinstance(dni, (float, int)) + and isinstance(dhi, (float, int)) + ): _wea += str(WeaData(time, dni, dhi)) - elif isinstance(time, list) and isinstance(dni, list) and isinstance(dhi, list): + elif ( + isinstance(time, list) + and isinstance(dni, list) + and isinstance(dhi, list) + ): rows = [str(WeaData(t, n, d)) for t, n, d in zip(time, dni, dhi)] _wea += "\n".join(rows) _ncols = len(time) else: - raise ValueError("Time, DNI, and DHI must be either single values or lists of values") + raise ValueError( + "Time, DNI, and DHI must be either single values or lists of values" + ) smx = pr.gendaymtx( _wea.encode(), outform="d", @@ -593,7 +630,9 @@ def get_sky_matrix(self, time: Union[datetime, List[datetime]], dni: Union[float dtype="d", ) - def get_sky_matrix_from_wea(self, mfactor: int, sun_only=False, onesun=False): + def get_sky_matrix_from_wea( + self, mfactor: int, sun_only=False, onesun=False + ): if self.wea_str is None: raise ValueError("No weather string available") _sun_str = pr.gendaymtx( @@ -616,7 +655,9 @@ def get_sky_matrix_from_wea(self, mfactor: int, sun_only=False, onesun=False): daylight_hours_only=True, mfactor=mfactor, ) - _nrows, _ncols, _ncomp, _dtype = parse_rad_header(pr.getinfo(_matrix).decode()) + _nrows, _ncols, _ncomp, _dtype = parse_rad_header( + pr.getinfo(_matrix).decode() + ) return load_binary_matrix( _matrix, nrows=_nrows, @@ -708,7 +749,9 @@ def calculate_view( A image as a numpy array """ sky_matrix = self.get_sky_matrix(time, dni, dhi) - return matrix_multiply_rgb(self.view_sky_matrices[view].array, sky_matrix) + return matrix_multiply_rgb( + self.view_sky_matrices[view].array, sky_matrix + ) def calculate_sensor( self, sensor: str, time: datetime, dni: float, dhi: float @@ -752,7 +795,11 @@ def calculate_view_from_wea(self, view: str) -> np.ndarray: 3, ) final = np.memmap( - f"{view}_2ph.dat", shape=shape, dtype=np.float64, mode="w+", order="F" + f"{view}_2ph.dat", + shape=shape, + dtype=np.float64, + mode="w+", + order="F", ) for idx in range(0, sky_matrix.shape[1], chunksize): end = min(idx + chunksize, sky_matrix.shape[1]) @@ -780,7 +827,9 @@ def calculate_sensor_from_wea(self, sensor: str) -> np.ndarray: raise ValueError("No wea data available") return matrix_multiply_rgb( self.sensor_sky_matrices[sensor].array, - self.get_sky_matrix_from_wea(int(self.config.settings.sky_basis[-1])), + self.get_sky_matrix_from_wea( + int(self.config.settings.sky_basis[-1]) + ), weights=[47.4, 119.9, 11.6], ) @@ -817,7 +866,8 @@ def __init__(self, config): pr.oconv( *config.model.materials.files, *config.model.scene.files, - stdin=config.model.materials.bytes + config.model.scene.bytes, + stdin=config.model.materials.bytes + + config.model.scene.bytes, ) ) self.window_senders: Dict[str, SurfaceSender] = {} @@ -944,7 +994,9 @@ def calculate_view( res = [] if isinstance(bsdf, list): if len(bsdf) != len(self.config.model.windows): - raise ValueError("Number of BSDF should match number of windows.") + raise ValueError( + "Number of BSDF should match number of windows." + ) for idx, _name in enumerate(self.config.model.windows): _bsdf = bsdf[idx] if isinstance(bsdf, list) else bsdf res.append( @@ -980,9 +1032,13 @@ def calculate_sensor( res = [] if isinstance(bsdf, list): if len(bsdf) != len(self.config.model.windows): - raise ValueError("Number of BSDF should match number of windows.") + raise ValueError( + "Number of BSDF should match number of windows." + ) for idx, _name in enumerate(self.config.model.windows): - _bsdf = self.config.model.materials.matrices[bsdf[_name]].matrix_data + _bsdf = self.config.model.materials.matrices[ + bsdf[_name] + ].matrix_data res.append( matrix_multiply_rgb( self.sensor_window_matrices[sensor].array[idx], @@ -1015,7 +1071,11 @@ def calculate_view_from_wea(self, view: str) -> np.ndarray: 3, ) final = np.memmap( - f"{view}_3ph.dat", shape=shape, dtype=np.float64, mode="w+", order="F" + f"{view}_3ph.dat", + shape=shape, + dtype=np.float64, + mode="w+", + order="F", ) for idx in range(0, sky_matrix.shape[1], chunksize): end = min(idx + chunksize, sky_matrix.shape[1]) @@ -1086,12 +1146,18 @@ def calculate_surface( ) -> np.ndarray: weights = [47.4, 119.9, 11.6] if solar_spectrum: - weights = [1., 1., 1.] + weights = [1.0, 1.0, 1.0] if sky_matrix is None: - sky_matrix = self.get_sky_matrix(time, dni, dhi, solar_spectrum=solar_spectrum) - res = np.zeros((self.surface_senders[surface].yres, sky_matrix.shape[1])) + sky_matrix = self.get_sky_matrix( + time, dni, dhi, solar_spectrum=solar_spectrum + ) + res = np.zeros( + (self.surface_senders[surface].yres, sky_matrix.shape[1]) + ) for idx, _name in enumerate(self.config.model.windows): - _bsdf = self.config.model.materials.matrices[bsdf[_name]].matrix_data + _bsdf = self.config.model.materials.matrices[ + bsdf[_name] + ].matrix_data res += matrix_multiply_rgb( self.surface_window_matrices[surface].array[idx], _bsdf, @@ -1141,7 +1207,9 @@ def calculate_edgps( gmaterial = _gms[sname] stdins.append(gmaterial.bytes) for prim in self.window_senders[wname].surfaces: - stdins.append(replace(prim, modifier=gmaterial.identifier).bytes) + stdins.append( + replace(prim, modifier=gmaterial.identifier).bytes + ) if (_pgs := self.config.model.windows[wname].proxy_geometry) != {}: for prim in _pgs[sname]: stdins.append(prim.bytes) @@ -1231,7 +1299,9 @@ def __init__(self, config: WorkflowConfig): pr.oconv( *config.model.materials.files, *config.model.scene.files, - stdin=(config.model.materials.bytes + config.model.scene.bytes), + stdin=( + config.model.materials.bytes + config.model.scene.bytes + ), ) ) self.blacked_out_octree: Path = self.octdir / f"{random_string(5)}.oct" @@ -1252,7 +1322,9 @@ def __init__(self, config: WorkflowConfig): self.vmap: Dict[str, np.ndarray] = {} self.cdmap: Dict[str, np.ndarray] = {} self.direct_sun_matrix: np.ndarray = self.get_sky_matrix_from_wea( - mfactor=int(self.config.settings.sun_basis[-1]), onesun=True, sun_only=True + mfactor=int(self.config.settings.sun_basis[-1]), + onesun=True, + sun_only=True, ) self._prepare_window_objects() self._prepare_sun_receivers() @@ -1267,7 +1339,9 @@ def _gen_blacked_out_octree(self): pr.xform(s, modifier="black") for s in self.config.model.scene.files ) if self.config.model.scene.bytes != b"": - black_scene += pr.xform(self.config.model.scene.bytes, modifier="black") + black_scene += pr.xform( + self.config.model.scene.bytes, modifier="black" + ) black = pr.Primitive("void", "plastic", "black", [], [0, 0, 0, 0, 0]) glow = pr.Primitive("void", "glow", "glowing", [], [1, 1, 1, 0]) with open(self.blacked_out_octree, "wb") as f: @@ -1338,7 +1412,9 @@ def _prepare_view_sender_objects(self): sender, list(self.window_receivers.values()), self.octree ) self.view_window_direct_matrices[_v] = Matrix( - sender, list(self.window_receivers.values()), self.blacked_out_octree + sender, + list(self.window_receivers.values()), + self.blacked_out_octree, ) self.view_sun_direct_matrices[_v] = SunMatrix( sender, self.view_sun_receiver, self.blacked_out_octree @@ -1353,7 +1429,9 @@ def _prepare_sensor_sender_objects(self): sender, list(self.window_receivers.values()), self.octree ) self.sensor_window_direct_matrices[_s] = Matrix( - sender, list(self.window_receivers.values()), self.blacked_out_octree + sender, + list(self.window_receivers.values()), + self.blacked_out_octree, ) self.sensor_sun_direct_matrices[_s] = SunMatrix( sender, self.sensor_sun_receiver, self.blacked_out_octree @@ -1365,7 +1443,9 @@ def _prepare_sun_receivers(self): parse_polygon(r.surfaces[0]).normal.tobytes() for r in self.window_receivers.values() ] - unique_window_normals = [np.frombuffer(arr) for arr in set(window_normals)] + unique_window_normals = [ + np.frombuffer(arr) for arr in set(window_normals) + ] self.sensor_sun_receiver = SunReceiver( self.config.settings.sun_basis, sun_matrix=self.direct_sun_matrix, @@ -1417,10 +1497,14 @@ def _prepare_mapping_octrees(self): blacked_out_windows = str(black) + " ".join(blacked_out_windows) glowing_windows = str(glow) + " ".join(glowing_windows) with open(self.vmap_oct, "wb") as wtr: - wtr.write(pr.oconv(stdin=glowing_windows.encode(), octree=self.octree)) + wtr.write( + pr.oconv(stdin=glowing_windows.encode(), octree=self.octree) + ) logger.info("Generating view matrix material map octree") with open(self.cdmap_oct, "wb") as wtr: - wtr.write(pr.oconv(stdin=blacked_out_windows.encode(), octree=self.octree)) + wtr.write( + pr.oconv(stdin=blacked_out_windows.encode(), octree=self.octree) + ) def generate_matrices(self): if self.mfile.exists(): @@ -1505,7 +1589,11 @@ def calculate_view_from_wea(self, view: str): 3, ) res = np.memmap( - f"{view}_5ph.dat", shape=shape, dtype=np.float64, mode="w+", order="F" + f"{view}_5ph.dat", + shape=shape, + dtype=np.float64, + mode="w+", + order="F", ) for idx in range(0, sky_matrix.shape[1], chunksize): end = min(idx + chunksize, sky_matrix.shape[1]) @@ -1518,7 +1606,8 @@ def calculate_view_from_wea(self, view: str): ) tdsmx = np.dot(tdmx, sky_matrix[:, idx:end, c]) vtdsmx = np.dot( - self.view_window_matrices[view].array[widx][:, :, c], tdsmx + self.view_window_matrices[view].array[widx][:, :, c], + tdsmx, ) tdmx = np.dot( csr_matrix(self.window_bsdfs[_name][:, :, c]), @@ -1526,7 +1615,8 @@ def calculate_view_from_wea(self, view: str): ) tdsmx = np.dot(tdmx, direct_sky_matrix[c][:, idx:end]) vtdsmx_d = np.dot( - self.view_window_direct_matrices[view].array[widx][c], tdsmx + self.view_window_direct_matrices[view].array[widx][c], + tdsmx, ) _res[c].append(vtdsmx - vtdsmx_d.toarray()) for c in range(3): @@ -1553,7 +1643,9 @@ def calculate_sensor_from_wea(self, sensor): ) direct_sky_matrix = to_sparse_matrix3(direct_sky_matrix) res3 = np.zeros((self.sensor_senders[sensor].yres, sky_matrix.shape[1])) - res3d = np.zeros((self.sensor_senders[sensor].yres, sky_matrix.shape[1])) + res3d = np.zeros( + (self.sensor_senders[sensor].yres, sky_matrix.shape[1]) + ) for idx, _name in enumerate(self.config.model.windows): res3 += matrix_multiply_rgb( self.sensor_window_matrices[sensor].array[idx], @@ -1569,7 +1661,9 @@ def calculate_sensor_from_wea(self, sensor): direct_sky_matrix, weights=[47.4, 119.9, 11.6], ) - rescd = np.zeros((self.sensor_senders[sensor].yres, sky_matrix.shape[1])) + rescd = np.zeros( + (self.sensor_senders[sensor].yres, sky_matrix.shape[1]) + ) for c, w in enumerate([47.4, 119.9, 11.6]): rescd += w * np.dot( self.sensor_sun_direct_matrices[sensor].array[c], From 91583fe3a2b1d7540fe3646478c80a77d7cad2f0 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Mon, 8 Jan 2024 19:44:03 -0800 Subject: [PATCH 06/16] fix(methods): fix viewconfig --- frads/methods.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frads/methods.py b/frads/methods.py index c67ac08..8049361 100755 --- a/frads/methods.py +++ b/frads/methods.py @@ -221,12 +221,15 @@ class ViewConfig: def __post_init__(self): if self.file != "": self.file_mtime = os.path.getmtime(self.file) - if not isinstance(self.file, Path): - self.file = Path(self.file) - if self.file.exists() and self.view == "": + if not isinstance(self.file, Path): + self.file = Path(self.file) + if os.path.exists(self.file) and self.view == "": self.view = pr.load_views(self.file)[0] - elif not isinstance(self.view, pr.View): - self.view = parse_view(self.view) + elif self.view != "": + if not isinstance(self.view, pr.View): + self.view = parse_view(self.view) + else: + raise ValueError("ViewConfig must have a file or view") @dataclass From a79c0c944f08a9dc51239ce7b96ae3c98a3b02df Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Tue, 9 Jan 2024 17:46:46 -0800 Subject: [PATCH 07/16] fix(methods): wea_data --- frads/methods.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frads/methods.py b/frads/methods.py index 8049361..b7908a7 100755 --- a/frads/methods.py +++ b/frads/methods.py @@ -165,7 +165,7 @@ def __post_init__(self): self.file = Path(self.file) if self.bytes == b"": if self.file != "": - with open(self.file) as f: + with open(self.file, "rb") as f: self.bytes = f.read() else: raise ValueError("WindowConfig must have either file or bytes") @@ -539,6 +539,7 @@ def __init__(self, config: WorkflowConfig): self.config.settings.time_zone, self.config.settings.site_elevation, ) + self.wea_data = None # Setup Temp and Octrees directory self.tmpdir = Path("Temp") From ae92589892272eb7c59ff7f491379e687ec156ed Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 10 Jan 2024 14:15:50 -0800 Subject: [PATCH 08/16] feat(init): add api --- frads/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frads/__init__.py b/frads/__init__.py index 6617a7a..bc9b019 100755 --- a/frads/__init__.py +++ b/frads/__init__.py @@ -76,6 +76,13 @@ TwoPhaseMethod, ThreePhaseMethod, FivePhaseMethod, + Model, + Settings, + SceneConfig, + ViewConfig, + WindowConfig, + SensorConfig, + MaterialConfig, ) from .sky import ( @@ -143,4 +150,11 @@ "parse_wea", "surfaces_view_factor", "unpack_primitives", + "Settings", + "Model", + "SceneConfig", + "ViewConfig", + "WindowConfig", + "SensorConfig", + "MaterialConfig", ] From 295193a917886656dd6885b481383f38ca6313da Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Mon, 22 Jan 2024 12:47:39 -0800 Subject: [PATCH 09/16] feat(extracss): edit header style --- docs/css/extra.css | 17 +++++++++++++++++ mkdocs.yml | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/css/extra.css diff --git a/docs/css/extra.css b/docs/css/extra.css new file mode 100644 index 0000000..cfeaed5 --- /dev/null +++ b/docs/css/extra.css @@ -0,0 +1,17 @@ +h1 { + color: #63666A; +} + +h2 { + color: #00313C; +} + +h3 { + color: #007681; +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3 { + font-weight: bold; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index a82fd7f..1481344 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,7 +32,8 @@ theme: - content.code.annotate - announce.dismiss - navigation.tabs - + - content.code.copy +extra_css: [css/extra.css] markdown_extensions: - admonition - attr_list @@ -47,6 +48,8 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format - def_list - pymdownx.details + - pymdownx.tabbed: + alternate_style: true plugins: - search From 9db31075122e489d40a55586e8e8773f1da0b8c8 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Mon, 22 Jan 2024 13:48:13 -0800 Subject: [PATCH 10/16] feat(window):create glazing system takes in str --- frads/window.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frads/window.py b/frads/window.py index b2c8490..3868065 100644 --- a/frads/window.py +++ b/frads/window.py @@ -243,7 +243,7 @@ def create_pwc_gaps(gaps: List[Gap]): def create_glazing_system( name: str, - layers: List[Union[Path, bytes]], + layers: List[Union[Path, str, bytes]], gaps: Optional[List[Gap]] = None, ) -> GlazingSystem: """Create a glazing system from a list of layers and gaps. @@ -274,13 +274,15 @@ def create_glazing_system( 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)) + if isinstance(layer, str) or isinstance(layer, Path): + if isinstance(layer, Path): + layer = str(layer) + if layer.endswith(".json"): + product_data = pwc.parse_json_file(layer) + elif layer.endswith(".xml"): + product_data = pwc.parse_bsdf_xml_file(layer) else: - product_data = pwc.parse_optics_file(str(layer)) + product_data = pwc.parse_optics_file(layer) elif isinstance(layer, bytes): try: product_data = pwc.parse_json(layer) From bebfac8f28bb47c96f3e12112777acf23e4b9565 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 24 Jan 2024 12:55:54 -0800 Subject: [PATCH 11/16] breaking(eplus_model):add_lighting needs lighting level --- frads/eplus_model.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/frads/eplus_model.py b/frads/eplus_model.py index e71e6e1..7e756d1 100644 --- a/frads/eplus_model.py +++ b/frads/eplus_model.py @@ -59,7 +59,9 @@ def add_glazing_system(self, glzsys: GlazingSystem): gas=gap.gas[0].gas.capitalize(), thickness=gap.thickness ) ) - layer_inputs: List[epmodel.ConstructionComplexFenestrationStateLayerInput] = [] + layer_inputs: List[ + epmodel.ConstructionComplexFenestrationStateLayerInput + ] = [] for i, layer in enumerate(glzsys.layers): layer_inputs.append( epmodel.ConstructionComplexFenestrationStateLayerInput( @@ -70,8 +72,12 @@ 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_front_absorptance[i], - directional_absorptance_back=glzsys.solar_back_absorptance[i], + directional_absorptance_front=glzsys.solar_front_absorptance[ + i + ], + directional_absorptance_back=glzsys.solar_back_absorptance[ + i + ], ) ) input = epmodel.ConstructionComplexFenestrationStateInput( @@ -84,11 +90,14 @@ def add_glazing_system(self, glzsys: GlazingSystem): ) self.add_construction_complex_fenestration_state(name, input) - def add_lighting(self, zone: str, replace: bool = False): + def add_lighting( + self, zone: str, lighting_level: float, replace: bool = False + ): """Add lighting object to EnergyPlusModel's epjs dictionary. Args: zone: Zone name to add lighting to. + lighting_level: Lighting level in Watts. replace: If True, replace existing lighting object in zone. Raises: @@ -96,7 +105,7 @@ def add_lighting(self, zone: str, replace: bool = False): ValueError: If lighting already exists in zone and replace is False. Examples: - >>> model.add_lighting("Zone1") + >>> model.add_lighting("Zone1", 10) """ if self.zone is None: raise ValueError("Zone not found in model.") @@ -132,10 +141,10 @@ def add_lighting(self, zone: str, replace: bool = False): # Add lighting schedule to epjs dictionary self.add( "schedule_constant", - "constant_off", + "constant_on", epm.ScheduleConstant( schedule_type_limits_name="on_off", - hourly_value=0, + hourly_value=1, ), ) @@ -148,15 +157,18 @@ def add_lighting(self, zone: str, replace: bool = False): fraction_radiant=0, fraction_replaceable=1, fraction_visible=1, - lighting_level=0, + lighting_level=lighting_level, return_air_fraction=0, - schedule_name="constant_off", + schedule_name="constant_on", zone_or_zonelist_or_space_or_spacelist_name=zone, ), ) def add_output( - self, output_type: str, output_name: str, reporting_frequency: str = "Timestep" + self, + output_type: str, + output_name: str, + reporting_frequency: str = "Timestep", ): """Add an output variable or meter to the epjs dictionary. From b201fc6376736d4f022e25ad2515f3db850e3431 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 24 Jan 2024 14:01:55 -0800 Subject: [PATCH 12/16] refactor(window):create_glazing_system --- frads/window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frads/window.py b/frads/window.py index 3868065..18e64af 100644 --- a/frads/window.py +++ b/frads/window.py @@ -1,5 +1,6 @@ from dataclasses import asdict, dataclass, field import json +import os from pathlib import Path import tempfile from typing import List, Optional, Tuple, Union @@ -275,6 +276,8 @@ def create_glazing_system( for layer in layers: product_data = None if isinstance(layer, str) or isinstance(layer, Path): + if not os.path.isfile(layer): + raise FileNotFoundError(f"{layer} does not exist.") if isinstance(layer, Path): layer = str(layer) if layer.endswith(".json"): From 3461fbdc3c4d8684f33e0abf2cfc9796b47bc7e7 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 24 Jan 2024 14:54:01 -0800 Subject: [PATCH 13/16] breaking(methods): config update --- frads/ep2rad.py | 68 ++++++++++++++++++++++++++--------- frads/methods.py | 93 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 44 deletions(-) diff --git a/frads/ep2rad.py b/frads/ep2rad.py index 0e924fa..99e6d27 100755 --- a/frads/ep2rad.py +++ b/frads/ep2rad.py @@ -145,7 +145,13 @@ def surface_to_polygon(srf: BuildingSurfaceDetailed) -> Polygon: if srf.vertices is None: raise ValueError("Surface has no vertices.") vertices = [ - np.array((v.vertex_x_coordinate, v.vertex_y_coordinate, v.vertex_z_coordinate)) + np.array( + ( + v.vertex_x_coordinate, + v.vertex_y_coordinate, + v.vertex_z_coordinate, + ) + ) for v in srf.vertices ] return Polygon(vertices) @@ -214,7 +220,9 @@ def parse_material(name: str, material: Material) -> EPlusOpaqueMaterial: ) -def parse_material_no_mass(name: str, material: MaterialNoMass) -> EPlusOpaqueMaterial: +def parse_material_no_mass( + name: str, material: MaterialNoMass +) -> EPlusOpaqueMaterial: """Parse EP Material:NoMass""" name = name.replace(" ", "_") roughness = material.roughness.value @@ -246,7 +254,9 @@ def parse_window_material_simple_glazing_system( shgc = material.solar_heat_gain_coefficient tmit = material.visible_transmittance or shgc tmis = tmit2tmis(tmit) - primitive = pr.Primitive("void", "glass", identifier, [], [tmis, tmis, tmis]) + primitive = pr.Primitive( + "void", "glass", identifier, [], [tmis, tmis, tmis] + ) return EPlusWindowMaterial(identifier, tmit, primitive) @@ -259,9 +269,13 @@ def parse_window_material_glazing( if material.optical_data_type.value.lower() == "bsdf": tmit = 1 else: - tmit = material.visible_transmittance_at_normal_incidence or default_tmit + tmit = ( + material.visible_transmittance_at_normal_incidence or default_tmit + ) tmis = tmit2tmis(tmit) - primitive = pr.Primitive("void", "glass", identifier, [], [tmis, tmis, tmis]) + primitive = pr.Primitive( + "void", "glass", identifier, [], [tmis, tmis, tmis] + ) return EPlusWindowMaterial(identifier, tmit, primitive) @@ -331,16 +345,22 @@ def _parse_construction(self) -> dict: cname, "default", layers, - sum(self.materials[layer.lower()].thickness for layer in layers), + sum( + self.materials[layer.lower()].thickness for layer in layers + ), ) - cfs, matrices = parse_construction_complex_fenestration_state(self.model) + cfs, matrices = parse_construction_complex_fenestration_state( + self.model + ) for key, val in matrices.items(): nested = [] mtx = val["tvf"] for i in range(0, len(mtx["values"]), mtx["nrows"]): nested.append(mtx["values"][i : i + mtx["ncolumns"]]) self.matrices[key] = { - "matrix_data": [[[ele, ele, ele] for ele in row] for row in nested] + "matrix_data": [ + [[ele, ele, ele] for ele in row] for row in nested + ] } constructions.update(cfs) return constructions @@ -380,12 +400,18 @@ def _process_zone(self, zone_name: str) -> dict: surfaces_fenestrations = self._pair_surfaces_fenestrations( surfaces, fenestrations ) - surface_polygons = [surface_to_polygon(srf) for srf in surfaces.values()] + surface_polygons = [ + surface_to_polygon(srf) for srf in surfaces.values() + ] center = polygon_center(*surface_polygons) view_direction = np.array([0.0, 0.0, 0.0]) for sname, swnf in surfaces_fenestrations.items(): opaque_surface_polygon = surface_to_polygon(swnf.surface) - _surface, _surface_fenestrations, window_polygons = self._process_surface( + ( + _surface, + _surface_fenestrations, + window_polygons, + ) = self._process_surface( sname, opaque_surface_polygon, swnf.surface.construction_name, @@ -397,7 +423,9 @@ def _process_zone(self, zone_name: str) -> dict: if swnf.surface.surface_type == SurfaceType.floor: sensors[sname] = { "data": gen_grid( - polygon=opaque_surface_polygon, height=0.76, spacing=0.61 + polygon=opaque_surface_polygon, + height=0.76, + spacing=0.61, ) } for window_polygon in window_polygons: @@ -409,7 +437,9 @@ def _process_zone(self, zone_name: str) -> dict: horiz=180, vert=180, ) - sensors[zone_name] = {"data": [center.tolist() + view_direction.tolist()]} + sensors[zone_name] = { + "data": [center.tolist() + view_direction.tolist()] + } return { "scene": {"bytes": b" ".join(scene)}, @@ -458,7 +488,7 @@ def _process_surface( surface_polygon -= fenestration_polygon windows[fname] = {"bytes": window.bytes} if fene.construction_name in self.matrices: - windows[fname]["matrix_file"] = fene.construction_name + windows[fname]["matrix_name"] = fene.construction_name # polygon to primitive construction = self.constructions[surface_construction_name] inner_material_name = construction.layers[-1].replace(" ", "_") @@ -470,7 +500,9 @@ def _process_surface( # extrude the surface by thickness if fenestrations != {}: - facade = thicken(surface_polygon, window_polygons, construction.thickness) + facade = thicken( + surface_polygon, window_polygons, construction.thickness + ) outer_material_name = construction.layers[0].replace(" ", "_") scene.append( polygon_primitive( @@ -521,7 +553,9 @@ def _pair_surfaces_fenestrations( for fname, fen in zone_fenestrations.items(): if fen.building_surface_name == sname: named_fen[fname] = fen - surface_fenestrations[sname] = SurfaceWithNamedFenestrations(srf, named_fen) + surface_fenestrations[sname] = SurfaceWithNamedFenestrations( + srf, named_fen + ) return surface_fenestrations def _process_fenestration( @@ -591,7 +625,9 @@ def create_settings(ep_model: EnergyPlusModel, epw_file: Optional[str]) -> dict: def epmodel_to_radmodel( - ep_model: EnergyPlusModel, epw_file: Optional[str] = None, add_views: bool = True + ep_model: EnergyPlusModel, + epw_file: Optional[str] = None, + add_views: bool = True, ) -> dict: """Convert EnergyPlus model to Radiance models where each zone is a separate model. diff --git a/frads/methods.py b/frads/methods.py index b7908a7..e52cb70 100755 --- a/frads/methods.py +++ b/frads/methods.py @@ -70,6 +70,9 @@ class SceneConfig: materials.update(parse_window_material_blind(material)) bytes: A raw data string to be used as the scene. files_mtime: Files last modification time. + + Raises: + ValueError: If both files and bytes are empty. """ files: List[Path] = field(default_factory=list) @@ -80,6 +83,8 @@ def __post_init__(self): if len(self.files) > 0: for fpath in self.files: self.files_mtime.append(os.path.getmtime(fpath)) + if self.bytes == b"" and len(self.files) == 0: + raise ValueError("SceneConfig must have either file or bytes") @dataclass @@ -116,7 +121,12 @@ class MaterialConfig: Attributes: file: A file to be used as the material. bytes: A raw data string to be used as the material. + matrices: A dictionary of matrix files/data. + glazing_materials: A dictionary of glazing materials used for edgps calculations. file_mtime: File last modification time. + + Raises: + ValueError: If file, bytes, and matrices are empty. """ files: List[Path] = field(default_factory=list) @@ -132,6 +142,15 @@ def __post_init__(self): for k, v in self.matrices.items(): if isinstance(v, dict): self.matrices[k] = MatrixConfig(**v) + if ( + self.bytes == b"" + and len(self.files) == 0 + and len(self.matrices) == 0 + and len(self.glazing_materials) == 0 + ): + raise ValueError( + "MaterialConfig must have either file, bytes, matrices, or glazing_materials" + ) @dataclass @@ -147,14 +166,14 @@ class WindowConfig: Attributes: file: A file to be used as the window group. bytes: A raw data string to be used as the window group. - shading_geometry_file: A file to be used as the shading geometry. - shading_geometry_bytes: A raw data string to be used as the shading geometry. + matrix_name: A matrix name to be used for the window group. + proxy_geometry: A raw data string to be used as the shading geometry. files_mtime: Files last modification time. """ file: Union[str, Path] = "" bytes: bytes = b"" - matrix_file: str = "" + matrix_name: str = "" proxy_geometry: Dict[str, List[pr.Primitive]] = field(default_factory=dict) files_mtime: List[float] = field(init=False, default_factory=list) @@ -383,31 +402,35 @@ class Model: views: A dictionary of ViewConfig """ - scene: "SceneConfig" - windows: Dict[str, "WindowConfig"] materials: "MaterialConfig" - sensors: Dict[str, "SensorConfig"] - views: Dict[str, "ViewConfig"] + scene: "SceneConfig" = field(default_factory=dict) + windows: Dict[str, "WindowConfig"] = field(default_factory=dict) + sensors: Dict[str, "SensorConfig"] = field(default_factory=dict) + views: Dict[str, "ViewConfig"] = field(default_factory=dict) surfaces: Dict[str, "SurfaceConfig"] = field(default_factory=dict) # Make Path() out of all path strings def __post_init__(self): - if isinstance(self.scene, dict): + if isinstance(self.scene, dict) and len(self.scene) > 0: self.scene = SceneConfig(**self.scene) if isinstance(self.materials, dict): self.materials = MaterialConfig(**self.materials) - for k, v in self.windows.items(): - if isinstance(v, dict): - self.windows[k] = WindowConfig(**v) - for k, v in self.sensors.items(): - if isinstance(v, dict): - self.sensors[k] = SensorConfig(**v) - for k, v in self.views.items(): - if isinstance(v, dict): - self.views[k] = ViewConfig(**v) - for k, v in self.surfaces.items(): - if isinstance(v, dict): - self.surfaces[k] = SurfaceConfig(**v) + if isinstance(self.windows, dict) and len(self.windows) > 0: + for k, v in self.windows.items(): + if isinstance(v, dict): + self.windows[k] = WindowConfig(**v) + if isinstance(self.sensors, dict) and len(self.sensors) > 0: + for k, v in self.sensors.items(): + if isinstance(v, dict): + self.sensors[k] = SensorConfig(**v) + if isinstance(self.views, dict) and len(self.views) > 0: + for k, v in self.views.items(): + if isinstance(v, dict): + self.views[k] = ViewConfig(**v) + if isinstance(self.surfaces, dict) and len(self.surfaces) > 0: + for k, v in self.surfaces.items(): + if isinstance(v, dict): + self.surfaces[k] = SurfaceConfig(**v) @dataclass @@ -429,6 +452,18 @@ class WorkflowConfig: hash_str: str = field(init=False) def __post_init__(self): + if ( + self.model.sensors == {} + and self.model.views == {} + and self.model.surfaces == {} + ): + raise ValueError( + f"Sensors, views, or surfaces must be specified for {self.settings.method} method" + ) + if self.model.scene == {} and self.model.windows == {}: + raise ValueError( + f"Scene or windows must be specified for {self.settings.method} method" + ) if isinstance(self.settings, dict): self.settings = Settings(**self.settings) if isinstance(self.model, dict): @@ -880,9 +915,9 @@ def __init__(self, config): self.daylight_matrices = {} for _name, window in self.config.model.windows.items(): _prims = pr.parse_primitive(window.bytes.decode()) - if window.matrix_file != "": + if window.matrix_name != "": self.window_bsdfs[_name] = self.config.model.materials.matrices[ - window.matrix_file + window.matrix_name ].matrix_data window_basis = [ k @@ -1175,7 +1210,7 @@ def calculate_edgps( self, view: str, bsdf: Dict[str, str], - date_time: datetime, + time: datetime, dni: float, dhi: float, ambient_bounce: int = 1, @@ -1186,7 +1221,7 @@ def calculate_edgps( Args: view: view name, must be in config.model.views bsdf: a dictionary of window name as key and bsdf matrix or matrix name as value - date_time: datetime object + time: datetime object dni: direct normal irradiance dhi: diffuse horizontal irradiance ambient_bounce: ambient bounce, default to 1. Could be set to zero for @@ -1198,7 +1233,7 @@ def calculate_edgps( stdins = [] stdins.append( gen_perez_sky( - date_time, + time, self.wea_metadata.latitude, self.wea_metadata.longitude, self.wea_metadata.timezone, @@ -1234,7 +1269,7 @@ def calculate_edgps( ev = self.calculate_sensor( view, bsdf, - date_time, + time, dni, dhi, ) @@ -1369,8 +1404,10 @@ def _prepare_window_objects(self): self.window_senders[_name] = SurfaceSender( _prims, self.config.settings.window_basis ) - if window.matrix_file != "": - self.window_bsdfs[_name] = load_matrix(window.matrix_file) + if window.matrix_name != "": + self.window_bsdfs[_name] = self.config.model.materials.matrices[ + window.matrix_name + ].matrix_data elif window.matrix_data != []: self.window_bsdfs[_name] = np.array(window.matrix_data) else: From ca6af38e3bc1235045463461c5411108b8937721 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Wed, 24 Jan 2024 14:56:12 -0800 Subject: [PATCH 14/16] docs(guide) --- docs/assets/sda.png | Bin 0 -> 71827 bytes docs/how-to/guide5.md | 332 ----------------------- docs/how-to/guide8.md | 125 --------- docs/how-to/guide_ep1.md | 180 ++++++++++++ docs/how-to/guide_ep2.md | 72 +++++ docs/how-to/guide_ep3.md | 150 ++++++++++ docs/how-to/{guide6.md => guide_rad1.md} | 0 docs/how-to/{guide7.md => guide_rad2.md} | 220 ++++++++------- docs/how-to/guide_rad3.md | 156 +++++++++++ docs/how-to/guide_radep1.md | 58 ++++ docs/how-to/guide_radep2.md | 289 ++++++++++++++++++++ docs/how-to/guide_radep3.md | 172 ++++++++++++ docs/how-to/index.md | 33 ++- 13 files changed, 1225 insertions(+), 562 deletions(-) create mode 100644 docs/assets/sda.png delete mode 100644 docs/how-to/guide5.md delete mode 100644 docs/how-to/guide8.md create mode 100644 docs/how-to/guide_ep1.md create mode 100644 docs/how-to/guide_ep2.md create mode 100644 docs/how-to/guide_ep3.md rename docs/how-to/{guide6.md => guide_rad1.md} (100%) rename docs/how-to/{guide7.md => guide_rad2.md} (50%) create mode 100644 docs/how-to/guide_rad3.md create mode 100644 docs/how-to/guide_radep1.md create mode 100644 docs/how-to/guide_radep2.md create mode 100644 docs/how-to/guide_radep3.md diff --git a/docs/assets/sda.png b/docs/assets/sda.png new file mode 100644 index 0000000000000000000000000000000000000000..ed1e016029cd1582c58e5a6a67a43d7c3fd52453 GIT binary patch literal 71827 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 literal 0 HcmV?d00001 diff --git a/docs/how-to/guide5.md b/docs/how-to/guide5.md deleted file mode 100644 index 7195e30..0000000 --- a/docs/how-to/guide5.md +++ /dev/null @@ -1,332 +0,0 @@ -# How to model dynamic shading control and daylight dimming with EnergyPlus? - - -The example demonstrates how to use a controller function to control the shading state, cooling setpoint temperature, and electric lighting power intensity during simulation. At the beginning of each timestep, EnergyPlus will call the controller function that operates the facade shading state based on exterior solar irradiance, cooling setpoint temperature based on time of day (pre-cooling), and electric lighting power intensity based on occupancy and workplane illuminance (daylight dimming). The workplane illuminance is calculated using the three-phase method through Radiance. - -**Workflow** - -1. [Setup an EnergyPlus Model](#1-setup-an-energyplus-model) - -2. [Setup EnergyPlus Simulation](#2-setup-energyplus-simulation) - - -```mermaid -graph LR - - subgraph IGSDB - A[Step 1.2 glazing products] - B[Step 1.2 shading products] - end - - subgraph frads - - C[Step 1.1 idf/epjs] --> |Initialize an EnergyPlus model| E; - - subgraph Step 2 EnergyPlus Simulation Setup - subgraph Radiance - R[Workplane Illuminance] - end - subgraph EnergyPlus - E[EnergyPlusModel]<--> K[Step 2.2 & 2.3 controller function

* switch shading state
* daylight dimming
* pre-cooling
] - E <--> R - K <--> R; - end - end - - subgraph WincalcEngine - A --> D[Step 1.3 glazing/shading system
for each CFS state]; - B --> D; - D --> |Add glazing systems| E; - end - - L[Step 1.4 lighting systems] --> |Add lighting| E; - - end -``` - -## 0. Import required Python libraries - -```python -from pathlib import Path -import frads as fr - -``` - -!!! tip "Tips: Reference EnergyPlus models and weather files" - The `pyenergyplus.dataset` module contains a dictionary of EnergyPlus models and weather files. The keys are the names of the models and weather files. The values are the file paths to the models and weather files. - - ``` - from pyenergyplus.dataset import ref_models, weather_files - ``` - - -## 1. Setup an EnergyPlus Model -### 1.1 Initialize an EnergyPlus model - -Initialize an EnergyPlus model by calling `load_energyplus_model` and passing in an EnergyPlus model in an idf or epjson file format. - -```python -epmodel = fr.load_energyplus_model(ref_models["medium_office"]) -``` - -or - -```python -epmodel = fr.load_energyplus_model("medium_office.idf") -``` - -### 1.2 Create glazing systems (Complex Fenestration States) - -!!! example "Create four glazing systems for the four electrochromic tinted states" - Each glazing system consists of: - - * One layer of electrochromic glass - * One gap (10% air and 90% argon) at 0.0127 m thickness - * One layer of clear glass - -Create a glazing system by calling `create_glazing_system`, which returns a `GlazingSystem` object. `create_glazing_system` takes in the following arguments: - -* `name`: the name of the glazing system. -* `layers`: a list of file paths to the glazing or shading layers in the glazing system, in order from exterior to interior. Visit the [IGSDB](https://igsdb.lbl.gov/) website to download `.json` files for glazing products and `.xml` files for shading products. -* `gaps`: a list of `Gap` objects. Each `Gap` object consists of a list of `Gas` objects and a float defining the gap thickness. The `Gas` object consists of the gas type and the gas fraction. The gas fraction is a float between 0 and 1. The default gap is air at 0.0127 m thickness. - -```python -gs_ec01 = fr.create_glazing_system( - name="ec01", - layers=[ - Path("products/igsdb_product_7405.json"), - Path("products/CLEAR_3.DAT"), - ], - gaps=[ - fr.Gap( - [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 - ) - ], -) -``` - -??? info "Create glazing systems for the other tinted electrochromic states" - ```python - gs_ec06 = fr.create_glazing_system( - name="ec06", - layers=[ - Path("products/igsdb_product_7407.json"), - Path("products/CLEAR_3.DAT"), - ], - gaps=[ - fr.Gap( - [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 - ) - ], - ) - - gs_ec18 = fr.create_glazing_system( - name="ec18", - layers=[ - Path("products/igsdb_product_7404.json"), - Path("products/CLEAR_3.DAT"), - ], - gaps=[ - fr.Gap( - [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 - ) - ], - ) - - gs_ec60 = fr.create_glazing_system( - name="ec60", - layers=[ - Path("products/igsdb_product_7406.json"), - Path("products/CLEAR_3.DAT"), - ], - gaps=[ - fr.Gap( - [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 - ) - ], - ) - ``` - -### 1.3 Add glazing systems to EnergyPlus model - -Call `add_glazing_system` from the `EnergyPlusModel` class to add glazing systems to the EnergyPlus model. `add_glazing_system` takes in a `GlazingSystem` object. - - -```python -epmodel.add_glazing_system(gs_ec01) -``` -??? info "Add other glazing systems to the EnergyPlus model" - ```python - epmodel.add_glazing_system(gs_ec06) - epmodel.add_glazing_system(gs_ec18) - epmodel.add_glazing_system(gs_ec60) - ``` - -### 1.4 Add lighting systems to EnergyPlus model -Call `add_lighting` from the `EnergyPlusModel` class to add lighting systems to the EnergyPlus model. `add_lighting` takes in the name of the zone to add lighting to and an optional `replace` argument. If `replace` is `True`, the zone's existing lighting system will be replaced by the new lighting system. If `replace` is `False` and the zone already has a lighting system, an error will be raised. The default value of `replace` is `False`. - -```python -epmodel.add_lighting( - zone="Perimeter_bot_ZN_1", - replace=True -) -``` - -## 2. Setup EnergyPlus Simulation - -### 2.1 Initialize EnergyPlus Simulation Setup - -Initialize EnergyPlus simulation setup by calling `EnergyPlusSetup` and passing in an EnergyPlus model and an optional weather file. - -To enable Radiance for daylighting simulation, set `enable_radiance` to `True`. The default value of `enable_radiance` is `False`. - -```python -eps = fr.EnergyPlusSetup( - epmodel, weather_files["usa_ca_san_francisco"], enable_radiance=True -) -``` - -### 2.2 Define control algorithms using a controller function - -The controller function will control the facade shading state, cooling setpoint temperature, and electric lighting power intensity in the EnergyPlus model during simulation. - -!!! example "Controller function" - The example shows how to implement control algorithms for zone "Perimeter_bot_ZN_1", which has window "Perimeter_bot_ZN_1_Wall_South_Window" and lighting "Perimeter_bot_ZN_1_Lights". - - * **Facade CFS state** based on exterior solar irradiance - * **Cooling setpoint temperature** based on time of day (pre-cooling) - * **Electric lighting power intensity** based on occupancy and workplane illuminance (daylight dimming) - -!!! notes "Actuate" - * **Generic actuator** - - Use `EnergyPlusSetup.actuate` to set or update the operating value of an actuator in the EnergyPlus model. `EnergyPlusSetup.actuate` takes in a component type, name, key, and value. The component type is the actuator category, e.g. "Weather Data". The name is the name of the actuator, e.g. "Outdoor Dew Point". The key is the instance of the variable to retrieve, e.g. "Environment". The value is the value to set the actuator to. - - * **Special actuator** - - * **Facade CFS state** - - `EnergyPlusSetup.actuate_cfs_state` takes in a window name and a CFS state (the name of the glazing system). - - * **Heating/Cooling setpoint temperature** - - `EnergyPlusSetup.actuate_heating_setpoint` takes in a zone name and a heating setpoint temperature. - `EnergyPlusSetup.actuate_cooling_setpoint` takes in a zone name and a cooling setpoint temperature. - - * **Electric lighting power intensity** - - `EnergyPlusSetup.actuate_lighting_power` takes in a lighting name and a lighting power intensity. - -!!! notes "Get variable value" - - Access EnergyPlus variable during simulation by using `EnergyPlusSetup.get_variable_value` and passing in a variable name and key - - !!! tip "Tips" - Use `EnergyPlusSetup.get_variable_value` to access the EnergyPlus variable during the simulation and use the variable as a control input. For example, use the exterior solar irradiance to control the facade CFS state. - -The controller function takes in a `state` argument. - -```py linenums="1" hl_lines="2 6 28 44" - -def controller(state): - # check if the api is fully ready - if not eps.api.exchange.api_data_fully_ready(state): - return - - # control facade shading state based on exterior solar irradiance - # get exterior solar irradiance - ext_irradiance = eps.get_variable_value( - name="Surface Outside Face Incident Solar Radiation Rate per Area", - key="Perimeter_bot_ZN_1_Wall_South_Window", - ) - # facade shading state control algorithm - if ext_irradiance <= 300: - ec = "60" - elif ext_irradiance <= 400 and ext_irradiance > 300: - ec = "18" - elif ext_irradiance <= 450 and ext_irradiance > 400: - ec = "06" - elif ext_irradiance > 450: - ec = "01" - cfs_state = f"ec{ec}" - # actuate facade shading state - eps.actuate_cfs_state( - window="Perimeter_bot_ZN_1_Wall_South_Window", - cfs_state=cfs_state, - ) - - # control cooling setpoint temperature based on the time of day - # pre-cooling - # get the current time - datetime = ep.get_datetime() - # cooling setpoint temperature control algorithm - if datetime.hour >= 16 and datetime.hour < 21: - clg_setpoint = 25.56 - elif datetime.hour >= 12 and datetime.hour < 16: - clg_setpoint = 21.67 - else: - clg_setpoint = 24.44 - # actuate cooling setpoint temperature - eps.actuate_cooling_setpoint( - zone="Perimeter_bot_ZN_1", value=clg_setpoint - ) - - # control lighting power based on occupancy and workplane illuminance - # daylight dimming - # get occupant count and direct and diffuse solar irradiance - occupant_count = eps.get_variable_value( - name="Zone People Occupant Count", key="PERIMETER_BOT_ZN_1" - ) - # calculate average workplane illuminance using Radiance - avg_wpi = eps.calculate_wpi( - zone="Perimeter_bot_ZN_1", - cfs_name={ - "Perimeter_bot_ZN_1_Wall_South_Window": cfs_state - }, - ).mean() - # electric lighting power control algorithm - if occupant_count > 0: - lighting_power = ( - 1 - min(avg_wpi / 500, 1) - ) * 1200 # 1200W is the nominal lighting power density - else: - lighting_power = 0 - # actuate electric lighting power - eps.actuate_lighting_power( - light="Perimeter_bot_ZN_1_Lights", - value=lighting_power, - ) -``` - -### 2.3 Set callback - -Register the controller functions to be called back by EnergyPlus during runtime by calling `set_callback`and passing in a callback point and function. Refer to [Application Guide for EMS](https://energyplus.net/assets/nrel_custom/pdfs/pdfs_v22.1.0/EMSApplicationGuide.pdf) for descriptions of the calling points. - -This example uses `callback_begin_system_timestep_before_predictor`. - -!!! quote "BeginTimestepBeforePredictor" - The calling point called “BeginTimestepBeforePredictor” occurs near the beginning of each timestep but before the predictor executes. “Predictor” refers to the step in EnergyPlus modeling when the zone loads are calculated. This calling point is useful for controlling components that affect the thermal loads the HVAC systems will then attempt to meet. Programs called from this point might actuate internal gains based on current weather or on the results from the previous timestep. Demand management routines might use this calling point to reduce lighting or process loads, change thermostat settings, etc. - -```Python -eps.set_callback("callback_begin_system_timestep_before_predictor", controller) - -``` - -### 2.4 Run simulation - -To simulate, use `run` with optional parameters: - -* output_directory: Output directory path. (default: current directory) -* output_prefix: Prefix for output files. (default: eplus) -* output_suffix: Suffix style for output files. (default: L) - * L: Legacy (e.g., eplustbl.csv) - * C: Capital (e.g., eplusTable.csv) - * D: Dash (e.g., eplus-table.csv) -* silent: If True, do not print EnergyPlus output to console. (default: False) -* annual: If True, force run annual simulation. (default: False) -* design_day: If True, force run design-day-only simulation. (default: False) - - -```python -eps.run() -``` diff --git a/docs/how-to/guide8.md b/docs/how-to/guide8.md deleted file mode 100644 index d80dee6..0000000 --- a/docs/how-to/guide8.md +++ /dev/null @@ -1,125 +0,0 @@ -# How to use the Three-Phase method to calculate eDGPs and workplane illuminance? - -This guide will show you how to set up a Three-Phase method workflow in Radiance to calculate eDGPs and workplane illuminance. - -To set up a Three-Phase method workflow, call the `ThreePhaseMethod` class by passing in a workflow configuration that contains information about the settings and model. See [How to set up a workflow configuration?](../guide7/) for more information. - - -**Workflow for setting up a three-phase method** - -1. Initialize a ThreePhaseMethod instance with a workflow configuration. - -2. (Optional) Save the matrices to file. - -3. Generate matrices. - -4. Calculate eDGPs or workplane illuminance. - -## 0. Import the required classes and functions - -```python -from datetime import datetime -import frads as fr -``` - -## 1. Initialize a ThreePhaseMethod instance with a workflow configuration - - -??? example "cfg" - ```json - dict_1 = { - "settings": { - "method": "3phase", - "sky_basis": "r1", - "epw_file": "", - "wea_file": "oak.wea", - "sensor_sky_matrix": ["-ab", "0"], - "view_sky_matrix": ["-ab", "0"], - "sensor_window_matrix": ["-ab", "0"], - "view_window_matrix": ["-ab", "0"], - "daylight_matrix": ["-ab", "0"] - }, - "model": { - "scene": { - "files": [ - "walls.rad", - "ceiling.rad", - "floor.rad", - "ground.rad" - ] - }, - "windows": { - "window_1": { - "file": "window_1.rad", - "matrix_file": "window_1.xml" - } - }, - "materials": { - "files": ["materials.mat"] - }, - "sensors": { - "sensor_1": {"file": "sensor_1.txt"}, - }, - "views": { - "view_1": { - "file": "view_1.vf", - "xres": 16, - "yres": 16 - } - } - } - } - ``` - - ``` python - cfg = fr.WorkflowConfig.from_dict(dict_1) - ``` - -```python -workflow = fr.ThreePhaseMethod(cfg) -``` - -## 2. (Optional) Save the matrices to file - -A *.npz file will be generated in the current working directory. The file name is a hash string of the configuration content. - -```python -workflow.config.settings.save_matrices = True #default=False -``` - -## 3. Generate matrices -Use the `generate_matrices()` method to generate the following matrices: - -- View --> window -- Sensor --> window -- Daylight - -```python -workflow.generate_matrices() -``` - -## 4.1 Calculate eDGPs - -```python -workflow.calculate_edgps( - view="view_1", - shades=["window_1.rad"], #shade geometry files - bsdf=workflow.window_bsdfs["window_1"], #shade BSDF - date_time=datetime(2023, 1, 1, 12), - dni=800, - dhi=100, - ambient_bounce=1, -) -``` - -## 4.2 Calculate workplane illuminance - -```python -workflow.calculate_sensor( - sensor="sensor_1", - bsdf=rad_workflow.window_bsdfs["window_1"], # shade BSDF - date_time=datetime(2023, 1, 1, 12), - dni=800, - dhi=100, -) -``` \ No newline at end of file diff --git a/docs/how-to/guide_ep1.md b/docs/how-to/guide_ep1.md new file mode 100644 index 0000000..157595f --- /dev/null +++ b/docs/how-to/guide_ep1.md @@ -0,0 +1,180 @@ +# How to run a simple EnergyPlus simulation? + +This guide will show you how to run a simple EnergyPlus simulation. After loading an EnergyPlus model, you can edit the objects and parameters in the model before running the simulation. + + +## 0. Import required Python libraries + +```python +import frads as fr +``` +**Optional: Load reference EnergyPlus model and weather files** + +You will need a working EnergyPlus model in idf or epjson format to initialize an EnergyPlus model. Or you can load an EnergyPlus reference model from `pyenergyplus.dataset`. + +```python +from pyenergyplus.dataset import ref_models, weather_files +``` + +!!! tip "Tips: Reference EnergyPlus models and weather files" + The `pyenergyplus.dataset` module contains a dictionary of EnergyPlus models and weather files. The keys are the names of the models and weather files. The values are the file paths to the models and weather files. + + ``` + >>> ref_models.keys() + dict_keys([ + 'full_service_restaurant', 'hospital', 'large_hotel', + 'large_office', 'medium_office', 'midrise_apartment', + 'outpatient', 'primary_school', 'quick_service_restaurant', + 'secondary_school', 'small_hotel', 'small_office', + 'standalone_retail', 'strip_mall', 'supermarket', 'warehouse' + ]) + ``` + +## 1 Initialize an EnergyPlus model + +Initialize an EnergyPlus model by calling `load_energyplus_model` and passing in a working idf or epjson file path. + + +### 1.1 Define EnergyPlus model file path + +=== "local file" + ``` python + idf = "medium_office.idf" + ``` + +=== "reference model" + ``` python + idf = ref_models["medium_office"] # (1) + ``` + + 1. from pyenergyplus.dataset + +### 1.2 Load the EnergyPlus model + +```python +epmodel = fr.load_energyplus_model(idf) +``` + +## 2 Edit the EnergyPlus model (optional) + +### All EnergyPlus objects + +You can access any EnergyPlus model objects (simulation parameters) as you would do to a class attribute. The EnergyPlus model objects share the same name as that in the Input Data File (IDF) but in lower case separated by underscores. For example, the `FenestrationSurface:Detailed` object in IDF is `fenestration_surface_detailed` in `EnergyPlusModel`. + +```python +>>> epmodel.fenestration_surface_detailed +``` +``` +{'Perimeter_bot_ZN_1_Wall_South_Window': FenestrationSurfaceDetailed(surface_type=, construction_name='Window Non-res Fixed', building_surface_name='Perimeter_bot_ZN_1_Wall_South', outside_boundary_condition_object=None, view_factor_to_ground=, frame_and_divider_name=None, multiplier=1.0, number_of_vertices=NumberOfVertice2(root=4.0), vertex_1_x_coordinate=1.5, vertex_1_y_coordinate=0.0, vertex_1_z_coordinate=2.3293, vertex_2_x_coordinate=1.5, vertex_2_y_coordinate=0.0, vertex_2_z_coordinate=1.0213, vertex_3_x_coordinate=10.5, vertex_3_y_coordinate=0.0, vertex_3_z_coordinate=1.0213, vertex_4_x_coordinate=10.5, vertex_4_y_coordinate=0.0, vertex_4_z_coordinate=2.3293)}' +``` + +!!! example "Example: Edit the `fenestration_surface_detailed` object" + + ```python title="Change the construction name of the window" + epmodel.fenestration_surface_detailed[ + "Perimeter_bot_ZN_1_Wall_South_Window" + ].construction_name = "gs1" + ``` + +!!! example "Example: Edit the `lights` object" + + ```python title="Change the watts per zone floor area" + epmodel.lights["Perimeter_bot_ZN_1_Lights"].watts_per_zone_floor_area = 10 + ``` + + +### Glazing system (complex fenestration system) + +Use `EnergyPlusModel.add_glazing_system()` to easily add glazing system (complex fenestration systems) to the `construction_complex_fenestration_state` object in the EnergyPlus model. + +First, use the `GlazingSystem` class to create a glazing system. Then use `EnergyPlusModel.add_glazing_system()` to add the glazing system to the EnergyPlus. See [How to create a glazing system?](guide_ep2.md) for more details. + +``` python +epmodel.add_glazing_system(gs1) # (1) +``` + +1. `gs1 = fr.create_glazing_system(name="gs1", layers=["product1.json", "product2.json"])` + +### Lighting + +Use `EnergyPlusModel.add_lighting()` to easily add lighting systems to the `lights` object in the EnergyPlus model. The function takes in the name of the zone to add lighting, the lighting level in the zone in Watts, and an optional `replace` argument. If `replace` is `True`, the zone's existing lighting system will be replaced by the new lighting system. If `replace` is `False` and the zone already has a lighting system, an error will be raised. The default value of `replace` is `False`. + +```python +epmodel.add_lighting(zone="Perimeter_bot_ZN_1", lighting_level=10, replace=True) +``` + +### Add Output + +Use `EnergyPlusModel.add_output()` to easily add output variables or meters to the `Output:Variable` or `Output:Meter` object in the EnergyPlus model. The method takes in the type of the output (variable or meter), name of the output, and the reporting frequency. The default reporting frequency is `Timestamp`. + +```python +epmodel.add_output( + output_type="variable", + output_name="Lights Electricity Rate", + reporting_frequency="Hourly", +) +``` + +!!! Tip + See .rdd file for all available output variables and .mdd file for all available output meters. + + +## 3. Run the EnergyPlus simulation + +Call `EnergyPlusSetup` class to set up the EnergyPlus simulation. `EnergyPlusSetup` takes in the EnergyPlus model and an optional weather file. If no weather file is provided, when calling `EnergyPlusSetup.run()`, you need to set `design_day` to `True` and run design-day-only simulation; otherwise, an error will be raised. Annual simulation requires a weather file. + +### 3.1 Define weather file path (optional) + +=== "local file" + ```python + weather_file = "USA_CA_San.Francisco.Intl.AP.724940_TMY3.epw" + ``` + +=== "reference weather file" + ```python + weather_file = weather_files["usa_ca_san_francisco"] # (1) + ``` + + 1. from pyenergyplus.dataset + + +### 3.2 Initialize EnergyPlus simulation setup +```python +epsetup = fr.EnergyPlusSetup(epmodel, weather_file) +``` + +### 3.3 Run the EnergyPlus simulation +Call `EnergyPlusSetup.run()` to run the EnergyPlus simulation. This will generate EnergyPlus output files in the working directory. + +The function has the following arguments: + +* output_directory: Output directory path. (default: current directory) +* output_prefix: Prefix for output files. (default: eplus) +* output_suffix: Suffix style for output files. (default: L) +* L: Legacy (e.g., eplustbl.csv) +* C: Capital (e.g., eplusTable.csv) +* D: Dash (e.g., eplus-table.csv) +* silent: If True, do not print EnergyPlus output to console. (default: False) +* annual: If True, force run annual simulation. (default: False) +* design_day: If True, force run design-day-only simulation. (default: False) + +=== "simple" + ```python + epsetup.run() + ``` + +=== "annual" + ```python + # need a weather file + epsetup.run(annual=True) + ``` + +=== "design day" + ```python + # need to set up design day parameters in EnergyPlus model. + epsetup.run(design_day=True) + ``` + + + + diff --git a/docs/how-to/guide_ep2.md b/docs/how-to/guide_ep2.md new file mode 100644 index 0000000..fde8311 --- /dev/null +++ b/docs/how-to/guide_ep2.md @@ -0,0 +1,72 @@ +# How to create a glazing system? + +This guide will show you how to create a glazing system (complex fenestration system) using the `GlazingSystem` class. The glazing system can be added to the EnergyPlus model's `construction_complex_fenestration_state` object. See [How to run a simple EnergyPlus simulation?](guide_ep1.md) for more details. + +The `GlazingSystem` class contains information about the glazing system's solar absorptance, solar and visible transmittance and reflectance, and etc. The solar and photopic results are calcuated using [pyWincalc](https://github.com/LBNL-ETA/pyWinCalc). + +Call `create_glazing_system()` to create a glazing system. The function takes in the name of the glazing system, a list of glazing/shading product files, and an optional list of gap layers. The function returns a `GlazingSystem` instance. + +The glazing and shading product files can be downloaded from the [IGSDB](https://igsdb.lbl.gov/) website. The downloaded glazing product files are in JSON format and the shading product files are in XML format. The product files contain information about the product's transmittance, reflectance, and etc. + +!!! note + The list of glazing/shading product files should be in order from exterior to interior. + +!!! note + The glazing system created by using `create_glazing_system()` has a default air gap at 0.0127 m thickness. + +## Import the required classes and functions + +```python +import frads as fr +``` + +## Example 1 Two layers of glazing products with default gap + +**Double clear glazing system** + +The glazing system consists of the following: + +* 1 layer of clear glass +* Gap: default air gap at 0.0127 m thickness +* 1 layer of clear glass + +```python +gs = fr.create_glazing_system( + name="double_clear", + layers=[ + Path("igsdb_product_364.json"), # clear glass + Path("igsdb_product_364.json"), # clear glass + ], +) +``` + +## Example 2 Two layers of glazing products with custom gap + +The `gaps` argument takes in a list of `Gap` objects. Each `Gap` object consists of a list of `Gas` objects and a float defining the gap thickness. The `Gas` object consists of the gas type and the gas fraction. The gas fraction is a float between 0 and 1. The sum of all gas fractions should be 1. + +**Electrochromatic glazing system** + +The glazing system consists of the following: + +* 1 layer of electrochromic glass +* 1 gap (10% air and 90% argon) at 0.0127 m thickness +* 1 layer of clear glass + +```python +gs = fr.create_glazing_system( + name="ec", + layers=[ + "igsdb_product_7405.json", # electrochromic glass + "igsdb_product_364.json", # clear glass + ], # (1) + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], +) +``` + +1. The list of glazing/shading product files should be in order from exterior to interior. + + diff --git a/docs/how-to/guide_ep3.md b/docs/how-to/guide_ep3.md new file mode 100644 index 0000000..b686a2a --- /dev/null +++ b/docs/how-to/guide_ep3.md @@ -0,0 +1,150 @@ +# How to set up a callback function in EnergyPlus? + +This guide will show you how to use the callback function to modify the EnergyPlus model during the simulation. + +The demonstration will use the callback function to change the cooling setpoint temperature based on time of the day or occupancy count at the beginning of each time step during runtime. + +The callback function is a Python function that can only takes in `state` as the argument. The callback function is where you define the control logic. + +Use `EnergyPlusModel.set_callback()` to set up a callback function. The function takes in the calling point and the callback function. The callback function is called at each time step at the calling point. See [Application Guide for EMS](https://energyplus.net/assets/nrel_custom/pdfs/pdfs_v22.1.0/EMSApplicationGuide.pdf) for details about the various calling points. + +## 0. Import required Python libraries + +```python +import frads as fr +from pyenergyplus.dataset import ref_models, weather_files +``` + +## 1. Initialize an EnergyPlus model + +You will need a working EnergyPlus model in idf or epjson format to initialize an EnergyPlus model. Or you can load an EnergyPlus reference model from `pyenergyplus.dataset`. See [How to run a simple EnergyPlus simulation?](guide_ep1.md) for more information on how to setup an EnergyPlus model. + +```python +epmodel = fr.load_energyplus_model(ref_models["medium_office"]) # (1) +``` + +1. EnergyPlus medium size office reference model from `pyenergyplus.dataset`. + +## 2. Initialize EnergyPlus Simulation Setup + +Initialize EnergyPlus simulation setup by calling `EnergyPlusSetup` and passing in an EnergyPlus model and an optional weather file. + +```python +epsetup = fr.EnergyPlusSetup( + epmodel, weather_files["usa_ca_san_francisco"], enable_radiance=True +) # (1) +``` + +1. San Francisco, CA weather file from `pyenergyplus.dataset`. + +## 3. Define the callback function + +Before going into the control logic defined in the callback function, you need to first check if the api is ready at the beginning of each time step. + +```python +def controller(state): +# check if the api is fully ready + if not epsetup.api.exchange.api_data_fully_ready(state): + return +``` + +### Update EnergyPlus model + +Use `EnergyPlusSetup.actuate` to set or update the operating value of an actuator in the EnergyPlus model. `EnergyPlusSetup.actuate` takes in a component type, name, key, and value. The component type is the actuator category, e.g. "Weather Data". The name is the name of the actuator, e.g. "Outdoor Dew Point". The key is the instance of the variable to retrieve, e.g. "Environment". The value is the value to set the actuator to. + +!!! tip + Use `EnergyPlusSetup.actuators` to get a list of actuators in the EnergyPlus model. [component type, name, key] + +There are also built-in actuator in frads that allows easier actuation of common actuators. See [Built-in Actuators](../ref/eplus.md/#frads.EnergyPlusSetup.actuate_cfs_state) for more details. + +* `EnergyPlusSetup.actuate_cfs_state` +* `EnergyPlusSetup.actuate_heating_setpoint` +* `EnergyPlusSetup.actuate_cooling_setpoint` +* `EnergyPlusSetup.actuate_lighting_power` + +First, get the current time from the EnergyPlus model by using `EnergyPlusSetup.get_datetime`. If the current time is between 9 am and 5 pm, set the cooling setpoint to 21 degree Celsius. Otherwise, set the cooling setpoint to 24 degree Celsius. + +=== "EnergyPlusSetup.actuate" + + ```python + def controller(state): + # check if the api is fully ready + if not epsetup.api.exchange.api_data_fully_ready(state): + return + # get the current time + datetime = epsetup.get_datetime() + if datetime.hour > 9 and datetime.hour < 17: + epsetup.actuate( + component_type="Zone Temperature Control", + name="Cooling Setpoint", + key="Perimeter_bot_ZN_1", + value=21, + ) + else: + epsetup.actuate( + component_type="Zone Temperature Control", + name="Cooling Setpoint", + key="Perimeter_bot_ZN_1", + value=24, + ) + ``` + +=== "EnergyPlusSetup.actuate_cooling_setpoint" + + ```python + def controller(state): + # check if the api is fully ready + if not epsetup.api.exchange.api_data_fully_ready(state): + return + # get the current time + datetime = epsetup.get_datetime() + if datetime.hour > 9 and datetime.hour < 17: + epsetup.actuate_cooling_setpoint(zone="Perimeter_bot_ZN_1", value=21) + else: + epsetup.actuate_cooling_setpoint(zone="Perimeter_bot_ZN_1", value=24) + ``` + +### Access EnergyPlus variable + +Access EnergyPlus variable during simulation by using `EnergyPlusSetup.get_variable_value` and passing in a variable name and key. + +!!! tip + Use `EnergyPlusSetup.get_variable_value` to access the EnergyPlus variable during the simulation and use the variable as a control input. + +Use `EnergyPlusSetup.get_variable_value` to get the current number of occupants in the zone. If the number of occupants is greater than 0, set the cooling setpoint to 21 degree Celsius. Otherwise, set the cooling setpoint to 24 degree Celsius. + +```python +def controller(state): + # check if the api is fully ready + if not epsetup.api.exchange.api_data_fully_ready(state): + return + # get the current number of occupants in the zone + num_occupants = epsetup.get_variable_value( + variable_name="Zone People Occupant Count", + key="Perimeter_bot_ZN_1", + ) + if num_occupants > 0: + epsetup.actuate_cooling_setpoint(zone="Perimeter_bot_ZN_1", value=21) + else: + epsetup.actuate_cooling_setpoint(zone="Perimeter_bot_ZN_1", value=24) +``` + +## 4. Set callback + +Use `EnergyPlusModel.set_callback()` to set up a callback function. The example uses `callback_begin_system_timestep_before_predictor`. + +!!! quote "BeginTimestepBeforePredictor" + The calling point called “BeginTimestepBeforePredictor” occurs near the beginning of each timestep but before the predictor executes. “Predictor” refers to the step in EnergyPlus modeling when the zone loads are calculated. This calling point is useful for controlling components that affect the thermal loads the HVAC systems will then attempt to meet. Programs called from this point might actuate internal gains based on current weather or on the results from the previous timestep. Demand management routines might use this calling point to reduce lighting or process loads, change thermostat settings, etc. + +```Python +epsetup.set_callback( + "callback_begin_system_timestep_before_predictor", + controller +) +``` + +## 5. Run the EnergyPlus simulation + +```python +epsetup.run() +``` \ No newline at end of file diff --git a/docs/how-to/guide6.md b/docs/how-to/guide_rad1.md similarity index 100% rename from docs/how-to/guide6.md rename to docs/how-to/guide_rad1.md diff --git a/docs/how-to/guide7.md b/docs/how-to/guide_rad2.md similarity index 50% rename from docs/how-to/guide7.md rename to docs/how-to/guide_rad2.md index 751b989..d26fcb0 100644 --- a/docs/how-to/guide7.md +++ b/docs/how-to/guide_rad2.md @@ -1,39 +1,46 @@ # How to set up a workflow configuration for Radiance simulation? -The workflow configuration can be used to run a two- or three- or five-Phase method simulation in Radiance. This guide will show you how to set up a workflow configuration using the `WorkflowConfig` class. +**What is a workflow configuration?** -The workflow configuration has two parts: settings and model. +A workflow configuration is an instance of the `WorkflowConfig` class. It is used to run a two- or three- or five-Phase method simulation in Radiance. -1. `settings` defines the simulation settings, such as number of parallel processes, sky basis, window basis, epw/wea file, latitude, longitude, timezone, site elevation, matrices sampling parameters, etc. -2. `model` defines the model, which includes the scene, windows, materials, sensors, and views. +The workflow configuration has two parts: -## Ways to set up a configuration file +1. `settings` -**Method 1** Use the `settings` and `model` attributes in the `WorkflowConfig` class to set up the settings and model parameters in the workflow configuration. The `settings` and `model` attributes are instances of the `Settings` and `Model` classes, respectively. + - i.e number of parallel processes, epw/wea file, latitude, longitude, matrices sampling parameters, and etc. + - See the [Settings](../ref/config.md#frads.methods.Settings) class for more details. -**Method 2** Use the `from_dict()` method of the `WorkflowConfig` class to generate a workflow configuration by passing a dictionary to the method. The dictionary should contain the settings and model parameters. +2. `model` -## 0. Import the required classes and functions + - i.e. scene, windows, materials, sensors, and views + - See the [Model](../ref/config.md#frads.methods.Model) class for more details. -```python -import frads as fr -``` +**How to set up a workflow configuration?** + +**Method 1** -## 1. Use the `settings` and `model` attributes +Create instances of the `Settings` and `Model` classes to represent the settings and model parameters. Then, pass the `Settings` and `Model` instances into the `WorkflowConfig` class to generate a workflow configuration. -### 1.1 Initialize a `WorkflowConfig` instance +**Method 2** + +Use `WorkflowConfig.from_dict()` to generate a workflow configuration by passing in a dictionary that contains the settings and model parameters. + +## 0. Import the required classes and functions ```python -cfg = fr.WorkflowConfig() +import frads as fr ``` -### 1.2 Set up the settings +## Method 1 -Use the `settings` attribute to set up the settings. The `settings` attribute is an instance of the `Settings` class. +## 1.1 Create an instance of `Settings` class -The `Settings` class has the following default settings. You can change the default settings by assigning new values to the attributes of the `Settings` class. +```python title="Create an instance of the Settings class" +settings = fr.Settings() +``` -??? note "Default settings" +??? note "Default setting" **name** The name of the simulation. (default="") **num_processors**: The number of processors to use for the simulation. (default=1) @@ -91,36 +98,39 @@ The `Settings` class has the following default settings. You can change the defa **daylight_matrix**: Daylight matrix sampling parameters. (default_factory=lambda: ["-ab", "2", "-c", "5000"]) +```python title="Edit default setting parameters" +# Edit the number of parallel processes +settings.num_processors = 4 -```python -# Set the number of parallel processes -cfg.settings.num_processors = 4 +# Provide a wea file +settings.wea_file = "oak.wea" ``` -### 1.2 Set up the model - -Use the `model` attribute to set up the model. The `model` attribute is an instance of the `Model` class. - -The `Model` class has the following attributes. +## 1.2 Create an instance of `Model` class -1. `scene`: The scene to use for the simulation. An instance of the `SceneConfig` class. +The `Model` class requires the following parameters: -2. `windows`: The windows to use for the simulation. A dictionary of instances of the `WindowConfig` class. +* `scene`: An instance of the `SceneConfig` class. +* `windows`: A dictionary of instances of the `WindowConfig` class. +* `materials`: An instance of the `MaterialConfig` class. +* `sensors`: A dictionary of instances of the `SensorConfig` class. +* `views`: A dictionary of instances of the `ViewConfig` class. -3. `materials`: The materials to use for the simulation. An instances of the `MaterialConfig` class. +### 1.2.1 Scene -4. `sensors`: The sensors to use for the simulation. A dictionary of instances of the `SensorConfig` class. - -5. `views`: The views to use for the simulation. A dictionary of instances of the `ViewConfig` class. - -```python title="scene" -scene = fr.SceneConfig() -scene.files = ["walls.rad", "ceiling.rad", "floor.rad", "ground.rad"] -cfg.model.scene = scene +```python title="Create an instance of the SceneConfig class" +scene = fr.SceneConfig( + files=[ + "walls.rad", + "ceiling.rad", + "floor.rad", + "ground.rad", + ] +) ``` -??? example "wall.rad" - geometry primitive +??? example "Scene geometry primitive example" + **walls.rad** ``` wall_mat polygon wall_1 0 @@ -132,18 +142,19 @@ cfg.model.scene = scene 0 3 0 ``` +### 1.2.2 Windows -```python title="windows" -window_1 = fr.WindowConfig() -window_1.file = "window_1.rad" -window_1.matrix_file = "window_1.xml" -cfg.model.windows = {"window_1": window_1} +```python title="Create an instance of the WindowConfig class" +window1 = fr.WindowConfig( + file="window1.rad", # window geomtry primitive file + matrix_name="window1_matrix" # specified in materials +) ``` -??? example "window_1.rad" - geometry primitive +??? example "Window geometry primitive example" + **window1.rad** ``` - window_mat polygon window_1 + window_mat polygon window1 0 0 12 @@ -153,58 +164,86 @@ cfg.model.windows = {"window_1": window_1} 0 2 1 ``` -```python title="materials" -materials = fr.MaterialConfig() -materials.files = ["materials.mat"] -cfg.model.materials = materials +### 1.2.3 Materials + +```python title="Create an instance of the MaterialConfig class" +materials = fr.MaterialConfig( + files=["materials.mat"], # material primitive file + matrices={ + "window1_matrix": {"matrix_file": "window1_bsdf.xml"} + } # window matrix file +) ``` -??? example "materials.mat" - material primitive +??? example "Materials primitive example" + **materials.mat** ``` void plastic wall_mat 0 0 5 0.5 0.5 0.5 0 0 ``` +### 1.2.4 Sensors + +```python title="Create an instance of the SensorConfig class" +sensor1 = fr.SensorConfig(file="grid.txt") # a file of sensor points -```python title="sensors" -sensor_1 = fr.SensorConfig() -sensor_1.files = ["sensor_1.txt"] -cfg.model.sensors = {"sensor_1": sensor_1} +sensor_view1 = fr.SensorConfig( + data=[[1, 1, 1, 0, -1, 0]] +) # a sensor point at (1, 1, 1) with a view direction of (0, -1, 0) ``` -??? example "sensor_1.txt" +??? example "Sensor points example" + **grid.txt** + x_viewpoint y_viewpoint z_viewpoint x_direction y_direction z_direction ``` - 1 1 1 0 -1 0 + 0 1 1 0 -1 0 + 0 1 2 0 -1 0 ``` -```python title="views" -view_1 = fr.ViewConfig() -view_1.file = "view_1.vf" -view_1.xres = 16 -view_1.yres = 16 -cfg.model.views = {"view_1": view_1} +### 1.2.5 Views + +```python +view1 = fr.ViewConfig(file = "view1.vf") ``` -??? example "view_1.vf" +??? example "View example" + view1.vf + view_type view_point view_direction view_up_direction view_horizontal_field_of_view view_vertical_field_of_view view_rotation_angle ``` -vta -vp 1 1 1 -vd 0 -1 0 -vu 0 0 1 -vh 180 -vv 180 ``` -## 2. Use the `from_dict()` method +### 1.2.6 Create an instance of the `Model` class -### 2.1 Initialize a `WorkflowConfig` instance with a dictionary +```python +model = fr.Model( + scene=scene, + windows={"window1": window1}, + materials=materials, + sensors={"sensor1": sensor1, "view1": sensor_view1}, # view1 is a sensor point corresponding to view1 in views + views={"view1": view1} +) +``` +## 1.3 Pass `Settings` and `Model` instances into `WorkflowConfig` class + ```python -cfg = fr.WorkflowConfig.from_dict(dict_1) +cfg = fr.WorkflowConfig(settings, model) ``` -??? example "dict_1" +## Method 2 + +## 2. Pass a dictionary into the `WorkflowConfig.from_dict()` method + +The dictionary should contain the settings and model parameters. + +??? example "dictionary example" + ```json - dict = { + dict1 = { "settings": { "method": "3phase", "sky_basis": "r1", @@ -214,51 +253,46 @@ cfg = fr.WorkflowConfig.from_dict(dict_1) "view_sky_matrix": ["-ab", "0"], "sensor_window_matrix": ["-ab", "0"], "view_window_matrix": ["-ab", "0"], - "daylight_matrix": ["-ab", "0"] + "daylight_matrix": ["-ab", "0"], }, "model": { "scene": { - "files": [ - "walls.rad", - "ceiling.rad", - "floor.rad", - "ground.rad" - ] + "files": ["walls.rad", "ceiling.rad", "floor.rad", "ground.rad"] }, "windows": { - "window_1": { - "file": "window_1.rad", - "matrix_file": "window_1.xml" + "window1": { + "file": "window1.rad", + "matrix_name": "window1_matrix", } }, "materials": { - "files": ["materials.mat"] + "files": ["materials.mat"], + "matrices": {"window1_matrix": {"matrix_file": "window1_bsdf.xml"}}, }, "sensors": { - "sensor_1": {"file": "sensor_1.txt"} + "sensor1": {"file": "sensor1.txt"}, + "view1": {"data": [[1, 1, 1, 0, -1, 0]]}, }, - "views": { - "view_1": { - "file": "view_1.vf", - "xres": 16, - "yres": 16 - } - } - } + "views": {"view1": {"file": "view1.vf"}}, + }, } ``` +```python +cfg = fr.WorkflowConfig.from_dict(dict1) +``` + + !!! tips "Use an EnergyPlus model to set up a workflow configuration" You can use the `epjson_to_rad()` function to convert an EnergyPlus model to a Radiance model. The function returns a dictionary of the Radiance model for each exterior zone in the EnergyPlus model. You can use the dictionary to set up the workflow configuration. ```python epmodel = fr.EnergyPlusModel("file.idf") # EnergyPlus model radmodel = fr.epjson_to_rad(epmodel) # Radiance model - dict_zone1 = radmodel["zone_1"] # Get the dictionary of the Radiance model for zone_1. + dict_zone1 = radmodel["zone1"] # Dictionary of zone1 ``` ```python cfg = fr.WorkflowConfig.from_dict(dict_zone1) ``` - diff --git a/docs/how-to/guide_rad3.md b/docs/how-to/guide_rad3.md new file mode 100644 index 0000000..f657bb5 --- /dev/null +++ b/docs/how-to/guide_rad3.md @@ -0,0 +1,156 @@ +# How to calculate workplane illuminance and edgps using three-phase method? + +This guide will show you how to calculate workplane illuminance and eDGPs (enhanced simplified Daylight Glare Probability) using the Three-Phase method in Radiance. + +**What is the Three-Phase method?** + +The Three-Phase method a way to perform annual daylight simulation of complex fenestration systems. The method divide flux transfer into three phases or matrices: + +* V(iew): flux transferred from simulated space to the interior of the fenestration +* T(ransmission): flux transferred through the fenestration (usually represented by a BSDF) +* D(aylight): flux transferred from the exterior of fenestration to the sky + +Multiplication of the three matrices with the sky matrix gives the illuminance at the simulated point. In the case where one wants to calculate the illuminance for different fenestration systems, one only needs to calculate the daylight and view matrice once and then multiply them with the transmission matrix of each fenestration system. + + +**Workflow for setting up a three-phase method** + +1. Initialize a ThreePhaseMethod instance with a workflow configuration. + +2. (Optional) Save the matrices to file. + +3. Generate matrices. + +4. Calculate workplane illuminance and eDGPs. + +## 0. Import the required classes and functions + +```python +from datetime import datetime +import frads as fr +``` + +## 1. Initialize a ThreePhaseMethod instance with a workflow configuration + +To set up a Three-Phase method workflow, call the `ThreePhaseMethod` class and pass in a workflow configuration that contains information about the settings and model. See [How to set up a workflow configuration?](guide_rad2.md/) for more information. + + +??? example "cfg" + ```json + dict1 = { + "settings": { + "method": "3phase", + "sky_basis": "r1", + "epw_file": "", + "wea_file": "oak.wea", + "sensor_sky_matrix": ["-ab", "0"], + "view_sky_matrix": ["-ab", "0"], + "sensor_window_matrix": ["-ab", "0"], + "view_window_matrix": ["-ab", "0"], + "daylight_matrix": ["-ab", "0"], + }, + "model": { + "scene": { + "files": ["walls.rad", "ceiling.rad", "floor.rad", "ground.rad"] + }, + "windows": { + "window1": { + "file": "window1.rad", + "matrix_name": "window1_matrix", + } + }, + "materials": { + "files": ["materials.mat"], + "matrices": {"window1_matrix": {"matrix_file": "window1_bsdf.xml"}}, + }, + "sensors": { + "sensor1": {"file": "sensor1.txt"}, + "view1": {"data": [[1, 1, 1, 0, -1, 0]]}, + }, + "views": {"view1": {"file": "view1.vf"}}, + }, + } + + ``` + + ``` python + cfg = fr.WorkflowConfig.from_dict(dict1) + ``` + +```python +workflow = fr.ThreePhaseMethod(cfg) +``` + +## 2. (Optional) Save the matrices to file + +A *.npz file will be generated in the current working directory. The file name is a hash string of the configuration content. + +```python +workflow.config.settings.save_matrices = True # default=False +``` + +## 3. Generate matrices +Use the `generate_matrices()` method to generate the following matrices: + +- View --> window +- Sensor --> window +- Daylight + +```python +workflow.generate_matrices() +``` + +!!! tip "get workflow from EnergyPlusSetup" + If you are using the ThreePhaseMethod class in EnergyPlusSetup, you can get the workflow from the EnergyPlusSetup instance. See [How to enable Radiance in EnergyPlus simulation?](guide_radep1.md) for more information. + + ```python + eps = fr.EnergyPlusSetup(epmodel, weather_file, enable_radiance=True) + workflow = eps.rworkflows[zone_name] + ``` + +## 4. Calculate + +### 4.1 workplane illuminance + +Use the `calculate_sensor()` method to calculate workplane illuminance for a sensor. Need to pass in the name of the sensor, a dictionary of window names and their corresponding BSDF matrix file names, datetime, direct normal irradiance (DNI), and diffuse horizontal irradiance (DHI). + +```python +workflow.calculate_sensor( + sensor="sensor1", + bsdf={"window1": "window1_matrix"}, + time=datetime(2023, 1, 1, 12), + dni=800, + dhi=100, +) +``` + +**what does calculate_sensor() do behind the scene?** + +It multiplies the view, transmission, daylight, and sky matrices +with weights in the red, green, and blue channels to get the illuminance at the sensor point. + +### 4.2 eDGPs + +**What is eDGPs?** + +eDGPs is an enhanced version of the simplified Daylight Glare Probability (DGPs) to evaluate the glare potential. + +Use the `calculate_edgps()` method to calculate eDGPs for a view. Need to pass in the name of the view, a dictionary of window names and their corresponding BSDF matrix file names, datetime, direct normal irradiance (DNI), diffuse horizontal irradiance (DHI), and ambient bounce. + +!!! Note + To calculate eDGPs for view1, you need to specify a view1 key name in `dict1["model"]["views"]` and `dict1["model"]["sensors"]`. + +```python +workflow.calculate_edgps( + view="view1", + bsdf={"window1": "window1_matrix"}, + time=datetime(2023, 1, 1, 12), + dni=800, + dhi=100, + ambient_bounce=1, +) +``` + +**what does calculate_edgps() do behind the scene?** + +First, use Radiance `rpict` to render a low-resolution (ambient bouce: 1) high dynamic range image (HDRI). Then, use Radiance `evalglare` to evaluate the DGP of the HDRI with illuminance modification calculated using the matrix multiplication. diff --git a/docs/how-to/guide_radep1.md b/docs/how-to/guide_radep1.md new file mode 100644 index 0000000..29f3bf0 --- /dev/null +++ b/docs/how-to/guide_radep1.md @@ -0,0 +1,58 @@ +# How to enable Radiance in EnergyPlus simulation? + +This guide will show you how to enable Radiance in EnergyPlus simulation. + +Users can enable Radiance for desired accuracy in daylighting simulation. Radiance can be used to calculate workplane illuminance, eDGPs, and etc. See [How to calculate workplane illuminance and eDGPs using three-phase method?](guide_rad3.md) for more information. + +**Workflow** + +1. [Setup an EnergyPlus Model](#1-setup-an-energyplus-model) + +2. [Setup EnergyPlus Simulation](#2-setup-energyplus-simulation) + + +## 0. Import the required classes and functions + +```python +import frads as fr +``` + +## 1. Setup an EnergyPlus Model + +You will need a working EnergyPlus model in idf or epjson format to initialize an EnergyPlus model. Or you can load an EnergyPlus reference model from `pyenergyplus.dataset`. See [How to run a simple EnergyPlus simulation?](guide_ep1.md) for more information on how to setup an EnergyPlus model. + +```python +epmodel = fr.load_energyplus_model("medium_office.idf") +``` + +## 2. Setup EnergyPlus Simulation +Initialize EnergyPlus simulation setup by calling `EnergyPlusSetup` and passing in an EnergyPlus model and an optional weather file. + +To enable Radiance for daylighting simulation, set `enable_radiance` to `True`; default is `False`. When `enable_radiance` is set to `True`, the `EnergyPlusSetup` class will automatically setup the three-phase method in Radiance. + +```python +epsetup = fr.EnergyPlusSetup( + epmodel, weather_files["usa_ca_san_francisco"], enable_radiance=True +) +``` + +After the radiance is enabled, the following calculations can be performed: + +=== "Workplane illuminance" + + `epsetup.calculate_wpi()` [more info](../ref/eplus.md#frads.EnergyPlusSetup.calculate_wpi) + + or + + `epsetup.rworkflows[zone_name].calculate_sensor()`[more info](../ref/threephase.md#frads.ThreePhaseMethod.calculate_sensor) + +=== "Simplified Daylight Glare Probability (eDGPs)" + + `epsetup.calculate_edgps()` [more info](../ref/eplus.md#frads.EnergyPlusSetup.calculate_edgps) + + or + + `epsetup.rworkflows[zone_name].calculate_edgps()`[more info](../ref/threephase.md#frads.ThreePhaseMethod.calculate_edgps) + + +See [How to calculate workplane illuminance and eDGPs using three-phase method?](guide_rad3.md) and [How to simulate spatial daylight autonomy using three-phase method?](guide_radep3.md) for more information on how to calculate workplane illuminance and eDGPs using Radiance. diff --git a/docs/how-to/guide_radep2.md b/docs/how-to/guide_radep2.md new file mode 100644 index 0000000..af017a8 --- /dev/null +++ b/docs/how-to/guide_radep2.md @@ -0,0 +1,289 @@ +# How to model dynamic shading control and daylight dimming with EnergyPlus? + +This guide will demonstrate how to use a controller function to control the shading state, cooling setpoint temperature, and electric lighting power intensity during simulation. + +The example is a medium office building with a four tinted states electrochromic glazing system. At the beginning of each timestep, EnergyPlus will call the controller function that operates the facade shading state based on exterior solar irradiance, cooling setpoint temperature based on time of day (pre-cooling), and electric lighting power intensity based on occupancy and workplane illuminance (daylight dimming). The workplane illuminance is calculated using the three-phase method in Radiance. + +**Workflow** + +1. [Setup an EnergyPlus Model](#1-setup-an-energyplus-model) + + 1.1 [Initialize an EnergyPlus model](#11-initialize-an-energyplus-model) + + 1.2 [Create glazing systems (Complex Fenestration States)](#12-create-glazing-systems-complex-fenestration-states) + + 1.3 [Add glazing systems to EnergyPlus model](#13-add-glazing-systems-to-energyplus-model) + + 1.4 [Add lighting systems to EnergyPlus model](#14-add-lighting-systems-to-energyplus-model) + +2. [Setup EnergyPlus Simulation](#2-setup-energyplus-simulation) + + 2.1 [Initialize EnergyPlus Simulation Setup](#21-initialize-energyplus-simulation-setup) + + 2.2 [Define control algorithms using a controller function](#22-define-control-algorithms-using-a-controller-function) + + 2.3 [Set callback](#23-set-callback) + + 2.4 [Run simulation](#24-run-simulation) + + +``` mermaid +graph LR + subgraph IGSDB + A[Step 1.2
glazing/shading products]; + end + + subgraph frads + + C[Step 1.1 idf/epjs] --> |Initialize an EnergyPlus model| E; + + subgraph Step 2 EnergyPlus Simulation Setup + subgraph Radiance + R[Workplane Illuminance]; + end + subgraph EnergyPlus + E[EnergyPlusModel]<--> K[Step 2.2 & 2.3
controller function]; + E <--> R; + K <--> R; + end + end + + subgraph WincalcEngine + A --> D[Step 1.3
create a glazing system
per CFS state]; + D --> |Add glazing systems| E; + end + + L[Step 1.4 lighting systems] --> |Add lighting| E; + + end +``` + +## 0. Import required Python libraries + +```python +import frads as fr +from pyenergyplus.dataset import ref_models, weather_files +``` + +## 1. Setup an EnergyPlus Model +### 1.1 Initialize an EnergyPlus model + +You will need a working EnergyPlus model in idf or epjson format to initialize an EnergyPlus model. Or you can load an EnergyPlus reference model from `pyenergyplus.dataset`. See [How to run a simple EnergyPlus simulation?](guide_ep1.md) for more information on how to setup an EnergyPlus model. + +```python +epmodel = fr.load_energyplus_model(ref_models["medium_office"]) # (1) +``` + +1. EnergyPlus medium size office reference model from `pyenergyplus.dataset`. + +### 1.2 Create glazing systems (Complex Fenestration States) + +!!! example "Create four glazing systems for the four electrochromic tinted states" + Each glazing system consists of: + + * One layer of electrochromic glass + * One gap (10% air and 90% argon) at 0.0127 m thickness + * One layer of clear glass + +Call `create_glazing_system` to create a glazing system. See [How to create a glazing system?](guide_ep2.md) for more details on how to create a glazing system. + +```python +gs_ec01 = fr.create_glazing_system( + name="ec01", + layers=[ + "igsdb_product_7405.json", # electrochromic glass Tvis: 0.01 + "igsdb_product_364.json", # clear glass + ], + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], +) +``` + +??? info "Create glazing systems for the other tinted electrochromic states" + ```python + gs_ec06 = fr.create_glazing_system( + name="ec06", + layers=[ + "igsdb_product_7407.json", + "igsdb_product_364.json", + ], + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], + ) + + gs_ec18 = fr.create_glazing_system( + name="ec18", + layers=[ + "igsdb_product_7404.json", + "igsdb_product_364.json", + ], + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], + ) + + gs_ec60 = fr.create_glazing_system( + name="ec60", + layers=[ + "igsdb_product_7406.json", + "igsdb_product_364.json", + ], + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], + ) + ``` + +### 1.3 Add glazing systems to EnergyPlus model + +Call `EnergyPlusModel.add_glazing_system()` to add glazing systems to the EnergyPlus model. + +```python +epmodel.add_glazing_system(gs_ec01) +``` +??? info "Add other glazing systems to the EnergyPlus model" + ```python + epmodel.add_glazing_system(gs_ec06) + epmodel.add_glazing_system(gs_ec18) + epmodel.add_glazing_system(gs_ec60) + ``` + +### 1.4 Add lighting systems to EnergyPlus model +Call `EnergyPlusModel.add_lighting` to add lighting systems to the EnergyPlus model. + +```python +epmodel.add_lighting( + zone="Perimeter_bot_ZN_1", + lighting_level=1200, # (1) + replace=True +) +``` + +1. 1200W is the maximum lighting power density for the zone. This will be dimmed based on the daylight illuminance. + +## 2. Setup EnergyPlus Simulation + +### 2.1 Initialize EnergyPlus Simulation Setup + +Initialize EnergyPlus simulation setup by calling `EnergyPlusSetup` and passing in an EnergyPlus model and an optional weather file. Enable Radiance for daylighting simulation by setting `enable_radiance` to `True`. See [How to enable Radiance in EnergyPlus simulation?](guide_radep1.md) for more information. + +```python +epsetup = fr.EnergyPlusSetup( + epmodel, weather_files["usa_ca_san_francisco"], enable_radiance=True +) # (1) +``` + +1. San Francisco, CA weather file from `pyenergyplus.dataset`. + +### 2.2 Define control algorithms using a controller function + +The controller function defines the control algorithm and control the facade shading state, cooling setpoint temperature, and electric lighting power intensity in the EnergyPlus model during simulation. + +!!! example "Controller function" + The example shows how to implement control algorithms for zone "Perimeter_bot_ZN_1", which has a window named "Perimeter_bot_ZN_1_Wall_South_Window" and lighting named "Perimeter_bot_ZN_1". + + * **Facade CFS state** based on exterior solar irradiance + * **Cooling setpoint temperature** based on time of day (pre-cooling) + * **Electric lighting power intensity** based on occupancy and workplane illuminance (daylight dimming) + + +The controller function takes in a `state` argument. See [How to set up a callback function in EnergyPlus?](guide_ep3.md) for more details on how to define a controller function. + +```py linenums="1" hl_lines="2 6 28 44" + +def controller(state): + # check if the api is fully ready + if not epsetup.api.exchange.api_data_fully_ready(state): + return + + # control facade shading state based on exterior solar irradiance + # get exterior solar irradiance + ext_irradiance = epsetup.get_variable_value( + name="Surface Outside Face Incident Solar Radiation Rate per Area", + key="Perimeter_bot_ZN_1_Wall_South_Window", + ) + # facade shading state control algorithm + if ext_irradiance <= 300: + ec = "60" + elif ext_irradiance <= 400 and ext_irradiance > 300: + ec = "18" + elif ext_irradiance <= 450 and ext_irradiance > 400: + ec = "06" + elif ext_irradiance > 450: + ec = "01" + cfs_state = f"ec{ec}" + # actuate facade shading state + epsetup.actuate_cfs_state( + window="Perimeter_bot_ZN_1_Wall_South_Window", + cfs_state=cfs_state, + ) + + # control cooling setpoint temperature based on the time of day + # pre-cooling + # get the current time + datetime = epsetup.get_datetime() + # cooling setpoint temperature control algorithm + if datetime.hour >= 16 and datetime.hour < 21: + clg_setpoint = 25.56 + elif datetime.hour >= 12 and datetime.hour < 16: + clg_setpoint = 21.67 + else: + clg_setpoint = 24.44 + # actuate cooling setpoint temperature + epsetup.actuate_cooling_setpoint( + zone="Perimeter_bot_ZN_1", value=clg_setpoint + ) + + # control lighting power based on occupancy and workplane illuminance + # daylight dimming + # get occupant count and direct and diffuse solar irradiance + occupant_count = epsetup.get_variable_value( + name="Zone People Occupant Count", key="PERIMETER_BOT_ZN_1" + ) + # calculate average workplane illuminance using Radiance + avg_wpi = epsetup.calculate_wpi( + zone="Perimeter_bot_ZN_1", + cfs_name={ + "Perimeter_bot_ZN_1_Wall_South_Window": cfs_state + }, + ).mean() + # electric lighting power control algorithm + if occupant_count > 0: + lighting_power = ( + 1 - min(avg_wpi / 500, 1) + ) * 1200 # 1200W is the nominal lighting power density + else: + lighting_power = 0 + # actuate electric lighting power + epsetup.actuate_lighting_power( + light="Perimeter_bot_ZN_1", + value=lighting_power, + ) +``` + +### 2.3 Set callback + +Register the controller functions to be called back by EnergyPlus during runtime by calling `set_callback`and passing in a callback point and function. See [How to set up a callback function in EnergyPlus?](guide_ep3.md) for more details. + +```Python +epsetup.set_callback( + "callback_begin_system_timestep_before_predictor", + controller +) +``` + +### 2.4 Run simulation + +```python +epsetup.run() +``` diff --git a/docs/how-to/guide_radep3.md b/docs/how-to/guide_radep3.md new file mode 100644 index 0000000..b0dc9c5 --- /dev/null +++ b/docs/how-to/guide_radep3.md @@ -0,0 +1,172 @@ +# How to simulate spatial daylight autonomy using three-phase method? + +This guide will show you how to calculate spatial daylight autonomy (sDA) using the Three-Phase method in Radiance. This guide shows how to automatically generate a Radiance model and three-phase method workflow from a EnergyPlus model. See [How to setup a workflow configuration?](guide_rad2.md) and [How to calculate workplane illuminance and eDGPs using three-phase method?](guide_rad3.md) for more information on how to setup a workflow configuration and calculate workplane illuminance without an EnergyPlus model. + +**What is spatial daylight autonomy?** + +Spatial daylight autonomy (sDA) is the percentage of the area that meets a minimum illuminance threshold for a specified fraction of the annual occupied hours. The target illuminance threshold is usually 300 lux for 50% of the occupied period. + +**Workflow** + +0. Import the required classes and functions + +1. Setup an EnergyPlus Model + 1. Initialize an EnergyPlus model + 2. Create glazing systems (Complex Fenestration States) + 3. Add the glazing system to the EnergyPlus model + +2. Setup EnergyPlus Simulation + 1. Initialize EnergyPlus Simulation Setup + 2. Calculate workplane illuminance + 3. Run the simulation + +3. Calculate sDA + +4. Visualize sDA (optional) + +## 0. Import the required classes and functions + +```python +import datetime +import numpy as np + +import frads as fr +from pyenergyplus.dataset import weather_files +``` + +## 1. Setup an EnergyPlus Model +### 1.1 Initialize an EnergyPlus model + +Initialize an EnergyPlus model by calling `load_energyplus_model` and passing in an EnergyPlus model in an idf or epjson file format. + +```python +epmodel = fr.load_energyplus_model("RefBldgMediumOfficeNew2004_southzone.idf") +``` + +### 1.2 Create glazing systems (Complex Fenestration States) + +```python title="Create a glazing system" +gs_ec01 = fr.create_glazing_system( + name="ec01", + layers=[ + "igsdb_product_7405.json", + "CLEAR_3.DAT", + ], + gaps=[ + fr.Gap( + [fr.Gas("air", 0.1), fr.Gas("argon", 0.9)], 0.0127 + ) + ], +) +``` + +```python title="Add the glazing system to the EnergyPlus model" +epmodel.add_glazing_system(gs_ec01) +``` + +## 2. Setup EnergyPlus Simulation +### 2.1 Initialize EnergyPlus Simulation Setup + +Initialize EnergyPlus simulation setup by calling `EnergyPlusSetup` and passing in an EnergyPlus model and an optional weather file. + +To enable Radiance for daylighting simulation, set `enable_radiance` to `True`. The default value of `enable_radiance` is `False`. This step will setup the three-phase method in Radiance. + +```python +eps = fr.EnergyPlusSetup( + epmodel, weather_files["usa_ca_san_francisco"], enable_radiance=True +) +``` + +### 2.2 Calculate workplane illuminance + +Use the `calculate_wpi()` method inside a callback function to calculate the workplane illuminance at each timestamp. Save the workplane illuminance to a variable. + +!!! note + The `calculate_wpi()` method calls the `ThreePhaseMethod` class in the background. See [How to calculate workplane illuminance and eDGPs using three-phase method?](guide_rad3.md) for more information on how to use the `ThreePhaseMethod` class directly. + +```python title="Create a list to store the workplane illuminance" +wpi_list = [] +``` + +```python title="Define a callback function to calculate the workplane illuminance" +def callback_func(state): + # check if the api is fully ready + if not eps.api.exchange.api_data_fully_ready(state): + return + + # get the current time + datetime = eps.get_datetime() + # only calculate workplane illuminance during daylight hours + if datetime.hour >= 8 and datetime.hour < 18: + wpi = eps.calculate_wpi( + zone="Perimeter_bot_ZN_1", + cfs_name={ + "Perimeter_bot_ZN_1_Wall_South_Window": "ec01", + }, # {window: glazing system} + ) # an array of illuminance for all sensors in the zone + wpi_list.append(wpi) +``` + +### 2.3 Run the simulation + +Set the callback function to `set_callback` and run the simulation. Refer to [Application Guide for EMS](https://energyplus.net/assets/nrel_custom/pdfs/pdfs_v22.1.0/EMSApplicationGuide.pdf) for descriptions of the calling points. + +```python title="Set the callback function" +eps.set_callback("callback_begin_system_timestep_before_predictor", callback) +``` + +```python title="Run the simulation" +eps.run(annual=True) # run annual simulation +``` + +## 3. Calculate sDA + +Each element in `wpi_list` is a numpy array of sensors' workplane illuminance at each timestamp. Concatenate the numpy arrays in the `wpi_list` to a single numpy array. Then calculate the percentage of time when the workplane illuminance is greater than 300 lux. + +```python title="Generate a numpy array of percentage of time when the sensor's workplane illuminance is greater than 300 lux" +wpi_all = np.concatenate(wpi_list, axis=1) +lx300 = np.sum(wpi_all >= 300, axis=1) / wpi_all.shape[1] * 100 +``` + +```python title="Generate a numpy array of x and y coordinates of the sensors" +xy = np.array( + eps.rconfigs["Perimeter_bot_ZN_1"] + .model.sensors["Perimeter_bot_ZN_1_Floor"] + .data +)[:, :2] +``` + +```python title="Concatenate the lx300 and xy numpy arrays to a single numpy array" +sda = np.concatenate([xy, lx300.reshape(-1, 1)], axis=1) +``` + +## 4. Visualize sDA (optional) + +```python title="import matplotlib" +import matplotlib.pyplot as plt +``` + +```python title="Plot the sDA" +fig, ax = plt.subplots(figsize=(4, 3.5)) +x, y, color = sda[:, 0], sda[:, 1], sda[:, 2] +plot = ax.scatter( + x, + y, + c=color, + cmap="plasma", + s=15, + vmin=0, + vmax=100, + rasterized=True, +) +ax.set( + xlabel = "x position [m]", + ylabel = "y position [m]", +) + +fig.colorbar(plot, ax=ax, label="Daylight Autonomy [%]") +fig.tight_layout() +``` + +![sda](../assets/sda.png) + diff --git a/docs/how-to/index.md b/docs/how-to/index.md index dc56b4b..db924a3 100644 --- a/docs/how-to/index.md +++ b/docs/how-to/index.md @@ -1,22 +1,31 @@ -This part of the project documentation focuses on a -**problem-oriented** approach. You'll tackle common -tasks that you might have, with the help of the code -provided in this project. +## Radiance -1. How to simulate spatial daylight autonomy using two-phase method? +1. [How to setup a simple rtrace workflow?](guide_rad1.md) -2. How to simulate spatial daylight autonomy using three-phase method? +2. [How to setup a workflow configuration?](guide_rad2.md) -3. How to simulate annual glare index using five-phase method? +3. [How to calculate workplane illuminance and edgps using three-phase method?](guide_rad3.md) -4. How to simulate annual melanopic equivalent daylight illuminance? +4. How to simulate spatial daylight autonomy using two-phase method? -5. [How to model dynamic shading control and daylight dimming with EnergyPlus?](guide5.md) +6. How to simulate annual glare index using five-phase method? -6. [How to setup a simple rtrace workflow?](guide6.md) +7. How to simulate annual melanopic equivalent daylight illuminance? -7. [How to setup a workflow configuration?](guide7.md) +## EnergyPlus -8. [How to setup a Three-Phase method workflow with a workflow configuration?](guide8.md) +1. [How to run a simple EnergyPlus simulation?](guide_ep1.md) + +2. [How to create a glazing system?](guide_ep2.md) + +3. [How to set up a callback function in EnergyPlus?](guide_ep3.md) + +## Radiance and EnergyPlus + +1. [How to enable Radiance in EnergyPlus simulation?](guide_radep1.md) + +2. [How to model dynamic shading control and daylight dimming with EnergyPlus?](guide_radep2.md) + +3. [How to simulate spatial daylight autonomy using three-phase method?](guide_radep3.md) From 7c1075df304a6412d8c755d68cc271cd8ed1d7de Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Tue, 30 Jan 2024 19:11:54 -0800 Subject: [PATCH 15/16] doc(guide_rad2): update --- docs/how-to/guide_rad2.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/how-to/guide_rad2.md b/docs/how-to/guide_rad2.md index d26fcb0..26ffcce 100644 --- a/docs/how-to/guide_rad2.md +++ b/docs/how-to/guide_rad2.md @@ -218,6 +218,13 @@ view1 = fr.ViewConfig(file = "view1.vf") ### 1.2.6 Create an instance of the `Model` class +!!! tip + * All phases require `materials` + * All phases require `sensors` or `views` + * Three- and Five-Phase methods require `windows` + * If a window matrix name is specified in `windows`, the corresponding window matrix file must be specified in `materials` + * There is a corresponding `sensors` point for each `views` point. This `sensors` point could be **automatically** generally when `views` is specified in `Model` or **manually** defined by the user as shown below. `view1` in sensors must have the same view direction and view position as `view1` in views; otherwise, an error will be raised. + ```python model = fr.Model( scene=scene, From c1d1e3714ab69125e9a726469571f7325decf7af Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Tue, 30 Jan 2024 19:12:44 -0800 Subject: [PATCH 16/16] fix(methods): workflow config --- frads/methods.py | 147 +++++++++++++++++++------- test/test_eplus.py | 17 ++- test/test_methods.py | 243 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 352 insertions(+), 55 deletions(-) diff --git a/frads/methods.py b/frads/methods.py index 8a07db0..bf6df4d 100755 --- a/frads/methods.py +++ b/frads/methods.py @@ -71,9 +71,6 @@ class SceneConfig: materials.update(parse_window_material_blind(material)) bytes: A raw data string to be used as the scene. files_mtime: Files last modification time. - - Raises: - ValueError: If both files and bytes are empty. """ files: List[Path] = field(default_factory=list) @@ -84,8 +81,6 @@ def __post_init__(self): if len(self.files) > 0: for fpath in self.files: self.files_mtime.append(os.path.getmtime(fpath)) - if self.bytes == b"" and len(self.files) == 0: - raise ValueError("SceneConfig must have either file or bytes") @dataclass @@ -127,7 +122,7 @@ class MaterialConfig: file_mtime: File last modification time. Raises: - ValueError: If file, bytes, and matrices are empty. + ValueError: If no file, bytes, or matrices are provided. """ files: List[Path] = field(default_factory=list) @@ -147,10 +142,9 @@ def __post_init__(self): self.bytes == b"" and len(self.files) == 0 and len(self.matrices) == 0 - and len(self.glazing_materials) == 0 ): raise ValueError( - "MaterialConfig must have either file, bytes, matrices, or glazing_materials" + "MaterialConfig must have either file, bytes or matrices" ) @@ -170,6 +164,9 @@ class WindowConfig: matrix_name: A matrix name to be used for the window group. proxy_geometry: A raw data string to be used as the shading geometry. files_mtime: Files last modification time. + + Raises: + ValueError: If neither file nor bytes are provided. """ file: Union[str, Path] = "" @@ -203,6 +200,9 @@ class SensorConfig: Default is an empty list. file_mtime: Modification time of the file. This attribute is automatically initialized based on the 'file' attribute. + + Raises: + ValueError: If neither file nor data are provided. """ file: str = "" @@ -213,9 +213,6 @@ def __post_init__(self): """ Post-initialization method to set the file modification time and load data from the file if necessary. - - Raises: - ValueError: If both 'file' and 'data' attributes are empty. """ if self.file != "": self.file_mtime = os.path.getmtime(self.file) @@ -232,6 +229,22 @@ def __post_init__(self): @dataclass class ViewConfig: + """ + A configuration class for views that includes information on the file, + data, x/y resoluation, and file modification time. + + Attributes: + file: Path to the file containing view data. Default is an empty string. + view: A View object. Default is an empty string. + xres: X resolution of the view. Default is 512. + yres: Y resolution of the view. Default is 512. + file_mtime: Modification time of the file. This attribute is + automatically initialized based on the 'file' attribute. + + Raises: + ValueError: If neither file nor view are provided. + """ + file: Union[str, Path] = "" view: Union[pr.View, str] = field(default_factory=str) xres: int = 512 @@ -249,11 +262,26 @@ def __post_init__(self): if not isinstance(self.view, pr.View): self.view = parse_view(self.view) else: - raise ValueError("ViewConfig must have a file or view") + raise ValueError("ViewConfig must have either file or view") @dataclass class SurfaceConfig: + """ + A configuration class for surfaces that includes information on the file, + data, basis, and file modification time. + + Attributes: + file: Path to the file containing surface data. Default is an empty string. + primitives: A list of primitives. Default is an empty list. + basis: A string representing the basis. Default is 'u'. + file_mtime: Modification time of the file. This attribute is + automatically initialized based on the 'file' attribute. + + Raises: + ValueError: If neither file nor primitives are provided. + """ + file: Union[str, Path] = "" primitives: List[pr.Primitive] = field(default_factory=list) basis: str = "u" @@ -267,7 +295,9 @@ def __post_init__(self): if self.file.exists() and len(self.primitives) == 0: self.primitives = pr.parse_primitive(self.file.read_text()) elif len(self.primitives) == 0: - raise ValueError("No primitives available") + raise ValueError( + "SurfaceConfig must have either file or primitives" + ) @dataclass @@ -404,7 +434,7 @@ class Model: """ materials: "MaterialConfig" - scene: "SceneConfig" = field(default_factory=dict) + scene: "SceneConfig" = field(default_factory=SceneConfig) windows: Dict[str, "WindowConfig"] = field(default_factory=dict) sensors: Dict[str, "SensorConfig"] = field(default_factory=dict) views: Dict[str, "ViewConfig"] = field(default_factory=dict) @@ -412,27 +442,65 @@ class Model: # Make Path() out of all path strings def __post_init__(self): - if isinstance(self.scene, dict) and len(self.scene) > 0: + if isinstance(self.scene, dict): self.scene = SceneConfig(**self.scene) if isinstance(self.materials, dict): self.materials = MaterialConfig(**self.materials) - if isinstance(self.windows, dict) and len(self.windows) > 0: - for k, v in self.windows.items(): - if isinstance(v, dict): - self.windows[k] = WindowConfig(**v) - if isinstance(self.sensors, dict) and len(self.sensors) > 0: - for k, v in self.sensors.items(): - if isinstance(v, dict): - self.sensors[k] = SensorConfig(**v) - if isinstance(self.views, dict) and len(self.views) > 0: - for k, v in self.views.items(): - if isinstance(v, dict): - self.views[k] = ViewConfig(**v) - if isinstance(self.surfaces, dict) and len(self.surfaces) > 0: - for k, v in self.surfaces.items(): - if isinstance(v, dict): - self.surfaces[k] = SurfaceConfig(**v) + for k, v in self.windows.items(): + if isinstance(v, dict): + self.windows[k] = WindowConfig(**v) + for k, v in self.sensors.items(): + if isinstance(v, dict): + self.sensors[k] = SensorConfig(**v) + for k, v in self.views.items(): + if isinstance(v, dict): + self.views[k] = ViewConfig(**v) + for k, v in self.surfaces.items(): + if isinstance(v, dict): + self.surfaces[k] = SurfaceConfig(**v) + + self.scene_cfg = True + self.windows_cfg = True + self.sensors_cfg = True + self.views_cfg = True + self.surfaces_cfg = True + + if self.scene == SceneConfig() or self.scene == {}: + self.scene_cfg = False + if self.windows == {}: + self.windows_cfg = False + if self.sensors == {}: + self.sensors_cfg = False + if self.views == {}: + self.views_cfg = False + if self.surfaces == {}: + self.surfaces_cfg = False + + # add view to sensors if not already there + for k, v in self.views.items(): + if k in self.sensors: + if self.sensors[k].data == [ + self.views[k].view.position + self.views[k].view.direction + ]: + continue + else: + raise ValueError( + f"Sensor {k} data does not match view {k} data" + ) + else: + self.sensors[k] = SensorConfig( + data=[ + self.views[k].view.position + + self.views[k].view.direction + ] + ) + for k, v in self.windows.items(): + if v.matrix_name != "": + if v.matrix_name not in self.materials.matrices: + raise ValueError( + f"{k} matrix name {v.matrix_name} not found in materials" + ) @dataclass class WorkflowConfig: @@ -454,16 +522,18 @@ class WorkflowConfig: def __post_init__(self): if ( - self.model.sensors == {} - and self.model.views == {} - and self.model.surfaces == {} + not self.model.sensors_cfg + and not self.model.views_cfg + and not self.model.surfaces_cfg ): raise ValueError( f"Sensors, views, or surfaces must be specified for {self.settings.method} method" ) - if self.model.scene == {} and self.model.windows == {}: + if ( + self.settings.method == "3phase" or self.settings.method == "5phase" + ) and not self.model.windows_cfg: raise ValueError( - f"Scene or windows must be specified for {self.settings.method} method" + f"Windows must be specified in Model for the {self.settings.method} method" ) if isinstance(self.settings, dict): self.settings = Settings(**self.settings) @@ -529,7 +599,10 @@ def __init__(self, config: WorkflowConfig): ) for name, surface in self.config.model.surfaces.items(): polygons = [parse_polygon(p) for p in surface.primitives] - flipped_primitives = [polygon_primitive(p.flip(), s.modifier, s.identifier) for p, s in zip(polygons, surface.primitives)] + flipped_primitives = [ + polygon_primitive(p.flip(), s.modifier, s.identifier) + for p, s in zip(polygons, surface.primitives) + ] self.surface_senders[name] = SurfaceSender( surfaces=flipped_primitives, basis=surface.basis, diff --git a/test/test_eplus.py b/test/test_eplus.py index 04c48c2..4971e79 100644 --- a/test/test_eplus.py +++ b/test/test_eplus.py @@ -10,6 +10,7 @@ def medium_office(): return load_energyplus_model(ref_models["medium_office"]) + @pytest.fixture def glazing_path(resources_dir): return resources_dir / "igsdb_product_7406.json" @@ -22,7 +23,9 @@ def test_add_glazingsystem(medium_office, 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.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) @@ -30,7 +33,7 @@ def test_add_glazingsystem(medium_office, glazing_path): def test_add_lighting(medium_office): try: - medium_office.add_lighting("z1") # zone does not exist + medium_office.add_lighting("z1", 100) # zone does not exist assert False except ValueError: pass @@ -38,14 +41,16 @@ def test_add_lighting(medium_office): def test_add_lighting1(medium_office): try: - medium_office.add_lighting("Perimeter_bot_ZN_1") # zone already has lighting + medium_office.add_lighting( + "Perimeter_bot_ZN_1", 100 + ) # zone already has lighting assert False except ValueError: pass def test_add_lighting2(medium_office): - medium_office.add_lighting("Perimeter_bot_ZN_1", replace=True) + medium_office.add_lighting("Perimeter_bot_ZN_1", 100, replace=True) assert isinstance(medium_office.lights, dict) assert isinstance(medium_office.schedule_constant, dict) @@ -54,7 +59,9 @@ def test_add_lighting2(medium_office): def test_output_variable(medium_office): """Test adding output variable to an EnergyPlusModel.""" - medium_office.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 medium_office.output_variable.values() diff --git a/test/test_methods.py b/test/test_methods.py index 5589027..6dc91d7 100644 --- a/test/test_methods.py +++ b/test/test_methods.py @@ -1,6 +1,18 @@ from datetime import datetime -from frads.methods import TwoPhaseMethod, ThreePhaseMethod, WorkflowConfig +from frads.methods import ( + TwoPhaseMethod, + ThreePhaseMethod, + WorkflowConfig, + Model, + Settings, + SceneConfig, + WindowConfig, + MaterialConfig, + ViewConfig, + SensorConfig, + SurfaceConfig, +) from frads.window import create_glazing_system, Gap, Gas from frads.ep2rad import epmodel_to_radmodel from frads.eplus import load_energyplus_model @@ -35,16 +47,18 @@ def cfg(resources_dir, objects_dir): "windows": { "upper_glass": { "file": objects_dir / "upper_glass.rad", - "matrix_file": "blinds30", + "matrix_name": "blinds30", }, "lower_glass": { "file": objects_dir / "lower_glass.rad", - "matrix_file": "blinds30", + "matrix_name": "blinds30", }, }, "materials": { "files": [objects_dir / "materials.mat"], - "matrices": {"blinds30": {"matrix_file": resources_dir / "blinds30.xml"}}, + "matrices": { + "blinds30": {"matrix_file": resources_dir / "blinds30.xml"} + }, }, "sensors": { "wpi": {"file": resources_dir / "grid.txt"}, @@ -53,14 +67,200 @@ def cfg(resources_dir, objects_dir): }, }, "views": { - "view1": {"file": resources_dir / "v1a.vf", "xres": 16, "yres": 16} - }, - "surfaces": { + "view1": { + "file": resources_dir / "v1a.vf", + "xres": 16, + "yres": 16, + } }, + "surfaces": {}, }, } +@pytest.fixture +def scene(objects_dir): + return SceneConfig( + files=[ + objects_dir / "walls.rad", + objects_dir / "ceiling.rad", + objects_dir / "floor.rad", + objects_dir / "ground.rad", + ] + ) + + +@pytest.fixture +def window_1(objects_dir): + return WindowConfig( + file=objects_dir / "upper_glass.rad", + matrix_name="blinds30", + ) + + +@pytest.fixture +def window_2(objects_dir): + return WindowConfig( + file=objects_dir / "upper_glass.rad", + # matrix_name="blinds30", + ) + + +@pytest.fixture +def materials(objects_dir): + return MaterialConfig( + files=[objects_dir / "materials.mat"], + matrices={"blinds30": {"matrix_file": objects_dir / "blinds30.xml"}}, + ) + + +@pytest.fixture +def wpi(resources_dir): + return SensorConfig( + file=resources_dir / "grid.txt", + ) + + +@pytest.fixture +def sensor_view_1(): + return SensorConfig( + data=[[17, 5, 4, 1, 0, 0]], + ) + + +@pytest.fixture +def view_1(resources_dir): + return ViewConfig( + file=resources_dir / "v1a.vf", + xres=16, + yres=16, + ) + + +def test_model1(scene, window_1, materials, wpi, sensor_view_1, view_1): + model = Model( + scene=scene, + windows={"window_1": window_1}, + materials=materials, + sensors={"wpi": wpi, "view1": sensor_view_1}, + views={"view_1": view_1}, + ) + assert model.scene.files == scene.files + assert model.windows["window_1"].file == window_1.file + assert model.windows["window_1"].matrix_name == window_1.matrix_name + assert model.materials.files == materials.files + assert ( + model.materials.matrices["blinds30"].matrix_file + == materials.matrices["blinds30"].matrix_file + ) + assert model.windows["window_1"].matrix_name in model.materials.matrices + assert model.sensors["wpi"].file == wpi.file + assert model.sensors["view_1"].data == sensor_view_1.data + assert model.views["view_1"].file == view_1.file + assert model.views["view_1"].xres == view_1.xres + assert model.views["view_1"].yres == view_1.yres + + +def test_model2(materials, wpi, view_1): + # auto-generate view_1 in sensors from view_1 in views + model = Model( + materials=materials, + sensors={"wpi": wpi}, + views={"view_1": view_1}, + ) + assert "view_1" in model.sensors + assert model.sensors["view_1"].data == [ + model.views["view_1"].view.position + + model.views["view_1"].view.direction + ] + assert isinstance(model.scene, SceneConfig) + assert isinstance(model.windows, dict) + assert model.scene.files == [] + assert model.scene.bytes == b"" + assert model.windows == {} + + +def test_model3(scene, window_1, materials, wpi, view_1): + # same name view and sensor but different position and direction + sensor_view_2 = SensorConfig( + data=[[1, 5, 4, 1, 0, 0]], + ) + + with pytest.raises(ValueError): + Model( + scene=scene, + windows={"window_1": window_1}, + materials=materials, + sensors={"wpi": wpi, "view_1": sensor_view_2}, + views={"view_1": view_1}, + ) + + +def test_model4( + objects_dir, scene, window_1, materials, wpi, sensor_view_1, view_1 +): + # window matrix name not in materials + materials = MaterialConfig(files=[objects_dir / "materials.mat"]) + + with pytest.raises(ValueError): + Model( + scene=scene, + windows={"window_1": window_1}, + materials=materials, + sensors={"wpi": wpi, "view_1": sensor_view_1}, + views={"view_1": view_1}, + ) + + +def test_no_sensors_views_surfaces_specified(scene, window_1, materials): + settings = Settings() + model = Model( + scene=scene, + windows={"window_1": window_1}, + materials=materials, + ) + with pytest.raises(ValueError): + WorkflowConfig(settings, model) + + +def test_windows_not_specified_for_3phase_or_5phase_method( + scene, materials, wpi, sensor_view_1, view_1 +): + settings = Settings() + model = Model( + scene=scene, + # windows={"window_1": window_1}, + materials=materials, + sensors={"wpi": wpi}, + views={"view_1": view_1}, + ) + with pytest.raises(ValueError): + WorkflowConfig(settings, model) + + +def test_three_phase2(scene, window_2, materials, wpi, sensor_view_1, view_1): + model = Model( + scene=scene, + windows={"window_1": window_2}, # window_2 has no matrix_name + materials=materials, + sensors={"wpi": wpi, "view_1": sensor_view_1}, + views={"view_1": view_1}, + ) + settings = Settings() + + cfg = WorkflowConfig(settings, model) + workflow = ThreePhaseMethod(cfg) + workflow.generate_matrices(view_matrices=False) + a = workflow.calculate_sensor( + "view_1", + {"window_1": "blinds30"}, # blinds30 is the matrix_name + datetime(2023, 1, 1, 12), + 800, + 100, + ) + assert a.shape == (1, 1) + + def test_two_phase(cfg): time = datetime(2023, 1, 1, 12) dni = 800 @@ -78,7 +278,12 @@ def test_three_phase(cfg, resources_dir): dhi = 100 config = WorkflowConfig.from_dict(cfg) blind_prim = pr.Primitive( - "void", "aBSDF", "blinds30", [str(resources_dir/"blinds30.xml"), "0", "0", "1", "."], []) + "void", + "aBSDF", + "blinds30", + [str(resources_dir / "blinds30.xml"), "0", "0", "1", "."], + [], + ) config.model.materials.glazing_materials = {"blinds30": blind_prim} with ThreePhaseMethod(config) as workflow: workflow.generate_matrices(view_matrices=False) @@ -115,11 +320,19 @@ def test_eprad_threephase(resources_dir): 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=weather_files["usa_ca_san_francisco"]) + 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} - zone_dict["model"]["sensors"]["view1"] = {"data": [[17, 5, 4, 1, 0, 0]]} + zone_dict["model"]["views"]["view1"] = { + "file": view_path, + "xres": 16, + "yres": 16, + } + zone_dict["model"]["sensors"]["view1"] = { + "data": [[6.0, 7.0, 0.76, 0.0, -1.0, 0.0]] + } zone_dict["model"]["materials"]["matrices"] = { "ec60": {"matrix_file": shade_bsdf_path} } @@ -136,7 +349,7 @@ def test_eprad_threephase(resources_dir): edgps = rad_workflow.calculate_edgps( view="view1", bsdf={f"{zone}_Wall_South_Window": "ec60"}, - date_time=dt, + time=dt, dni=dni, dhi=dhi, ambient_bounce=1, @@ -150,7 +363,11 @@ def test_eprad_threephase(resources_dir): assert rad_workflow.view_senders["view1"].view.vert == 180 assert rad_workflow.view_senders["view1"].xres == 16 - assert list(rad_workflow.daylight_matrices.values())[0].array.shape == (145, 146, 3) + assert list(rad_workflow.daylight_matrices.values())[0].array.shape == ( + 145, + 146, + 3, + ) assert ( list(rad_workflow.sensor_window_matrices.values())[0].ncols == [145] and list(rad_workflow.sensor_window_matrices.values())[0].ncomp == 3