diff --git a/.gitignore b/.gitignore index d852e2a..54f10c7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ tox.ini #generate html model model.html -venv/* \ No newline at end of file +venv/* +*.devcontainer \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6698567..2ad2f51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.7 as base RUN apt-get update \ + && apt-get -y install ffmpeg libsm6 libxext6 xvfb --no-install-recommends \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 24138a6..5d9540b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ## Estidama-Daylight App +### An app to run compliance analysis for PBRS (Pearl Building Rating System) LBi-7 Daylight & Glare credit for Abu Dhabi ![App](/images/app.png) ## To run the app locally diff --git a/app.py b/app.py index cff8e85..d7f93da 100644 --- a/app.py +++ b/app.py @@ -6,7 +6,6 @@ from pathlib import Path from pollination_streamlit_io import get_host -from pollination_streamlit.api.client import ApiClient from model import sensor_grids from introduction import introduction @@ -79,15 +78,13 @@ def main(): st.session_state.api_client = api_client with tab3: - st.session_state.job_url = 'https://app.pollination.cloud/devang/projects/demo/studies/8123d1b6-71f5-414b-9601-269d5eda5c46' - st.session_state.api_client = ApiClient( - api_token='CC0E061B.0C4E4FA7BBA51BB7BEE453D3') - if 'job_url' not in st.session_state: st.error('Go back to the Simulation tab and submit the job.') return - sim_dict, res_dict = visualization(st.session_state.job_url, + sim_dict, res_dict = visualization(st.session_state.host, + st.session_state.hbjson_path, + st.session_state.job_url, st.session_state.api_client, st.session_state.temp_folder) if sim_dict and res_dict: diff --git a/estidama.py b/estidama.py index df8cca6..8953d13 100644 --- a/estidama.py +++ b/estidama.py @@ -308,11 +308,14 @@ def description(self) -> Union[None, str]: if self._month == 9: return 'Equinox' elif self._month == 6: - return 'Solstice' + return 'Summer Solstice' - def as_string(self) -> str: + def __str__(self) -> str: return f'{self._month}_{self._day}_{self._hour}' + def __repr__(self) -> str: + return f'{self.description()} @ {self._hour}:00' + SIM_TIMES = [PointInTime(9, 21, 10), PointInTime(9, 21, 12), PointInTime(9, 21, 14), PointInTime(6, 21, 10), PointInTime(6, 21, 12), PointInTime(6, 21, 14)] diff --git a/helper.py b/helper.py index 7536e81..1261901 100644 --- a/helper.py +++ b/helper.py @@ -3,6 +3,7 @@ import json import streamlit as st from pathlib import Path +from typing import Dict from honeybee.model import Model as HBModel from honeybee.room import Room @@ -48,3 +49,38 @@ def hash_model(hb_model: HBModel) -> dict: def hash_room(room: Room) -> dict: """Help Streamlit hash a Honeybee room object.""" return {'name': room.identifier, 'volume': room.volume, 'faces': len(room.faces)} + + +def create_analytical_mesh(results_folder: Path, hb_model: HBModel) -> dict: + """ Generate analysis grid for sketchup and rhino + + args: + results_folder: Path to the result folder with grids_info.json and .res files. + hb_model: A Honeybee model. + + returns: + An analytical mesh object. + """ + hb_model = hb_model.to_dict() + + info_file = results_folder.joinpath('grids_info.json') + info = json.loads(info_file.read_text()) + grids = hb_model['properties']['radiance']['sensor_grids'] + + geometries = [] + merged_values = [] + for i, grid in enumerate(info): + result_file = Path(results_folder, f"{grid['full_id']}.res") + values = [float(v) for v in result_file.read_text().splitlines()] + # clean dict + mesh = json.dumps(grids[i]['mesh']) + + merged_values += values + geometries.append(json.loads(mesh)) + + analytical_mesh = { + "type": "AnalyticalMesh", + "mesh": geometries, + "values": merged_values + } + return analytical_mesh diff --git a/images/app.png b/images/app.png index 66c00d1..eaf32b6 100644 Binary files a/images/app.png and b/images/app.png differ diff --git a/introduction.py b/introduction.py index 2fe009a..a390c84 100644 --- a/introduction.py +++ b/introduction.py @@ -120,6 +120,8 @@ def introduction(host: str, target_folder: Path, hbjson_path = write_hbjson(target_folder, hb_model) show_model(hbjson_path, target_folder, key='model') + else: + st.success('Model linked. Move to the next tab to select occupied areas.') else: hb_model = None diff --git a/model.py b/model.py index 687508d..92ace7b 100644 --- a/model.py +++ b/model.py @@ -14,7 +14,7 @@ from honeybee.facetype import Floor from honeybee_vtk.vtkjs.schema import SensorGridOptions -from pollination_streamlit_io import send_hbjson +from pollination_streamlit_io import send_hbjson, send_geometry from helper import write_hbjson, hash_model, hash_room from web import show_model @@ -64,6 +64,9 @@ def add_sensor_grids(hb_model: HBModel, rooms: List[Room], """ grids = [generate_room_grid(room, grid_size, tolerance) for room in rooms] + if hb_model.properties.radiance.sensor_grids: + hb_model.properties.radiance.sensor_grids = () + model = hb_model.duplicate() model.properties.radiance.add_sensor_grids(grids) diff --git a/requirements.txt b/requirements.txt index 1081bf2..d16b933 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ - pollination-streamlit-viewer>=0.4.5 pollination-streamlit-io>=0.31.3 pollination-streamlit>=0.3.0 streamlit>=1.11.0 streamlit-folium>=0.6.14 -honeybee-vtk >= 0.38.3 -pandas==1.3.5 +honeybee-vtk>=0.38.3 +pandas>=1.3.5 +plotly>=5.9.0 \ No newline at end of file diff --git a/result.py b/result.py index 1c9863b..e667eb6 100644 --- a/result.py +++ b/result.py @@ -172,11 +172,11 @@ def prepare_results(program: Program, occupied_rooms: List[Room], compliant_areas = [] file_name = f'grid_{room.display_name}.res' for sim_time in SIM_TIMES: - sim_id = sim_dict[sim_time.as_string()] + sim_id = sim_dict[str(sim_time)] res_file_path = res_file_dict[sim_id].joinpath(file_name) compliant_area = room.floor_area*percentage_complied( res_file_path, program.min_threshold) - data[sim_time.as_string()].append(compliant_area) + data[str(sim_time)].append(compliant_area) compliant_areas.append(compliant_area) average_compliant_area = mean(compliant_areas) @@ -186,7 +186,7 @@ def prepare_results(program: Program, occupied_rooms: List[Room], data['names'].append('Total') data['areas'].append(f'{total_area}') for sim_time in SIM_TIMES: - data[sim_time.as_string()].append('') + data[str(sim_time)].append('') data['average'].append(f'{total_average_area}') data_df = pd.DataFrame.from_dict(data) @@ -230,17 +230,17 @@ def result(program: Program, occupied_rooms: List[Room], st.plotly_chart(figure, use_container_width=True) if compliant_area_percentage >= program.credit_2_threshold: - st.success(f'{compliant_area_percentage*100}% area complies with the' - ' requirements. Therefore, 2 Credit points can be claimed.') + st.success(f'**{compliant_area_percentage*100}%** area complies with the' + ' requirements. Therefore, **2 Credit points** can be claimed.') additional_notes(program) st.balloons() elif compliant_area_percentage >= program.credit_1_threshold: - st.success(f'{compliant_area_percentage*100}% area complies with the' - ' requirements. Therefore, 1 Credit point can be claimed.') + st.success(f'**{compliant_area_percentage*100}%** area complies with the' + ' requirements. Therefore, **1 Credit point** can be claimed.') additional_notes(program) st.balloons() else: st.write( - f'Only {compliant_area_percentage*100}% area complies with the requirements.' - ' Hence, no credit point can be claimed.') + f'Only **{compliant_area_percentage*100}%** area complies with the' + ' requirements. Hence, no credit point can be claimed.') diff --git a/simulation.py b/simulation.py index 6c697a6..875d84a 100644 --- a/simulation.py +++ b/simulation.py @@ -71,7 +71,7 @@ def create_job(hbjson_path: Path, api_client: ApiClient, owner: str, project: st argument['sky'] = cie_sky(latitude, longitude, point.month, point.day, point.hour, north_angle, ground_reflectance) arguments.append(argument) - argument['month_day_hour'] = point.as_string() + argument['month_day_hour'] = str(point) new_job.arguments = arguments diff --git a/visualization.py b/visualization.py index ce8f34b..0c369a5 100644 --- a/visualization.py +++ b/visualization.py @@ -9,12 +9,16 @@ from typing import Dict, Tuple, Union from pathlib import Path +from honeybee.model import Model as HBModel + from pollination_streamlit_viewer import viewer from pollination_streamlit.api.client import ApiClient from pollination_streamlit.interactors import Job +from pollination_streamlit_io import send_hbjson, send_geometry from queenbee.job.job import JobStatusEnum from estidama import SIM_TIMES +from helper import create_analytical_mesh class SimStatus(Enum): @@ -146,13 +150,15 @@ def generate_dicts(job: Job, target_folder: Path) -> Tuple[Dict[str, str], return sim_dict, viz_dict, res_file_dict -def visualization(job_url: str, +def visualization(host: str, hbjson_path: Path, job_url: str, api_client: ApiClient, target_folder: Path) -> Tuple[Union[None, Dict[str, str]], Union[None, Dict[str, Path]]]: """UI of visualization tab of the Estidama-daylight app. args: + host: A string representing the environment the app is running inside. + hbjson_path: Path to the HBJSON file with grids. job_url: Valid URL of a job on Pollination as a string. api_client: ApiClient object containing Pollination credentials. target_folder: Path to the target folder where outputs from the finished job @@ -181,25 +187,41 @@ def visualization(job_url: str, st.write('See how much daylight the occupied areas receive on selected points' ' in time during the year.') - col0, col1 = st.columns(2) - - with col0: - for sim_time in SIM_TIMES[:3]: - id = sim_dict[sim_time.as_string()] - viz = viz_dict[id].joinpath('point_in_time.vtkjs') - st.write( - f'Daylight levels on {sim_time.description()} @ {sim_time.hour}:00') - viewer(key=f'{sim_time.as_string()}_viewer', - content=viz.read_bytes(), style={'height': '344px'}) - - with col1: - for sim_time in SIM_TIMES[3:]: - id = sim_dict[sim_time.as_string()] - viz = viz_dict[id].joinpath('point_in_time.vtkjs') - st.write( - f'Daylight levels on Summer {sim_time.description()} @ {sim_time.hour}:00') - viewer(key=f'{sim_time.as_string()}_viewer', - content=viz.read_bytes(), style={'height': '344px'}) + if host.lower() == 'web': + col0, col1 = st.columns(2) + + with col0: + for sim_time in SIM_TIMES[:3]: + id = sim_dict[str(sim_time)] + viz = viz_dict[id].joinpath('point_in_time.vtkjs') + st.write(f'{sim_time.description()} @ {sim_time.hour}:00') + viewer(key=f'{str(sim_time)}_viewer', + content=viz.read_bytes(), style={'height': '344px'}) + + with col1: + for sim_time in SIM_TIMES[3:]: + id = sim_dict[str(sim_time)] + viz = viz_dict[id].joinpath('point_in_time.vtkjs') + st.write(f'{sim_time.description()} @ {sim_time.hour}:00') + viewer(key=f'{str(sim_time)}_viewer', + content=viz.read_bytes(), style={'height': '344px'}) + + elif host.lower() == 'rhino' or host.lower() == 'sketchup': + + options = { + f'{sim_time.description()} @ {sim_time.hour}:00': sim_time + for sim_time in SIM_TIMES} + + option = st.radio(f'Select time', list(options.keys())) + + sim_time = options[option] + id = sim_dict[str(sim_time)] + res_path = res_file_dict[id] + + hb_model = HBModel.from_hbjson(hbjson_path) + send_hbjson(key='model-results', hbjson=hb_model.to_dict()) + analytical_mesh = create_analytical_mesh(res_path, hb_model) + send_geometry(key=f'{str(sim_time)}_viz', geometry=analytical_mesh) st.write('Go to the next tab to see the results.')