OpenSimula 0.7.1__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 (76) hide show
  1. {opensimula-0.7.1 → opensimula-0.8.5}/LICENSE +1 -1
  2. {opensimula-0.7.1 → opensimula-0.8.5}/PKG-INFO +3 -5
  3. {opensimula-0.7.1 → opensimula-0.8.5}/README.md +0 -1
  4. {opensimula-0.7.1 → opensimula-0.8.5}/pyproject.toml +8 -5
  5. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Component.py +32 -20
  6. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Parameters.py +31 -5
  7. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Project.py +90 -89
  8. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Simulation.py +17 -72
  9. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/__init__.py +2 -1
  10. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Building_surface.py +41 -1
  11. opensimula-0.8.5/src/OpenSimula/components/Chiller_heat_pump.py +155 -0
  12. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Construction.py +5 -3
  13. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/File_data.py +7 -3
  14. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/HVAC_MZW_system.py +39 -18
  15. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/HVAC_SZW_system.py +129 -42
  16. opensimula-0.8.5/src/OpenSimula/components/HVAC_water_system.py +314 -0
  17. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Opening.py +2 -1
  18. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Opening_type.py +3 -3
  19. opensimula-0.8.5/src/OpenSimula/components/Pump.py +50 -0
  20. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Space.py +294 -127
  21. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Surface.py +14 -0
  22. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Water_coil.py +11 -9
  23. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/__init__.py +4 -0
  24. opensimula-0.8.5/src/OpenSimula/editor/__init__.py +19 -0
  25. opensimula-0.8.5/src/OpenSimula/editor/schema.py +229 -0
  26. opensimula-0.8.5/src/OpenSimula/editor/static/editor.css +388 -0
  27. opensimula-0.8.5/src/OpenSimula/editor/static/editor.js +1047 -0
  28. opensimula-0.8.5/src/OpenSimula/editor/validation.py +91 -0
  29. opensimula-0.8.5/src/OpenSimula/editor/widget.py +246 -0
  30. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/visual_3D/Environment_3D.py +113 -28
  31. opensimula-0.8.5/src/OpenSimula/visual_3D/Polygon_3D.py +370 -0
  32. {opensimula-0.7.1/src/opensimula.egg-info → opensimula-0.8.5/src/OpenSimula.egg-info}/PKG-INFO +3 -5
  33. opensimula-0.8.5/src/OpenSimula.egg-info/SOURCES.txt +113 -0
  34. {opensimula-0.7.1/src/opensimula.egg-info → opensimula-0.8.5/src/OpenSimula.egg-info}/requires.txt +2 -3
  35. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Case_600FF.py +4 -3
  36. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Case_610.py +5 -4
  37. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_HVAC_DX_system.py +13 -12
  38. opensimula-0.8.5/test/test_HVAC_FAN_COIL_water_system.py +401 -0
  39. opensimula-0.8.5/test/test_HVAC_water_system_process.py +250 -0
  40. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Project.py +43 -0
  41. opensimula-0.8.5/test/test_editor_schema.py +202 -0
  42. opensimula-0.8.5/test/test_editor_widget.py +513 -0
  43. opensimula-0.8.5/test/test_underground_repeated_simulation.py +202 -0
  44. opensimula-0.7.1/src/opensimula/visual_3D/Polygon_3D.py +0 -257
  45. opensimula-0.7.1/src/opensimula.egg-info/SOURCES.txt +0 -58
  46. {opensimula-0.7.1 → opensimula-0.8.5}/setup.cfg +0 -0
  47. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Child.py +0 -0
  48. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Iterative_process.py +0 -0
  49. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Message.py +0 -0
  50. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Parameter_container.py +0 -0
  51. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/Variable.py +0 -0
  52. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Building.py +0 -0
  53. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Calculator.py +0 -0
  54. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/DX_unit.py +0 -0
  55. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Day_schedule.py +0 -0
  56. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Fan.py +0 -0
  57. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/File_met.py +0 -0
  58. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Frame.py +0 -0
  59. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Glazing.py +0 -0
  60. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/HVAC_DX_system.py +0 -0
  61. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/HVAC_perfect_system.py +0 -0
  62. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Material.py +0 -0
  63. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Solar_surface.py +0 -0
  64. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Space_type.py +0 -0
  65. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Test_component.py +0 -0
  66. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Week_schedule.py +0 -0
  67. {opensimula-0.7.1/src/opensimula → opensimula-0.8.5/src/OpenSimula}/components/Year_schedule.py +0 -0
  68. {opensimula-0.7.1/src/opensimula.egg-info → opensimula-0.8.5/src/OpenSimula.egg-info}/dependency_links.txt +0 -0
  69. {opensimula-0.7.1/src/opensimula.egg-info → opensimula-0.8.5/src/OpenSimula.egg-info}/top_level.txt +0 -0
  70. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Calculator.py +0 -0
  71. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Construction.py +0 -0
  72. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_File_data.py +0 -0
  73. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_File_met.py +0 -0
  74. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_HVAC_DX_Equipment.py +0 -0
  75. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Iterative_process.py +0 -0
  76. {opensimula-0.7.1 → opensimula-0.8.5}/test/test_Schedules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Juan F. Coronel
