OpenSimula 0.6.1__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opensimula-0.6.1 → opensimula-0.7.1}/PKG-INFO +1 -2
- {opensimula-0.6.1 → opensimula-0.7.1}/pyproject.toml +2 -3
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Project.py +8 -8
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/__init__.py +1 -1
- opensimula-0.6.1/src/opensimula/components/HVAC_DX_equipment.py → opensimula-0.7.1/src/opensimula/components/DX_unit.py +3 -3
- opensimula-0.6.1/src/opensimula/components/HVAC_fan_equipment.py → opensimula-0.7.1/src/opensimula/components/Fan.py +3 -3
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/HVAC_DX_system.py +1 -1
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/HVAC_MZW_system.py +5 -5
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/HVAC_SZW_system.py +4 -4
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Surface.py +1 -1
- opensimula-0.6.1/src/opensimula/components/HVAC_coil_equipment.py → opensimula-0.7.1/src/opensimula/components/Water_coil.py +3 -3
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/__init__.py +3 -3
- opensimula-0.7.1/src/opensimula/visual_3D/Environment_3D.py +492 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/visual_3D/Polygon_3D.py +21 -26
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula.egg-info/PKG-INFO +1 -2
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula.egg-info/SOURCES.txt +3 -3
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula.egg-info/requires.txt +0 -1
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_HVAC_DX_Equipment.py +2 -2
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_HVAC_DX_system.py +1 -1
- opensimula-0.6.1/src/opensimula/visual_3D/Environment_3D.py +0 -257
- {opensimula-0.6.1 → opensimula-0.7.1}/LICENSE +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/README.md +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/setup.cfg +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Child.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Component.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Iterative_process.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Message.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Parameter_container.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Parameters.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Simulation.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/Variable.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Building.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Building_surface.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Calculator.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Construction.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Day_schedule.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/File_data.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/File_met.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Frame.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Glazing.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/HVAC_perfect_system.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Material.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Opening.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Opening_type.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Solar_surface.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Space.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Space_type.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Test_component.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Week_schedule.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula/components/Year_schedule.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula.egg-info/dependency_links.txt +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/src/opensimula.egg-info/top_level.txt +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Calculator.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Case_600FF.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Case_610.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Construction.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_File_data.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_File_met.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Iterative_process.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Project.py +0 -0
- {opensimula-0.6.1 → opensimula-0.7.1}/test/test_Schedules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensimula
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Component-based time simulation environment in Python
|
|
5
5
|
Author-email: "Juan F. Coronel" <jfc@us.es>
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -18,7 +18,6 @@ Requires-Dist: psychrolib>=2.5.0
|
|
|
18
18
|
Requires-Dist: scipy>=1.16.2
|
|
19
19
|
Requires-Dist: shapely>=2.1.1
|
|
20
20
|
Requires-Dist: tqdm>=4.67.1
|
|
21
|
-
Requires-Dist: triangle>=20250106
|
|
22
21
|
Requires-Dist: vedo>=2025.5.4
|
|
23
22
|
Dynamic: license-file
|
|
24
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opensimula"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
requires-python = ">=3.12"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name="Juan F. Coronel", email="jfc@us.es" },
|
|
@@ -20,8 +20,7 @@ dependencies = [
|
|
|
20
20
|
"scipy>=1.16.2",
|
|
21
21
|
"shapely>=2.1.1",
|
|
22
22
|
"tqdm>=4.67.1",
|
|
23
|
-
|
|
24
|
-
"vedo>=2025.5.4",
|
|
23
|
+
"vedo>=2025.5.4",
|
|
25
24
|
]
|
|
26
25
|
|
|
27
26
|
[dependency-groups]
|
|
@@ -676,23 +676,23 @@ class Project(Parameter_container):
|
|
|
676
676
|
if hasattr(component, "get_polygon_3D"):
|
|
677
677
|
env_3D.add_polygon_3D(component.get_polygon_3D())
|
|
678
678
|
|
|
679
|
-
def show_3D(self):
|
|
679
|
+
def show_3D(self, jupyter=False):
|
|
680
680
|
env_3D = Environment_3D()
|
|
681
681
|
self.create_3D_environment(env_3D)
|
|
682
|
-
env_3D.show(polygons_type="initial")
|
|
683
|
-
|
|
684
|
-
def show_3D_shadows(self, date):
|
|
682
|
+
env_3D.show(polygons_type="initial", jupyter=jupyter)
|
|
683
|
+
|
|
684
|
+
def show_3D_shadows(self, date, jupyter=False):
|
|
685
685
|
env_3D = Environment_3D()
|
|
686
686
|
self.create_3D_environment(env_3D)
|
|
687
687
|
file_met = self.parameter("simulation_file_met").component
|
|
688
688
|
cos = file_met.sun_cosines(date)
|
|
689
689
|
if len(cos) == 3:
|
|
690
690
|
env_3D.calculate_shadows(cos)
|
|
691
|
-
env_3D.show(polygons_type="Building_shadows")
|
|
691
|
+
env_3D.show(polygons_type="Building_shadows", jupyter=jupyter)
|
|
692
692
|
else:
|
|
693
693
|
self._sim_.message(Message(date.strftime("%H:%M, %d/%m/%Y") + " is night", "WARNING"))
|
|
694
694
|
|
|
695
|
-
def show_3D_shadows_animation(self, date):
|
|
695
|
+
def show_3D_shadows_animation(self, date, jupyter=False):
|
|
696
696
|
env_3D = Environment_3D()
|
|
697
697
|
self.create_3D_environment(env_3D)
|
|
698
698
|
file_met = self.parameter("simulation_file_met").component
|
|
@@ -701,9 +701,9 @@ class Project(Parameter_container):
|
|
|
701
701
|
for hour in range(24):
|
|
702
702
|
new_date = date.replace(hour=hour, minute=0, second=0)
|
|
703
703
|
cos = file_met.sun_cosines(new_date)
|
|
704
|
-
if len(cos) == 3:
|
|
704
|
+
if len(cos) == 3: # Day time
|
|
705
705
|
texts.append(new_date.strftime("%H:%M, %d/%m/%Y"))
|
|
706
706
|
cosines.append(cos)
|
|
707
|
-
env_3D.show_animation(texts, cosines, polygons_type="Building_shadows")
|
|
707
|
+
env_3D.show_animation(texts, cosines, polygons_type="Building_shadows", jupyter=jupyter)
|
|
708
708
|
|
|
709
709
|
|
|
@@ -3,11 +3,11 @@ from opensimula.Parameters import Parameter_float, Parameter_float_list, Paramet
|
|
|
3
3
|
from opensimula.Component import Component
|
|
4
4
|
from scipy.optimize import fsolve
|
|
5
5
|
|
|
6
|
-
class
|
|
6
|
+
class DX_unit(Component):
|
|
7
7
|
def __init__(self, name, project):
|
|
8
8
|
Component.__init__(self, name, project)
|
|
9
|
-
self.parameter("type").value = "
|
|
10
|
-
self.parameter("description").value = "
|
|
9
|
+
self.parameter("type").value = "DX_unit"
|
|
10
|
+
self.parameter("description").value = "Direct Expansion unit manufacturer information"
|
|
11
11
|
self.add_parameter(Parameter_float("nominal_air_flow", 1, "m³/s", min=0))
|
|
12
12
|
self.add_parameter(Parameter_float("nominal_total_cooling_capacity", 0, "W", min=0))
|
|
13
13
|
self.add_parameter(Parameter_float("nominal_sensible_cooling_capacity", 0, "W", min=0))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from opensimula.Parameters import Parameter_float, Parameter_math_exp
|
|
2
2
|
from opensimula.Component import Component
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class Fan(Component):
|
|
5
5
|
def __init__(self, name, project):
|
|
6
6
|
Component.__init__(self, name, project)
|
|
7
|
-
self.parameter("type").value = "
|
|
8
|
-
self.parameter("description").value = "
|
|
7
|
+
self.parameter("type").value = "Fan"
|
|
8
|
+
self.parameter("description").value = "Fan equipment manufacturer information"
|
|
9
9
|
self.add_parameter(Parameter_float("nominal_air_flow", 1, "m³/s", min=0))
|
|
10
10
|
self.add_parameter(Parameter_float("nominal_pressure", 1, "Pa", min=0))
|
|
11
11
|
self.add_parameter(Parameter_float("nominal_power", 1, "W", min=0))
|
|
@@ -10,7 +10,7 @@ class HVAC_DX_system(Component):
|
|
|
10
10
|
Component.__init__(self, name, project)
|
|
11
11
|
self.parameter("type").value = "HVAC_DX_system"
|
|
12
12
|
self.parameter("description").value = "HVAC Direct Expansion system for time simulation"
|
|
13
|
-
self.add_parameter(Parameter_component("equipment", "not_defined", ["
|
|
13
|
+
self.add_parameter(Parameter_component("equipment", "not_defined", ["DX_unit"]))
|
|
14
14
|
self.add_parameter(Parameter_component("spaces", "not_defined", ["Space"])) # Space, TODO: Add Air_distribution, Energy_load
|
|
15
15
|
self.add_parameter(Parameter_float("air_flow", 1, "m³/s", min=0))
|
|
16
16
|
self.add_parameter(Parameter_math_exp("outdoor_air_fraction", "0", "frac"))
|
|
@@ -32,16 +32,16 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
|
|
|
32
32
|
)
|
|
33
33
|
)
|
|
34
34
|
self.add_parameter(
|
|
35
|
-
Parameter_component("cooling_coil", "not_defined", ["
|
|
35
|
+
Parameter_component("cooling_coil", "not_defined", ["Water_coil"])
|
|
36
36
|
)
|
|
37
37
|
self.add_parameter(
|
|
38
|
-
Parameter_component("heating_coil", "not_defined", ["
|
|
38
|
+
Parameter_component("heating_coil", "not_defined", ["Water_coil"])
|
|
39
39
|
)
|
|
40
40
|
self.add_parameter(
|
|
41
|
-
Parameter_component("supply_fan", "not_defined", ["
|
|
41
|
+
Parameter_component("supply_fan", "not_defined", ["Fan"])
|
|
42
42
|
)
|
|
43
43
|
self.add_parameter(
|
|
44
|
-
Parameter_component("return_fan", "not_defined", ["
|
|
44
|
+
Parameter_component("return_fan", "not_defined", ["Fan"])
|
|
45
45
|
)
|
|
46
46
|
self.add_parameter(Parameter_float("air_flow", 1, "m³/s", min=0))
|
|
47
47
|
self.add_parameter(
|
|
@@ -108,7 +108,7 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
|
|
|
108
108
|
self.add_parameter(Parameter_boolean("vav", False))
|
|
109
109
|
self.add_parameter(
|
|
110
110
|
Parameter_component_list(
|
|
111
|
-
"reheat_coils", ["not_defined", "not_defined"], ["
|
|
111
|
+
"reheat_coils", ["not_defined", "not_defined"], ["Water_coil"]
|
|
112
112
|
)
|
|
113
113
|
)
|
|
114
114
|
|
|
@@ -20,16 +20,16 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
|
|
|
20
20
|
Parameter_component("space", "not_defined", ["Space"])
|
|
21
21
|
) # Space
|
|
22
22
|
self.add_parameter(
|
|
23
|
-
Parameter_component("cooling_coil", "not_defined", ["
|
|
23
|
+
Parameter_component("cooling_coil", "not_defined", ["Water_coil"])
|
|
24
24
|
)
|
|
25
25
|
self.add_parameter(
|
|
26
|
-
Parameter_component("heating_coil", "not_defined", ["
|
|
26
|
+
Parameter_component("heating_coil", "not_defined", ["Water_coil"])
|
|
27
27
|
)
|
|
28
28
|
self.add_parameter(
|
|
29
|
-
Parameter_component("supply_fan", "not_defined", ["
|
|
29
|
+
Parameter_component("supply_fan", "not_defined", ["Fan"])
|
|
30
30
|
)
|
|
31
31
|
self.add_parameter(
|
|
32
|
-
Parameter_component("return_fan", "not_defined", ["
|
|
32
|
+
Parameter_component("return_fan", "not_defined", ["Fan"])
|
|
33
33
|
)
|
|
34
34
|
self.add_parameter(Parameter_float("air_flow", 1, "m³/s", min=0))
|
|
35
35
|
self.add_parameter(Parameter_float("return_air_flow", 1, "m³/s", min=0))
|
|
@@ -12,7 +12,7 @@ class Surface(Component):
|
|
|
12
12
|
Component.__init__(self, name, project)
|
|
13
13
|
# Parameters
|
|
14
14
|
self.parameter("type").value = "Surface"
|
|
15
|
-
self.add_parameter(Parameter_float("azimuth", 0, "°", min=-180, max=180)) #
|
|
15
|
+
self.add_parameter(Parameter_float("azimuth", 0, "°", min=-180, max=180)) # Angle from building -Y axis to horizontal projection of exterior normal -> S: 0º, E: 90º, W: -90º, N: 180º
|
|
16
16
|
self.add_parameter(Parameter_float("altitude", 0, "°", min=-90, max=90)) # Surface y vs Building y -> vertical: 0º, facing up: 90º, facing down: -90º
|
|
17
17
|
self.add_parameter(Parameter_options(
|
|
18
18
|
"shape", "RECTANGLE", ["RECTANGLE", "POLYGON"]))
|
|
@@ -4,11 +4,11 @@ from opensimula.Component import Component
|
|
|
4
4
|
from scipy.optimize import fsolve
|
|
5
5
|
import psychrolib as sicro
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class Water_coil(Component):
|
|
8
8
|
def __init__(self, name, project):
|
|
9
9
|
Component.__init__(self, name, project)
|
|
10
|
-
self.parameter("type").value = "
|
|
11
|
-
self.parameter("description").value = "
|
|
10
|
+
self.parameter("type").value = "Water_coil"
|
|
11
|
+
self.parameter("description").value = "Water coil equipment manufacturer information"
|
|
12
12
|
self.add_parameter(Parameter_float("nominal_air_flow", 1, "m³/s", min=0))
|
|
13
13
|
self.add_parameter(Parameter_float("nominal_cooling_water_flow", 1, "m³/s", min=0))
|
|
14
14
|
self.add_parameter(Parameter_float("nominal_total_cooling_capacity", 0, "W", min=0))
|
|
@@ -16,9 +16,9 @@ from opensimula.components.Building_surface import Building_surface
|
|
|
16
16
|
from opensimula.components.Solar_surface import Solar_surface
|
|
17
17
|
from opensimula.components.Opening import Opening
|
|
18
18
|
from opensimula.components.Calculator import Calculator
|
|
19
|
-
from opensimula.components.
|
|
20
|
-
from opensimula.components.
|
|
21
|
-
from opensimula.components.
|
|
19
|
+
from opensimula.components.DX_unit import DX_unit
|
|
20
|
+
from opensimula.components.Water_coil import Water_coil
|
|
21
|
+
from opensimula.components.Fan import Fan
|
|
22
22
|
from opensimula.components.HVAC_DX_system import HVAC_DX_system
|
|
23
23
|
from opensimula.components.HVAC_SZW_system import HVAC_SZW_system
|
|
24
24
|
from opensimula.components.HVAC_MZW_system import HVAC_MZW_system
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import numpy as np
|
|
3
|
+
import math
|
|
4
|
+
from scipy.interpolate import RegularGridInterpolator
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import vedo as vedo
|
|
7
|
+
|
|
8
|
+
class Environment_3D:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.pol_3D = []
|
|
11
|
+
self.pol_sunny = []
|
|
12
|
+
self.pol_shadows = []
|
|
13
|
+
self.sunny_fraction = []
|
|
14
|
+
self.solar_tables_calculated = False
|
|
15
|
+
|
|
16
|
+
def add_polygon_3D(self, polygon_3D):
|
|
17
|
+
self.pol_3D.append(polygon_3D)
|
|
18
|
+
self.sunny_fraction.append(1.0)
|
|
19
|
+
|
|
20
|
+
def get_vedo_meshes(self, polygons_type="initial"):
|
|
21
|
+
meshes = []
|
|
22
|
+
if polygons_type == "initial":
|
|
23
|
+
for polygon_3D in self.pol_3D:
|
|
24
|
+
if polygon_3D.visible:
|
|
25
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
26
|
+
mesh.polygon_name = polygon_3D.name
|
|
27
|
+
meshes.append(mesh)
|
|
28
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
29
|
+
elif polygons_type == "sunny":
|
|
30
|
+
for polygon_3D in self.pol_sunny:
|
|
31
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
32
|
+
mesh.polygon_name = polygon_3D.name
|
|
33
|
+
meshes.append(mesh)
|
|
34
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
35
|
+
elif polygons_type == "shadows":
|
|
36
|
+
for polygon_3D in self.pol_shadows:
|
|
37
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
38
|
+
mesh.polygon_name = polygon_3D.name
|
|
39
|
+
meshes.append(mesh)
|
|
40
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
41
|
+
elif polygons_type == "sunny+shadows":
|
|
42
|
+
for polygon_3D in self.pol_sunny:
|
|
43
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
44
|
+
mesh.polygon_name = polygon_3D.name
|
|
45
|
+
meshes.append(mesh)
|
|
46
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
47
|
+
for polygon_3D in self.pol_shadows:
|
|
48
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
49
|
+
mesh.polygon_name = polygon_3D.name
|
|
50
|
+
meshes.append(mesh)
|
|
51
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
52
|
+
elif polygons_type == "Building_shadows":
|
|
53
|
+
for polygon_3D in self.pol_sunny:
|
|
54
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
55
|
+
mesh.polygon_name = polygon_3D.name
|
|
56
|
+
meshes.append(mesh)
|
|
57
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
58
|
+
for polygon_3D in self.pol_shadows:
|
|
59
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
60
|
+
mesh.polygon_name = polygon_3D.name
|
|
61
|
+
meshes.append(mesh)
|
|
62
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
63
|
+
for polygon_3D in self.pol_3D:
|
|
64
|
+
if polygon_3D.visible and polygon_3D.calculate_shadows == False:
|
|
65
|
+
mesh = polygon_3D.get_vedo_mesh()
|
|
66
|
+
mesh.polygon_name = polygon_3D.name
|
|
67
|
+
meshes.append(mesh)
|
|
68
|
+
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
69
|
+
|
|
70
|
+
return meshes
|
|
71
|
+
|
|
72
|
+
# ── Plotly helpers (jupyter mode) ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def _rgb_str_(self, color_name):
|
|
75
|
+
rgb = vedo.colors.get_color(color_name)
|
|
76
|
+
return f"rgb({int(rgb[0]*255)},{int(rgb[1]*255)},{int(rgb[2]*255)})"
|
|
77
|
+
|
|
78
|
+
def _polygon_to_plotly_(self, polygon_3D):
|
|
79
|
+
"""Returns (go.Mesh3d, go.Scatter3d outline) for a single Polygon_3D."""
|
|
80
|
+
import plotly.graph_objects as go
|
|
81
|
+
points, faces = polygon_3D._triangulate_()
|
|
82
|
+
pts = np.array(points)
|
|
83
|
+
fcs = np.array(faces)
|
|
84
|
+
color = self._rgb_str_(polygon_3D.color)
|
|
85
|
+
mesh = go.Mesh3d(
|
|
86
|
+
x=pts[:, 0], y=pts[:, 1], z=pts[:, 2],
|
|
87
|
+
i=fcs[:, 0], j=fcs[:, 1], k=fcs[:, 2],
|
|
88
|
+
color=color, opacity=polygon_3D.opacity,
|
|
89
|
+
name=polygon_3D.name,
|
|
90
|
+
hovertemplate=f"<b>{polygon_3D.name}</b><extra></extra>",
|
|
91
|
+
flatshading=True,
|
|
92
|
+
lighting=dict(ambient=0.85, diffuse=0.5, specular=0.05),
|
|
93
|
+
)
|
|
94
|
+
boundary = np.array(polygon_3D.polygon3D)
|
|
95
|
+
xs = list(boundary[:, 0]) + [boundary[0, 0], None]
|
|
96
|
+
ys = list(boundary[:, 1]) + [boundary[0, 1], None]
|
|
97
|
+
zs = list(boundary[:, 2]) + [boundary[0, 2], None]
|
|
98
|
+
outline = go.Scatter3d(
|
|
99
|
+
x=xs, y=ys, z=zs,
|
|
100
|
+
mode="lines",
|
|
101
|
+
line=dict(color="black", width=2),
|
|
102
|
+
showlegend=False, hoverinfo="skip",
|
|
103
|
+
)
|
|
104
|
+
return mesh, outline
|
|
105
|
+
|
|
106
|
+
def _merge_to_plotly_(self, polygon_list, name=""):
|
|
107
|
+
"""Merge multiple Polygon_3D into a single go.Mesh3d (for animation frames)."""
|
|
108
|
+
import plotly.graph_objects as go
|
|
109
|
+
if not polygon_list:
|
|
110
|
+
return go.Mesh3d(x=[0], y=[0], z=[0], i=[], j=[], k=[],
|
|
111
|
+
visible=False, showlegend=False, hoverinfo="skip")
|
|
112
|
+
all_pts, all_i, all_j, all_k, all_colors = [], [], [], [], []
|
|
113
|
+
offset = 0
|
|
114
|
+
for pol in polygon_list:
|
|
115
|
+
points, faces = pol._triangulate_()
|
|
116
|
+
pts = np.array(points)
|
|
117
|
+
fcs = np.array(faces)
|
|
118
|
+
all_pts.append(pts)
|
|
119
|
+
all_i.extend((fcs[:, 0] + offset).tolist())
|
|
120
|
+
all_j.extend((fcs[:, 1] + offset).tolist())
|
|
121
|
+
all_k.extend((fcs[:, 2] + offset).tolist())
|
|
122
|
+
c = self._rgb_str_(pol.color)
|
|
123
|
+
all_colors.extend([c] * len(fcs))
|
|
124
|
+
offset += len(pts)
|
|
125
|
+
pts_arr = np.concatenate(all_pts, axis=0)
|
|
126
|
+
return go.Mesh3d(
|
|
127
|
+
x=pts_arr[:, 0], y=pts_arr[:, 1], z=pts_arr[:, 2],
|
|
128
|
+
i=all_i, j=all_j, k=all_k,
|
|
129
|
+
facecolor=all_colors,
|
|
130
|
+
flatshading=True, name=name,
|
|
131
|
+
hoverinfo="skip",
|
|
132
|
+
lighting=dict(ambient=0.85, diffuse=0.5, specular=0.05),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _merge_outlines_to_plotly_(self, polygon_list):
|
|
136
|
+
"""Merge outlines of multiple Polygon_3D into a single go.Scatter3d."""
|
|
137
|
+
import plotly.graph_objects as go
|
|
138
|
+
xs, ys, zs = [], [], []
|
|
139
|
+
for pol in polygon_list:
|
|
140
|
+
boundary = np.array(pol.polygon3D)
|
|
141
|
+
if len(boundary) == 0:
|
|
142
|
+
continue
|
|
143
|
+
xs.extend(list(boundary[:, 0]) + [boundary[0, 0], None])
|
|
144
|
+
ys.extend(list(boundary[:, 1]) + [boundary[0, 1], None])
|
|
145
|
+
zs.extend(list(boundary[:, 2]) + [boundary[0, 2], None])
|
|
146
|
+
return go.Scatter3d(
|
|
147
|
+
x=xs, y=ys, z=zs,
|
|
148
|
+
mode="lines",
|
|
149
|
+
line=dict(color="black", width=2),
|
|
150
|
+
showlegend=False, hoverinfo="skip",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _scene_bounds_(self, pols):
|
|
154
|
+
"""Return (xmin, xmax, ymin, ymax, zmin, zmax) for a list of Polygon_3D."""
|
|
155
|
+
all_pts = np.concatenate([np.array(p.polygon3D) for p in pols if len(p.polygon3D) > 0], axis=0)
|
|
156
|
+
return (all_pts[:, 0].min(), all_pts[:, 0].max(),
|
|
157
|
+
all_pts[:, 1].min(), all_pts[:, 1].max(),
|
|
158
|
+
all_pts[:, 2].min(), all_pts[:, 2].max())
|
|
159
|
+
|
|
160
|
+
def _zero_plane_traces_(self, pols):
|
|
161
|
+
"""Return thin-line Scatter3d outlines for the x=0, y=0, z=0 planes."""
|
|
162
|
+
import plotly.graph_objects as go
|
|
163
|
+
if not pols:
|
|
164
|
+
return []
|
|
165
|
+
xmin, xmax, ymin, ymax, zmin, zmax = self._scene_bounds_(pols)
|
|
166
|
+
style = dict(color="rgba(100,100,100,0.5)", width=1)
|
|
167
|
+
traces = []
|
|
168
|
+
|
|
169
|
+
def rect_trace(xs, ys, zs):
|
|
170
|
+
return go.Scatter3d(
|
|
171
|
+
x=xs, y=ys, z=zs,
|
|
172
|
+
mode="lines",
|
|
173
|
+
line=style,
|
|
174
|
+
showlegend=False, hoverinfo="skip",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# z=0 plane outline (only if 0 is within z range)
|
|
178
|
+
if zmin <= 0 <= zmax:
|
|
179
|
+
traces.append(rect_trace(
|
|
180
|
+
[xmin, xmax, xmax, xmin, xmin],
|
|
181
|
+
[ymin, ymin, ymax, ymax, ymin],
|
|
182
|
+
[0, 0, 0, 0, 0],
|
|
183
|
+
))
|
|
184
|
+
# x=0 plane outline
|
|
185
|
+
if xmin <= 0 <= xmax:
|
|
186
|
+
traces.append(rect_trace(
|
|
187
|
+
[0, 0, 0, 0, 0],
|
|
188
|
+
[ymin, ymax, ymax, ymin, ymin],
|
|
189
|
+
[zmin, zmin, zmax, zmax, zmin],
|
|
190
|
+
))
|
|
191
|
+
# y=0 plane outline
|
|
192
|
+
if ymin <= 0 <= ymax:
|
|
193
|
+
traces.append(rect_trace(
|
|
194
|
+
[xmin, xmax, xmax, xmin, xmin],
|
|
195
|
+
[0, 0, 0, 0, 0],
|
|
196
|
+
[zmin, zmin, zmax, zmax, zmin],
|
|
197
|
+
))
|
|
198
|
+
return traces
|
|
199
|
+
|
|
200
|
+
def _plotly_scene_layout_(self):
|
|
201
|
+
import plotly.graph_objects as go
|
|
202
|
+
axis_style = dict(
|
|
203
|
+
showbackground=True,
|
|
204
|
+
backgroundcolor="rgb(220, 220, 220)",
|
|
205
|
+
tickfont=dict(size=9, color="gray"),
|
|
206
|
+
title_font=dict(size=10, color="gray"),
|
|
207
|
+
)
|
|
208
|
+
return go.Layout(
|
|
209
|
+
paper_bgcolor="rgb(235, 235, 235)",
|
|
210
|
+
scene=dict(
|
|
211
|
+
xaxis=dict(title="X [m]", **axis_style),
|
|
212
|
+
yaxis=dict(title="Y [m]", **axis_style),
|
|
213
|
+
zaxis=dict(title="Z [m]", **axis_style),
|
|
214
|
+
aspectmode="data",
|
|
215
|
+
bgcolor="rgb(235, 235, 235)",
|
|
216
|
+
),
|
|
217
|
+
margin=dict(l=0, r=0, t=0, b=0),
|
|
218
|
+
showlegend=False,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# ── Public show methods ─────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
def show(self, polygons_type="initial", jupyter=False):
|
|
224
|
+
if jupyter:
|
|
225
|
+
import plotly.graph_objects as go
|
|
226
|
+
if polygons_type == "initial":
|
|
227
|
+
pols = [p for p in self.pol_3D if p.visible]
|
|
228
|
+
elif polygons_type == "Building_shadows":
|
|
229
|
+
pols = (list(self.pol_sunny) + list(self.pol_shadows) +
|
|
230
|
+
[p for p in self.pol_3D if p.visible and not p.calculate_shadows])
|
|
231
|
+
elif polygons_type == "sunny":
|
|
232
|
+
pols = list(self.pol_sunny)
|
|
233
|
+
elif polygons_type == "shadows":
|
|
234
|
+
pols = list(self.pol_shadows)
|
|
235
|
+
else:
|
|
236
|
+
pols = list(self.pol_sunny) + list(self.pol_shadows)
|
|
237
|
+
traces = []
|
|
238
|
+
for pol in pols:
|
|
239
|
+
mesh, outline = self._polygon_to_plotly_(pol)
|
|
240
|
+
traces.extend([mesh, outline])
|
|
241
|
+
traces.extend(self._zero_plane_traces_(pols))
|
|
242
|
+
go.Figure(data=traces, layout=self._plotly_scene_layout_()).show()
|
|
243
|
+
else:
|
|
244
|
+
vedo.settings.default_backend = "vtk"
|
|
245
|
+
meshes = self.get_vedo_meshes(polygons_type)
|
|
246
|
+
text_obj = [None]
|
|
247
|
+
|
|
248
|
+
def on_left_click(evt):
|
|
249
|
+
if text_obj[0] is not None:
|
|
250
|
+
vp.remove(text_obj[0])
|
|
251
|
+
msh = evt.object
|
|
252
|
+
if not msh:
|
|
253
|
+
text_obj[0] = vedo.Text2D(" ", pos='top-left')
|
|
254
|
+
else:
|
|
255
|
+
text_obj[0] = vedo.Text2D(f"{msh.polygon_name}", pos='top-left')
|
|
256
|
+
vp.add(text_obj[0])
|
|
257
|
+
vp.render()
|
|
258
|
+
|
|
259
|
+
vp = vedo.Plotter(title="opensimula")
|
|
260
|
+
vp.add_callback('mouse click', on_left_click)
|
|
261
|
+
vp.show(*meshes, axes=1, viewup="z").close()
|
|
262
|
+
|
|
263
|
+
def show_animation(self, texts, cosines, polygons_type="initial", jupyter=False):
|
|
264
|
+
if jupyter:
|
|
265
|
+
import plotly.graph_objects as go
|
|
266
|
+
|
|
267
|
+
# Static traces: surfaces that don't participate in shadow calculation
|
|
268
|
+
static_pols = [p for p in self.pol_3D if p.visible and not p.calculate_shadows]
|
|
269
|
+
static_traces = []
|
|
270
|
+
for pol in static_pols:
|
|
271
|
+
mesh, outline = self._polygon_to_plotly_(pol)
|
|
272
|
+
static_traces.extend([mesh, outline])
|
|
273
|
+
all_pols = list(self.pol_3D) + list(self.pol_sunny) + list(self.pol_shadows)
|
|
274
|
+
static_traces.extend(self._zero_plane_traces_([p for p in all_pols if len(p.polygon3D) > 0]))
|
|
275
|
+
n_static = len(static_traces)
|
|
276
|
+
|
|
277
|
+
# Build frames (sunny + shadow merged meshes update per frame)
|
|
278
|
+
self.calculate_shadows(cosines[0], create_polygons=True)
|
|
279
|
+
init_sunny = self._merge_to_plotly_(self.pol_sunny, "sunny")
|
|
280
|
+
init_sunny_outline = self._merge_outlines_to_plotly_(self.pol_sunny)
|
|
281
|
+
init_shadow = self._merge_to_plotly_(self.pol_shadows, "shadow")
|
|
282
|
+
init_shadow_outline = self._merge_outlines_to_plotly_(self.pol_shadows)
|
|
283
|
+
|
|
284
|
+
frames = []
|
|
285
|
+
for i, (cos, text) in enumerate(zip(cosines, texts)):
|
|
286
|
+
self.calculate_shadows(cos, create_polygons=True)
|
|
287
|
+
frames.append(go.Frame(
|
|
288
|
+
data=[self._merge_to_plotly_(self.pol_sunny, "sunny"),
|
|
289
|
+
self._merge_outlines_to_plotly_(self.pol_sunny),
|
|
290
|
+
self._merge_to_plotly_(self.pol_shadows, "shadow"),
|
|
291
|
+
self._merge_outlines_to_plotly_(self.pol_shadows)],
|
|
292
|
+
traces=[n_static, n_static + 1, n_static + 2, n_static + 3],
|
|
293
|
+
name=str(i),
|
|
294
|
+
layout=go.Layout(title_text=text),
|
|
295
|
+
))
|
|
296
|
+
|
|
297
|
+
layout = self._plotly_scene_layout_()
|
|
298
|
+
layout.update(
|
|
299
|
+
title_text=texts[0],
|
|
300
|
+
margin=dict(l=0, r=0, t=30, b=80),
|
|
301
|
+
updatemenus=[dict(
|
|
302
|
+
type="buttons", direction="left",
|
|
303
|
+
pad={"r": 10, "t": 20}, x=0.1, y=0,
|
|
304
|
+
buttons=[
|
|
305
|
+
dict(label="▶ Play", method="animate",
|
|
306
|
+
args=[None, {"frame": {"duration": 800, "redraw": True},
|
|
307
|
+
"fromcurrent": True}]),
|
|
308
|
+
dict(label="⏸ Pause", method="animate",
|
|
309
|
+
args=[[None], {"frame": {"duration": 0}, "mode": "immediate"}]),
|
|
310
|
+
],
|
|
311
|
+
)],
|
|
312
|
+
sliders=[dict(
|
|
313
|
+
active=0,
|
|
314
|
+
steps=[dict(
|
|
315
|
+
method="animate",
|
|
316
|
+
args=[[str(i)], {"frame": {"duration": 0, "redraw": True},
|
|
317
|
+
"mode": "immediate"}],
|
|
318
|
+
label=texts[i],
|
|
319
|
+
) for i in range(len(texts))],
|
|
320
|
+
x=0, y=0, len=1.0, pad={"b": 10, "t": 30},
|
|
321
|
+
)],
|
|
322
|
+
)
|
|
323
|
+
go.Figure(
|
|
324
|
+
data=static_traces + [init_sunny, init_sunny_outline, init_shadow, init_shadow_outline],
|
|
325
|
+
layout=layout,
|
|
326
|
+
frames=frames,
|
|
327
|
+
).show()
|
|
328
|
+
else:
|
|
329
|
+
vedo.settings.default_backend = "vtk"
|
|
330
|
+
meshes = []
|
|
331
|
+
|
|
332
|
+
def slider_func(widget, event):
|
|
333
|
+
idx = int(widget.value)
|
|
334
|
+
widget.title = texts[idx]
|
|
335
|
+
vp.remove(meshes.pop(0))
|
|
336
|
+
cos = cosines[idx]
|
|
337
|
+
self.calculate_shadows(cos, create_polygons=True)
|
|
338
|
+
meshes.append(self.get_vedo_meshes(polygons_type))
|
|
339
|
+
vp.add(meshes[0])
|
|
340
|
+
|
|
341
|
+
self.calculate_shadows(cosines[0], create_polygons=True)
|
|
342
|
+
meshes.append(self.get_vedo_meshes(polygons_type))
|
|
343
|
+
vp = vedo.Plotter(title="opensimula", axes=1)
|
|
344
|
+
vp.add(meshes[0])
|
|
345
|
+
vp.add_slider(
|
|
346
|
+
slider_func,
|
|
347
|
+
0, len(cosines) - 1,
|
|
348
|
+
value=0,
|
|
349
|
+
pos="bottom-right",
|
|
350
|
+
title=texts[0],
|
|
351
|
+
)
|
|
352
|
+
vp.show(viewup="z").close()
|
|
353
|
+
|
|
354
|
+
def delete_shadows(self):
|
|
355
|
+
self.pol_sunny = []
|
|
356
|
+
self.pol_shadows = []
|
|
357
|
+
self.sunny_fraction = []
|
|
358
|
+
|
|
359
|
+
def calculate_shadows(self, sun_position, create_polygons=True):
|
|
360
|
+
self.sunny_fraction = []
|
|
361
|
+
if create_polygons:
|
|
362
|
+
self.pol_sunny = []
|
|
363
|
+
self.pol_shadows = []
|
|
364
|
+
|
|
365
|
+
for polygon in self.pol_3D:
|
|
366
|
+
if polygon.calculate_shadows:
|
|
367
|
+
sunny_polygons, shadow_polygons = polygon.get_sunny_shadow_polygon3D(
|
|
368
|
+
self, sun_position
|
|
369
|
+
)
|
|
370
|
+
if sunny_polygons is not None:
|
|
371
|
+
sunny_area = 0
|
|
372
|
+
for sunny_polygon in sunny_polygons:
|
|
373
|
+
if create_polygons:
|
|
374
|
+
self.pol_sunny.append(sunny_polygon)
|
|
375
|
+
sunny_area += sunny_polygon.area
|
|
376
|
+
self.sunny_fraction.append(sunny_area / polygon.area)
|
|
377
|
+
else:
|
|
378
|
+
self.sunny_fraction.append(0) # No sunny area
|
|
379
|
+
if create_polygons:
|
|
380
|
+
if shadow_polygons is not None:
|
|
381
|
+
for shadow_polygon in shadow_polygons:
|
|
382
|
+
self.pol_shadows.append(shadow_polygon)
|
|
383
|
+
|
|
384
|
+
def get_sunny_polygon_list(self):
|
|
385
|
+
sunny_polygons = []
|
|
386
|
+
for polygon in self.pol_3D:
|
|
387
|
+
if polygon.calculate_shadows:
|
|
388
|
+
sunny_polygons.append(polygon)
|
|
389
|
+
return sunny_polygons
|
|
390
|
+
|
|
391
|
+
def get_sunny_index(self, name):
|
|
392
|
+
sunny_polygons = self.get_sunny_polygon_list()
|
|
393
|
+
for i in range(len(sunny_polygons)):
|
|
394
|
+
if sunny_polygons[i].name == name:
|
|
395
|
+
return i
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
def calculate_solar_tables(self):
|
|
399
|
+
self._calculate_shadow_interpolation_table()
|
|
400
|
+
self._calculate_diffuse_shadow()
|
|
401
|
+
self.solar_tables_calculated = True
|
|
402
|
+
|
|
403
|
+
def _calculate_shadow_interpolation_table(self):
|
|
404
|
+
self.shadow_azimuth_grid = np.linspace(0, 350, 36)
|
|
405
|
+
self.shadow_altitude_grid = np.linspace(-85, 85, 18)
|
|
406
|
+
sunny_polygons = self.get_sunny_polygon_list()
|
|
407
|
+
self.sunny_fraction_tables = np.zeros((len(sunny_polygons), 36, 18))
|
|
408
|
+
j = 0
|
|
409
|
+
for azimuth in self.shadow_azimuth_grid:
|
|
410
|
+
azi_rd = math.radians(azimuth)
|
|
411
|
+
k = 0
|
|
412
|
+
for altitude in self.shadow_altitude_grid:
|
|
413
|
+
alt_rd = math.radians(altitude)
|
|
414
|
+
sun_position = np.array(
|
|
415
|
+
[
|
|
416
|
+
math.cos(alt_rd) * math.sin(azi_rd),
|
|
417
|
+
-math.cos(alt_rd) * math.cos(azi_rd),
|
|
418
|
+
math.sin(alt_rd),
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
self.calculate_shadows(sun_position, create_polygons=False)
|
|
423
|
+
sunny_frac = self.sunny_fraction
|
|
424
|
+
for i in range(len(sunny_frac)):
|
|
425
|
+
self.sunny_fraction_tables[i][j][k] = sunny_frac[i]
|
|
426
|
+
k = k + 1
|
|
427
|
+
j = j + 1
|
|
428
|
+
|
|
429
|
+
self.sunny_interpolation_functions = []
|
|
430
|
+
for i in range(0, len(sunny_polygons)):
|
|
431
|
+
self.sunny_interpolation_functions.append(
|
|
432
|
+
RegularGridInterpolator(
|
|
433
|
+
(self.shadow_azimuth_grid, self.shadow_altitude_grid),
|
|
434
|
+
self.sunny_fraction_tables[i],
|
|
435
|
+
bounds_error=False,
|
|
436
|
+
fill_value=None,
|
|
437
|
+
method="cubic",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _calculate_diffuse_shadow(self):
|
|
442
|
+
def integral(i,polygon_i):
|
|
443
|
+
sunny_value = 0
|
|
444
|
+
shadow_value = 0
|
|
445
|
+
n = 0
|
|
446
|
+
for j in range(len(self.shadow_azimuth_grid)):
|
|
447
|
+
for k in range(len(self.shadow_altitude_grid)):
|
|
448
|
+
theta = polygon_i.get_angle_with_normal(
|
|
449
|
+
self.shadow_azimuth_grid[j], self.shadow_altitude_grid[k]
|
|
450
|
+
)
|
|
451
|
+
if theta < math.pi / 2:
|
|
452
|
+
f = 0.5 * math.sin(2 * theta)
|
|
453
|
+
sunny_value = sunny_value + f
|
|
454
|
+
shadow_value = (
|
|
455
|
+
shadow_value + f * self.sunny_fraction_tables[i][j][k]
|
|
456
|
+
)
|
|
457
|
+
n = n + 1
|
|
458
|
+
return shadow_value / sunny_value
|
|
459
|
+
|
|
460
|
+
self.shadow_diffuse_fraction = []
|
|
461
|
+
sunny_polygon_list = self.get_sunny_polygon_list()
|
|
462
|
+
for i in range(0, len(sunny_polygon_list)):
|
|
463
|
+
polygon_i = sunny_polygon_list[i]
|
|
464
|
+
self.shadow_diffuse_fraction.append(integral(i, polygon_i))
|
|
465
|
+
|
|
466
|
+
def get_diffuse_sunny_fraction(self, sunny_i):
|
|
467
|
+
if self.solar_tables_calculated == True:
|
|
468
|
+
return self.shadow_diffuse_fraction[sunny_i]
|
|
469
|
+
else:
|
|
470
|
+
return 1
|
|
471
|
+
|
|
472
|
+
def get_direct_interpolated_sunny_fraction(self, sunny_i, azi, alt):
|
|
473
|
+
if self.solar_tables_calculated == True:
|
|
474
|
+
if azi < 0:
|
|
475
|
+
azi = azi + 360
|
|
476
|
+
return self.sunny_interpolation_functions[sunny_i]((azi, alt))
|
|
477
|
+
else:
|
|
478
|
+
return 1
|
|
479
|
+
|
|
480
|
+
def get_direct_sunny_fraction(self, sunny_i):
|
|
481
|
+
if self.sunny_fraction == []:
|
|
482
|
+
return 1
|
|
483
|
+
else:
|
|
484
|
+
return self.sunny_fraction[sunny_i]
|
|
485
|
+
|
|
486
|
+
def show_sunny_fraction(self, sunny_i):
|
|
487
|
+
fig, ax = plt.subplots()
|
|
488
|
+
X, Y = np.meshgrid(self.shadow_azimuth_grid, self.shadow_altitude_grid)
|
|
489
|
+
ax.imshow(self.sunny_fraction_tables[sunny_i], vmin=0, vmax=1)
|
|
490
|
+
plt.show()
|
|
491
|
+
|
|
492
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import numpy as np
|
|
3
|
-
from shapely.geometry import Polygon
|
|
3
|
+
from shapely.geometry import Polygon, MultiPoint
|
|
4
|
+
from shapely.ops import triangulate as shapely_triangulate
|
|
4
5
|
import vedo
|
|
5
|
-
from triangle import triangulate
|
|
6
6
|
|
|
7
7
|
class Polygon_3D():
|
|
8
8
|
def __init__(self, name, origin, azimuth, altitude, polygon2D, holes2D=[], color="white", opacity=1.0, visible=True,shading=True,calculate_shadows=True):
|
|
@@ -74,32 +74,27 @@ class Polygon_3D():
|
|
|
74
74
|
return mesh
|
|
75
75
|
|
|
76
76
|
def _triangulate_(self):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
all_verts = list(self.polygon2D)
|
|
78
|
+
for hole in self.holes2D:
|
|
79
|
+
all_verts.extend(hole)
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for loop in (self.polygon2D, *self.holes2D):
|
|
84
|
-
verts.append(loop)
|
|
85
|
-
edges.append(nv + edge_idxs(len(loop)))
|
|
86
|
-
nv += len(loop)
|
|
81
|
+
tris = [t for t in shapely_triangulate(MultiPoint(all_verts))
|
|
82
|
+
if self.shapely_polygon.contains(t.centroid)]
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return (self._convert_2D_to_3D_(points), f)
|
|
84
|
+
vert_map = {}
|
|
85
|
+
verts = []
|
|
86
|
+
faces = []
|
|
87
|
+
for tri in tris:
|
|
88
|
+
face = []
|
|
89
|
+
for c in list(tri.exterior.coords)[:-1]:
|
|
90
|
+
key = (round(c[0], 10), round(c[1], 10))
|
|
91
|
+
if key not in vert_map:
|
|
92
|
+
vert_map[key] = len(verts)
|
|
93
|
+
verts.append(c)
|
|
94
|
+
face.append(vert_map[key])
|
|
95
|
+
faces.append(face)
|
|
96
|
+
|
|
97
|
+
return (self._convert_2D_to_3D_(np.array(verts)), np.array(faces))
|
|
103
98
|
|
|
104
99
|
def _are_vertices_counterclockwise_(self,puntos):
|
|
105
100
|
# Se suma el primer punto al final para cerrar el polígono.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensimula
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Component-based time simulation environment in Python
|
|
5
5
|
Author-email: "Juan F. Coronel" <jfc@us.es>
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -18,7 +18,6 @@ Requires-Dist: psychrolib>=2.5.0
|
|
|
18
18
|
Requires-Dist: scipy>=1.16.2
|
|
19
19
|
Requires-Dist: shapely>=2.1.1
|
|
20
20
|
Requires-Dist: tqdm>=4.67.1
|
|
21
|
-
Requires-Dist: triangle>=20250106
|
|
22
21
|
Requires-Dist: vedo>=2025.5.4
|
|
23
22
|
Dynamic: license-file
|
|
24
23
|
|
|
@@ -20,17 +20,16 @@ src/opensimula/components/Building.py
|
|
|
20
20
|
src/opensimula/components/Building_surface.py
|
|
21
21
|
src/opensimula/components/Calculator.py
|
|
22
22
|
src/opensimula/components/Construction.py
|
|
23
|
+
src/opensimula/components/DX_unit.py
|
|
23
24
|
src/opensimula/components/Day_schedule.py
|
|
25
|
+
src/opensimula/components/Fan.py
|
|
24
26
|
src/opensimula/components/File_data.py
|
|
25
27
|
src/opensimula/components/File_met.py
|
|
26
28
|
src/opensimula/components/Frame.py
|
|
27
29
|
src/opensimula/components/Glazing.py
|
|
28
|
-
src/opensimula/components/HVAC_DX_equipment.py
|
|
29
30
|
src/opensimula/components/HVAC_DX_system.py
|
|
30
31
|
src/opensimula/components/HVAC_MZW_system.py
|
|
31
32
|
src/opensimula/components/HVAC_SZW_system.py
|
|
32
|
-
src/opensimula/components/HVAC_coil_equipment.py
|
|
33
|
-
src/opensimula/components/HVAC_fan_equipment.py
|
|
34
33
|
src/opensimula/components/HVAC_perfect_system.py
|
|
35
34
|
src/opensimula/components/Material.py
|
|
36
35
|
src/opensimula/components/Opening.py
|
|
@@ -40,6 +39,7 @@ src/opensimula/components/Space.py
|
|
|
40
39
|
src/opensimula/components/Space_type.py
|
|
41
40
|
src/opensimula/components/Surface.py
|
|
42
41
|
src/opensimula/components/Test_component.py
|
|
42
|
+
src/opensimula/components/Water_coil.py
|
|
43
43
|
src/opensimula/components/Week_schedule.py
|
|
44
44
|
src/opensimula/components/Year_schedule.py
|
|
45
45
|
src/opensimula/components/__init__.py
|
|
@@ -2,10 +2,10 @@ import opensimula as osim
|
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
4
|
case_dict = {
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "Test_DX_unit",
|
|
6
6
|
"components": [
|
|
7
7
|
{
|
|
8
|
-
"type": "
|
|
8
|
+
"type": "DX_unit",
|
|
9
9
|
"name": "HVAC_equipment",
|
|
10
10
|
"nominal_air_flow": 0.4248,
|
|
11
11
|
"nominal_total_cooling_capacity": 7951,
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import numpy as np
|
|
3
|
-
import math
|
|
4
|
-
from scipy.interpolate import RegularGridInterpolator
|
|
5
|
-
import matplotlib.pyplot as plt
|
|
6
|
-
import vedo as vedo
|
|
7
|
-
|
|
8
|
-
class Environment_3D:
|
|
9
|
-
def __init__(self):
|
|
10
|
-
self.pol_3D = []
|
|
11
|
-
self.pol_sunny = []
|
|
12
|
-
self.pol_shadows = []
|
|
13
|
-
self.sunny_fraction = []
|
|
14
|
-
self.solar_tables_calculated = False
|
|
15
|
-
|
|
16
|
-
def add_polygon_3D(self, polygon_3D):
|
|
17
|
-
self.pol_3D.append(polygon_3D)
|
|
18
|
-
self.sunny_fraction.append(1.0)
|
|
19
|
-
|
|
20
|
-
def get_vedo_meshes(self, polygons_type="initial"):
|
|
21
|
-
meshes = []
|
|
22
|
-
if polygons_type == "initial":
|
|
23
|
-
for polygon_3D in self.pol_3D:
|
|
24
|
-
if polygon_3D.visible:
|
|
25
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
26
|
-
mesh.polygon_name = polygon_3D.name
|
|
27
|
-
meshes.append(mesh)
|
|
28
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
29
|
-
elif polygons_type == "sunny":
|
|
30
|
-
for polygon_3D in self.pol_sunny:
|
|
31
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
32
|
-
mesh.polygon_name = polygon_3D.name
|
|
33
|
-
meshes.append(mesh)
|
|
34
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
35
|
-
elif polygons_type == "shadows":
|
|
36
|
-
for polygon_3D in self.pol_shadows:
|
|
37
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
38
|
-
mesh.polygon_name = polygon_3D.name
|
|
39
|
-
meshes.append(mesh)
|
|
40
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
41
|
-
elif polygons_type == "sunny+shadows":
|
|
42
|
-
for polygon_3D in self.pol_sunny:
|
|
43
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
44
|
-
mesh.polygon_name = polygon_3D.name
|
|
45
|
-
meshes.append(mesh)
|
|
46
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
47
|
-
for polygon_3D in self.pol_shadows:
|
|
48
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
49
|
-
mesh.polygon_name = polygon_3D.name
|
|
50
|
-
meshes.append(mesh)
|
|
51
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
52
|
-
elif polygons_type == "Building_shadows":
|
|
53
|
-
for polygon_3D in self.pol_sunny:
|
|
54
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
55
|
-
mesh.polygon_name = polygon_3D.name
|
|
56
|
-
meshes.append(mesh)
|
|
57
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
58
|
-
for polygon_3D in self.pol_shadows:
|
|
59
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
60
|
-
mesh.polygon_name = polygon_3D.name
|
|
61
|
-
meshes.append(mesh)
|
|
62
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
63
|
-
for polygon_3D in self.pol_3D:
|
|
64
|
-
if polygon_3D.visible and polygon_3D.calculate_shadows == False:
|
|
65
|
-
mesh = polygon_3D.get_vedo_mesh()
|
|
66
|
-
mesh.polygon_name = polygon_3D.name
|
|
67
|
-
meshes.append(mesh)
|
|
68
|
-
meshes.append(mesh.silhouette("2d").c("black").linewidth(5))
|
|
69
|
-
|
|
70
|
-
return meshes
|
|
71
|
-
|
|
72
|
-
def show(self, polygons_type="initial"):
|
|
73
|
-
vedo.settings.default_backend = 'vtk'
|
|
74
|
-
meshes = self.get_vedo_meshes(polygons_type)
|
|
75
|
-
|
|
76
|
-
text_obj = [None] # Usamos una lista para que sea mutable en el callback
|
|
77
|
-
|
|
78
|
-
def on_left_click(evt):
|
|
79
|
-
if text_obj[0] is not None:
|
|
80
|
-
plt.remove(text_obj[0]) # Borra el texto anterior
|
|
81
|
-
msh = evt.object
|
|
82
|
-
if not msh:
|
|
83
|
-
text_obj[0] = vedo.Text2D(" ", pos='top-left')
|
|
84
|
-
else:
|
|
85
|
-
text_obj[0] = vedo.Text2D(f"{msh.polygon_name}", pos='top-left')
|
|
86
|
-
plt.add(text_obj[0])
|
|
87
|
-
plt.render()
|
|
88
|
-
|
|
89
|
-
plt = vedo.Plotter(title="opensimula")
|
|
90
|
-
plt.add_callback('mouse click', on_left_click)
|
|
91
|
-
plt.show(*meshes, axes=1, viewup="z").close()
|
|
92
|
-
|
|
93
|
-
def show_animation(self, texts, cosines , polygons_type="initial" ):
|
|
94
|
-
vedo.settings.default_backend = 'vtk'
|
|
95
|
-
meshes = []
|
|
96
|
-
def slider_func(widget, event):
|
|
97
|
-
idx = int(widget.value)
|
|
98
|
-
widget.title = texts[idx]
|
|
99
|
-
plt.remove( meshes.pop(0))
|
|
100
|
-
cos = cosines[idx]
|
|
101
|
-
self.calculate_shadows(cos, create_polygons=True)
|
|
102
|
-
meshes.append(self.get_vedo_meshes(polygons_type))
|
|
103
|
-
plt.add(meshes[0])
|
|
104
|
-
|
|
105
|
-
# Show first frame
|
|
106
|
-
self.calculate_shadows(cosines[0], create_polygons=True)
|
|
107
|
-
meshes.append(self.get_vedo_meshes(polygons_type))
|
|
108
|
-
plt = vedo.Plotter(title="opensimula", axes=1)
|
|
109
|
-
plt.add(meshes[0])
|
|
110
|
-
plt.add_slider(
|
|
111
|
-
slider_func,
|
|
112
|
-
0, len(cosines)-1, # slider range
|
|
113
|
-
value=0, # initial value
|
|
114
|
-
pos="bottom-right", # position of the slider
|
|
115
|
-
title=texts[0]
|
|
116
|
-
)
|
|
117
|
-
plt.show(viewup="z").close()
|
|
118
|
-
|
|
119
|
-
def delete_shadows(self):
|
|
120
|
-
self.pol_sunny = []
|
|
121
|
-
self.pol_shadows = []
|
|
122
|
-
self.sunny_fraction = []
|
|
123
|
-
|
|
124
|
-
def calculate_shadows(self, sun_position, create_polygons=True):
|
|
125
|
-
self.sunny_fraction = []
|
|
126
|
-
if create_polygons:
|
|
127
|
-
self.pol_sunny = []
|
|
128
|
-
self.pol_shadows = []
|
|
129
|
-
|
|
130
|
-
for polygon in self.pol_3D:
|
|
131
|
-
if polygon.calculate_shadows:
|
|
132
|
-
sunny_polygons, shadow_polygons = polygon.get_sunny_shadow_polygon3D(
|
|
133
|
-
self, sun_position
|
|
134
|
-
)
|
|
135
|
-
if sunny_polygons is not None:
|
|
136
|
-
sunny_area = 0
|
|
137
|
-
for sunny_polygon in sunny_polygons:
|
|
138
|
-
if create_polygons:
|
|
139
|
-
self.pol_sunny.append(sunny_polygon)
|
|
140
|
-
sunny_area += sunny_polygon.area
|
|
141
|
-
self.sunny_fraction.append(sunny_area / polygon.area)
|
|
142
|
-
else:
|
|
143
|
-
self.sunny_fraction.append(0) # No sunny area
|
|
144
|
-
if create_polygons:
|
|
145
|
-
if shadow_polygons is not None:
|
|
146
|
-
for shadow_polygon in shadow_polygons:
|
|
147
|
-
self.pol_shadows.append(shadow_polygon)
|
|
148
|
-
|
|
149
|
-
def get_sunny_polygon_list(self):
|
|
150
|
-
sunny_polygons = []
|
|
151
|
-
for polygon in self.pol_3D:
|
|
152
|
-
if polygon.calculate_shadows:
|
|
153
|
-
sunny_polygons.append(polygon)
|
|
154
|
-
return sunny_polygons
|
|
155
|
-
|
|
156
|
-
def get_sunny_index(self, name):
|
|
157
|
-
sunny_polygons = self.get_sunny_polygon_list()
|
|
158
|
-
for i in range(len(sunny_polygons)):
|
|
159
|
-
if sunny_polygons[i].name == name:
|
|
160
|
-
return i
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
def calculate_solar_tables(self):
|
|
164
|
-
self._calculate_shadow_interpolation_table()
|
|
165
|
-
self._calculate_diffuse_shadow()
|
|
166
|
-
self.solar_tables_calculated = True
|
|
167
|
-
|
|
168
|
-
def _calculate_shadow_interpolation_table(self):
|
|
169
|
-
self.shadow_azimuth_grid = np.linspace(0, 350, 36)
|
|
170
|
-
self.shadow_altitude_grid = np.linspace(-85, 85, 18)
|
|
171
|
-
sunny_polygons = self.get_sunny_polygon_list()
|
|
172
|
-
self.sunny_fraction_tables = np.zeros((len(sunny_polygons), 36, 18))
|
|
173
|
-
j = 0
|
|
174
|
-
for azimuth in self.shadow_azimuth_grid:
|
|
175
|
-
azi_rd = math.radians(azimuth)
|
|
176
|
-
k = 0
|
|
177
|
-
for altitude in self.shadow_altitude_grid:
|
|
178
|
-
alt_rd = math.radians(altitude)
|
|
179
|
-
sun_position = np.array(
|
|
180
|
-
[
|
|
181
|
-
math.cos(alt_rd) * math.sin(azi_rd),
|
|
182
|
-
-math.cos(alt_rd) * math.cos(azi_rd),
|
|
183
|
-
math.sin(alt_rd),
|
|
184
|
-
]
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
self.calculate_shadows(sun_position, create_polygons=False)
|
|
188
|
-
sunny_frac = self.sunny_fraction
|
|
189
|
-
for i in range(len(sunny_frac)):
|
|
190
|
-
self.sunny_fraction_tables[i][j][k] = sunny_frac[i]
|
|
191
|
-
k = k + 1
|
|
192
|
-
j = j + 1
|
|
193
|
-
|
|
194
|
-
self.sunny_interpolation_functions = []
|
|
195
|
-
for i in range(0, len(sunny_polygons)):
|
|
196
|
-
self.sunny_interpolation_functions.append(
|
|
197
|
-
RegularGridInterpolator(
|
|
198
|
-
(self.shadow_azimuth_grid, self.shadow_altitude_grid),
|
|
199
|
-
self.sunny_fraction_tables[i],
|
|
200
|
-
bounds_error=False,
|
|
201
|
-
fill_value=None,
|
|
202
|
-
method="cubic",
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
def _calculate_diffuse_shadow(self):
|
|
207
|
-
def integral(i,polygon_i):
|
|
208
|
-
sunny_value = 0
|
|
209
|
-
shadow_value = 0
|
|
210
|
-
n = 0
|
|
211
|
-
for j in range(len(self.shadow_azimuth_grid)):
|
|
212
|
-
for k in range(len(self.shadow_altitude_grid)):
|
|
213
|
-
theta = polygon_i.get_angle_with_normal(
|
|
214
|
-
self.shadow_azimuth_grid[j], self.shadow_altitude_grid[k]
|
|
215
|
-
)
|
|
216
|
-
if theta < math.pi / 2:
|
|
217
|
-
f = 0.5 * math.sin(2 * theta)
|
|
218
|
-
sunny_value = sunny_value + f
|
|
219
|
-
shadow_value = (
|
|
220
|
-
shadow_value + f * self.sunny_fraction_tables[i][j][k]
|
|
221
|
-
)
|
|
222
|
-
n = n + 1
|
|
223
|
-
return shadow_value / sunny_value
|
|
224
|
-
|
|
225
|
-
self.shadow_diffuse_fraction = []
|
|
226
|
-
sunny_polygon_list = self.get_sunny_polygon_list()
|
|
227
|
-
for i in range(0, len(sunny_polygon_list)):
|
|
228
|
-
polygon_i = sunny_polygon_list[i]
|
|
229
|
-
self.shadow_diffuse_fraction.append(integral(i, polygon_i))
|
|
230
|
-
|
|
231
|
-
def get_diffuse_sunny_fraction(self, sunny_i):
|
|
232
|
-
if self.solar_tables_calculated == True:
|
|
233
|
-
return self.shadow_diffuse_fraction[sunny_i]
|
|
234
|
-
else:
|
|
235
|
-
return 1
|
|
236
|
-
|
|
237
|
-
def get_direct_interpolated_sunny_fraction(self, sunny_i, azi, alt):
|
|
238
|
-
if self.solar_tables_calculated == True:
|
|
239
|
-
if azi < 0:
|
|
240
|
-
azi = azi + 360
|
|
241
|
-
return self.sunny_interpolation_functions[sunny_i]((azi, alt))
|
|
242
|
-
else:
|
|
243
|
-
return 1
|
|
244
|
-
|
|
245
|
-
def get_direct_sunny_fraction(self, sunny_i):
|
|
246
|
-
if self.sunny_fraction == []:
|
|
247
|
-
return 1
|
|
248
|
-
else:
|
|
249
|
-
return self.sunny_fraction[sunny_i]
|
|
250
|
-
|
|
251
|
-
def show_sunny_fraction(self, sunny_i):
|
|
252
|
-
fig, ax = plt.subplots()
|
|
253
|
-
X, Y = np.meshgrid(self.shadow_azimuth_grid, self.shadow_altitude_grid)
|
|
254
|
-
ax.imshow(self.sunny_fraction_tables[sunny_i], vmin=0, vmax=1)
|
|
255
|
-
plt.show()
|
|
256
|
-
|
|
257
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|