diff --git a/docs/assets/sda.png b/docs/assets/sda.png
new file mode 100644
index 0000000..ed1e016
Binary files /dev/null and b/docs/assets/sda.png differ
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/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/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/guide_rad2.md b/docs/how-to/guide_rad2.md
new file mode 100644
index 0000000..26ffcce
--- /dev/null
+++ b/docs/how-to/guide_rad2.md
@@ -0,0 +1,305 @@
+# How to set up a workflow configuration for Radiance simulation?
+
+**What is a workflow configuration?**
+
+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.
+
+The workflow configuration has two parts:
+
+1. `settings`
+
+ - 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.
+
+2. `model`
+
+ - i.e. scene, windows, materials, sensors, and views
+ - See the [Model](../ref/config.md#frads.methods.Model) class for more details.
+
+**How to set up a workflow configuration?**
+
+**Method 1**
+
+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.
+
+**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
+import frads as fr
+```
+
+## Method 1
+
+## 1.1 Create an instance of `Settings` class
+
+```python title="Create an instance of the Settings class"
+settings = fr.Settings()
+```
+
+??? note "Default setting"
+ **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 title="Edit default setting parameters"
+# Edit the number of parallel processes
+settings.num_processors = 4
+
+# Provide a wea file
+settings.wea_file = "oak.wea"
+```
+
+## 1.2 Create an instance of `Model` class
+
+The `Model` class requires the following parameters:
+
+* `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.
+
+### 1.2.1 Scene
+
+```python title="Create an instance of the SceneConfig class"
+scene = fr.SceneConfig(
+ files=[
+ "walls.rad",
+ "ceiling.rad",
+ "floor.rad",
+ "ground.rad",
+ ]
+)
+```
+
+??? example "Scene geometry primitive example"
+ **walls.rad**
+ ```
+ wall_mat polygon wall_1
+ 0
+ 0
+ 12
+ 0 0 0
+ 0 0 3
+ 0 3 3
+ 0 3 0
+ ```
+
+### 1.2.2 Windows
+
+```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 geometry primitive example"
+ **window1.rad**
+ ```
+ window_mat polygon window1
+ 0
+ 0
+ 12
+ 0 1 1
+ 0 1 2
+ 0 2 2
+ 0 2 1
+ ```
+
+### 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 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
+
+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 points example"
+ **grid.txt**
+
+ x_viewpoint y_viewpoint z_viewpoint x_direction y_direction z_direction
+ ```
+ 0 1 1 0 -1 0
+ 0 1 2 0 -1 0
+ ```
+
+### 1.2.5 Views
+
+```python
+view1 = fr.ViewConfig(file = "view1.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
+ ```
+
+### 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,
+ 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(settings, model)
+```
+
+## 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
+ 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)
+```
+
+
+!!! 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["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 b9890a5..db924a3 100644
--- a/docs/how-to/index.md
+++ b/docs/how-to/index.md
@@ -1,19 +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?
+## EnergyPlus
+
+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)
diff --git a/frads/__init__.py b/frads/__init__.py
index ae748cc..ba71367 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",
]
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/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.
diff --git a/frads/methods.py b/frads/methods.py
index a78e671..bf6df4d 100755
--- a/frads/methods.py
+++ b/frads/methods.py
@@ -86,7 +86,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:
@@ -115,7 +117,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 no file, bytes, or matrices are provided.
"""
files: List[Path] = field(default_factory=list)
@@ -131,6 +138,14 @@ 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
+ ):
+ raise ValueError(
+ "MaterialConfig must have either file, bytes or matrices"
+ )
@dataclass
@@ -146,14 +161,17 @@ 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.
+
+ Raises:
+ ValueError: If neither file nor bytes are provided.
"""
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)
@@ -163,8 +181,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, "rb") as f:
+ self.bytes = f.read()
+ else:
+ raise ValueError("WindowConfig must have either file or bytes")
@dataclass
@@ -179,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 = ""
@@ -189,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)
@@ -199,7 +220,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")
@@ -207,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
@@ -216,16 +254,34 @@ 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 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"
@@ -239,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
@@ -340,7 +398,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"]
@@ -366,11 +433,11 @@ 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=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)
surfaces: Dict[str, "SurfaceConfig"] = field(default_factory=dict)
# Make Path() out of all path strings
@@ -392,6 +459,48 @@ def __post_init__(self):
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:
@@ -412,11 +521,27 @@ class WorkflowConfig:
hash_str: str = field(init=False)
def __post_init__(self):
+ if (
+ 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.settings.method == "3phase" or self.settings.method == "5phase"
+ ) and not self.model.windows_cfg:
+ raise ValueError(
+ f"Windows must be specified in Model for the {self.settings.method} method"
+ )
if isinstance(self.settings, dict):
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":
@@ -474,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,
@@ -488,12 +616,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
@@ -518,6 +650,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")
@@ -558,7 +691,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).
@@ -573,14 +712,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",
@@ -596,7 +745,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(
@@ -619,7 +770,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,
@@ -711,7 +864,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
@@ -755,7 +910,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])
@@ -783,7 +942,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],
)
@@ -820,7 +981,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] = {}
@@ -829,9 +991,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
@@ -947,7 +1109,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(
@@ -983,9 +1147,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],
@@ -1018,7 +1186,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])
@@ -1089,12 +1261,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,
@@ -1108,7 +1286,7 @@ def calculate_edgps(
self,
view: str,
bsdf: Dict[str, str],
- date_time: datetime,
+ time: datetime,
dni: float,
dhi: float,
ambient_bounce: int = 1,
@@ -1119,7 +1297,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
@@ -1131,7 +1309,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,
@@ -1144,7 +1322,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)
@@ -1165,7 +1345,7 @@ def calculate_edgps(
ev = self.calculate_sensor(
view,
bsdf,
- date_time,
+ time,
dni,
dhi,
)
@@ -1234,7 +1414,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"
@@ -1255,7 +1437,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()
@@ -1270,7 +1454,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:
@@ -1294,8 +1480,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:
@@ -1341,7 +1529,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
@@ -1356,7 +1546,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
@@ -1368,7 +1560,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,
@@ -1420,10 +1614,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():
@@ -1508,7 +1706,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])
@@ -1521,7 +1723,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]),
@@ -1529,7 +1732,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):
@@ -1556,7 +1760,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],
@@ -1572,7 +1778,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],
diff --git a/frads/window.py b/frads/window.py
index b2c8490..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
@@ -243,7 +244,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 +275,17 @@ 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 not os.path.isfile(layer):
+ raise FileNotFoundError(f"{layer} does not exist.")
+ 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)
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
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