3
+ Copyright (c) 2026 Juan F. Coronel
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensimula
3
- Version: 0.7.1
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
@@ -43,5 +42,4 @@ Component-based time simulation environment in Python
43
42
  - nbformat
44
43
  - openpyxl
45
44
  - tqdm
46
- - triangle
47
45
  - vedo
@@ -20,5 +20,4 @@ Component-based time simulation environment in Python
20
20
  - nbformat
21
21
  - openpyxl
22
22
  - tqdm
23
- - triangle
24
23
  - vedo
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opensimula"
3
- version = "0.7.1"
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
 
@@ -38,7 +38,7 @@ class Component(Parameter_container):
38
38
 
39
39
  Args:
40
40
  units (bool, optional): Includes unit in the name of the variable. Defaults to True.
41
- frequency (None or str, optional): frequency of the values: None, "H" Hour, "D" Day, "M" Month, "Y" Year . Defaults to None.
41
+ frequency (None or str, optional): frequency of the values: None, "hourly", "daily", "monthly", "yearly". Defaults to None.
42
42
  value (str, optional): "mean", "sum", "sum_pos", "sum_neg", "max" or "min". Defaults to "mean".
43
43
  interval (None or list of two dates): List with the start and end dates of the period to be included in the dataframe, if the value is None all values are included.
44
44
  pos_neg_columns (list of str, optional): List of variables that will be included in separate columns positive and negative values. Defaults to [].
@@ -62,14 +62,19 @@ class Component(Parameter_container):
62
62
  data[col + "_pos"] = data[col].apply(lambda x: x if x > 0 else 0)
63
63
  data[col + "_neg"] = data[col].apply(lambda x: x if x < 0 else 0)
64
64
  if frequency is not None:
65
+ freq={"hourly": "h", "daily": "D", "monthly": "ME", "yearly": "YE"}
65
66
  if value == "mean":
66
- data = data.resample(frequency, on='date').mean()
67
+ data = data.resample(freq[frequency], on='date').mean()
67
68
  elif value == "sum":
68
- data = data.resample(frequency, on='date').sum()
69
+ data = data.resample(freq[frequency], on='date').sum()
70
+ elif value == "sum_pos":
71
+ data = data.resample(freq[frequency], on='date').apply(lambda x: x.clip(lower=0).sum())
72
+ elif value == "sum_neg":
73
+ data = data.resample(freq[frequency], on='date').apply(lambda x: x.clip(upper=0).sum())
69
74
  elif value == "max":
70
- data = data.resample(frequency, on='date').max()
75
+ data = data.resample(freq[frequency], on='date').max()
71
76
  elif value == "min":
