OpenSimula 0.8.0__tar.gz → 0.8.5__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.
Files changed (75) hide show
  1. {opensimula-0.8.0 → opensimula-0.8.5}/PKG-INFO +3 -4
  2. {opensimula-0.8.0 → opensimula-0.8.5}/pyproject.toml +8 -5
  3. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Component.py +22 -15
  4. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Parameters.py +31 -5
  5. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Project.py +90 -89
  6. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Simulation.py +0 -63
  7. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Building_surface.py +41 -1
  8. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Chiller_heat_pump.py +1 -1
  9. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Construction.py +5 -3
  10. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Opening.py +2 -1
  11. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Opening_type.py +3 -3
  12. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Space.py +118 -46
  13. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Surface.py +14 -0
  14. opensimula-0.8.5/src/OpenSimula/editor/__init__.py +19 -0
  15. opensimula-0.8.5/src/OpenSimula/editor/schema.py +229 -0
  16. opensimula-0.8.5/src/OpenSimula/editor/static/editor.css +388 -0
  17. opensimula-0.8.5/src/OpenSimula/editor/static/editor.js +1047 -0
  18. opensimula-0.8.5/src/OpenSimula/editor/validation.py +91 -0
  19. opensimula-0.8.5/src/OpenSimula/editor/widget.py +246 -0
  20. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/visual_3D/Environment_3D.py +113 -28
  21. opensimula-0.8.5/src/OpenSimula/visual_3D/Polygon_3D.py +370 -0
  22. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula.egg-info/PKG-INFO +3 -4
  23. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula.egg-info/SOURCES.txt +10 -1
  24. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula.egg-info/requires.txt +2 -3
  25. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Case_600FF.py +4 -3
  26. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Case_610.py +5 -4
  27. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_HVAC_DX_system.py +13 -12
  28. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_HVAC_FAN_COIL_water_system.py +11 -10
  29. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Project.py +43 -0
  30. opensimula-0.8.5/test/test_editor_schema.py +202 -0
  31. opensimula-0.8.5/test/test_editor_widget.py +513 -0
  32. opensimula-0.8.5/test/test_underground_repeated_simulation.py +202 -0
  33. opensimula-0.8.0/src/OpenSimula/visual_3D/Polygon_3D.py +0 -257
  34. {opensimula-0.8.0 → opensimula-0.8.5}/LICENSE +0 -0
  35. {opensimula-0.8.0 → opensimula-0.8.5}/README.md +0 -0
  36. {opensimula-0.8.0 → opensimula-0.8.5}/setup.cfg +0 -0
  37. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Child.py +0 -0
  38. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Iterative_process.py +0 -0
  39. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Message.py +0 -0
  40. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Parameter_container.py +0 -0
  41. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/Variable.py +0 -0
  42. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/__init__.py +0 -0
  43. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Building.py +0 -0
  44. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Calculator.py +0 -0
  45. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/DX_unit.py +0 -0
  46. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Day_schedule.py +0 -0
  47. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Fan.py +0 -0
  48. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/File_data.py +0 -0
  49. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/File_met.py +0 -0
  50. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Frame.py +0 -0
  51. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Glazing.py +0 -0
  52. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/HVAC_DX_system.py +0 -0
  53. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/HVAC_MZW_system.py +0 -0
  54. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/HVAC_SZW_system.py +0 -0
  55. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/HVAC_perfect_system.py +0 -0
  56. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/HVAC_water_system.py +0 -0
  57. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Material.py +0 -0
  58. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Pump.py +0 -0
  59. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Solar_surface.py +0 -0
  60. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Space_type.py +0 -0
  61. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Test_component.py +0 -0
  62. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Water_coil.py +0 -0
  63. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Week_schedule.py +0 -0
  64. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/Year_schedule.py +0 -0
  65. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula/components/__init__.py +0 -0
  66. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula.egg-info/dependency_links.txt +0 -0
  67. {opensimula-0.8.0 → opensimula-0.8.5}/src/OpenSimula.egg-info/top_level.txt +0 -0
  68. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Calculator.py +0 -0
  69. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Construction.py +0 -0
  70. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_File_data.py +0 -0
  71. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_File_met.py +0 -0
  72. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_HVAC_DX_Equipment.py +0 -0
  73. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_HVAC_water_system_process.py +0 -0
  74. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Iterative_process.py +0 -0
  75. {opensimula-0.8.0 → opensimula-0.8.5}/test/test_Schedules.py +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensimula
3
- Version: 0.8.0
3
+ Version: 0.8.5
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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: dash>=3.2.0
10
- Requires-Dist: dash-ag-grid>=32.3.2
11
- Requires-Dist: dash-bootstrap-components>=2.0.4
9
+ Requires-Dist: anywidget>=0.11.0
10
+ Requires-Dist: jsonschema>=4.26.0
12
11
  Requires-Dist: nbformat>=5.10.4
13
12
  Requires-Dist: numpy>=2.3.3
14
13
  Requires-Dist: openpyxl>=3.1.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opensimula"
3
- version = "0.8.0"
3
+ version = "0.8.5"
4
4
  requires-python = ">=3.12"
5
5
  authors = [
6
6
  { name="Juan F. Coronel", email="jfc@us.es" },
@@ -8,9 +8,8 @@ authors = [
8
8
  description = "Component-based time simulation environment in Python"
9
9
  readme = "README.md"
10
10
  dependencies = [
11
- "dash>=3.2.0",
12
- "dash-ag-grid>=32.3.2",
13
- "dash-bootstrap-components>=2.0.4",
11
+ "anywidget>=0.11.0",
12
+ "jsonschema>=4.26.0",
14
13
  "nbformat>=5.10.4",
15
14
  "numpy>=2.3.3",
16
15
  "openpyxl>=3.1.5",
@@ -20,7 +19,7 @@ dependencies = [
20
19
  "scipy>=1.16.2",
21
20
  "shapely>=2.1.1",
22
21
  "tqdm>=4.67.1",
23
- "vedo>=2025.5.4",
22
+ "vedo>=2025.5.4",
24
23
  ]
25
24
 
26
25
  [dependency-groups]
@@ -31,6 +30,10 @@ dev = [
31
30
  "marimo>=0.19.0",
32
31
  ]
33
32
 
33
+ [tool.setuptools.package-data]
34
+ # The widget loads these at runtime; without this they are left out of the wheel.
35
+ "opensimula.editor" = ["static/*.js", "static/*.css"]
36
+
34
37
  [build-system]
35
38
  requires = ["setuptools>=61.2", "wheel"]
36
39
 
@@ -82,36 +82,43 @@ class Component(Parameter_container):
82
82
 
83
83
  # ____________ Functions that must be overwriten for time simulation _________________
84
84
 
85
- def get_all_referenced_components(self):
85
+ def get_all_referenced_components(self, _seen=None):
86
86
  """Get list of all referenced components, first itself. Look recursively at the referenced components
87
87
 
88
+ Args:
89
+ _seen (set, optional): components already walked, used internally.
90
+ References can form a cycle, if only because a wrong name gets
91
+ typed into a reference, and without remembering where it has
92
+ been the walk follows it until the stack runs out.
93
+
88
94
  Returns:
89
95
  component_list (component[])
90
96
  """
97
+ if _seen is None:
98
+ _seen = set()
99
+ if id(self) in _seen:
100
+ return []
101
+ _seen.add(id(self))
102
+
91
103
  comp_list = []
104
+
105
+ def walk(component):
106
+ if component is not None:
107
+ comp_list.extend(component.get_all_referenced_components(_seen))
108
+
92
109
  for key, value in self.parameter_dict().items():
93
110
  if value.type == "Parameter_component":
94
- if value.component is not None:
95
- sublist = value.component.get_all_referenced_components()
96
- for subcomp in sublist:
97
- comp_list.append(subcomp)
111
+ walk(value.component)
98
112
  elif value.type == "Parameter_component_list":
99
113
  for comp in value.component:
100
- if comp is not None:
101
- sublist = comp.get_all_referenced_components()
102
- for subcomp in sublist:
103
- comp_list.append(subcomp)
114
+ walk(comp)
104
115
  if value.type == "Parameter_variable":
105
116
  if value.variable is not None:
106
- sublist = value.variable.parent.get_all_referenced_components()
107
- for subcomp in sublist:
108
- comp_list.append(subcomp)
117
+ walk(value.variable.parent)
109
118
  elif value.type == "Parameter_variable_list":
110
119
  for var in value.variable:
111
120
  if var is not None:
112
- sublist = var.parent.get_all_referenced_components()
113
- for subcomp in sublist:
114
- comp_list.append(subcomp)
121
+ walk(var.parent)
115
122
  comp_list.append(self)
116
123
  return comp_list
117
124
 
@@ -184,7 +184,9 @@ class Parameter_string_list(Parameter):
184
184
  else:
185
185
  for el in value:
186
186
  el = str(el)
187
- self._value_ = value
187
+ # Own copy: keeping the caller list would let it be mutated from
188
+ # outside, and would be shared by every parameter assigned from it
189
+ self._value_ = list(value)
188
190
 
189
191
 
190
192
  # _____________ Parameter_int ___________________________
@@ -201,6 +203,14 @@ class Parameter_int(Parameter):
201
203
  def unit(self):
202
204
  return self._unit_
203
205
 
206
+ @property
207
+ def min(self):
208
+ return self._min_
209
+
210
+ @property
211
+ def max(self):
212
+ return self._max_
213
+
204
214
  @property
205
215
  def value(self):
206
216
  return self._value_
@@ -235,6 +245,14 @@ class Parameter_int_list(Parameter):
235
245
  def unit(self):
236
246
  return self._unit_
237
247
 
248
+ @property
249
+ def min(self):
250
+ return self._min_
251
+
252
+ @property
253
+ def max(self):
254
+ return self._max_
255
+
238
256
  @property
239
257
  def value(self):
240
258
  return self._value_
@@ -389,7 +407,9 @@ class Parameter_options_list(Parameter):
389
407
  else:
390
408
  for el in value:
391
409
  el = str(el)
392
- self._value_ = value
410
+ # Own copy: keeping the caller list would let it be mutated from
411
+ # outside, and would be shared by every parameter assigned from it
412
+ self._value_ = list(value)
393
413
 
394
414
  @property
395
415
  def options(self):
@@ -484,7 +504,9 @@ class Parameter_component_list(Parameter):
484
504
  # Is a list
485
505
  for el in value:
486
506
  el = str(el)
487
- self._value_ = value
507
+ # Own copy: keeping the caller list would let it be mutated from
508
+ # outside, and would be shared by every parameter assigned from it
509
+ self._value_ = list(value)
488
510
 
489
511
  self._external_ = []
490
512
  for el in self.value:
@@ -622,7 +644,9 @@ class Parameter_variable_list(Parameter):
622
644
  # Is a list
623
645
  for el in value:
624
646
  el = str(el)
625
- self._value_ = value
647
+ # Own copy: keeping the caller list would let it be mutated from
648
+ # outside, and would be shared by every parameter assigned from it
649
+ self._value_ = list(value)
626
650
 
627
651
  format_error = False
628
652
  self._symbol_ = []
@@ -744,7 +768,9 @@ class Parameter_math_exp_list(Parameter):
744
768
  # Is a list
745
769
  for el in value:
746
770
  el = str(el)
747
- self._value_ = value
771
+ # Own copy: keeping the caller list would let it be mutated from
772
+ # outside, and would be shared by every parameter assigned from it
773
+ self._value_ = list(value)
748
774
 
749
775
  def evaluate(self, i, values_dic):
750
776
  try:
@@ -2,9 +2,6 @@ import json
2
2
  import datetime as dt
3
3
  import numpy as np
4
4
  import pandas as pd
5
- from dash import Dash, callback, Input, Output, html, State
6
- import dash_ag_grid as dag
7
- import dash_bootstrap_components as dbc
8
5
  import psychrolib as sicro
9
6
  from tqdm import trange
10
7
  from opensimula.Message import Message
@@ -59,13 +56,14 @@ class Project(Parameter_container):
59
56
  )
60
57
  self.add_parameter(
61
58
  Parameter_options(
62
- "shadow_calculation", "INSTANT", ["NO", "INSTANT", "INTERPOLATION"]
59
+ "shadow_calculation", "INTERPOLATION", ["NO", "INSTANT", "INTERPOLATION"]
63
60
  )
64
61
  )
65
62
  self.add_parameter(Parameter_float("albedo", 0.2, "frac", min=0, max=1))
66
63
 
67
64
  self._sim_ = sim
68
65
  self._components_ = []
66
+ self._components_dict_ = {} # name -> component index, see component()
69
67
  sicro.SetUnitSystem(sicro.SI)
70
68
  atm_p = sicro.GetStandardAtmPressure(0)
71
69
  w_50 = sicro.GetHumRatioFromRelHum(22.5, 0.5, atm_p)
@@ -93,6 +91,18 @@ class Project(Parameter_container):
93
91
  "RHOCP_W": rhocp_water,
94
92
  }
95
93
 
94
+ def clear(self):
95
+ """Remove every component from the Project
96
+
97
+ The parameters of the project itself keep their values; only its
98
+ components are dropped. Needed before reloading a definition into a
99
+ project that already holds one, because read_dict() and read_json()
100
+ add components, they do not replace them.
101
+ """
102
+ self._components_ = []
103
+ self._components_dict_ = {}
104
+ self._ordered_component_list_ = []
105
+
96
106
  def del_component(self, component):
97
107
  """Delete component from Project
98
108
 
@@ -100,6 +110,13 @@ class Project(Parameter_container):
100
110
  component (Component): Component to be removed from the project
101
111
  """
102
112
  self._components_.remove(component)
113
+ # Drop every index entry pointing at it, so a later component reusing
114
+ # its name cannot resolve to the deleted one.
115
+ self._components_dict_ = {
116
+ key: comp
117
+ for key, comp in self._components_dict_.items()
118
+ if comp is not component
119
+ }
103
120
 
104
121
  def component(self, name):
105
122
  """Find and return component with its name
@@ -110,8 +127,16 @@ class Project(Parameter_container):
110
127
  Returns:
111
128
  component (Component): component found, None if not found.
112
129
  """
130
+ # Components are looked up by name millions of times during a
131
+ # simulation, so hits are served from an index. The cached component is
132
+ # revalidated against its current name because components can be
133
+ # renamed after being indexed; a stale entry falls back to the scan.
134
+ comp = self._components_dict_.get(name)
135
+ if comp is not None and comp.parameter("name").value == name:
136
+ return comp
113
137
  for comp in self._components_:
114
138
  if comp.parameter("name").value == name:
139
+ self._components_dict_[name] = comp
115
140
  return comp
116
141
  return None
117
142
 
@@ -586,100 +611,76 @@ class Project(Parameter_container):
586
611
  def simulation_dataframe(self):
587
612
  return self._sim_df
588
613
 
589
- def component_editor(self, comp_type="all"):
590
- editor = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
591
- df = self.component_dataframe(comp_type=comp_type, string_format=True)
592
- self._n_clicks_new_comp_ = 0
593
- self._n_clicks_del_comp_ = 0
594
- disabled_new = False
595
- if comp_type == "all":
596
- disabled_new = True
597
-
598
- column_definition = [
599
- {
600
- "field": "name",
601
- "checkboxSelection": True,
602
- "headerCheckboxSelection": True,
603
- }
604
- ]
605
- for i in df.columns:
606
- if i != "name":
607
- column_definition.append({"field": i})
608
-
609
- editor.layout = html.Div(
610
- [
611
- dbc.Label("Components editor:"),
612
- html.Br(),
613
- dbc.Button(
614
- "New component",
615
- id="btn-new-comp",
616
- disabled=disabled_new,
617
- n_clicks=0,
618
- ),
619
- dbc.Button(
620
- "Delete selected components",
621
- id="btn-del-comp",
622
- n_clicks=0,
623
- style={"margin-left": "15px"},
624
- ),
625
- html.Br(),
626
- html.Br(),
627
- dag.AgGrid(
628
- id="comp-table",
629
- rowData=df.to_dict("records"),
630
- columnDefs=column_definition,
631
- columnSize="sizeToFit",
632
- defaultColDef={"filter": True, "editable": True},
633
- style={"height": "500px"},
634
- dashGridOptions={
635
- "rowSelection": "multiple",
636
- "suppressRowClickSelection": True,
637
- "pagination": True,
638
- },
639
- ),
640
- ]
641
- )
614
+ def editor(self):
615
+ """Interactive editor for the whole project definition.
642
616
 
643
- @callback(
644
- Output("comp-table", "rowData"),
645
- Input("comp-table", "cellValueChanged"),
646
- Input("btn-new-comp", "n_clicks"),
647
- Input("btn-del-comp", "n_clicks"),
648
- State("comp-table", "selectedRows"),
649
- prevent_initial_call=True,
650
- )
651
- def update_data(changed, n_clicks_new, n_clicks_del, selectedRows):
652
- if self._n_clicks_new_comp_ < n_clicks_new:
653
- self.new_component(comp_type, "new_comp_" + str(n_clicks_new))
654
- self._n_clicks_new_comp_ = n_clicks_new
655
- elif self._n_clicks_del_comp_ < n_clicks_del:
656
- for row in selectedRows:
657
- self.del_component(self.component(row["name"]))
658
- self._n_clicks_del_comp_ = n_clicks_del
659
- else:
660
- if changed is not None:
661
- if changed[0]["colId"] == "name":
662
- self.component(changed[0]["oldValue"]).parameter(
663
- "name"
664
- ).value = changed[0]["value"]
665
- else:
666
- self.component(changed[0]["data"]["name"]).parameter(
667
- changed[0]["colId"]
668
- ).value = changed[0]["value"]
669
- df_end = self.component_dataframe(comp_type=comp_type, string_format=True)
670
- return df_end.to_dict("records")
617
+ Returns an anywidget that works in Jupyter and, wrapped in
618
+ mo.ui.anywidget(), in Marimo. The document is read with write_dict();
619
+ the widget does not write back to this project.
671
620
 
672
- editor.run(jupyter_height=600)
621
+ Returns:
622
+ ProjectEditor: widget holding the project document in .value
623
+ """
624
+ # Imported here: opensimula.editor reaches Project through Simulation,
625
+ # so a module level import would close the cycle.
626
+ from opensimula.editor import ProjectEditor
627
+
628
+ return ProjectEditor(self)
673
629
 
674
630
  def create_3D_environment(self, env_3D):
675
631
  for component in self.component_list("all"):
676
- if hasattr(component, "get_polygon_3D"):
677
- env_3D.add_polygon_3D(component.get_polygon_3D())
632
+ if not hasattr(component, "get_polygon_3D"):
633
+ continue
634
+ try:
635
+ polygon = component.get_polygon_3D()
636
+ except (AttributeError, IndexError, TypeError):
637
+ # A surface whose space, or an opening whose surface, does not
638
+ # resolve has no place to be. It is left out rather than
639
+ # bringing the whole view down; check() names the reference.
640
+ continue
641
+ self._set_polygon_origin_(polygon, component)
642
+ env_3D.add_polygon_3D(polygon)
643
+
644
+ def _set_polygon_origin_(self, polygon, component):
645
+ """Record which component a polygon came from, and which spaces it bounds.
646
+
647
+ Polygon_3D is geometry and knows nothing of the model, so the link is
648
+ made here, where the component is still at hand. It is what lets a
649
+ viewer show one space at a time; an interior partition belongs to the
650
+ two spaces on either side of it.
651
+ """
652
+ polygon.component = component.parameter("name").value
653
+ polygon.component_type = component.parameter("type").value
654
+
655
+ surface = component
656
+ if polygon.component_type == "Opening":
657
+ surface = component.get_surface()
658
+ if surface is None:
659
+ return
660
+
661
+ parameters = surface.parameter_dict()
662
+ if "surface_type" in parameters:
663
+ polygon.surface_type = parameters["surface_type"].value
664
+ if "spaces" in parameters:
665
+ polygon.spaces = [
666
+ name for name in parameters["spaces"].value if name != "not_defined"
667
+ ]
678
668
 
669
+ def geometry_dict(self):
670
+ """Geometry of the project as plain data, for a 3D viewer
671
+
672
+ Returns:
673
+ dict: {"meshes": [...], "spaces": [...]}, one mesh per polygon with
674
+ its triangles and the component and spaces it belongs to
675
+ """
676
+ env_3D = Environment_3D()
677
+ self.create_3D_environment(env_3D)
678
+ return env_3D.geometry_dict()
679
+
679
680
  def show_3D(self, jupyter=False):
680
681
  env_3D = Environment_3D()
681
682
  self.create_3D_environment(env_3D)
682
- env_3D.show(polygons_type="initial", jupyter=jupyter)
683
+ return env_3D.show(polygons_type="initial", jupyter=jupyter)
683
684
 
684
685
  def show_3D_shadows(self, date, jupyter=False):
685
686
  env_3D = Environment_3D()
@@ -2,9 +2,6 @@ from opensimula.Project import Project
2
2
  import pandas as pd
3
3
  import plotly.express as px
4
4
  from plotly.subplots import make_subplots
5
- from dash import Dash, callback, Input, Output, html, State
6
- import dash_ag_grid as dag
7
- import dash_bootstrap_components as dbc
8
5
 
9
6
 
10
7
  class Simulation:
@@ -163,63 +160,3 @@ class Simulation:
163
160
  # fig.update_traces(showlegend=True)
164
161
  subfig.show()
165
162
 
166
- def project_editor(self):
167
- editor = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
168
- df = self.project_dataframe(string_format=True)
169
- self._n_clicks_new_project_ = 0
170
- self._n_clicks_del_project_ = 0
171
- column_definition = [
172
- {"field": "name", "checkboxSelection": True, "headerCheckboxSelection": True}]
173
- for i in df.columns:
174
- if i != "name":
175
- column_definition.append({"field": i})
176
-
177
- editor.layout = html.Div([
178
- dbc.Label("Projects editor: "),
179
- html.Br(),
180
- dbc.Button('New project', id='btn-new-project', n_clicks=0),
181
- dbc.Button('Delete selected projects',
182
- id='btn-del-project', n_clicks=0, style={"margin-left": "15px"}),
183
- html.Br(),
184
- html.Br(),
185
- dag.AgGrid(
186
- id="project-table",
187
- rowData=df.to_dict("records"),
188
- columnDefs=column_definition,
189
- columnSize="sizeToFit",
190
- defaultColDef={"filter": True, "editable": True},
191
- style={"height": '250px'},
192
- dashGridOptions={
193
- "rowSelection": "multiple",
194
- "suppressRowClickSelection": True,
195
- "pagination": True,
196
- })
197
- ])
198
-
199
- @callback(
200
- Output('project-table', 'rowData'),
201
- Input('project-table', 'cellValueChanged'),
202
- Input('btn-new-project', 'n_clicks'),
203
- Input('btn-del-project', 'n_clicks'),
204
- State('project-table', 'selectedRows'),
205
- prevent_initial_call=True)
206
- def update_data(changed, n_clicks_new, n_clicks_del, selectedRows):
207
- if (self._n_clicks_new_project_ < n_clicks_new):
208
- self.new_project("new_project_"+str(n_clicks_new))
209
- self._n_clicks_new_project_ = n_clicks_new
210
- elif (self._n_clicks_del_project_ < n_clicks_del):
211
- for row in selectedRows:
212
- self.del_project(self.project(row["name"]))
213
- self._n_clicks_del_project_ = n_clicks_del
214
- else:
215
- if changed is not None:
216
- if changed[0]["colId"] == "name":
217
- self.project(changed[0]['oldValue']).parameter(
218
- "name").value = changed[0]["value"]
219
- else:
220
- self.project(changed[0]['data']["name"]).parameter(
221
- changed[0]["colId"]).value = changed[0]["value"]
222
- df_end = self.project_dataframe(string_format=True)
223
- return df_end.to_dict("records")
224
-
225
- editor.run(jupyter_height=350)
@@ -1,4 +1,5 @@
1
1
  import math
2
+ from shapely.validation import explain_validity
2
3
  from opensimula.Message import Message
3
4
  from opensimula.components.Surface import Surface
4
5
  from opensimula.Parameters import (
@@ -106,10 +107,49 @@ class Building_surface(Surface):
106
107
  # Create openings list
107
108
  if self._surface_type_ == "EXTERIOR" or self._surface_type_ == "INTERIOR":
108
109
  self._create_openings_list_()
110
+ errors.extend(self._check_polygon_validity_())
111
+ return errors
112
+
113
+ def _check_polygon_validity_(self):
114
+ """Warn when the surface outline with its openings has no valid single-piece shape.
115
+
116
+ get_polygon_3D() already recovers an opening flush with the surface
117
+ edge (a door reaching the floor, a window flush with a wall edge, usual
118
+ in imported geometry), see Polygon_3D._build_shapely_polygon_, so that
119
+ alone is not worth a warning. What it cannot recover from is an opening
120
+ that splits the surface into disconnected pieces, or geometry that
121
+ stays invalid after that recovery.
122
+ """
123
+ errors = []
124
+ if len(self.openings) == 0:
125
+ return errors
126
+ polygon = Polygon_3D._build_shapely_polygon_(
127
+ self.get_polygon_2D(), [op.get_polygon_2D() for op in self.openings]
128
+ )
129
+ if not polygon.is_valid:
130
+ msg = (
131
+ f"{self.parameter('name').value}, the surface outline with its openings "
132
+ f"is not a valid polygon ({explain_validity(polygon)}). Shadow results "
133
+ "for this surface may be unreliable."
134
+ )
135
+ errors.append(Message(msg, "WARNING"))
136
+ elif polygon.geom_type != "Polygon":
137
+ msg = (
138
+ f"{self.parameter('name').value}, its openings split the surface outline "
139
+ "into disconnected pieces. Shadow results for this surface may be unreliable."
140
+ )
141
+ errors.append(Message(msg, "WARNING"))
109
142
  return errors
110
143
 
111
144
  def get_building(self):
112
- return self.parameter("spaces").component[0].get_building()
145
+ space = self.get_space()
146
+ # Only a Space knows its building directly. Following anything else
147
+ # chases its get_building() in turn, and a name that points back here,
148
+ # which "spaces" accepts because it is only checked later, would run
149
+ # the stack out before check() could report it.
150
+ if space is None or space.parameter("type").value != "Space":
151
+ return None
152
+ return space.get_building()
113
153
 
114
154
  def get_space(self, side=0):
115
155
  return self.parameter("spaces").component[side]
@@ -5,7 +5,7 @@ from opensimula.Component import Component
5
5
  class Chiller_heat_pump(Component):
6
6
  def __init__(self, name, project):
7
7
  Component.__init__(self, name, project)
8
- self.parameter("type").value = "Chiller"
8
+ self.parameter("type").value = "Chiller_heat_pump"
9
9
  self.parameter("description").value = "Chiller equipment manufacturer information"
10
10
  self.add_parameter(Parameter_options("chiller_type", "CHILLER", ["CHILLER", "HEAT_PUMP", "CHILLER_HEAT_PUMP"]))
11
11
  self.add_parameter(Parameter_options("condensation_type", "AIR_CONDENSED", ["AIR_CONDENSED", "WATER_CONDENSED"]))
@@ -17,7 +17,7 @@ class Construction(Component):
17
17
  self.add_parameter(Parameter_float_list(
18
18
  "lw_epsilon", [0.9, 0.9], "frac", min=0, max=1))
19
19
  self.add_parameter(Parameter_component_list(
20
- "materials", [], "Material"))
20
+ "materials", [], ["Material"]))
21
21
  self.add_parameter(Parameter_float_list("thicknesses", [], "m", min=0))