72
- data = data.resample(frequency, on='date').min()
77
+ data = data.resample(freq[frequency], on='date').min()
73
78
  if interval is not None:
74
79
  data = data[(data['date'] > interval[0]) &
75
80
  (data['date'] < interval[1])]
@@ -77,36 +82,43 @@ class Component(Parameter_container):
77
82
 
78
83
  # ____________ Functions that must be overwriten for time simulation _________________
79
84
 
80
- def get_all_referenced_components(self):
85
+ def get_all_referenced_components(self, _seen=None):
81
86
  """Get list of all referenced components, first itself. Look recursively at the referenced components
82
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
+
83
94
  Returns:
84
95
  component_list (component[])
85
96
  """
97
+ if _seen is None:
98
+ _seen = set()
99
+ if id(self) in _seen:
100
+ return []
101
+ _seen.add(id(self))
102
+
86
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
+
87
109
  for key, value in self.parameter_dict().items():
88
110
  if value.type == "Parameter_component":
89
- if value.component is not None:
90
- sublist = value.component.get_all_referenced_components()
91
- for subcomp in sublist:
92
- comp_list.append(subcomp)
111
+ walk(value.component)
93
112
  elif value.type == "Parameter_component_list":
94
113
  for comp in value.component:
95
- if comp is not None:
96
- sublist = comp.get_all_referenced_components()
97
- for subcomp in sublist:
98
- comp_list.append(subcomp)
114
+ walk(comp)
99
115
  if value.type == "Parameter_variable":
100
116
  if value.variable is not None:
101
- sublist = value.variable.parent.get_all_referenced_components()
102
- for subcomp in sublist:
103
- comp_list.append(subcomp)
117
+ walk(value.variable.parent)
104
118
  elif value.type == "Parameter_variable_list":
105
119
  for var in value.variable:
106
120
  if var is not None:
107
- sublist = var.parent.get_all_referenced_components()
108
- for subcomp in sublist:
109
- comp_list.append(subcomp)
121
+ walk(var.parent)
110
122
  comp_list.append(self)
111
123
  return comp_list
112
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:
@@ -96,18 +93,17 @@ class Simulation:
96
93
  """Return the list of messages"""
97
94
  return self._messages_
98
95
 
99
- def plot(self, dates, variables, names=[], axis=[], frequency=None, value="mean"):
96
+ def plot(self, dates, variables, names=[], axis=[], frequency=None, value="mean",interval=None):
100
97
  """_summary_
101
98
  Draw variables graph (using plotly)
102
99
 
103
100
  Args:
104
101
  variables: List of hourly variables
105
102
  axis: list of axis y 1 or 2 to use for each variable, empty all in first axis
106
- frequency (None or str, optional): frequency of the values: None, "H" Hour, "D" Day, "M" Month, "Y" Year . Defaults to None.
107
- value (str, optional): "mean", "sum", "max" or "min". Defaults to "mean".
108
-
103
+ frequency (None or str, optional): frequency of the values: None, "hourly", "daily", "monthly", "yearly". Defaults to None.
104
+ value (str, optional): "mean", "sum", "sum_pos", "sum_neg", "max" or "min". Defaults to "mean".
105
+ interval (None or list of two dates): List with the start and end dates of the period to be included in the dataframe, if the value is None all values are included.
109
106
  """
110
-
111
107
  series = {}
112
108
  series["date"] = dates
113
109
  for i in range(len(variables)):
@@ -121,15 +117,24 @@ class Simulation:
121
117
  series[variables[i].key] = variables[i].values
122
118
  data = pd.DataFrame(series)
123
119
  if frequency is not None:
120
+ freq={"hourly": "h", "daily": "D", "monthly": "ME", "yearly": "YE"}
124
121
  if value == "mean":
125
- data = data.resample(frequency, on='date').mean()
122
+ data = data.resample(freq[frequency], on='date').mean()
126
123
  elif value == "sum":