22
22
 
23
23
  def check(self):
@@ -35,8 +35,10 @@ class Construction(Component):
35
35
  material_name (str): Name of the material component to add
36
36
  thickness (float): Thickness of the layer in meters
37
37
  """
38
- materials = self.parameter("materials").value
39
- thicknesses = self.parameter("thicknesses").value
38
+ # Copies, because the getter returns the parameter own list and
39
+ # inserting into it would modify the construction before validation
40
+ materials = list(self.parameter("materials").value)
41
+ thicknesses = list(self.parameter("thicknesses").value)
40
42
  materials.insert(0, material_name)
41
43
  thicknesses.insert(0, thickness)
42
44
  self.parameter("materials").value = materials
@@ -93,7 +93,8 @@ class Opening(Component):
93
93
  return errors
94
94
 
95
95
  def get_building(self):
96
- return self.get_surface().get_building()
96
+ surface = self.get_surface()
97
+ return None if surface is None else surface.get_building()
97
98
 
98
99
  def get_space(self, side=0):
99
100
  return self.get_surface().get_space(side)
@@ -11,11 +11,11 @@ class Opening_type(Component):
11
11
  "description").value = "Window or doors with glazing, construction, frame and shading."
12
12
 
13
13
  self.add_parameter(Parameter_component(
14
- "glazing", "not_defined", "Glazing"))
14
+ "glazing", "not_defined", ["Glazing"]))
15
15
  self.add_parameter(Parameter_component(
16
- "frame", "not_defined", "Frame"))
16
+ "frame", "not_defined", ["Frame"]))
17
17
  self.add_parameter(Parameter_component(
18
- "construction", "not_defined", "Construction"))
18
+ "construction", "not_defined", ["Construction"]))
19
19
  self.add_parameter(Parameter_float(
20
20
  "glazing_fraction", 0.9, "frac", min=0, max=1))
21
21
  self.add_parameter(Parameter_float(