127
- data = data.resample(frequency, on='date').sum()
124
+ data = data.resample(freq[frequency], on='date').sum()
125
+ elif value == "sum_pos":
126
+ data = data.resample(freq[frequency], on='date').apply(lambda x: x.clip(lower=0).sum())
127
+ elif value == "sum_neg":
128
+ data = data.resample(freq[frequency], on='date').apply(lambda x: x.clip(upper=0).sum())
128
129
  elif value == "max":
129
- data = data.resample(frequency, on='date').max()
130
+ data = data.resample(freq[frequency], on='date').max()
130
131
  elif value == "min":
131
- data = data.resample(frequency, on='date').min()
132
+ data = data.resample(freq[frequency], on='date').min()
132
133
  data["date"] = data.index
134
+ if interval is not None:
135
+ data = data[(data['date'] > interval[0]) &
136
+ (data['date'] < interval[1])]
137
+
133
138
 
134
139
  subfig = make_subplots(specs=[[{"secondary_y": True}]])
135
140
 
@@ -155,63 +160,3 @@ class Simulation:
155
160
  # fig.update_traces(showlegend=True)
156
161
  subfig.show()
157
162
 
158
- def project_editor(self):
159
- editor = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
160
- df = self.project_dataframe(string_format=True)
161
- self._n_clicks_new_project_ = 0
162
- self._n_clicks_del_project_ = 0
163
- column_definition = [
164
- {"field": "name", "checkboxSelection": True, "headerCheckboxSelection": True}]
165
- for i in df.columns:
166
- if i != "name":
167
- column_definition.append({"field": i})
168
-
169
- editor.layout = html.Div([
170
- dbc.Label("Projects editor: "),
171
- html.Br(),
172
- dbc.Button('New project', id='btn-new-project', n_clicks=0),
173
- dbc.Button('Delete selected projects',
174
- id='btn-del-project', n_clicks=0, style={"margin-left": "15px"}),
175
- html.Br(),
176
- html.Br(),
177
- dag.AgGrid(
178
- id="project-table",
179
- rowData=df.to_dict("records"),
180
- columnDefs=column_definition,
181
- columnSize="sizeToFit",
182
- defaultColDef={"filter": True, "editable": True},
183
- style={"height": '250px'},
184
- dashGridOptions={
185
- "rowSelection": "multiple",
186
- "suppressRowClickSelection": True,
187
- "pagination": True,
188
- })
189
- ])
190
-
191
- @callback(
192
- Output('project-table', 'rowData'),
193
- Input('project-table', 'cellValueChanged'),
194
- Input('btn-new-project', 'n_clicks'),
195
- Input('btn-del-project', 'n_clicks'),
196
- State('project-table', 'selectedRows'),
197
- prevent_initial_call=True)
198
- def update_data(changed, n_clicks_new, n_clicks_del, selectedRows):
199
- if (self._n_clicks_new_project_ < n_clicks_new):
200
- self.new_project("new_project_"+str(n_clicks_new))
201
- self._n_clicks_new_project_ = n_clicks_new
202
- elif (self._n_clicks_del_project_ < n_clicks_del):
203
- for row in selectedRows:
204
- self.del_project(self.project(row["name"]))
205
- self._n_clicks_del_project_ = n_clicks_del
206
- else:
207
- if changed is not None:
208
- if changed[0]["colId"] == "name":
209
- self.project(changed[0]['oldValue']).parameter(
210
- "name").value = changed[0]["value"]
211
- else:
212
- self.project(changed[0]['data']["name"]).parameter(
213
- changed[0]["colId"]).value = changed[0]["value"]
214
- df_end = self.project_dataframe(string_format=True)
215
- return df_end.to_dict("records")
216
-
217
- editor.run(jupyter_height=350)
@@ -5,4 +5,5 @@ opensimula is a Python Package for perform time simulations
5
5
  """
6
6
  from opensimula.Simulation import Simulation
7
7
  from opensimula.Iterative_process import Iterative_process
8
- VERSION = "0.7.1"
8
+ from importlib.metadata import version
9
+ VERSION = version("opensimula")